Skip to content

Commit 069cf41

Browse files
committed
Use built-in function to convert source code to ast
1 parent a6f5d8a commit 069cf41

File tree

8 files changed

+446
-394
lines changed

8 files changed

+446
-394
lines changed
Lines changed: 41 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,46 @@
11
defmodule ElixirLS.LanguageServer.Providers.CodeAction.Helpers do
22
alias ElixirLS.LanguageServer.Protocol.TextEdit
3-
alias ElixirLS.LanguageServer.Providers.CodeMod.Ast
4-
alias ElixirLS.LanguageServer.Providers.CodeMod.Text
3+
alias ElixirLS.LanguageServer.Providers.CodeMod.Diff
4+
5+
@spec to_text_edits(String.t(), String.t()) :: {:ok, [TextEdit.t()]} | :error
6+
def to_text_edits(unformatted_text, updated_text) do
7+
formatted_text =
8+
unformatted_text
9+
|> Code.format_string!(line_length: :infinity)
10+
|> IO.iodata_to_binary()
11+
12+
change_text_edits = Diff.diff(formatted_text, updated_text)
13+
14+
with {:ok, changed_line} <- changed_line(change_text_edits) do
15+
is_line_formatted =
16+
unformatted_text
17+
|> Diff.diff(formatted_text)
18+
|> Enum.filter(fn %TextEdit{range: range} ->
19+
range["start"]["line"] == changed_line or range["end"]["line"] == changed_line
20+
end)
21+
|> Enum.empty?()
22+
23+
if is_line_formatted do
24+
{:ok, change_text_edits}
25+
else
26+
:error
27+
end
28+
end
29+
end
30+
31+
defp changed_line(text_edits) do
32+
lines =
33+
text_edits
34+
|> Enum.flat_map(fn %TextEdit{range: range} ->
35+
[range["start"]["line"], range["end"]["line"]]
36+
end)
37+
|> Enum.uniq()
38+
39+
case lines do
40+
[line] -> {:ok, line}
41+
_ -> :error
42+
end
43+
end
544

645
@spec update_line(TextEdit.t(), non_neg_integer()) :: TextEdit.t()
746
def update_line(
@@ -16,38 +55,4 @@ defmodule ElixirLS.LanguageServer.Providers.CodeAction.Helpers do
1655
}
1756
}
1857
end
19-
20-
@spec to_one_line_string(Ast.t()) :: {:ok, String.t()} | :error
21-
def to_one_line_string(updated_ast) do
22-
updated_ast
23-
|> Ast.to_string()
24-
# We're dealing with a single error on a single line.
25-
# If the line doesn't compile (like it has a do with no end), ElixirSense
26-
# adds additional lines to documents with errors. Also, in case of a one-line do,
27-
# ElixirSense creates do with end from the AST.
28-
|> maybe_recover_one_line_do(updated_ast)
29-
|> Text.fetch_line(0)
30-
end
31-
32-
@do_regex ~r/\s*do\s*/
33-
defp maybe_recover_one_line_do(updated_text, {_name, context, _children} = _updated_ast) do
34-
wrong_do_end_conditions = [
35-
not Keyword.has_key?(context, :do),
36-
not Keyword.has_key?(context, :end),
37-
Regex.match?(@do_regex, updated_text),
38-
String.ends_with?(updated_text, "\nend")
39-
]
40-
41-
if Enum.all?(wrong_do_end_conditions) do
42-
updated_text
43-
|> String.replace(@do_regex, ", do: ")
44-
|> String.trim_trailing("\nend")
45-
else
46-
updated_text
47-
end
48-
end
49-
50-
defp maybe_recover_one_line_do(updated_text, _updated_ast) do
51-
updated_text
52-
end
5358
end

apps/language_server/lib/language_server/providers/code_action/replace_remote_function.ex

Lines changed: 62 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,9 @@ defmodule ElixirLS.LanguageServer.Providers.CodeAction.ReplaceRemoteFunction do
66

77
use ElixirLS.LanguageServer.Protocol
88

