Skip to content

Commit e01f381

Browse files
committed
add replacing unknown remote function to code actions
1 parent 85487e7 commit e01f381

File tree

6 files changed

+489
-49
lines changed

6 files changed

+489
-49
lines changed
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
defmodule ElixirLS.LanguageServer.Experimental.CodeMod.ReplaceRemoteFunction do
2+
alias ElixirLS.LanguageServer.Experimental.CodeMod.Ast
3+
alias ElixirLS.LanguageServer.Experimental.CodeMod.Diff
4+
alias ElixirLS.LanguageServer.Experimental.Protocol.Types.TextEdit
5+
6+
@spec text_edits(String.t(), Ast.t(), [atom()], atom(), atom()) ::
7+
{:ok, [TextEdit.t()]} | :error
8+
def text_edits(original_text, ast, module, name, suggestion) do
9+
with {:ok, transformed} <- apply_transforms(original_text, ast, module, name, suggestion) do
10+
{:ok, Diff.diff(original_text, transformed)}
11+
end
12+
end
13+
14+
defp apply_transforms(line_text, quoted_ast, module, name, suggestion) do
15+
leading_indent = leading_indent(line_text)
16+
17+
updated_ast =
18+
Macro.postwalk(quoted_ast, fn
19+
{:., meta1, [{:__aliases__, meta2, ^module}, ^name]} ->
20+
{:., meta1, [{:__aliases__, meta2, module}, suggestion]}
21+
22+
other ->
23+
other
24+
end)
25+
26+
if updated_ast != quoted_ast do
27+
updated_ast
28+
|> Ast.to_string()
29+
# We're dealing with a single error on a single line.
30+
# If the line doesn't compile (like it has a do with no end), ElixirSense
31+
# adds additional lines do documents with errors, so take the first line, as it's
32+
# the properly transformed source
33+
|> fetch_line(0)
34+
|> case do
35+
{:ok, text} ->
36+
{:ok, "#{leading_indent}#{text}"}
37+
38+
error ->
39+
error
40+
end
41+
else
42+
:error
43+
end
44+
end
45+
46+
@indent_regex ~r/^\s+/
47+
defp leading_indent(line_text) do
48+
case Regex.scan(@indent_regex, line_text) do
49+
[indent] -> indent
50+
_ -> ""
51+
end
52+
end
53+
54+
defp fetch_line(message, line_number) do
55+
line =
56+
message
57+
|> String.split(["\r\n", "\r", "\n"])
58+
|> Enum.at(line_number)
59+
60+
case line do
61+
nil -> :error
62+
other -> {:ok, other}
63+
end
64+
end
65+
end
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
defmodule ElixirLS.LanguageServer.Experimental.Provider.CodeAction.ReplaceRemoteFunction do
2+
@moduledoc """
3+
Code actions that replace unknown remote function with ones suggested by the warning message
4+
"""
5+
6+
alias ElixirLS.LanguageServer.Experimental.CodeMod
7+
alias ElixirLS.LanguageServer.Experimental.CodeMod.Ast
8+
alias ElixirLS.LanguageServer.Experimental.Protocol.Types.CodeAction, as: CodeActionResult
9+
alias ElixirLS.LanguageServer.Experimental.Protocol.Types.Diagnostic
10+
alias ElixirLS.LanguageServer.Experimental.Protocol.Types.TextEdit
11+
alias ElixirLS.LanguageServer.Experimental.Protocol.Types.WorkspaceEdit
12+
alias ElixirLS.LanguageServer.Experimental.SourceFile
13+
14+
@pattern ~r/(.*)\/(.*) is undefined or private. .*:\n(.*)/s
15+
16+
@spec pattern() :: Regex.t()
17+
def pattern, do: @pattern
18+
19+
@spec apply(SourceFile.t(), Diagnostic.t()) :: [CodeActionResult.t()]
20+
def apply(source_file, diagnostic) do
21+
with {:ok, module, name} <- extract_function(diagnostic.message),
22+
{:ok, suggestions} <- extract_suggestions(diagnostic.message),
23+
one_based_line = extract_line(diagnostic),
24+
{:ok, replies} <-
25+
build_code_actions(source_file, one_based_line, module, name, suggestions) do
26+
replies
27+
else
28+
_ ->
29+
[]
30+
end
31+
end
32+
33+
defp extract_function(message) do
34+
case Regex.scan(@pattern, message) do
35+
[[_, full_name, _, _]] ->
36+
{module, name} = separate_module_from_name(full_name)
37+
{:ok, module, name}
38+
39+
_ ->
40+
:error
41+
end
42+
end
43+
44+
defp separate_module_from_name(full_name) do
45+
{name, module} =
46+
full_name
47+
|> String.split(".")
48+
|> Enum.map(&String.to_atom/1)
49+
|> List.pop_at(-1)
50+
51+
{module, name}
52+
end
53+
54+
@suggestion_pattern ~r/\* .*\/[\d]+/
55+
defp extract_suggestions(message) do
56+
case Regex.scan(@pattern, message) do
57+
[[_, _, arity, suggestions_string]] ->
58+
suggestions =
59+
@suggestion_pattern
60+
|> Regex.scan(suggestions_string)
61+
|> Enum.flat_map(fn [suggestion] ->
62+
case String.split(suggestion, [" ", "/"]) do
63+
["*", name, ^arity] -> [String.to_atom(name)]
64+
_ -> []
65+
end
66+
end)
67+
68+
{:ok, suggestions}
69+
70+
_ ->
71+
:error
72+
end
73+
end
74+
75+
defp extract_line(%Diagnostic{} = diagnostic) do
76+
diagnostic.range.start.line
77+
end
78+
79+
defp build_code_actions(%SourceFile{} = source_file, one_based_line, module, name, suggestions) do
80+
with {:ok, line_text} <- SourceFile.fetch_text_at(source_file, one_based_line),
81+
{:ok, line_ast} <- Ast.from(line_text),
82+
{:ok, edits_per_suggestion} <-
83+
text_edits_per_suggestion(line_text, line_ast, module, name, suggestions) do
84+
case edits_per_suggestion do
85+
[] ->
86+
:error
87+
88+
[_ | _] ->
89+
edits_per_suggestion =
90+
Enum.map(edits_per_suggestion, fn {text_edits, suggestion} ->
91+
text_edits = Enum.map(text_edits, &update_line(&1, one_based_line))
92+
{text_edits, suggestion}
93+
end)
94+
95+
replies =
96+
Enum.map(edits_per_suggestion, fn {text_edits, function_name} ->
97+
CodeActionResult.new(
98+
title: construct_title(module, function_name),
99+
kind: :quick_fix,
100+
edit: WorkspaceEdit.new(changes: %{source_file.uri => text_edits})
101+
)
102+
end)
103+
104+
{:ok, replies}
105+
end
106+
end
107+
end
108+
109+
defp text_edits_per_suggestion(line_text, line_ast, module, name, suggestions) do
110+
Enum.reduce(suggestions, {:ok, []}, fn
111+
suggestion, {:ok, edits_per_suggestions} ->
112+
case CodeMod.ReplaceRemoteFunction.text_edits(
113+
line_text,
114+
line_ast,
115+
module,
116+
name,
117+
suggestion
118+
) do
119+
{:ok, []} -> {:ok, edits_per_suggestions}
120+
{:ok, text_edits} -> {:ok, [{text_edits, suggestion} | edits_per_suggestions]}
121+
:error -> :error
122+
end
123+
124+
_suggestion, :error ->
125+
:error
126+
end)
127+
end
128+
129+
defp update_line(%TextEdit{} = text_edit, line_number) do
130+
text_edit
131+
|> put_in([:range, :start, :line], line_number - 1)
132+
|> put_in([:range, :end, :line], line_number - 1)
133+
end
134+
135+
defp construct_title(module_list, function_name) do
136+
module_string =
137+
module_list
138+
|> Enum.map(fn module ->
139+
module
140+
|> Atom.to_string()
141+
|> String.trim_leading("Elixir.")
142+
end)
143+
|> Enum.join(".")
144+
145+
"Replace function with #{module_string}.#{function_name}"
146+
end
147+
end

