Skip to content

Commit bf80999

Browse files
authored
feat: document symbols (#69)
Closes #41
1 parent db11c1c commit bf80999

File tree

5 files changed

+659
-21
lines changed

5 files changed

+659
-21
lines changed

Diff for: lib/next_ls.ex

+23-9
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ defmodule NextLS do
2020
alias GenLSP.Requests.{
2121
Initialize,
2222
Shutdown,
23+
TextDocumentDocumentSymbol,
2324
TextDocumentFormatting,
2425
WorkspaceSymbol
2526
}
@@ -28,21 +29,21 @@ defmodule NextLS do
2829
DidOpenTextDocumentParams,
2930
InitializeParams,
3031
InitializeResult,
32+
Location,
3133
Position,
3234
Range,
33-
Location,
3435
SaveOptions,
3536
ServerCapabilities,
37+
SymbolInformation,
3638
TextDocumentItem,
3739
TextDocumentSyncOptions,
3840
TextEdit,
3941
WorkDoneProgressBegin,
40-
WorkDoneProgressEnd,
41-
SymbolInformation
42+
WorkDoneProgressEnd
4243
}
4344

44-
alias NextLS.Runtime
4545
alias NextLS.DiagnosticCache
46+
alias NextLS.Runtime
4647
alias NextLS.SymbolTable
4748

4849
def start_link(args) do
@@ -85,10 +86,7 @@ defmodule NextLS do
8586
end
8687

8788
@impl true
88-
def handle_request(
89-
%Initialize{params: %InitializeParams{root_uri: root_uri}},
90-
lsp
91-
) do
89+
def handle_request(%Initialize{params: %InitializeParams{root_uri: root_uri}}, lsp) do
9290
{:reply,
9391
%InitializeResult{
9492
capabilities: %ServerCapabilities{
@@ -98,12 +96,28 @@ defmodule NextLS do
9896
change: TextDocumentSyncKind.full()
9997
},
10098
document_formatting_provider: true,
101-
workspace_symbol_provider: true
99+
workspace_symbol_provider: true,
100+
document_symbol_provider: true
102101
},
103102
server_info: %{name: "NextLS"}
104103
}, assign(lsp, root_uri: root_uri)}
105104
end
106105

106+
def handle_request(%TextDocumentDocumentSymbol{params: %{text_document: %{uri: uri}}}, lsp) do
107+
symbols =
108+
try do
109+
lsp.assigns.documents[uri]
110+
|> Enum.join("\n")
111+
|> NextLS.DocumentSymbol.fetch()
112+
rescue
113+
e ->
114+
GenLSP.error(lsp, Exception.format_banner(:error, e, __STACKTRACE__))
115+
nil
116+
end
117+
118+
{:reply, symbols, lsp}
119+
end
120+
107121
def handle_request(%WorkspaceSymbol{params: %{query: query}}, lsp) do
108122
filter = fn sym ->
109123
if query == "" do

Diff for: lib/next_ls/document_symbol.ex

