Skip to content

Commit aa9f957

Browse files
committed
feat(commands): from-pipe
1 parent 5ba29e5 commit aa9f957

File tree

4 files changed

+352
-1
lines changed

4 files changed

+352
-1
lines changed

lib/next_ls.ex

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,8 @@ defmodule NextLS do
142142
document_formatting_provider: true,
143143
execute_command_provider: %GenLSP.Structures.ExecuteCommandOptions{
144144
commands: [
145-
"to-pipe"
145+
"to-pipe",
146+
"from-pipe"
146147
]
147148
},
148149
hover_provider: true,
@@ -618,6 +619,19 @@ defmodule NextLS do
618619
) do
619620
reply =
620621
case command do
622+
"from-pipe" ->
623+
[arguments] = params.arguments
624+
625+
uri = arguments["uri"]
626+
position = arguments["position"]
627+
text = lsp.assigns.documents[uri]
628+
629+
NextLS.Commands.FromPipe.new(%{
630+
uri: uri,
631+
text: text,
632+
position: position
633+
})
634+
621635
"to-pipe" ->
622636
[arguments] = params.arguments
623637

lib/next_ls/commands/from_pipe.ex

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
defmodule NextLS.Commands.FromPipe do
2+
@moduledoc false
3+
alias GenLSP.Enumerations.ErrorCodes
4+
alias GenLSP.Structures.Position
5+
alias GenLSP.Structures.Range
6+
alias GenLSP.Structures.TextEdit
7+
alias GenLSP.Structures.WorkspaceEdit
8+
alias NextLS.EditHelpers
9+
10+
defp opts do
11+
Schematic.map(%{
12+
position: Position.schematic(),
13+
uri: Schematic.str(),
14+
text: Schematic.list(Schematic.str())
15+
})
16+
end
17+
18+
def new(opts) do
19+
with {:ok, %{text: text, uri: uri, position: position}} <- Schematic.unify(opts(), Map.new(opts)),
20+
{:ok, %TextEdit{} = edit} <- from_pipe_edit(text, position) do
21+
%WorkspaceEdit{
22+
changes: %{
23+
uri => [edit]
24+
}
25+
}
26+
else
27+
{:error, message} ->
28+
%GenLSP.ErrorResponse{code: ErrorCodes.parse_error(), message: inspect(message)}
29+
30+
{:error, _ast, messages} ->
31+
error =
32+
Enum.map_join(messages, "\n", fn {ctx, message} ->
33+
message <> " on line: #{ctx[:line]}, column: #{ctx[:column]}"
34+
end)
35+
36+
%GenLSP.ErrorResponse{code: ErrorCodes.parse_error(), message: error}
37+
end
38+
end
39+
40+
defp find_pipe_lines(ast, position) do
41+
pipes =
42+
ast
43+
|> Macro.postwalker()
44+
|> Enum.filter(fn
45+
{:|>, context, _} -> (context[:line] || 0) - 1 == position.line
46+
_ -> false
47+
end)
48+
49+
if pipes != [] do
50+
pipe_to_inline = Enum.min_by(pipes, fn {_, context, _} -> abs(position.character - context[:column]) end)
51+
{:|>, _ctx, [{_, lhs, _}, {_, rhs, _}]} = pipe_to_inline
52+
open = lhs
53+
closing = rhs[:closing] || rhs[:end_of_expression] || rhs[:end]
54+
55+
range = %Range{
56+
start: %Position{line: open[:line] - 1, character: open[:column] - 1},
57+
end: %Position{line: closing[:line] - 1, character: closing[:column]}
58+
}
59+
60+
{:ok, pipe_to_inline, range}
61+
else
62+
{:error, "could not find a pipe to inline"}
63+
end
64+
end
65+
66+
defp from_pipe_edit(text, position) do
67+
with {:ok, ast} <- code_to_ast(text),
68+
{:ok, ast_to_edit, range} <- find_pipe_lines(ast, position),
69+
{:ok, indent} = EditHelpers.get_indent(text, position.line),
70+
edit <- get_edit(ast_to_edit, indent) do
71+
{:ok, %TextEdit{new_text: edit, range: range}}
72+
else
73+
error -> error
74+
end
75+
end
76+
77+
defp code_to_ast(lines) do
78+
lines
79+
|> Enum.join("\n")
80+
|> Spitfire.parse()
81+
end
82+
83+
defp get_edit(ast, indent) do
84+
ast
85+
|> inline_pipe()
86+
|> Macro.to_string()
87+
|> EditHelpers.add_indent_to_edit(indent)
88+
end
89+
90+
defp inline_pipe(ast) do
91+
{result, _} =
92+
Macro.postwalk(ast, _changed = false, fn
93+
{:|>, _context, [left_arg, right_arg]}, false ->
94+
{call, context, args} = right_arg
95+
ast = {call, context, [left_arg | args]}
96+
{ast, true}
97+
98+
ast, acc ->
99+
{ast, acc}
100+
end)
101+
102+
result
103+
end
104+
end
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
defmodule NextLS.Commands.FromPipeTest do
2+
use ExUnit.Case, async: true
3+
4+
alias GenLSP.Structures.TextEdit
5+
alias GenLSP.Structures.WorkspaceEdit
6+
alias NextLS.Commands.FromPipe
7+
8+
@parse_error_code -32_700
9+
10+
describe "from-pipe" do
11+
test "works on one liners" do
12+
uri = "my_app.ex"
13+
14+
text =
15+
String.split(
16+
"""
17+
defmodule MyApp do
18+
def to_list(map) do
19+
map |> Enum.to_list()
20+
end
21+
end
22+
""",
23+
"\n"
24+
)
25+
26+
position = %{"line" => 2, "character" => 9}
27+
expected_line = Enum.at(text, 2)
28+
29+
assert %WorkspaceEdit{changes: %{^uri => [edit = %TextEdit{range: range}]}} =
30+
FromPipe.new(%{uri: uri, text: text, position: position})
31+
32+
assert edit.new_text == "Enum.to_list(map)"
33+
assert range.start.line == 2
34+
assert range.start.character == 4
35+
assert range.end.line == 2
36+
assert range.end.character == String.length(expected_line)
37+
end
38+
39+
test "works on one liners with multiple pipes" do
40+
uri = "my_app.ex"
41+
42+
text =
43+
String.split(
44+
"""
45+
defmodule MyApp do
46+
def to_list(map) do
47+
map |> Enum.to_list() |> Map.new()
48+
end
49+
end
50+
""",
51+
"\n"
52+
)
53+
54+
position = %{"line" => 2, "character" => 9}
55+
56+
assert %WorkspaceEdit{changes: %{^uri => [edit = %TextEdit{range: range}]}} =
57+
FromPipe.new(%{uri: uri, text: text, position: position})
58+
59+
assert edit.new_text == "Enum.to_list(map)"
60+
assert range.start.line == 2
61+
assert range.start.character == 4
62+
assert range.end.line == 2
63+
assert range.end.character == 25
64+
end
65+
66+
test "works on separate lines when the cursor is on the pipe" do
67+
# When the cursor is on the pipe
68+
# We should get the line before it to build the ast
69+
uri = "my_app.ex"
70+
71+
text =
72+
String.split(
73+
"""
74+
defmodule MyApp do
75+
def to_list(map) do
76+
map
77+
|> Enum.to_list()
78+
|> Map.new()
79+
end
80+
end
81+
""",
82+
"\n"
83+
)
84+
85+
position = %{"line" => 3, "character" => 5}
86+
expected_line = Enum.at(text, 3)
87+
88+
assert %WorkspaceEdit{changes: %{^uri => [edit = %TextEdit{range: range}]}} =
89+
FromPipe.new(%{uri: uri, text: text, position: position})
90+
91+
assert edit.new_text == "Enum.to_list(map)"
92+
assert range.start.line == 2
93+
assert range.start.character == 4
94+
assert range.end.line == 3
95+
assert range.end.character == String.length(expected_line)
96+
end
97+
98+
test "works on separate lines when the cursor is on the var" do
99+
# When the cursor is on the var
100+
# we should get the next line to build the ast
101+
uri = "my_app.ex"
102+
103+
text =
104+
String.split(
105+
"""
106+
defmodule MyApp do
107+
def to_list(map) do
108+
map
109+
|> Enum.to_list()
110+
|> Map.new()
111+
end
112+
end
113+
""",
114+
"\n"
115+
)
116+
117+
position = %{"line" => 3, "character" => 5}
118+
expected_line = Enum.at(text, 3)
119+
120+
assert %WorkspaceEdit{changes: %{^uri => [edit = %TextEdit{range: range}]}} =
121+
FromPipe.new(%{uri: uri, text: text, position: position})
122+
123+
assert edit.new_text == "Enum.to_list(map)"
124+
assert range.start.line == 2
125+
assert range.start.character == 4
126+
assert range.end.line == 3
127+
assert range.end.character == String.length(expected_line)
128+
end
129+
130+
test "we get an error reply if the ast is bad" do
131+
uri = "my_app.ex"
132+
133+
text =
134+
String.split(
135+
"""
136+
defmodule MyApp do
137+
def to_list(map) do
138+
|> map
139+
|> Enum.to_list()
140+
end
141+
end
142+
""",
143+
"\n"
144+
)
145+
146+
position = %{"line" => 3, "character" => 5}
147+
148+
assert %GenLSP.ErrorResponse{code: @parse_error_code, message: message} =
149+
FromPipe.new(%{uri: uri, text: text, position: position})
150+
151+
assert message =~ "unknown token: end on line: 6, column: 1"
152+
end
153+
154+
test "we handle schematic errors" do
155+
assert %GenLSP.ErrorResponse{code: @parse_error_code, message: message} = FromPipe.new(%{bad_arg: :is_very_bad})
156+
157+
assert message =~ "position: \"expected a map\""
158+
end
159+
160+
test "handles a pipe expression on multiple lines" do
161+
uri = "my_app.ex"
162+
163+
text =
164+
String.split(
165+
"""
166+
defmodule MyApp do
167+
def all_odd?(map) do
168+
map
169+
|> Enum.all?(fn {x, y} ->
170+
Integer.is_odd(y)
171+
end)
172+
end
173+
end
174+
""",
175+
"\n"
176+
)
177+
178+
expected_edit =
179+
String.trim_trailing("""
180+
Enum.all?(
181+
map,
182+
fn {x, y} ->
183+
Integer.is_odd(y)
184+
end
185+
)
186+
""")
187+
188+
# When the position is on `map` and on the cursor
189+
position = %{"line" => 3, "character" => 10}
190+
191+
expected_line = Enum.at(text, 5)
192+
193+
assert %WorkspaceEdit{changes: %{^uri => [edit = %TextEdit{range: range}]}} =
194+
FromPipe.new(%{uri: uri, text: text, position: position})
195+
196+
assert edit.new_text == expected_edit
197+
assert range.start.line == 2
198+
assert range.start.character == 4
199+
assert range.end.line == 5
200+
assert range.end.character == String.length(expected_line)
201+
end
202+
end
203+
end

