Skip to content

Commit

Permalink
wip - temp save
Browse files Browse the repository at this point in the history
  • Loading branch information
scottming committed Mar 19, 2024
1 parent d56c7db commit 257e77c
Show file tree
Hide file tree
Showing 6 changed files with 209 additions and 35 deletions.
Original file line number Diff line number Diff line change
@@ -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()) ::
Expand All @@ -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} ->
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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()) ::
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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, _} =
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -290,21 +279,85 @@ 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),
{:ok, entries} <- Search.Indexer.Source.index(document.path, text),
: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),
Expand All @@ -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
Expand Down
41 changes: 37 additions & 4 deletions apps/server/lib/lexical/server/provider/handlers/rename.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)}")
Expand All @@ -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

0 comments on commit 257e77c

Please sign in to comment.