Skip to content

feat: workspace symbols #31

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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