-
Notifications
You must be signed in to change notification settings - Fork 196
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add expandMacro custom command (#498)
* add expandMacro command * deprecate custom elixirDocument/macroExpansion method * refactor command executor * add tests * Apply suggestions from code review Co-authored-by: Jason Axelson <[email protected]> * Fix formatting Co-authored-by: Jason Axelson <[email protected]> Co-authored-by: Jason Axelson <[email protected]>
- Loading branch information
1 parent
e1538ed
commit af7ba81
Showing
6 changed files
with
268 additions
and
87 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
96 changes: 96 additions & 0 deletions
96
apps/language_server/lib/language_server/providers/execute_command/apply_spec.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,96 @@ | ||
defmodule ElixirLS.LanguageServer.Providers.ExecuteCommand.ApplySpec do | ||
@moduledoc """ | ||
This module implements a custom command inserting dialyzer suggested function spec. | ||
Generates source file edit as a result. | ||
""" | ||
|
||
alias ElixirLS.LanguageServer.{JsonRpc, SourceFile} | ||
import ElixirLS.LanguageServer.Protocol | ||
alias ElixirLS.LanguageServer.Server | ||
|
||
@behaviour ElixirLS.LanguageServer.Providers.ExecuteCommand | ||
|
||
@default_target_line_length 98 | ||
|
||
@impl ElixirLS.LanguageServer.Providers.ExecuteCommand | ||
def execute("spec:" <> _, args, state) do | ||
[ | ||
%{ | ||
"uri" => uri, | ||
"mod" => mod, | ||
"fun" => fun, | ||
"arity" => arity, | ||
"spec" => spec, | ||
"line" => line | ||
} | ||
] = args | ||
|
||
mod = String.to_atom(mod) | ||
fun = String.to_atom(fun) | ||
|
||
source_file = Server.get_source_file(state, uri) | ||
|
||
cur_text = source_file.text | ||
|
||
# In case line has changed since this suggestion was generated, look for the function's current | ||
# line number and fall back to the previous line number if we can't guess the new one | ||
line = | ||
if SourceFile.function_def_on_line?(cur_text, line, fun) do | ||
line | ||
else | ||
new_line = SourceFile.function_line(mod, fun, arity) | ||
|
||
if SourceFile.function_def_on_line?(cur_text, line, fun) do | ||
new_line | ||
else | ||
raise "Function definition has moved since suggestion was generated. " <> | ||
"Try again after file has been recompiled." | ||
end | ||
end | ||
|
||
cur_line = Enum.at(SourceFile.lines(cur_text), line - 1) | ||
[indentation] = Regex.run(Regex.recompile!(~r/^\s*/), cur_line) | ||
|
||
# Attempt to format to fit within the preferred line length, fallback to having it all on one | ||
# line if anything fails | ||
formatted = | ||
try do | ||
target_line_length = | ||
case SourceFile.formatter_opts(uri) do | ||
{:ok, opts} -> Keyword.get(opts, :line_length, @default_target_line_length) | ||
:error -> @default_target_line_length | ||
end | ||
|
||
target_line_length = target_line_length - String.length(indentation) | ||
|
||
Code.format_string!("@spec #{spec}", line_length: target_line_length) | ||
|> IO.iodata_to_binary() | ||
|> SourceFile.lines() | ||
|> Enum.map(&(indentation <> &1)) | ||
|> Enum.join("\n") | ||
|> Kernel.<>("\n") | ||
rescue | ||
_ -> | ||
"#{indentation}@spec #{spec}\n" | ||
end | ||
|
||
edit_result = | ||
JsonRpc.send_request("workspace/applyEdit", %{ | ||
"label" => "Add @spec to #{mod}.#{fun}/#{arity}", | ||
"edit" => %{ | ||
"changes" => %{ | ||
uri => [%{"range" => range(line - 1, 0, line - 1, 0), "newText" => formatted}] | ||
} | ||
} | ||
}) | ||
|
||
case edit_result do | ||
{:ok, %{"applied" => true}} -> | ||
{:ok, nil} | ||
|
||
other -> | ||
{:error, :server_error, | ||
"cannot insert spec, workspace/applyEdit returned #{inspect(other)}"} | ||
end | ||
end | ||
end |
47 changes: 47 additions & 0 deletions
47
apps/language_server/lib/language_server/providers/execute_command/expand_macro.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,47 @@ | ||
defmodule ElixirLS.LanguageServer.Providers.ExecuteCommand.ExpandMacro do | ||
@moduledoc """ | ||
This module implements a custom command expanding an elixir macro. | ||
Returns a formatted source fragment. | ||
""" | ||
|
||
alias ElixirLS.LanguageServer.Server | ||
|
||
@behaviour ElixirLS.LanguageServer.Providers.ExecuteCommand | ||
|
||
@impl ElixirLS.LanguageServer.Providers.ExecuteCommand | ||
def execute("expandMacro", [uri, text, line], state) | ||
when is_binary(text) and is_integer(line) do | ||
source_file = Server.get_source_file(state, uri) | ||
cur_text = source_file.text | ||
|
||
if String.trim(text) != "" do | ||
formatted = | ||
ElixirSense.expand_full(cur_text, text, line + 1) | ||
|> Map.new(fn {key, value} -> | ||
key = | ||
key | ||
|> Atom.to_string() | ||
|> Macro.camelize() | ||
|> String.replace("Expand", "expand") | ||
|
||
formatted = value |> Code.format_string!() |> List.to_string() | ||
{key, formatted <> "\n"} | ||
end) | ||
|
||
{:ok, formatted} | ||
else | ||
# special case to avoid | ||
# warning: invalid expression (). If you want to invoke or define a function, make sure there are | ||
# no spaces between the function name and its arguments. If you wanted to pass an empty block or code, | ||
# pass a value instead, such as a nil or an atom | ||
# nofile:1 | ||
{:ok, | ||
%{ | ||
"expand" => "\n", | ||
"expandAll" => "\n", | ||
"expandOnce" => "\n", | ||
"expandPartial" => "\n" | ||
}} | ||
end | ||
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
100 changes: 100 additions & 0 deletions
100
apps/language_server/test/providers/execute_command/expand_macro_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,100 @@ | ||
defmodule ElixirLS.LanguageServer.Providers.ExecuteCommand.ExpandMacroTest do | ||
use ExUnit.Case | ||
|
||
alias ElixirLS.LanguageServer.{Server, SourceFile} | ||
alias ElixirLS.LanguageServer.Providers.ExecuteCommand.ExpandMacro | ||
|
||
test "nothing to expand" do | ||
uri = "file:///some_file.ex" | ||
|
||
text = """ | ||
defmodule Abc do | ||
use ElixirLS.Test.MacroA | ||
end | ||
""" | ||
|
||
assert {:ok, res} = | ||
ExpandMacro.execute("expandMacro", [uri, "", 1], %Server{ | ||
source_files: %{ | ||
uri => %SourceFile{ | ||
text: text | ||
} | ||
} | ||
}) | ||
|
||
assert res == %{ | ||
"expand" => "\n", | ||
"expandAll" => "\n", | ||
"expandOnce" => "\n", | ||
"expandPartial" => "\n" | ||
} | ||
|
||
assert {:ok, res} = | ||
ExpandMacro.execute("expandMacro", [uri, "abc", 1], %Server{ | ||
source_files: %{ | ||
uri => %SourceFile{ | ||
text: text | ||
} | ||
} | ||
}) | ||
|
||
assert res == %{ | ||
"expand" => "abc\n", | ||
"expandAll" => "abc\n", | ||
"expandOnce" => "abc\n", | ||
"expandPartial" => "abc\n" | ||
} | ||
end | ||
|
||
test "expands macro" do | ||
uri = "file:///some_file.ex" | ||
|
||
text = """ | ||
defmodule Abc do | ||
use ElixirLS.Test.MacroA | ||
end | ||
""" | ||
|
||
assert {:ok, res} = | ||
ExpandMacro.execute("expandMacro", [uri, "use ElixirLS.Test.MacroA", 1], %Server{ | ||
source_files: %{ | ||
uri => %SourceFile{ | ||
text: text | ||
} | ||
} | ||
}) | ||
|
||
assert res == %{ | ||
"expand" => """ | ||
require(ElixirLS.Test.MacroA) | ||
ElixirLS.Test.MacroA.__using__([]) | ||
""", | ||
"expandAll" => """ | ||
require(ElixirLS.Test.MacroA) | ||
( | ||
import(ElixirLS.Test.MacroA) | ||
def(macro_a_func) do | ||
:ok | ||
end | ||
) | ||
""", | ||
"expandOnce" => """ | ||
require(ElixirLS.Test.MacroA) | ||
ElixirLS.Test.MacroA.__using__([]) | ||
""", | ||
"expandPartial" => """ | ||
require(ElixirLS.Test.MacroA) | ||
( | ||
import(ElixirLS.Test.MacroA) | ||
def(macro_a_func) do | ||
:ok | ||
end | ||
) | ||
""" | ||
} | ||
end | ||
end |