From 33aef265f20ff76b8f96465ed3f05cb41da4d24c Mon Sep 17 00:00:00 2001 From: Luca Cervello Date: Tue, 12 Mar 2024 17:24:56 +0100 Subject: [PATCH] WIP --- lib/next_ls.ex | 21 ++++ lib/next_ls/helpers/ast_helpers.ex | 60 +++++++++++ lib/next_ls/logger.ex | 2 +- lib/next_ls/signature_help.ex | 76 ++++++++++++++ test/next_ls/helpers/ast_helpers_test.exs | 49 +++++++++ test/next_ls/signature_help_test.exs | 121 ++++++++++++++++++++++ test/next_ls_test.exs | 46 ++++---- 7 files changed, 351 insertions(+), 24 deletions(-) create mode 100644 lib/next_ls/signature_help.ex create mode 100644 test/next_ls/signature_help_test.exs diff --git a/lib/next_ls.ex b/lib/next_ls.ex index c51b8e11..6c5fe3d5 100644 --- a/lib/next_ls.ex +++ b/lib/next_ls.ex @@ -24,6 +24,7 @@ defmodule NextLS do alias GenLSP.Requests.TextDocumentFormatting alias GenLSP.Requests.TextDocumentHover alias GenLSP.Requests.TextDocumentReferences + alias GenLSP.Requests.TextDocumentSignatureHelp alias GenLSP.Requests.WorkspaceApplyEdit alias GenLSP.Requests.WorkspaceSymbol alias GenLSP.Structures.ApplyWorkspaceEditParams @@ -41,6 +42,8 @@ defmodule NextLS do alias GenLSP.Structures.Range alias GenLSP.Structures.SaveOptions alias GenLSP.Structures.ServerCapabilities + alias GenLSP.Structures.SignatureHelp + alias GenLSP.Structures.SignatureHelpParams alias GenLSP.Structures.SymbolInformation alias GenLSP.Structures.TextDocumentIdentifier alias GenLSP.Structures.TextDocumentItem @@ -53,6 +56,7 @@ defmodule NextLS do alias NextLS.DiagnosticCache alias NextLS.Progress alias NextLS.Runtime + alias NextLS.SignatureHelp def start_link(args) do {args, opts} = @@ -146,6 +150,9 @@ defmodule NextLS do "from-pipe" ] }, + signature_help_provider: %GenLSP.Structures.SignatureHelpOptions{ + trigger_characters: ["(", ","] + }, hover_provider: true, workspace_symbol_provider: true, document_symbol_provider: true, @@ -699,6 +706,20 @@ defmodule NextLS do {:reply, nil, lsp} end + def handle_request( + %TextDocumentSignatureHelp{params: %SignatureHelpParams{text_document: %{uri: uri}, position: position}}, + lsp + ) 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) + end + end) + + {:reply, List.first(result), lsp} + end + def handle_request(%Shutdown{}, lsp) do {:reply, nil, assign(lsp, exit_code: 0)} end diff --git a/lib/next_ls/helpers/ast_helpers.ex b/lib/next_ls/helpers/ast_helpers.ex index 109cff13..e370715e 100644 --- a/lib/next_ls/helpers/ast_helpers.ex +++ b/lib/next_ls/helpers/ast_helpers.ex @@ -152,4 +152,64 @@ 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 diff --git a/lib/next_ls/logger.ex b/lib/next_ls/logger.ex index bba77e9c..9e1f2818 100644 --- a/lib/next_ls/logger.ex +++ b/lib/next_ls/logger.ex @@ -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) diff --git a/lib/next_ls/signature_help.ex b/lib/next_ls/signature_help.ex new file mode 100644 index 00000000..4c0f2a65 --- /dev/null +++ b/lib/next_ls/signature_help.ex @@ -0,0 +1,76 @@ +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 -> + 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 diff --git a/test/next_ls/helpers/ast_helpers_test.exs b/test/next_ls/helpers/ast_helpers_test.exs index ca329d46..369c0744 100644 --- a/test/next_ls/helpers/ast_helpers_test.exs +++ b/test/next_ls/helpers/ast_helpers_test.exs @@ -74,4 +74,53 @@ 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 diff --git a/test/next_ls/signature_help_test.exs b/test/next_ls/signature_help_test.exs new file mode 100644 index 00000000..37b9573b --- /dev/null +++ b/test/next_ls/signature_help_test.exs @@ -0,0 +1,121 @@ +defmodule NextLS.SignatureHelpTest do + use ExUnit.Case, async: true + + import GenLSP.Test + import NextLS.Support.Utils + + @moduletag :tmp_dir + + describe "function" do + @describetag root_paths: ["my_proj"] + setup %{tmp_dir: tmp_dir} do + File.mkdir_p!(Path.join(tmp_dir, "my_proj/lib")) + File.write!(Path.join(tmp_dir, "my_proj/mix.exs"), mix_exs()) + [cwd: tmp_dir] + end + + setup %{cwd: cwd} do + remote = Path.join(cwd, "my_proj/lib/remote.ex") + + File.write!(remote, """ + defmodule Remote do + def bang!(bang) do + bang + end + end + """) + + imported = Path.join(cwd, "my_proj/lib/imported.ex") + + File.write!(imported, """ + defmodule Imported do + def boom(boom1, _boom2) do + boom1 + end + end + """) + + bar = Path.join(cwd, "my_proj/lib/bar.ex") + + File.write!(bar, """ + defmodule Bar do + def run() do + Remote.bang!() + end + end + """) + + [bar: bar, imported: imported, remote: remote] + end + + setup :with_lsp + + test "get signature help", %{client: client, bar: bar} = 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) + + request(client, %{ + method: "textDocument/signatureHelp", + id: 4, + jsonrpc: "2.0", + params: %{ + position: %{line: 3, character: 16}, + textDocument: %{uri: uri} + } + }) + + assert_result 4, %{ + "activeParameter" => 0, + "activeSignature" => 0, + "signatures" => [ + %{ + "activeParameter" => 0, + "parameters" => [ + %{"label" => "bang"} + ], + "documentation" => "need help", + "label" => "bang!" + } + ] + } + end + + test "get signature help 2", %{client: client, bar: bar} = 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) + + request(client, %{ + method: "textDocument/signatureHelp", + id: 4, + jsonrpc: "2.0", + params: %{ + position: %{line: 8, character: 10}, + textDocument: %{uri: uri} + } + }) + + assert_result 4, %{ + "activeParameter" => 0, + "activeSignature" => 0, + "signatures" => [ + %{ + "activeParameter" => 0, + "parameters" => [ + %{"label" => "bang"} + ], + "documentation" => "need help", + "label" => "bang!" + } + ] + } + end + end +end diff --git a/test/next_ls_test.exs b/test/next_ls_test.exs index ffcb7cf6..bfb994b4 100644 --- a/test/next_ls_test.exs +++ b/test/next_ls_test.exs @@ -70,29 +70,29 @@ defmodule NextLSTest do assert_result 2, nil end - test "returns method not found for unimplemented requests", %{client: client} do - id = System.unique_integer([:positive]) - - assert :ok == notify(client, %{method: "initialized", jsonrpc: "2.0", params: %{}}) - - assert :ok == - request(client, %{ - method: "textDocument/signatureHelp", - id: id, - jsonrpc: "2.0", - params: %{position: %{line: 0, character: 0}, textDocument: %{uri: ""}} - }) - - assert_notification "window/logMessage", %{ - "message" => "[Next LS] Method Not Found: textDocument/signatureHelp", - "type" => 2 - } - - assert_error ^id, %{ - "code" => -32_601, - "message" => "Method Not Found: textDocument/signatureHelp" - } - end + # test "returns method not found for unimplemented requests", %{client: client} do + # id = System.unique_integer([:positive]) + + # assert :ok == notify(client, %{method: "initialized", jsonrpc: "2.0", params: %{}}) + + # assert :ok == + # request(client, %{ + # method: "textDocument/signatureHelp", + # id: id, + # jsonrpc: "2.0", + # params: %{position: %{line: 0, character: 0}, textDocument: %{uri: ""}} + # }) + + # assert_notification "window/logMessage", %{ + # "message" => "[Next LS] Method Not Found: textDocument/signatureHelp", + # "type" => 2 + # } + + # assert_error ^id, %{ + # "code" => -32_601, + # "message" => "Method Not Found: textDocument/signatureHelp" + # } + # end test "can initialize the server" do assert_result 1, %{