From 257e77c884db9a5a89efed664b6b273b14238f95 Mon Sep 17 00:00:00 2001 From: Scott Ming Date: Tue, 19 Mar 2024 14:37:34 +0800 Subject: [PATCH] wip - temp save --- .../lexical/remote_control/code_mod/rename.ex | 4 +- .../code_mod/rename/document_changes.ex | 12 ++ .../remote_control/code_mod/rename/file.ex | 52 +++++++++ .../remote_control/code_mod/rename/module.ex | 29 +++-- .../remote_control/code_mod/rename_test.exs | 106 ++++++++++++++---- .../server/provider/handlers/rename.ex | 41 ++++++- 6 files changed, 209 insertions(+), 35 deletions(-) create mode 100644 apps/remote_control/lib/lexical/remote_control/code_mod/rename/document_changes.ex create mode 100644 apps/remote_control/lib/lexical/remote_control/code_mod/rename/file.ex 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 350692f70..3fc0bb578 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,8 +1,8 @@ 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.DocumentChanges alias __MODULE__ @spec prepare(Analysis.t(), Position.t()) :: @@ -14,7 +14,7 @@ defmodule Lexical.RemoteControl.CodeMod.Rename do @rename_mapping %{module: Rename.Module} @spec rename(Analysis.t(), Position.t(), String.t()) :: - {:ok, %{Lexical.uri() => [Edit.t()]}} | {:error, term()} + {:ok, [DocumentChanges.t()]} | {:error, term()} def rename(%Analysis{} = analysis, %Position{} = position, new_name) do case Rename.Prepare.resolve(analysis, position) do {:ok, {renamable, entity}, range} -> diff --git a/apps/remote_control/lib/lexical/remote_control/code_mod/rename/document_changes.ex b/apps/remote_control/lib/lexical/remote_control/code_mod/rename/document_changes.ex new file mode 100644 index 000000000..a3ef92ffd --- /dev/null +++ b/apps/remote_control/lib/lexical/remote_control/code_mod/rename/document_changes.ex @@ -0,0 +1,12 @@ +defmodule Lexical.RemoteControl.CodeMod.Rename.DocumentChanges do + defstruct [:uri, :edits, :rename_file] + + @type t :: %__MODULE__{ + uri: Lexical.uri(), + edits: [Lexical.Document.Edit.t()], + rename_file: {Lexical.uri(), Lexical.uri()} | nil + } + def new(uri, edits, rename_file \\ nil) do + %__MODULE__{uri: uri, edits: edits, rename_file: rename_file} + end +end diff --git a/apps/remote_control/lib/lexical/remote_control/code_mod/rename/file.ex b/apps/remote_control/lib/lexical/remote_control/code_mod/rename/file.ex new file mode 100644 index 000000000..c73640a20 --- /dev/null +++ b/apps/remote_control/lib/lexical/remote_control/code_mod/rename/file.ex @@ -0,0 +1,52 @@ +defmodule Lexical.RemoteControl.CodeMod.Rename.File do + alias Lexical.Document + alias Lexical.RemoteControl.Search.Store + require Logger + + def maybe_rename(entry, diff) do + with true <- not has_parent?(entry), + true <- not has_any_siblings(entry) do + {old_suffix, new_suffix} = diff + + to_path = rename_path(entry.path, old_suffix, new_suffix) + to_path && {Document.Path.ensure_uri(entry.path), Document.Path.ensure_uri(to_path)} + else + _ -> nil + end + end + + defp has_parent?(entry) do + case Store.parent(entry) do + {:ok, _} -> true + _ -> false + end + end + + defp has_any_siblings(entry) do + case Store.siblings(entry) do + {:ok, [_]} -> false + {:ok, [_ | _]} -> true + _ -> false + end + end + + defp rename_path(path, old_suffix, new_suffix) do + from_path_suffix = Macro.underscore(old_suffix) + to_path_suffix = new_suffix |> Macro.underscore() + + root_name = Path.rootname(path) + # Most of our module renaming involves renaming the *latter* part, + # so we should start from the back and only replace once. + reversed = String.reverse(root_name) + reversed_old = String.reverse(from_path_suffix) + reversed_to = String.reverse(to_path_suffix) + extname = Path.extname(path) + + if String.ends_with?(root_name, from_path_suffix) do + result = + reversed |> String.replace(reversed_old, reversed_to, global: false) |> String.reverse() + + result <> extname + 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 4ce0d48c5..3921a15cb 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 @@ -6,21 +6,25 @@ defmodule Lexical.RemoteControl.CodeMod.Rename.Module do alias Lexical.Document.Position alias Lexical.Document.Range alias Lexical.RemoteControl.CodeIntelligence.Entity + alias Lexical.RemoteControl.CodeMod.Rename alias Lexical.RemoteControl.Search.Store + alias Lexical.RemoteControl.CodeMod.Rename.DocumentChanges require Logger import Line - @spec rename(Range.t(), String.t(), atom()) :: %{Lexical.uri() => [Edit.t()]} + @spec rename(Range.t(), String.t(), atom()) :: [DocumentChanges.t()] def rename(%Range{} = old_range, new_name, entity) do - {old_suffix, new_suffix} = old_range |> range_text() |> diff(new_name) + {old_suffix, new_suffix} = diff = old_range |> range_text() |> diff(new_name) results = exacts(entity, old_suffix) ++ descendants(entity, old_suffix) - Enum.group_by( - results, - &Document.Path.ensure_uri(&1.path), - &Edit.new(new_suffix, &1.range) - ) + results + |> Enum.group_by(&Document.Path.ensure_uri(&1.path)) + |> Enum.map(fn {uri, entries} -> + rename_file = maybe_rename_file(entries, diff) + edits = Enum.map(entries, &Edit.new(new_suffix, &1.range)) + DocumentChanges.new(uri, edits, rename_file) + end) end @spec resolve(Analysis.t(), Position.t()) :: @@ -68,6 +72,17 @@ defmodule Lexical.RemoteControl.CodeMod.Rename.Module do |> adjust_range_for_descendants(entity, old_suffix) end + defp maybe_rename_file(entries, diff) do + entries + |> Enum.map(&Rename.File.maybe_rename(&1, diff)) + |> Enum.find_value(fn x -> + # every group should have only one `rename_file` + if not is_nil(x) do + x + end + end) + end + defp entry_matching?(entry, old_suffix) do entry.range |> range_text() |> String.contains?(old_suffix) end diff --git a/apps/remote_control/test/lexical/remote_control/code_mod/rename_test.exs b/apps/remote_control/test/lexical/remote_control/code_mod/rename_test.exs index 1f428ffc7..ce062b43c 100644 --- a/apps/remote_control/test/lexical/remote_control/code_mod/rename_test.exs +++ b/apps/remote_control/test/lexical/remote_control/code_mod/rename_test.exs @@ -1,6 +1,5 @@ defmodule Lexical.RemoteControl.CodeMod.RenameTest do alias Lexical.Document - alias Lexical.Project alias Lexical.RemoteControl alias Lexical.RemoteControl.CodeMod.Rename alias Lexical.RemoteControl.Search @@ -40,16 +39,6 @@ defmodule Lexical.RemoteControl.CodeMod.RenameTest do {:ok, project: project} end - setup %{project: project} do - uri = subject_uri(project) - - on_exit(fn -> - Document.Store.close(uri) - end) - - %{uri: uri} - end - describe "prepare/2" do test "returns the module name" do {:ok, result, _} = @@ -71,7 +60,7 @@ defmodule Lexical.RemoteControl.CodeMod.RenameTest do assert result == "TopLevel.Foo" end - test "returns the whole module name even if the cusor is not at the end" do + test "returns the whole module name even if the cursor is not at the end" do {:ok, result, _} = ~q[ defmodule Top|Level.Foo do @@ -217,7 +206,7 @@ defmodule Lexical.RemoteControl.CodeMod.RenameTest do end end - describe "rename descendants" do + describe "rename module descendants" do test "rename the descendants" do {:ok, result} = ~q[ defmodule TopLevel.|Module do @@ -290,8 +279,61 @@ defmodule Lexical.RemoteControl.CodeMod.RenameTest do end end - defp rename(%Project{} = project \\ project(), source, new_name) do - uri = subject_uri(project) + describe "rename file" do + test "succeeds when the path matching the `lib/*` rule", %{project: project} do + {:ok, {applied, rename_file}} = + ~q[ + defmodule |Foo do + end + ] |> rename("Renamed", "lib/foo.ex") + + assert applied =~ ~S[defmodule Renamed do] + assert {_, to_uri} = rename_file + assert to_uri == subject_uri(project, "lib/renamed.ex") + end + + test "it shouldn't rename file if the module has parent module within that file" do + {:ok, {applied, rename_file}} = + ~q[ + defmodule FooServer do + defmodule |State do + end + end + ] |> rename("Renamed", "lib/foo_server.ex") + + assert applied =~ ~S[defmodule Renamed do] + assert is_nil(rename_file) + end + + test "it shouldn't rename file if the module has any siblings within that file" do + {:ok, {applied, rename_file}} = + ~q[ + defmodule |Foo do + end + + defmodule Bar do + end + ] |> rename("Renamed", "lib/foo.ex") + + assert applied =~ ~S[defmodule Renamed do] + assert is_nil(rename_file) + end + + test "it shouldn't rename file if the module file not ends_with renamed module name" do + {:ok, {applied, rename_file}} = + ~q[ + defmodule |Foo do + end + ] |> rename("Renamed", "lib/special_file.ex") + + assert applied =~ ~S[defmodule Renamed do] + assert is_nil(rename_file) + end + end + + defp rename(source, new_name, path \\ nil) do + project = project() + uri = subject_uri(project, path) with {position, text} <- pop_cursor(source), {:ok, document} <- open_document(uri, text), @@ -299,12 +341,23 @@ defmodule Lexical.RemoteControl.CodeMod.RenameTest do :ok <- Search.Store.replace(entries), analysis = Lexical.Ast.analyze(document), {:ok, uri_with_changes} <- Rename.rename(analysis, position, new_name) do - changes = uri_with_changes |> Map.values() |> List.flatten() - {:ok, apply_edits(document, changes)} + changes = uri_with_changes |> Enum.map(& &1.edits) |> List.flatten() + applied = apply_edits(document, changes) + + result = + if path do + rename_file = uri_with_changes |> Enum.map(& &1.rename_file) |> List.first() + {applied, rename_file} + else + applied + end + + {:ok, result} end end - defp prepare(project \\ project(), code) do + defp prepare(code) do + project = project() uri = subject_uri(project) with {position, text} <- pop_cursor(code), @@ -317,10 +370,19 @@ defmodule Lexical.RemoteControl.CodeMod.RenameTest do end end - defp subject_uri(project) do - project - |> file_path(Path.join("lib", "project.ex")) - |> Document.Path.ensure_uri() + defp subject_uri(project, path \\ nil) do + path = path || Path.join("wont_rename_file_folder", "project.ex") + + uri = + project + |> file_path(path) + |> Document.Path.ensure_uri() + + on_exit(fn -> + Document.Store.close(uri) + end) + + uri end defp open_document(uri, content) do diff --git a/apps/server/lib/lexical/server/provider/handlers/rename.ex b/apps/server/lib/lexical/server/provider/handlers/rename.ex index 31abbaced..33cfe82f2 100644 --- a/apps/server/lib/lexical/server/provider/handlers/rename.ex +++ b/apps/server/lib/lexical/server/provider/handlers/rename.ex @@ -3,7 +3,9 @@ defmodule Lexical.Server.Provider.Handlers.Rename do alias Lexical.Document alias Lexical.Protocol.Requests.Rename alias Lexical.Protocol.Responses - alias Lexical.Protocol.Types.Workspace.Edit + alias Lexical.Protocol.Types.RenameFile + alias Lexical.Protocol.Types.TextDocument + alias Lexical.Protocol.Types.Workspace alias Lexical.RemoteControl.Api alias Lexical.Server.Provider.Env require Logger @@ -21,12 +23,27 @@ defmodule Lexical.Server.Provider.Handlers.Rename do defp rename(project, analysis, position, new_name, id) do case Api.rename(project, analysis, position, new_name) do - {:ok, results} when results == %{} -> + {:ok, []} -> {:reply, nil} {:ok, results} -> - edit = Edit.new(changes: results) - {:reply, Responses.Rename.new(id, edit)} + text_document_edits = + results + |> Enum.group_by(& &1.uri, fn result -> result.edits end) + |> Enum.map(fn {uri, edits_list} -> + new_text_document_edit(uri, List.flatten(edits_list)) + end) + + rename_files = + results + |> Stream.map(& &1.rename_file) + |> Stream.reject(&(&1 == nil)) + |> Enum.map(&new_rename_file/1) + + workspace_edit = + Workspace.Edit.new(document_changes: text_document_edits ++ rename_files) + + {:reply, Responses.Rename.new(id, workspace_edit)} {:error, {:unsupported_entity, entity}} -> Logger.info("Unrenameable entity: #{inspect(entity)}") @@ -36,4 +53,20 @@ defmodule Lexical.Server.Provider.Handlers.Rename do {:reply, Responses.Rename.error(id, :request_failed, inspect(reason))} end end + + defp new_text_document_edit(uri, edits) do + text_document = TextDocument.OptionalVersioned.Identifier.new(uri: uri, version: 0) + TextDocument.Edit.new(edits: edits, text_document: text_document) + end + + defp new_rename_file({from_uri, to_uri}) do + options = RenameFile.Options.new(overwrite: true) + + RenameFile.new( + kind: "rename", + new_uri: to_uri, + old_uri: from_uri, + options: options + ) + end end