diff --git a/lib/next_ls.ex b/lib/next_ls.ex index 72205036..910a0423 100644 --- a/lib/next_ls.ex +++ b/lib/next_ls.ex @@ -781,8 +781,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] = @@ -794,7 +794,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 e2fa2d9d..9631619e 100644 --- a/lib/next_ls/helpers/ast_helpers.ex +++ b/lib/next_ls/helpers/ast_helpers.ex @@ -178,11 +178,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 @@ -194,5 +194,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