Skip to content

Commit

Permalink
fix: swap out dets for sqlite3 (#131)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
mhanberg authored Jul 30, 2023
1 parent 7dc6629 commit 422df17
Show file tree
Hide file tree
Showing 15 changed files with 356 additions and 406 deletions.
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 "next-ls-$RANDOM" -S mix run --no-halt -e "Application.ensure_all_started(:next_ls)" -- "$@"
iex --sname "next-ls-$RANDOM" -S mix run --no-halt -e "Application.ensure_all_started(:next_ls)" -- "$@"
64 changes: 36 additions & 28 deletions lib/next_ls.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ defmodule NextLS do
@moduledoc false
use GenLSP

import NextLS.DB.Query

alias GenLSP.Enumerations.ErrorCodes
alias GenLSP.Enumerations.TextDocumentSyncKind
alias GenLSP.ErrorResponse
Expand Down Expand Up @@ -33,11 +35,11 @@ defmodule NextLS do
alias GenLSP.Structures.TextDocumentSyncOptions
alias GenLSP.Structures.TextEdit
alias GenLSP.Structures.WorkspaceFoldersChangeEvent
alias NextLS.DB
alias NextLS.Definition
alias NextLS.DiagnosticCache
alias NextLS.Progress
alias NextLS.Runtime
alias NextLS.SymbolTable

def start_link(args) do
{args, opts} =
Expand Down Expand Up @@ -120,21 +122,16 @@ defmodule NextLS do

def handle_request(%TextDocumentDefinition{params: %{text_document: %{uri: uri}, position: position}}, lsp) do
result =
dispatch(lsp.assigns.registry, :symbol_tables, fn entries ->
for {_, %{symbol_table: symbol_table, reference_table: ref_table}} <- entries do
case Definition.fetch(
URI.parse(uri).path,
{position.line + 1, position.character + 1},
symbol_table,
ref_table
) do
dispatch(lsp.assigns.registry, :databases, fn entries ->
for {pid, _} <- entries do
case Definition.fetch(URI.parse(uri).path, {position.line + 1, position.character + 1}, pid) do
nil ->
nil

[] ->
nil

[{file, line, column} | _] ->
[[_pk, _mod, file, _type, _name, line, column] | _] ->
%Location{
uri: "file://#{file}",
range: %Range{
Expand Down Expand Up @@ -184,10 +181,10 @@ defmodule NextLS do
end

symbols =
dispatch(lsp.assigns.registry, :symbol_tables, fn entries ->
for {pid, _} <- entries, %SymbolTable.Symbol{} = symbol <- SymbolTable.symbols(pid), filter.(symbol.name) do
dispatch(lsp.assigns.registry, :databases, fn entries ->
for {pid, _} <- entries, symbol <- DB.symbols(pid), filter.(symbol.name) do
name =
if symbol.type != :defstruct do
if symbol.type != "defstruct" do
"#{symbol.type} #{symbol.name}"
else
"#{symbol.name}"
Expand All @@ -201,11 +198,11 @@ defmodule NextLS do
range: %Range{
start: %Position{
line: symbol.line - 1,
character: symbol.col - 1
character: symbol.column - 1
},
end: %Position{
line: symbol.line - 1,
character: symbol.col - 1
character: symbol.column - 1
}
}
}
Expand Down Expand Up @@ -260,10 +257,6 @@ defmodule NextLS do
end

def handle_request(%Shutdown{}, lsp) do
dispatch(lsp.assigns.registry, :symbol_tables, fn entries ->
for {pid, _} <- entries, do: SymbolTable.close(pid)
end)

{:reply, nil, assign(lsp, exit_code: 0)}
end

Expand Down Expand Up @@ -323,6 +316,7 @@ defmodule NextLS do
path: Path.join(working_dir, ".elixir-tools"),
name: name,
registry: lsp.assigns.registry,
logger: lsp.assigns.logger,
runtime: [
task_supervisor: lsp.assigns.runtime_task_supervisor,
working_dir: working_dir,
Expand Down Expand Up @@ -463,16 +457,30 @@ defmodule NextLS do
def handle_notification(%WorkspaceDidChangeWatchedFiles{params: %DidChangeWatchedFilesParams{changes: changes}}, lsp) do
type = GenLSP.Enumerations.FileChangeType.deleted()

# TODO
# ✅ delete from documents
# ✅ delete all references that occur in this file
# ✅ delete all symbols from that file
lsp =
for %{type: ^type, uri: uri} <- changes, reduce: lsp do
lsp ->
dispatch(lsp.assigns.registry, :symbol_tables, fn entries ->
dispatch(lsp.assigns.registry, :databases, fn entries ->
for {pid, _} <- entries do
SymbolTable.remove(pid, uri)
file = URI.parse(uri).path

NextLS.DB.query(
pid,
~Q"""
DELETE FROM symbols
WHERE symbols.file = ?;
""",
[file]
)

NextLS.DB.query(
pid,
~Q"""
DELETE FROM 'references' AS refs
WHERE refs.file = ?;
""",
[file]
)
end
end)

Expand Down Expand Up @@ -563,10 +571,10 @@ defmodule NextLS do
end
end

defp elixir_kind_to_lsp_kind(:defmodule), do: GenLSP.Enumerations.SymbolKind.module()
defp elixir_kind_to_lsp_kind(:defstruct), do: GenLSP.Enumerations.SymbolKind.struct()
defp elixir_kind_to_lsp_kind("defmodule"), do: GenLSP.Enumerations.SymbolKind.module()
defp elixir_kind_to_lsp_kind("defstruct"), do: GenLSP.Enumerations.SymbolKind.struct()

defp elixir_kind_to_lsp_kind(kind) when kind in [:def, :defp, :defmacro, :defmacrop],
defp elixir_kind_to_lsp_kind(kind) when kind in ["def", "defp", "defmacro", "defmacrop"],
do: GenLSP.Enumerations.SymbolKind.function()

# NOTE: this is only possible because the registry is not partitioned
Expand Down
193 changes: 193 additions & 0 deletions lib/next_ls/db.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
defmodule NextLS.DB do
@moduledoc false
use GenServer

import __MODULE__.Query

@type query :: String.t()

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

@spec query(pid(), query(), list()) :: list()
def query(server, query, args \\ []), do: GenServer.call(server, {:query, query, args})

@spec symbols(pid()) :: list(map())
def symbols(server), do: GenServer.call(server, :symbols)

@spec insert_symbol(pid(), map()) :: :ok
def insert_symbol(server, payload), do: GenServer.cast(server, {:insert_symbol, payload})

@spec insert_reference(pid(), map()) :: :ok
def insert_reference(server, payload), do: GenServer.cast(server, {:insert_reference, payload})

def init(args) do
file = Keyword.fetch!(args, :file)
registry = Keyword.fetch!(args, :registry)
logger = Keyword.fetch!(args, :logger)
Registry.register(registry, :databases, %{})
{:ok, conn} = :esqlite3.open(file)

NextLS.DB.Schema.init({conn, logger})

{:ok,
%{
conn: conn,
file: file,
logger: logger
}}
end

def handle_call({:query, query, args}, _from, %{conn: conn} = s) do
rows = __query__({conn, s.logger}, query, args)

{:reply, rows, s}
end

def handle_call(:symbols, _from, %{conn: conn} = s) do
rows =
__query__(
{conn, s.logger},
~Q"""
SELECT
*
FROM
symbols;
""",
[]
)

symbols =
for [_pk, module, file, type, name, line, column] <- rows do
%{
module: module,
file: file,
type: type,
name: name,
line: line,
column: column
}
end

{:reply, symbols, s}
end

def handle_cast({:insert_symbol, symbol}, %{conn: conn} = s) do
%{
module: mod,
module_line: module_line,
struct: struct,
file: file,
defs: defs
} = symbol

__query__(
{conn, s.logger},
~Q"""
DELETE FROM symbols
WHERE module = ?;
""",
[mod]
)

__query__(
{conn, s.logger},
~Q"""
INSERT INTO symbols (module, file, type, name, line, 'column')
VALUES (?, ?, ?, ?, ?, ?);
""",
[mod, file, "defmodule", mod, module_line, 1]
)

if struct do
{_, _, meta, _} = defs[:__struct__]

__query__(
{conn, s.logger},
~Q"""
INSERT INTO symbols (module, file, type, name, line, 'column')
VALUES (?, ?, ?, ?, ?, ?);
""",
[mod, file, "defstruct", "%#{Macro.to_string(mod)}{}", meta[:line], 1]
)
end

for {name, {:v1, type, _meta, clauses}} <- defs, {meta, _, _, _} <- clauses do
__query__(
{conn, s.logger},
~Q"""
INSERT INTO symbols (module, file, type, name, line, 'column')
VALUES (?, ?, ?, ?, ?, ?);
""",
[mod, file, type, name, meta[:line], meta[:column] || 1]
)
end

{:noreply, s}
end

def handle_cast({:insert_reference, reference}, %{conn: conn} = s) do
%{
meta: meta,
identifier: identifier,
file: file,
type: type,
module: module
} = reference

col = meta[:column] || 0

{{start_line, start_column}, {end_line, end_column}} =
{{meta[:line], col},
{meta[:line], col + String.length(identifier |> to_string() |> String.replace("Elixir.", ""))}}

__query__(
{conn, s.logger},
~Q"""
DELETE FROM 'references' AS refs
WHERE refs.file = ?
AND (? <= refs.start_line
AND refs.start_line <= ?
AND ? <= refs.start_column
AND refs.start_column <= ?)
OR (? <= refs.end_line
AND refs.end_line <= ?
AND ? <= refs.end_column
AND refs.end_column <= ?);
""",
[file, start_line, end_line, start_column, end_column, start_line, end_line, start_column, end_column]
)

__query__(
{conn, s.logger},
~Q"""
INSERT INTO 'references' (identifier, arity, file, type, module, start_line, start_column, end_line, end_column)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);
""",
[identifier, reference[:arity], file, type, module, start_line, start_column, end_line, end_column]
)

{:noreply, s}
end

def __query__({conn, logger}, query, args) do
args = Enum.map(args, &cast/1)

with {:error, _e} <- :esqlite3.q(conn, query, cast(args)) do
error = :esqlite3.error_info(conn).errmsg
NextLS.Logger.error(logger, error)
{:error, error}
end
end

defp cast(arg) do
cond do
is_atom(arg) and String.starts_with?(to_string(arg), "Elixir.") ->
Macro.to_string(arg)

true ->
arg
end
end
end
18 changes: 18 additions & 0 deletions lib/next_ls/db/format.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
defmodule NextLS.DB.Format do
@moduledoc false
# @behaviour Mix.Tasks.Format

# @impl Mix.Tasks.Format
# def features(_opts), do: [sigils: [:Q], extensions: []]

# @impl Mix.Tasks.Format
# def format(input, _formatter_opts, _opts \\ []) do
# path = Path.join(System.tmp_dir!(), "#{System.unique_integer()}-temp.sql")
# File.write!(path, input)
# {result, 0} = System.cmd("pg_format", [path])

# File.rm!(path)

# String.trim(result) <> "\n"
# end
end
6 changes: 6 additions & 0 deletions lib/next_ls/db/query.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
defmodule NextLS.DB.Query do
@moduledoc false
defmacro sigil_Q({:<<>>, _, [bin]}, _mods) do
bin
end
end
Loading

0 comments on commit 422df17

Please sign in to comment.