Skip to content

Commit

Permalink
feat: add signature help
Browse files Browse the repository at this point in the history
  • Loading branch information
lucacervello committed Mar 18, 2024
1 parent 33aef26 commit c693292
Show file tree
Hide file tree
Showing 7 changed files with 87 additions and 219 deletions.
2 changes: 1 addition & 1 deletion lib/next_ls.ex
Original file line number Diff line number Diff line change
Expand Up @@ -713,7 +713,7 @@ defmodule NextLS do
result =
dispatch(lsp.assigns.registry, :databases, fn entries ->
for {pid, _} <- entries do
SignatureHelp.fetch(URI.parse(uri).path, {position.line + 1, position.character + 1}, pid, lsp.assigns.logger)
SignatureHelp.fetch(URI.parse(uri).path, {position.line + 1, position.character + 1}, pid)
end
end)

Expand Down
60 changes: 0 additions & 60 deletions lib/next_ls/helpers/ast_helpers.ex
Original file line number Diff line number Diff line change
Expand Up @@ -152,64 +152,4 @@ defmodule NextLS.ASTHelpers do
end
end)
end

defmodule Functions do
@moduledoc false

alias Sourceror.Zipper, as: Z

def get_function_params(code, identifier, line, _col) do
ast =
NextLS.Parser.parse!(code, columns: true)

identifier = String.to_atom(identifier)

{_ast, args} =
Macro.prewalk(ast, nil, fn
{^identifier, [line: ^line, column: _], args} = ast, _acc -> {ast, args}
other, acc -> {other, acc}
end)

if args do
args
else
[]
end
end

def get_function_name_from_params(code, line, col) do
pos = [line: line + 1, column: col + 1]

ast =
case Spitfire.parse(code) do
{:ok, ast} ->
ast

{:error, ast, _errors} ->
ast
end

{_ast, result} =
ast
|> Z.zip()
|> Z.traverse(nil, fn tree, acc ->
node = Z.node(tree)
range = Sourceror.get_range(node)

if not is_nil(range) and
match?({:., _, [{:__aliases__, _, _aliases}, _identifier]}, node) do
if Sourceror.compare_positions(range.end, pos) == :lt do
{:., _, [{:__aliases__, _, aliases}, identifier]} = node
{tree, {aliases, identifier}}
else
{tree, acc}
end
else
{tree, acc}
end
end)

result
end
end
end
2 changes: 1 addition & 1 deletion lib/next_ls/logger.ex
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ defmodule NextLS.Logger do

def handle_cast({:log, type, msg}, state) do
apply(GenLSP, type, [state.lsp, String.trim("[Next LS] #{msg}")])

case type do
:log -> Logger.debug(msg)
:warning -> Logger.warning(msg)
Expand Down
108 changes: 41 additions & 67 deletions lib/next_ls/signature_help.ex
Original file line number Diff line number Diff line change
@@ -1,76 +1,50 @@
defmodule NextLS.SignatureHelp do
@moduledoc false

import NextLS.DB.Query

alias GenLSP.Enumerations.MarkupKind
alias GenLSP.Structures.MarkupContent
alias GenLSP.Structures.ParameterInformation
alias GenLSP.Structures.SignatureHelp
alias GenLSP.Structures.SignatureInformation
alias NextLS.ASTHelpers
alias NextLS.DB

def fetch(file, {line, col}, db, _logger) do
code = File.read!(file)

{mod, func} =
ASTHelpers.Functions.get_function_name_from_params(code, line, col)

query =
~Q"""
SELECT
*
FROM
symbols
WHERE
symbols.module = ?
AND symbols.name = ?;
"""

args = [Enum.map_join(mod, ".", &Atom.to_string/1), Atom.to_string(func)]

symbol = DB.query(db, query, args)

result =
case symbol do
nil ->
alias NextLS.Definition

def fetch(file, {line, col}, db) do
case Definition.fetch(file, {line, col}, db) do
nil ->
nil

[] ->
nil

[[_, _mod, _file, type, label, params, _line, _col | _] | _] = _definition ->
if type in ["def", "defp"] do
term_params =
:erlang.binary_to_term(params)

code_params =
term_params
|> Macro.to_string()
|> String.replace_prefix("[", "(")
|> String.replace_suffix("]", ")")

params_info =
term_params
|> Enum.map(&Macro.to_string/1)
|> Enum.map(fn name ->
%ParameterInformation{
label: name
}
end)

%SignatureHelp{
signatures: [
%SignatureInformation{
label: "#{label}#{code_params}",
parameters: params_info
}
]
}
else
nil

