Skip to content

Commit dfa8116

Browse files
lukaszsamsonaxelson
authored andcommitted
Improvements to document outline (#76)
* Add nested outline view Fixes #188 * cleanup * do not crash on atom modules * do not crash on (sub)modules with __MODULE__ special form * extract guards, private macros and delegates * handle protocols and implementations * treat protocol as interface * more test cases * don't crash when unquote in module definition * handle structs and exceptions * handle types specs and callbacks * attribute handling normalized * support ex_unit setup and setup_all * Support config files * extract stuct properties * fix crash when keyword list passed to config * support elixir modules in config keys * use map_join * run formatter conditionally * break formatting to check if CI works * change if condition * experiment with other syntax * add multiline marker
1 parent 877d977 commit dfa8116

File tree

3 files changed

+1386
-143
lines changed

3 files changed

+1386
-143
lines changed

.travis.yml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
language: elixir
22
script:
33
- MIX_ENV=test mix compile --force --warnings-as-errors
4-
- mix format --check-formatted
4+
- |
5+
if [[ "$CHECK_FORMATTED" -eq 1 ]]
6+
then
7+
mix format --check-formatted
8+
else
9+
echo "Not checking formatting"
10+
fi
511
- mix test
612
env:
713
global:
@@ -19,3 +25,4 @@ matrix:
1925
elixir: 1.8.1
2026
- otp_release: 22.0
2127
elixir: 1.9.1
28+
env: CHECK_FORMATTED=1

apps/language_server/lib/language_server/providers/document_symbols.ex

Lines changed: 205 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -31,111 +31,270 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbols do
3131
type_parameter: 26
3232
}
3333

34+
@defs [:def, :defp, :defmacro, :defmacrop, :defguard, :defguardp, :defdelegate]
35+
36+
@docs [
37+
:doc,
38+
:moduledoc,
39+
:typedoc
40+
]
41+
3442
def symbols(uri, text) do
3543
symbols = list_symbols(text) |> Enum.map(&build_symbol_information(uri, &1))
3644
{:ok, symbols}
3745
end
3846

3947
defp list_symbols(src) do
40-
{_ast, symbol_list} =
41-
Code.string_to_quoted!(src, columns: true, line: 0)
42-
|> Macro.prewalk([], fn ast, symbols ->
43-
{ast, extract_module(ast) ++ symbols}
44-
end)
45-
46-
symbol_list
48+
Code.string_to_quoted!(src, columns: true, line: 0)
49+
|> extract_modules()
4750
end
4851

4952
# Identify and extract the module symbol, and the symbols contained within the module
50-
defp extract_module({:defmodule, _, _child_ast} = ast) do
51-
{_, _, [{:__aliases__, location, module_name}, [do: module_body]]} = ast
53+
defp extract_modules({:__block__, [], ast}) do
54+
ast |> Enum.map(&extract_modules(&1)) |> List.flatten()
55+
end
56+
57+
defp extract_modules({defname, _, _child_ast} = ast)
58+
when defname in [:defmodule, :defprotocol, :defimpl] do
59+
[extract_symbol("", ast)]
60+
end
61+
62+
defp extract_modules({:config, _, _} = ast) do
63+
[extract_symbol("", ast)]
64+
end
5265

66+
defp extract_modules(_ast), do: []
67+
68+
# Modules, protocols
69+
defp extract_symbol(_module_name, {defname, location, [module_expression, [do: module_body]]})
70+
when defname in [:defmodule, :defprotocol] do
5371
mod_defns =
5472
case module_body do
5573
{:__block__, [], mod_defns} -> mod_defns
5674
stmt -> [stmt]
5775
end
5876

59-
module_name = Enum.join(module_name, ".")
77+
module_name = extract_module_name(module_expression)
6078

6179
module_symbols =
6280
mod_defns
6381
|> Enum.map(&extract_symbol(module_name, &1))
6482
|> Enum.reject(&is_nil/1)
6583

