Skip to content

Commit c1aa20c

Browse files
authored
feat: workspace symbols (#31)
1 parent 37fc91a commit c1aa20c

File tree

8 files changed

+216
-18
lines changed

8 files changed

+216
-18
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Still in heavy development, currently supporting the following features:
1010

1111
- Compiler Diagnostics
1212
- Code Formatting
13+
- Workspace Symbols
1314

1415
## Editor Support
1516

lib/next_ls.ex

+43-4
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ defmodule NextLS do
2020
alias GenLSP.Requests.{
2121
Initialize,
2222
Shutdown,
23-
TextDocumentFormatting
23+
TextDocumentFormatting,
24+
WorkspaceSymbol
2425
}
2526

2627
alias GenLSP.Structures.{
@@ -29,13 +30,15 @@ defmodule NextLS do
2930
InitializeResult,
3031
Position,
3132
Range,
33+
Location,
3234
SaveOptions,
3335
ServerCapabilities,
3436
TextDocumentItem,
3537
TextDocumentSyncOptions,
3638
TextEdit,
3739
WorkDoneProgressBegin,
38-
WorkDoneProgressEnd
40+
WorkDoneProgressEnd,
41+
SymbolInformation
3942
}
4043

4144
alias NextLS.Runtime
@@ -94,12 +97,38 @@ defmodule NextLS do
9497
save: %SaveOptions{include_text: true},
9598
change: TextDocumentSyncKind.full()
9699
},
97-
document_formatting_provider: true
100+
document_formatting_provider: true,
101+
workspace_symbol_provider: true
98102
},
99103
server_info: %{name: "NextLS"}
100104
}, assign(lsp, root_uri: root_uri)}
101105
end
102106

107+
def handle_request(%WorkspaceSymbol{params: %{query: _query}}, lsp) do
108+
symbols =
109+
for %SymbolTable.Symbol{} = symbol <- SymbolTable.symbols(lsp.assigns.symbol_table) do
110+
%SymbolInformation{
111+
name: to_string(symbol.name),
112+
kind: elixir_kind_to_lsp_kind(symbol.type),
113+
location: %Location{
114+
uri: "file://#{symbol.file}",
115+
range: %Range{
116+
start: %Position{
117+
line: symbol.line - 1,
118+
character: symbol.col - 1
119+
},
120+
end: %Position{
121+
line: symbol.line - 1,
122+
character: symbol.col - 1
123+
}
124+
}
125+
}
126+
}
127+
end
128+
129+
{:reply, symbols, lsp}
130+
end
131+
103132
def handle_request(%TextDocumentFormatting{params: %{text_document: %{uri: uri}}}, lsp) do
104133
document = lsp.assigns.documents[uri]
105134
runtime = lsp.assigns.runtime
@@ -274,10 +303,13 @@ defmodule NextLS do
274303

275304
def handle_info({:tracer, payload}, lsp) do
276305
SymbolTable.put_symbols(lsp.assigns.symbol_table, payload)
306+
GenLSP.log(lsp, "[NextLS] Updated the symbols table!")
277307
{:noreply, lsp}
278308
end
279309

280310
def handle_info(:publish, lsp) do
311+
GenLSP.log(lsp, "[NextLS] Compiled!")
312+
281313
all =
282314
for {_namespace, cache} <- DiagnosticCache.get(lsp.assigns.cache), {file, diagnostics} <- cache, reduce: %{} do
283315
d -> Map.update(d, file, diagnostics, fn value -> value ++ diagnostics end)
@@ -351,7 +383,8 @@ defmodule NextLS do
351383
{:noreply, lsp}
352384
end
353385

354-
def handle_info(_message, lsp) do
386+
def handle_info(message, lsp) do
387+
GenLSP.log(lsp, "[NextLS] Unhanded message: #{inspect(message)}")
355388
{:noreply, lsp}
356389
end
357390

@@ -397,4 +430,10 @@ defmodule NextLS do
397430
_ -> "dev"
398431
end
399432
end
433+
434+
defp elixir_kind_to_lsp_kind(:defmodule), do: GenLSP.Enumerations.SymbolKind.module()
435+
defp elixir_kind_to_lsp_kind(:defstruct), do: GenLSP.Enumerations.SymbolKind.struct()
436+
437+
defp elixir_kind_to_lsp_kind(kind) when kind in [:def, :defp, :defmacro, :defmacrop],
438+
do: GenLSP.Enumerations.SymbolKind.function()
400439
end

lib/next_ls/symbol_table.ex

+43-2
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,12 @@ defmodule NextLS.SymbolTable do
1616

