From fbd8f3cc836d92759dbd614a71a27e15dabd328a Mon Sep 17 00:00:00 2001 From: Scott Ming Date: Sat, 16 Mar 2024 16:43:16 +0800 Subject: [PATCH] This PR is primarily copied from #374, but compared to the previous implementation, we have made two changes: 1. We constrained the scope of renaming, meaning that renaming a module is only allowed at its definition. This simplification reduces a lot of computations. 2. Based on the first point, our preparation no longer returns just a single local module, but instead, the entire module. This means that module renaming has become more powerful. We can not only rename a single local module but also simplify a module. For example, renaming TopLevel.Parent.Child to TopLevel.Child, or renaming some middle parts, like TopLevel.Parent.Child to TopLevel.Renamed. I personally really like this change, especially the second point, which makes module renaming much more practical. --- apps/common/lib/lexical/ast/module.ex | 20 -- .../lib/lexical/remote_control/api.ex | 4 +- .../lexical/remote_control/code_mod/rename.ex | 26 ++ .../code_mod/rename/callable.ex | 16 ++ .../remote_control/code_mod/rename/module.ex | 173 ++++++----- .../remote_control/code_mod/rename/prepare.ex | 80 ++++-- .../module_test.exs => rename_test.exs} | 271 +++++++++--------- 7 files changed, 331 insertions(+), 259 deletions(-) create mode 100644 apps/remote_control/lib/lexical/remote_control/code_mod/rename/callable.ex rename apps/remote_control/test/lexical/remote_control/code_mod/{rename/module_test.exs => rename_test.exs} (60%) diff --git a/apps/common/lib/lexical/ast/module.ex b/apps/common/lib/lexical/ast/module.ex index a7804ad09..20948809a 100644 --- a/apps/common/lib/lexical/ast/module.ex +++ b/apps/common/lib/lexical/ast/module.ex @@ -28,24 +28,4 @@ defmodule Lexical.Ast.Module do |> inspect() |> name() end - - @doc """ - local module name is the last part of a module name - - ## Examples: - - iex> local_name("Lexical.Ast.Module") - "Module" - """ - def local_name(entity) when is_atom(entity) do - entity - |> inspect() - |> local_name() - end - - def local_name(entity) when is_binary(entity) do - entity - |> String.split(".") - |> List.last() - end end diff --git a/apps/remote_control/lib/lexical/remote_control/api.ex b/apps/remote_control/lib/lexical/remote_control/api.ex index bf86527c8..e94ab8af6 100644 --- a/apps/remote_control/lib/lexical/remote_control/api.ex +++ b/apps/remote_control/lib/lexical/remote_control/api.ex @@ -52,11 +52,11 @@ defmodule Lexical.RemoteControl.Api do %Analysis{} = analysis, %Position{} = position ) do - RemoteControl.call(project, CodeIntelligence.Rename.Module, :prepare, [analysis, position]) + RemoteControl.call(project, CodeMod.Rename, :prepare, [analysis, position]) end def rename(%Project{} = project, %Analysis{} = analysis, %Position{} = position, new_name) do - RemoteControl.call(project, CodeIntelligence.Rename.Module, :rename, [ + RemoteControl.call(project, CodeMod.Rename, :rename, [ analysis, position, new_name diff --git a/apps/remote_control/lib/lexical/remote_control/code_mod/rename.ex b/apps/remote_control/lib/lexical/remote_control/code_mod/rename.ex index 1c3eda99c..79a3b3986 100644 --- a/apps/remote_control/lib/lexical/remote_control/code_mod/rename.ex +++ b/apps/remote_control/lib/lexical/remote_control/code_mod/rename.ex @@ -1,2 +1,28 @@ defmodule Lexical.RemoteControl.CodeMod.Rename do + alias Lexical.Ast.Analysis + alias Lexical.Document.Edit + alias Lexical.Document.Position + alias Lexical.Document.Range + alias Lexical.RemoteControl.CodeMod.Rename.Prepare + + @spec prepare(Analysis.t(), Position.t()) :: + {:ok, {atom(), String.t()}, Range.t()} | {:error, term()} + def prepare(%Analysis{} = analysis, %Position{} = position) do + Prepare.prepare(analysis, position) + end + + @renamable_mapping %{call: __MODULE__.Callable, module: __MODULE__.Module} + + @spec rename(Analysis.t(), Position.t(), String.t()) :: + {:ok, %{Lexical.uri() => [Edit.t()]}} | {:error, term()} + def rename(%Analysis{} = analysis, %Position{} = position, new_name) do + case Prepare.resolve(analysis, position) do + {:ok, {renamable, entity}, range} -> + rename_module = @renamable_mapping[renamable] + {:ok, rename_module.rename(range, new_name, entity)} + + {:error, error} -> + {:error, error} + end + end end diff --git a/apps/remote_control/lib/lexical/remote_control/code_mod/rename/callable.ex b/apps/remote_control/lib/lexical/remote_control/code_mod/rename/callable.ex new file mode 100644 index 000000000..5264a499b --- /dev/null +++ b/apps/remote_control/lib/lexical/remote_control/code_mod/rename/callable.ex @@ -0,0 +1,16 @@ +defmodule Lexical.RemoteControl.CodeMod.Rename.Callable do + alias Lexical.Ast.Analysis + alias Lexical.Document.Position + + alias Lexical.RemoteControl.CodeIntelligence.Entity + + def resolve(%Analysis{} = analysis, %Position{} = position) do + case Entity.resolve(analysis, position) do + {:ok, {callable, module, local_name, _}, range} when callable in [:call] -> + {:ok, {:call, {module, local_name}}, range} + + _ -> + {:error, :not_a_callable} + end + end +end diff --git a/apps/remote_control/lib/lexical/remote_control/code_mod/rename/module.ex b/apps/remote_control/lib/lexical/remote_control/code_mod/rename/module.ex index ec8694ecf..52c85449f 100644 --- a/apps/remote_control/lib/lexical/remote_control/code_mod/rename/module.ex +++ b/apps/remote_control/lib/lexical/remote_control/code_mod/rename/module.ex @@ -1,127 +1,144 @@ defmodule Lexical.RemoteControl.CodeMod.Rename.Module do - alias Lexical.Ast alias Lexical.Ast.Analysis alias Lexical.Document alias Lexical.Document.Edit + alias Lexical.Document.Line alias Lexical.Document.Position - alias Lexical.RemoteControl.CodeMod.Rename.Prepare + alias Lexical.Document.Range + alias Lexical.RemoteControl.CodeIntelligence.Entity alias Lexical.RemoteControl.Search.Store require Logger - @spec rename(Analysis.t(), Position.t(), String.t()) :: - {:ok, %{Lexical.uri() => [Edit.t()]}} | {:error, term()} - def rename(%Analysis{} = analysis, %Position{} = position, new_name) do - with {:ok, entity, range} <- Prepare.resolve_module(analysis, position) do - edits = - analysis.document - |> search_related_candidates(position, entity, range) - |> to_edits_by_uri(new_name) + import Line - {:ok, edits} - end - end - - defp search_related_candidates(document, position, entity, range) do - local_module_name = Prepare.local_module_name(range) - entities = exacts(entity, local_module_name) - - # Users won't always want to rename descendants of a module. - # For instance, when there are no more submodules after the cursor. - # like: `defmodule TopLevel.Mo|dule do` - # in most cases, users only want to rename the module itself. - # - # However, there's an exception when the cursor is in the middle, - # such as `Top.Mo|dule.ChildModule`. If we rename it to `Top.Renamed.Child`, - # it would be natural to also rename `Module.ChildModule` to `Renamed.Child`. - if at_the_middle_of_module?(document, position, range) do - entities ++ descendants(entity, local_module_name) - else - entities - end + @spec rename(Range.t(), String.t(), atom()) :: %{Lexical.uri() => [Edit.t()]} + def rename(%Range{} = old_range, new_name, entity) do + {old_suffix, new_suffix} = old_range |> range_text() |> diff(new_name) + results = exacts(entity, old_suffix) ++ descendants(entity, old_suffix) + to_edits_by_uri(results, new_suffix) end - defp at_the_middle_of_module?(document, position, range) do - range_text = Prepare.range_text(range) - - case Ast.surround_context(document, position) do - {:ok, %{context: {:alias, alias}}} -> - String.length(range_text) < length(alias) + @spec resolve(Analysis.t(), Position.t()) :: + {:ok, {atom(), atom()}, Range.t()} | {:error, term()} + def resolve(%Analysis{} = analysis, %Position{} = position) do + case Entity.resolve(analysis, position) do + {:ok, {module_or_struct, module}, range} when module_or_struct in [:struct, :module] -> + {:ok, {:module, module}, range} _ -> - false + {:error, :not_a_module} end end - defp descendants(entity, local_module_name) do - entity_string = inspect(entity) - prefix = "#{entity_string}." + defp diff(old_range_text, new_name) do + diff = String.myers_difference(old_range_text, new_name) - prefix - |> Store.prefix(type: :module) - |> Enum.filter(&(entry_matching?(&1, local_module_name) and has_dots_in_range?(&1))) - |> adjust_range(entity) + eq = + if match?([{:eq, _eq} | _], diff) do + diff |> hd() |> elem(1) + else + "" + end + + old_suffix = String.replace(old_range_text, ~r"^#{eq}", "") + new_suffix = String.replace(new_name, ~r"^#{eq}", "") + {old_suffix, new_suffix} end - defp exacts(entity, local_module_name) do + defp exacts(entity, old_suffix) do entity_string = inspect(entity) entity_string |> Store.exact(type: :module) - |> Enum.filter(&entry_matching?(&1, local_module_name)) + |> Enum.filter(&entry_matching?(&1, old_suffix)) + |> adjust_range_for_exacts(old_suffix) + end + + defp descendants(entity, old_suffix) do + prefix = "#{inspect(entity)}." + + prefix + |> Store.prefix(type: :module) + |> Enum.filter(&(entry_matching?(&1, old_suffix) and has_dots_in_range?(&1))) + |> adjust_range_for_descendants(entity, old_suffix) end - defp entry_matching?(entry, local_module_name) do - entry.range |> Prepare.range_text() |> String.contains?(local_module_name) + defp entry_matching?(entry, old_suffix) do + entry.range |> range_text() |> String.contains?(old_suffix) end defp has_dots_in_range?(entry) do - entry.range |> Prepare.range_text() |> String.contains?(".") + entry.range |> range_text() |> String.contains?(".") end - defp adjust_range(entries, entity) do + defp adjust_range_for_exacts(entries, old_suffix) do + for entry <- entries do + start_character = entry.range.end.character - String.length(old_suffix) + start_position = %{entry.range.start | character: start_character} + range = %{entry.range | start: start_position} + %{entry | range: range} + end + end + + defp adjust_range_for_descendants(entries, entity, old_suffix) do for entry <- entries, - uri = Document.Path.ensure_uri(entry.path), - range_result = resolve_local_module_range(uri, entry.range.start, entity), - match?({:ok, _}, range_result) do - {_, range} = range_result + range_text = range_text(entry.range), + matches = Regex.scan(~r"#{old_suffix}", range_text, return: :index), + result = resolve_module_range(entry, entity, matches), + match?({:ok, _}, result) do + {_, range} = result %{entry | range: range} end end - defp resolve_local_module_range(uri, position, entity) do + defp range_text(range) do + line(text: text) = range.end.context_line + String.slice(text, range.start.character - 1, range.end.character - range.start.character) + end + + defp resolve_module_range(_entry, _entity, []) do + {:error, :not_found} + end + + defp resolve_module_range(entry, _entity, [[{start, length}]]) do + range = adjust_range_characters(entry.range, {start, length}) + {:ok, range} + end + + defp resolve_module_range(entry, entity, [[{start, length}] | tail] = _matches) do + # This function is mainly for the duplicated suffixes + # For example, if we have a module named `Foo.Bar.Foo.Bar` and we want to rename it to `Foo.Bar.Baz` + # The `Foo.Bar` will be duplicated in the range text, so we need to resolve the correct range + # and only rename the second occurrence of `Foo.Bar` + uri = Document.Path.ensure_uri(entry.path) + with {:ok, _} <- Document.Store.open_temporary(uri), - {:ok, document, analysis} <- Document.Store.fetch(uri, :analysis), - {:ok, result, range} <- Prepare.resolve_module(analysis, position) do + {:ok, _document, analysis} <- Document.Store.fetch(uri, :analysis), + start_character = entry.range.start.character + start, + position = %{entry.range.start | character: start_character}, + {:ok, {:module, result}, range} <- resolve(analysis, position) do if result == entity do + range = adjust_range_characters(range, {start, length}) {:ok, range} else - last_part_length = result |> Ast.Module.local_name() |> String.length() - # Move the cursor to the next part: - # `|Parent.Next.Target.Child` -> 'Parent.|Next.Target.Child' -> 'Parent.Next.|Target.Child' - character = position.character + last_part_length + 1 - position = Position.new(document, position.line, character) - resolve_local_module_range(uri, position, entity) + resolve_module_range(entry, entity, tail) end - else - _ -> - Logger.error("Failed to find entity range for #{inspect(uri)} at #{inspect(position)}") - :error end end - defp to_edits_by_uri(results, new_name) do + defp adjust_range_characters(range, {start, length} = _matched_old_suffix) do + start_character = range.start.character + start + end_character = start_character + length + start_position = %{range.start | character: start_character} + end_end_position = %{range.start | character: end_character} + %{range | start: start_position, end: end_end_position} + end + + defp to_edits_by_uri(results, new_suffix) do Enum.group_by( results, &Document.Path.ensure_uri(&1.path), - fn result -> - local_module_name_length = result.range |> Prepare.local_module_name() |> String.length() - # e.g: `Parent.|ToBeRenameModule`, we need the start position of `ToBeRenameModule` - start_character = result.range.end.character - local_module_name_length - start_position = %{result.range.start | character: start_character} - - new_range = %{result.range | start: start_position} - Edit.new(new_name, new_range) - end + &Edit.new(new_suffix, &1.range) ) end end diff --git a/apps/remote_control/lib/lexical/remote_control/code_mod/rename/prepare.ex b/apps/remote_control/lib/lexical/remote_control/code_mod/rename/prepare.ex index 38985176c..3923cb087 100644 --- a/apps/remote_control/lib/lexical/remote_control/code_mod/rename/prepare.ex +++ b/apps/remote_control/lib/lexical/remote_control/code_mod/rename/prepare.ex @@ -1,43 +1,77 @@ defmodule Lexical.RemoteControl.CodeMod.Rename.Prepare do - alias Lexical.Ast alias Lexical.Ast.Analysis alias Lexical.Document.Position - alias Lexical.Document.Line alias Lexical.Document.Range alias Lexical.RemoteControl.CodeIntelligence.Entity + alias Lexical.RemoteControl.CodeMod.Rename + alias Lexical.RemoteControl.Search.Store - import Line + require Logger - @spec prepare(Analysis.t(), Position.t()) :: {:ok, String.t(), Range.t()} | {:error, term()} + @spec prepare(Analysis.t(), Position.t()) :: + {:ok, {atom(), String.t()}, Range.t()} | {:error, term()} def prepare(%Analysis{} = analysis, %Position{} = position) do - case resolve_module(analysis, position) do - {:ok, _, range} -> - {:ok, local_module_name(range), range} + case resolve(analysis, position) do + {:ok, {:module, module}, range} -> + {:ok, inspect(module), range} - {:error, _} -> - {:error, :unsupported_entity} + {:ok, {:call, {_m, f}}, range} -> + {:ok, to_string(f), range} + + {:error, error} -> + {:error, error} end end - def resolve_module(analysis, position) do - case Entity.resolve(analysis, position) do - {:ok, {module_or_struct, module}, range} when module_or_struct in [:struct, :module] -> - {:ok, module, range} - - {:ok, other, _} -> - {:error, {:unsupported_entity, other}} + @spec resolve(Analysis.t(), Position.t()) :: + {:ok, {atom(), atom()} | {atom(), tuple()}, Range.t()} | {:error, term()} + def resolve(%Analysis{} = analysis, %Position{} = position) do + case do_resolve(analysis, position) do + {:ok, {:module, module}, range} -> + if rename_at_declaration?(module, range) do + {:ok, {:module, module}, range} + else + {:error, {:unsupported_location, :module}} + end - {:error, reason} -> - {:error, reason} + other -> + other end end - def range_text(range) do - line(text: text) = range.end.context_line - String.slice(text, range.start.character - 1, range.end.character - range.start.character) + defp rename_at_declaration?(module, rename_range) do + case Store.exact(module, type: :module, subtype: :definition) do + [definition] -> + rename_range == definition.range + + _ -> + false + end end - def local_module_name(%Range{} = range) do - range |> range_text() |> Ast.Module.local_name() + @renamable_module [Rename.Callable, Rename.Module] + + defp do_resolve(%Analysis{} = analysis, %Position{} = position) do + result = + Enum.find_value(@renamable_module, fn module -> + result = module.resolve(analysis, position) + + if match?({:ok, _, _}, result) do + result + end + end) + + if is_nil(result) do + case Entity.resolve(analysis, position) do + {:ok, other, _} -> + Logger.info("Unsupported entity for renaming: #{inspect(other)}") + {:error, :unsupported_entity} + + {:error, reason} -> + {:error, reason} + end + else + result + end end end diff --git a/apps/remote_control/test/lexical/remote_control/code_mod/rename/module_test.exs b/apps/remote_control/test/lexical/remote_control/code_mod/rename_test.exs similarity index 60% rename from apps/remote_control/test/lexical/remote_control/code_mod/rename/module_test.exs rename to apps/remote_control/test/lexical/remote_control/code_mod/rename_test.exs index 362216feb..4adf69dc1 100644 --- a/apps/remote_control/test/lexical/remote_control/code_mod/rename/module_test.exs +++ b/apps/remote_control/test/lexical/remote_control/code_mod/rename_test.exs @@ -1,4 +1,4 @@ -defmodule Lexical.RemoteControl.CodeMod.Rename.ModuleTest do +defmodule Lexical.RemoteControl.CodeMod.RenameTest do alias Lexical.Document alias Lexical.Project alias Lexical.RemoteControl @@ -50,6 +50,72 @@ defmodule Lexical.RemoteControl.CodeMod.Rename.ModuleTest do %{uri: uri} end + describe "prepare/2" do + test "returns the module name" do + {:ok, result, _} = + ~q[ + defmodule |Foo do + end + ] |> prepare() + + assert result == "Foo" + end + + test "returns the whole module name" do + {:ok, result, _} = + ~q[ + defmodule TopLevel.|Foo do + end + ] |> prepare() + + assert result == "TopLevel.Foo" + end + + test "returns location error when renaming a module occurs in a reference." do + assert {:error, {:unsupported_location, :module}} == + ~q[ + defmodule Foo do + end + + defmodule Bar do + alias |Foo + end + ] |> prepare() + end + + test "returns the function name" do + {:ok, result, _} = + ~q[ + defmodule Foo do + def |bar do + end + end + ] |> prepare() + + assert result == "bar" + end + + test "returns the macro name" do + {:ok, result, _} = + ~q[ + defmodule Foo do + defmacro |bar(thing) do + end + end + ] |> prepare() + + assert result == "bar" + end + + test "returns error when the entity is not found" do + assert {:error, :not_found} = + ~q[ + x = 1 + |x + ] |> prepare() + end + end + describe "rename exact module" do test "succeeds when the cursor is at the definition" do {:ok, result} = @@ -61,27 +127,54 @@ defmodule Lexical.RemoteControl.CodeMod.Rename.ModuleTest do assert result =~ ~S[defmodule Renamed do] end - test "succeeds when the cursor is at the alias" do - {:ok, result} = - ~q[ + test "failed when the cursor is at the alias" do + assert {:error, {:unsupported_location, :module}} == + ~q[ defmodule Baz do alias |Foo end ] |> rename("Renamed") + end + + test "succeeds when the module has multiple dots" do + {:ok, result} = ~q[ + defmodule TopLevel.Foo.|Bar do + end + ] |> rename("TopLevel.Foo.Renamed") + + assert result =~ ~S[defmodule TopLevel.Foo.Renamed do] + end + + test "succeeds when renaming the middle part of the module" do + {:ok, result} = + ~q[ + defmodule TopLevel.Foo.|Bar do + end + ] |> rename("TopLevel.Renamed.Bar") + + assert result =~ ~S[defmodule TopLevel.Renamed.Bar do] + end - assert result =~ ~S[alias Renamed] + test "succeeds when simplifing the module name" do + {:ok, result} = + ~q[ + defmodule TopLevel.Foo.|Bar do + end + ] |> rename("TopLevel.Renamed") + + assert result =~ ~S[defmodule TopLevel.Renamed do] end test "succeeds when the definition is in a nested module" do {:ok, result} = ~q[ defmodule TopLevel do - defmodule Foo do + defmodule |Foo do end end defmodule TopLevelTest do - alias TopLevel.|Foo + alias TopLevel.Foo end ] |> rename("Renamed") @@ -100,69 +193,20 @@ defmodule Lexical.RemoteControl.CodeMod.Rename.ModuleTest do test "succeeds when the cursor is in the multiple aliases off of single alias" do {:ok, result} = ~q[ + defmodule Foo.|Second do + end + defmodule TopLevel do alias Foo.{ First, Second, Third.Fourth } - |Second end - ] |> rename("Renamed") + ] |> rename("Foo.Renamed") assert result =~ ~S[ First, Renamed,] end - test "only rename the aliased when the cursor is at the aliased" do - {:ok, result} = - ~q[ - defmodule TopLevel do - alias Foo.Bar, as: FooBar - |FooBar - end - ] - |> rename("Renamed") - - assert result =~ ~S[alias Foo.Bar, as: Renamed] - assert result =~ ~S[ Renamed] - end - - test "succeeds when the cursor is at the aliased child" do - {:ok, result} = - ~q[ - defmodule TopLevel.Foo.Bar do - end - - defmodule TopLevel.Another do - alias TopLevel.Foo, as: Parent - Parent.|Bar - end - ] - |> rename("Renamed") - - assert result =~ ~S[defmodule TopLevel.Foo.Renamed] - - assert result =~ ~S[ Parent.Renamed] - end - - test "only rename aliased when the cursor is at the aliased" do - {:ok, result} = - ~q[ - defmodule TopLevel.Foo.Bar do - end - - defmodule TopLevel.Another do - alias TopLevel.Foo, as: Parent - |Parent.Bar - end - ] - |> rename("Renamed") - - assert result =~ ~S[defmodule TopLevel.Foo.Bar do] - - assert result =~ ~S[alias TopLevel.Foo, as: Renamed] - assert result =~ ~S[ Renamed.Bar] - end - test "shouldn't rename the relative module" do {:ok, result} = ~q[ @@ -175,81 +219,54 @@ defmodule Lexical.RemoteControl.CodeMod.Rename.ModuleTest do assert result =~ ~S[defmodule FooTest do] end + end - test "shouldn't rename the descendants when the cursor is at the end of the module" do + describe "rename descendants" do + test "rename the descendants" do {:ok, result} = ~q[ - defmodule TopLevel.Module do # ✓ + defmodule TopLevel.|Module do end - defmodule TopLevel.Module.Another do # x - alias TopLevel.|Module + defmodule TopLevel.Module.Another do end - ] |> rename("Renamed") - - refute result =~ ~S[defmodule TopLevel.Renamed.Another] + ] |> rename("TopLevel.Renamed") + assert result =~ ~S[defmodule TopLevel.Renamed.Another] assert result =~ ~S[defmodule TopLevel.Renamed do] - assert result =~ ~S[alias TopLevel.Renamed] - end - end - - describe "rename descendants" do - test "succeeds when the cursor is in the middle of definition" do - {:ok, result} = - ~q[ - defmodule TopLevel.|Middle.Module do - alias TopLevel.Middle.Module - end - ] |> rename("Renamed") - - assert result =~ ~S[defmodule TopLevel.Renamed.Module] - assert result =~ ~S[alias TopLevel.Renamed.Module] - end - - test "succeeds when the cursor is in the middle of reference" do - {:ok, result} = - ~q[ - defmodule TopLevel.Second.Middle.Module do - alias TopLevel.Second.|Middle.Module - end - ] |> rename("Renamed") - - assert result =~ ~S[defmodule TopLevel.Second.Renamed.Module] - assert result =~ ~S[alias TopLevel.Second.Renamed.Module] end test "succeeds when there are same module name is in the cursor neighborhood" do {:ok, result} = ~q[ - defmodule TopLevel.Foo do + defmodule Foo.Bar.Foo.|Bar do end - defmodule TopLevel.Foo.Foo do + defmodule Foo.Bar.Foo.Bar.Baz do end defmodule TopLevel.Another do - alias TopLevel.Foo.|Foo + alias Foo.Bar.Foo.Bar.Baz end - ] |> rename("Renamed") + ] |> rename("Foo.Bar.Foo.Renamed") - assert result =~ ~S[defmodule TopLevel.Foo do] - assert result =~ ~S[defmodule TopLevel.Foo.Renamed do] - assert result =~ ~S[alias TopLevel.Foo.Renamed] + assert result =~ ~S[defmodule Foo.Bar.Foo.Renamed do] + assert result =~ ~S[defmodule Foo.Bar.Foo.Renamed.Baz do] + assert result =~ ~S[alias Foo.Bar.Foo.Renamed.Baz] end test "succeeds even if there are descendants with the same name" do {:ok, result} = ~q[ - defmodule TopLevel.Foo do + defmodule TopLevel.|Foo do defmodule Foo do # skip this end end defmodule TopLevel.Bar do - alias TopLevel.|Foo.Foo + alias TopLevel.Foo.Foo end ] - |> rename("Renamed") + |> rename("TopLevel.Renamed") assert result =~ ~S[defmodule TopLevel.Renamed do] assert result =~ ~S[defmodule Foo do # skip this] @@ -275,37 +292,6 @@ defmodule Lexical.RemoteControl.CodeMod.Rename.ModuleTest do assert result =~ ~S[defmodule Renamed do] assert result =~ ~S[%Renamed{}] end - - test "succeeds when the cursor is at the reference" do - {:ok, result} = - ~q[ - defmodule Foo do - defstruct bar: 1 - end - - defmodule Bar do - def foo do - %Fo|o{} - end - end - ] |> rename("Renamed") - - assert result =~ ~S[defmodule Renamed do] - assert result =~ ~S[defmodule Bar do] - assert result =~ ~S[%Renamed{}] - end - end - - describe "unsupported" do - test "rename a function" do - assert {:error, {:unsupported_entity, {:call, Foo, :bar, 0}}} = - ~q[ - defmodule Foo do - def |bar do - end - end - ] |> rename("baz") - end end defp rename(%Project{} = project \\ project(), source, new_name) do @@ -316,12 +302,25 @@ defmodule Lexical.RemoteControl.CodeMod.Rename.ModuleTest do {:ok, entries} <- Search.Indexer.Source.index(document.path, text), :ok <- Search.Store.replace(entries), analysis = Lexical.Ast.analyze(document), - {:ok, uri_with_changes} <- Rename.Module.rename(analysis, position, new_name) do + {:ok, uri_with_changes} <- Rename.rename(analysis, position, new_name) do changes = uri_with_changes |> Map.values() |> List.flatten() {:ok, apply_edits(document, changes)} end end + defp prepare(project \\ project(), code) do + uri = subject_uri(project) + + with {position, text} <- pop_cursor(code), + {:ok, document} <- open_document(uri, text), + {:ok, entries} <- Search.Indexer.Source.index(document.path, text), + :ok <- Search.Store.replace(entries), + analysis = Lexical.Ast.analyze(document), + {:ok, result} <- Rename.prepare(analysis, position) do + result + end + end + defp subject_uri(project) do project |> file_path(Path.join("lib", "project.ex"))