Skip to content

Commit

Permalink
Add basic code action support (elixir-lsp#718)
Browse files Browse the repository at this point in the history
  • Loading branch information
lucacervello committed Oct 2, 2022
1 parent dacdcdf commit 9fe8ea1
Show file tree
Hide file tree
Showing 4 changed files with 147 additions and 3 deletions.
11 changes: 11 additions & 0 deletions apps/language_server/lib/language_server/protocol.ex
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,17 @@ defmodule ElixirLS.LanguageServer.Protocol do
end
end

defmacro code_action_req(id, uri, diagnostics) do
quote do
request(unquote(id), "textDocument/codeAction", %{
"context" => %{"diagnostics" => unquote(diagnostics)},
"textDocument" => %{
"uri" => unquote(uri)
}
})
end
end

# Other utilities

defmacro range(start_line, start_character, end_line, end_character) do
Expand Down
61 changes: 61 additions & 0 deletions apps/language_server/lib/language_server/providers/code_action.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
defmodule ElixirLS.LanguageServer.Providers.CodeAction do
use ElixirLS.LanguageServer.Protocol

def code_actions(uri, diagnostics) do
actions =
diagnostics
|> Enum.map(fn diagnostic -> actions(uri, diagnostic) end)
|> List.flatten()

{:ok, actions}
end

defp actions(uri, %{"message" => message, "range" => range} = diagnostic) do
[
{~r/variable "(.*)" is unused/, &prefix_with_underscore/2},
{~r/variable "(.*)" is unused/, &remove_variable/2}
]
|> Enum.filter(fn {r, _fun} -> String.match?(message, r) end)
|> Enum.map(fn {_r, fun} -> fun.(uri, diagnostic) end)
end

defp prefix_with_underscore(uri, %{"message" => message, "range" => range} = diagnostic) do
%{
"title" => "Add '_' to unused variable",
"kind" => "quickfix",
"edit" => %{
"changes" => %{
uri => [
%{
"newText" => "_",
"range" =>
range(
range["start"]["line"],
range["start"]["character"],
range["start"]["line"],
range["start"]["character"]
)
}
]
}
}
}
end

defp remove_variable(uri, %{"range" => range} = diagnostic) do
%{
"title" => "Remove unused variable",
"kind" => "quickfix",
"edit" => %{
"changes" => %{
uri => [
%{
"newText" => "",
"range" => range
}
]
}
}
}
end
end
14 changes: 11 additions & 3 deletions apps/language_server/lib/language_server/server.ex
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ defmodule ElixirLS.LanguageServer.Server do
OnTypeFormatting,
CodeLens,
ExecuteCommand,
FoldingRange
FoldingRange,
CodeAction
}

alias ElixirLS.Utils.Launch
Expand Down Expand Up @@ -326,7 +327,9 @@ defmodule ElixirLS.LanguageServer.Server do
# close notification send before
JsonRpc.log_message(
:warning,
"Received textDocument/didOpen for file that is already open. Received uri: #{inspect(uri)}"
"Received textDocument/didOpen for file that is already open. Received uri: #{
inspect(uri)
}"
)

state
Expand Down Expand Up @@ -788,6 +791,10 @@ defmodule ElixirLS.LanguageServer.Server do
end
end

defp handle_request(code_action_req(id, uri, diagnostics) = req, state = %__MODULE__{}) do
{:async, fn -> CodeAction.code_actions(uri, diagnostics) end, state}
end

defp handle_request(%{"method" => "$/" <> _}, state = %__MODULE__{}) do
# "$/" requests that the server doesn't support must return method_not_found
{:error, :method_not_found, nil, state}
Expand Down Expand Up @@ -839,7 +846,8 @@ defmodule ElixirLS.LanguageServer.Server do
"workspace" => %{
"workspaceFolders" => %{"supported" => false, "changeNotifications" => false}
},
"foldingRangeProvider" => true
"foldingRangeProvider" => true,
"codeActionProvider" => true
}
end

Expand Down
64 changes: 64 additions & 0 deletions apps/language_server/test/server_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -1494,6 +1494,70 @@ defmodule ElixirLS.LanguageServer.ServerTest do
end
end

describe "textDocument/codeAction" do
test "return code actions on unused variables", %{server: server} do
uri = "file:///file.ex"
fake_initialize(server)

Server.receive_packet(server, did_open(uri, "elixir", 1, ""))

Server.receive_packet(
server,
code_action_req(1, uri, [
%{
"message" =>
"variable \"foo\" is unused (if the variable is not meant to be used, prefix it with an underscore)",
"range" => %{
"end" => %{"character" => 13, "line" => 19},
"start" => %{"character" => 4, "line" => 19}
},
"severity" => 1,
"source" => "Elixir"
}
])
)

resp = assert_receive(%{"id" => 1}, 5000)

assert response(1, [
%{
"edit" => %{
"changes" => %{
"file:///file.ex" => [
%{
"newText" => "_",
"range" => %{
"end" => %{"character" => 4, "line" => 19},
"start" => %{"character" => 4, "line" => 19}
}
}
]
}
},
"kind" => "quickfix",
"title" => "Add '_' to unused variable"
},
%{
"edit" => %{
"changes" => %{
"file:///file.ex" => [
%{
"newText" => "",
"range" => %{
"end" => %{"character" => 13, "line" => 19},
"start" => %{"character" => 4, "line" => 19}
}
}
]
}
},
"kind" => "quickfix",
"title" => "Remove unused variable"
}
]) == resp
end
end

defp with_new_server(func) do
server = start_supervised!({Server, nil})
packet_capture = start_supervised!({PacketCapture, self()})
Expand Down

0 comments on commit 9fe8ea1

Please sign in to comment.