Skip to content

Commit

Permalink
feat: workspace symbols (#31)
Browse files Browse the repository at this point in the history
  • Loading branch information
mhanberg committed Jun 25, 2023
1 parent 37fc91a commit c1aa20c
Show file tree
Hide file tree
Showing 8 changed files with 216 additions and 18 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Still in heavy development, currently supporting the following features:

- Compiler Diagnostics
- Code Formatting
- Workspace Symbols

## Editor Support

Expand Down
47 changes: 43 additions & 4 deletions lib/next_ls.ex
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ defmodule NextLS do
alias GenLSP.Requests.{
Initialize,
Shutdown,
TextDocumentFormatting
TextDocumentFormatting,
WorkspaceSymbol
}

alias GenLSP.Structures.{
Expand All @@ -29,13 +30,15 @@ defmodule NextLS do
InitializeResult,
Position,
Range,
Location,
SaveOptions,
ServerCapabilities,
TextDocumentItem,
TextDocumentSyncOptions,
TextEdit,
WorkDoneProgressBegin,
WorkDoneProgressEnd
WorkDoneProgressEnd,
SymbolInformation
}

alias NextLS.Runtime
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -274,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)
Expand Down Expand Up @@ -351,7 +383,8 @@ defmodule NextLS do
{:noreply, lsp}
end

def handle_info(_message, lsp) do
def handle_info(message, lsp) do
GenLSP.log(lsp, "[NextLS] Unhanded message: #{inspect(message)}")
{:noreply, lsp}
end

Expand Down Expand Up @@ -397,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
45 changes: 43 additions & 2 deletions lib/next_ls/symbol_table.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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,
Expand All @@ -61,7 +102,7 @@ defmodule NextLS.SymbolTable do
type: type,
name: name,
line: meta[:line],
col: meta[:column]
col: meta[:column] || 1
}}
)
end
Expand Down
12 changes: 10 additions & 2 deletions priv/monkey/_next_ls_private_compiler.ex
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions test/next_ls/runtime_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,9 @@ defmodule NextLs.RuntimeTest do
] = Runtime.compile(pid)

if Version.match?(System.version(), ">= 1.15.0") do
assert position == {2, 11}
assert position == {4, 11}
else
assert position == 2
assert position == 4
end

File.write!(file, """
Expand Down
20 changes: 15 additions & 5 deletions test/next_ls/symbol_table_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -23,28 +23,38 @@ 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

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],
Expand Down
103 changes: 100 additions & 3 deletions test/next_ls_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,16 @@ 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)
rvisor = start_supervised!({DynamicSupervisor, [strategy: :one_for_one]})
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,
Expand Down Expand Up @@ -167,8 +169,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" => ^char},
"end" => %{"line" => 1, "character" => 999}
"start" => %{"line" => 3, "character" => ^char},
"end" => %{"line" => 3, "character" => 999}
}
}
]
Expand Down Expand Up @@ -302,4 +304,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
Loading

0 comments on commit c1aa20c

Please sign in to comment.