Skip to content

Commit

Permalink
Use the new algorithm
Browse files Browse the repository at this point in the history
The execution steps of this new algorithm are as follows:

The execution steps of this new algorithm are as follows:

1. If there is only one module within three generations,
only that module will be returned, e.g.: `%Lexical.Document.Changes{}`
2. If there are more structs within three generations and the first layer of the struct has ancestor structs,
the ancestor structs and more will be mixedly returned, but not the ancestor module.
3. If there are no ancestor structs, only more will be returned.

In addition, we do not return any structs beyond two generations due to `dots_count + 1`.
  • Loading branch information
scottming committed Jul 29, 2023
1 parent f236b6a commit 4febf4c
Show file tree
Hide file tree
Showing 7 changed files with 105 additions and 52 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -29,41 +29,46 @@ defmodule Lexical.Server.CodeIntelligence.Completion.Translations.ModuleOrBehavi
end

defp complete_in_struct_reference(%Env{} = env, builder, module) do
ancestors_and_more =
env.project
|> immediate_descendent_struct_modules(module.full_name)
|> ancestors_and_more()

Enum.flat_map(ancestors_and_more, fn {child_module_name, more} ->
local_name = local_module_name(module.full_name, child_module_name, module.name)

env
|> Translations.Struct.completion(builder, local_name, child_module_name, more)
|> List.wrap()
immediate_descendent_structs =
immediate_descendent_struct_modules(env.project, module.full_name)

structs_map = Map.new(immediate_descendent_structs, fn module -> {module, true} end)
dot_counts = module_dot_counts(module.full_name)
ancestors = ancestors(immediate_descendent_structs, dot_counts)

Enum.flat_map(ancestors, fn ancestor ->
local_name = local_module_name(module.full_name, ancestor, module.name)

more =
env.project
|> Intelligence.collect_struct_modules(ancestor, to: :infinity)
|> Enum.count()

if struct?(ancestor, structs_map) do
[
Translations.Struct.completion(env, builder, local_name, ancestor),
Translations.Struct.completion(env, builder, local_name, ancestor, more - 1)
]
else
[Translations.Struct.completion(env, builder, local_name, ancestor, more)]
end
end)
end

defp ancestors_and_more(modules) do
modules
|> group_by_ancestor()
|> Enum.map(fn {ancestor, descendants} ->
{ancestor, length(descendants)}
end)
defp struct?(module, structs_map) do
Map.has_key?(structs_map, module)
end

defp group_by_ancestor(modules) do
sorted = Enum.sort(modules)

{group, _} =
Enum.reduce(sorted, {%{}, List.first(sorted)}, fn module, {grouped, last_module} ->
if String.starts_with?(module, last_module) do
{Map.update(grouped, last_module, [], &[module | &1]), last_module}
else
{Map.put(grouped, module, []), module}
end
end)
defp ancestors(results, dot_counts) do
results
|> Enum.map(fn module ->
module |> String.split(".") |> Enum.take(dot_counts + 1) |> Enum.join(".")
end)
|> Enum.uniq()
end

group
defp module_dot_counts(module_name) do
module_name |> String.graphemes() |> Enum.count(&(&1 == "."))
end

def completion(%Env{} = env, builder, module_name, detail \\ nil) do
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ defmodule Lexical.Server.CodeIntelligence.Completion.Translations.Struct do
end
end

def completion(%Env{} = env, builder, module_name, full_name, 0) do
completion(env, builder, module_name, full_name)
def completion(%Env{} = _env, _builder, _module_name, _full_name, 0) do
nil
end

def completion(%Env{} = env, builder, module_name, full_name, more) when is_integer(more) do
Expand All @@ -39,10 +39,7 @@ defmodule Lexical.Server.CodeIntelligence.Completion.Translations.Struct do
insert_text = "#{module_name}."
range = edit_range(env)

current_module_struct = completion(env, builder, module_name, full_name)
more = builder.text_edit_snippet(env, insert_text, range, builder_opts)

[current_module_struct, more]
builder.text_edit_snippet(env, insert_text, range, builder_opts)
end

def completion(%Env{} = env, builder, struct_name, full_name) do
Expand Down
27 changes: 21 additions & 6 deletions apps/server/lib/lexical/server/project/intelligence.ex
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,11 @@ defmodule Lexical.Server.Project.Intelligence do
Enum.any?(state.struct_modules, &prefixes_match?(module_path, &1, range))
end

def descendent_struct_modules(%__MODULE__{} = state, prefix, %Range{} = range) do
def descendent_struct_modules(%__MODULE__{} = state, prefix, range_or_infinity) do
module_path = module_path(prefix)

for struct_path <- state.struct_modules,
prefixes_match?(module_path, struct_path, range) do
prefixes_match?(module_path, struct_path, range_or_infinity) do
Enum.join(struct_path, ".")
end
end
Expand All @@ -41,6 +41,10 @@ defmodule Lexical.Server.Project.Intelligence do
|> String.split(".")
end

defp prefixes_match?([], _remainder, :infinity) do
true
end

defp prefixes_match?([], remainder, %Range{} = range) do
length(remainder) in range
end
Expand Down Expand Up @@ -83,7 +87,7 @@ defmodule Lexical.Server.Project.Intelligence do
@type module_spec :: module() | String.t()
@type module_name :: String.t()
@type generation_spec :: generation_name | non_neg_integer
@type generation_option :: {:from, generation_spec} | {:to, generation_spec}
@type generation_option :: {:from, generation_spec} | {:to, generation_spec} | {:to, :infinity}
@type generation_options :: [generation_option]

# Public api
Expand All @@ -104,7 +108,8 @@ defmodule Lexical.Server.Project.Intelligence do
for child, etc) or named generations (`:self`, `:child`, `:grandchild`, etc). For example,
the collectionn range: `from: :child, to: :great_grandchild` will collect all struct
modules where the root module is thier parent up to and including all modules where the
root module is their great grandparent, and is equivalent to the range `1..2`.
root module is their great grandparent, and is equivalent to the range `1..2`,
Of course, if you want to return all the struct_modules, you can simply use `to: :infinity`.
`range`: A `Range` struct containing the starting and ending generations. The module passed in
as `root_module` is generation 0, its child is generation 1, its grandchild is generation 2,
Expand All @@ -120,6 +125,12 @@ defmodule Lexical.Server.Project.Intelligence do
collect_struct_modules(project, root_module, extract_range(opts))
end

