From 9907063f37eefe4de5436db051897066c3cfda87 Mon Sep 17 00:00:00 2001 From: Luca Cervello Date: Tue, 12 Mar 2024 17:24:56 +0100 Subject: [PATCH 1/6] feat: add signature help --- lib/next_ls.ex | 35 ++++ lib/next_ls/signature_help.ex | 94 ++++++++++ test/next_ls/signature_help_test.exs | 254 +++++++++++++++++++++++++++ test/next_ls_test.exs | 6 +- 4 files changed, 386 insertions(+), 3 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 4a5cc898..512512c8 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 require NextLS.Runtime @@ -157,6 +161,9 @@ defmodule NextLS do "alias-refactor" ] }, + signature_help_provider: %GenLSP.Structures.SignatureHelpOptions{ + trigger_characters: ["(", ","] + }, hover_provider: true, workspace_symbol_provider: true, document_symbol_provider: true, @@ -817,6 +824,34 @@ defmodule NextLS do {:reply, nil, lsp} end + def handle_request( + %TextDocumentSignatureHelp{params: %SignatureHelpParams{text_document: %{uri: uri}, position: position}}, + lsp + ) do + signature_help = + case SignatureHelp.fetch_mod_and_name(uri, {position.line + 1, position.character + 1}) do + {:ok, {mod, name}} -> + docs = + dispatch(lsp.assigns.registry, :runtimes, fn entries -> + [result] = + for {runtime, %{uri: wuri}} <- entries, String.starts_with?(uri, wuri) do + Runtime.call(runtime, {Code, :fetch_docs, [mod]}) + end + + result + end) + + docs + |> SignatureHelp.format(name) + |> List.first() + + {:error, :not_found} -> + nil + end + + {:reply, signature_help, lsp} + end + def handle_request(%Shutdown{}, lsp) do {:reply, nil, assign(lsp, exit_code: 0)} end diff --git a/lib/next_ls/signature_help.ex b/lib/next_ls/signature_help.ex new file mode 100644 index 00000000..85fb310d --- /dev/null +++ b/lib/next_ls/signature_help.ex @@ -0,0 +1,94 @@ +defmodule NextLS.SignatureHelp do + @moduledoc false + + alias GenLSP.Enumerations.MarkupKind + alias GenLSP.Structures.MarkupContent + alias GenLSP.Structures.ParameterInformation + alias GenLSP.Structures.SignatureHelp + alias GenLSP.Structures.SignatureInformation + alias Sourceror.Zipper + + def fetch_mod_and_name(uri, position) do + with {:ok, text} <- File.read(URI.parse(uri).path), + ast = + text + |> Spitfire.parse() + |> then(fn + {:ok, ast} -> ast + {:error, ast, _} -> ast + end), + {:ok, result} <- find_node(ast, position) do + case result do + {{:., _, [{:__aliases__, _, modules}, name]}, _, _} -> {:ok, {Module.concat(modules), name}} + end + end + end + + def format({:ok, {:docs_v1, _, :elixir, _, _, _, docs}}, func_name) do + docs + |> Enum.filter(fn + {{_, name, _arity}, _, _, _, _} -> name == func_name + end) + |> Enum.map(fn + {{_, _name, _arity}, _, [signature], _, _} -> + params_info = + signature + |> Spitfire.parse!() + |> then(fn {_, _, args} -> + Enum.map(args, fn {name, _, _} -> name end) + end) + |> Enum.map(fn name -> + %ParameterInformation{ + label: Atom.to_string(name) + } + end) + + %SignatureHelp{ + signatures: [ + %SignatureInformation{ + label: signature, + parameters: params_info, + documentation: %MarkupContent{ + kind: MarkupKind.markdown(), + value: "" + } + } + ] + } + + # {{_, _name, _arity}, _, [], _, _} -> + # [] + + _otherwise -> + [] + end) + end + + def format({:ok, {:error, :module_not_found}}, _func_name) do + [] + end + + defp find_node(ast, {line, column}) do + position = [line: line, column: column] + + result = + ast + |> Zipper.zip() + |> Zipper.find(fn + {{:., _, _}, _metadata, _} = node -> + range = Sourceror.get_range(node) + + Sourceror.compare_positions(range.start, position) == :lt && + Sourceror.compare_positions(range.end, position) == :gt + + _ -> + false + end) + + if result do + {:ok, Zipper.node(result)} + else + {:error, :not_found} + 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..5e9ad385 --- /dev/null +++ b/test/next_ls/signature_help_test.exs @@ -0,0 +1,254 @@ +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.mkdir_p!(Path.join(tmp_dir, "my_proj/lib/remote")) + 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 + + def bangs!(bang1, _bang2) do + bang1 + end + end + """) + + nested_alias = Path.join(cwd, "my_proj/lib/remote/nested_alias.ex") + + File.write!(nested_alias, """ + defmodule Remote.NestedAlias 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 + alias Remote.NestedAlias + + def run() do + Remote.bang!("bang") + + Remote.bangs!("bang1", "bang2") + + Remote.bangs!( + "bang1", + "bang2" + ) + + NestedAlias.bang!("bang") + end + end + """) + + baz = Path.join(cwd, "my_proj/lib/baz.ex") + + File.write!(baz, """ + defmodule Baz do + import Imported + + def run() do + boom([1, 2], 1) + + get_in(%{boom: %{bar: 1}}, [:boom, :bar]) + end + end + """) + + [bar: bar, imported: imported, remote: remote, baz: baz, nested_alias: nested_alias] + end + + setup :with_lsp + + setup context do + assert :ok == notify(context.client, %{method: "initialized", jsonrpc: "2.0", params: %{}}) + assert_is_ready(context, "my_proj") + assert_compiled(context, "my_proj") + assert_notification "$/progress", %{"value" => %{"kind" => "end", "message" => "Finished indexing!"}} + end + + test "get signature help", %{client: client, bar: bar} do + uri = uri(bar) + + request(client, %{ + method: "textDocument/signatureHelp", + id: 4, + jsonrpc: "2.0", + params: %{ + position: %{line: 4, character: 19}, + textDocument: %{uri: uri} + } + }) + + assert_result 4, %{ + "signatures" => [ + %{ + "parameters" => [ + %{"label" => "bang"} + ], + "label" => "bang!(bang)" + } + ] + } + end + + test "get signature help with multiple params", %{client: client, bar: bar} do + uri = uri(bar) + + request(client, %{ + method: "textDocument/signatureHelp", + id: 4, + jsonrpc: "2.0", + params: %{ + position: %{line: 6, character: 13}, + textDocument: %{uri: uri} + } + }) + + assert_result 4, %{ + "signatures" => [ + %{ + "parameters" => [ + %{"label" => "bang1"}, + %{"label" => "bang2"} + ], + "label" => "bangs!(bang1, bang2)" + } + ] + } + end + + test "get signature help with parameters on multiple lines", %{client: client, bar: bar} do + uri = uri(bar) + + request(client, %{ + method: "textDocument/signatureHelp", + id: 4, + jsonrpc: "2.0", + params: %{ + position: %{line: 9, character: 13}, + textDocument: %{uri: uri} + } + }) + + assert_result 4, %{ + "signatures" => [ + %{ + "parameters" => [ + %{"label" => "bang1"}, + %{"label" => "bang2"} + ], + "label" => "bangs!(bang1, bang2)" + } + ] + } + end + + # test "get signature help with aliased module", %{client: client, bar: bar} do + # uri = uri(bar) + + # request(client, %{ + # method: "textDocument/signatureHelp", + # id: 4, + # jsonrpc: "2.0", + # params: %{ + # position: %{line: 12, character: 13}, + # textDocument: %{uri: uri} + # } + # }) + + # assert_result 4, %{ + # "signatures" => [ + # %{ + # "parameters" => [ + # %{"label" => "bang"} + # ], + # "label" => "bang!(bang)" + # } + # ] + # } + # end + + # test "get signature from imported functions", %{client: client, baz: baz} do + # uri = uri(baz) + + # request(client, %{ + # method: "textDocument/signatureHelp", + # id: 4, + # jsonrpc: "2.0", + # params: %{ + # position: %{line: 4, character: 13}, + # textDocument: %{uri: uri} + # } + # }) + + # assert_result 4, %{ + # "signatures" => [ + # %{ + # "parameters" => [ + # %{"label" => "boom1"}, + # %{"label" => "boom2"} + # ], + # "label" => "boom(boom1, boom2)" + # } + # ] + # } + # end + + # test "get signature for kernel functions", %{client: client, baz: baz} do + # uri = uri(baz) + + # request(client, %{ + # method: "textDocument/signatureHelp", + # id: 4, + # jsonrpc: "2.0", + # params: %{ + # position: %{line: 9, character: 13}, + # textDocument: %{uri: uri} + # } + # }) + + # assert_result 4, %{ + # "signatures" => [ + # %{ + # "parameters" => [ + # %{"label" => "boom1"}, + # %{"label" => "boom2"} + # ], + # "label" => "get_in(boom1, boom2)" + # } + # ] + # } + # end + end +end diff --git a/test/next_ls_test.exs b/test/next_ls_test.exs index 23769f07..8a168df3 100644 --- a/test/next_ls_test.exs +++ b/test/next_ls_test.exs @@ -77,20 +77,20 @@ defmodule NextLSTest do assert :ok == request(client, %{ - method: "textDocument/signatureHelp", + 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/signatureHelp", + "message" => "[Next LS] Method Not Found: textDocument/typeDefinition", "type" => 2 } assert_error ^id, %{ "code" => -32_601, - "message" => "Method Not Found: textDocument/signatureHelp" + "message" => "Method Not Found: textDocument/typeDefinition" } end From 114744d29e17ca1997dbca70fac964181bedb1ca Mon Sep 17 00:00:00 2001 From: Luca Cervello Date: Tue, 2 Apr 2024 10:48:45 +0200 Subject: [PATCH 2/6] fix: after review --- lib/next_ls.ex | 4 +- lib/next_ls/autocomplete.ex | 2 +- lib/next_ls/helpers/ast_helpers.ex | 28 ++++++ .../{hover_helpers.ex => docs_helpers.ex} | 2 +- lib/next_ls/signature_help.ex | 86 ++++++------------- ...helpers_test.exs => docs_helpers_test.exs} | 14 +-- test/next_ls/signature_help_test.exs | 7 +- 7 files changed, 73 insertions(+), 70 deletions(-) rename lib/next_ls/helpers/{hover_helpers.ex => docs_helpers.ex} (98%) rename test/next_ls/helpers/{hover_helpers_test.exs => docs_helpers_test.exs} (95%) diff --git a/lib/next_ls.ex b/lib/next_ls.ex index 512512c8..2c7ff9b1 100644 --- a/lib/next_ls.ex +++ b/lib/next_ls.ex @@ -414,7 +414,7 @@ defmodule NextLS do """ ## #{reference.module} - #{NextLS.HoverHelpers.to_markdown(content_type, mod_doc)} + #{NextLS.DocsHelpers.to_markdown(content_type, mod_doc)} """ "function" -> @@ -428,7 +428,7 @@ defmodule NextLS do """ ## #{Macro.to_string(mod)}.#{reference.identifier}/#{reference.arity} - #{NextLS.HoverHelpers.to_markdown(content_type, fdoc)} + #{NextLS.DocsHelpers.to_markdown(content_type, fdoc)} """ _ -> diff --git a/lib/next_ls/autocomplete.ex b/lib/next_ls/autocomplete.ex index 742ded29..35a58245 100644 --- a/lib/next_ls/autocomplete.ex +++ b/lib/next_ls/autocomplete.ex @@ -703,7 +703,7 @@ defmodule NextLS.Autocomplete do """ ## #{Macro.to_string(mod)}.#{name}/#{arity} - #{NextLS.HoverHelpers.to_markdown(content_type, fdoc)} + #{NextLS.DocsHelpers.to_markdown(content_type, fdoc)} """ _ -> diff --git a/lib/next_ls/helpers/ast_helpers.ex b/lib/next_ls/helpers/ast_helpers.ex index 55fca7f6..81766de5 100644 --- a/lib/next_ls/helpers/ast_helpers.ex +++ b/lib/next_ls/helpers/ast_helpers.ex @@ -193,4 +193,32 @@ defmodule NextLS.ASTHelpers do zipper -> {:ok, zipper} end end + + defmodule Function do + @moduledoc false + + def find_aliased_function_call_within(ast, {line, column}) do + position = [line: line, column: column] + + result = + ast + |> Zipper.zip() + |> Zipper.find(fn + {{:., _, _}, _metadata, _} = node -> + range = Sourceror.get_range(node) + + Sourceror.compare_positions(range.start, position) == :lt && + Sourceror.compare_positions(range.end, position) == :gt + + _ -> + false + end) + + if result do + {:ok, Zipper.node(result)} + else + {:error, :not_found} + end + end + end end diff --git a/lib/next_ls/helpers/hover_helpers.ex b/lib/next_ls/helpers/docs_helpers.ex similarity index 98% rename from lib/next_ls/helpers/hover_helpers.ex rename to lib/next_ls/helpers/docs_helpers.ex index efe4d4e8..f7637a01 100644 --- a/lib/next_ls/helpers/hover_helpers.ex +++ b/lib/next_ls/helpers/docs_helpers.ex @@ -1,4 +1,4 @@ -defmodule NextLS.HoverHelpers do +defmodule NextLS.DocsHelpers do @moduledoc false @spec to_markdown(String.t(), String.t() | list()) :: String.t() diff --git a/lib/next_ls/signature_help.ex b/lib/next_ls/signature_help.ex index 85fb310d..17ffed89 100644 --- a/lib/next_ls/signature_help.ex +++ b/lib/next_ls/signature_help.ex @@ -6,89 +6,59 @@ defmodule NextLS.SignatureHelp do alias GenLSP.Structures.ParameterInformation alias GenLSP.Structures.SignatureHelp alias GenLSP.Structures.SignatureInformation - alias Sourceror.Zipper + alias NextLS.ASTHelpers def fetch_mod_and_name(uri, position) do with {:ok, text} <- File.read(URI.parse(uri).path), ast = text - |> Spitfire.parse() + |> Spitfire.parse(literal_encoder: &{:ok, {:__literal__, &2, [&1]}}) |> then(fn {:ok, ast} -> ast {:error, ast, _} -> ast end), - {:ok, result} <- find_node(ast, position) do + {:ok, result} <- ASTHelpers.Function.find_aliased_function_call_within(ast, position) do case result do {{:., _, [{:__aliases__, _, modules}, name]}, _, _} -> {:ok, {Module.concat(modules), name}} end end end - def format({:ok, {:docs_v1, _, :elixir, _, _, _, docs}}, func_name) do - docs - |> Enum.filter(fn - {{_, name, _arity}, _, _, _, _} -> name == func_name - end) - |> Enum.map(fn - {{_, _name, _arity}, _, [signature], _, _} -> - params_info = - signature - |> Spitfire.parse!() - |> then(fn {_, _, args} -> - Enum.map(args, fn {name, _, _} -> name end) - end) - |> Enum.map(fn name -> + def format({:ok, {:docs_v1, _, _lang, content_type, _, _, docs}}, func_name) do + for {{_, name, _arity}, _, [signature], fdoc, _} <- docs, name == func_name do + params_info = + signature + |> Spitfire.parse!() + |> then(fn {_, _, args} -> + Enum.map(args, fn {name, _, _} -> %ParameterInformation{ label: Atom.to_string(name) } end) + end) - %SignatureHelp{ - signatures: [ - %SignatureInformation{ - label: signature, - parameters: params_info, - documentation: %MarkupContent{ - kind: MarkupKind.markdown(), - value: "" - } - } - ] - } - - # {{_, _name, _arity}, _, [], _, _} -> - # [] - - _otherwise -> - [] - end) + %SignatureHelp{ + signatures: [ + %SignatureInformation{ + label: signature, + parameters: params_info, + documentation: maybe_doc(content_type, fdoc) + } + ] + } + end end def format({:ok, {:error, :module_not_found}}, _func_name) do [] end - defp find_node(ast, {line, column}) do - position = [line: line, column: column] - - result = - ast - |> Zipper.zip() - |> Zipper.find(fn - {{:., _, _}, _metadata, _} = node -> - range = Sourceror.get_range(node) - - Sourceror.compare_positions(range.start, position) == :lt && - Sourceror.compare_positions(range.end, position) == :gt - - _ -> - false - end) - - if result do - {:ok, Zipper.node(result)} - else - {:error, :not_found} - end + defp maybe_doc(content_type, %{"en" => fdoc}) do + %MarkupContent{ + kind: MarkupKind.markdown(), + value: NextLS.DocsHelpers.to_markdown(content_type, fdoc) + } end + + defp maybe_doc(_content_type, _fdoc), do: nil end diff --git a/test/next_ls/helpers/hover_helpers_test.exs b/test/next_ls/helpers/docs_helpers_test.exs similarity index 95% rename from test/next_ls/helpers/hover_helpers_test.exs rename to test/next_ls/helpers/docs_helpers_test.exs index 012502f6..d2e45a77 100644 --- a/test/next_ls/helpers/hover_helpers_test.exs +++ b/test/next_ls/helpers/docs_helpers_test.exs @@ -1,7 +1,7 @@ -defmodule NextLS.HoverHelpersTest do +defmodule NextLS.DocsHelpersTest do use ExUnit.Case, async: true - alias NextLS.HoverHelpers + alias NextLS.DocsHelpers describe "converts erlang html format to markdown" do test "some divs and p and code" do @@ -35,7 +35,7 @@ defmodule NextLS.HoverHelpersTest do ]} ] - actual = HoverHelpers.to_markdown("application/erlang+html", html) + actual = DocsHelpers.to_markdown("application/erlang+html", html) assert actual == String.trim(""" @@ -60,7 +60,7 @@ defmodule NextLS.HoverHelpersTest do ]} ] - actual = HoverHelpers.to_markdown("application/erlang+html", html) + actual = DocsHelpers.to_markdown("application/erlang+html", html) assert actual == String.trim(""" @@ -103,7 +103,7 @@ defmodule NextLS.HoverHelpersTest do {:p, [], ["Allowed in guard tests."]} ] - actual = HoverHelpers.to_markdown("application/erlang+html", html) + actual = DocsHelpers.to_markdown("application/erlang+html", html) assert actual == String.trim(""" @@ -191,7 +191,7 @@ defmodule NextLS.HoverHelpersTest do ]} ] - actual = HoverHelpers.to_markdown("application/erlang+html", html) + actual = DocsHelpers.to_markdown("application/erlang+html", html) assert String.trim(actual) == String.trim(""" @@ -231,7 +231,7 @@ defmodule NextLS.HoverHelpersTest do {:p, [], ["Returns ", {:code, [], ["error"]}, " if no value is associated with ", {:code, [], ["Flag"]}, "."]} ] - actual = HoverHelpers.to_markdown("application/erlang+html", html) + actual = DocsHelpers.to_markdown("application/erlang+html", html) assert String.trim(actual) == String.trim(""" diff --git a/test/next_ls/signature_help_test.exs b/test/next_ls/signature_help_test.exs index 5e9ad385..f306c540 100644 --- a/test/next_ls/signature_help_test.exs +++ b/test/next_ls/signature_help_test.exs @@ -20,6 +20,7 @@ defmodule NextLS.SignatureHelpTest do File.write!(remote, """ defmodule Remote do + @doc "doc example" def bang!(bang) do bang end @@ -116,7 +117,11 @@ defmodule NextLS.SignatureHelpTest do "parameters" => [ %{"label" => "bang"} ], - "label" => "bang!(bang)" + "label" => "bang!(bang)", + "documentation" => %{ + "kind" => "markdown", + "value" => "doc example" + } } ] } From 5c270e01e76395ddaa789f6751c35c1c7ad9488b Mon Sep 17 00:00:00 2001 From: Luca Cervello Date: Tue, 2 Apr 2024 15:19:27 +0200 Subject: [PATCH 3/6] fix: rename ast helpers function --- lib/next_ls/helpers/ast_helpers.ex | 2 +- lib/next_ls/signature_help.ex | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/next_ls/helpers/ast_helpers.ex b/lib/next_ls/helpers/ast_helpers.ex index 81766de5..0259e0c6 100644 --- a/lib/next_ls/helpers/ast_helpers.ex +++ b/lib/next_ls/helpers/ast_helpers.ex @@ -197,7 +197,7 @@ defmodule NextLS.ASTHelpers do defmodule Function do @moduledoc false - def find_aliased_function_call_within(ast, {line, column}) do + def find_remote_function_call_within(ast, {line, column}) do position = [line: line, column: column] result = diff --git a/lib/next_ls/signature_help.ex b/lib/next_ls/signature_help.ex index 17ffed89..88bd13d6 100644 --- a/lib/next_ls/signature_help.ex +++ b/lib/next_ls/signature_help.ex @@ -17,7 +17,7 @@ defmodule NextLS.SignatureHelp do {:ok, ast} -> ast {:error, ast, _} -> ast end), - {:ok, result} <- ASTHelpers.Function.find_aliased_function_call_within(ast, position) do + {:ok, result} <- ASTHelpers.Function.find_remote_function_call_within(ast, position) do case result do {{:., _, [{:__aliases__, _, modules}, name]}, _, _} -> {:ok, {Module.concat(modules), name}} end From 042435d875863b986a481233173288c4f64692e2 Mon Sep 17 00:00:00 2001 From: Luca Cervello Date: Tue, 9 Apr 2024 09:31:49 +0200 Subject: [PATCH 4/6] fix: use lsp.assings.documents --- lib/next_ls.ex | 4 +++- lib/next_ls/signature_help.ex | 20 ++++++++++---------- test/next_ls/signature_help_test.exs | 9 +++++++++ test/support/utils.ex | 17 +++++++++++++++++ 4 files changed, 39 insertions(+), 11 deletions(-) diff --git a/lib/next_ls.ex b/lib/next_ls.ex index 2c7ff9b1..e6e5d19b 100644 --- a/lib/next_ls.ex +++ b/lib/next_ls.ex @@ -828,8 +828,10 @@ defmodule NextLS do %TextDocumentSignatureHelp{params: %SignatureHelpParams{text_document: %{uri: uri}, position: position}}, lsp ) do + text = Enum.join(lsp.assigns.documents[uri], "\n") + signature_help = - case SignatureHelp.fetch_mod_and_name(uri, {position.line + 1, position.character + 1}) do + case SignatureHelp.fetch_mod_and_name(text, {position.line + 1, position.character + 1}) do {:ok, {mod, name}} -> docs = dispatch(lsp.assigns.registry, :runtimes, fn entries -> diff --git a/lib/next_ls/signature_help.ex b/lib/next_ls/signature_help.ex index 88bd13d6..e3c326dc 100644 --- a/lib/next_ls/signature_help.ex +++ b/lib/next_ls/signature_help.ex @@ -8,16 +8,16 @@ defmodule NextLS.SignatureHelp do alias GenLSP.Structures.SignatureInformation alias NextLS.ASTHelpers - def fetch_mod_and_name(uri, position) do - with {:ok, text} <- File.read(URI.parse(uri).path), - ast = - text - |> Spitfire.parse(literal_encoder: &{:ok, {:__literal__, &2, [&1]}}) - |> then(fn - {:ok, ast} -> ast - {:error, ast, _} -> ast - end), - {:ok, result} <- ASTHelpers.Function.find_remote_function_call_within(ast, position) do + def fetch_mod_and_name(text, position) do + ast = + text + |> Spitfire.parse(literal_encoder: &{:ok, {:__literal__, &2, [&1]}}) + |> then(fn + {:ok, ast} -> ast + {:error, ast, _} -> ast + end) + + with {:ok, result} <- ASTHelpers.Function.find_remote_function_call_within(ast, position) do case result do {{:., _, [{:__aliases__, _, modules}, name]}, _, _} -> {:ok, {Module.concat(modules), name}} end diff --git a/test/next_ls/signature_help_test.exs b/test/next_ls/signature_help_test.exs index f306c540..d9564e3d 100644 --- a/test/next_ls/signature_help_test.exs +++ b/test/next_ls/signature_help_test.exs @@ -101,6 +101,9 @@ defmodule NextLS.SignatureHelpTest do test "get signature help", %{client: client, bar: bar} do uri = uri(bar) + did_open(client, bar, File.read!(bar)) + did_change(client, uri) + request(client, %{ method: "textDocument/signatureHelp", id: 4, @@ -130,6 +133,9 @@ defmodule NextLS.SignatureHelpTest do test "get signature help with multiple params", %{client: client, bar: bar} do uri = uri(bar) + did_open(client, bar, File.read!(bar)) + did_change(client, uri) + request(client, %{ method: "textDocument/signatureHelp", id: 4, @@ -156,6 +162,9 @@ defmodule NextLS.SignatureHelpTest do test "get signature help with parameters on multiple lines", %{client: client, bar: bar} do uri = uri(bar) + did_open(client, bar, File.read!(bar)) + did_change(client, uri) + request(client, %{ method: "textDocument/signatureHelp", id: 4, diff --git a/test/support/utils.ex b/test/support/utils.ex index fe063d22..fb2a759c 100644 --- a/test/support/utils.ex +++ b/test/support/utils.ex @@ -172,4 +172,21 @@ defmodule NextLS.Support.Utils do }) end end + + defmacro did_change(client, uri) do + quote do + assert :ok == notify(unquote(client), %{ + method: "workspace/didChangeWatchedFiles", + jsonrpc: "2.0", + params: %{ + changes: [ + %{ + type: GenLSP.Enumerations.FileChangeType.changed(), + uri: unquote(uri) + } + ] + } + }) + end + end end From 8bb60aae3b8225288718b00b20aa9b478b8f0f72 Mon Sep 17 00:00:00 2001 From: Luca Cervello Date: Tue, 9 Apr 2024 12:56:41 +0200 Subject: [PATCH 5/6] feat: add active parameter --- lib/next_ls.ex | 6 +- lib/next_ls/helpers/ast_helpers.ex | 28 ++- lib/next_ls/signature_help.ex | 41 +++- test/next_ls/signature_help_test.exs | 315 ++++++++++++++++----------- 4 files changed, 248 insertions(+), 142 deletions(-) diff --git a/lib/next_ls.ex b/lib/next_ls.ex index e6e5d19b..2d9a5247 100644 --- a/lib/next_ls.ex +++ b/lib/next_ls.ex @@ -831,8 +831,8 @@ defmodule NextLS do text = Enum.join(lsp.assigns.documents[uri], "\n") signature_help = - case SignatureHelp.fetch_mod_and_name(text, {position.line + 1, position.character + 1}) do - {:ok, {mod, name}} -> + case SignatureHelp.fetch(text, {position.line + 1, position.character + 1}) do + {:ok, {mod, name, param_index}} -> docs = dispatch(lsp.assigns.registry, :runtimes, fn entries -> [result] = @@ -844,7 +844,7 @@ defmodule NextLS do end) docs - |> SignatureHelp.format(name) + |> SignatureHelp.format(name, param_index) |> List.first() {:error, :not_found} -> diff --git a/lib/next_ls/helpers/ast_helpers.ex b/lib/next_ls/helpers/ast_helpers.ex index 0259e0c6..11fb5ad3 100644 --- a/lib/next_ls/helpers/ast_helpers.ex +++ b/lib/next_ls/helpers/ast_helpers.ex @@ -204,11 +204,11 @@ defmodule NextLS.ASTHelpers do ast |> Zipper.zip() |> Zipper.find(fn - {{:., _, _}, _metadata, _} = node -> - range = Sourceror.get_range(node) + {:|>, _, [_, {{:., _, _}, _metadata, _} = func_node]} -> + inside?(func_node, position) - Sourceror.compare_positions(range.start, position) == :lt && - Sourceror.compare_positions(range.end, position) == :gt + {{:., _, _}, _metadata, _} = node -> + inside?(node, position) _ -> false @@ -220,5 +220,25 @@ defmodule NextLS.ASTHelpers do {:error, :not_found} end end + + def find_params_index(ast, {line, column}) do + ast + |> Sourceror.get_args() + |> Enum.map(&Sourceror.get_meta/1) + |> Enum.find_index(fn meta -> + if meta[:closing] do + line <= meta[:closing][:line] and line >= meta[:line] + else + meta[:line] == line and column <= meta[:column] + end + end) + end + + defp inside?(node, position) do + range = Sourceror.get_range(node) + + Sourceror.compare_positions(range.start, position) == :lt && + Sourceror.compare_positions(range.end, position) == :gt + end end end diff --git a/lib/next_ls/signature_help.ex b/lib/next_ls/signature_help.ex index e3c326dc..2f21b85b 100644 --- a/lib/next_ls/signature_help.ex +++ b/lib/next_ls/signature_help.ex @@ -8,7 +8,7 @@ defmodule NextLS.SignatureHelp do alias GenLSP.Structures.SignatureInformation alias NextLS.ASTHelpers - def fetch_mod_and_name(text, position) do + def fetch(text, position) do ast = text |> Spitfire.parse(literal_encoder: &{:ok, {:__literal__, &2, [&1]}}) @@ -19,22 +19,36 @@ defmodule NextLS.SignatureHelp do with {:ok, result} <- ASTHelpers.Function.find_remote_function_call_within(ast, position) do case result do - {{:., _, [{:__aliases__, _, modules}, name]}, _, _} -> {:ok, {Module.concat(modules), name}} + {:|>, _, [_, {{:., _, [{:__aliases__, _, modules}, name]}, _, _} = node]} -> + param_index = ASTHelpers.Function.find_params_index(node, position) + + if param_index do + {:ok, {Module.concat(modules), name, param_index + 1}} + else + {:ok, {Module.concat(modules), name, nil}} + end + + {{:., _, [{:__aliases__, _, modules}, name]}, _, _} = node -> + param_index = ASTHelpers.Function.find_params_index(node, position) + + {:ok, {Module.concat(modules), name, param_index}} + + _otherwise -> + {:error, :not_found} end end end - def format({:ok, {:docs_v1, _, _lang, content_type, _, _, docs}}, func_name) do + def format({:ok, {:docs_v1, _, _lang, content_type, _, _, docs}}, func_name, param_index) do for {{_, name, _arity}, _, [signature], fdoc, _} <- docs, name == func_name do params_info = signature |> Spitfire.parse!() - |> then(fn {_, _, args} -> - Enum.map(args, fn {name, _, _} -> - %ParameterInformation{ - label: Atom.to_string(name) - } - end) + |> Sourceror.get_args() + |> Enum.map(fn {name, _, _} -> + %ParameterInformation{ + label: Atom.to_string(name) + } end) %SignatureHelp{ @@ -42,14 +56,19 @@ defmodule NextLS.SignatureHelp do %SignatureInformation{ label: signature, parameters: params_info, - documentation: maybe_doc(content_type, fdoc) + documentation: maybe_doc(content_type, fdoc), + active_parameter: param_index } ] } end end - def format({:ok, {:error, :module_not_found}}, _func_name) do + def format({:ok, {:error, :module_not_found}}, _func_name, _param_index) do + [] + end + + def format({:error, :not_ready}, _func_name, _param_index) do [] end diff --git a/test/next_ls/signature_help_test.exs b/test/next_ls/signature_help_test.exs index d9564e3d..1b860c80 100644 --- a/test/next_ls/signature_help_test.exs +++ b/test/next_ls/signature_help_test.exs @@ -51,57 +51,146 @@ defmodule NextLS.SignatureHelpTest do end """) - bar = Path.join(cwd, "my_proj/lib/bar.ex") + [imported: imported, remote: remote, nested_alias: nested_alias] + end + + setup :with_lsp + + setup context do + assert :ok == notify(context.client, %{method: "initialized", jsonrpc: "2.0", params: %{}}) + assert_is_ready(context, "my_proj") + assert_compiled(context, "my_proj") + assert_notification "$/progress", %{"value" => %{"kind" => "end", "message" => "Finished indexing!"}} + end - File.write!(bar, """ + test "get signature help", %{client: client, cwd: cwd} do + did_open(client, Path.join(cwd, "my_proj/lib/bar.ex"), """ defmodule Bar do - alias Remote.NestedAlias + def run do + Remote.bang!("bang1") + end + end + """) - def run() do - Remote.bang!("bang") + uri = "file://#{cwd}/my_proj/lib/bar.ex" - Remote.bangs!("bang1", "bang2") + did_change(client, uri) - Remote.bangs!( - "bang1", - "bang2" - ) + request(client, %{ + method: "textDocument/signatureHelp", + id: 4, + jsonrpc: "2.0", + params: %{ + position: %{line: 2, character: 15}, + textDocument: %{uri: uri} + } + }) + + assert_result 4, %{ + "signatures" => [ + %{ + "parameters" => [ + %{"label" => "bang"} + ], + "label" => "bang!(bang)", + "documentation" => %{ + "kind" => "markdown", + "value" => "doc example" + }, + "activeParameter" => 0 + } + ] + } + end - NestedAlias.bang!("bang") + test "get signature help with multiple params", %{client: client, cwd: cwd} do + did_open(client, Path.join(cwd, "my_proj/lib/bar.ex"), """ + defmodule Bar do + def run do + Remote.bangs!("bang1", "bang2") end end """) - baz = Path.join(cwd, "my_proj/lib/baz.ex") + uri = "file://#{cwd}/my_proj/lib/bar.ex" - File.write!(baz, """ - defmodule Baz do - import Imported + did_change(client, uri) - def run() do - boom([1, 2], 1) + request(client, %{ + method: "textDocument/signatureHelp", + id: 4, + jsonrpc: "2.0", + params: %{ + position: %{line: 2, character: 15}, + textDocument: %{uri: uri} + } + }) - get_in(%{boom: %{bar: 1}}, [:boom, :bar]) + assert_result 4, %{ + "signatures" => [ + %{ + "parameters" => [ + %{"label" => "bang1"}, + %{"label" => "bang2"} + ], + "label" => "bangs!(bang1, bang2)", + "activeParameter" => 0 + } + ] + } + end + + test "get signature help with multiple params and active parameter 1", %{client: client, cwd: cwd} do + did_open(client, Path.join(cwd, "my_proj/lib/bar.ex"), """ + defmodule Bar do + def run do + Remote.bangs!("bang1", "bang2") end end """) - [bar: bar, imported: imported, remote: remote, baz: baz, nested_alias: nested_alias] - end + uri = "file://#{cwd}/my_proj/lib/bar.ex" - setup :with_lsp + did_change(client, uri) - setup context do - assert :ok == notify(context.client, %{method: "initialized", jsonrpc: "2.0", params: %{}}) - assert_is_ready(context, "my_proj") - assert_compiled(context, "my_proj") - assert_notification "$/progress", %{"value" => %{"kind" => "end", "message" => "Finished indexing!"}} + request(client, %{ + method: "textDocument/signatureHelp", + id: 4, + jsonrpc: "2.0", + params: %{ + position: %{line: 2, character: 22}, + textDocument: %{uri: uri} + } + }) + + assert_result 4, %{ + "signatures" => [ + %{ + "parameters" => [ + %{"label" => "bang1"}, + %{"label" => "bang2"} + ], + "label" => "bangs!(bang1, bang2)", + "activeParameter" => 1 + } + ] + } end - test "get signature help", %{client: client, bar: bar} do - uri = uri(bar) + test "get signature help with parameters on multiple lines", %{client: client, cwd: cwd} do + did_open(client, Path.join(cwd, "my_proj/lib/bar.ex"), """ + defmodule Bar do + def run do + Remote.bangs!( + "bang1", + "bang2" + ) + end + end + """) + + uri = "file://#{cwd}/my_proj/lib/bar.ex" - did_open(client, bar, File.read!(bar)) did_change(client, uri) request(client, %{ @@ -109,7 +198,7 @@ defmodule NextLS.SignatureHelpTest do id: 4, jsonrpc: "2.0", params: %{ - position: %{line: 4, character: 19}, + position: %{line: 4, character: 6}, textDocument: %{uri: uri} } }) @@ -118,22 +207,27 @@ defmodule NextLS.SignatureHelpTest do "signatures" => [ %{ "parameters" => [ - %{"label" => "bang"} + %{"label" => "bang1"}, + %{"label" => "bang2"} ], - "label" => "bang!(bang)", - "documentation" => %{ - "kind" => "markdown", - "value" => "doc example" - } + "label" => "bangs!(bang1, bang2)", + "activeParameter" => 1 } ] } end - test "get signature help with multiple params", %{client: client, bar: bar} do - uri = uri(bar) + test "get signature help with pipe", %{client: client, cwd: cwd} do + did_open(client, Path.join(cwd, "my_proj/lib/bar.ex"), """ + defmodule Bar do + def run do + "bang1" |> Remote.bangs!("bang2") + end + end + """) + + uri = "file://#{cwd}/my_proj/lib/bar.ex" - did_open(client, bar, File.read!(bar)) did_change(client, uri) request(client, %{ @@ -141,7 +235,7 @@ defmodule NextLS.SignatureHelpTest do id: 4, jsonrpc: "2.0", params: %{ - position: %{line: 6, character: 13}, + position: %{line: 2, character: 25}, textDocument: %{uri: uri} } }) @@ -153,16 +247,26 @@ defmodule NextLS.SignatureHelpTest do %{"label" => "bang1"}, %{"label" => "bang2"} ], - "label" => "bangs!(bang1, bang2)" + "label" => "bangs!(bang1, bang2)", + "activeParameter" => 1 } ] } end - test "get signature help with parameters on multiple lines", %{client: client, bar: bar} do - uri = uri(bar) + test "get signature help with multiple pipe", %{client: client, cwd: cwd} do + did_open(client, Path.join(cwd, "my_proj/lib/bar.ex"), """ + defmodule Bar do + def run do + ["bang", "bang"] + |> Enum.map(fn name -> "super" <> name end) + |> Remote.bangs!() + end + end + """) + + uri = "file://#{cwd}/my_proj/lib/bar.ex" - did_open(client, bar, File.read!(bar)) did_change(client, uri) request(client, %{ @@ -170,7 +274,7 @@ defmodule NextLS.SignatureHelpTest do id: 4, jsonrpc: "2.0", params: %{ - position: %{line: 9, character: 13}, + position: %{line: 3, character: 25}, textDocument: %{uri: uri} } }) @@ -179,90 +283,53 @@ defmodule NextLS.SignatureHelpTest do "signatures" => [ %{ "parameters" => [ - %{"label" => "bang1"}, - %{"label" => "bang2"} + %{"label" => "enumerable"}, + %{"label" => "fun"} ], - "label" => "bangs!(bang1, bang2)" + "label" => "map(enumerable, fun)", + "activeParameter" => 1 } ] } end - # test "get signature help with aliased module", %{client: client, bar: bar} do - # uri = uri(bar) - - # request(client, %{ - # method: "textDocument/signatureHelp", - # id: 4, - # jsonrpc: "2.0", - # params: %{ - # position: %{line: 12, character: 13}, - # textDocument: %{uri: uri} - # } - # }) - - # assert_result 4, %{ - # "signatures" => [ - # %{ - # "parameters" => [ - # %{"label" => "bang"} - # ], - # "label" => "bang!(bang)" - # } - # ] - # } - # end - - # test "get signature from imported functions", %{client: client, baz: baz} do - # uri = uri(baz) - - # request(client, %{ - # method: "textDocument/signatureHelp", - # id: 4, - # jsonrpc: "2.0", - # params: %{ - # position: %{line: 4, character: 13}, - # textDocument: %{uri: uri} - # } - # }) - - # assert_result 4, %{ - # "signatures" => [ - # %{ - # "parameters" => [ - # %{"label" => "boom1"}, - # %{"label" => "boom2"} - # ], - # "label" => "boom(boom1, boom2)" - # } - # ] - # } - # end - - # test "get signature for kernel functions", %{client: client, baz: baz} do - # uri = uri(baz) - - # request(client, %{ - # method: "textDocument/signatureHelp", - # id: 4, - # jsonrpc: "2.0", - # params: %{ - # position: %{line: 9, character: 13}, - # textDocument: %{uri: uri} - # } - # }) - - # assert_result 4, %{ - # "signatures" => [ - # %{ - # "parameters" => [ - # %{"label" => "boom1"}, - # %{"label" => "boom2"} - # ], - # "label" => "get_in(boom1, boom2)" - # } - # ] - # } - # end + test "get signature help with param function on multiple lines", %{client: client, cwd: cwd} do + did_open(client, Path.join(cwd, "my_proj/lib/bar.ex"), """ + defmodule Bar do + def run do + Enum.map([1, 2, 3], fn n -> + n + 1 + end) + end + end + """) + + uri = "file://#{cwd}/my_proj/lib/bar.ex" + + did_change(client, uri) + + request(client, %{ + method: "textDocument/signatureHelp", + id: 4, + jsonrpc: "2.0", + params: %{ + position: %{line: 3, character: 3}, + textDocument: %{uri: uri} + } + }) + + assert_result 4, %{ + "signatures" => [ + %{ + "parameters" => [ + %{"label" => "enumerable"}, + %{"label" => "fun"} + ], + "label" => "map(enumerable, fun)", + "activeParameter" => 1 + } + ] + } + end end end From ad3957c00b304835895b2f8e13cbf96300384624 Mon Sep 17 00:00:00 2001 From: Luca Cervello Date: Thu, 16 May 2024 14:44:23 +0200 Subject: [PATCH 6/6] fix: after merge --- lib/next_ls/signature_help.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/next_ls/signature_help.ex b/lib/next_ls/signature_help.ex index 2f21b85b..838cee78 100644 --- a/lib/next_ls/signature_help.ex +++ b/lib/next_ls/signature_help.ex @@ -75,7 +75,7 @@ defmodule NextLS.SignatureHelp do defp maybe_doc(content_type, %{"en" => fdoc}) do %MarkupContent{ kind: MarkupKind.markdown(), - value: NextLS.DocsHelpers.to_markdown(content_type, fdoc) + value: NextLS.Docs.to_markdown(content_type, fdoc) } end