+222
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
defmodule NextLS.DocumentSymbol do
2+
alias GenLSP.Structures.{
3+
Position,
4+
Range,
5+
DocumentSymbol
6+
}
7+
8+
# we set the literal encoder so that we can know when atoms and strings start and end
9+
# this makes it useful for knowing the exact locations of struct field definitions
10+
@spec fetch(text :: String.t()) :: list(DocumentSymbol.t())
11+
def fetch(text) do
12+
text
13+
|> Code.string_to_quoted!(
14+
literal_encoder: fn literal, meta ->
15+
if is_atom(literal) or is_binary(literal) do
16+
{:ok, {:__literal__, meta, [literal]}}
17+
else
18+
{:ok, literal}
19+
end
20+
end,
21+
unescape: false,
22+
token_metadata: true,
23+
columns: true
24+
)
25+
|> walker(nil)
26+
|> List.wrap()
27+
end
28+
29+
defp walker([{{:__literal__, _, [:do]}, {_, _, _exprs} = ast}], mod) do
30+
walker(ast, mod)
31+
end
32+
33+
defp walker({:__block__, _, exprs}, mod) do
34+
for expr <- exprs, sym = walker(expr, mod), sym != nil do
35+
sym
36+
end
37+
end
38+
39+
defp walker({:defmodule, meta, [name | children]}, _mod) do
40+
name = Macro.to_string(unliteral(name))
41+
42+
%DocumentSymbol{
43+
name: name,
44+
kind: GenLSP.Enumerations.SymbolKind.module(),
45+
children: List.flatten(for(child <- children, sym = walker(child, name), sym != nil, do: sym)),
46+
range: %Range{
47+
start: %Position{line: meta[:line] - 1, character: meta[:column] - 1},
48+
end: %Position{line: meta[:end][:line] - 1, character: meta[:end][:column] - 1}
49+
},
50+
selection_range: %Range{
51+
start: %Position{line: meta[:line] - 1, character: meta[:column] - 1},
52+
end: %Position{line: meta[:line] - 1, character: meta[:column] - 1}
53+
}
54+
}
55+
end
56+
57+
defp walker({:describe, meta, [name | children]}, mod) do
58+
name = ("describe " <> Macro.to_string(unliteral(name))) |> String.replace("\n", "")
59+
60+
%DocumentSymbol{
61+
name: name,
62+
kind: GenLSP.Enumerations.SymbolKind.class(),
63+
children: List.flatten(for(child <- children, sym = walker(child, mod), sym != nil, do: sym)),
64+
range: %Range{
65+
start: %Position{line: meta[:line] - 1, character: meta[:column] - 1},
66+
end: %Position{line: meta[:end][:line] - 1, character: meta[:end][:column] - 1}
67+
},
68+
selection_range: %Range{
69+
start: %Position{line: meta[:line] - 1, character: meta[:column] - 1},
70+
end: %Position{line: meta[:line] - 1, character: meta[:column] - 1}
71+
}
72+
}
73+
end
74+
75+
defp walker({:defstruct, meta, [fields]}, mod) do
76+
fields =
77+
for field <- fields do
78+
{name, start_line, start_column} =
79+
case field do
80+
{:__literal__, meta, [name]} ->
81+
start_line = meta[:line] - 1
82+
start_column = meta[:column] - 1
83+
name = Macro.to_string(name)
84+
85+
{name, start_line, start_column}
86+
87+
{{:__literal__, meta, [name]}, default} ->
88+
start_line = meta[:line] - 1
89+
start_column = meta[:column] - 1
90+
name = to_string(name) <> ": " <> Macro.to_string(unliteral(default))
91+
92+
{name, start_line, start_column}
93+
end
94+
95+
%DocumentSymbol{
96+
name: name,
97+
children: [],
98+
kind: GenLSP.Enumerations.SymbolKind.field(),
99+
range: %Range{
100+
start: %Position{
101+
line: start_line,
102+
character: start_column
103+
},
104+
end: %Position{
105+
line: start_line,
106+
character: start_column + String.length(name)
107+
}
108+
},
109+
selection_range: %Range{
110+
start: %Position{line: start_line, character: start_column},
111+
end: %Position{line: start_line, character: start_column}
112+
}
113+
}
114+
end
115+
116+
%DocumentSymbol{
117+
name: "%#{mod}{}",
118+
children: fields,
119+
kind: elixir_kind_to_lsp_kind(:defstruct),
120+
range: %Range{
121+
start: %Position{
122+
line: meta[:line] - 1,
123+
character: meta[:column] - 1
124+
},
125+
end: %Position{
126+
line: meta[:end_of_expression][:line] - 1,
127+
character: meta[:end_of_expression][:column] - 1
128+
}
129+
},
130+
selection_range: %Range{
131+
start: %Position{line: meta[:line] - 1, character: meta[:column] - 1},
132+
end: %Position{line: meta[:line] - 1, character: meta[:column] - 1}
133+
}
134+
}
135+
end
136+
137+
defp walker({:@, meta, [{_name, _, value}]} = attribute, _) when length(value) > 0 do
138+
%DocumentSymbol{
139+
name: attribute |> unliteral() |> Macro.to_string() |> String.replace("\n", ""),
140+
children: [],
141+
kind: elixir_kind_to_lsp_kind(:@),
142+
range: %Range{
143+
start: %Position{
144+
line: meta[:line] - 1,
145+
character: meta[:column] - 1
146+
},
147+
end: %Position{
148+
line: (meta[:end_of_expression] || meta)[:line] - 1,
149+
character: (meta[:end_of_expression] || meta)[:column] - 1
150+
}
151+
},
152+
selection_range: %Range{
153+
start: %Position{line: meta[:line] - 1, character: meta[:column] - 1},
154+
end: %Position{line: meta[:line] - 1, character: meta[:column] - 1}
155+
}
156+
}
157+
end
158+
159+
defp walker({type, meta, [name | _children]}, _) when type in [:test, :feature, :property] do
160+
%DocumentSymbol{
161+
name: "#{type} #{Macro.to_string(unliteral(name))}" |> String.replace("\n", ""),
162+
children: [],
163+
kind: GenLSP.Enumerations.SymbolKind.constructor(),
164+
range: %Range{
165+
start: %Position{
166+
line: meta[:line] - 1,
167+
character: meta[:column] - 1
168+
},
169+
end: %Position{
170+
line: (meta[:end] || meta[:end_of_expression] || meta)[:line] - 1,
171+
character: (meta[:end] || meta[:end_of_expression] || meta)[:column] - 1
172+
}
173+
},
174+
selection_range: %Range{
175+
start: %Position{line: meta[:line] - 1, character: meta[:column] - 1},
176+
end: %Position{line: meta[:line] - 1, character: meta[:column] - 1}
177+
}
178+
}
179+
end
180+
181+
defp walker({type, meta, [name | _children]}, _) when type in [:def, :defp, :defmacro, :defmacro] do
182+
%DocumentSymbol{
183+
name: "#{type} #{name |> unliteral() |> Macro.to_string()}" |> String.replace("\n", ""),
184+
children: [],
185+
kind: elixir_kind_to_lsp_kind(type),
186+
range: %Range{
187+
start: %Position{
188+
line: meta[:line] - 1,
189+
character: meta[:column] - 1
190+
},
191+
end: %Position{
192+
line: (meta[:end] || meta[:end_of_expression] || meta)[:line] - 1,
193+
character: (meta[:end] || meta[:end_of_expression] || meta)[:column] - 1
194+
}
195+
},
196+
selection_range: %Range{
197+
start: %Position{line: meta[:line] - 1, character: meta[:column] - 1},
198+
end: %Position{line: meta[:line] - 1, character: meta[:column] - 1}
199+
}
200+
}
201+
end
202+
203+
defp walker(_ast, _) do
204+
nil
205+
end
206+
207+
defp unliteral(ast) do
208+
Macro.prewalk(ast, fn
209+
{:__literal__, _, [literal]} ->
210+
literal
211+
212+
node ->
213+
node
214+
end)
215+
end
216+
217+
defp elixir_kind_to_lsp_kind(:defstruct), do: GenLSP.Enumerations.SymbolKind.struct()
218+
defp elixir_kind_to_lsp_kind(:@), do: GenLSP.Enumerations.SymbolKind.property()
219+
220+
defp elixir_kind_to_lsp_kind(kind) when kind in [:def, :defp, :defmacro, :defmacrop, :test, :describe],
221+
do: GenLSP.Enumerations.SymbolKind.function()
222+
end

