Skip to content

Commit 6b245c2

Browse files
authored
[feat] Group functions by name + type + arity in document symbols (#833)
If a module had multiple heads for a function that only differed in parameter pattern matches, document symbols was very cluttered. This change groups functions in a module by their name and arity, with each implementation nested in that grouping. This allows editors to collapse function groups to save space. This change also removes any whitespace or linebreaks from function names.
1 parent cda5742 commit 6b245c2

File tree

3 files changed

+197
-7
lines changed

3 files changed

+197
-7
lines changed

apps/remote_control/lib/lexical/remote_control/code_intelligence/symbols.ex

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
defmodule Lexical.RemoteControl.CodeIntelligence.Symbols do
22
alias Lexical.Document
3+
alias Lexical.Document.Range
34
alias Lexical.RemoteControl.CodeIntelligence.Symbols
45
alias Lexical.RemoteControl.Search
56
alias Lexical.RemoteControl.Search.Indexer
@@ -71,10 +72,8 @@ defmodule Lexical.RemoteControl.CodeIntelligence.Symbols do
7172
children =
7273
entries_by_block_id
7374
|> rebuild_structure(document, entry.id)
74-
|> Enum.sort_by(fn %Symbols.Document{} = symbol ->
75-
start = symbol.range.start
76-
{start.line, start.character}
77-
end)
75+
|> Enum.sort_by(&sort_by_start/1)
76+
|> group_functions()
7877

7978
Symbols.Document.from(document, entry, children)
8079
else
@@ -86,4 +85,41 @@ defmodule Lexical.RemoteControl.CodeIntelligence.Symbols do
8685
_ -> []
8786
end
8887
end
88+
89+
defp group_functions(children) do
90+
{functions, other} = Enum.split_with(children, &match?({:function, _}, &1.original_type))
91+
92+
grouped_functions =
93+
functions
94+
|> Enum.group_by(fn symbol ->
95+
symbol.subject |> String.split(".") |> List.last() |> String.trim()
96+
end)
97+
|> Enum.map(fn
98+
{_name_and_arity, [definition]} ->
99+
definition
100+
101+
{name_and_arity, [first | _] = defs} ->
102+
last = List.last(defs)
103+
[type, _] = String.split(first.name, " ", parts: 2)
104+
name = "#{type} #{name_and_arity}"
105+
106+
children =
107+
Enum.map(defs, fn child ->
108+
[_, rest] = String.split(child.name, " ", parts: 2)
109+
%Symbols.Document{child | name: rest}
110+
end)
111+
112+
range = Range.new(first.range.start, last.range.end)
113+
%Symbols.Document{first | name: name, range: range, children: children}
114+
end)
115+
116+
grouped_functions
117+
|> Enum.concat(other)
118+
|> Enum.sort_by(&sort_by_start/1)
119+
end
120+
121+
defp sort_by_start(%Symbols.Document{} = symbol) do
122+
start = symbol.range.start
123+
{start.line, start.character}
124+
end
89125
end

apps/remote_control/lib/lexical/remote_control/code_intelligence/symbols/document.ex

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ defmodule Lexical.RemoteControl.CodeIntelligence.Symbols.Document do
33
alias Lexical.Formats
44
alias Lexical.RemoteControl.Search.Indexer.Entry
55

6-
defstruct [:name, :type, :range, :detail_range, :detail, children: []]
6+
defstruct [:name, :type, :range, :detail_range, :detail, :original_type, :subject, children: []]
77

88
def from(%Document{} = document, %Entry{} = entry, children \\ []) do
99
case name_and_type(entry.type, entry, document) do
@@ -16,7 +16,9 @@ defmodule Lexical.RemoteControl.CodeIntelligence.Symbols.Document do
1616
type: type,
1717
range: range,
1818
detail_range: entry.range,
19-
children: children
19+
children: children,
20+
original_type: entry.type,
21+
subject: entry.subject
2022
}}
2123

2224
_ ->
@@ -28,7 +30,10 @@ defmodule Lexical.RemoteControl.CodeIntelligence.Symbols.Document do
2830

2931
defp name_and_type({:function, type}, %Entry{} = entry, %Document{} = document)
3032
when type in [:public, :private, :delegate] do
31-
fragment = Document.fragment(document, entry.range.start, entry.range.end)
33+
fragment =
34+
document
35+
|> Document.fragment(entry.range.start, entry.range.end)
36+
|> remove_line_breaks_and_multiple_spaces()
3237

3338
prefix =
3439
case type do
@@ -87,4 +92,8 @@ defmodule Lexical.RemoteControl.CodeIntelligence.Symbols.Document do
8792
defp name_and_type(type, %Entry{} = entry, _document) do
8893
{to_string(entry.subject), type}
8994
end
95+
96+
defp remove_line_breaks_and_multiple_spaces(string) do
97+
string |> String.split(~r/\s/) |> Enum.reject(&match?("", &1)) |> Enum.join(" ")
98+
end
9099
end

apps/remote_control/test/lexical/remote_control/code_intelligence/symbols_test.exs

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,151 @@ defmodule Lexical.RemoteControl.CodeIntelligence.SymbolsTest do
220220
assert function.name == "defp my_fn"
221221
end
222222