1717
@spec put_symbols(pid() | atom(), list(tuple())) :: :ok
1818
def put_symbols(server, symbols), do: GenServer.cast(server, {:put_symbols, symbols})
19+
1920
@spec symbols(pid() | atom()) :: list(struct())
2021
def symbols(server), do: GenServer.call(server, :symbols)
2122

23+
def close(server), do: GenServer.call(server, :close)
24+
2225
def init(args) do
2326
path = Keyword.fetch!(args, :path)
2427

@@ -42,16 +45,54 @@ defmodule NextLS.SymbolTable do
4245
{:reply, symbols, state}
4346
end
4447

48+
def handle_call(:close, _, state) do
49+
:dets.close(state.table)
50+
51+
{:reply, :ok, state}
52+
end
53+
4554
def handle_cast({:put_symbols, symbols}, state) do
4655
%{
4756
module: mod,
57+
module_line: module_line,
58+
struct: struct,
4859
file: file,
4960
defs: defs
5061
} = symbols
5162

5263
:dets.delete(state.table, mod)
5364

54-
for {name, {:v1, type, _meta, clauses}} <- defs, {meta, _, _, _} <- clauses do
65+
:dets.insert(
66+
state.table,
67+
{mod,
68+
%Symbol{
69+
module: mod,
70+
file: file,
71+
type: :defmodule,
72+
name: Macro.to_string(mod),
73+
line: module_line,
74+
col: 1
75+
}}
76+
)
77+
78+
if struct do
79+
{_, _, meta, _} = defs[:__struct__]
80+
81+
:dets.insert(
82+
state.table,
83+
{mod,
84+
%Symbol{
85+
module: mod,
86+
file: file,
87+
type: :defstruct,
88+
name: "%#{Macro.to_string(mod)}{}",
89+
line: meta[:line],
90+
col: 1
91+
}}
92+
)
93+
end
94+
95+
for {name, {:v1, type, _meta, clauses}} <- defs, name != :__struct__, {meta, _, _, _} <- clauses do
5596
:dets.insert(
5697
state.table,
5798
{mod,
@@ -61,7 +102,7 @@ defmodule NextLS.SymbolTable do
61102
type: type,
62103
name: name,
63104
line: meta[:line],
64-
col: meta[:column]
105+
col: meta[:column] || 1
65106
}}
66107
)
67108
end

priv/monkey/_next_ls_private_compiler.ex

+10-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
defmodule NextLSPrivate.Tracer do
2-
def trace({:on_module, _, _}, env) do
2+
def trace({:on_module, bytecode, _}, env) do
33
parent = "NEXTLS_PARENT_PID" |> System.get_env() |> Base.decode64!() |> :erlang.binary_to_term()
44

55
defs = Module.definitions_in(env.module)
@@ -9,7 +9,15 @@ defmodule NextLSPrivate.Tracer do
99
{name, Module.get_definition(env.module, {name, arity})}
1010
end
1111

12-
Process.send(parent, {:tracer, %{file: env.file, module: env.module, defs: defs}}, [])
12+
{:ok, {_, [{'Dbgi', bin}]}} = :beam_lib.chunks(bytecode, ['Dbgi'])
13+
14+
{:debug_info_v1, _, {_, %{line: line, struct: struct}, _}} = :erlang.binary_to_term(bin)
15+
16+
Process.send(
17+
parent,
18+
{:tracer, %{file: env.file, module: env.module, module_line: line, struct: struct, defs: defs}},
19+
[]
20+
)
1321

1422
:ok
1523
end

test/next_ls/runtime_test.exs

+2-2
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,9 @@ defmodule NextLs.RuntimeTest do
7373
] = Runtime.compile(pid)
7474

7575
if Version.match?(System.version(), ">= 1.15.0") do
76-
assert position == {2, 11}
76+
assert position == {4, 11}
7777
else
78-
assert position == 2
78+
assert position == 4
7979
end
8080