9+
alias ElixirLS.LanguageServer.Protocol.TextEdit
910
alias ElixirLS.LanguageServer.Providers.CodeAction.CodeActionResult
1011
alias ElixirLS.LanguageServer.Providers.CodeMod.Ast
11-
alias ElixirLS.LanguageServer.Providers.CodeMod.Diff
12-
alias ElixirLS.LanguageServer.Providers.CodeMod.Text
1312
alias ElixirLS.LanguageServer.SourceFile
1413
alias ElixirSense.Core.Parser
1514

@@ -18,11 +17,14 @@ defmodule ElixirLS.LanguageServer.Providers.CodeAction.ReplaceRemoteFunction do
1817
@spec apply(SourceFile.t(), String.t(), [map()]) :: [CodeActionResult.t()]
1918
def apply(%SourceFile{} = source_file, uri, diagnostics) do
2019
Enum.flat_map(diagnostics, fn diagnostic ->
21-
with {:ok, module, function, arity, line_number} <- extract_function_and_line(diagnostic),
22-
{:ok, suggestions} <- prepare_suggestions(module, function, arity) do
23-
to_code_actions(source_file, line_number, module, function, suggestions, uri)
24-
else
25-
_ -> []
20+
case extract_function_and_line(diagnostic) do
21+
{:ok, module, function, arity, line} ->
22+
suggestions = prepare_suggestions(module, function, arity)
23+
24+
build_code_actions(source_file, line, module, function, suggestions, uri)
25+
26+
:error ->
27+
[]
2628
end
2729
end)
2830
end
@@ -38,6 +40,9 @@ defmodule ElixirLS.LanguageServer.Providers.CodeAction.ReplaceRemoteFunction do
3840
with [[_, module_and_function, arity]] <- Regex.scan(@function_re, message),
3941
{:ok, module, function_name} <- separate_module_from_function(module_and_function) do
4042
{:ok, module, function_name, String.to_integer(arity)}
43+
else
44+
_ ->
45+
:error
4146
end
4247
end
4348

@@ -65,17 +70,14 @@ defmodule ElixirLS.LanguageServer.Providers.CodeAction.ReplaceRemoteFunction do
6570
@function_threshold 0.77
6671
@max_suggestions 5
6772
defp prepare_suggestions(module, function, arity) do
68-
suggestions =
69-
for {module_function, ^arity} <- module_functions(module),
70-
distance = module_function |> Atom.to_string() |> String.jaro_distance(function),
71-
distance >= @function_threshold do
72-
{distance, module_function}
73-
end
74-
|> Enum.sort(:desc)
75-
|> Enum.take(@max_suggestions)
76-
|> Enum.map(fn {_distance, module_function} -> module_function end)
77-
78-
{:ok, suggestions}
73+
for {module_function, ^arity} <- module_functions(module),
74+
distance = module_function |> Atom.to_string() |> String.jaro_distance(function),
75+
distance >= @function_threshold do
76+
{distance, module_function}
77+
end
78+
|> Enum.sort(:desc)
79+
|> Enum.take(@max_suggestions)
80+
|> Enum.map(fn {_distance, module_function} -> module_function end)
7981
end
8082

8183
defp module_functions(module) do
@@ -86,12 +88,12 @@ defmodule ElixirLS.LanguageServer.Providers.CodeAction.ReplaceRemoteFunction do
8688
end
8789
end
8890

