Skip to content

Commit

Permalink
Rename file when renaming module (lexical-lsp#651)
Browse files Browse the repository at this point in the history
* Rename the file while renaming the module.

It first determines whether the module meets certain hard constraints, such as having no parent, then performs some conventional checks. If both are satisfied, it proceeds to return the file renaming.

* Maybe insert special folder for PhoenixWeb's module

* Apply some code review changes for using `Document.Changes`

Use `Document.Changes` instead of `CodeMod.Rename.DocumentChanges`

Make `Document.Changes.RenameFile` a struct and use `root_module?`
instead of `has_silibings?` and `has_parent?`

* Use `rename progress marker` instead of suspending build

* Use `file_changed` instead of `file_compile_requested` for reindexing

* `uri_with_operations_counts` by client name

* Apply some code review suggestions for `rename/file`

* Apply suggestions from code review

Co-authored-by: Steve Cohen <[email protected]>

* Do not need to care about the `DidClose` message

* Apply some code review suggestions

1. Move the `if-else` logic to the `Commands.Rename` module
2. Add `file_opened` message type
3. modify the format error message

* Shouldn't returns `RenameFile` if the file name not changed

* Change `uri_with_operation_counts` to `uri_with_expected_operation`

and special some message type for emacs

---------

Co-authored-by: Steve Cohen <[email protected]>
  • Loading branch information
scottming and scohen committed May 31, 2024
1 parent 2e2e176 commit a1f0369
Show file tree
Hide file tree
Showing 21 changed files with 777 additions and 86 deletions.
38 changes: 38 additions & 0 deletions apps/remote_control/lib/lexical/remote_control/api.ex
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,21 @@ defmodule Lexical.RemoteControl.Api do
RemoteControl.call(project, RemoteControl, :compile_document, [document])
end

def maybe_schedule_compile(project, triggered_file_uri, triggered_message) do
RemoteControl.call(project, Build, :maybe_schedule_compile, [
triggered_file_uri,
triggered_message
])
end

def maybe_compile_document(project, document, triggered_message) do
RemoteControl.call(project, Build, :maybe_compile_document, [
project,
document,
triggered_message
])
end

def expand_alias(
%Project{} = project,
segments_or_module,
Expand Down Expand Up @@ -54,6 +69,29 @@ defmodule Lexical.RemoteControl.Api do
])
end

def prepare_rename(
%Project{} = project,
%Analysis{} = analysis,
%Position{} = position
) do
RemoteControl.call(project, CodeMod.Rename, :prepare, [analysis, position])
end

def rename(
%Project{} = project,
%Analysis{} = analysis,
%Position{} = position,
new_name,
client_name
) do
RemoteControl.call(project, CodeMod.Rename, :rename, [
analysis,
position,
new_name,
client_name
])
end

def complete(%Project{} = project, %Env{} = env) do
Logger.info("Completion for #{inspect(env.position)}")
RemoteControl.call(project, RemoteControl, :complete, [env])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ defmodule Lexical.RemoteControl.Api.Messages do

defrecord :file_changed, uri: nil, from_version: nil, to_version: nil, open?: false

defrecord :file_opened, uri: nil, version: nil

defrecord :file_compile_requested, project: nil, build_number: 0, uri: nil

defrecord :file_compiled,
Expand Down Expand Up @@ -79,6 +81,12 @@ defmodule Lexical.RemoteControl.Api.Messages do
open?: boolean()
)

@type file_opened ::
record(:file_opened,
uri: Lexical.uri(),
version: non_neg_integer()
)

