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] Group functions by name + type + arity in document symbols #833

Merged
merged 1 commit into from
Oct 28, 2024
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
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
defmodule Lexical.RemoteControl.CodeIntelligence.Symbols do
alias Lexical.Document
alias Lexical.Document.Range
alias Lexical.RemoteControl.CodeIntelligence.Symbols
alias Lexical.RemoteControl.Search
alias Lexical.RemoteControl.Search.Indexer
Expand Down Expand Up @@ -71,10 +72,8 @@ defmodule Lexical.RemoteControl.CodeIntelligence.Symbols do
children =
entries_by_block_id
|> rebuild_structure(document, entry.id)
|> Enum.sort_by(fn %Symbols.Document{} = symbol ->
start = symbol.range.start
{start.line, start.character}
end)
|> Enum.sort_by(&sort_by_start/1)
|> group_functions()

Symbols.Document.from(document, entry, children)
else
Expand All @@ -86,4 +85,41 @@ defmodule Lexical.RemoteControl.CodeIntelligence.Symbols do
_ -> []
end
end

defp group_functions(children) do
{functions, other} = Enum.split_with(children, &match?({:function, _}, &1.original_type))

grouped_functions =
functions
|> Enum.group_by(fn symbol ->
symbol.subject |> String.split(".") |> List.last() |> String.trim()
end)
|> Enum.map(fn
{_name_and_arity, [definition]} ->
definition

{name_and_arity, [first | _] = defs} ->
last = List.last(defs)
[type, _] = String.split(first.name, " ", parts: 2)
name = "#{type} #{name_and_arity}"

children =
Enum.map(defs, fn child ->
[_, rest] = String.split(child.name, " ", parts: 2)
%Symbols.Document{child | name: rest}
end)

range = Range.new(first.range.start, last.range.end)
%Symbols.Document{first | name: name, range: range, children: children}
end)

grouped_functions
|> Enum.concat(other)
|> Enum.sort_by(&sort_by_start/1)
end

defp sort_by_start(%Symbols.Document{} = symbol) do
start = symbol.range.start
{start.line, start.character}
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ defmodule Lexical.RemoteControl.CodeIntelligence.Symbols.Document do
alias Lexical.Formats
alias Lexical.RemoteControl.Search.Indexer.Entry

defstruct [:name, :type, :range, :detail_range, :detail, children: []]
defstruct [:name, :type, :range, :detail_range, :detail, :original_type, :subject, children: []]

def from(%Document{} = document, %Entry{} = entry, children \\ []) do
case name_and_type(entry.type, entry, document) do
Expand All @@ -16,7 +16,9 @@ defmodule Lexical.RemoteControl.CodeIntelligence.Symbols.Document do
type: type,
range: range,
detail_range: entry.range,
children: children
children: children,
original_type: entry.type,
subject: entry.subject
}}

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

defp name_and_type({:function, type}, %Entry{} = entry, %Document{} = document)
when type in [:public, :private, :delegate] do
fragment = Document.fragment(document, entry.range.start, entry.range.end)
fragment =
document
|> Document.fragment(entry.range.start, entry.range.end)
|> remove_line_breaks_and_multiple_spaces()

prefix =
case type do
Expand Down Expand Up @@ -87,4 +92,8 @@ defmodule Lexical.RemoteControl.CodeIntelligence.Symbols.Document do
defp name_and_type(type, %Entry{} = entry, _document) do
{to_string(entry.subject), type}
end

defp remove_line_breaks_and_multiple_spaces(string) do
string |> String.split(~r/\s/) |> Enum.reject(&match?("", &1)) |> Enum.join(" ")
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,151 @@ defmodule Lexical.RemoteControl.CodeIntelligence.SymbolsTest do
assert function.name == "defp my_fn"
end

test "multiple arity functions are grouped" do
{[module], doc} =
~q[
defmodule Module do
def function_arity(:foo), do: :ok
def function_arity(:bar), do: :ok
def function_arity(:baz), do: :ok
end
]
|> document_symbols()

