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

Second attempt to make struct completion more consistent #225

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
Expand Up @@ -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
Expand All @@ -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)")

Expand All @@ -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.
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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}."
]

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.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", %{
Expand All @@ -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."

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
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
13 changes: 13 additions & 0 deletions projects/lexical_shared/lib/lexical/formats.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading
Loading