-
-
Notifications
You must be signed in to change notification settings - Fork 83
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Introduced rename functionality across modules
Enhanced the application by integrating rename functionality, increasing its editing capabilities. This includes supporting the preparation and execution of rename operations in various modules such as the lexical AST module, protocol request and response modules, remote control API, and server provider handling. Key additions entail aliasing for easier reference, handling new rename-related requests and responses, and implementing server state updates to acknowledge rename capabilities. The changes underscore our dedication to improving the tool's utility and ensuring a more dynamic and responsive user experience in code manipulation scenarios. Filter out the `:struct` type references Add some handler tests for `rename` Add doc for `local_module_name` Move `rename` from `code_intelligence` to `code_mod` Move the prepare logic to a individual module This PR is primarily copied from #374, but compared to the previous implementation, we have made two changes: 1. We constrained the scope of renaming, meaning that renaming a module is only allowed at its definition. This simplification reduces a lot of computations. 2. Based on the first point, our preparation no longer returns just a single local module, but instead, the entire module. This means that module renaming has become more powerful. We can not only rename a single local module but also simplify a module. For example, renaming TopLevel.Parent.Child to TopLevel.Child, or renaming some middle parts, like TopLevel.Parent.Child to TopLevel.Renamed. I personally really like this change, especially the second point, which makes module renaming much more practical. Surround the whole module when renaming happens Remove logic related to the rename function. Apply some code review suggestions Fix a bug when expanding the module Fix a merge error Apply a format module usage suggestion Update apps/remote_control/lib/lexical/remote_control/code_mod/rename.ex Co-authored-by: Steve Cohen <[email protected]> Apply the `defdelegate` suggestion Fix a bug when the descendant containing the old suffix
- Loading branch information
Showing
15 changed files
with
940 additions
and
1 deletion.
There are no files selected for viewing
10 changes: 10 additions & 0 deletions
10
apps/protocol/lib/generated/lexical/protocol/types/prepare_rename/params.ex
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
# This file's contents are auto-generated. Do not edit. | ||
defmodule Lexical.Protocol.Types.PrepareRename.Params do | ||
alias Lexical.Proto | ||
alias Lexical.Protocol.Types | ||
use Proto | ||
|
||
deftype position: Types.Position, | ||
text_document: Types.TextDocument.Identifier, | ||
work_done_token: optional(Types.Progress.Token) | ||
end |
23 changes: 23 additions & 0 deletions
23
apps/protocol/lib/generated/lexical/protocol/types/prepare_rename_result.ex
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
# This file's contents are auto-generated. Do not edit. | ||
defmodule Lexical.Protocol.Types.PrepareRenameResult do | ||
alias Lexical.Proto | ||
alias Lexical.Protocol.Types | ||
|
||
defmodule PrepareRenameResult do | ||
use Proto | ||
deftype placeholder: string(), range: Types.Range | ||
end | ||
|
||
defmodule PrepareRenameResult1 do | ||
use Proto | ||
deftype default_behavior: boolean() | ||
end | ||
|
||
use Proto | ||
|
||
defalias one_of([ | ||
Types.Range, | ||
Lexical.Protocol.Types.PrepareRenameResult.PrepareRenameResult, | ||
Lexical.Protocol.Types.PrepareRenameResult.PrepareRenameResult1 | ||
]) | ||
end |
11 changes: 11 additions & 0 deletions
11
apps/protocol/lib/generated/lexical/protocol/types/rename/params.ex
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
# This file's contents are auto-generated. Do not edit. | ||
defmodule Lexical.Protocol.Types.Rename.Params do | ||
alias Lexical.Proto | ||
alias Lexical.Protocol.Types | ||
use Proto | ||
|
||
deftype new_name: string(), | ||
position: Types.Position, | ||
text_document: Types.TextDocument.Identifier, | ||
work_done_token: optional(Types.Progress.Token) | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
22 changes: 22 additions & 0 deletions
22
apps/remote_control/lib/lexical/remote_control/code_mod/rename.ex
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
defmodule Lexical.RemoteControl.CodeMod.Rename do | ||
alias Lexical.Ast.Analysis | ||
alias Lexical.Document.Edit | ||
alias Lexical.Document.Position | ||
alias Lexical.Document.Range | ||
alias __MODULE__ | ||
|
||
@spec prepare(Analysis.t(), Position.t()) :: | ||
{:ok, {atom(), String.t()}, Range.t()} | {:error, term()} | ||
defdelegate prepare(analysis, position), to: Rename.Prepare | ||
|
||
@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 | ||
with {:ok, {renamable, entity}, range} <- Rename.Prepare.resolve(analysis, position) do | ||
rename_module = @rename_mapping[renamable] | ||
{:ok, rename_module.rename(range, new_name, entity)} | ||
end | ||
end | ||
end |
164 changes: 164 additions & 0 deletions
164
apps/remote_control/lib/lexical/remote_control/code_mod/rename/module.ex
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,164 @@ | ||
defmodule Lexical.RemoteControl.CodeMod.Rename.Module do | ||
alias Lexical.Ast.Analysis | ||
alias Lexical.Document | ||
alias Lexical.Document.Edit | ||
alias Lexical.Document.Line | ||
alias Lexical.Document.Position | ||
alias Lexical.Document.Range | ||
alias Lexical.RemoteControl.CodeIntelligence.Entity | ||
alias Lexical.RemoteControl.Search.Store | ||
require Logger | ||
|
||
import Line | ||
|
||
@spec rename(Range.t(), String.t(), atom()) :: %{Lexical.uri() => [Edit.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) | ||
) | ||
end | ||
|
||
@spec resolve(Analysis.t() | Lexical.path(), Position.t()) :: | ||
{:ok, {atom(), atom()}, Range.t()} | {:error, term()} | ||
def resolve(%Analysis{} = analysis, %Position{} = position) do | ||
case Entity.resolve(analysis, position) do | ||
{:ok, {module_or_struct, module}, range} when module_or_struct in [:struct, :module] -> | ||
{:ok, {:module, module}, range} | ||
|
||
_ -> | ||
{:error, :not_a_module} | ||
end | ||
end | ||
|
||
def resolve(path, %Position{} = position) do | ||
uri = Document.Path.ensure_uri(path) | ||
|
||
with {:ok, _} <- Document.Store.open_temporary(uri), | ||
{:ok, _document, analysis} <- Document.Store.fetch(uri, :analysis) do | ||
resolve(analysis, position) | ||
end | ||
end | ||
|
||
defp diff(old_range_text, new_name) do | ||
diff = String.myers_difference(old_range_text, new_name) | ||
|
||
eq = | ||
if match?([{:eq, _eq} | _], diff) do | ||
diff |> hd() |> elem(1) | ||
else | ||
"" | ||
end | ||
|
||
old_suffix = String.replace(old_range_text, ~r"^#{eq}", "") | ||
new_suffix = String.replace(new_name, ~r"^#{eq}", "") | ||
{old_suffix, new_suffix} | ||
end | ||
|
||
defp exacts(entity, old_suffix) do | ||
entity_string = inspect(entity) | ||
|
||
entity_string | ||
|> Store.exact(type: :module) | ||
|> Enum.filter(&entry_matching?(&1, old_suffix)) | ||
|> adjust_range_for_exacts(old_suffix) | ||
end | ||
|
||
defp descendants(entity, old_suffix) do | ||
prefix = "#{inspect(entity)}." | ||
|
||
prefix | ||
|> Store.prefix(type: :module) | ||
|> Enum.filter(&(entry_matching?(&1, old_suffix) and has_dots_in_range?(&1))) | ||
|> adjust_range_for_descendants(entity, old_suffix) | ||
end | ||
|
||
defp entry_matching?(entry, old_suffix) do | ||
entry.range |> range_text() |> String.contains?(old_suffix) | ||
end | ||
|
||
defp has_dots_in_range?(entry) do | ||
entry.range |> range_text() |> String.contains?(".") | ||
end | ||
|
||
defp adjust_range_for_exacts(entries, old_suffix) do | ||
for entry <- entries do | ||
start_character = entry.range.end.character - String.length(old_suffix) | ||
start_position = %{entry.range.start | character: start_character} | ||
range = %{entry.range | start: start_position} | ||
%{entry | range: range} | ||
end | ||
end | ||
|
||
defp adjust_range_for_descendants(entries, entity, old_suffix) do | ||
for entry <- entries, | ||
range_text = range_text(entry.range), | ||
matches = matches(range_text, old_suffix), | ||
result = resolve_module_range(entry, entity, matches), | ||
match?({:ok, _}, result) do | ||
{_, range} = result | ||
%{entry | range: range} | ||
end | ||
end | ||
|
||
defp range_text(range) do | ||
line(text: text) = range.end.context_line | ||
String.slice(text, range.start.character - 1, range.end.character - range.start.character) | ||
end | ||
|
||
defp resolve_module_range(_entry, _entity, []) do | ||
{:error, :not_found} | ||
end | ||
|
||
defp resolve_module_range(entry, entity, [[{start, length}]]) do | ||
range = adjust_range_characters(entry.range, {start, length}) | ||
|
||
with {:ok, {:module, result}, _} <- resolve(entry.path, range.start), | ||
true <- entity == result do | ||
{:ok, range} | ||
end | ||
end | ||
|
||
defp resolve_module_range(entry, entity, [[{start, length}] | tail] = _matches) do | ||
# This function is mainly for the duplicated suffixes | ||
# For example, if we have a module named `Foo.Bar.Foo.Bar` and we want to rename it to `Foo.Bar.Baz` | ||
# The `Foo.Bar` will be duplicated in the range text, so we need to resolve the correct range | ||
# and only rename the second occurrence of `Foo.Bar` | ||
start_character = entry.range.start.character + start | ||
position = %{entry.range.start | character: start_character} | ||
|
||
with {:ok, {:module, result}, range} <- resolve(entry.path, position) do | ||
if result == entity do | ||
range = adjust_range_characters(range, {start, length}) | ||
{:ok, range} | ||
else | ||
resolve_module_range(entry, entity, tail) | ||
end | ||
end | ||
end | ||
|
||
defp matches(range_text, "") do | ||
# When expanding a module, the old_suffix is an empty string, | ||
# so we need to scan the module before the period | ||
for [{start, length}] <- Regex.scan(~r/\w+(?=\.)/, range_text, return: :index) do | ||
[{start + length, 0}] | ||
end | ||
end | ||
|
||
defp matches(range_text, old_suffix) do | ||
Regex.scan(~r/#{old_suffix}/, range_text, return: :index) | ||
end | ||
|
||
defp adjust_range_characters(%Range{} = range, {start, length} = _matched_old_suffix) do | ||
start_character = range.start.character + start | ||
end_character = start_character + length | ||
|
||
range | ||
|> put_in([:start, :character], start_character) | ||
|> put_in([:end, :character], end_character) | ||
end | ||
end |
87 changes: 87 additions & 0 deletions
87
apps/remote_control/lib/lexical/remote_control/code_mod/rename/prepare.ex
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
defmodule Lexical.RemoteControl.CodeMod.Rename.Prepare do | ||
alias Lexical.Ast | ||
alias Lexical.Ast.Analysis | ||
alias Lexical.Document.Position | ||
alias Lexical.Document.Range | ||
alias Lexical.Formats | ||
alias Lexical.RemoteControl.CodeIntelligence.Entity | ||
alias Lexical.RemoteControl.CodeMod.Rename | ||
alias Lexical.RemoteControl.Search.Store | ||
|
||
require Logger | ||
|
||
@spec prepare(Analysis.t(), Position.t()) :: | ||
{:ok, {atom(), String.t()}, Range.t()} | {:error, term()} | ||
def prepare(%Analysis{} = analysis, %Position{} = position) do | ||
case resolve(analysis, position) do | ||
{:ok, {:module, module}, range} -> | ||
{:ok, Formats.module(module), range} | ||
|
||
{:error, error} -> | ||
{:error, error} | ||
end | ||
end | ||
|
||
@spec resolve(Analysis.t(), Position.t()) :: | ||
{:ok, {atom(), atom()} | {atom(), tuple()}, Range.t()} | {:error, term()} | ||
def resolve(%Analysis{} = analysis, %Position{} = position) do | ||
case do_resolve(analysis, position) do | ||
{:ok, {:module, _module}, _range} -> | ||
{module, range} = surround_the_whole_module(analysis, position) | ||
|
||
if cursor_at_declaration?(module, range) do | ||
{:ok, {:module, module}, range} | ||
else | ||
{:error, {:unsupported_location, :module}} | ||
end | ||
|
||
other -> | ||
other | ||
end | ||
end | ||
|
||
defp surround_the_whole_module(analysis, position) do | ||
# When renaming occurs, we want users to be able to choose any place in the defining module, | ||
# not just the last local module, like: `defmodule |Foo.Bar do` also works. | ||
{:ok, %{end: {_end_line, end_character}}} = Ast.surround_context(analysis, position) | ||
end_position = %{position | character: end_character - 1} | ||
{:ok, {:module, module}, range} = do_resolve(analysis, end_position) | ||
{module, range} | ||
end | ||
|
||
defp cursor_at_declaration?(module, rename_range) do | ||
case Store.exact(module, type: :module, subtype: :definition) do | ||
[definition] -> | ||
rename_range == definition.range | ||
|
||
_ -> | ||
false | ||
end | ||
end | ||
|
||
@renamable_modules [Rename.Module] | ||
|
||
defp do_resolve(%Analysis{} = analysis, %Position{} = position) do | ||
result = | ||
Enum.find_value(@renamable_modules, fn module -> | ||
result = module.resolve(analysis, position) | ||
|
||
if match?({:ok, _, _}, result) do | ||
result | ||
end | ||
end) | ||
|
||
if is_nil(result) do | ||
case Entity.resolve(analysis, position) do | ||
{:ok, other, _} -> | ||
Logger.info("Unsupported entity for renaming: #{inspect(other)}") | ||
{:error, :unsupported_entity} | ||
|
||
{:error, reason} -> | ||
{:error, reason} | ||
end | ||
else | ||
result | ||
end | ||
end | ||
end |
Oops, something went wrong.