Skip to content
This repository was archived by the owner on Oct 8, 2025. It is now read-only.
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
4 changes: 3 additions & 1 deletion bin/nextls
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
#!/usr/bin/env -S elixir --sname undefined
#!/usr/bin/env elixir

Node.start("next-ls-#{System.system_time()}", :shortnames)

System.no_halt(true)

Expand Down
2 changes: 1 addition & 1 deletion bin/start
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@

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

elixir --sname undefined -S mix run --no-halt -e "Application.ensure_all_started(:next_ls)" -- "$@"
elixir --sname "next-ls-$RANDOM" -S mix run --no-halt -e "Application.ensure_all_started(:next_ls)" -- "$@"
13 changes: 11 additions & 2 deletions lib/next_ls.ex
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ defmodule NextLS do

alias NextLS.Runtime
alias NextLS.DiagnosticCache
alias NextLS.SymbolTable

def start_link(args) do
{args, opts} =
Expand All @@ -48,7 +49,8 @@ defmodule NextLS do
:task_supervisor,
:dynamic_supervisor,
:extensions,
:extension_registry
:extension_registry,
:symbol_table
])

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

{:ok,
assign(lsp,
exit_code: 1,
documents: %{},
refresh_refs: %{},
cache: cache,
symbol_table: symbol_table,
task_supervisor: task_supervisor,
dynamic_supervisor: dynamic_supervisor,
extension_registry: extension_registry,
Expand Down Expand Up @@ -268,6 +272,11 @@ defmodule NextLS do
{:noreply, lsp}
end

def handle_info({:tracer, payload}, lsp) do
SymbolTable.put_symbols(lsp.assigns.symbol_table, payload)
{:noreply, lsp}
end

def handle_info(:publish, lsp) do
all =
for {_namespace, cache} <- DiagnosticCache.get(lsp.assigns.cache), {file, diagnostics} <- cache, reduce: %{} do
Expand Down Expand Up @@ -342,7 +351,7 @@ defmodule NextLS do
{:noreply, lsp}
end

def handle_info(_, lsp) do
def handle_info(_message, lsp) do
{:noreply, lsp}
end

Expand Down
4 changes: 3 additions & 1 deletion lib/next_ls/lsp_supervisor.ex
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,12 @@ defmodule NextLS.LSPSupervisor do
{DynamicSupervisor, name: NextLS.DynamicSupervisor},
{Task.Supervisor, name: NextLS.TaskSupervisor},
{GenLSP.Buffer, buffer_opts},
{NextLS.DiagnosticCache, [name: :diagnostic_cache]},
{NextLS.DiagnosticCache, name: :diagnostic_cache},
{NextLS.SymbolTable, name: :symbol_table, path: Path.expand("~/.cache/nvim/elixir-tools.nvim")},
{Registry, name: NextLS.ExtensionRegistry, keys: :duplicate},
{NextLS,
cache: :diagnostic_cache,
symbol_table: :symbol_table,
task_supervisor: NextLS.TaskSupervisor,
dynamic_supervisor: NextLS.DynamicSupervisor,
extension_registry: NextLS.ExtensionRegistry}
Expand Down
5 changes: 4 additions & 1 deletion lib/next_ls/runtime.ex
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ defmodule NextLS.Runtime do

@impl GenServer
def init(opts) do
sname = "nextls#{System.system_time()}"
sname = "nextls-runtime-#{System.system_time()}"
working_dir = Keyword.fetch!(opts, :working_dir)
parent = Keyword.fetch!(opts, :parent)
extension_registry = Keyword.fetch!(opts, :extension_registry)
Expand All @@ -55,6 +55,7 @@ defmodule NextLS.Runtime do
:stream,
cd: working_dir,
env: [
{'NEXTLS_PARENT_PID', :erlang.term_to_binary(parent) |> Base.encode64() |> String.to_charlist()},
{'MIX_ENV', 'dev'},
{'MIX_BUILD_ROOT', '.elixir-tools/_build'}
],
Expand Down Expand Up @@ -87,6 +88,8 @@ defmodule NextLS.Runtime do
|> Path.join("monkey/_next_ls_private_compiler.ex")
|> then(&:rpc.call(node, Code, :compile_file, [&1]))

:rpc.call(node, Code, :put_compiler_option, [:parser_options, [columns: true, token_metadata: true]])

send(me, {:node, node})
else
_ -> send(me, :cancel)
Expand Down
71 changes: 71 additions & 0 deletions lib/next_ls/symbol_table.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
defmodule NextLS.SymbolTable do
@moduledoc false
use GenServer

defmodule Symbol do
defstruct [:file, :module, :type, :name, :line, :col]

def new(args) do
struct(__MODULE__, args)
end
end

def start_link(args) do
GenServer.start_link(__MODULE__, Keyword.take(args, [:path]), Keyword.take(args, [:name]))
end

@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 init(args) do
path = Keyword.fetch!(args, :path)

{:ok, name} =
:dets.open_file(:symbol_table,
file: Path.join(path, "symbol_table.dets") |> String.to_charlist(),
type: :duplicate_bag
)

{:ok, %{table: name}}
end

def handle_call(:symbols, _, state) do
symbols =
:dets.foldl(
fn {_key, symbol}, acc -> [symbol | acc] end,
[],
state.table
)

{:reply, symbols, state}
end

def handle_cast({:put_symbols, symbols}, state) do
%{
module: mod,
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: type,
name: name,
line: meta[:line],
col: meta[:column]
}}
)
end

{:noreply, state}
end
end
23 changes: 22 additions & 1 deletion priv/monkey/_next_ls_private_compiler.ex
Original file line number Diff line number Diff line change
@@ -1,3 +1,24 @@
defmodule NextLSPrivate.Tracer do
def trace({:on_module, _, _}, env) do
parent = "NEXTLS_PARENT_PID" |> System.get_env() |> Base.decode64!() |> :erlang.binary_to_term()