Diff for: lib/next_ls/symbol_table.ex

+33-4
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,16 @@ defmodule NextLS.SymbolTable do
55
defmodule Symbol do
66
defstruct [:file, :module, :type, :name, :line, :col]
77

8+
@type t :: %__MODULE__{
9+
file: String.t(),
10+
module: module(),
11+
type: atom(),
12+
name: atom(),
13+
line: integer(),
14+
col: integer()
15+
}
16+
17+
@spec new(keyword()) :: t()
818
def new(args) do
919
struct(__MODULE__, args)
1020
end
@@ -20,6 +30,10 @@ defmodule NextLS.SymbolTable do
2030
@spec symbols(pid() | atom()) :: list(struct())
2131
def symbols(server), do: GenServer.call(server, :symbols)
2232

33+
@spec symbols(pid() | atom(), String.t()) :: list(struct())
34+
def symbols(server, file), do: GenServer.call(server, {:symbols, file})
35+
36+
@spec close(pid() | atom()) :: :ok | {:error, term()}
2337
def close(server), do: GenServer.call(server, :close)
2438

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

53+
def handle_call({:symbols, file}, _, state) do
54+
symbols =
55+
case :dets.lookup(state.table, file) do
56+
[{_, symbols} | _rest] -> symbols
57+
_ -> []
58+
end
59+
60+
{:reply, symbols, state}
61+
end
62+
3963
def handle_call(:symbols, _, state) do
4064
symbols =
4165
:dets.foldl(
42-
fn {_key, symbol}, acc -> [symbol | acc] end,
66+
fn {_key, symbol}, acc ->
67+
if String.match?(to_string(symbol.name), ~r/__.*__/) do
68+
acc
69+
else
70+
[symbol | acc]
71+
end
72+
end,
4373
[],
4474
state.table
4575
)
@@ -63,6 +93,7 @@ defmodule NextLS.SymbolTable do
6393
} = symbols
6494

6595
:dets.delete(state.table, mod)
96+
:dets.delete(state.table, file)
6697

6798
:dets.insert(
6899
state.table,
@@ -94,9 +125,7 @@ defmodule NextLS.SymbolTable do
94125
)
95126
end
96127

97-
for {name, {:v1, type, _meta, clauses}} <- defs,
98-
not String.match?(to_string(name), ~r/__.*__/),
99-
{meta, _, _, _} <- clauses do
128+
for {name, {:v1, type, _meta, clauses}} <- defs, {meta, _, _, _} <- clauses do
100129
:dets.insert(
101130
state.table,
102131
{mod,

0 commit comments

Comments
 (0)