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..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 @@ -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 @@ -20,42 +25,80 @@ defmodule Lexical.Server.CodeIntelligence.Completion.Translations.ModuleOrBehavi end defp do_translate(%_{} = module, builder, %Env{} = env) do - struct_reference? = Env.in_context?(env, :struct_reference) + if Env.in_context?(env, :struct_reference) do + complete_in_struct_reference(env, builder, module) + else + detail = builder.fallback(module.summary, module.full_name) + completion(env, builder, module.name, detail) + end + end - defines_struct? = Intelligence.defines_struct?(env.project, module.full_name) + 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) - defines_struct_in_descendents? = - immediate_descendent_defines_struct?(env.project, module.full_name) and - length(immediate_descendent_structs) > 1 - - cond do - struct_reference? and defines_struct_in_descendents? and defines_struct? -> - more = length(immediate_descendent_structs) - 1 + 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_mapset = MapSet.new(immediate_descendent_structs) + 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_or_struct.full_name, ancestor, module_or_struct.name) + + more = + env.project + |> Intelligence.collect_struct_modules(ancestor, to: :infinity) + |> Enum.count() + + if struct?(ancestor, structs_mapset) 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) - - 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 struct?(module, structs_mapset) do + MapSet.member?(structs_mapset, module) + end - true -> - detail = builder.fallback(module.summary, module.name) - completion(env, builder, module.name, detail) - 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 + # 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)") @@ -64,7 +107,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 +117,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..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,29 +1,19 @@ defmodule Lexical.Server.CodeIntelligence.Completion.Translations.Struct do alias Future.Code, as: Code - alias Lexical.RemoteControl.Completion.Candidate + alias Lexical.Formats 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 + 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..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 @@ -131,20 +131,28 @@ defmodule Lexical.Server.CodeIntelligence.Completion.Translations.StructTest do 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" + 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" - assert order_line.label == "Structs.Order.Line" - assert order_line.detail == "Project.Structs.Order.Line" + assert order_line.label == "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