From 28501cb99c1ec40eb7eea84de6d3c97360d604ea Mon Sep 17 00:00:00 2001 From: Steve Cohen Date: Thu, 24 Aug 2023 17:19:29 -0700 Subject: [PATCH] Suggest a module name for defmodule completion (#338) * Suggest module name for defmodule The defmodule completion snippet now does a best-effort attempt to guess the module name. The guessing takes into account lib, test and test/support directories, and if it doesn't detect any of them, just uses the file name. --- .../completion/translations/macro.ex | 44 ++++++++++++++- .../completion/translations/macro_test.exs | 55 ++++++++++++++++++- .../completion/translations/struct_test.exs | 4 +- .../support/lexical/test/completion_case.ex | 20 ++++++- 4 files changed, 115 insertions(+), 8 deletions(-) diff --git a/apps/server/lib/lexical/server/code_intelligence/completion/translations/macro.ex b/apps/server/lib/lexical/server/code_intelligence/completion/translations/macro.ex index cf8ae36db..4dcae1cde 100644 --- a/apps/server/lib/lexical/server/code_intelligence/completion/translations/macro.ex +++ b/apps/server/lib/lexical/server/code_intelligence/completion/translations/macro.ex @@ -1,4 +1,5 @@ defmodule Lexical.Server.CodeIntelligence.Completion.Translations.Macro do + alias Lexical.Document alias Lexical.RemoteControl.Completion.Candidate alias Lexical.Server.CodeIntelligence.Completion.Env alias Lexical.Server.CodeIntelligence.Completion.Translatable @@ -52,9 +53,10 @@ defmodule Lexical.Server.CodeIntelligence.Completion.Translations.Macro do def translate(%Candidate.Macro{name: "defmodule"} = macro, builder, env) do label = "defmodule (Define a module)" + suggestion = suggest_module_name(env.document) snippet = """ - defmodule ${1:module name} do + defmodule ${1:#{suggestion}} do $0 end """ @@ -561,4 +563,44 @@ defmodule Lexical.Server.CodeIntelligence.Completion.Translations.Macro do def translate(%Candidate.Macro{}, _builder, _env) do :skip end + + def suggest_module_name(%Document{} = document) do + result = + document.path + |> Path.split() + |> Enum.reduce(false, fn + "lib", _ -> + {:lib, []} + + "test", _ -> + {:test, []} + + "support", {:test, _} -> + {:lib, []} + + _, false -> + false + + element, {type, elements} -> + camelized = + element + |> Path.rootname() + |> Macro.camelize() + + {type, [camelized | elements]} + end) + + case result do + {_, parts} -> + parts + |> Enum.reverse() + |> Enum.join(".") + + false -> + document.path + |> Path.basename() + |> Path.rootname() + |> Macro.camelize() + end + end end diff --git a/apps/server/test/lexical/server/code_intelligence/completion/translations/macro_test.exs b/apps/server/test/lexical/server/code_intelligence/completion/translations/macro_test.exs index b1ee0f0c1..b1ab4f365 100644 --- a/apps/server/test/lexical/server/code_intelligence/completion/translations/macro_test.exs +++ b/apps/server/test/lexical/server/code_intelligence/completion/translations/macro_test.exs @@ -107,7 +107,7 @@ defmodule Lexical.Server.CodeIntelligence.Completion.Translations.MacroTest do assert %Completion.Item{} = completion end - test "defmodule", %{project: project} do + test "defmodule for lib paths", %{project: project} do assert {:ok, completion} = project |> complete("defmodule|") @@ -118,7 +118,58 @@ defmodule Lexical.Server.CodeIntelligence.Completion.Translations.MacroTest do assert completion.insert_text_format == :snippet assert completion.insert_text == """ - defmodule ${1:module name} do + defmodule ${1:File} do + $0 + end + """ + end + + test "defmodule for test paths", %{project: project} do + assert {:ok, completion} = + project + |> complete("defmodule|", path: "test/foo/project/my_test.exs") + |> fetch_completion("defmodule ") + + assert completion.detail + assert completion.label == "defmodule (Define a module)" + assert completion.insert_text_format == :snippet + + assert completion.insert_text == """ + defmodule ${1:Foo.Project.MyTest} do + $0 + end + """ + end + + test "defmodule for test/support paths", %{project: project} do + assert {:ok, completion} = + project + |> complete("defmodule|", path: "test/support/path/to/file.ex") + |> fetch_completion("defmodule ") + + assert completion.detail + assert completion.label == "defmodule (Define a module)" + assert completion.insert_text_format == :snippet + + assert completion.insert_text == """ + defmodule ${1:Path.To.File} do + $0 + end + """ + end + + test "defmodule for other paths", %{project: project} do + assert {:ok, completion} = + project + |> complete("defmodule|", path: "/this/is/another/path.ex") + |> fetch_completion("defmodule ") + + assert completion.detail + assert completion.label == "defmodule (Define a module)" + assert completion.insert_text_format == :snippet + + assert completion.insert_text == """ + defmodule ${1:Path} do $0 end """ 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 ed905259b..9f8967479 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,7 +131,7 @@ 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.Structs.|", "%") + |> complete("%Project.Structs.|", trigger_character: "%") |> Enum.sort_by(& &1.label) assert account.label == "Account" @@ -150,7 +150,7 @@ defmodule Lexical.Server.CodeIntelligence.Completion.Translations.StructTest do test "when using % and child is not a struct, just `more` snippet is returned", %{ project: project } do - assert [more] = complete(project, "%Project.|", "%") + assert [more] = complete(project, "%Project.|", trigger_character: "%") assert more.label == "Structs...(4 more structs)" assert more.detail == "Project.Structs." end diff --git a/apps/server/test/support/lexical/test/completion_case.ex b/apps/server/test/support/lexical/test/completion_case.ex index 3ba112cc0..efd847efd 100644 --- a/apps/server/test/support/lexical/test/completion_case.ex +++ b/apps/server/test/support/lexical/test/completion_case.ex @@ -40,13 +40,27 @@ defmodule Lexical.Test.Server.CompletionCase do Document.to_string(edited_document) end - def complete(project, text, trigger_character \\ nil) do + def complete(project, text, opts \\ []) do + trigger_character = Keyword.get(opts, :trigger_character) {line, column} = cursor_position(text) text = strip_cursor(text) root_path = Project.root_path(project) - file_path = Path.join([root_path, "lib", "file.ex"]) + + file_path = + case Keyword.fetch(opts, :path) do + {:ok, path} -> + if Path.expand(path) == path do + # it's absolute + path + else + Path.join(root_path, path) + end + + :error -> + Path.join([root_path, "lib", "file.ex"]) + end document = file_path @@ -59,7 +73,7 @@ defmodule Lexical.Test.Server.CompletionCase do if is_binary(trigger_character) do CompletionContext.new( trigger_kind: :trigger_character, - trigger_character: trigger_character + trigger_character: nil ) else CompletionContext.new(trigger_kind: :trigger_character)