diff --git a/lib/next_ls.ex b/lib/next_ls.ex index d3fc541f..9ee84900 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} = @@ -148,6 +152,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, @@ -746,6 +753,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 ffcb7cf6..963fe388 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