[] ->
nil

[[_, _mod, file, type, label, params, line, col | _] | _] = _definition ->
if type in ["def", "defp"] do
code_params = params |> :erlang.binary_to_term() |> Macro.to_string() |> dbg()

signature_params =
params
|> :erlang.binary_to_term()
|> Enum.map(fn {name, _, _} ->
%ParameterInformation{
label: Atom.to_string(name)
}
end)
|> dbg()

%SignatureHelp{
signatures: [
%SignatureInformation{
label: "#{label}.#{code_params}",
documentation: "need help",
parameters: signature_params
# active_parameter: 0
}
]
# active_signature: 1,
# active_parameter: 0
}
else
nil
end
end

result
end
end
end
end
49 changes: 0 additions & 49 deletions test/next_ls/helpers/ast_helpers_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -74,53 +74,4 @@ defmodule NextLS.ASTHelpersTest do
assert {{5, 5}, {5, 8}} == Aliases.extract_alias_range(code, {start, stop}, :Four)
end
end

describe "extract function params" do
test "simple function params" do
code = """
@doc "foo doc"
def foo(bar, baz) do
:ok
end
"""

assert [{:bar, [line: 2, column: 9], nil}, {:baz, [line: 2, column: 14], nil}] ==
ASTHelpers.Functions.get_function_params(code, "foo", 2, 5)
end
end

describe "extract function name from params" do
test "alias function" do
code = """
defmodule MyModule do
List.starts_with?(
end
"""

assert {[:List], :starts_with?} ==
ASTHelpers.Functions.get_function_name_from_params(code, 2, 21)
end

test "nested alias function" do
code = """
defmodule MyModule do
List.starts_with?(String.trim()
end
"""

assert {[:String], :trim} ==
ASTHelpers.Functions.get_function_name_from_params(code, 2, 37)
end

test "simple function" do
code = """
defmodule MyModule do
put_in(
end
"""

assert :put_in ==
ASTHelpers.Functions.get_function_name_from_params(code, 2, 21)
end
end
end
39 changes: 21 additions & 18 deletions test/next_ls/signature_help_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ defmodule NextLS.SignatureHelpTest do

File.write!(imported, """
defmodule Imported do
def boom(boom1, _boom2) do
def boom([] = boom1, _boom2) do
boom1
end
end
Expand All @@ -40,12 +40,22 @@ defmodule NextLS.SignatureHelpTest do
File.write!(bar, """
defmodule Bar do
def run() do
Remote.bang!()
Remote.bang!("!")
end
end
""")

[bar: bar, imported: imported, remote: remote]
baz = Path.join(cwd, "my_proj/lib/baz.ex")

File.write!(baz, """
defmodule Baz do
def run() do
Imported.boom([1, 2], 1)
end
end
""")

[bar: bar, imported: imported, remote: remote, baz: baz]
end

setup :with_lsp
Expand All @@ -63,56 +73,49 @@ defmodule NextLS.SignatureHelpTest do
id: 4,
jsonrpc: "2.0",
params: %{
position: %{line: 3, character: 16},
position: %{line: 2, character: 13},
textDocument: %{uri: uri}
}
})

assert_result 4, %{
"activeParameter" => 0,
"activeSignature" => 0,
"signatures" => [
%{
"activeParameter" => 0,
"parameters" => [
%{"label" => "bang"}
],
"documentation" => "need help",
"label" => "bang!"
"label" => "bang!(bang)"
}
]
}
end

test "get signature help 2", %{client: client, bar: bar} = context do
test "get signature help 2", %{client: client, baz: baz} = context do
assert :ok == notify(client, %{method: "initialized", jsonrpc: "2.0", params: %{}})

assert_is_ready(context, "my_proj")
assert_notification "$/progress", %{"value" => %{"kind" => "end", "message" => "Finished indexing!"}}

uri = uri(bar)
uri = uri(baz)

request(client, %{
method: "textDocument/signatureHelp",
id: 4,
jsonrpc: "2.0",
params: %{
position: %{line: 8, character: 10},
position: %{line: 2, character: 13},
textDocument: %{uri: uri}
}
})

assert_result 4, %{
"activeParameter" => 0,
"activeSignature" => 0,
"signatures" => [
%{
"activeParameter" => 0,
"parameters" => [
%{"label" => "bang"}
%{"label" => "[] = boom1"},
%{"label" => "_boom2"}
],
"documentation" => "need help",
"label" => "bang!"
"label" => "boom([] = boom1, _boom2)"
}
]
}
Expand Down
Loading

0 comments on commit c693292

Please sign in to comment.