|
1 | 1 | defmodule Lexical.RemoteControl.CodeMod.Rename.Module do |
2 | | - alias Lexical.Ast |
3 | 2 | alias Lexical.Ast.Analysis |
4 | 3 | alias Lexical.Document |
5 | 4 | alias Lexical.Document.Edit |
| 5 | + alias Lexical.Document.Line |
6 | 6 | alias Lexical.Document.Position |
7 | | - alias Lexical.RemoteControl.CodeMod.Rename.Prepare |
| 7 | + alias Lexical.Document.Range |
| 8 | + alias Lexical.RemoteControl.CodeIntelligence.Entity |
8 | 9 | alias Lexical.RemoteControl.Search.Store |
9 | 10 | require Logger |
10 | 11 |
|
11 | | - @spec rename(Analysis.t(), Position.t(), String.t()) :: |
12 | | - {:ok, %{Lexical.uri() => [Edit.t()]}} | {:error, term()} |
13 | | - def rename(%Analysis{} = analysis, %Position{} = position, new_name) do |
14 | | - with {:ok, entity, range} <- Prepare.resolve_module(analysis, position) do |
15 | | - edits = |
16 | | - analysis.document |
17 | | - |> search_related_candidates(position, entity, range) |
18 | | - |> to_edits_by_uri(new_name) |
| 12 | + import Line |
19 | 13 |
|
20 | | - {:ok, edits} |
21 | | - end |
22 | | - end |
23 | | - |
24 | | - defp search_related_candidates(document, position, entity, range) do |
25 | | - local_module_name = Prepare.local_module_name(range) |
26 | | - entities = exacts(entity, local_module_name) |
27 | | - |
28 | | - # Users won't always want to rename descendants of a module. |
29 | | - # For instance, when there are no more submodules after the cursor. |
30 | | - # like: `defmodule TopLevel.Mo|dule do` |
31 | | - # in most cases, users only want to rename the module itself. |
32 | | - # |
33 | | - # However, there's an exception when the cursor is in the middle, |
34 | | - # such as `Top.Mo|dule.ChildModule`. If we rename it to `Top.Renamed.Child`, |
35 | | - # it would be natural to also rename `Module.ChildModule` to `Renamed.Child`. |
36 | | - if at_the_middle_of_module?(document, position, range) do |
37 | | - entities ++ descendants(entity, local_module_name) |
38 | | - else |
39 | | - entities |
40 | | - end |
| 14 | + @spec rename(Range.t(), String.t(), atom()) :: %{Lexical.uri() => [Edit.t()]} |
| 15 | + def rename(%Range{} = old_range, new_name, entity) do |
| 16 | + {old_suffix, new_suffix} = old_range |> range_text() |> diff(new_name) |
| 17 | + results = exacts(entity, old_suffix) ++ descendants(entity, old_suffix) |
| 18 | + to_edits_by_uri(results, new_suffix) |
41 | 19 | end |
42 | 20 |
|
43 | | - defp at_the_middle_of_module?(document, position, range) do |
44 | | - range_text = Prepare.range_text(range) |
45 | | - |
46 | | - case Ast.surround_context(document, position) do |
47 | | - {:ok, %{context: {:alias, alias}}} -> |
48 | | - String.length(range_text) < length(alias) |
| 21 | + @spec resolve(Analysis.t(), Position.t()) :: |
| 22 | + {:ok, {atom(), atom()}, Range.t()} | {:error, term()} |
| 23 | + def resolve(%Analysis{} = analysis, %Position{} = position) do |
| 24 | + case Entity.resolve(analysis, position) do |
| 25 | + {:ok, {module_or_struct, module}, range} when module_or_struct in [:struct, :module] -> |
| 26 | + {:ok, {:module, module}, range} |
49 | 27 |
|
50 | 28 | _ -> |
51 | | - false |
| 29 | + {:error, :not_a_module} |
52 | 30 | end |
53 | 31 | end |
54 | 32 |
|
55 | | - defp descendants(entity, local_module_name) do |
56 | | - entity_string = inspect(entity) |
57 | | - prefix = "#{entity_string}." |
| 33 | + defp diff(old_range_text, new_name) do |
| 34 | + diff = String.myers_difference(old_range_text, new_name) |
58 | 35 |
|
59 | | - prefix |
60 | | - |> Store.prefix(type: :module) |
61 | | - |> Enum.filter(&(entry_matching?(&1, local_module_name) and has_dots_in_range?(&1))) |
62 | | - |> adjust_range(entity) |
| 36 | + eq = |
| 37 | + if match?([{:eq, _eq} | _], diff) do |
| 38 | + diff |> hd() |> elem(1) |
| 39 | + else |
| 40 | + "" |
| 41 | + end |
| 42 | + |
| 43 | + old_suffix = String.replace(old_range_text, ~r"^#{eq}", "") |
| 44 | + new_suffix = String.replace(new_name, ~r"^#{eq}", "") |
| 45 | + {old_suffix, new_suffix} |
63 | 46 | end |
64 | 47 |
|
65 | | - defp exacts(entity, local_module_name) do |
| 48 | + defp exacts(entity, old_suffix) do |
66 | 49 | entity_string = inspect(entity) |
67 | 50 |
|
68 | 51 | entity_string |
69 | 52 | |> Store.exact(type: :module) |
70 | | - |> Enum.filter(&entry_matching?(&1, local_module_name)) |
| 53 | + |> Enum.filter(&entry_matching?(&1, old_suffix)) |
| 54 | + |> adjust_range_for_exacts(old_suffix) |
| 55 | + end |
| 56 | + |
| 57 | + defp descendants(entity, old_suffix) do |
| 58 | + prefix = "#{inspect(entity)}." |
| 59 | + |
| 60 | + prefix |
| 61 | + |> Store.prefix(type: :module) |
| 62 | + |> Enum.filter(&(entry_matching?(&1, old_suffix) and has_dots_in_range?(&1))) |
| 63 | + |> adjust_range_for_descendants(entity, old_suffix) |
71 | 64 | end |
72 | 65 |
|
73 | | - defp entry_matching?(entry, local_module_name) do |
74 | | - entry.range |> Prepare.range_text() |> String.contains?(local_module_name) |
| 66 | + defp entry_matching?(entry, old_suffix) do |
| 67 | + entry.range |> range_text() |> String.contains?(old_suffix) |
75 | 68 | end |
76 | 69 |
|
77 | 70 | defp has_dots_in_range?(entry) do |
78 | | - entry.range |> Prepare.range_text() |> String.contains?(".") |
| 71 | + entry.range |> range_text() |> String.contains?(".") |
79 | 72 | end |
80 | 73 |
|
81 | | - defp adjust_range(entries, entity) do |
| 74 | + defp adjust_range_for_exacts(entries, old_suffix) do |
| 75 | + for entry <- entries do |
| 76 | + start_character = entry.range.end.character - String.length(old_suffix) |
| 77 | + start_position = %{entry.range.start | character: start_character} |
| 78 | + range = %{entry.range | start: start_position} |
| 79 | + %{entry | range: range} |
| 80 | + end |
| 81 | + end |
| 82 | + |
| 83 | + defp adjust_range_for_descendants(entries, entity, old_suffix) do |
82 | 84 | for entry <- entries, |
83 | | - uri = Document.Path.ensure_uri(entry.path), |
84 | | - range_result = resolve_local_module_range(uri, entry.range.start, entity), |
85 | | - match?({:ok, _}, range_result) do |
86 | | - {_, range} = range_result |
| 85 | + range_text = range_text(entry.range), |
| 86 | + matches = Regex.scan(~r"#{old_suffix}", range_text, return: :index), |
| 87 | + result = resolve_module_range(entry, entity, matches), |
| 88 | + match?({:ok, _}, result) do |
| 89 | + {_, range} = result |
87 | 90 | %{entry | range: range} |
88 | 91 | end |
89 | 92 | end |
90 | 93 |
|
91 | | - defp resolve_local_module_range(uri, position, entity) do |
| 94 | + defp range_text(range) do |
| 95 | + line(text: text) = range.end.context_line |
| 96 | + String.slice(text, range.start.character - 1, range.end.character - range.start.character) |
| 97 | + end |
| 98 | + |
| 99 | + defp resolve_module_range(_entry, _entity, []) do |
| 100 | + {:error, :not_found} |
| 101 | + end |
| 102 | + |
| 103 | + defp resolve_module_range(entry, _entity, [[{start, length}]]) do |
| 104 | + range = adjust_range_characters(entry.range, {start, length}) |
| 105 | + {:ok, range} |
| 106 | + end |
| 107 | + |
| 108 | + defp resolve_module_range(entry, entity, [[{start, length}] | tail] = _matches) do |
| 109 | + # This function is mainly for the duplicated suffixes |
| 110 | + # For example, if we have a module named `Foo.Bar.Foo.Bar` and we want to rename it to `Foo.Bar.Baz` |
| 111 | + # The `Foo.Bar` will be duplicated in the range text, so we need to resolve the correct range |
| 112 | + # and only rename the second occurrence of `Foo.Bar` |
| 113 | + uri = Document.Path.ensure_uri(entry.path) |
| 114 | + |
92 | 115 | with {:ok, _} <- Document.Store.open_temporary(uri), |
93 | | - {:ok, document, analysis} <- Document.Store.fetch(uri, :analysis), |
94 | | - {:ok, result, range} <- Prepare.resolve_module(analysis, position) do |
| 116 | + {:ok, _document, analysis} <- Document.Store.fetch(uri, :analysis), |
| 117 | + start_character = entry.range.start.character + start, |
| 118 | + position = %{entry.range.start | character: start_character}, |
| 119 | + {:ok, {:module, result}, range} <- resolve(analysis, position) do |
95 | 120 | if result == entity do |
| 121 | + range = adjust_range_characters(range, {start, length}) |
96 | 122 | {:ok, range} |
97 | 123 | else |
98 | | - last_part_length = result |> Ast.Module.local_name() |> String.length() |
99 | | - # Move the cursor to the next part: |
100 | | - # `|Parent.Next.Target.Child` -> 'Parent.|Next.Target.Child' -> 'Parent.Next.|Target.Child' |
101 | | - character = position.character + last_part_length + 1 |
102 | | - position = Position.new(document, position.line, character) |
103 | | - resolve_local_module_range(uri, position, entity) |
| 124 | + resolve_module_range(entry, entity, tail) |
104 | 125 | end |
105 | | - else |
106 | | - _ -> |
107 | | - Logger.error("Failed to find entity range for #{inspect(uri)} at #{inspect(position)}") |
108 | | - :error |
109 | 126 | end |
110 | 127 | end |
111 | 128 |
|
112 | | - defp to_edits_by_uri(results, new_name) do |
| 129 | + defp adjust_range_characters(range, {start, length} = _matched_old_suffix) do |
| 130 | + start_character = range.start.character + start |
| 131 | + end_character = start_character + length |
| 132 | + start_position = %{range.start | character: start_character} |
| 133 | + end_end_position = %{range.start | character: end_character} |
| 134 | + %{range | start: start_position, end: end_end_position} |
| 135 | + end |
| 136 | + |
| 137 | + defp to_edits_by_uri(results, new_suffix) do |
113 | 138 | Enum.group_by( |
114 | 139 | results, |
115 | 140 | &Document.Path.ensure_uri(&1.path), |
116 | | - fn result -> |
117 | | - local_module_name_length = result.range |> Prepare.local_module_name() |> String.length() |
118 | | - # e.g: `Parent.|ToBeRenameModule`, we need the start position of `ToBeRenameModule` |
119 | | - start_character = result.range.end.character - local_module_name_length |
120 | | - start_position = %{result.range.start | character: start_character} |
121 | | - |
122 | | - new_range = %{result.range | start: start_position} |
123 | | - Edit.new(new_name, new_range) |
124 | | - end |
| 141 | + &Edit.new(new_suffix, &1.range) |
125 | 142 | ) |
126 | 143 | end |
127 | 144 | end |
0 commit comments