Skip to content

Commit

Permalink
feat: document symbols
Browse files Browse the repository at this point in the history
Closes #41
  • Loading branch information
mhanberg committed Jun 29, 2023
1 parent 80d0679 commit bb63928
Show file tree
Hide file tree
Showing 5 changed files with 660 additions and 21 deletions.
32 changes: 23 additions & 9 deletions lib/next_ls.ex
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ defmodule NextLS do
alias GenLSP.Requests.{
Initialize,
Shutdown,
TextDocumentDocumentSymbol,
TextDocumentFormatting,
WorkspaceSymbol
}
Expand All @@ -28,21 +29,21 @@ defmodule NextLS do
DidOpenTextDocumentParams,
InitializeParams,
InitializeResult,
Location,
Position,
Range,
Location,
SaveOptions,
ServerCapabilities,
SymbolInformation,
TextDocumentItem,
TextDocumentSyncOptions,
TextEdit,
WorkDoneProgressBegin,
WorkDoneProgressEnd,
SymbolInformation
WorkDoneProgressEnd
}

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

def start_link(args) do
Expand Down Expand Up @@ -85,10 +86,7 @@ defmodule NextLS do
end

@impl true
def handle_request(
%Initialize{params: %InitializeParams{root_uri: root_uri}},
lsp
) do
def handle_request(%Initialize{params: %InitializeParams{root_uri: root_uri}}, lsp) do
{:reply,
%InitializeResult{
capabilities: %ServerCapabilities{
Expand All @@ -98,12 +96,28 @@ defmodule NextLS do
change: TextDocumentSyncKind.full()
},
document_formatting_provider: true,
workspace_symbol_provider: true
workspace_symbol_provider: true,
document_symbol_provider: true
},
server_info: %{name: "NextLS"}
}, assign(lsp, root_uri: root_uri)}
end

def handle_request(%TextDocumentDocumentSymbol{params: %{text_document: %{uri: uri}}}, lsp) do
symbols =
try do
lsp.assigns.documents[uri]
|> Enum.join("\n")
|> NextLS.DocumentSymbol.fetch()
rescue
e ->
GenLSP.error(lsp, Exception.format_banner(:error, e, __STACKTRACE__))
nil
end

{:reply, symbols, lsp}
end

def handle_request(%WorkspaceSymbol{params: %{query: query}}, lsp) do
filter = fn sym ->
if query == "" do
Expand Down
223 changes: 223 additions & 0 deletions lib/next_ls/document_symbol.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
defmodule NextLS.DocumentSymbol do
alias GenLSP.Structures.{
Position,
Range,
DocumentSymbol
}

# we set the literal encoder so that we can know when atoms and strings start and end
# this makes it useful for knowing the exact locations of struct field definitions
@spec fetch(text :: String.t()) :: list(DocumentSymbol.t())
def fetch(text) do
text
|> Code.string_to_quoted!(
literal_encoder: fn literal, meta ->
if is_atom(literal) or is_binary(literal) do
{:ok, {:__literal__, meta, [literal]}}
else
{:ok, literal}
end
end,
unescape: false,
token_metadata: true,
columns: true
)
|> walker(nil)
|> List.wrap()
end

defp walker([{{:__literal__, _, [:do]}, {_, _, _exprs} = ast}], mod) do
walker(ast, mod)
end

defp walker({:__block__, _, exprs}, mod) do
for expr <- exprs, sym = walker(expr, mod), sym != nil do
sym
end
end

defp walker({:defmodule, meta, [name | children]}, _mod) do
name = Macro.to_string(unliteral(name))

%DocumentSymbol{
name: name,
kind: GenLSP.Enumerations.SymbolKind.module(),
children: List.flatten(for(child <- children, sym = walker(child, name), sym != nil, do: sym)),
range: %Range{
start: %Position{line: meta[:line] - 1, character: meta[:column] - 1},
end: %Position{line: meta[:end][:line] - 1, character: meta[:end][:column] - 1}
},
selection_range: %Range{
start: %Position{line: meta[:line] - 1, character: meta[:column] - 1},
end: %Position{line: meta[:line] - 2, character: meta[:column] - 1}
}
}
end