test/next_ls/commands/pipe_test.exs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,4 +82,34 @@ defmodule NextLS.Commands.PipeTest do
8282
assert range["end"] == %{"character" => 27, "line" => 2}
8383
end)
8484
end
85+
86+
test "transforms pipes to function expressions", %{client: client, bar_path: bar} do
87+
bar_uri = uri(bar)
88+
id = 2
89+
90+
request client, %{
91+
method: "workspace/executeCommand",
92+
id: id,
93+
jsonrpc: "2.0",
94+
params: %{
95+
command: "from-pipe",
96+
arguments: [%{uri: bar_uri, position: %{line: 2, character: 0}}]
97+
}
98+
}
99+
100+
assert_request(client, "workspace/applyEdit", 500, fn params ->
101+
assert %{"edit" => edit, "label" => "Pipe"} = params
102+
103+
assert %{
104+
"changes" => %{
105+
^bar_uri => [%{"newText" => text, "range" => range}]
106+
}
107+
} = edit
108+
109+
expected = "Enum.to_list(Map.new())"
110+
assert text == expected
111+
assert range["start"] == %{"character" => 8, "line" => 2}
112+
assert range["end"] == %{"character" => 31, "line" => 2}
113+
end)
114+
end
85115
end

0 commit comments

Comments
 (0)