apps/language_server/lib/language_server/experimental/provider/code_action/replace_with_underscore.ex

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,28 +5,26 @@ defmodule ElixirLS.LanguageServer.Experimental.Provider.CodeAction.ReplaceWithUn
55

66
alias ElixirLS.LanguageServer.Experimental.CodeMod
77
alias ElixirLS.LanguageServer.Experimental.CodeMod.Ast
8-
alias ElixirLS.LanguageServer.Experimental.Protocol.Requests.CodeAction
98
alias ElixirLS.LanguageServer.Experimental.Protocol.Types.CodeAction, as: CodeActionResult
109
alias ElixirLS.LanguageServer.Experimental.Protocol.Types.Diagnostic
1110
alias ElixirLS.LanguageServer.Experimental.Protocol.Types.TextEdit
1211
alias ElixirLS.LanguageServer.Experimental.Protocol.Types.WorkspaceEdit
1312
alias ElixirLS.LanguageServer.Experimental.SourceFile
1413

15-
@spec apply(CodeAction.t()) :: [CodeActionResult.t()]
16-
def apply(%CodeAction{} = code_action) do
17-
source_file = code_action.source_file
18-
diagnostics = get_in(code_action, [:context, :diagnostics]) || []
14+
@pattern ~r/variable "([^"]+)" is unused/
1915

