Skip to content

Commit 37fc91a

Browse files
authored
feat: basic symbol table (#30)
1 parent a34a872 commit 37fc91a

File tree

11 files changed

+237
-11
lines changed

11 files changed

+237
-11
lines changed

bin/nextls

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
#!/usr/bin/env -S elixir --sname undefined
1+
#!/usr/bin/env elixir
2+
3+
Node.start("next-ls-#{System.system_time()}", :shortnames)
24

35
System.no_halt(true)
46

bin/start

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@
44

55
cd "$(dirname "$0")"/.. || exit 1
66

7-
elixir --sname undefined -S mix run --no-halt -e "Application.ensure_all_started(:next_ls)" -- "$@"
7+
elixir --sname "next-ls-$RANDOM" -S mix run --no-halt -e "Application.ensure_all_started(:next_ls)" -- "$@"

lib/next_ls.ex

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ defmodule NextLS do
4040

4141
alias NextLS.Runtime
4242
alias NextLS.DiagnosticCache
43+
alias NextLS.SymbolTable
4344

4445
def start_link(args) do
4546
{args, opts} =
@@ -48,7 +49,8 @@ defmodule NextLS do
4849
:task_supervisor,
4950
:dynamic_supervisor,
5051
:extensions,
51-
:extension_registry
52+
:extension_registry,
53+
:symbol_table
5254
])
5355

5456
GenLSP.start_link(__MODULE__, args, opts)
@@ -61,13 +63,15 @@ defmodule NextLS do
6163
extension_registry = Keyword.fetch!(args, :extension_registry)
6264
extensions = Keyword.get(args, :extensions, [NextLS.ElixirExtension])
6365
cache = Keyword.fetch!(args, :cache)
66+
symbol_table = Keyword.fetch!(args, :symbol_table)
6467

