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

Respect existing capitalization in defmodule completion #819

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
4 changes: 4 additions & 0 deletions apps/remote_control/lib/lexical/remote_control.ex
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ defmodule Lexical.RemoteControl do

defdelegate modules_with_prefix(prefix, predicate), to: RemoteControl.Modules, as: :with_prefix

defdelegate adjust_module_name_capitalization(name),
to: RemoteControl.Modules,
as: :adjust_name_capitalization

defdelegate docs(module, opts \\ []), to: CodeIntelligence.Docs, as: :for_module

defdelegate register_listener(listener_pid, message_types), to: RemoteControl.Dispatch
Expand Down
4 changes: 4 additions & 0 deletions apps/remote_control/lib/lexical/remote_control/api.ex
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ defmodule Lexical.RemoteControl.Api do
RemoteControl.call(project, RemoteControl, :modules_with_prefix, [prefix, predicate])
end

def adjust_module_name_capitalization(%Project{} = project, name) when is_binary(name) do
RemoteControl.call(project, RemoteControl, :adjust_module_name_capitalization, [name])
end

@spec docs(Project.t(), module()) :: {:ok, CodeIntelligence.Docs.t()} | {:error, any()}
def docs(%Project{} = project, module, opts \\ []) when is_atom(module) do
RemoteControl.call(project, RemoteControl, :docs, [module, opts])
Expand Down
35 changes: 35 additions & 0 deletions apps/remote_control/lib/lexical/remote_control/modules.ex
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,41 @@ defmodule Lexical.RemoteControl.Modules do
with_prefix("Elixir." <> prefix, mfa)
end

@doc """
Adjusts module name capitalization to existing modules' names in the project.

For example, when passed `Project.Http.Server`, it returns:
- `Project.HTTP.Server`, if there's any module named `Project.HTTP` or `Project.HTTP.*`
- `Project.Http.Server` otherwise
"""
@spec adjust_name_capitalization(String.t()) :: String.t()
def adjust_name_capitalization(name) do
name_downcase = String.downcase(name)
name_split = String.split(name, ".")
project_modules = all_modules()

closest_name_split =
project_modules
|> Enum.map(fn {module_string, _already_loaded?} ->
with "Elixir." <> ms <- module_string, do: ms
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure if this is the way 🤔

end)
|> Enum.max_by(fn module_string ->
:binary.longest_common_prefix([String.downcase(module_string), name_downcase])
end)
|> String.split(".")

prefix =
closest_name_split
|> Enum.zip(name_split)
|> Enum.take_while(fn {closest_name, name} ->
String.downcase(closest_name) == String.downcase(name)
end)
|> Enum.map(fn {closest_name, _name} -> closest_name end)

suffix = Enum.drop(name_split, length(prefix))
Enum.join(prefix ++ suffix, ".")
end
Comment on lines +208 to +233
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made a separate comment about a potentially different way of adjusting capitalization, but if we stick with this approach, I think this can be done with one iteration over existing modules:

Suggested change
def adjust_name_capitalization(name) do
name_downcase = String.downcase(name)
name_split = String.split(name, ".")
project_modules = all_modules()
closest_name_split =
project_modules
|> Enum.map(fn {module_string, _already_loaded?} ->
with "Elixir." <> ms <- module_string, do: ms
end)
|> Enum.max_by(fn module_string ->
:binary.longest_common_prefix([String.downcase(module_string), name_downcase])
end)
|> String.split(".")
prefix =
closest_name_split
|> Enum.zip(name_split)
|> Enum.take_while(fn {closest_name, name} ->
String.downcase(closest_name) == String.downcase(name)
end)
|> Enum.map(fn {closest_name, _name} -> closest_name end)
suffix = Enum.drop(name_split, length(prefix))
Enum.join(prefix ++ suffix, ".")
end
def adjust_name_capitalization(name) do
name_downcase = String.downcase(name)
{prefix, byte_length} =
Enum.reduce(all_modules(), {"", 0}, fn
{"Elixir." <> existing, _loaded?}, {prefix, length} ->
new_length =
:binary.longest_common_prefix([String.downcase(existing), name_downcase])
if new_length > length do
{binary_slice(existing, 0, new_length), new_length}
else
{prefix, length}
end
_, acc ->
acc
end)
<<_::binary-size(byte_length), suffix::binary>> = name
prefix <> suffix
end


defp apply_predicate(module_arg, {invoked_module, function, args}) do
apply(invoked_module, function, [module_arg | args])
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,6 @@ defmodule Project.Baz do
def baz do
end
end

defmodule Project.HTTP.Server do
end
11 changes: 11 additions & 0 deletions apps/remote_control/test/lexical/remote_control/modules_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,15 @@ defmodule Lexical.RemoteControl.ModulesTest do
Modules.with_prefix("GenEvent", {Kernel, :macro_exported?, [:__using__, 1]})
end
end

describe "case adjustment" do
test "doesn't change name if no prefix exists" do
name = "NonExistent.Module.Foo"
assert name == Modules.adjust_name_capitalization(name)
end

test "changes name if the prefix exists" do
assert "GenEvent.Foo.Bar" == Modules.adjust_name_capitalization("Genevent.Foo.Bar")
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ defmodule Lexical.Server.CodeIntelligence.Completion.Translations.Macro do
label = "defmodule (define a module)"
suggestion = suggest_module_name(env.document)

suggestion =
Lexical.RemoteControl.Api.adjust_module_name_capitalization(env.project, suggestion)

snippet = """
defmodule ${1:#{suggestion}} do
$0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,23 @@ defmodule Lexical.Server.CodeIntelligence.Completion.Translations.MacroTest do
"""
end

test "defmodule requiring capitalization adjustment", %{project: project} do
assert {:ok, completion} =
project
|> complete("defmodule|", path: "/lib/project/http/server/helper.ex")
|> fetch_completion("defmodule ")

assert completion.detail
assert completion.label == "defmodule (define a module)"
assert completion.insert_text_format == :snippet

assert apply_completion(completion) == """
defmodule ${1:Project.HTTP.Server.Helper} do
$0
end\
"""
end

test "defprotocol only has a single completion", %{project: project} do
assert {:ok, completion} =
project
Expand Down