20-
diagnostics
21-
|> Enum.flat_map(fn %Diagnostic{} = diagnostic ->
22-
with {:ok, variable_name, one_based_line} <- extract_variable_and_line(diagnostic),
23-
{:ok, reply} <- build_code_action(source_file, one_based_line, variable_name) do
24-
[reply]
25-
else
26-
_ ->
27-
[]
28-
end
29-
end)
16+
@spec pattern() :: Regex.t()
17+
def pattern, do: @pattern
18+
19+
@spec apply(SourceFile.t(), Diagnostic.t()) :: [CodeActionResult.t()]
20+
def apply(source_file, diagnostic) do
21+
with {:ok, variable_name, one_based_line} <- extract_variable_and_line(diagnostic),
22+
{:ok, reply} <- build_code_action(source_file, one_based_line, variable_name) do
23+
[reply]
24+
else
25+
_ ->
26+
[]
27+
end
3028
end
3129

3230
defp build_code_action(%SourceFile{} = source_file, one_based_line, variable_name) do
@@ -66,9 +64,8 @@ defmodule ElixirLS.LanguageServer.Experimental.Provider.CodeAction.ReplaceWithUn
6664
end
6765
end
6866

69-
@variable_re ~r/variable "([^"]+)" is unused/
7067
defp extract_variable_name(message) do
71-
case Regex.scan(@variable_re, message) do
68+
case Regex.scan(@pattern, message) do
7269
[[_, variable_name]] ->
7370
{:ok, String.to_atom(variable_name)}
7471

apps/language_server/lib/language_server/experimental/provider/handlers/code_action.ex

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,30 @@
11
defmodule ElixirLS.LanguageServer.Experimental.Provider.Handlers.CodeAction do
2+
alias ElixirLS.LanguageServer.Experimental.Provider.CodeAction.ReplaceRemoteFunction
23
alias ElixirLS.LanguageServer.Experimental.Provider.CodeAction.ReplaceWithUnderscore
34
alias ElixirLS.LanguageServer.Experimental.Provider.Env
45
alias ElixirLS.LanguageServer.Experimental.Protocol.Requests
56
alias ElixirLS.LanguageServer.Experimental.Protocol.Responses
6-
alias ElixirLS.LanguageServer.Experimental.Provider.CodeAction.ReplaceWithUnderscore
77

88
require Logger
99

1010
def handle(%Requests.CodeAction{} = request, %Env{}) do
11-
code_actions = ReplaceWithUnderscore.apply(request)
11+
source_file = request.source_file
12+
diagnostics = get_in(request, [:context, :diagnostics]) || []
13+
14+
code_actions =
15+
Enum.flat_map(diagnostics, fn %{message: message} = diagnostic ->
16+
cond do
17+
String.match?(message, ReplaceRemoteFunction.pattern()) ->
18+
ReplaceRemoteFunction.apply(source_file, diagnostic)
19+
20+
String.match?(message, ReplaceWithUnderscore.pattern()) ->
21+
ReplaceWithUnderscore.apply(source_file, diagnostic)
22+
23+
true ->
24+
[]
25+
end
26+
end)
27+
1228
reply = Responses.CodeAction.new(request.id, code_actions)
1329

1430
{:reply, reply}

0 commit comments

Comments
 (0)