From c69329267574d89a8b4c36e4eca5d2da5ac069f9 Mon Sep 17 00:00:00 2001 From: Luca Cervello Date: Mon, 18 Mar 2024 17:17:45 +0100 Subject: [PATCH] feat: add signature help --- lib/next_ls.ex | 2 +- lib/next_ls/helpers/ast_helpers.ex | 60 ------------ lib/next_ls/logger.ex | 2 +- lib/next_ls/signature_help.ex | 108 ++++++++-------------- test/next_ls/helpers/ast_helpers_test.exs | 49 ---------- test/next_ls/signature_help_test.exs | 39 ++++---- test/next_ls_test.exs | 46 ++++----- 7 files changed, 87 insertions(+), 219 deletions(-) diff --git a/lib/next_ls.ex b/lib/next_ls.ex index 6c5fe3d5..42772b9e 100644 --- a/lib/next_ls.ex +++ b/lib/next_ls.ex @@ -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) diff --git a/lib/next_ls/helpers/ast_helpers.ex b/lib/next_ls/helpers/ast_helpers.ex index e370715e..109cff13 100644 --- a/lib/next_ls/helpers/ast_helpers.ex +++ b/lib/next_ls/helpers/ast_helpers.ex @@ -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 diff --git a/lib/next_ls/logger.ex b/lib/next_ls/logger.ex index 9e1f2818..bba77e9c 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 index 4c0f2a65..a68e3094 100644 --- a/lib/next_ls/signature_help.ex +++ b/lib/next_ls/signature_help.ex @@ -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 diff --git a/test/next_ls/helpers/ast_helpers_test.exs b/test/next_ls/helpers/ast_helpers_test.exs index 369c0744..ca329d46 100644 --- a/test/next_ls/helpers/ast_helpers_test.exs +++ b/test/next_ls/helpers/ast_helpers_test.exs @@ -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 diff --git a/test/next_ls/signature_help_test.exs b/test/next_ls/signature_help_test.exs index 37b9573b..d0820a19 100644 --- a/test/next_ls/signature_help_test.exs +++ b/test/next_ls/signature_help_test.exs @@ -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 @@ -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 @@ -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)" } ] } diff --git a/test/next_ls_test.exs b/test/next_ls_test.exs index bfb994b4..963fe388 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/typeDefinition", + id: id, + jsonrpc: "2.0", + params: %{position: %{line: 0, character: 0}, textDocument: %{uri: ""}} + }) + + assert_notification "window/logMessage", %{ + "message" => "[Next LS] Method Not Found: textDocument/typeDefinition", + "type" => 2 + } + + assert_error ^id, %{ + "code" => -32_601, + "message" => "Method Not Found: textDocument/typeDefinition" + } + end test "can initialize the server" do assert_result 1, %{