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: document symbols #69

Merged
merged 1 commit into from
Jun 29, 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
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
222 changes: 222 additions & 0 deletions lib/next_ls/document_symbol.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
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] - 1, 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(: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