defs = Module.definitions_in(env.module)

defs =
for {name, arity} = _def <- defs do
{name, Module.get_definition(env.module, {name, arity})}
end

Process.send(parent, {:tracer, %{file: env.file, module: env.module, defs: defs}}, [])

:ok
end

def trace(_event, _env) do
:ok
end
end

defmodule :_next_ls_private_compiler do
@moduledoc false

Expand All @@ -15,7 +36,7 @@ defmodule :_next_ls_private_compiler do
# --no-compile, so nothing was compiled, but the
# task was not re-enabled it seems
Mix.Task.rerun("deps.loadpaths")
Mix.Task.rerun("compile", ["--no-protocol-consolidation", "--return-errors"])
Mix.Task.rerun("compile", ["--no-protocol-consolidation", "--return-errors", "--tracer", "NextLSPrivate.Tracer"])
rescue
e -> {:error, e}
end
Expand Down
8 changes: 7 additions & 1 deletion test/next_ls/runtime_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,18 @@ 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: position,
compiler_name: "Elixir",
details: nil
}
] = Runtime.compile(pid)

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

File.write!(file, """
defmodule Bar do
def foo(arg1) do
Expand Down
108 changes: 108 additions & 0 deletions test/next_ls/symbol_table_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
defmodule NextLS.SymbolTableTest do
use ExUnit.Case, async: true
@moduletag :tmp_dir

alias NextLS.SymbolTable

setup %{tmp_dir: dir} do
pid = start_supervised!({SymbolTable, [path: dir]})

Process.link(pid)
[pid: pid, dir: dir]
end

test "creates a dets table", %{dir: dir, pid: pid} do
assert File.exists?(Path.join([dir, "symbol_table.dets"]))
assert :sys.get_state(pid).table == :symbol_table
end

test "builds the symbol table", %{pid: pid} do
symbols = symbols()

SymbolTable.put_symbols(pid, symbols)

assert [
%SymbolTable.Symbol{
module: "NextLS",
file: "/Users/alice/next_ls/lib/next_ls.ex",
type: :def,
name: :start_link,
line: 45,
col: nil
},
%SymbolTable.Symbol{
module: "NextLS",
file: "/Users/alice/next_ls/lib/next_ls.ex",
type: :def,
name: :start_link,
line: 44,
col: nil
}
] == SymbolTable.symbols(pid)
end

defp symbols() do
%{
file: "/Users/alice/next_ls/lib/next_ls.ex",
module: "NextLS",
defs: [
start_link:
{:v1, :def, [line: 44],
[
{[line: 44], [{:args, [version: 0, line: 44, column: 18], nil}], [],
{:__block__, [],
[
{:=,
[
end_of_expression: [newlines: 2, line: 52, column: 9],
line: 45,
column: 18
],
[
{{:args, [version: 1, line: 45, column: 6], nil}, {:opts, [version: 2, line: 45, column: 12], nil}},
{{:., [line: 46, column: 14], [Keyword, :split]},
[closing: [line: 52, column: 8], line: 46, column: 15],
[
{:args, [version: 0, line: 46, column: 21], nil},
[:cache, :task_supervisor, :dynamic_supervisor, :extensions, :extension_registry]
]}
]},
{{:., [line: 54, column: 11], [GenLSP, :start_link]},
[closing: [line: 54, column: 45], line: 54, column: 12],
[
NextLS,
{:args, [version: 1, line: 54, column: 35], nil},
{:opts, [version: 2, line: 54, column: 41], nil}
]}
]}},
{[line: 45], [{:args, [version: 0, line: 45, column: 18], nil}], [],
{:__block__, [],
[
{:=,
[
end_of_expression: [newlines: 2, line: 52, column: 9],
line: 45,
column: 18
],
[
{{:args, [version: 1, line: 45, column: 6], nil}, {:opts, [version: 2, line: 45, column: 12], nil}},
{{:., [line: 46, column: 14], [Keyword, :split]},
[closing: [line: 52, column: 8], line: 46, column: 15],
[
{:args, [version: 0, line: 46, column: 21], nil},
[:cache, :task_supervisor, :dynamic_supervisor, :extensions, :extension_registry]
]}
]},
{{:., [line: 54, column: 11], [GenLSP, :start_link]},
[closing: [line: 54, column: 45], line: 54, column: 12],
[
NextLS,
{:args, [version: 1, line: 54, column: 35], nil},
{:opts, [version: 2, line: 54, column: 41], nil}
]}
]}}
]}
]
}
end
end
8 changes: 6 additions & 2 deletions test/next_ls_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,16 @@ 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]})

server =
server(NextLS,
task_supervisor: tvisor,
dynamic_supervisor: rvisor,
extension_registry: Registry.NextLSTest,
extensions: extensions,
cache: cache
cache: cache,
symbol_table: symbol_table
)

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

char = if Version.match?(System.version(), ">= 1.15.0"), do: 11, else: 0

assert_notification "textDocument/publishDiagnostics", %{
"uri" => ^uri,
"diagnostics" => [
Expand All @@ -163,7 +167,7 @@ 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},
"start" => %{"line" => 1, "character" => ^char},
"end" => %{"line" => 1, "character" => 999}
}
}
Expand Down
2 changes: 1 addition & 1 deletion test/test_helper.exs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{:ok, _pid} = Node.start(:"nextls#{System.system_time()}", :shortnames)

Logger.configure(level: :warn)
Logger.configure(level: :warning)

timeout =
if System.get_env("CI", "false") == "true" do
Expand Down