@type file_compile_requested ::
record(:file_compile_requested,
project: Lexical.Project.t(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ defmodule Lexical.RemoteControl.Application do
[
RemoteControl.Api.Proxy,
{RemoteControl.Commands.Reindex, nil},
RemoteControl.Commands.Rename,
RemoteControl.Module.Loader,
{RemoteControl.Dispatch, progress: true},
RemoteControl.ModuleMappings,
Expand Down
17 changes: 17 additions & 0 deletions apps/remote_control/lib/lexical/remote_control/build.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ defmodule Lexical.RemoteControl.Build do
alias Lexical.RemoteControl
alias Lexical.RemoteControl.Build.Document.Compilers.HEEx
alias Lexical.RemoteControl.Build.State
alias Lexical.RemoteControl.Commands.Rename
alias Lexical.VM.Versions

require Logger
Expand Down Expand Up @@ -35,6 +36,22 @@ defmodule Lexical.RemoteControl.Build do
:ok
end

def maybe_schedule_compile(triggered_file_uri, message) do
if Rename.in_progress?() do
Rename.update_progress(triggered_file_uri, message)
else
GenServer.cast(__MODULE__, {:compile, false})
end
end

def maybe_compile_document(%Project{} = project, %Document{} = document, message) do
if Rename.in_progress?() do
Rename.update_progress(document.uri, message)
else
compile_document(project, document)
end
end

# this is for testing
def force_compile_document(%Document{} = document) do
with false <- Path.absname(document.path) == "mix.exs",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,22 @@ defmodule Lexical.RemoteControl.CodeIntelligence.Entity do
)
end

@spec phoenix_controller_module?(module()) :: boolean()
def phoenix_controller_module?(module) do
function_exists?(module, :call, 2) and function_exists?(module, :action, 2)
end

@spec phoenix_liveview_module?(module()) :: boolean()
def phoenix_liveview_module?(module) do
function_exists?(module, :mount, 3) and function_exists?(module, :render, 1)
end

@spec phoenix_component_module?(module()) :: boolean()
def phoenix_component_module?(module) do
function_exists?(module, :__components__, 0) and
function_exists?(module, :__phoenix_component_verify__, 1)
end

defp check_commented(%Analysis{} = analysis, %Position{} = position) do
if Analysis.commented?(analysis, position) do
:error
Expand Down Expand Up @@ -259,14 +275,6 @@ defmodule Lexical.RemoteControl.CodeIntelligence.Entity do
end
end

defp phoenix_controller_module?(module) do
function_exists?(module, :call, 2) and function_exists?(module, :action, 2)
end

defp phoenix_liveview_module?(module) do
function_exists?(module, :mount, 3) and function_exists?(module, :render, 1)
end

# Take only the segments at and before the cursor, e.g.
# Foo|.Bar.Baz -> Foo
# Foo.|Bar.Baz -> Foo.Bar
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ defmodule Lexical.RemoteControl.CodeMod.Format do
alias Lexical.RemoteControl
alias Lexical.RemoteControl.Build
alias Lexical.RemoteControl.CodeMod.Diff
alias Lexical.RemoteControl.Commands.Rename

require Logger

Expand Down Expand Up @@ -99,13 +100,22 @@ defmodule Lexical.RemoteControl.CodeMod.Format do
def edits(%Document{} = document) do
project = RemoteControl.get_project()

with :ok <- Build.compile_document(project, document),
with :ok <- ensure_not_renaming(),
:ok <- Build.compile_document(project, document),
{:ok, formatted} <- do_format(project, document) do
edits = Diff.diff(document, formatted)
{:ok, Changes.new(document, edits)}
end
end

defp ensure_not_renaming do
if Rename.in_progress?() do
{:error, :rename_in_progress}
else
:ok
end
end

defp do_format(%Project{} = project, %Document{} = document) do
project_path = Project.project_path(project)

Expand Down
50 changes: 45 additions & 5 deletions apps/remote_control/lib/lexical/remote_control/code_mod/rename.ex
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
defmodule Lexical.RemoteControl.CodeMod.Rename do
alias Lexical.Ast.Analysis
alias Lexical.Document.Edit
alias Lexical.Document
alias Lexical.Document.Position
alias Lexical.Document.Range
alias Lexical.Protocol.Notifications.DidChange
alias Lexical.Protocol.Notifications.DidSave
alias Lexical.RemoteControl.Commands

alias __MODULE__

@spec prepare(Analysis.t(), Position.t()) ::
Expand All @@ -11,12 +15,48 @@ 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()}
def rename(%Analysis{} = analysis, %Position{} = position, new_name) do
@spec rename(Analysis.t(), Position.t(), String.t(), String.t() | nil) ::
{:ok, [Document.Changes.t()]} | {:error, term()}
def rename(%Analysis{} = analysis, %Position{} = position, new_name, client_name) do
with {:ok, {renamable, entity}, range} <- Rename.Prepare.resolve(analysis, position) do
rename_module = @rename_mapping[renamable]
{:ok, rename_module.rename(range, new_name, entity)}
results = rename_module.rename(range, new_name, entity)
set_rename_progress(results, client_name)
{:ok, results}
end
end

defp set_rename_progress(document_changes_list, client_name) do
client_name
|> uri_with_expected_operation(document_changes_list)
|> Commands.Rename.set_rename_progress()
end

defp uri_with_expected_operation(client_name, document_changes_list)
when client_name in ["Visual Studio Code", "emacs"] do
document_changes_list
|> Enum.flat_map(fn %Document.Changes{document: document, rename_file: rename_file} ->
if rename_file do
# when the file is renamed, we won't receive `DidSave` for the old file
[{rename_file.old_uri, DidChange}, {rename_file.new_uri, DidSave}]
else
[{document.uri, DidSave}]
end
end)
|> Map.new()
end

defp uri_with_expected_operation(_, document_changes_list) do
document_changes_list
|> Enum.flat_map(fn %Document.Changes{document: document, rename_file: rename_file} ->
if rename_file do
[{document.uri, DidSave}]
else
# Some editors do not directly save the file after renaming, such as *neovim*.
# when the file is not renamed, we'll only received `DidChange` for the old file
[{document.uri, DidChange}]
end
end)
|> Map.new()
end
end
Loading

0 comments on commit a1f0369

Please sign in to comment.