66-
[%{type: :module, name: module_name, location: location, container: nil}] ++ module_symbols
84+
type =
85+
case defname do
86+
:defmodule -> :module
87+
:defprotocol -> :interface
88+
end
89+
90+
%{type: type, name: module_name, location: location, children: module_symbols}
91+
end
92+
93+
# Protocol implementations
94+
defp extract_symbol(
95+
module_name,
96+
{:defimpl, location, [protocol_expression, [for: for_expression], [do: module_body]]}
97+
) do
98+
extract_symbol(
99+
module_name,
100+
{:defmodule, location,
101+
[[protocol: protocol_expression, implementations: for_expression], [do: module_body]]}
102+
)
103+
end
104+
105+
# Struct and exception
106+
defp extract_symbol(_module_name, {defname, location, [properties | _]})
107+
when defname in [:defstruct, :defexception] do
108+
name =
109+
case defname do
110+
:defstruct -> "struct"
111+
:defexception -> "exception"
112+
end
113+
114+
children =
115+
if is_list(properties) do
116+
properties
117+
|> Enum.map(&extract_property(&1, location))
118+
|> Enum.reject(&is_nil/1)
119+
else
120+
[]
121+
end
122+
123+
%{type: :struct, name: name, location: location, children: children}
67124
end
68125

69-
defp extract_module(_ast), do: []
126+
# Docs
127+
defp extract_symbol(_, {:@, _, [{kind, _, _}]}) when kind in @docs, do: nil
70128

71-
# Module Variable
72-
defp extract_symbol(_, {:@, _, [{:moduledoc, _, _}]}), do: nil
73-
defp extract_symbol(_, {:@, _, [{:doc, _, _}]}), do: nil
74-
defp extract_symbol(_, {:@, _, [{:spec, _, _}]}), do: nil
75-
defp extract_symbol(_, {:@, _, [{:behaviour, _, _}]}), do: nil
76-
defp extract_symbol(_, {:@, _, [{:impl, _, _}]}), do: nil
77-
defp extract_symbol(_, {:@, _, [{:type, _, _}]}), do: nil
78-
defp extract_symbol(_, {:@, _, [{:typedoc, _, _}]}), do: nil
79-
defp extract_symbol(_, {:@, _, [{:enforce_keys, _, _}]}), do: nil
129+
# Types
130+
defp extract_symbol(_current_module, {:@, _, [{type_kind, location, type_expression}]})
131+
when type_kind in [:type, :typep, :opaque, :spec, :callback, :macrocallback] do
132+
type_name =
133+
case type_expression do
134+
[{:"::", _, [{_, _, _} = type_head | _]}] ->
135+
Macro.to_string(type_head)
80136

81-
defp extract_symbol(current_module, {:@, _, [{name, location, _}]}) do
82-
%{type: :constant, name: "@#{name}", location: location, container: current_module}
137+
[{:when, _, [{:"::", _, [{_, _, _} = type_head, _]}, _]}] ->
138+
Macro.to_string(type_head)
139+
end
140+
141+
type = if type_kind in [:type, :typep, :opaque], do: :class, else: :event
142+
143+
%{
144+
type: type,
145+
name: type_name,
146+
location: location,
147+
children: []
148+
}
149+
end
150+
151+
# Other attributes
152+
defp extract_symbol(_current_module, {:@, _, [{name, location, _}]}) do
153+
%{type: :constant, name: "@#{name}", location: location, children: []}
83154
end
84155

85-
# Function
86-
defp extract_symbol(current_module, {:def, _, [{_, location, _} = fn_head | _]}) do
156+
# Function, macro, guard, delegate
157+
defp extract_symbol(_current_module, {defname, _, [{_, location, _} = fn_head | _]})
158+
when defname in @defs do
87159
%{
88160
type: :function,
89161
name: Macro.to_string(fn_head),
90162
location: location,
91-
container: current_module
163+
children: []
92164
}
93165
end
94166

95-
# Private Function
96-
defp extract_symbol(current_module, {:defp, _, [{_, location, _} = fn_head | _]}) do
167+
# ExUnit test
168+
defp extract_symbol(_current_module, {:test, location, [name | _]}) do
97169
%{
98170
type: :function,
99-
name: Macro.to_string(fn_head),
171+
name: ~s(test "#{name}"),
100172
location: location,
101-
container: current_module
173+
children: []
102174
}
103175
end
104176

105-
# Macro
106-
defp extract_symbol(current_module, {:defmacro, _, [{_, location, _} = fn_head | _]}) do
177+
# ExUnit setup and setup_all callbacks
178+
defp extract_symbol(_current_module, {name, location, [_name | _]})
179+
when name in [:setup, :setup_all] do
107180
%{
108181
type: :function,
109-
name: Macro.to_string(fn_head),
182+
name: "#{name}",
110183
location: location,
111-
container: current_module
184+
children: []
112185
}
113186
end
114187

