Skip to content

Commit 7e9109e

Browse files
committed
add replacing unknown function to code actions
1 parent 2984b66 commit 7e9109e

File tree

4 files changed

+152
-10
lines changed

4 files changed

+152
-10
lines changed

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

Lines changed: 71 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,30 @@
11
defmodule ElixirLS.LanguageServer.Providers.CodeAction do
22
use ElixirLS.LanguageServer.Protocol
33

4-
def code_actions(uri, diagnostics) do
4+
alias ElixirLS.LanguageServer.SourceFile
5+
6+
@unknown_remote_function_pattern ~r/(.*)\/(.*) is undefined or private. .*:\n(.*)/s
7+
8+
def code_actions(uri, diagnostics, source_file) do
59
actions =
610
diagnostics
7-
|> Enum.map(fn diagnostic -> actions(uri, diagnostic) end)
11+
|> Enum.map(fn diagnostic -> actions(uri, diagnostic, source_file) end)
812
|> List.flatten()
913

1014
{:ok, actions}
1115
end
1216

13-
defp actions(uri, %{"message" => message} = diagnostic) do
17+
defp actions(uri, %{"message" => message} = diagnostic, source_file) do
1418
[
15-
{~r/variable "(.*)" is unused/, &prefix_with_underscore/2},
16-
{~r/variable "(.*)" is unused/, &remove_variable/2}
19+
{~r/variable "(.*)" is unused/, &prefix_with_underscore/3},
20+
{~r/variable "(.*)" is unused/, &remove_variable/3},
21+
{@unknown_remote_function_pattern, &replace_unknown_function/3}
1722
]
1823
|> Enum.filter(fn {r, _fun} -> String.match?(message, r) end)
19-
|> Enum.map(fn {_r, fun} -> fun.(uri, diagnostic) end)
24+
|> Enum.map(fn {_r, fun} -> fun.(uri, diagnostic, source_file) end)
2025
end
2126

22-
defp prefix_with_underscore(uri, %{"range" => range}) do
27+
defp prefix_with_underscore(uri, %{"range" => range}, _source_file) do
2328
%{
2429
"title" => "Add '_' to unused variable",
2530
"kind" => "quickfix",
@@ -42,7 +47,7 @@ defmodule ElixirLS.LanguageServer.Providers.CodeAction do
4247
}
4348
end
4449

