Skip to content

Commit 9a4860a

Browse files
committed
WIP
1 parent d5c9c0a commit 9a4860a

File tree

7 files changed

+348
-24
lines changed

7 files changed

+348
-24
lines changed

lib/next_ls.ex

+21
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ defmodule NextLS do
2424
alias GenLSP.Requests.TextDocumentFormatting
2525
alias GenLSP.Requests.TextDocumentHover
2626
alias GenLSP.Requests.TextDocumentReferences
27+
alias GenLSP.Requests.TextDocumentSignatureHelp
2728
alias GenLSP.Requests.WorkspaceApplyEdit
2829
alias GenLSP.Requests.WorkspaceSymbol
2930
alias GenLSP.Structures.ApplyWorkspaceEditParams
@@ -41,6 +42,8 @@ defmodule NextLS do
4142
alias GenLSP.Structures.Range
4243
alias GenLSP.Structures.SaveOptions
4344
alias GenLSP.Structures.ServerCapabilities
45+
alias GenLSP.Structures.SignatureHelp
46+
alias GenLSP.Structures.SignatureHelpParams
4447
alias GenLSP.Structures.SymbolInformation
4548
alias GenLSP.Structures.TextDocumentIdentifier
4649
alias GenLSP.Structures.TextDocumentItem
@@ -53,6 +56,7 @@ defmodule NextLS do
5356
alias NextLS.DiagnosticCache
5457
alias NextLS.Progress
5558
alias NextLS.Runtime
59+
alias NextLS.SignatureHelp
5660

5761
def start_link(args) do
5862
{args, opts} =
@@ -146,6 +150,9 @@ defmodule NextLS do
146150
"from-pipe"
147151
]
148152
},
153+
signature_help_provider: %GenLSP.Structures.SignatureHelpOptions{
154+
trigger_characters: ["(", ","]
155+
},
149156
hover_provider: true,
150157
workspace_symbol_provider: true,
151158
document_symbol_provider: true,
@@ -699,6 +706,20 @@ defmodule NextLS do
699706
{:reply, nil, lsp}
700707
end
701708

709+
def handle_request(
710+
%TextDocumentSignatureHelp{params: %SignatureHelpParams{text_document: %{uri: uri}, position: position}},
711+
lsp
712+
) do
713+
result =
714+
dispatch(lsp.assigns.registry, :databases, fn entries ->
715+
for {pid, _} <- entries do
716+
SignatureHelp.fetch(URI.parse(uri).path, {position.line + 1, position.character + 1}, pid, lsp.assigns.logger)
717+
end
718+
end)
719+
720+
{:reply, List.first(result), lsp}
721+
end
722+
702723
def handle_request(%Shutdown{}, lsp) do
703724
{:reply, nil, assign(lsp, exit_code: 0)}
704725
end

lib/next_ls/helpers/ast_helpers.ex

+60
Original file line numberDiff line numberDiff line change
@@ -152,4 +152,64 @@ defmodule NextLS.ASTHelpers do
152152
end
153153
end)
154154
end
155+
156+
defmodule Functions do
157+
@moduledoc false
158+
159+
alias Sourceror.Zipper, as: Z
160+
161+
def get_function_params(code, identifier, line, _col) do
162+
ast =
163+
NextLS.Parser.parse!(code, columns: true)
164+
165+
identifier = String.to_atom(identifier)
166+
167+
{_ast, args} =
168+
Macro.prewalk(ast, nil, fn
169+
{^identifier, [line: ^line, column: _], args} = ast, _acc -> {ast, args}
170+
other, acc -> {other, acc}
171+
end)
172+
173+
if args do
174+
args
175+
else
176+
[]
177+
end
178+
end
179+
180+
def get_function_name_from_params(code, line, col) do
181+
pos = [line: line + 1, column: col + 1]
182+
183+
ast =
184+
case Spitfire.parse(code) do
185+
{:ok, ast} ->
186+
ast
187+
188+
{:error, ast, _errors} ->
189+
ast
190+
end
191+
192+
{_ast, result} =
193+
ast
194+
|> Z.zip()
195+
|> Z.traverse(nil, fn tree, acc ->
196+
node = Z.node(tree)
197+
range = Sourceror.get_range(node)
198+
199+
if not is_nil(range) and
200+
match?({:., _, [{:__aliases__, _, _aliases}, _identifier]}, node) do
201+
if Sourceror.compare_positions(range.end, pos) == :lt do
202+
{:., _, [{:__aliases__, _, aliases}, identifier]} = node
203+
{tree, {aliases, identifier}}
204+
else
205+
{tree, acc}
206+
end
207+
else
208+
{tree, acc}
209+
end
210+
end)
211+
212+
result
213+
end
214+
end
155215
end

