From 6dd0aabcfe7785574f0d45c6443db904f23ea7b0 Mon Sep 17 00:00:00 2001 From: Scott Ming Date: Wed, 2 Aug 2023 09:12:26 +0800 Subject: [PATCH 1/3] Second attempt to make struct completion more consistent 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`. --- .../translations/module_or_behaviour.ex | 81 ++++++++++++------- .../completion/translations/struct.ex | 11 ++- .../lexical/server/project/intelligence.ex | 27 +++++-- .../translations/module_or_behaviour_test.exs | 15 ++-- .../completion/translations/struct_test.exs | 34 +++++++- .../server/project/intelligence_test.exs | 14 +++- .../lexical_shared/lib/lexical/formats.ex | 13 +++ .../test/lexical/formats_test.exs | 12 +++ 8 files changed, 155 insertions(+), 52 deletions(-) diff --git a/apps/server/lib/lexical/server/code_intelligence/completion/translations/module_or_behaviour.ex b/apps/server/lib/lexical/server/code_intelligence/completion/translations/module_or_behaviour.ex index d01829203..a6c3606e5 100644 --- a/apps/server/lib/lexical/server/code_intelligence/completion/translations/module_or_behaviour.ex +++ b/apps/server/lib/lexical/server/code_intelligence/completion/translations/module_or_behaviour.ex @@ -20,40 +20,55 @@ defmodule Lexical.Server.CodeIntelligence.Completion.Translations.ModuleOrBehavi end defp do_translate(%_{} = module, builder, %Env{} = env) do - struct_reference? = Env.in_context?(env, :struct_reference) - - defines_struct? = Intelligence.defines_struct?(env.project, module.full_name) + if Env.in_context?(env, :struct_reference) do + complete_in_struct_reference(env, builder, module) + else + detail = builder.fallback(module.summary, module.name) + completion(env, builder, module.name, detail) + end + end + defp complete_in_struct_reference(%Env{} = env, builder, module) do immediate_descendent_structs = immediate_descendent_struct_modules(env.project, module.full_name) - defines_struct_in_descendents? = - immediate_descendent_defines_struct?(env.project, module.full_name) and - length(immediate_descendent_structs) > 1 + 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) - cond do - struct_reference? and defines_struct_in_descendents? and defines_struct? -> - more = length(immediate_descendent_structs) - 1 + 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, module.name, module.full_name, more), - Translations.Struct.completion(env, builder, module.name, module.full_name) + 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 - struct_reference? and defines_struct? -> - Translations.Struct.completion(env, builder, module.name, module.full_name) + defp struct?(module, structs_map) do + Map.has_key?(structs_map, module) + end - struct_reference? and - immediate_descendent_defines_struct?(env.project, module.full_name) -> - Enum.map(immediate_descendent_structs, fn child_module_name -> - local_name = local_module_name(module.full_name, child_module_name) - Translations.Struct.completion(env, builder, local_name, child_module_name) - 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 - true -> - detail = builder.fallback(module.summary, module.name) - completion(env, builder, module.name, detail) - end + 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 @@ -64,7 +79,7 @@ defmodule Lexical.Server.CodeIntelligence.Completion.Translations.ModuleOrBehavi |> builder.boost(0, 2) end - defp local_module_name(parent_module, child_module) do + defp local_module_name(parent_module, child_module, aliased_module) do # Returns the "local" module name, so if you're completing # Types.Som and the module completion is "Types.Something.Else", # "Something.Else" is returned. @@ -74,18 +89,22 @@ defmodule Lexical.Server.CodeIntelligence.Completion.Translations.ModuleOrBehavi local_module_name = Enum.join(parent_pieces, ".") local_module_length = String.length(local_module_name) - child_module - |> String.slice(local_module_length..-1) - |> strip_leading_period() + local_name = + child_module + |> String.slice(local_module_length..-1) + |> strip_leading_period() + + if String.starts_with?(local_name, aliased_module) do + local_name + else + [_ | tail] = String.split(local_name, ".") + Enum.join([aliased_module | tail], ".") + end end defp strip_leading_period(<<".", rest::binary>>), do: rest defp strip_leading_period(string_without_period), do: string_without_period - defp immediate_descendent_defines_struct?(%Lexical.Project{} = project, module_name) do - Intelligence.defines_struct?(project, module_name, to: :grandchild) - end - defp immediate_descendent_struct_modules(%Lexical.Project{} = project, module_name) do Intelligence.collect_struct_modules(project, module_name, to: :grandchild) end diff --git a/apps/server/lib/lexical/server/code_intelligence/completion/translations/struct.ex b/apps/server/lib/lexical/server/code_intelligence/completion/translations/struct.ex index 535fb33d1..1a6bbbe0e 100644 --- a/apps/server/lib/lexical/server/code_intelligence/completion/translations/struct.ex +++ b/apps/server/lib/lexical/server/code_intelligence/completion/translations/struct.ex @@ -1,5 +1,7 @@ defmodule Lexical.Server.CodeIntelligence.Completion.Translations.Struct do alias Future.Code, as: Code + alias Lexical.Formats + alias Lexical.RemoteControl.Completion.Candidate alias Lexical.Server.CodeIntelligence.Completion.Env alias Lexical.Server.CodeIntelligence.Completion.Translatable @@ -20,10 +22,17 @@ defmodule Lexical.Server.CodeIntelligence.Completion.Translations.Struct do end end + 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 + singular = "${count} more struct" + plural = "${count} more structs" + builder_opts = [ kind: :module, - label: "#{module_name}...(#{more} more structs)", + label: "#{module_name}...(#{Formats.plural(more, singular, plural)})", detail: "#{full_name}." ] diff --git a/apps/server/lib/lexical/server/project/intelligence.ex b/apps/server/lib/lexical/server/project/intelligence.ex index e73004725..394a79d21 100644 --- a/apps/server/lib/lexical/server/project/intelligence.ex +++ b/apps/server/lib/lexical/server/project/intelligence.ex @@ -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 @@ -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 @@ -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 @@ -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, @@ -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() @@ -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 @@ -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) diff --git a/apps/server/test/lexical/server/code_intelligence/completion/translations/module_or_behaviour_test.exs b/apps/server/test/lexical/server/code_intelligence/completion/translations/module_or_behaviour_test.exs index 66d51744e..048a2e317 100644 --- a/apps/server/test/lexical/server/code_intelligence/completion/translations/module_or_behaviour_test.exs +++ b/apps/server/test/lexical/server/code_intelligence/completion/translations/module_or_behaviour_test.exs @@ -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.Line" - 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", %{ @@ -221,9 +216,9 @@ defmodule Lexical.Server.CodeIntelligence.Completion.Translations.ModuleOrBehavi %Ord| ] - [order_line, order] = complete(project, source) + [order, order_line] = complete(project, source) - assert order_line.label == "Order...(1 more structs)" + assert order_line.label == "Order...(1 more struct)" assert order_line.kind == :module assert apply_completion(order_line) =~ "%Order." diff --git a/apps/server/test/lexical/server/code_intelligence/completion/translations/struct_test.exs b/apps/server/test/lexical/server/code_intelligence/completion/translations/struct_test.exs index a59ca4b94..d51815e0a 100644 --- a/apps/server/test/lexical/server/code_intelligence/completion/translations/struct_test.exs +++ b/apps/server/test/lexical/server/code_intelligence/completion/translations/struct_test.exs @@ -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" @@ -143,8 +160,19 @@ defmodule Lexical.Server.CodeIntelligence.Completion.Translations.StructTest do assert order.label == "Structs.Order" assert order.detail == "Project.Structs.Order" - assert order_line.label == "Structs.Order.Line" - assert order_line.detail == "Project.Structs.Order.Line" + # 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 diff --git a/apps/server/test/lexical/server/project/intelligence_test.exs b/apps/server/test/lexical/server/project/intelligence_test.exs index 95b88ef65..4536599d9 100644 --- a/apps/server/test/lexical/server/project/intelligence_test.exs +++ b/apps/server/test/lexical/server/project/intelligence_test.exs @@ -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 @@ -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 diff --git a/projects/lexical_shared/lib/lexical/formats.ex b/projects/lexical_shared/lib/lexical/formats.ex index 2875d041d..752a15a9a 100644 --- a/projects/lexical_shared/lib/lexical/formats.ex +++ b/projects/lexical_shared/lib/lexical/formats.ex @@ -88,4 +88,17 @@ defmodule Lexical.Formats do defp to_milliseconds(millis, :millisecond) do millis end + + def plural(count, singular, plural) do + case count do + 0 -> templatize(count, plural) + 1 -> templatize(count, singular) + _n -> templatize(count, plural) + end + end + + defp templatize(count, template) do + count_string = Integer.to_string(count) + String.replace(template, "${count}", count_string) + end end diff --git a/projects/lexical_shared/test/lexical/formats_test.exs b/projects/lexical_shared/test/lexical/formats_test.exs index b4f73ee95..850f38335 100644 --- a/projects/lexical_shared/test/lexical/formats_test.exs +++ b/projects/lexical_shared/test/lexical/formats_test.exs @@ -33,4 +33,16 @@ defmodule Lexical.FormatsTest do assert "0.02 ms" = Formats.time(20) end end + + describe "plural/3" do + test "returns singular when count is 1" do + assert Formats.plural(1, "${count} apple", "${count} apples") == "1 apple" + end + + test "returns plural when count is not 1" do + assert Formats.plural(0, "${count} apple", "${count} apples") == "0 apples" + assert Formats.plural(2, "${count} apple", "${count} apples") == "2 apples" + assert Formats.plural(3, "${count} apple", "${count} apples") == "3 apples" + end + end end From a99d436f3d9d3605d8fd0a1807a5010bf3230133 Mon Sep 17 00:00:00 2001 From: Scott Ming Date: Sat, 29 Jul 2023 21:23:38 +0800 Subject: [PATCH 2/3] Fix the inconsistency of struct completion Though we have stripped the `%` symbol, ElixirSense still returns structs sometimes, So we need to handle them in the same way as the module, which will bring more consistency. --- .../translations/module_or_behaviour.ex | 39 ++++++++++++++++--- .../completion/translations/struct.ex | 19 --------- .../completion/translations/struct_test.exs | 28 ++----------- 3 files changed, 37 insertions(+), 49 deletions(-) diff --git a/apps/server/lib/lexical/server/code_intelligence/completion/translations/module_or_behaviour.ex b/apps/server/lib/lexical/server/code_intelligence/completion/translations/module_or_behaviour.ex index a6c3606e5..f252f5792 100644 --- a/apps/server/lib/lexical/server/code_intelligence/completion/translations/module_or_behaviour.ex +++ b/apps/server/lib/lexical/server/code_intelligence/completion/translations/module_or_behaviour.ex @@ -5,12 +5,17 @@ defmodule Lexical.Server.CodeIntelligence.Completion.Translations.ModuleOrBehavi alias Lexical.Server.CodeIntelligence.Completion.Translations alias Lexical.Server.Project.Intelligence - use Translatable.Impl, for: [Candidate.Module, Candidate.Behaviour, Candidate.Protocol] + use Translatable.Impl, + for: [Candidate.Module, Candidate.Struct, Candidate.Behaviour, Candidate.Protocol] def translate(%Candidate.Module{} = module, builder, %Env{} = env) do do_translate(module, builder, env) end + def translate(%Candidate.Struct{} = module, builder, %Env{} = env) do + do_translate(module, builder, env) + end + def translate(%Candidate.Behaviour{} = behaviour, builder, %Env{} = env) do do_translate(behaviour, builder, env) end @@ -23,21 +28,43 @@ defmodule Lexical.Server.CodeIntelligence.Completion.Translations.ModuleOrBehavi if Env.in_context?(env, :struct_reference) do complete_in_struct_reference(env, builder, module) else - detail = builder.fallback(module.summary, module.name) + detail = builder.fallback(module.summary, module.full_name) completion(env, builder, module.name, detail) end end - defp complete_in_struct_reference(%Env{} = env, builder, module) do + defp complete_in_struct_reference(%Env{} = env, builder, %Candidate.Struct{} = struct) do + immediate_descendent_structs = + immediate_descendent_struct_modules(env.project, struct.full_name) + + if Enum.empty?(immediate_descendent_structs) do + Translations.Struct.completion(env, builder, struct.name, struct.full_name) + else + do_complete_in_struct_reference(env, builder, struct, immediate_descendent_structs) + end + end + + defp complete_in_struct_reference(%Env{} = env, builder, %Candidate.Module{} = module) do 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) + do_complete_in_struct_reference(env, builder, module, immediate_descendent_structs) + end + + defp do_complete_in_struct_reference( + %Env{} = env, + builder, + module_or_struct, + immediate_descendent_structs + ) do + structs_map = + Map.new(immediate_descendent_structs, fn module_or_struct -> {module_or_struct, true} end) + + dot_counts = module_dot_counts(module_or_struct.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) + local_name = local_module_name(module_or_struct.full_name, ancestor, module_or_struct.name) more = env.project diff --git a/apps/server/lib/lexical/server/code_intelligence/completion/translations/struct.ex b/apps/server/lib/lexical/server/code_intelligence/completion/translations/struct.ex index 1a6bbbe0e..6ba1c6866 100644 --- a/apps/server/lib/lexical/server/code_intelligence/completion/translations/struct.ex +++ b/apps/server/lib/lexical/server/code_intelligence/completion/translations/struct.ex @@ -1,26 +1,7 @@ defmodule Lexical.Server.CodeIntelligence.Completion.Translations.Struct do alias Future.Code, as: Code alias Lexical.Formats - - alias Lexical.RemoteControl.Completion.Candidate alias Lexical.Server.CodeIntelligence.Completion.Env - alias Lexical.Server.CodeIntelligence.Completion.Translatable - alias Lexical.Server.CodeIntelligence.Completion.Translations - - use Translatable.Impl, for: Candidate.Struct - - def translate(%Candidate.Struct{} = struct, builder, %Env{} = env) do - if Env.in_context?(env, :struct_reference) do - completion(env, builder, struct.name, struct.full_name) - else - Translations.ModuleOrBehaviour.completion( - env, - builder, - struct.name, - struct.full_name - ) - end - end def completion(%Env{} = _env, _builder, _module_name, _full_name, 0) do nil diff --git a/apps/server/test/lexical/server/code_intelligence/completion/translations/struct_test.exs b/apps/server/test/lexical/server/code_intelligence/completion/translations/struct_test.exs index d51815e0a..ed905259b 100644 --- a/apps/server/test/lexical/server/code_intelligence/completion/translations/struct_test.exs +++ b/apps/server/test/lexical/server/code_intelligence/completion/translations/struct_test.exs @@ -128,42 +128,22 @@ 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.Structs.|", "%") |> Enum.sort_by(& &1.label) - assert account.label == "Structs.Account" + assert account.label == "Account" assert account.detail == "Project.Structs.Account" - assert user.label == "Structs.User" + assert user.label == "User" assert user.detail == "Project.Structs.User" - assert order.label == "Structs.Order" + assert order.label == "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.label == "Order...(1 more struct)" assert order_line.detail == "Project.Structs.Order." end From dfaf17fc4a0120fe35578d5e32d5448fbfa8c402 Mon Sep 17 00:00:00 2001 From: Scott Ming Date: Wed, 2 Aug 2023 09:07:14 +0800 Subject: [PATCH 3/3] Apply the code review suggestions * Use `Mapset` instead of `Map` * Use recursive function instead of `String.graphemes/1` --- .../translations/module_or_behaviour.ex | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/apps/server/lib/lexical/server/code_intelligence/completion/translations/module_or_behaviour.ex b/apps/server/lib/lexical/server/code_intelligence/completion/translations/module_or_behaviour.ex index f252f5792..7d418fba3 100644 --- a/apps/server/lib/lexical/server/code_intelligence/completion/translations/module_or_behaviour.ex +++ b/apps/server/lib/lexical/server/code_intelligence/completion/translations/module_or_behaviour.ex @@ -57,9 +57,7 @@ defmodule Lexical.Server.CodeIntelligence.Completion.Translations.ModuleOrBehavi module_or_struct, immediate_descendent_structs ) do - structs_map = - Map.new(immediate_descendent_structs, fn module_or_struct -> {module_or_struct, true} end) - + structs_mapset = MapSet.new(immediate_descendent_structs) dot_counts = module_dot_counts(module_or_struct.full_name) ancestors = ancestors(immediate_descendent_structs, dot_counts) @@ -71,7 +69,7 @@ defmodule Lexical.Server.CodeIntelligence.Completion.Translations.ModuleOrBehavi |> Intelligence.collect_struct_modules(ancestor, to: :infinity) |> Enum.count() - if struct?(ancestor, structs_map) do + if struct?(ancestor, structs_mapset) do [ Translations.Struct.completion(env, builder, local_name, ancestor), Translations.Struct.completion(env, builder, local_name, ancestor, more - 1) @@ -82,8 +80,8 @@ defmodule Lexical.Server.CodeIntelligence.Completion.Translations.ModuleOrBehavi end) end - defp struct?(module, structs_map) do - Map.has_key?(structs_map, module) + defp struct?(module, structs_mapset) do + MapSet.member?(structs_mapset, module) end defp ancestors(results, dot_counts) do @@ -94,9 +92,12 @@ defmodule Lexical.Server.CodeIntelligence.Completion.Translations.ModuleOrBehavi |> Enum.uniq() end - defp module_dot_counts(module_name) do - module_name |> String.graphemes() |> Enum.count(&(&1 == ".")) - end + # this skips grapheme translations + defp module_dot_counts(module_name), do: module_dot_counts(module_name, 0) + + defp module_dot_counts(<<>>, count), do: count + defp module_dot_counts(<<".", rest::binary>>, count), do: module_dot_counts(rest, count + 1) + defp module_dot_counts(<<_::utf8, rest::binary>>, count), do: module_dot_counts(rest, count) def completion(%Env{} = env, builder, module_name, detail \\ nil) do detail = builder.fallback(detail, "#{module_name} (Module)")