diff --git a/lib/next_ls.ex b/lib/next_ls.ex index ed30b3fb..0b95e77d 100644 --- a/lib/next_ls.ex +++ b/lib/next_ls.ex @@ -19,6 +19,7 @@ defmodule NextLS do alias GenLSP.Requests.TextDocumentDefinition alias GenLSP.Requests.TextDocumentDocumentSymbol alias GenLSP.Requests.TextDocumentFormatting + alias GenLSP.Requests.TextDocumentReferences alias GenLSP.Requests.WorkspaceSymbol alias GenLSP.Structures.DidChangeWatchedFilesParams alias GenLSP.Structures.DidChangeWorkspaceFoldersParams @@ -108,6 +109,7 @@ defmodule NextLS do document_formatting_provider: true, workspace_symbol_provider: true, document_symbol_provider: true, + references_provider: true, definition_provider: true, workspace: %{ workspace_folders: %GenLSP.Structures.WorkspaceFoldersServerCapabilities{ @@ -171,6 +173,64 @@ defmodule NextLS do {:reply, symbols, lsp} end + # TODO handle `context: %{includeDeclaration: true}` to include the current symbol definition among + # the results. + def handle_request(%TextDocumentReferences{params: %{position: position, text_document: %{uri: uri}}}, lsp) do + file = URI.parse(uri).path + line = position.line + 1 + col = position.character + 1 + + locations = + dispatch(lsp.assigns.registry, :databases, fn databases -> + Enum.flat_map(databases, fn {database, _} -> + references = + case symbol_info(file, line, col, database) do + {:function, module, function} -> + DB.query( + database, + ~Q""" + SELECT file, start_line, end_line, start_column, end_column + FROM "references" as refs + WHERE refs.identifier = ? + AND refs.type = ? + AND refs.module = ? + AND NOT like('/home/runner/work/elixir/%', refs.file) + """, + [function, "function", module] + ) + + {:module, module} -> + DB.query( + database, + ~Q""" + SELECT file, start_line, end_line, start_column, end_column + FROM "references" as refs + WHERE refs.module = ? + AND refs.type = ? + AND NOT like('/home/runner/work/elixir/%', refs.file) + """, + [module, "alias"] + ) + + :unknown -> + [] + end + + for [file, start_line, end_line, start_column, end_column] <- references do + %Location{ + uri: "file://#{file}", + range: %Range{ + start: %Position{line: clamp(start_line - 1), character: clamp(start_column - 1)}, + end: %Position{line: clamp(end_line - 1), character: clamp(end_column - 1)} + } + } + end + end) + end) + + {:reply, locations, lsp} + end + def handle_request(%WorkspaceSymbol{params: %{query: query}}, lsp) do filter = fn sym -> if query == "" do @@ -603,4 +663,57 @@ defmodule NextLS do {^ref, result} -> result end end + + defp symbol_info(file, line, col, database) do + definition_query = + ~Q""" + SELECT module, type, name + FROM "symbols" sym + WHERE sym.file = ? + AND sym.line = ? + ORDER BY sym.id ASC + LIMIT 1 + """ + + reference_query = ~Q""" + SELECT identifier, type, module + FROM "references" 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 + """ + + case DB.query(database, definition_query, [file, line]) do + [[module, "defmodule", _]] -> + {:module, module} + + [[module, "defstruct", _]] -> + {:module, module} + + [[module, "def", function]] -> + {:function, module, function} + + [[module, "defp", function]] -> + {:function, module, function} + + [[module, "defmacro", function]] -> + {:function, module, function} + + _unknown_definition -> + case DB.query(database, reference_query, [file, line, line, col, col]) do + [[function, "function", module]] -> + {:function, module, function} + + [[_alias, "alias", module]] -> + {:module, module} + + _unknown_reference -> + :unknown + end + end + end + + defp clamp(line), do: max(line, 0) end diff --git a/test/next_ls_test.exs b/test/next_ls_test.exs index f0a56c54..99e2f6f6 100644 --- a/test/next_ls_test.exs +++ b/test/next_ls_test.exs @@ -888,6 +888,104 @@ defmodule NextLSTest do end end + describe "find references" 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 + peace = Path.join(cwd, "my_proj/lib/peace.ex") + + File.write!(peace, """ + defmodule MyApp.Peace do + def and_love() do + "✌️" + end + end + """) + + bar = Path.join(cwd, "my_proj/lib/bar.ex") + + File.write!(bar, """ + defmodule Bar do + alias MyApp.Peace + def run() do + Peace.and_love() + end + end + """) + + [bar: bar, peace: peace] + end + + setup :with_lsp + + test "list function references", %{client: client, bar: bar, peace: peace} do + assert :ok == notify(client, %{method: "initialized", jsonrpc: "2.0", params: %{}}) + assert_request(client, "client/registerCapability", fn _params -> nil end) + assert_notification "window/logMessage", %{"message" => "[NextLS] Runtime for folder my_proj is ready..."} + assert_notification "window/logMessage", %{"message" => "[NextLS] Compiled!"} + + request(client, %{ + method: "textDocument/references", + id: 4, + jsonrpc: "2.0", + params: %{ + position: %{line: 1, character: 6}, + textDocument: %{uri: uri(peace)}, + context: %{includeDeclaration: true} + } + }) + + uri = uri(bar) + + assert_result 4, + [ + %{ + "uri" => ^uri, + "range" => %{ + "start" => %{"line" => 3, "character" => 10}, + "end" => %{"line" => 3, "character" => 18} + } + } + ] + end + + test "list module references", %{client: client, bar: bar, peace: peace} do + assert :ok == notify(client, %{method: "initialized", jsonrpc: "2.0", params: %{}}) + assert_request(client, "client/registerCapability", fn _params -> nil end) + assert_notification "window/logMessage", %{"message" => "[NextLS] Runtime for folder my_proj is ready..."} + assert_notification "window/logMessage", %{"message" => "[NextLS] Compiled!"} + + request(client, %{ + method: "textDocument/references", + id: 4, + jsonrpc: "2.0", + params: %{ + position: %{line: 0, character: 10}, + textDocument: %{uri: uri(peace)}, + context: %{includeDeclaration: true} + } + }) + + uri = uri(bar) + + assert_result 4, + [ + %{ + "uri" => ^uri, + "range" => %{ + "start" => %{"line" => 3, "character" => 4}, + "end" => %{"line" => 3, "character" => 9} + } + } + ] + end + end + describe "workspaces" do setup %{tmp_dir: tmp_dir} do [cwd: tmp_dir]