From f097634ade7e7986ac99bf9f2933563de93d7af5 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 | 99 ++++++++++++++++ .../remote_control/code_mod/rename/module.ex | 23 +++- .../remote_control/code_mod/rename_test.exs | 107 ++++++++++++++---- .../server/provider/handlers/rename.ex | 40 ++++++- 6 files changed, 251 insertions(+), 34 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..10c3ee8da --- /dev/null +++ b/apps/remote_control/lib/lexical/remote_control/code_mod/rename/file.ex @@ -0,0 +1,99 @@ +defmodule Lexical.RemoteControl.CodeMod.Rename.File do + alias Lexical.Ast + alias Lexical.Document + alias Lexical.RemoteControl.Search.Store + alias Lexical.RemoteControl + + def maybe_rename(entry, new_suffix) do + with false <- has_parent?(entry), + false <- has_any_siblings?(entry) do + rename_path(entry, new_suffix) + 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 fetch_new_name(entry, new_suffix) do + uri = Document.Path.ensure_uri(entry.path) + text_edits = [Document.Edit.new(new_suffix, entry.range)] + + with {:ok, document} <- Document.Store.open_temporary(uri), + {:ok, edited_document} = + Document.apply_content_changes(document, document.version + 1, text_edits), + {:ok, %{context: {:alias, alias}}} <- + Ast.surround_context(edited_document, entry.range.start) do + {:ok, to_string(alias)} + else + _ -> :error + end + end + + defp rename_path(entry, new_suffix) do + relative_path = relative_path(entry.path) + + with {:ok, prefix} <- fetch_conventional_prefix(relative_path), + {:ok, new_name} <- fetch_new_name(entry, new_suffix) do + extname = Path.extname(entry.path) + suffix = Macro.underscore(new_name) + new_path = Path.join(prefix, "#{suffix}#{extname}") + + {Document.Path.ensure_uri(entry.path), Document.Path.ensure_uri(new_path)} + else + _ -> nil + end + end + + defp relative_path(path) do + Path.relative_to(path, root_path()) + end + + defp root_path do + RemoteControl.get_project() + |> Map.get(:root_uri) + |> Document.Path.ensure_path() + end + + defp fetch_conventional_prefix(path) do + result = + path + |> Path.split() + |> Enum.chunk_every(2, 2) + |> Enum.reduce({[], []}, fn + ["apps", app_name], _ -> + {[], [app_name, "apps"]} + + ["lib", follow_element], {elements, prefix} -> + {[follow_element | elements], ["lib" | prefix]} + + ["test", follow_element], {elements, prefix} -> + {[follow_element | elements], ["test" | prefix]} + + remain, {elements, prefix} -> + {remain ++ elements, prefix} + end) + + case result do + {_, []} -> + :error + + {_module_path, prefix} -> + prefix = prefix |> Enum.reverse() |> Enum.join("/") + {:ok, prefix} + 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 f1ba2059b..3cd4a6f4d 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) 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, new_suffix) + 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,13 @@ defmodule Lexical.RemoteControl.CodeMod.Rename.Module do |> adjust_range_for_descendants(entity, old_suffix) end + defp maybe_rename_file(entries, new_suffix) do + entries + |> Enum.map(&Rename.File.maybe_rename(&1, new_suffix)) + # every group should have only one `rename_file` + |> Enum.find(&(not is_nil(&1))) + 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 e679d0573..b61435d92 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 @@ -325,8 +314,62 @@ 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 "it shouldn't rename file if the module has parent module within that file" do + {:ok, {_applied, nil}} = + ~q[ + defmodule FooServer do + defmodule |State do + end + end + ] |> rename("Renamed", "lib/foo_server.ex") + end + + test "it shouldn't rename file if the module has any siblings within that file" do + assert {:ok, {_applied, nil}} = + ~q[ + defmodule |Foo do + end + + defmodule Bar do + end + ] |> rename("Renamed", "lib/foo.ex") + end + + test "it shouldn't rename file if the path doesn't match the any convensions" do + assert {:ok, {_applied, nil}} = + ~q[ + defmodule |Foo.Mix do + end + ] |> rename("Renamed", "mix.ex") + end + + test "succeeds when the path matching the `lib/*` convension", %{project: project} do + {:ok, {_applied, rename_file}} = + ~q[ + defmodule |Foo do + end + ] |> rename("Renamed", "lib/foo.ex") + + assert {_, to_uri} = rename_file + assert to_uri == subject_uri(project, "lib/renamed.ex") + end + + test "succeeds when the path matching the `apps/*` convension", %{project: project} do + {:ok, {_applied, rename_file}} = + ~q[ + defmodule |Foo.Bar do + end + ] |> rename("Renamed.Bar", "apps/an_app/lib/foo/bar.ex") + + assert {_, to_uri} = rename_file + assert to_uri == subject_uri(project, "apps/an_app/lib/renamed.ex") + 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), @@ -334,12 +377,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), @@ -352,10 +406,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..765b455b7 100644 --- a/apps/server/lib/lexical/server/provider/handlers/rename.ex +++ b/apps/server/lib/lexical/server/provider/handlers/rename.ex @@ -3,8 +3,11 @@ 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.RemoteControl.CodeMod.Rename.DocumentChanges alias Lexical.Server.Provider.Env require Logger @@ -21,12 +24,25 @@ 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 = + Enum.map(results, fn %DocumentChanges{edits: edits, uri: uri} -> + new_text_document_edit(uri, edits) + 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 +52,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