Skip to content

Commit 422df17

Browse files
authored
fix: swap out dets for sqlite3 (#131)
Also fixes a bug with go to definition. it turns out that multiple references were being stored that overlapped in range, so we'd get multiple references, which didn't match the pattern, so it returned nil. Now, sqlite easily deletes any references that overlap with the one we are inserting in that moment.
1 parent 7dc6629 commit 422df17

15 files changed

+356
-406
lines changed

bin/start

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@
44

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

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

lib/next_ls.ex

+36-28
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ defmodule NextLS do
22
@moduledoc false
33
use GenLSP
44

5+
import NextLS.DB.Query
6+
57
alias GenLSP.Enumerations.ErrorCodes
68
alias GenLSP.Enumerations.TextDocumentSyncKind
79
alias GenLSP.ErrorResponse
@@ -33,11 +35,11 @@ defmodule NextLS do
3335
alias GenLSP.Structures.TextDocumentSyncOptions
3436
alias GenLSP.Structures.TextEdit
3537
alias GenLSP.Structures.WorkspaceFoldersChangeEvent
38+
alias NextLS.DB
3639
alias NextLS.Definition
3740
alias NextLS.DiagnosticCache
3841
alias NextLS.Progress
3942
alias NextLS.Runtime
40-
alias NextLS.SymbolTable
4143

4244
def start_link(args) do
4345
{args, opts} =
@@ -120,21 +122,16 @@ defmodule NextLS do
120122

121123
def handle_request(%TextDocumentDefinition{params: %{text_document: %{uri: uri}, position: position}}, lsp) do
122124
result =
123-
dispatch(lsp.assigns.registry, :symbol_tables, fn entries ->
124-
for {_, %{symbol_table: symbol_table, reference_table: ref_table}} <- entries do
125-
case Definition.fetch(
126-
URI.parse(uri).path,
127-
{position.line + 1, position.character + 1},
128-
symbol_table,
129-
ref_table
130-
) do
125+
dispatch(lsp.assigns.registry, :databases, fn entries ->
126+
for {pid, _} <- entries do
127+
case Definition.fetch(URI.parse(uri).path, {position.line + 1, position.character + 1}, pid) do
131128
nil ->
132129
nil
133130

134131
[] ->
135132
nil
136133

137-
[{file, line, column} | _] ->
134+
[[_pk, _mod, file, _type, _name, line, column] | _] ->
138135
%Location{
139136
uri: "file://#{file}",
140137
range: %Range{
@@ -184,10 +181,10 @@ defmodule NextLS do
184181
end
185182

186183
symbols =
187-
dispatch(lsp.assigns.registry, :symbol_tables, fn entries ->
188-
for {pid, _} <- entries, %SymbolTable.Symbol{} = symbol <- SymbolTable.symbols(pid), filter.(symbol.name) do
184+
dispatch(lsp.assigns.registry, :databases, fn entries ->
185+
for {pid, _} <- entries, symbol <- DB.symbols(pid), filter.(symbol.name) do
189186
name =
190-
if symbol.type != :defstruct do
187+
if symbol.type != "defstruct" do
191188
"#{symbol.type} #{symbol.name}"
192189
else
193190
"#{symbol.name}"
@@ -201,11 +198,11 @@ defmodule NextLS do
201198
range: %Range{
202199
start: %Position{
203200
line: symbol.line - 1,
204-
character: symbol.col - 1
201+
character: symbol.column - 1
205202
},
206203
end: %Position{
207204
line: symbol.line - 1,
208-
character: symbol.col - 1
205+
character: symbol.column - 1
209206
}
210207
}
211208
}
@@ -260,10 +257,6 @@ defmodule NextLS do
260257
end
261258

262259
def handle_request(%Shutdown{}, lsp) do
263-
dispatch(lsp.assigns.registry, :symbol_tables, fn entries ->
264-
for {pid, _} <- entries, do: SymbolTable.close(pid)
265-
end)
266-
267260
{:reply, nil, assign(lsp, exit_code: 0)}
268261
end
269262

@@ -323,6 +316,7 @@ defmodule NextLS do
323316
path: Path.join(working_dir, ".elixir-tools"),
324317
name: name,
325318
registry: lsp.assigns.registry,
319+
logger: lsp.assigns.logger,
326320
runtime: [
327321
task_supervisor: lsp.assigns.runtime_task_supervisor,
328322
working_dir: working_dir,
@@ -463,16 +457,30 @@ defmodule NextLS do
463457
def handle_notification(%WorkspaceDidChangeWatchedFiles{params: %DidChangeWatchedFilesParams{changes: changes}}, lsp) do
464458
type = GenLSP.Enumerations.FileChangeType.deleted()
465459

466-
# TODO
467-
# ✅ delete from documents
468-
# ✅ delete all references that occur in this file
469-
# ✅ delete all symbols from that file
470460
lsp =
471461
for %{type: ^type, uri: uri} <- changes, reduce: lsp do
472462
lsp ->
473-
dispatch(lsp.assigns.registry, :symbol_tables, fn entries ->
463+
dispatch(lsp.assigns.registry, :databases, fn entries ->
474464
for {pid, _} <- entries do
475-
SymbolTable.remove(pid, uri)
465+
file = URI.parse(uri).path
466+
467+
NextLS.DB.query(
468+
pid,
469+
~Q"""
470+
DELETE FROM symbols
471+
WHERE symbols.file = ?;
472+
""",
473+
[file]
474+
)
475+
476+
NextLS.DB.query(
477+
pid,
478+
~Q"""
479+
DELETE FROM 'references' AS refs
480+
WHERE refs.file = ?;
481+
""",
482+
[file]
483+
)
476484
end
477485
end)
478486

@@ -563,10 +571,10 @@ defmodule NextLS do
563571
end
564572
end
565573

566-
defp elixir_kind_to_lsp_kind(:defmodule), do: GenLSP.Enumerations.SymbolKind.module()
567-
defp elixir_kind_to_lsp_kind(:defstruct), do: GenLSP.Enumerations.SymbolKind.struct()
574+
defp elixir_kind_to_lsp_kind("defmodule"), do: GenLSP.Enumerations.SymbolKind.module()
575+
defp elixir_kind_to_lsp_kind("defstruct"), do: GenLSP.Enumerations.SymbolKind.struct()
568576

569-
defp elixir_kind_to_lsp_kind(kind) when kind in [:def, :defp, :defmacro, :defmacrop],
577+
defp elixir_kind_to_lsp_kind(kind) when kind in ["def", "defp", "defmacro", "defmacrop"],
570578
do: GenLSP.Enumerations.SymbolKind.function()
571579

572580
# NOTE: this is only possible because the registry is not partitioned

lib/next_ls/db.ex

+193
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
defmodule NextLS.DB do
2+
@moduledoc false
3+
use GenServer
4+
5+
import __MODULE__.Query
6+
7+
@type query :: String.t()
8+
9+
def start_link(args) do
10+
GenServer.start_link(__MODULE__, args, Keyword.take(args, [:name]))
11+
end
12+
13+
@spec query(pid(), query(), list()) :: list()
14+
def query(server, query, args \\ []), do: GenServer.call(server, {:query, query, args})
15+
16+
@spec symbols(pid()) :: list(map())
17+
def symbols(server), do: GenServer.call(server, :symbols)
18+
19+
@spec insert_symbol(pid(), map()) :: :ok
20+
def insert_symbol(server, payload), do: GenServer.cast(server, {:insert_symbol, payload})
21+
22+
@spec insert_reference(pid(), map()) :: :ok
23+
def insert_reference(server, payload), do: GenServer.cast(server, {:insert_reference, payload})
24+
25+
def init(args) do
26+
file = Keyword.fetch!(args, :file)
27+
registry = Keyword.fetch!(args, :registry)
28+
logger = Keyword.fetch!(args, :logger)
29+
Registry.register(registry, :databases, %{})
30+
{:ok, conn} = :esqlite3.open(file)
31+
32+
NextLS.DB.Schema.init({conn, logger})
33+
34+
{:ok,
35+
%{
36+
conn: conn,
37+
file: file,
38+
logger: logger
39+
}}
40+
end
41+
42+
def handle_call({:query, query, args}, _from, %{conn: conn} = s) do
43+
rows = __query__({conn, s.logger}, query, args)
44+
45+
{:reply, rows, s}
46+
end
47+
48+
def handle_call(:symbols, _from, %{conn: conn} = s) do
49+
rows =
50+
__query__(
51+
{conn, s.logger},
52+
~Q"""
53+
SELECT
54+
*
55+
FROM
56+
symbols;
57+
""",
58+
[]
59+
)
60+
61+
symbols =
62+
for [_pk, module, file, type, name, line, column] <- rows do
63+
%{
64+
module: module,
65+
file: file,
66+
type: type,
67+
name: name,
68+
line: line,
69+
column: column
70+
}
71+
end
72+
73+
{:reply, symbols, s}
74+
end
75+
76+
def handle_cast({:insert_symbol, symbol}, %{conn: conn} = s) do
77+
%{
78+
module: mod,
79+
module_line: module_line,
80+
struct: struct,
81+
file: file,
82+
defs: defs
83+
} = symbol
84+
85+
__query__(
86+
{conn, s.logger},
87+
~Q"""
88+
DELETE FROM symbols
89+
WHERE module = ?;
90+
""",
91+
[mod]
92+
)
93+
94+
__query__(
95+
{conn, s.logger},
96+
~Q"""
97+
INSERT INTO symbols (module, file, type, name, line, 'column')
98+
VALUES (?, ?, ?, ?, ?, ?);
99+
""",
100+
[mod, file, "defmodule", mod, module_line, 1]
101+
)
102+
103+
if struct do
104+
{_, _, meta, _} = defs[:__struct__]
105+
106+
__query__(
107+
{conn, s.logger},
108+
~Q"""
109+
INSERT INTO symbols (module, file, type, name, line, 'column')
110+
VALUES (?, ?, ?, ?, ?, ?);
111+
""",
112+
[mod, file, "defstruct", "%#{Macro.to_string(mod)}{}", meta[:line], 1]
113+
)
114+
end
115+
116+
for {name, {:v1, type, _meta, clauses}} <- defs, {meta, _, _, _} <- clauses do
117+
__query__(
118+
{conn, s.logger},
119+
~Q"""
120+
INSERT INTO symbols (module, file, type, name, line, 'column')
121+
VALUES (?, ?, ?, ?, ?, ?);
122+
""",
123+
[mod, file, type, name, meta[:line], meta[:column] || 1]
124+
)
125+
end
126+
127+
{:noreply, s}
128+
end
129+
130+
def handle_cast({:insert_reference, reference}, %{conn: conn} = s) do
131+
%{
132+
meta: meta,
133+
identifier: identifier,
134+
file: file,
135+
type: type,
136+
module: module
137+
} = reference
138+
139+
col = meta[:column] || 0
140+
141+
{{start_line, start_column}, {end_line, end_column}} =
142+
{{meta[:line], col},
143+
{meta[:line], col + String.length(identifier |> to_string() |> String.replace("Elixir.", ""))}}
144+
145+
__query__(
146+
{conn, s.logger},
147+
~Q"""
148+
DELETE FROM 'references' AS refs
149+
WHERE refs.file = ?
150+
AND (? <= refs.start_line
151+
AND refs.start_line <= ?
152+
AND ? <= refs.start_column
153+
AND refs.start_column <= ?)
154+
OR (? <= refs.end_line
155+
AND refs.end_line <= ?
156+
AND ? <= refs.end_column
157+
AND refs.end_column <= ?);
158+
""",
159+
[file, start_line, end_line, start_column, end_column, start_line, end_line, start_column, end_column]
160+
)
161+
162+
__query__(
163+
{conn, s.logger},
164+
~Q"""
165+
INSERT INTO 'references' (identifier, arity, file, type, module, start_line, start_column, end_line, end_column)
166+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);
167+
""",
168+
[identifier, reference[:arity], file, type, module, start_line, start_column, end_line, end_column]
169+
)
170+
171+
{:noreply, s}
172+
end
173+
174+
def __query__({conn, logger}, query, args) do
175+
args = Enum.map(args, &cast/1)
176+
177+
with {:error, _e} <- :esqlite3.q(conn, query, cast(args)) do
178+
error = :esqlite3.error_info(conn).errmsg
179+
NextLS.Logger.error(logger, error)
180+
{:error, error}
181+
end
182+
end
183+
184+
defp cast(arg) do
185+
cond do
186+
is_atom(arg) and String.starts_with?(to_string(arg), "Elixir.") ->
187+
Macro.to_string(arg)
188+
189+
true ->
190+
arg
191+
end
192+
end
193+
end

lib/next_ls/db/format.ex

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
defmodule NextLS.DB.Format do
2+
@moduledoc false
3+
# @behaviour Mix.Tasks.Format
4+
5+
# @impl Mix.Tasks.Format
6+
# def features(_opts), do: [sigils: [:Q], extensions: []]
7+
8+
# @impl Mix.Tasks.Format
9+
# def format(input, _formatter_opts, _opts \\ []) do
10+
# path = Path.join(System.tmp_dir!(), "#{System.unique_integer()}-temp.sql")
11+
# File.write!(path, input)
12+
# {result, 0} = System.cmd("pg_format", [path])
13+
14+
# File.rm!(path)
15+
16+
# String.trim(result) <> "\n"
17+
# end
18+
end

lib/next_ls/db/query.ex

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
defmodule NextLS.DB.Query do
2+
@moduledoc false
3+
defmacro sigil_Q({:<<>>, _, [bin]}, _mods) do
4+
bin
5+
end
6+
end

0 commit comments

Comments
 (0)