From 404d8fd775ab8420f8199f02287da869f7c87c9e 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 | 22 ++++ lib/next_ls/signature_help.ex | 117 ++++++++++++++++++++++ test/next_ls/helpers/ast_helpers_test.exs | 14 +++ test/next_ls/signature_help_test.exs | 94 +++++++++++++++++ 5 files changed, 268 insertions(+) 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 833120e4..eb4d31fb 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) + 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..72721427 100644 --- a/lib/next_ls/helpers/ast_helpers.ex +++ b/lib/next_ls/helpers/ast_helpers.ex @@ -152,4 +152,26 @@ defmodule NextLS.ASTHelpers do end end) end + + defmodule Functions do + @moduledoc false + 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 + end end diff --git a/lib/next_ls/signature_help.ex b/lib/next_ls/signature_help.ex new file mode 100644 index 00000000..cc69ea95 --- /dev/null +++ b/lib/next_ls/signature_help.ex @@ -0,0 +1,117 @@ +defmodule NextLS.SignatureHelp do + @moduledoc false + + import NextLS.DB.Query + + alias GenLSP.Structures.ParameterInformation + alias GenLSP.Structures.SignatureHelp + alias GenLSP.Structures.SignatureInformation + alias NextLS.ASTHelpers + alias NextLS.DB + + def fetch(file, {line, col}, db) do + symbol = fetch_symbol(file, line, col, db) + + case symbol do + nil -> + nil + + [] -> + nil + + [[_, _mod, file, type, label, line, col | _] | _] = _definition -> + if type in ["def", "defp"] do + code = File.read!(file) + + params = + code + |> ASTHelpers.Functions.get_function_params(label, line, col) + |> Enum.map(fn {name, _, _} -> + %ParameterInformation{ + label: Atom.to_string(name) + } + end) + + %SignatureHelp{ + signatures: [ + %SignatureInformation{ + label: label, + documentation: "need help", + parameters: params, + active_parameter: 0 + } + ], + active_signature: 0, + active_parameter: 0 + } + else + nil + end + end + end + + defp fetch_symbol(file, line, col, db) do + rows = + DB.query( + db, + ~Q""" + SELECT + * + FROM + 'references' AS refs + WHERE + refs.file = ? + AND refs.start_line <= ? + AND ? <= refs.end_line + AND refs.start_column <= ? + AND ? <= refs.end_column + ORDER BY refs.id asc + LIMIT 1; + """, + [file, line, line, col, col] + ) + + reference = + case rows do + [[_pk, identifier, _arity, _file, type, module, _start_l, _start_c, _end_l, _end_c | _]] -> + %{identifier: identifier, type: type, module: module} + + [] -> + nil + end + + with %{identifier: identifier, type: type, module: module} <- reference do + query = + ~Q""" + SELECT + * + FROM + symbols + WHERE + symbols.module = ? + AND symbols.name = ?; + """ + + args = + case type do + "alias" -> + [module, module] + + "function" -> + [module, identifier] + + "attribute" -> + [module, identifier] + + _ -> + nil + end + + if args do + DB.query(db, query, args) + else + nil + 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 ca329d46..2858a073 100644 --- a/test/next_ls/helpers/ast_helpers_test.exs +++ b/test/next_ls/helpers/ast_helpers_test.exs @@ -74,4 +74,18 @@ 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 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..17550183 --- /dev/null +++ b/test/next_ls/signature_help_test.exs @@ -0,0 +1,94 @@ +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) do + boom1 + end + end + """) + + bar = Path.join(cwd, "my_proj/lib/bar.ex") + + File.write!(bar, """ + defmodule Bar do + import Imported + def run() do + Remote.bang!("‼️") + process() + end + + defp process() do + boom("💣") + :ok + 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: 15}, + textDocument: %{uri: uri} + } + }) + + assert_result 4, %{ + "activeParameter" => 0, + "activeSignature" => 0, + "signatures" => [ + %{ + "activeParameter" => 0, + "parameters" => [ + %{"label" => "bang"} + ], + "documentation" => "need help", + "label" => "bang!" + } + ] + } + end + end +end