-
-
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.
- Loading branch information
Showing
4 changed files
with
352 additions
and
1 deletion.
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
defmodule NextLS.Commands.FromPipe do | ||
@moduledoc false | ||
alias GenLSP.Enumerations.ErrorCodes | ||
alias GenLSP.Structures.Position | ||
alias GenLSP.Structures.Range | ||
alias GenLSP.Structures.TextEdit | ||
alias GenLSP.Structures.WorkspaceEdit | ||
alias NextLS.EditHelpers | ||
|
||
defp opts do | ||
Schematic.map(%{ | ||
position: Position.schematic(), | ||
uri: Schematic.str(), | ||
text: Schematic.list(Schematic.str()) | ||
}) | ||
end | ||
|
||
def new(opts) do | ||
with {:ok, %{text: text, uri: uri, position: position}} <- Schematic.unify(opts(), Map.new(opts)), | ||
{:ok, %TextEdit{} = edit} <- from_pipe_edit(text, position) do | ||
%WorkspaceEdit{ | ||
changes: %{ | ||
uri => [edit] | ||
} | ||
} | ||
else | ||
{:error, message} -> | ||
%GenLSP.ErrorResponse{code: ErrorCodes.parse_error(), message: inspect(message)} | ||
|
||
{:error, _ast, messages} -> | ||
error = | ||
Enum.map_join(messages, "\n", fn {ctx, message} -> | ||
message <> " on line: #{ctx[:line]}, column: #{ctx[:column]}" | ||
end) | ||
|
||
%GenLSP.ErrorResponse{code: ErrorCodes.parse_error(), message: error} | ||
end | ||
end | ||
|
||
defp find_pipe_lines(ast, position) do | ||
pipes = | ||
ast | ||
|> Macro.postwalker() | ||
|> Enum.filter(fn | ||
{:|>, context, _} -> (context[:line] || 0) - 1 == position.line | ||
_ -> false | ||
end) | ||
|
||
if pipes != [] do | ||
pipe_to_inline = Enum.min_by(pipes, fn {_, context, _} -> abs(position.character - context[:column]) end) | ||
{:|>, _ctx, [{_, lhs, _}, {_, rhs, _}]} = pipe_to_inline | ||
open = lhs | ||
closing = rhs[:closing] || rhs[:end_of_expression] || rhs[:end] | ||
|
||
range = %Range{ | ||
start: %Position{line: open[:line] - 1, character: open[:column] - 1}, | ||
end: %Position{line: closing[:line] - 1, character: closing[:column]} | ||
} | ||
|
||
{:ok, pipe_to_inline, range} | ||
else | ||
{:error, "could not find a pipe to inline"} | ||
end | ||
end | ||
|
||
defp from_pipe_edit(text, position) do | ||
with {:ok, ast} <- code_to_ast(text), | ||
{:ok, ast_to_edit, range} <- find_pipe_lines(ast, position), | ||
{:ok, indent} = EditHelpers.get_indent(text, position.line), | ||
edit <- get_edit(ast_to_edit, indent) do | ||
{:ok, %TextEdit{new_text: edit, range: range}} | ||
else | ||
error -> error | ||
end | ||
end | ||
|
||
defp code_to_ast(lines) do | ||
lines | ||
|> Enum.join("\n") | ||
|> Spitfire.parse() | ||
end | ||
|
||
defp get_edit(ast, indent) do | ||
ast | ||
|> inline_pipe() | ||
|> Macro.to_string() | ||
|> EditHelpers.add_indent_to_edit(indent) | ||
end | ||
|
||
defp inline_pipe(ast) do | ||
{result, _} = | ||
Macro.postwalk(ast, _changed = false, fn | ||
{:|>, _context, [left_arg, right_arg]}, false -> | ||
{call, context, args} = right_arg | ||
ast = {call, context, [left_arg | args]} | ||
{ast, true} | ||
|
||
ast, acc -> | ||
{ast, acc} | ||
end) | ||
|
||
result | ||
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,203 @@ | ||
defmodule NextLS.Commands.FromPipeTest do | ||
use ExUnit.Case, async: true | ||
|
||
alias GenLSP.Structures.TextEdit | ||
alias GenLSP.Structures.WorkspaceEdit | ||
alias NextLS.Commands.FromPipe | ||
|
||
@parse_error_code -32_700 | ||
|
||
describe "from-pipe" do | ||
test "works on one liners" do | ||
uri = "my_app.ex" | ||
|
||
text = | ||
String.split( | ||
""" | ||
defmodule MyApp do | ||
def to_list(map) do | ||
map |> Enum.to_list() | ||
end | ||
end | ||
""", | ||
"\n" | ||
) | ||
|
||
position = %{"line" => 2, "character" => 9} | ||
expected_line = Enum.at(text, 2) | ||
|
||
assert %WorkspaceEdit{changes: %{^uri => [edit = %TextEdit{range: range}]}} = | ||
FromPipe.new(%{uri: uri, text: text, position: position}) | ||
|
||
assert edit.new_text == "Enum.to_list(map)" | ||
assert range.start.line == 2 | ||
assert range.start.character == 4 | ||
assert range.end.line == 2 | ||
assert range.end.character == String.length(expected_line) | ||
end | ||
|
||
test "works on one liners with multiple pipes" do | ||
uri = "my_app.ex" | ||
|
||
text = | ||
String.split( | ||
""" | ||
defmodule MyApp do | ||
def to_list(map) do | ||
map |> Enum.to_list() |> Map.new() | ||
end | ||
end | ||
""", | ||
"\n" | ||
) | ||
|
||
position = %{"line" => 2, "character" => 9} | ||
|
||
assert %WorkspaceEdit{changes: %{^uri => [edit = %TextEdit{range: range}]}} = | ||
FromPipe.new(%{uri: uri, text: text, position: position}) | ||
|
||
assert edit.new_text == "Enum.to_list(map)" | ||
assert range.start.line == 2 | ||
assert range.start.character == 4 | ||
assert range.end.line == 2 | ||
assert range.end.character == 25 | ||
end | ||
|
||
test "works on separate lines when the cursor is on the pipe" do | ||
# When the cursor is on the pipe | ||
# We should get the line before it to build the ast | ||
uri = "my_app.ex" | ||
|
||
text = | ||
String.split( | ||
""" | ||
defmodule MyApp do | ||
def to_list(map) do | ||
map | ||
|> Enum.to_list() | ||
|> Map.new() | ||
end | ||
end | ||
""", | ||
"\n" | ||
) | ||
|
||
position = %{"line" => 3, "character" => 5} | ||
expected_line = Enum.at(text, 3) | ||
|
||
assert %WorkspaceEdit{changes: %{^uri => [edit = %TextEdit{range: range}]}} = | ||
FromPipe.new(%{uri: uri, text: text, position: position}) | ||
|
||
assert edit.new_text == "Enum.to_list(map)" | ||
assert range.start.line == 2 | ||
assert range.start.character == 4 | ||
assert range.end.line == 3 | ||
assert range.end.character == String.length(expected_line) | ||
end | ||
|
||
test "works on separate lines when the cursor is on the var" do | ||
# When the cursor is on the var | ||
# we should get the next line to build the ast | ||
uri = "my_app.ex" | ||
|
||
text = | ||
String.split( | ||
""" | ||
defmodule MyApp do | ||
def to_list(map) do | ||
map | ||
|> Enum.to_list() | ||
|> Map.new() | ||
end | ||
end | ||
""", | ||
"\n" | ||
) | ||
|
||
position = %{"line" => 3, "character" => 5} | ||
expected_line = Enum.at(text, 3) | ||
|
||
assert %WorkspaceEdit{changes: %{^uri => [edit = %TextEdit{range: range}]}} = | ||
FromPipe.new(%{uri: uri, text: text, position: position}) | ||
|
||
assert edit.new_text == "Enum.to_list(map)" | ||
assert range.start.line == 2 | ||
assert range.start.character == 4 | ||
assert range.end.line == 3 | ||
assert range.end.character == String.length(expected_line) | ||
end | ||
|
||
test "we get an error reply if the ast is bad" do | ||
uri = "my_app.ex" | ||
|
||
text = | ||
String.split( | ||
""" | ||
defmodule MyApp do | ||
def to_list(map) do | ||
|> map | ||
|> Enum.to_list() | ||
end | ||
end | ||
""", | ||
"\n" | ||
) | ||
|
||
position = %{"line" => 3, "character" => 5} | ||
|
||
assert %GenLSP.ErrorResponse{code: @parse_error_code, message: message} = | ||
FromPipe.new(%{uri: uri, text: text, position: position}) | ||
|
||
assert message =~ "unknown token: end on line: 6, column: 1" | ||
end | ||
|
||
test "we handle schematic errors" do | ||
assert %GenLSP.ErrorResponse{code: @parse_error_code, message: message} = FromPipe.new(%{bad_arg: :is_very_bad}) | ||
|
||
assert message =~ "position: \"expected a map\"" | ||
end | ||
|
||
test "handles a pipe expression on multiple lines" do | ||
uri = "my_app.ex" | ||
|
||
text = | ||
String.split( | ||
""" | ||
defmodule MyApp do | ||
def all_odd?(map) do | ||
map | ||
|> Enum.all?(fn {x, y} -> | ||
Integer.is_odd(y) | ||
end) | ||
end | ||
end | ||
""", | ||
"\n" | ||
) | ||
|
||
expected_edit = | ||
String.trim_trailing(""" | ||
Enum.all?( | ||
map, | ||
fn {x, y} -> | ||
Integer.is_odd(y) | ||
end | ||
) | ||
""") | ||
|
||
# When the position is on `map` and on the cursor | ||
position = %{"line" => 3, "character" => 10} | ||
|
||
expected_line = Enum.at(text, 5) | ||
|
||
assert %WorkspaceEdit{changes: %{^uri => [edit = %TextEdit{range: range}]}} = | ||
FromPipe.new(%{uri: uri, text: text, position: position}) | ||
|
||
assert edit.new_text == expected_edit | ||
assert range.start.line == 2 | ||
assert range.start.character == 4 | ||
assert range.end.line == 5 | ||
assert range.end.character == String.length(expected_line) | ||
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