defp walker({:describe, meta, [name | children]}, mod) do
name = ("describe " <> Macro.to_string(unliteral(name))) |> String.replace("\n", "")

%DocumentSymbol{
name: name,
kind: GenLSP.Enumerations.SymbolKind.class(),
children: List.flatten(for(child <- children, sym = walker(child, mod), sym != nil, do: sym)),
range: %Range{
start: %Position{line: meta[:line] - 1, character: meta[:column] - 1},
end: %Position{line: meta[:end][:line] - 1, character: meta[:end][:column] - 1}
},
selection_range: %Range{
start: %Position{line: meta[:line] - 1, character: meta[:column] - 1},
end: %Position{line: meta[:line] - 1, character: meta[:column] - 1}
}
}
end

defp walker({:defstruct, meta, [fields]}, mod) do
fields =
for field <- fields do
{name, start_line, start_column} =
case field do
{:__literal__, meta, [name]} ->
start_line = meta[:line] - 1
start_column = meta[:column] - 1
name = Macro.to_string(name)

{name, start_line, start_column}

{{:__literal__, meta, [name]}, default} ->
start_line = meta[:line] - 1
start_column = meta[:column] - 1
name = to_string(name) <> ": " <> Macro.to_string(unliteral(default))

{name, start_line, start_column}
end

%DocumentSymbol{
name: name,
children: [],
kind: GenLSP.Enumerations.SymbolKind.field(),
range: %Range{
start: %Position{
line: start_line,
character: start_column
},
end: %Position{
line: start_line,
character: start_column + String.length(name)
}
},
selection_range: %Range{
start: %Position{line: start_line, character: start_column},
end: %Position{line: start_line, character: start_column}
}
}
end

%DocumentSymbol{
name: "%#{mod}{}",
children: fields,
kind: elixir_kind_to_lsp_kind(:defstruct),
range: %Range{
start: %Position{
line: meta[:line] - 1,
character: meta[:column] - 1
},
end: %Position{
line: meta[:end_of_expression][:line] - 1,
character: meta[:end_of_expression][:column] - 1
}
},
selection_range: %Range{
start: %Position{line: meta[:line] - 1, character: meta[:column] - 1},
end: %Position{line: meta[:line] - 1, character: meta[:column] - 1}
}
}
end

defp walker({:@, meta, [{_name, _, value}]} = attribute, _) when length(value) > 0 do
%DocumentSymbol{
name: attribute |> unliteral() |> Macro.to_string() |> String.replace("\n", ""),
children: [],
kind: elixir_kind_to_lsp_kind(:@),
range: %Range{
start: %Position{
line: meta[:line] - 1,
character: meta[:column] - 1
},
end: %Position{
line: (meta[:end_of_expression] || meta)[:line] - 1,
character: (meta[:end_of_expression] || meta)[:column] - 1
}
},
selection_range: %Range{
start: %Position{line: meta[:line] - 1, character: meta[:column] - 1},
end: %Position{line: meta[:line] - 1, character: meta[:column] - 1}
}
}
end

defp walker({type, meta, [name | _children]}, _) when type in [:test, :feature, :property] do
%DocumentSymbol{
name: "#{type} #{Macro.to_string(unliteral(name))}" |> String.replace("\n", ""),
children: [],
kind: GenLSP.Enumerations.SymbolKind.constructor(),
range: %Range{
start: %Position{
line: meta[:line] - 1,
character: meta[:column] - 1
},
end: %Position{
line: (meta[:end] || meta[:end_of_expression] || meta)[:line] - 1,
character: (meta[:end] || meta[:end_of_expression] || meta)[:column] - 1
}
},
selection_range: %Range{
start: %Position{line: meta[:line] - 1, character: meta[:column] - 1},
end: %Position{line: meta[:line] - 1, character: meta[:column] - 1}
}
}
end

