Skip to content
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

feat: basic symbol table #30

Merged
merged 4 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
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
@@ -1,6 +1,6 @@
defmodule NextLS do
@moduledoc false
use GenLSP

Check warning on line 3 in lib/next_ls.ex

View workflow job for this annotation

GitHub Actions / Test (1.15.x/24.x)

Logger.warn/1 is deprecated. Use Logger.warning/2 instead

Check warning on line 3 in lib/next_ls.ex

View workflow job for this annotation

GitHub Actions / Test (1.15.x/24.x)

Logger.warn/1 is deprecated. Use Logger.warning/2 instead

Check warning on line 3 in lib/next_ls.ex

View workflow job for this annotation

GitHub Actions / Test (1.15.x/25.x)

Logger.warn/1 is deprecated. Use Logger.warning/2 instead

Check warning on line 3 in lib/next_ls.ex

View workflow job for this annotation

GitHub Actions / Test (1.15.x/25.x)

Logger.warn/1 is deprecated. Use Logger.warning/2 instead

alias GenLSP.ErrorResponse

Expand Down Expand Up @@ -40,6 +40,7 @@

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

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

GenLSP.start_link(__MODULE__, args, opts)
Expand All @@ -61,13 +63,15 @@
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 @@
{: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 @@
{: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
Loading