def collect_struct_modules(%Project{} = project, root_module, :infinity) do
project
|> name()
|> GenServer.call({:collect_struct_modules, root_module, :infinity})
end

def collect_struct_modules(%Project{} = project, root_module, %Range{} = range) do
project
|> name()
Expand Down Expand Up @@ -169,11 +180,11 @@ defmodule Lexical.Server.Project.Intelligence do

@impl GenServer
def handle_call(
{:collect_struct_modules, parent_module, %Range{} = range},
{:collect_struct_modules, parent_module, range_or_infinity},
_from,
%State{} = state
) do
{:reply, State.descendent_struct_modules(state, parent_module, range), state}
{:reply, State.descendent_struct_modules(state, parent_module, range_or_infinity), state}
end

@impl GenServer
Expand All @@ -200,6 +211,10 @@ defmodule Lexical.Server.Project.Intelligence do
:"#{Project.name(project)}::intelligence"
end

defp extract_range(to: :infinity) do
:infinity
end

defp extract_range(opts) when is_list(opts) do
from = Keyword.get(opts, :from, :self)
from = Map.get(@generations, from, from)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -202,15 +202,10 @@ defmodule Lexical.Server.CodeIntelligence.Completion.Translations.ModuleOrBehavi
test "should offer no other types of completions", %{project: project} do
assert [] = complete(project, "%MapSet.|")

assert [account, order, order_line, user] =
project
|> complete("%Project.|")
|> Enum.sort_by(& &1.label)
assert [completion] = complete(project, "%Project.|")

assert account.label == "Structs.Account"
assert order.label == "Structs.Order"
assert order_line.label == "Structs.Order...(1 more struct)"
assert user.label == "Structs.User"
assert completion.label == "Structs...(4 more structs)"
assert completion.detail == "Project.Structs."
end

test "should offer two completions when there are struct and its descendants", %{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,10 +128,27 @@ defmodule Lexical.Server.CodeIntelligence.Completion.Translations.StructTest do
assert apply_completion(completion) == expected
end

test "when using %, part child structs are returned", %{project: project} do
assert [account, order, user] =
project
|> complete("%Project.Structs.|", "%")
|> Enum.sort_by(& &1.label)

assert account.label == "Account"
assert account.detail == "Project.Structs.Account"

assert order.label == "Order"
assert order.detail == "Project.Structs.Order"

assert user.label == "User"
assert user.detail == "Project.Structs.User"
end

@tag :skip
test "when using %, child structs are returned", %{project: project} do
assert [account, order, order_line, user] =
project
|> complete("%Project.|", "%")
|> complete("%Project.Structs.|", "%")
|> Enum.sort_by(& &1.label)

assert account.label == "Structs.Account"
Expand All @@ -143,10 +160,21 @@ defmodule Lexical.Server.CodeIntelligence.Completion.Translations.StructTest do
assert order.label == "Structs.Order"
assert order.detail == "Project.Structs.Order"

# NOTE: though we have stripped the "%" symbol, ElixirSense still returns structs,
# so I think we need to handle it in the same way as the module,
# which will bring more consistency.
assert order_line.label == "Structs.Order...(1 more struct)"
assert order_line.detail == "Project.Structs.Order."
end

test "when using % and child is not a struct, just `more` snippet is returned", %{
project: project
} do
assert [more] = complete(project, "%Project.|", "%")
assert more.label == "Structs...(4 more structs)"
assert more.detail == "Project.Structs."
end

test "it should complete struct fields", %{project: project} do
source = ~q[
defmodule Fake do
Expand Down
14 changes: 13 additions & 1 deletion apps/server/test/lexical/server/project/intelligence_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,10 @@ defmodule Lexical.Server.Project.IntelligenceTest do
Intelligence.collect_struct_modules(project, "Parent", from: :child, to: :child)

assert ["Parent.Child.GrandchildWithStruct"] =
Intelligence.collect_struct_modules(project, Parent.Child, from: :child, to: :child)
Intelligence.collect_struct_modules(project, Parent.Child,
from: :child,
to: :child
)
end

test "collecting a range of structs", %{project: project} do
Expand Down Expand Up @@ -172,5 +175,14 @@ defmodule Lexical.Server.Project.IntelligenceTest do
assert ["Parent.Child.GrandchildWithStruct"] =
Intelligence.collect_struct_modules(project, "Parent", 2..3)
end

test "collecting modules using `:infinity`", %{project: project} do
collected = Intelligence.collect_struct_modules(project, "Parent", :infinity)

assert [grandchild_struct, child_struct] = collected

assert child_struct == "Parent.ChildWithStruct"
assert grandchild_struct == "Parent.Child.GrandchildWithStruct"
end
end
end
3 changes: 2 additions & 1 deletion projects/lexical_shared/lib/lexical/formats.ex
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ defmodule Lexical.Formats do
end

defp templatize(count, template) do
String.replace(template, "${count}", "#{count}")
count_string = Integer.to_string(count)
String.replace(template, "${count}", count_string)
end
end

0 comments on commit 4febf4c

Please sign in to comment.