89-
defp to_code_actions(%SourceFile{} = source_file, line_number, module, name, suggestions, uri) do
91+
defp build_code_actions(%SourceFile{} = source_file, line, module, name, suggestions, uri) do
9092
suggestions
9193
|> Enum.reduce([], fn suggestion, acc ->
92-
case apply_transform(source_file, line_number, module, name, suggestion) do
94+
case text_edits(source_file, line, module, name, suggestion) do
9395
{:ok, [_ | _] = text_edits} ->
94-
text_edits = Enum.map(text_edits, &update_line(&1, line_number))
96+
text_edits = Enum.map(text_edits, &update_line(&1, line))
9597

9698
code_action =
9799
CodeActionResult.new("Rename to #{suggestion}", "quickfix", text_edits, uri)
@@ -105,58 +107,51 @@ defmodule ElixirLS.LanguageServer.Providers.CodeAction.ReplaceRemoteFunction do
105107
|> Enum.reverse()
106108
end
107109

108-
defp apply_transform(source_file, line_number, module, name, suggestion) do
109-
with {:ok, text} <- fetch_line(source_file, line_number),
110-
{:ok, ast} <- Ast.from(text) do
111-
function_atom = String.to_atom(name)
112-
113-
leading_indent = Text.leading_indent(text)
114-
trailing_comment = Text.trailing_comment(text)
115-
116-
ast
117-
|> Macro.postwalk(fn
118-
{:., function_meta, [{:__aliases__, module_meta, module_alias}, ^function_atom]} ->
119-
case expand_alias(source_file, module_alias, line_number) do
120-
{:ok, ^module} ->
121-
{:., function_meta, [{:__aliases__, module_meta, module_alias}, suggestion]}
122-
123-
_ ->
124-
{:., function_meta, [{:__aliases__, module_meta, module_alias}, function_atom]}
125-
end
126-
127-
# erlang call
128-
{:., function_meta, [^module, ^function_atom]} ->
129-
{:., function_meta, [module, suggestion]}
130-
131-
other ->
132-
other
133-
end)
134-
|> to_one_line_string()
135-
|> case do
136-
{:ok, updated_text} ->
137-
text_edits = Diff.diff(text, "#{leading_indent}#{updated_text}#{trailing_comment}")
138-
139-
{:ok, text_edits}
140-
141-
:error ->
142-
:error
143-
end
110+
@spec text_edits(SourceFile.t(), non_neg_integer(), atom(), String.t(), atom()) ::
111+
{:ok, [TextEdit.t()]} | :error
112+
defp text_edits(%SourceFile{} = source_file, line, module, name, suggestion) do
113+
with {:ok, updated_text} <- apply_transform(source_file, line, module, name, suggestion) do
114+
to_text_edits(source_file.text, updated_text)
144115
end
145116
end
146117

147-
defp fetch_line(%SourceFile{} = source_file, line_number) do
148-
lines = SourceFile.lines(source_file)
118+
defp apply_transform(source_file, line, module, name, suggestion) do
119+
with {:ok, ast, comments} <- Ast.from(source_file) do
120+
function_atom = String.to_atom(name)
149121

150-
if length(lines) > line_number do
151-
{:ok, Enum.at(lines, line_number)}
152-
else
153-
:error
122+
one_based_line = line + 1
123+
124+
updated_text =
125+
ast
126+
|> Macro.postwalk(fn
127+
{:., [line: ^one_based_line],
128+
[{:__aliases__, module_meta, module_alias}, ^function_atom]} ->
129+
case expand_alias(source_file, module_alias, line) do
130+
{:ok, ^module} ->
131+
{:., [line: one_based_line],
132+
[{:__aliases__, module_meta, module_alias}, suggestion]}
133+
134+
_ ->
135+
{:., [line: one_based_line],
136+
[{:__aliases__, module_meta, module_alias}, function_atom]}
137+
end
138+
139+
# erlang call
140+
{:., [line: ^one_based_line], [{:__block__, module_meta, [^module]}, ^function_atom]} ->
141+
{:., [line: one_based_line], [{:__block__, module_meta, [module]}, suggestion]}
142+
143+
other ->
144+
other
145+
end)
146+
|> Ast.to_string(comments)
147+
148+
{:ok, updated_text}
154149
end
155150
end
156151

157152
@spec expand_alias(SourceFile.t(), [atom()], non_neg_integer()) :: {:ok, atom()} | :error
158-
defp expand_alias(source_file, module_alias, line_number) do
159-
with {:ok, aliases} <- aliases_at(source_file, line_number) do
153+
defp expand_alias(source_file, module_alias, line) do
154+
with {:ok, aliases} <- aliases_at(source_file, line) do
160155
aliases
161156
|> Enum.map(fn {module, aliased} ->
162157
module = module |> module_to_alias() |> List.first()
@@ -177,8 +172,8 @@ defmodule ElixirLS.LanguageServer.Providers.CodeAction.ReplaceRemoteFunction do
177172
end
178173
end
179174

180-
defp aliases_at(source_file, line_number) do
181-
one_based_line = line_number + 1
175+
defp aliases_at(source_file, line) do
176+
one_based_line = line + 1
182177

183178
metadata = Parser.parse_string(source_file.text, true, true, {one_based_line, 1})
184179

0 commit comments

Comments
 (0)