defp walker({type, meta, [name | _children]}, _) when type in [:def, :defp, :defmacro, :defmacro] do
%DocumentSymbol{
name: "#{type} #{name |> unliteral() |> Macro.to_string()}" |> String.replace("\n", ""),
children: [],
kind: elixir_kind_to_lsp_kind(type),
range: %Range{
start: %Position{
line: meta[:line] - 1,
character: meta[:column] - 1
},
end: %Position{
line: (meta[:end] || meta[:end_of_expression] || meta)[:line] - 1,
character: (meta[:end] || meta[:end_of_expression] || meta)[:column] - 1
}
},
selection_range: %Range{
start: %Position{line: meta[:line] - 1, character: meta[:column] - 1},
end: %Position{line: meta[:line] - 1, character: meta[:column] - 1}
}
}
end

defp walker(_ast, _) do
nil
end

defp unliteral(ast) do
Macro.prewalk(ast, fn
{:__literal__, _, [literal]} ->
literal

node ->
node
end)
end

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

Check warning on line 217 in lib/next_ls/document_symbol.ex

View workflow job for this annotation

GitHub Actions / dialyzer

pattern_match

The pattern can never match the type :@ | :def | :defmacro | :defp | :defstruct.
defp elixir_kind_to_lsp_kind(:defstruct), do: GenLSP.Enumerations.SymbolKind.struct()
defp elixir_kind_to_lsp_kind(:@), do: GenLSP.Enumerations.SymbolKind.property()

defp elixir_kind_to_lsp_kind(kind) when kind in [:def, :defp, :defmacro, :defmacrop, :test, :describe],
do: GenLSP.Enumerations.SymbolKind.function()
end
37 changes: 33 additions & 4 deletions lib/next_ls/symbol_table.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ defmodule NextLS.SymbolTable do
defmodule Symbol do
defstruct [:file, :module, :type, :name, :line, :col]

@type t :: %__MODULE__{
file: String.t(),
module: module(),
type: atom(),
name: atom(),
line: integer(),
col: integer()
}

@spec new(keyword()) :: t()
def new(args) do
struct(__MODULE__, args)
end
Expand All @@ -20,6 +30,10 @@ defmodule NextLS.SymbolTable do
@spec symbols(pid() | atom()) :: list(struct())
def symbols(server), do: GenServer.call(server, :symbols)

@spec symbols(pid() | atom(), String.t()) :: list(struct())
def symbols(server, file), do: GenServer.call(server, {:symbols, file})

@spec close(pid() | atom()) :: :ok | {:error, term()}
def close(server), do: GenServer.call(server, :close)

def init(args) do
Expand All @@ -36,10 +50,26 @@ defmodule NextLS.SymbolTable do
{:ok, %{table: name}}
end

def handle_call({:symbols, file}, _, state) do
symbols =
case :dets.lookup(state.table, file) do
[{_, symbols} | _rest] -> symbols
_ -> []
end

{:reply, symbols, state}
end

def handle_call(:symbols, _, state) do
symbols =
:dets.foldl(
fn {_key, symbol}, acc -> [symbol | acc] end,
fn {_key, symbol}, acc ->
if String.match?(to_string(symbol.name), ~r/__.*__/) do
acc
else
[symbol | acc]
end
end,
[],
state.table
)
Expand All @@ -63,6 +93,7 @@ defmodule NextLS.SymbolTable do
} = symbols

:dets.delete(state.table, mod)
:dets.delete(state.table, file)

:dets.insert(
state.table,
Expand Down Expand Up @@ -94,9 +125,7 @@ defmodule NextLS.SymbolTable do
)
end

for {name, {:v1, type, _meta, clauses}} <- defs,
not String.match?(to_string(name), ~r/__.*__/),
{meta, _, _, _} <- clauses do
for {name, {:v1, type, _meta, clauses}} <- defs, {meta, _, _, _} <- clauses do
:dets.insert(
state.table,
{mod,
Expand Down
Loading

0 comments on commit bb63928

Please sign in to comment.