115-
# Test
116-
defp extract_symbol(current_module, {:test, location, [name | _]}) do
188+
# ExUnit describe
189+
defp extract_symbol(current_module, {:describe, location, [name | ast]}) do
190+
[[do: module_body]] = ast
191+
192+
mod_defns =
193+
case module_body do
194+
{:__block__, [], mod_defns} -> mod_defns
195+
stmt -> [stmt]
196+
end
197+
198+
module_symbols =
199+
mod_defns
200+
|> Enum.map(&extract_symbol(current_module, &1))
201+
|> Enum.reject(&is_nil/1)
202+
117203
%{
118204
type: :function,
119-
name: ~s(test "#{name}"),
205+
name: ~s(describe "#{name}"),
120206
location: location,
121-
container: current_module
207+
children: module_symbols
122208
}
123209
end
124210

211+
# Config entry
212+
defp extract_symbol(_current_module, {:config, location, [app, config_entry | _]})
213+
when is_atom(app) do
214+
keys =
215+
case config_entry do
216+
list when is_list(list) ->
217+
list
218+
|> Enum.map(fn {key, _} -> Macro.to_string(key) end)
219+
220+
key ->
221+
[Macro.to_string(key)]
222+
end
223+
224+
for key <- keys do
225+
%{
226+
type: :key,
227+
name: "config :#{app} #{key}",
228+
location: location,
229+
children: []
230+
}
231+
end
232+
end
233+
125234
defp extract_symbol(_, _), do: nil
126235

236+
defp build_symbol_information(uri, info) when is_list(info),
237+
do: Enum.map(info, &build_symbol_information(uri, &1))
238+
127239
defp build_symbol_information(uri, info) do
128240
%{
129241
name: info.name,
130242
kind: @symbol_enum[info.type],
131-
containerName: info.container,
132-
location: %{
133-
uri: uri,
134-
range: %{
135-
start: %{line: info.location[:line], character: info.location[:column] - 1},
136-
end: %{line: info.location[:line], character: info.location[:column] - 1}
137-
}
138-
}
243+
range: location_to_range(info.location),
244+
selectionRange: location_to_range(info.location),
245+
children: build_symbol_information(uri, info.children)
139246
}
140247
end
248+
249+
defp location_to_range(location) do
250+
%{
251+
start: %{line: location[:line], character: location[:column] - 1},
252+
end: %{line: location[:line], character: location[:column] - 1}
253+
}
254+
end
255+
256+
defp extract_module_name(protocol: protocol, implementations: implementations) do
257+
extract_module_name(protocol) <> ", for: " <> extract_module_name(implementations)
258+
end
259+
260+
defp extract_module_name(list) when is_list(list) do
261+
list_stringified = list |> Enum.map_join(", ", &extract_module_name/1)
262+
263+
"[" <> list_stringified <> "]"
264+
end
265+
266+
defp extract_module_name({:__aliases__, location, [{:__MODULE__, _, nil} = head | tail]}) do
267+
extract_module_name(head) <> "." <> extract_module_name({:__aliases__, location, tail})
268+
end
269+
270+
defp extract_module_name({:__aliases__, _location, module_names}) do
271+
Enum.join(module_names, ".")
272+
end
273+
274+
defp extract_module_name({:__MODULE__, _location, nil}) do
275+
"__MODULE__"
276+
end
277+
278+
defp extract_module_name(module) when is_atom(module) do
279+
case Atom.to_string(module) do
280+
"Elixir." <> elixir_module_rest -> elixir_module_rest
281+
erlang_module -> erlang_module
282+
end
283+
end
284+
285+
defp extract_module_name(_), do: "# unknown"
286+
287+
defp extract_property(property_name, location) when is_atom(property_name) do
288+
%{
289+
type: :property,
290+
name: "#{property_name}",
291+
location: location,
292+
children: []
293+
}
294+
end
295+
296+
defp extract_property({property_name, _default}, location),
297+
do: extract_property(property_name, location)
298+
299+
defp extract_property(_, _), do: nil
141300
end

0 commit comments

Comments
 (0)