lib/next_ls/logger.ex

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ defmodule NextLS.Logger do
2424

2525
def handle_cast({:log, type, msg}, state) do
2626
apply(GenLSP, type, [state.lsp, String.trim("[Next LS] #{msg}")])
27-
27+
2828
case type do
2929
:log -> Logger.debug(msg)
3030
:warning -> Logger.warning(msg)

lib/next_ls/signature_help.ex

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
defmodule NextLS.SignatureHelp do
2+
@moduledoc false
3+
4+
import NextLS.DB.Query
5+
6+
alias GenLSP.Structures.ParameterInformation
7+
alias GenLSP.Structures.SignatureHelp
8+
alias GenLSP.Structures.SignatureInformation
9+
alias NextLS.ASTHelpers
10+
alias NextLS.DB
11+
12+
def fetch(file, {line, col}, db, _logger) do
13+
code = File.read!(file)
14+
15+
{mod, func} =
16+
ASTHelpers.Functions.get_function_name_from_params(code, line, col)
17+
18+
query =
19+
~Q"""
20+
SELECT
21+
*
22+
FROM
23+
symbols
24+
WHERE
25+
symbols.module = ?
26+
AND symbols.name = ?;
27+
"""
28+
29+
args = [Enum.map_join(mod, ".", &Atom.to_string/1), Atom.to_string(func)]
30+
31+
symbol = DB.query(db, query, args)
32+
33+
result =
34+
case symbol do
35+
nil ->
36+
nil
37+
38+
[] ->
39+
nil
40+
41+
[[_, _mod, file, type, label, line, col | _] | _] = _definition ->
42+
if type in ["def", "defp"] do
43+
code = File.read!(file)
44+
45+
params =
46+
code
47+
|> ASTHelpers.Functions.get_function_params(label, line, col)
48+
|> Enum.map(fn {name, _, _} ->
49+
%ParameterInformation{
50+
label: Atom.to_string(name)
51+
}
52+
end)
53+
54+
%SignatureHelp{
55+
signatures: [
56+
%SignatureInformation{
57+
label: label,
58+
documentation: "need help",
59+
parameters: params
60+
# active_parameter: 0
61+
}
62+
],
63+
# active_signature: 1,
64+
active_parameter: 0
65+
}
66+
else
67+
nil
68+
end
69+
end
70+
71+
result
72+
end
73+
end

test/next_ls/helpers/ast_helpers_test.exs

+49
Original file line numberDiff line numberDiff line change
@@ -74,4 +74,53 @@ defmodule NextLS.ASTHelpersTest do
7474
assert {{5, 5}, {5, 8}} == Aliases.extract_alias_range(code, {start, stop}, :Four)
7575
end
7676
end
77+
78+
describe "extract function params" do
79+
test "simple function params" do
80+
code = """
81+
@doc "foo doc"
82+
def foo(bar, baz) do
83+
:ok
84+
end
85+
"""
86+
87+
assert [{:bar, [line: 2, column: 9], nil}, {:baz, [line: 2, column: 14], nil}] ==
88+
ASTHelpers.Functions.get_function_params(code, "foo", 2, 5)
89+
end
90+
end
91+
92+
describe "extract function name from params" do
93+
test "alias function" do
94+
code = """
95+
defmodule MyModule do
96+
List.starts_with?(
97+
end
98+
"""
99+
100+
assert {[:List], :starts_with?} ==
101+
ASTHelpers.Functions.get_function_name_from_params(code, 2, 21)
102+
end
103+
104+
test "nested alias function" do
105+
code = """
106+
defmodule MyModule do
107+
List.starts_with?(String.trim()
108+
end
109+
"""
110+
111+
assert {[:String], :trim} ==
112+
ASTHelpers.Functions.get_function_name_from_params(code, 2, 37)
113+
end
114+
115+
test "simple function" do
116+
code = """
117+
defmodule MyModule do
118+
put_in(
119+
end
120+
"""
121+
122+
assert :put_in ==
123+
ASTHelpers.Functions.get_function_name_from_params(code, 2, 21)
124+
end
125+
end
77126
end