6568
{:ok,
6669
assign(lsp,
6770
exit_code: 1,
6871
documents: %{},
6972
refresh_refs: %{},
7073
cache: cache,
74+
symbol_table: symbol_table,
7175
task_supervisor: task_supervisor,
7276
dynamic_supervisor: dynamic_supervisor,
7377
extension_registry: extension_registry,
@@ -268,6 +272,11 @@ defmodule NextLS do
268272
{:noreply, lsp}
269273
end
270274

275+
def handle_info({:tracer, payload}, lsp) do
276+
SymbolTable.put_symbols(lsp.assigns.symbol_table, payload)
277+
{:noreply, lsp}
278+
end
279+
271280
def handle_info(:publish, lsp) do
272281
all =
273282
for {_namespace, cache} <- DiagnosticCache.get(lsp.assigns.cache), {file, diagnostics} <- cache, reduce: %{} do
@@ -342,7 +351,7 @@ defmodule NextLS do
342351
{:noreply, lsp}
343352
end
344353

345-
def handle_info(_, lsp) do
354+
def handle_info(_message, lsp) do
346355
{:noreply, lsp}
347356
end
348357

lib/next_ls/lsp_supervisor.ex

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,12 @@ defmodule NextLS.LSPSupervisor do
3636
{DynamicSupervisor, name: NextLS.DynamicSupervisor},
3737
{Task.Supervisor, name: NextLS.TaskSupervisor},
3838
{GenLSP.Buffer, buffer_opts},
39-
{NextLS.DiagnosticCache, [name: :diagnostic_cache]},
39+
{NextLS.DiagnosticCache, name: :diagnostic_cache},
40+
{NextLS.SymbolTable, name: :symbol_table, path: Path.expand("~/.cache/nvim/elixir-tools.nvim")},
4041
{Registry, name: NextLS.ExtensionRegistry, keys: :duplicate},
4142
{NextLS,
4243
cache: :diagnostic_cache,
44+
symbol_table: :symbol_table,
4345
task_supervisor: NextLS.TaskSupervisor,
4446
dynamic_supervisor: NextLS.DynamicSupervisor,
4547
extension_registry: NextLS.ExtensionRegistry}

lib/next_ls/runtime.ex

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ defmodule NextLS.Runtime do
4040

4141
@impl GenServer
4242
def init(opts) do
43-
sname = "nextls#{System.system_time()}"
43+
sname = "nextls-runtime-#{System.system_time()}"
4444
working_dir = Keyword.fetch!(opts, :working_dir)
4545
parent = Keyword.fetch!(opts, :parent)
4646
extension_registry = Keyword.fetch!(opts, :extension_registry)
@@ -55,6 +55,7 @@ defmodule NextLS.Runtime do
5555
:stream,
5656
cd: working_dir,
5757
env: [
58+
{'NEXTLS_PARENT_PID', :erlang.term_to_binary(parent) |> Base.encode64() |> String.to_charlist()},
5859
{'MIX_ENV', 'dev'},
5960
{'MIX_BUILD_ROOT', '.elixir-tools/_build'}
6061
],
@@ -87,6 +88,8 @@ defmodule NextLS.Runtime do
8788
|> Path.join("monkey/_next_ls_private_compiler.ex")
8889
|> then(&:rpc.call(node, Code, :compile_file, [&1]))
8990

91+
:rpc.call(node, Code, :put_compiler_option, [:parser_options, [columns: true, token_metadata: true]])
92+
9093
send(me, {:node, node})
9194
else
9295
_ -> send(me, :cancel)

lib/next_ls/symbol_table.ex

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
defmodule NextLS.SymbolTable do
2+
@moduledoc false
3+
use GenServer
4+
5+
defmodule Symbol do
6+
defstruct [:file, :module, :type, :name, :line, :col]
7+
8+
def new(args) do
9+
struct(__MODULE__, args)
10+
end
11+
end
12+
13+
def start_link(args) do
14+
GenServer.start_link(__MODULE__, Keyword.take(args, [:path]), Keyword.take(args, [:name]))
15+
end
16+
17+
@spec put_symbols(pid() | atom(), list(tuple())) :: :ok
18+
def put_symbols(server, symbols), do: GenServer.cast(server, {:put_symbols, symbols})
19+
@spec symbols(pid() | atom()) :: list(struct())
20+
def symbols(server), do: GenServer.call(server, :symbols)
21+
22+
def init(args) do
23+
path = Keyword.fetch!(args, :path)
24+
25+
{:ok, name} =
26+
:dets.open_file(:symbol_table,
27+
file: Path.join(path, "symbol_table.dets") |> String.to_charlist(),
28+
type: :duplicate_bag
29+
)
30+
31+
{:ok, %{table: name}}
32+
end
33+
34+
def handle_call(:symbols, _, state) do
35+
symbols =
36+
:dets.foldl(
37+
fn {_key, symbol}, acc -> [symbol | acc] end,
38+
[],
39+
state.table
40+
)
41+
42+
{:reply, symbols, state}
43+
end
44+
45+
def handle_cast({:put_symbols, symbols}, state) do
46+
%{
47+
module: mod,
48+
file: file,
49+
defs: defs
50+
} = symbols
51+
52+
:dets.delete(state.table, mod)
53+
54+
for {name, {:v1, type, _meta, clauses}} <- defs, {meta, _, _, _} <- clauses do
55+
:dets.insert(
56+
state.table,
57+
{mod,
58+
%Symbol{
59+
module: mod,
60+
file: file,
61+
type: type,
62+
name: name,
63+
line: meta[:line],
64+
col: meta[:column]
65+
}}
66+
)
67+
end
68+
69+
{:noreply, state}
70+
end
71+
end

priv/monkey/_next_ls_private_compiler.ex

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,24 @@
1+
defmodule NextLSPrivate.Tracer do
2+
def trace({:on_module, _, _}, env) do
3+
parent = "NEXTLS_PARENT_PID" |> System.get_env() |> Base.decode64!() |> :erlang.binary_to_term()
4+
5+
defs = Module.definitions_in(env.module)
6+
7+
defs =
8+
for {name, arity} = _def <- defs do
9+
{name, Module.get_definition(env.module, {name, arity})}
10+
end
11+
12+
Process.send(parent, {:tracer, %{file: env.file, module: env.module, defs: defs}}, [])
13+
14+
:ok
15+
end
16+
17+
def trace(_event, _env) do
18+
:ok
19+
end
20+
end
21+
122
defmodule :_next_ls_private_compiler do
223
@moduledoc false
324

@@ -15,7 +36,7 @@ defmodule :_next_ls_private_compiler do
1536
# --no-compile, so nothing was compiled, but the
1637
# task was not re-enabled it seems
1738
Mix.Task.rerun("deps.loadpaths")
18-
Mix.Task.rerun("compile", ["--no-protocol-consolidation", "--return-errors"])
39+
Mix.Task.rerun("compile", ["--no-protocol-consolidation", "--return-errors", "--tracer", "NextLSPrivate.Tracer"])
1940
rescue
2041
e -> {:error, e}
2142
end

test/next_ls/runtime_test.exs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,12 +66,18 @@ defmodule NextLs.RuntimeTest do
6666
severity: :warning,
6767
message:
6868
"variable \"arg1\" is unused (if the variable is not meant to be used, prefix it with an underscore)",
69-
position: 2,
69+
position: position,
7070
compiler_name: "Elixir",
7171
details: nil
7272
}
7373
] = Runtime.compile(pid)
7474

