-
-
Notifications
You must be signed in to change notification settings - Fork 40
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add require code action (#375)
* feat: add require code action This adds a require code action that adds `require Module` to your module whenever a macro is used without requiring it beforehand. It tries to insert the require after all the top level Elixir macros(moduledoc, alias, require, import). * Refactor indent clause * Fix formatting * Refactor module name with &Macro.to_string/1 * reword title --------- Co-authored-by: Mitchell Hanberg <[email protected]>
- Loading branch information
Showing
6 changed files
with
391 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
121 changes: 121 additions & 0 deletions
121
lib/next_ls/extensions/elixir_extension/code_action/require.ex
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
defmodule NextLS.ElixirExtension.CodeAction.Require do | ||
@moduledoc false | ||
|
||
alias GenLSP.Structures.CodeAction | ||
alias GenLSP.Structures.Diagnostic | ||
alias GenLSP.Structures.Position | ||
alias GenLSP.Structures.Range | ||
alias GenLSP.Structures.TextEdit | ||
alias GenLSP.Structures.WorkspaceEdit | ||
|
||
@one_indentation_level " " | ||
@spec new(diagnostic :: Diagnostic.t(), [text :: String.t()], uri :: String.t()) :: [CodeAction.t()] | ||
def new(%Diagnostic{} = diagnostic, text, uri) do | ||
range = diagnostic.range | ||
|
||
with {:ok, require_module} <- get_edit(diagnostic.message), | ||
{:ok, ast} <- parse_ast(text), | ||
{:ok, defm} <- nearest_defmodule(ast, range), | ||
indentation <- get_indent(text, defm), | ||
nearest <- find_nearest_node_for_require(defm), | ||
range <- get_edit_range(nearest) do | ||
[ | ||
%CodeAction{ | ||
title: "Add missing require for #{require_module}", | ||
diagnostics: [diagnostic], | ||
edit: %WorkspaceEdit{ | ||
changes: %{ | ||
uri => [ | ||
%TextEdit{ | ||
new_text: indentation <> "require #{require_module}\n", | ||
range: range | ||
} | ||
] | ||
} | ||
} | ||
} | ||
] | ||
else | ||
_error -> | ||
[] | ||
end | ||
end | ||
|
||
defp parse_ast(text) do | ||
text | ||
|> Enum.join("\n") | ||
|> Spitfire.parse() | ||
end | ||
|
||
defp nearest_defmodule(ast, range) do | ||
defmodules = | ||
ast | ||
|> Macro.prewalker() | ||
|> Enum.filter(fn | ||
{:defmodule, _, _} -> true | ||
_ -> false | ||
end) | ||
|
||
if defmodules != [] do | ||
defm = | ||
Enum.min_by(defmodules, fn {_, ctx, _} -> | ||
range.start.character - ctx[:line] + 1 | ||
end) | ||
|
||
{:ok, defm} | ||
else | ||
{:error, "no defmodule definition"} | ||
end | ||
end | ||
|
||
@module_name ~r/require\s+([^\s]+)\s+before/ | ||
defp get_edit(message) do | ||
case Regex.run(@module_name, message) do | ||
[_, module] -> {:ok, module} | ||
_ -> {:error, "unable to find require"} | ||
end | ||
end | ||
|
||
# Context starts from 1 while LSP starts from 0 | ||
# which works for us since we want to insert the require on the next line | ||
defp get_edit_range(context) do | ||
%Range{ | ||
start: %Position{line: context[:line], character: 0}, | ||
end: %Position{line: context[:line], character: 0} | ||
} | ||
end | ||
|
||
@indent ~r/^(\s*).*/ | ||
defp get_indent(text, {_, defm_context, _}) do | ||
line = defm_context[:line] - 1 | ||
|
||
indent = | ||
text | ||
|> Enum.at(line) | ||
|> then(&Regex.run(@indent, &1)) | ||
|> List.last() | ||
|
||
indent <> @one_indentation_level | ||
end | ||
|
||
@top_level_macros [:import, :alias, :require] | ||
defp find_nearest_node_for_require({:defmodule, context, _} = ast) do | ||
top_level_macros = | ||
ast | ||
|> Macro.prewalker() | ||
|> Enum.filter(fn | ||
{:@, _, [{:moduledoc, _, _}]} -> true | ||
{macro, _, _} when macro in @top_level_macros -> true | ||
_ -> false | ||
end) | ||
|
||
case top_level_macros do | ||
[] -> | ||
context | ||
|
||
_ -> | ||
{_, context, _} = Enum.max_by(top_level_macros, fn {_, ctx, _} -> ctx[:line] end) | ||
context | ||
end | ||
end | ||
end |
202 changes: 202 additions & 0 deletions
202
test/next_ls/extensions/elixir_extension/code_action/require_test.exs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,202 @@ | ||
defmodule NextLS.ElixirExtension.RequireTest do | ||
use ExUnit.Case, async: true | ||
|
||
alias GenLSP.Structures.CodeAction | ||
alias GenLSP.Structures.Position | ||
alias GenLSP.Structures.Range | ||
alias GenLSP.Structures.TextEdit | ||
alias GenLSP.Structures.WorkspaceEdit | ||
alias NextLS.ElixirExtension.CodeAction.Require | ||
|
||
test "adds require to module" do | ||
text = | ||
String.split( | ||
""" | ||
defmodule Test.Require do | ||
def hello() do | ||
Logger.info("foo") | ||
end | ||
end | ||
""", | ||
"\n" | ||
) | ||
|
||
start = %Position{character: 0, line: 1} | ||
|
||
diagnostic = %GenLSP.Structures.Diagnostic{ | ||
data: %{"namespace" => "elixir", "type" => "require"}, | ||
message: "you must require Logger before invoking the macro Logger.info/1", | ||
source: "Elixir", | ||
range: %GenLSP.Structures.Range{ | ||
start: start, | ||
end: %{start | character: 999} | ||
} | ||
} | ||
|
||
uri = "file:///home/owner/my_project/hello.ex" | ||
|
||
assert [code_action] = Require.new(diagnostic, text, uri) | ||
assert is_struct(code_action, CodeAction) | ||
assert [diagnostic] == code_action.diagnostics | ||
assert code_action.title == "Add missing require for Logger" | ||
|
||
assert %WorkspaceEdit{ | ||
changes: %{ | ||
^uri => [ | ||
%TextEdit{ | ||
new_text: " require Logger\n", | ||
range: %Range{start: ^start, end: ^start} | ||
} | ||
] | ||
} | ||
} = code_action.edit | ||
end | ||
|
||
test "adds require after moduledoc" do | ||
text = | ||
String.split( | ||
""" | ||
defmodule Test.Require do | ||
@moduledoc | ||
def hello() do | ||
Logger.info("foo") | ||
end | ||
end | ||
""", | ||
"\n" | ||
) | ||
|
||
start = %Position{character: 0, line: 2} | ||
|
||
diagnostic = %GenLSP.Structures.Diagnostic{ | ||
data: %{"namespace" => "elixir", "type" => "require"}, | ||
message: "you must require Logger before invoking the macro Logger.info/1", | ||
source: "Elixir", | ||
range: %GenLSP.Structures.Range{ | ||
start: start, | ||
end: %{start | character: 999} | ||
} | ||
} | ||
|
||
uri = "file:///home/owner/my_project/hello.ex" | ||
|
||
assert [code_action] = Require.new(diagnostic, text, uri) | ||
assert is_struct(code_action, CodeAction) | ||
assert [diagnostic] == code_action.diagnostics | ||
assert code_action.title == "Add missing require for Logger" | ||
|
||
assert %WorkspaceEdit{ | ||
changes: %{ | ||
^uri => [ | ||
%TextEdit{ | ||
new_text: " require Logger\n", | ||
range: %Range{start: ^start, end: ^start} | ||
} | ||
] | ||
} | ||
} = code_action.edit | ||
end | ||
|
||
test "adds require after alias" do | ||
text = | ||
String.split( | ||
""" | ||
defmodule Test.Require do | ||
@moduledoc | ||
import Test.Foo | ||
alias Test.Bar | ||
def hello() do | ||
Logger.info("foo") | ||
end | ||
end | ||
""", | ||
"\n" | ||
) | ||
|
||
start = %Position{character: 0, line: 4} | ||
|
||
diagnostic = %GenLSP.Structures.Diagnostic{ | ||
data: %{"namespace" => "elixir", "type" => "require"}, | ||
message: "you must require Logger before invoking the macro Logger.info/1", | ||
source: "Elixir", | ||
range: %GenLSP.Structures.Range{ | ||
start: start, | ||
end: %{start | character: 999} | ||
} | ||
} | ||
|
||
uri = "file:///home/owner/my_project/hello.ex" | ||
|
||
assert [code_action] = Require.new(diagnostic, text, uri) | ||
assert is_struct(code_action, CodeAction) | ||
assert [diagnostic] == code_action.diagnostics | ||
assert code_action.title == "Add missing require for Logger" | ||
|
||
assert %WorkspaceEdit{ | ||
changes: %{ | ||
^uri => [ | ||
%TextEdit{ | ||
new_text: " require Logger\n", | ||
range: %Range{start: ^start, end: ^start} | ||
} | ||
] | ||
} | ||
} = code_action.edit | ||
end | ||
|
||
test "figures out the correct module" do | ||
text = | ||
String.split( | ||
""" | ||
defmodule Test do | ||
defmodule Foo do | ||
def hello() do | ||
IO.inspect("foo") | ||
end | ||
end | ||
defmodule Require do | ||
@moduledoc | ||
import Test.Foo | ||
alias Test.Bar | ||
def hello() do | ||
Logger.info("foo") | ||
end | ||
end | ||
end | ||
""", | ||
"\n" | ||
) | ||
|
||
start = %Position{character: 0, line: 11} | ||
|
||
diagnostic = %GenLSP.Structures.Diagnostic{ | ||
data: %{"namespace" => "elixir", "type" => "require"}, | ||
message: "you must require Logger before invoking the macro Logger.info/1", | ||
source: "Elixir", | ||
range: %GenLSP.Structures.Range{ | ||
start: start, | ||
end: %{start | character: 999} | ||
} | ||
} | ||
|
||
uri = "file:///home/owner/my_project/hello.ex" | ||
|
||
assert [code_action] = Require.new(diagnostic, text, uri) | ||
assert is_struct(code_action, CodeAction) | ||
assert [diagnostic] == code_action.diagnostics | ||
assert code_action.title == "Add missing require for Logger" | ||
|
||
assert %WorkspaceEdit{ | ||
changes: %{ | ||
^uri => [ | ||
%TextEdit{ | ||
new_text: " require Logger\n", | ||
range: %Range{start: ^start, end: ^start} | ||
} | ||
] | ||
} | ||
} = code_action.edit | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.