assert [parent] = module.children
assert parent.name == "def function_arity/1"

expected_range =
"""
«def function_arity(:foo), do: :ok
def function_arity(:bar), do: :ok
def function_arity(:baz), do: :ok»
"""
|> String.trim_trailing()

assert decorate(doc, parent.range) =~ expected_range
assert [first, second, third] = parent.children

assert first.name == "function_arity(:foo)"
assert decorate(doc, first.range) =~ "«def function_arity(:foo), do: :ok»"
assert decorate(doc, first.detail_range) =~ "def «function_arity(:foo)», do: :ok"

assert second.name == "function_arity(:bar)"
assert decorate(doc, second.range) =~ "«def function_arity(:bar), do: :ok»"
assert decorate(doc, second.detail_range) =~ "def «function_arity(:bar)», do: :ok"

assert third.name == "function_arity(:baz)"
assert decorate(doc, third.range) =~ "«def function_arity(:baz), do: :ok»"
assert decorate(doc, third.detail_range) =~ "def «function_arity(:baz)», do: :ok"
end

test "multiple arity private functions are grouped" do
{[module], doc} =
~q[
defmodule Module do
defp function_arity(:foo), do: :ok
defp function_arity(:bar), do: :ok
defp function_arity(:baz), do: :ok
end
]
|> document_symbols()

assert [parent] = module.children
assert parent.name == "defp function_arity/1"

expected_range =
"""
«defp function_arity(:foo), do: :ok
defp function_arity(:bar), do: :ok
defp function_arity(:baz), do: :ok»
"""
|> String.trim_trailing()

assert decorate(doc, parent.range) =~ expected_range
assert [first, second, third] = parent.children

assert first.name == "function_arity(:foo)"
assert decorate(doc, first.range) =~ "«defp function_arity(:foo), do: :ok»"
assert decorate(doc, first.detail_range) =~ "defp «function_arity(:foo)», do: :ok"

assert second.name == "function_arity(:bar)"
assert decorate(doc, second.range) =~ "«defp function_arity(:bar), do: :ok»"
assert decorate(doc, second.detail_range) =~ "defp «function_arity(:bar)», do: :ok"

assert third.name == "function_arity(:baz)"
assert decorate(doc, third.range) =~ "«defp function_arity(:baz), do: :ok»"
assert decorate(doc, third.detail_range) =~ "defp «function_arity(:baz)», do: :ok"
end

test "groups public and private functions separately" do
{[module], _doc} =
~q[
defmodule Module do
def fun_one(:foo), do: :ok
def fun_one(:bar), do: :ok

defp fun_one(:foo, :bar), do: :ok
defp fun_one(:bar, :baz), do: :ok
end
]
|> document_symbols()

assert [first, second] = module.children
assert first.name == "def fun_one/1"
assert second.name == "defp fun_one/2"
end

test "line breaks are stripped" do
{[module], _doc} =
~q[
defmodule Module do
def long_function(
arg_1,
arg_2,
arg_3) do
end
end
]
|> document_symbols()

assert [function] = module.children
assert function.name == "def long_function( arg_1, arg_2, arg_3)"
end

test "line breaks are stripped for grouped functions" do
{[module], _doc} =
~q[
defmodule Module do
def long_function(
:foo,
arg_2,
arg_3) do
end

def long_function(
:bar,
arg_2,
arg_3) do
end
def long_function(
:baz,
arg_2,
arg_3) do
end

end
]
|> document_symbols()

assert [function] = module.children
assert function.name == "def long_function/3"

assert [first, second, third] = function.children
assert first.name == "long_function( :foo, arg_2, arg_3)"
assert second.name == "long_function( :bar, arg_2, arg_3)"
assert third.name == "long_function( :baz, arg_2, arg_3)"
end

test "struct definitions are found" do
{[module], doc} =
~q{
Expand Down
Loading