From 998429dc4761f131a1d7f7a65b96e45ecf6cdd91 Mon Sep 17 00:00:00 2001 From: Mitchell Hanberg Date: Sun, 25 Jun 2023 14:27:17 -0400 Subject: [PATCH] feat: workspace symbols --- README.md | 1 + lib/next_ls.ex | 49 +++++++++-- lib/next_ls/symbol_table.ex | 45 +++++++++- priv/monkey/_next_ls_private_compiler.ex | 12 ++- test/next_ls/runtime_test.exs | 2 +- test/next_ls/symbol_table_test.exs | 20 +++-- test/next_ls_test.exs | 103 ++++++++++++++++++++++- test/support/project/lib/bar.ex | 2 + 8 files changed, 214 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 43263c9a..9846f710 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ Still in heavy development, currently supporting the following features: - Compiler Diagnostics - Code Formatting +- Workspace Symbols ## Editor Support diff --git a/lib/next_ls.ex b/lib/next_ls.ex index 16f5af89..9cfeadf4 100644 --- a/lib/next_ls.ex +++ b/lib/next_ls.ex @@ -20,7 +20,8 @@ defmodule NextLS do alias GenLSP.Requests.{ Initialize, Shutdown, - TextDocumentFormatting + TextDocumentFormatting, + WorkspaceSymbol } alias GenLSP.Structures.{ @@ -29,13 +30,15 @@ defmodule NextLS do InitializeResult, Position, Range, + Location, SaveOptions, ServerCapabilities, TextDocumentItem, TextDocumentSyncOptions, TextEdit, WorkDoneProgressBegin, - WorkDoneProgressEnd + WorkDoneProgressEnd, + SymbolInformation } alias NextLS.Runtime @@ -94,12 +97,38 @@ defmodule NextLS do save: %SaveOptions{include_text: true}, change: TextDocumentSyncKind.full() }, - document_formatting_provider: true + document_formatting_provider: true, + workspace_symbol_provider: true }, server_info: %{name: "NextLS"} }, assign(lsp, root_uri: root_uri)} end + def handle_request(%WorkspaceSymbol{params: %{query: _query}}, lsp) do + symbols = + for %SymbolTable.Symbol{} = symbol <- SymbolTable.symbols(lsp.assigns.symbol_table) do + %SymbolInformation{ + name: to_string(symbol.name), + kind: elixir_kind_to_lsp_kind(symbol.type), + location: %Location{ + uri: "file://#{symbol.file}", + range: %Range{ + start: %Position{ + line: symbol.line - 1, + character: symbol.col - 1 + }, + end: %Position{ + line: symbol.line - 1, + character: symbol.col - 1 + } + } + } + } + end + + {:reply, symbols, lsp} + end + def handle_request(%TextDocumentFormatting{params: %{text_document: %{uri: uri}}}, lsp) do document = lsp.assigns.documents[uri] runtime = lsp.assigns.runtime @@ -215,8 +244,6 @@ defmodule NextLS do }, %{assigns: %{ready: true}} = lsp ) do - dbg(self()) - dbg(Node.self()) token = token() progress_start(lsp, token, "Compiling...") @@ -276,10 +303,13 @@ defmodule NextLS do def handle_info({:tracer, payload}, lsp) do SymbolTable.put_symbols(lsp.assigns.symbol_table, payload) + GenLSP.log(lsp, "[NextLS] Updated the symbols table!") {:noreply, lsp} end def handle_info(:publish, lsp) do + GenLSP.log(lsp, "[NextLS] Compiled!") + all = for {_namespace, cache} <- DiagnosticCache.get(lsp.assigns.cache), {file, diagnostics} <- cache, reduce: %{} do d -> Map.update(d, file, diagnostics, fn value -> value ++ diagnostics end) @@ -354,8 +384,7 @@ defmodule NextLS do end def handle_info(message, lsp) do - IO.puts("catcall") - dbg(message) + GenLSP.log(lsp, "[NextLS] Unhanded message: #{inspect(message)}") {:noreply, lsp} end @@ -401,4 +430,10 @@ defmodule NextLS do _ -> "dev" end end + + defp elixir_kind_to_lsp_kind(:defmodule), do: GenLSP.Enumerations.SymbolKind.module() + defp elixir_kind_to_lsp_kind(:defstruct), do: GenLSP.Enumerations.SymbolKind.struct() + + defp elixir_kind_to_lsp_kind(kind) when kind in [:def, :defp, :defmacro, :defmacrop], + do: GenLSP.Enumerations.SymbolKind.function() end diff --git a/lib/next_ls/symbol_table.ex b/lib/next_ls/symbol_table.ex index c3a32594..45b4646a 100644 --- a/lib/next_ls/symbol_table.ex +++ b/lib/next_ls/symbol_table.ex @@ -16,9 +16,12 @@ defmodule NextLS.SymbolTable do @spec put_symbols(pid() | atom(), list(tuple())) :: :ok def put_symbols(server, symbols), do: GenServer.cast(server, {:put_symbols, symbols}) + @spec symbols(pid() | atom()) :: list(struct()) def symbols(server), do: GenServer.call(server, :symbols) + def close(server), do: GenServer.call(server, :close) + def init(args) do path = Keyword.fetch!(args, :path) @@ -42,16 +45,54 @@ defmodule NextLS.SymbolTable do {:reply, symbols, state} end + def handle_call(:close, _, state) do + :dets.close(state.table) + + {:reply, :ok, state} + end + def handle_cast({:put_symbols, symbols}, state) do %{ module: mod, + module_line: module_line, + struct: struct, file: file, defs: defs } = symbols :dets.delete(state.table, mod) - for {name, {:v1, type, _meta, clauses}} <- defs, {meta, _, _, _} <- clauses do + :dets.insert( + state.table, + {mod, + %Symbol{ + module: mod, + file: file, + type: :defmodule, + name: Macro.to_string(mod), + line: module_line, + col: 1 + }} + ) + + if struct do + {_, _, meta, _} = defs[:__struct__] + + :dets.insert( + state.table, + {mod, + %Symbol{ + module: mod, + file: file, + type: :defstruct, + name: "%#{Macro.to_string(mod)}{}", + line: meta[:line], + col: 1 + }} + ) + end + + for {name, {:v1, type, _meta, clauses}} <- defs, name != :__struct__, {meta, _, _, _} <- clauses do :dets.insert( state.table, {mod, @@ -61,7 +102,7 @@ defmodule NextLS.SymbolTable do type: type, name: name, line: meta[:line], - col: meta[:column] + col: meta[:column] || 1 }} ) end diff --git a/priv/monkey/_next_ls_private_compiler.ex b/priv/monkey/_next_ls_private_compiler.ex index 7e784fe8..78eec97c 100644 --- a/priv/monkey/_next_ls_private_compiler.ex +++ b/priv/monkey/_next_ls_private_compiler.ex @@ -1,5 +1,5 @@ defmodule NextLSPrivate.Tracer do - def trace({:on_module, _, _}, env) do + def trace({:on_module, bytecode, _}, env) do parent = "NEXTLS_PARENT_PID" |> System.get_env() |> Base.decode64!() |> :erlang.binary_to_term() defs = Module.definitions_in(env.module) @@ -9,7 +9,15 @@ defmodule NextLSPrivate.Tracer do {name, Module.get_definition(env.module, {name, arity})} end - Process.send(parent, {:tracer, %{file: env.file, module: env.module, defs: defs}}, []) + {:ok, {_, [{'Dbgi', bin}]}} = :beam_lib.chunks(bytecode, ['Dbgi']) + + {:debug_info_v1, _, {_, %{line: line, struct: struct}, _}} = :erlang.binary_to_term(bin) + + Process.send( + parent, + {:tracer, %{file: env.file, module: env.module, module_line: line, struct: struct, defs: defs}}, + [] + ) :ok end diff --git a/test/next_ls/runtime_test.exs b/test/next_ls/runtime_test.exs index 962744ba..bae6e1a1 100644 --- a/test/next_ls/runtime_test.exs +++ b/test/next_ls/runtime_test.exs @@ -66,7 +66,7 @@ defmodule NextLs.RuntimeTest do severity: :warning, message: "variable \"arg1\" is unused (if the variable is not meant to be used, prefix it with an underscore)", - position: 2, + position: 4, compiler_name: "Elixir", details: nil } diff --git a/test/next_ls/symbol_table_test.exs b/test/next_ls/symbol_table_test.exs index 14db538e..d776da9a 100644 --- a/test/next_ls/symbol_table_test.exs +++ b/test/next_ls/symbol_table_test.exs @@ -23,20 +23,28 @@ defmodule NextLS.SymbolTableTest do assert [ %SymbolTable.Symbol{ - module: "NextLS", + module: NextLS, file: "/Users/alice/next_ls/lib/next_ls.ex", type: :def, name: :start_link, line: 45, - col: nil + col: 1 }, %SymbolTable.Symbol{ - module: "NextLS", + module: NextLS, file: "/Users/alice/next_ls/lib/next_ls.ex", type: :def, name: :start_link, line: 44, - col: nil + col: 1 + }, + %SymbolTable.Symbol{ + module: NextLS, + file: "/Users/alice/next_ls/lib/next_ls.ex", + type: :defmodule, + name: "NextLS", + line: 1, + col: 1 } ] == SymbolTable.symbols(pid) end @@ -44,7 +52,9 @@ defmodule NextLS.SymbolTableTest do defp symbols() do %{ file: "/Users/alice/next_ls/lib/next_ls.ex", - module: "NextLS", + module: NextLS, + module_line: 1, + struct: nil, defs: [ start_link: {:v1, :def, [line: 44], diff --git a/test/next_ls_test.exs b/test/next_ls_test.exs index e32f5df5..d255b6b1 100644 --- a/test/next_ls_test.exs +++ b/test/next_ls_test.exs @@ -8,6 +8,8 @@ defmodule NextLSTest do setup %{tmp_dir: tmp_dir} do File.cp_r!("test/support/project", tmp_dir) + File.rm_rf!(Path.join(tmp_dir, ".elixir-tools")) + root_path = Path.absname(tmp_dir) tvisor = start_supervised!(Task.Supervisor) @@ -15,7 +17,7 @@ defmodule NextLSTest do start_supervised!({Registry, [keys: :unique, name: Registry.NextLSTest]}) extensions = [NextLS.ElixirExtension] cache = start_supervised!(NextLS.DiagnosticCache) - symbol_table = start_supervised!({NextLS.SymbolTable, [path: tmp_dir]}) + symbol_table = start_supervised!({NextLS.SymbolTable, path: tmp_dir}) server = server(NextLS, @@ -165,8 +167,8 @@ defmodule NextLSTest do "message" => "variable \"arg1\" is unused (if the variable is not meant to be used, prefix it with an underscore)", "range" => %{ - "start" => %{"line" => 1, "character" => 0}, - "end" => %{"line" => 1, "character" => 999} + "start" => %{"line" => 3, "character" => 0}, + "end" => %{"line" => 3, "character" => 999} } } ] @@ -300,4 +302,99 @@ defmodule NextLSTest do assert_result 2, nil end + + test "workspace symbols", %{client: client, cwd: cwd} do + assert :ok == + notify(client, %{ + method: "initialized", + jsonrpc: "2.0", + params: %{} + }) + + assert_notification "window/logMessage", %{"message" => "[NextLS] Runtime ready..."} + assert_notification "window/logMessage", %{"message" => "[NextLS] Compiled!"} + + request client, %{ + method: "workspace/symbol", + id: 2, + jsonrpc: "2.0", + params: %{ + query: "" + } + } + + assert_result 2, symbols + + assert %{ + "kind" => 12, + "location" => %{ + "range" => %{ + "start" => %{ + "line" => 3, + "character" => 0 + }, + "end" => %{ + "line" => 3, + "character" => 0 + } + }, + "uri" => "file://#{cwd}/lib/bar.ex" + }, + "name" => "foo" + } in symbols + + assert %{ + "kind" => 2, + "location" => %{ + "range" => %{ + "start" => %{ + "line" => 0, + "character" => 0 + }, + "end" => %{ + "line" => 0, + "character" => 0 + } + }, + "uri" => "file://#{cwd}/lib/bar.ex" + }, + "name" => "Bar" + } in symbols + + assert %{ + "kind" => 23, + "location" => %{ + "range" => %{ + "start" => %{ + "line" => 1, + "character" => 0 + }, + "end" => %{ + "line" => 1, + "character" => 0 + } + }, + "uri" => "file://#{cwd}/lib/bar.ex" + }, + "name" => "%Bar{}" + } in symbols + + assert %{ + "kind" => 2, + "location" => %{ + "range" => %{ + "start" => %{ + "line" => 3, + "character" => 0 + }, + "end" => %{ + "line" => 3, + "character" => 0 + } + }, + "uri" => "file://#{cwd}/lib/code_action.ex" + }, + "name" => "Foo.CodeAction.NestedMod" + } in symbols + end end diff --git a/test/support/project/lib/bar.ex b/test/support/project/lib/bar.ex index 0f6fd837..43dea95a 100644 --- a/test/support/project/lib/bar.ex +++ b/test/support/project/lib/bar.ex @@ -1,4 +1,6 @@ defmodule Bar do + defstruct [:foo] + def foo(arg1) do end end