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 20, 2024
1 parent e86d49e commit f097634
Show file tree
Hide file tree
Showing 6 changed files with 251 additions and 34 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,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
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)
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()) ::
Expand Down Expand Up @@ -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
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 @@ -325,21 +314,86 @@ 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),
{: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 @@ -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
Expand Down
40 changes: 36 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,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

Expand All @@ -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)}")
Expand All @@ -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

0 comments on commit f097634

Please sign in to comment.