8181
File.write!(file, """

test/next_ls/symbol_table_test.exs

+15-5
Original file line numberDiff line numberDiff line change
@@ -23,28 +23,38 @@ defmodule NextLS.SymbolTableTest do
2323

2424
assert [
2525
%SymbolTable.Symbol{
26-
module: "NextLS",
26+
module: NextLS,
2727
file: "/Users/alice/next_ls/lib/next_ls.ex",
2828
type: :def,
2929
name: :start_link,
3030
line: 45,
31-
col: nil
31+
col: 1
3232
},
3333
%SymbolTable.Symbol{
34-
module: "NextLS",
34+
module: NextLS,
3535
file: "/Users/alice/next_ls/lib/next_ls.ex",
3636
type: :def,
3737
name: :start_link,
3838
line: 44,
39-
col: nil
39+
col: 1
40+
},
41+
%SymbolTable.Symbol{
42+
module: NextLS,
43+
file: "/Users/alice/next_ls/lib/next_ls.ex",
44+
type: :defmodule,
45+
name: "NextLS",
46+
line: 1,
47+
col: 1
4048
}
4149
] == SymbolTable.symbols(pid)
4250
end
4351

4452
defp symbols() do
4553
%{
4654
file: "/Users/alice/next_ls/lib/next_ls.ex",
47-
module: "NextLS",
55+
module: NextLS,
56+
module_line: 1,
57+
struct: nil,
4858
defs: [
4959
start_link:
5060
{:v1, :def, [line: 44],

test/next_ls_test.exs

+100-3
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,16 @@ defmodule NextLSTest do
88
setup %{tmp_dir: tmp_dir} do
99
File.cp_r!("test/support/project", tmp_dir)
1010

11+
File.rm_rf!(Path.join(tmp_dir, ".elixir-tools"))
12+
1113
root_path = Path.absname(tmp_dir)
1214

1315
tvisor = start_supervised!(Task.Supervisor)
1416
rvisor = start_supervised!({DynamicSupervisor, [strategy: :one_for_one]})
1517
start_supervised!({Registry, [keys: :unique, name: Registry.NextLSTest]})
1618
extensions = [NextLS.ElixirExtension]
1719
cache = start_supervised!(NextLS.DiagnosticCache)
18-
symbol_table = start_supervised!({NextLS.SymbolTable, [path: tmp_dir]})
20+
symbol_table = start_supervised!({NextLS.SymbolTable, path: tmp_dir})
1921

2022
server =
2123
server(NextLS,
@@ -167,8 +169,8 @@ defmodule NextLSTest do
167169
"message" =>
168170
"variable \"arg1\" is unused (if the variable is not meant to be used, prefix it with an underscore)",
169171
"range" => %{
170-
"start" => %{"line" => 1, "character" => ^char},
171-
"end" => %{"line" => 1, "character" => 999}
172+
"start" => %{"line" => 3, "character" => ^char},
173+
"end" => %{"line" => 3, "character" => 999}
172174
}
173175
}
174176
]
@@ -302,4 +304,99 @@ defmodule NextLSTest do
302304

303305
assert_result 2, nil
304306
end
307+
308+
test "workspace symbols", %{client: client, cwd: cwd} do
309+
assert :ok ==
310+
notify(client, %{
311+
method: "initialized",
312+
jsonrpc: "2.0",
313+
params: %{}
314+
})
315+
316+
assert_notification "window/logMessage", %{"message" => "[NextLS] Runtime ready..."}
317+
assert_notification "window/logMessage", %{"message" => "[NextLS] Compiled!"}
318+
319+
request client, %{
320+
method: "workspace/symbol",
321+
id: 2,
322+
jsonrpc: "2.0",
323+
params: %{
324+
query: ""
325+
}
326+
}
327+
328+
assert_result 2, symbols
329+
330+
assert %{
331+
"kind" => 12,
332+
"location" => %{
333+
"range" => %{
334+
"start" => %{
335+
"line" => 3,
336+
"character" => 0
337+
},
338+
"end" => %{
339+
"line" => 3,
340+
"character" => 0
341+
}
342+
},
343+
"uri" => "file://#{cwd}/lib/bar.ex"
344+
},
345+
"name" => "foo"
346+
} in symbols
347+
348+
assert %{
349+
"kind" => 2,
350+
"location" => %{
351+
"range" => %{
352+
"start" => %{
353+
"line" => 0,
354+
"character" => 0
355+
},
356+
"end" => %{
357+
"line" => 0,
358+
"character" => 0
359+
}
360+
},
361+
"uri" => "file://#{cwd}/lib/bar.ex"
362+
},
363+
"name" => "Bar"
364+
} in symbols
365+
366+
assert %{
367+
"kind" => 23,
368+
"location" => %{
369+
"range" => %{
370+
"start" => %{
371+
"line" => 1,
372+
"character" => 0
373+
},
374+
"end" => %{
375+
"line" => 1,
376+
"character" => 0
377+
}
378+
},
379+
"uri" => "file://#{cwd}/lib/bar.ex"
380+
},
381+
"name" => "%Bar{}"
382+
} in symbols
383+
384+
assert %{
385+
"kind" => 2,
386+
"location" => %{
387+
"range" => %{
388+
"start" => %{
389+
"line" => 3,
390+
"character" => 0
391+
},
392+
"end" => %{
393+
"line" => 3,
394+
"character" => 0
395+
}
396+
},
397+
"uri" => "file://#{cwd}/lib/code_action.ex"
398+
},
399+
"name" => "Foo.CodeAction.NestedMod"
400+
} in symbols
401+
end
305402
end

0 commit comments

Comments
 (0)