45-
defp remove_variable(uri, %{"range" => range}) do
50+
defp remove_variable(uri, %{"range" => range}, _source_file) do
4651
%{
4752
"title" => "Remove unused variable",
4853
"kind" => "quickfix",
@@ -58,4 +63,62 @@ defmodule ElixirLS.LanguageServer.Providers.CodeAction do
5863
}
5964
}
6065
end
66+
67+
defp replace_unknown_function(uri, %{"message" => message, "range" => range}, source_file) do
68+
[_, full_function_name, _function_arity, candidates_string] =
69+
Regex.run(@unknown_remote_function_pattern, message)
70+
71+
function_module =
72+
full_function_name
73+
|> String.split(".")
74+
|> Enum.slice(0..-2//1)
75+
|> Enum.join(".")
76+
77+
start_line = start_line_from_range(range)
78+
79+
source_line =
80+
source_file
81+
|> SourceFile.lines()
82+
|> Enum.at(start_line)
83+
84+
candidates_string
85+
|> parse_candidates_string()
86+
|> Enum.map(&(function_module <> "." <> &1))
87+
|> Enum.reject(&(&1 == full_function_name))
88+
|> Enum.map(fn full_candidate_name ->
89+
%{
90+
"title" => "Replace unknown function with '#{full_candidate_name}'",
91+
"kind" => "quickfix",
92+
"edit" => %{
93+
"changes" => %{
94+
uri => [
95+
%{
96+
"range" =>
97+
range(
98+
start_line,
99+
0,
100+
start_line,
101+
String.length(source_line)
102+
),
103+
"newText" => String.replace(source_line, full_function_name, full_candidate_name)
104+
}
105+
]
106+
}
107+
}
108+
}
109+
end)
110+
end
111+
112+
defp start_line_from_range(%{"start" => %{"line" => start_line}}), do: start_line
113+
114+
defp parse_candidates_string(str) do
115+
pattern = ~r"[ ]*\* (?<function_name>.*)/.*"
116+
117+
str
118+
|> String.split("\n")
119+
|> Enum.map(&Regex.run(pattern, &1, capture: :all_names))
120+
|> Enum.reject(&is_nil/1)
121+
|> List.flatten()
122+
|> Enum.uniq()
123+
end
61124
end

apps/language_server/lib/language_server/server.ex

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -781,7 +781,9 @@ defmodule ElixirLS.LanguageServer.Server do
781781
end
782782

783783
defp handle_request(code_action_req(_id, uri, diagnostics), state = %__MODULE__{}) do
784-
{:async, fn -> CodeAction.code_actions(uri, diagnostics) end, state}
784+
source_file = get_source_file(state, uri)
785+
786+
{:async, fn -> CodeAction.code_actions(uri, diagnostics, source_file) end, state}
785787
end
786788

787789
defp handle_request(%{"method" => "$/" <> _}, state = %__MODULE__{}) do
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
defmodule ElixirLS.LanguageServer.Providers.CodeActionTest do
2+
use ExUnit.Case
3+
4+
alias ElixirLS.LanguageServer.Providers.CodeAction
5+
alias ElixirLS.LanguageServer.SourceFile
6+
7+
test "replace unknown function" do
8+
uri = "file:///some_file.ex"
9+
10+
text = """
11+
defmodule Example do
12+
def foo do
13+
var = Enum.counts([2, 3])
14+
end
15+
end
16+
"""
17+
18+
source_file = %SourceFile{text: text}
19+
20+
message = "Enum.counts/1 is undefined or private. Did you mean:
21+
22+
* concat/1
23+
* concat/2
24+
* count/1
25+
* count/2
26+
"
27+
28+
diagnostic = [
29+
%{
30+
"message" => message,
31+
"range" => %{
32+
"end" => %{"character" => 21, "line" => 2},
33+
"start" => %{"character" => 10, "line" => 2}
34+
},
35+
"severity" => 2,
36+
"source" => "Elixir"
37+
}
38+
]
39+
40+
assert {:ok, [replace_with_concat, replace_with_count]} =
41+
CodeAction.code_actions(uri, diagnostic, source_file)
42+
43+
assert %{
44+
"edit" => %{
45+
"changes" => %{
46+
^uri => [
47+
%{
48+
"newText" => " var = Enum.concat([2, 3])",
49+
"range" => %{
50+
"end" => %{"character" => 29, "line" => 2},
51+
"start" => %{"character" => 0, "line" => 2}
52+
}
53+
}
54+
]
55+
}
56+
}
57+
} = replace_with_concat
58+
59+
assert %{
60+
"edit" => %{
61+
"changes" => %{
62+
^uri => [
63+
%{
64+
"newText" => " var = Enum.count([2, 3])",
65+
"range" => %{
66+
"end" => %{"character" => 29, "line" => 2},
67+
"start" => %{"character" => 0, "line" => 2}
68+
}
69+
}
70+
]
71+
}
72+
}
73+
} = replace_with_count
74+
end
75+
end

apps/language_server/test/providers/completion_test.exs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,9 @@ defmodule ElixirLS.LanguageServer.Providers.CompletionTest do
141141

142142
{line, char} = {3, 17}
143143
TestUtils.assert_has_cursor_char(text, line, char)
144-
{:ok, %{"items" => [first_suggestion | _tail]}} = Completion.completion(text, line, char, @supports)
144+
145+
{:ok, %{"items" => [first_suggestion | _tail]}} =
146+
Completion.completion(text, line, char, @supports)
145147

146148
assert first_suggestion["label"] === "fn"
147149
end

0 commit comments

Comments
 (0)