223+
test "multiple arity functions are grouped" do
224+
{[module], doc} =
225+
~q[
226+
defmodule Module do
227+
def function_arity(:foo), do: :ok
228+
def function_arity(:bar), do: :ok
229+
def function_arity(:baz), do: :ok
230+
end
231+
]
232+
|> document_symbols()
233+
234+
assert [parent] = module.children
235+
assert parent.name == "def function_arity/1"
236+
237+
expected_range =
238+
"""
239+
«def function_arity(:foo), do: :ok
240+
def function_arity(:bar), do: :ok
241+
def function_arity(:baz), do: :ok»
242+
"""
243+
|> String.trim_trailing()
244+
245+
assert decorate(doc, parent.range) =~ expected_range
246+
assert [first, second, third] = parent.children
247+
248+
assert first.name == "function_arity(:foo)"
249+
assert decorate(doc, first.range) =~ "«def function_arity(:foo), do: :ok»"
250+
assert decorate(doc, first.detail_range) =~ "def «function_arity(:foo)», do: :ok"
251+
252+
assert second.name == "function_arity(:bar)"
253+
assert decorate(doc, second.range) =~ "«def function_arity(:bar), do: :ok»"
254+
assert decorate(doc, second.detail_range) =~ "def «function_arity(:bar)», do: :ok"
255+
256+
assert third.name == "function_arity(:baz)"
257+
assert decorate(doc, third.range) =~ "«def function_arity(:baz), do: :ok»"
258+
assert decorate(doc, third.detail_range) =~ "def «function_arity(:baz)», do: :ok"
259+
end
260+
261+
test "multiple arity private functions are grouped" do
262+
{[module], doc} =
263+
~q[
264+
defmodule Module do
265+
defp function_arity(:foo), do: :ok
266+
defp function_arity(:bar), do: :ok
267+
defp function_arity(:baz), do: :ok
268+
end
269+
]
270+
|> document_symbols()
271+
272+
assert [parent] = module.children
273+
assert parent.name == "defp function_arity/1"
274+
275+
expected_range =
276+
"""
277+
«defp function_arity(:foo), do: :ok
278+
defp function_arity(:bar), do: :ok
279+
defp function_arity(:baz), do: :ok»
280+
"""
281+
|> String.trim_trailing()
282+
283+
assert decorate(doc, parent.range) =~ expected_range
284+
assert [first, second, third] = parent.children
285+
286+
assert first.name == "function_arity(:foo)"
287+
assert decorate(doc, first.range) =~ "«defp function_arity(:foo), do: :ok»"
288+
assert decorate(doc, first.detail_range) =~ "defp «function_arity(:foo)», do: :ok"
289+
290+
assert second.name == "function_arity(:bar)"
291+
assert decorate(doc, second.range) =~ "«defp function_arity(:bar), do: :ok»"
292+
assert decorate(doc, second.detail_range) =~ "defp «function_arity(:bar)», do: :ok"
293+
294+
assert third.name == "function_arity(:baz)"
295+
assert decorate(doc, third.range) =~ "«defp function_arity(:baz), do: :ok»"
296+
assert decorate(doc, third.detail_range) =~ "defp «function_arity(:baz)», do: :ok"
297+
end
298+
299+
test "groups public and private functions separately" do
300+
{[module], _doc} =
301+
~q[
302+
defmodule Module do
303+
def fun_one(:foo), do: :ok
304+
def fun_one(:bar), do: :ok
305+
306+
defp fun_one(:foo, :bar), do: :ok
307+
defp fun_one(:bar, :baz), do: :ok
308+
end
309+
]
310+
|> document_symbols()
311+
312+
assert [first, second] = module.children
313+
assert first.name == "def fun_one/1"
314+
assert second.name == "defp fun_one/2"
315+
end
316+
317+
test "line breaks are stripped" do
318+
{[module], _doc} =
319+
~q[
320+
defmodule Module do
321+
def long_function(
322+
arg_1,
323+
arg_2,
324+
arg_3) do
325+
end
326+
end
327+
]
328+
|> document_symbols()
329+
330+
assert [function] = module.children
331+
assert function.name == "def long_function( arg_1, arg_2, arg_3)"
332+
end
333+
334+
test "line breaks are stripped for grouped functions" do
335+
{[module], _doc} =
336+
~q[
337+
defmodule Module do
338+
def long_function(
339+
:foo,
340+
arg_2,
341+
arg_3) do
342+
end
343+
344+
def long_function(
345+
:bar,
346+
arg_2,
347+
arg_3) do
348+
end
349+
def long_function(
350+
:baz,
351+
arg_2,
352+
arg_3) do
353+
end
354+
355+
end
356+
]
357+
|> document_symbols()
358+
359+
assert [function] = module.children
360+
assert function.name == "def long_function/3"
361+
362+
assert [first, second, third] = function.children
363+
assert first.name == "long_function( :foo, arg_2, arg_3)"
364+
assert second.name == "long_function( :bar, arg_2, arg_3)"
365+
assert third.name == "long_function( :baz, arg_2, arg_3)"
366+
end
367+
223368
test "struct definitions are found" do
224369
{[module], doc} =
225370
~q{

0 commit comments

Comments
 (0)