75+
if Version.match?(System.version(), ">= 1.15.0") do
76+
assert position == {2, 11}
77+
else
78+
assert position == 2
79+
end
80+
7581
File.write!(file, """
7682
defmodule Bar do
7783
def foo(arg1) do

test/next_ls/symbol_table_test.exs

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
defmodule NextLS.SymbolTableTest do
2+
use ExUnit.Case, async: true
3+
@moduletag :tmp_dir
4+
5+
alias NextLS.SymbolTable
6+
7+
setup %{tmp_dir: dir} do
8+
pid = start_supervised!({SymbolTable, [path: dir]})
9+
10+
Process.link(pid)
11+
[pid: pid, dir: dir]
12+
end
13+
14+
test "creates a dets table", %{dir: dir, pid: pid} do
15+
assert File.exists?(Path.join([dir, "symbol_table.dets"]))
16+
assert :sys.get_state(pid).table == :symbol_table
17+
end
18+
19+
test "builds the symbol table", %{pid: pid} do
20+
symbols = symbols()
21+
22+
SymbolTable.put_symbols(pid, symbols)
23+
24+
assert [
25+
%SymbolTable.Symbol{
26+
module: "NextLS",
27+
file: "/Users/alice/next_ls/lib/next_ls.ex",
28+
type: :def,
29+
name: :start_link,
30+
line: 45,
31+
col: nil
32+
},
33+
%SymbolTable.Symbol{
34+
module: "NextLS",
35+
file: "/Users/alice/next_ls/lib/next_ls.ex",
36+
type: :def,
37+
name: :start_link,
38+
line: 44,
39+
col: nil
40+
}
41+
] == SymbolTable.symbols(pid)
42+
end
43+
44+
defp symbols() do
45+
%{
46+
file: "/Users/alice/next_ls/lib/next_ls.ex",
47+
module: "NextLS",
48+
defs: [
49+
start_link:
50+
{:v1, :def, [line: 44],
51+
[
52+
{[line: 44], [{:args, [version: 0, line: 44, column: 18], nil}], [],
53+
{:__block__, [],
54+
[
55+
{:=,
56+
[
57+
end_of_expression: [newlines: 2, line: 52, column: 9],
58+
line: 45,
59+
column: 18
60+
],
61+
[
62+
{{:args, [version: 1, line: 45, column: 6], nil}, {:opts, [version: 2, line: 45, column: 12], nil}},
63+
{{:., [line: 46, column: 14], [Keyword, :split]},
64+
[closing: [line: 52, column: 8], line: 46, column: 15],
65+
[
66+
{:args, [version: 0, line: 46, column: 21], nil},
67+
[:cache, :task_supervisor, :dynamic_supervisor, :extensions, :extension_registry]
68+
]}
69+
]},
70+
{{:., [line: 54, column: 11], [GenLSP, :start_link]},
71+
[closing: [line: 54, column: 45], line: 54, column: 12],
72+
[
73+
NextLS,
74+
{:args, [version: 1, line: 54, column: 35], nil},
75+
{:opts, [version: 2, line: 54, column: 41], nil}
76+
]}
77+
]}},
78+
{[line: 45], [{:args, [version: 0, line: 45, column: 18], nil}], [],
79+
{:__block__, [],
80+
[
81+
{:=,
82+
[
83+
end_of_expression: [newlines: 2, line: 52, column: 9],
84+
line: 45,
85+
column: 18
86+
],
87+
[
88+
{{:args, [version: 1, line: 45, column: 6], nil}, {:opts, [version: 2, line: 45, column: 12], nil}},
89+
{{:., [line: 46, column: 14], [Keyword, :split]},
90+
[closing: [line: 52, column: 8], line: 46, column: 15],
91+
[
92+
{:args, [version: 0, line: 46, column: 21], nil},
93+
[:cache, :task_supervisor, :dynamic_supervisor, :extensions, :extension_registry]
94+
]}
95+
]},
96+
{{:., [line: 54, column: 11], [GenLSP, :start_link]},
97+
[closing: [line: 54, column: 45], line: 54, column: 12],
98+
[
99+
NextLS,
100+
{:args, [version: 1, line: 54, column: 35], nil},
101+
{:opts, [version: 2, line: 54, column: 41], nil}
102+
]}
103+
]}}
104+
]}
105+
]
106+
}
107+
end
108+
end

test/next_ls_test.exs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,16 @@ defmodule NextLSTest do
1515
start_supervised!({Registry, [keys: :unique, name: Registry.NextLSTest]})
1616
extensions = [NextLS.ElixirExtension]
1717
cache = start_supervised!(NextLS.DiagnosticCache)
18+
symbol_table = start_supervised!({NextLS.SymbolTable, [path: tmp_dir]})
1819

1920
server =
2021
server(NextLS,
2122
task_supervisor: tvisor,
2223
dynamic_supervisor: rvisor,
2324
extension_registry: Registry.NextLSTest,
2425
extensions: extensions,
25-
cache: cache
26+
cache: cache,
27+
symbol_table: symbol_table
2628
)
2729

2830
Process.link(server.lsp)
@@ -154,6 +156,8 @@ defmodule NextLSTest do
154156
path: Path.join([cwd, "lib", file])
155157
})
156158

159+
char = if Version.match?(System.version(), ">= 1.15.0"), do: 11, else: 0
160+
157161
assert_notification "textDocument/publishDiagnostics", %{
158162
"uri" => ^uri,
159163
"diagnostics" => [
@@ -163,7 +167,7 @@ defmodule NextLSTest do
163167
"message" =>
164168
"variable \"arg1\" is unused (if the variable is not meant to be used, prefix it with an underscore)",
165169
"range" => %{
166-
"start" => %{"line" => 1, "character" => 0},
170+
"start" => %{"line" => 1, "character" => ^char},
167171
"end" => %{"line" => 1, "character" => 999}
168172
}
169173
}

test/test_helper.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{:ok, _pid} = Node.start(:"nextls#{System.system_time()}", :shortnames)
22

3-
Logger.configure(level: :warn)
3+
Logger.configure(level: :warning)
44

55
timeout =
66
if System.get_env("CI", "false") == "true" do

0 commit comments

Comments
 (0)