test/next_ls/signature_help_test.exs

+121
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
defmodule NextLS.SignatureHelpTest do
2+
use ExUnit.Case, async: true
3+
4+
import GenLSP.Test
5+
import NextLS.Support.Utils
6+
7+
@moduletag :tmp_dir
8+
9+
describe "function" do
10+
@describetag root_paths: ["my_proj"]
11+
setup %{tmp_dir: tmp_dir} do
12+
File.mkdir_p!(Path.join(tmp_dir, "my_proj/lib"))
13+
File.write!(Path.join(tmp_dir, "my_proj/mix.exs"), mix_exs())
14+
[cwd: tmp_dir]
15+
end
16+
17+
setup %{cwd: cwd} do
18+
remote = Path.join(cwd, "my_proj/lib/remote.ex")
19+
20+
File.write!(remote, """
21+
defmodule Remote do
22+
def bang!(bang) do
23+
bang
24+
end
25+
end
26+
""")
27+
28+
imported = Path.join(cwd, "my_proj/lib/imported.ex")
29+
30+
File.write!(imported, """
31+
defmodule Imported do
32+
def boom(boom1, _boom2) do
33+
boom1
34+
end
35+
end
36+
""")
37+
38+
bar = Path.join(cwd, "my_proj/lib/bar.ex")
39+
40+
File.write!(bar, """
41+
defmodule Bar do
42+
def run() do
43+
Remote.bang!()
44+
end
45+
end
46+
""")
47+
48+
[bar: bar, imported: imported, remote: remote]
49+
end
50+
51+
setup :with_lsp
52+
53+
test "get signature help", %{client: client, bar: bar} = context do
54+
assert :ok == notify(client, %{method: "initialized", jsonrpc: "2.0", params: %{}})
55+
56+
assert_is_ready(context, "my_proj")
57+
assert_notification "$/progress", %{"value" => %{"kind" => "end", "message" => "Finished indexing!"}}
58+
59+
uri = uri(bar)
60+
61+
request(client, %{
62+
method: "textDocument/signatureHelp",
63+
id: 4,
64+
jsonrpc: "2.0",
65+
params: %{
66+
position: %{line: 3, character: 16},
67+
textDocument: %{uri: uri}
68+
}
69+
})
70+
71+
assert_result 4, %{
72+
"activeParameter" => 0,
73+
"activeSignature" => 0,
74+
"signatures" => [
75+
%{
76+
"activeParameter" => 0,
77+
"parameters" => [
78+
%{"label" => "bang"}
79+
],
80+
"documentation" => "need help",
81+
"label" => "bang!"
82+
}
83+
]
84+
}
85+
end
86+
87+
test "get signature help 2", %{client: client, bar: bar} = context do
88+
assert :ok == notify(client, %{method: "initialized", jsonrpc: "2.0", params: %{}})
89+
90+
assert_is_ready(context, "my_proj")
91+
assert_notification "$/progress", %{"value" => %{"kind" => "end", "message" => "Finished indexing!"}}
92+
93+
uri = uri(bar)
94+
95+
request(client, %{
96+
method: "textDocument/signatureHelp",
97+
id: 4,
98+
jsonrpc: "2.0",
99+
params: %{
100+
position: %{line: 8, character: 10},
101+
textDocument: %{uri: uri}
102+
}
103+
})
104+
105+
assert_result 4, %{
106+
"activeParameter" => 0,
107+
"activeSignature" => 0,
108+
"signatures" => [
109+
%{
110+
"activeParameter" => 0,
111+
"parameters" => [
112+
%{"label" => "bang"}
113+
],
114+
"documentation" => "need help",
115+
"label" => "bang!"
116+
}
117+
]
118+
}
119+
end
120+
end
121+
end

0 commit comments

Comments
 (0)