Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Code Actions refactor #453

Merged
merged 2 commits into from
Oct 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 7 additions & 8 deletions apps/remote_control/lib/lexical/remote_control/api.ex
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
defmodule Lexical.RemoteControl.Api do
alias Lexical.Document
alias Lexical.Document.Position
alias Lexical.Document.Range
alias Lexical.Project
alias Lexical.RemoteControl
alias Lexical.RemoteControl.Build
alias Lexical.RemoteControl.CodeAction
alias Lexical.RemoteControl.CodeIntelligence
alias Lexical.RemoteControl.CodeMod

Expand All @@ -20,17 +22,14 @@ defmodule Lexical.RemoteControl.Api do
RemoteControl.call(project, CodeMod.Format, :edits, [project, document])
end

def replace_with_underscore(
def code_actions(
%Project{} = project,
%Document{} = document,
line_number,
variable_name
%Range{} = range,
diagnostics,
kinds
) do
RemoteControl.call(project, CodeMod.ReplaceWithUnderscore, :edits, [
document,
line_number,
variable_name
])
RemoteControl.call(project, CodeAction, :for_range, [document, range, diagnostics, kinds])
end

def complete(%Project{} = project, %Document{} = document, %Position{} = position) do
Expand Down
56 changes: 56 additions & 0 deletions apps/remote_control/lib/lexical/remote_control/code_action.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
defmodule Lexical.RemoteControl.CodeAction do
alias Lexical.Document
alias Lexical.Document.Changes
alias Lexical.Document.Range
alias Lexical.RemoteControl.CodeAction.Diagnostic
alias Lexical.RemoteControl.CodeAction.Handlers

defstruct [:title, :kind, :changes, :uri]

@type code_action_kind ::
:empty
| :quick_fix
| :refactor
| :refactor_extract
| :refactor_inline
| :refactor_rewrite
| :source
| :source_organize_imports
| :source_fix_all

@type t :: %__MODULE__{
title: String.t(),
kind: code_action_kind,
changes: Changes.t(),
uri: Lexical.uri()
}

@handlers [Handlers.ReplaceWithUnderscore]

@spec new(Lexical.uri(), String.t(), code_action_kind(), Changes.t()) :: t()
def new(uri, title, kind, changes) do
%__MODULE__{uri: uri, title: title, changes: changes, kind: kind}
end

@spec for_range(Document.t(), Range.t(), [Diagnostic.t()], [code_action_kind] | :all) :: [t()]
def for_range(%Document{} = doc, %Range{} = range, diagnostics, kinds) do
results =
Enum.flat_map(@handlers, fn handler ->
if applies?(kinds, handler) do
handler.actions(doc, range, diagnostics)
else
[]
end
end)

results
end

defp applies?(:all, _handler_module) do
true
end

defp applies?(kinds, handler_module) do
kinds -- handler_module.kinds() != kinds
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
defmodule Lexical.RemoteControl.CodeAction.Diagnostic do
alias Lexical.Document.Range

defstruct [:range, :message, :source]
@type message :: String.t()
@type source :: String.t()
@type t :: %__MODULE__{
range: Range.t(),
message: message() | nil,
source: source() | nil
}

@spec new(Range.t(), message(), source() | nil) :: t
def new(%Range{} = range, message, source) do
%__MODULE__{range: range, message: message, source: source}
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
defmodule Lexical.RemoteControl.CodeAction.Handler do
alias Lexical.Document
alias Lexical.Document.Range
alias Lexical.RemoteControl.CodeAction
alias Lexical.RemoteControl.CodeAction.Diagnostic

@callback actions(Document.t(), Range.t(), [Diagnostic.t()]) :: [CodeAction.t()]
@callback kinds() :: [CodeAction.code_action_kind()]
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
defmodule Lexical.RemoteControl.CodeAction.Handlers.ReplaceWithUnderscore do
alias Lexical.Ast
alias Lexical.Document
alias Lexical.Document.Changes
alias Lexical.Document.Range
alias Lexical.RemoteControl.CodeAction
alias Lexical.RemoteControl.CodeAction.Diagnostic
alias Sourceror.Zipper

@behaviour CodeAction.Handler

@impl CodeAction.Handler
def actions(%Document{} = doc, %Range{}, diagnostics) do
Enum.reduce(diagnostics, [], fn %Diagnostic{} = diagnostic, acc ->
with {:ok, variable_name, line_number} <- extract_variable_and_line(diagnostic),
{:ok, changes} <- to_changes(doc, line_number, variable_name) do
action = CodeAction.new(doc.uri, "Rename to _#{variable_name}", :quick_fix, changes)

[action | acc]
else
_ ->
acc
end
end)
end

@impl CodeAction.Handler
def kinds do
[:quick_fix]
end

@spec to_changes(Document.t(), non_neg_integer(), String.t() | atom) ::
{:ok, Changes.t()} | :error
defp to_changes(%Document{} = document, line_number, variable_name) do
case apply_transform(document, line_number, variable_name) do
{:ok, edits} ->
{:ok, Changes.new(document, edits)}

error ->
error
end
end

defp apply_transform(document, line_number, unused_variable_name) do
underscored_variable_name = :"_#{unused_variable_name}"

result =
Ast.traverse_line(document, line_number, [], fn
%Zipper{node: {^unused_variable_name, _meta, nil} = node} = zipper, patches ->
[patch] = Sourceror.Patch.rename_identifier(node, underscored_variable_name)
{zipper, [patch | patches]}

zipper, acc ->
{zipper, acc}
end)

with {:ok, _, patches} <- result do
Ast.patches_to_edits(document, patches)
end
end

defp extract_variable_and_line(%Diagnostic{} = diagnostic) do
with {:ok, variable_name} <- extract_variable_name(diagnostic.message),
{:ok, line} <- extract_line(diagnostic) do
{:ok, variable_name, line}
end
end

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

_ ->
{:error, {:no_variable, message}}
end
end

defp extract_line(%Diagnostic{} = diagnostic) do
{:ok, diagnostic.range.start.line}
end
end

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
defmodule Lexical.RemoteControl.CodeMod.ReplaceWithUnderscoreTest do
defmodule Lexical.RemoteControl.CodeAction.Handlers.ReplaceWithUnderscoreTest do
alias Lexical.Document
alias Lexical.RemoteControl.CodeMod.ReplaceWithUnderscore
alias Lexical.RemoteControl.CodeAction.Diagnostic
alias Lexical.RemoteControl.CodeAction.Handlers.ReplaceWithUnderscore

use Lexical.Test.CodeMod.Case

Expand All @@ -9,9 +10,27 @@ defmodule Lexical.RemoteControl.CodeMod.ReplaceWithUnderscoreTest do
line_number = Keyword.get(options, :line, 1)
document = Document.new("file:///file.ex", original_text, 0)

with {:ok, document_edits} <- ReplaceWithUnderscore.edits(document, line_number, variable) do
{:ok, document_edits.edits}
end
message =
"""
warning: variable "#{variable}" is unused (if the variable is not meant to be used, prefix it with an underscore)
/file.ex:#{line_number}
"""
|> String.trim()

range =
Document.Range.new(
Document.Position.new(document, line_number, 0),
Document.Position.new(document, line_number + 1, 0)
)

diagnostic = Diagnostic.new(range, message, nil)

changes =
document
|> ReplaceWithUnderscore.actions(range, [diagnostic])
|> Enum.flat_map(& &1.changes.edits)

{:ok, changes}
end

describe "fixes in parameters" do
Expand Down

This file was deleted.

Loading
Loading