From 774e7cba5ec8d20e2354b575e88ad8d1b7e2d57e Mon Sep 17 00:00:00 2001 From: Mitchell Hanberg Date: Mon, 26 Feb 2024 12:38:12 -0500 Subject: [PATCH] feat(commands): from-pipe (#378) Co-authored-by: Nikola Jichev --- lib/next_ls.ex | 18 +- lib/next_ls/commands/{to_pipe.ex => pipe.ex} | 63 +- lib/next_ls/document_symbol.ex | 4 +- lib/next_ls/runtime/sidecar.ex | 2 + test/next_ls/commands/pipe_test.exs | 605 ++++++++++++++++-- test/next_ls/commands/to_pipe_test.exs | 372 ----------- .../extensions/credo_extension_test.exs | 29 + test/next_ls/pipe_test.exs | 115 ++++ 8 files changed, 769 insertions(+), 439 deletions(-) rename lib/next_ls/commands/{to_pipe.ex => pipe.ex} (62%) delete mode 100644 test/next_ls/commands/to_pipe_test.exs create mode 100644 test/next_ls/pipe_test.exs diff --git a/lib/next_ls.ex b/lib/next_ls.ex index 83a0a03f..2468a2db 100644 --- a/lib/next_ls.ex +++ b/lib/next_ls.ex @@ -142,7 +142,8 @@ defmodule NextLS do document_formatting_provider: true, execute_command_provider: %GenLSP.Structures.ExecuteCommandOptions{ commands: [ - "to-pipe" + "to-pipe", + "from-pipe" ] }, hover_provider: true, @@ -618,6 +619,19 @@ defmodule NextLS do ) do reply = case command do + "from-pipe" -> + [arguments] = params.arguments + + uri = arguments["uri"] + position = arguments["position"] + text = lsp.assigns.documents[uri] + + NextLS.Commands.Pipe.from(%{ + uri: uri, + text: text, + position: position + }) + "to-pipe" -> [arguments] = params.arguments @@ -625,7 +639,7 @@ defmodule NextLS do position = arguments["position"] text = lsp.assigns.documents[uri] - NextLS.Commands.ToPipe.run(%{ + NextLS.Commands.Pipe.to(%{ uri: uri, text: text, position: position diff --git a/lib/next_ls/commands/to_pipe.ex b/lib/next_ls/commands/pipe.ex similarity index 62% rename from lib/next_ls/commands/to_pipe.ex rename to lib/next_ls/commands/pipe.ex index 5c78ad1b..76ff8fd8 100644 --- a/lib/next_ls/commands/to_pipe.ex +++ b/lib/next_ls/commands/pipe.ex @@ -1,4 +1,4 @@ -defmodule NextLS.Commands.ToPipe do +defmodule NextLS.Commands.Pipe do @moduledoc false import Schematic @@ -18,7 +18,7 @@ defmodule NextLS.Commands.ToPipe do }) end - def run(opts) do + def to(opts) do with {:ok, %{text: text, uri: uri, position: position}} <- unify(opts(), Map.new(opts)), {:ok, ast} = parse(text), {:ok, {t, m, [argument | rest]} = original} <- get_node(ast, position) do @@ -46,6 +46,34 @@ defmodule NextLS.Commands.ToPipe do end end + def from(opts) do + with {:ok, %{text: text, uri: uri, position: position}} <- unify(opts(), Map.new(opts)), + {:ok, ast} = parse(text), + {:ok, {:|>, _m, [left, {right, _, args}]} = original} <- get_pipe_node(ast, position) do + range = make_range(original) + indent = EditHelpers.get_indent(text, range.start.line) + unpiped = {right, [], [left | args]} + + %WorkspaceEdit{ + changes: %{ + uri => [ + %TextEdit{ + new_text: + EditHelpers.add_indent_to_edit( + Macro.to_string(unpiped), + indent + ), + range: range + } + ] + } + } + else + {:error, message} -> + %GenLSP.ErrorResponse{code: ErrorCodes.parse_error(), message: inspect(message)} + end + end + defp parse(lines) do lines |> Enum.join("\n") @@ -109,4 +137,35 @@ defmodule NextLS.Commands.ToPipe do {:ok, node} end end + + def get_pipe_node(ast, pos) do + pos = [line: pos.line + 1, column: pos.character + 1] + + result = + ast + |> Z.zip() + |> Z.traverse(nil, fn tree, acc -> + node = Z.node(tree) + range = Sourceror.get_range(node) + + if not is_nil(range) and match?({:|>, _, _}, node) do + if Sourceror.compare_positions(range.start, pos) == :lt && + Sourceror.compare_positions(range.end, pos) == :gt do + {tree, node} + else + {tree, acc} + end + else + {tree, acc} + end + end) + + case result do + {_, nil} -> + {:error, "could not find a pipe operator at the cursor position"} + + {_, {_t, _m, [_argument | _rest]} = node} -> + {:ok, node} + end + end end diff --git a/lib/next_ls/document_symbol.ex b/lib/next_ls/document_symbol.ex index 1fa21cca..7a283f69 100644 --- a/lib/next_ls/document_symbol.ex +++ b/lib/next_ls/document_symbol.ex @@ -33,9 +33,7 @@ defmodule NextLS.DocumentSymbol do ast end - ast - |> walker(nil) - |> List.wrap() + List.wrap(walker(ast, nil)) end defp walker([{{:__literal__, _, [:do]}, {_, _, _exprs} = ast}], mod) do diff --git a/lib/next_ls/runtime/sidecar.ex b/lib/next_ls/runtime/sidecar.ex index a2b2e4b3..83adc419 100644 --- a/lib/next_ls/runtime/sidecar.ex +++ b/lib/next_ls/runtime/sidecar.ex @@ -17,6 +17,7 @@ defmodule NextLS.Runtime.Sidecar do end def handle_info({:tracer, :dbg, term}, state) do + # credo:disable-for-next-line dbg(term) {:noreply, state} @@ -82,6 +83,7 @@ defmodule NextLS.Runtime.Sidecar do end def handle_info({{:tracer, :dbg}, payload}, state) do + # credo:disable-for-next-line dbg(payload) {:noreply, state} end diff --git a/test/next_ls/commands/pipe_test.exs b/test/next_ls/commands/pipe_test.exs index 22254a64..f276faae 100644 --- a/test/next_ls/commands/pipe_test.exs +++ b/test/next_ls/commands/pipe_test.exs @@ -1,85 +1,570 @@ defmodule NextLS.Commands.PipeTest do use ExUnit.Case, async: true - import GenLSP.Test - import NextLS.Support.Utils + alias GenLSP.Structures.TextEdit + alias GenLSP.Structures.WorkspaceEdit + alias NextLS.Commands.Pipe - @moduletag :tmp_dir - @moduletag root_paths: ["my_proj"] + @parse_error_code -32_700 - setup %{tmp_dir: tmp_dir} do - File.mkdir_p!(Path.join(tmp_dir, "my_proj/lib")) - File.write!(Path.join(tmp_dir, "my_proj/mix.exs"), mix_exs()) + describe "to-pipe" do + test "works on one liners" do + uri = "my_app.ex" - cwd = Path.join(tmp_dir, "my_proj") + text = + String.split( + """ + defmodule MyApp do + def to_list(map) do + Enum.to_list(map) + end + end + """, + "\n" + ) - foo_path = Path.join(cwd, "lib/foo.ex") + expected_edit = "map |> Enum.to_list()" - foo = """ - defmodule Foo do - def to_list() do - Enum.to_list(Map.new()) - end + line = 2 + position = %{"line" => line, "character" => 19} + + assert %WorkspaceEdit{changes: %{^uri => [edit = %TextEdit{range: range}]}} = + Pipe.to(%{uri: uri, text: text, position: position}) + + assert edit.new_text == expected_edit + assert range.start.line == line + assert range.start.character == 4 + assert range.end.line == line + assert range.end.character == 21 + end + + test "works on one liners with imports" do + uri = "my_app.ex" + + text = + String.split( + """ + defmodule MyApp do + import Enum + + def to_list(map) do + to_list(map) + end + end + """, + "\n" + ) + + line = 4 + position = %{"line" => line, "character" => 5} + expected_edit = "map |> to_list()" + + assert %WorkspaceEdit{changes: %{^uri => [edit = %TextEdit{range: range}]}} = + Pipe.to(%{uri: uri, text: text, position: position}) + + assert edit.new_text == expected_edit + assert range.start.line == line + assert range.start.character == 4 + assert range.end.line == line + assert range.end.character == 16 + end + + test "works on one liners with nested function calls" do + uri = "my_app.ex" + + text = + String.split( + """ + defmodule MyApp do + def to_list(map) do + to_list(Map.new()) + end + end + """, + "\n" + ) + + expected_edit = "Map.new() |> to_list()" + + line = 2 + position = %{"line" => line, "character" => 10} + + assert %WorkspaceEdit{changes: %{^uri => [edit = %TextEdit{range: range}]}} = + Pipe.to(%{uri: uri, text: text, position: position}) + + assert edit.new_text == expected_edit + assert range.start.line == line + assert range.start.character == 4 + assert range.end.line == line + assert range.end.character == 22 end - """ - File.write!(foo_path, foo) + test "works on one liners with nested function calls with qualified calls" do + uri = "my_app.ex" - bar_path = Path.join(cwd, "lib/bar.ex") + text = + String.split( + """ + defmodule MyApp do + def to_list(map) do + Enum.to_list(Map.new()) + end + end + """, + "\n" + ) - bar = """ - defmodule Bar do - def to_list() do + expected_edit = + String.trim_trailing(""" Map.new() |> Enum.to_list() - end + """) + + line = 2 + position = %{"line" => line, "character" => 7} + + assert %WorkspaceEdit{changes: %{^uri => [edit = %TextEdit{range: range}]}} = + Pipe.to(%{uri: uri, text: text, position: position}) + + assert edit.new_text == expected_edit + assert range.start.line == line + assert range.start.character == 4 + assert range.end.line == 2 + assert range.end.character == 27 end - """ - File.write!(bar_path, bar) + @test_scenarios [ + {6, "to_list(Map.new)", "Map.new() |> to_list()"}, + {6, "to_list(a, b, c)", "a |> to_list(b, c)"}, + {10, "Foo.Bar.baz(foo, bar)", "foo |> Foo.Bar.baz(bar)"}, + {10, "Foo.Bar.baz(foo, bar, Map.new())", "foo |> Foo.Bar.baz(bar, Map.new())"} + ] - [foo: foo, foo_path: foo_path, bar: bar, bar_path: bar_path] - end + test "small test scenarios work" do + uri = "foo.ex" + position = %{"line" => 0, "character" => 0} + + Enum.each(@test_scenarios, fn {character, to_transform, expected} -> + position = %{position | "character" => character} + + assert %WorkspaceEdit{changes: %{^uri => [edit]}} = + Pipe.to(%{uri: uri, text: [to_transform], position: position}) + + assert edit.new_text == expected + end) + end + + test "handles broken code" do + uri = "my_app.ex" + + text = + String.split( + """ + defmodule MyApp do + def bad_ast(map) do + Enum.to_list(foo + end + end + """, + "\n" + ) + + position = %{"line" => 2, "character" => 15} + + assert %WorkspaceEdit{ + change_annotations: nil, + changes: %{ + "my_app.ex" => [ + %TextEdit{ + new_text: "foo |> Enum.to_list()", + range: %GenLSP.Structures.Range{ + end: %GenLSP.Structures.Position{character: 20, line: 2}, + start: %GenLSP.Structures.Position{character: 4, line: 2} + } + } + ] + }, + document_changes: nil + } = + Pipe.to(%{uri: uri, text: text, position: position}) + end + + test "handles bad cursor position" do + uri = "my_app.ex" + + text = + String.split( + """ + foo = [:one, two] + """, + "\n" + ) + + position = %{"line" => 0, "character" => 5} + + assert %GenLSP.ErrorResponse{code: @parse_error_code, message: message} = + Pipe.to(%{uri: uri, text: text, position: position}) + + assert message =~ "could not find an argument to extract at the cursor position" + end + + test "handles schematic errors" do + assert %GenLSP.ErrorResponse{code: @parse_error_code, message: message} = Pipe.to(%{bad_arg: :is_very_bad}) + + assert message =~ "position: \"expected a map\"" + end + + test "handles an expression on multiple lines" do + uri = "my_app.ex" + + text = + String.split( + """ + defmodule MyApp do + def all_odd?(map) do + Enum.all?(map, fn {x, y} -> + Integer.is_odd(y) + end) + end + end + """, + "\n" + ) + + expected_edit = + String.trim_trailing(""" + map + |> Enum.all?(fn {x, y} -> + Integer.is_odd(y) + end) + """) + + position = %{"line" => 2, "character" => 15} + + assert %WorkspaceEdit{changes: %{^uri => [edit = %TextEdit{range: range}]}} = + Pipe.to(%{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 == 4 + assert range.end.character == 8 + end + + test "handles piping into a case/if/unless" do + uri = "my_app.ex" + + text = + String.split( + """ + defmodule MyApp do + def check(result) do + case result do + {:ok, _success} -> :ok + {:error, error} -> IO.inspect(error) + end + end + end + """, + "\n" + ) + + expected_edit = + String.trim_trailing(""" + result + |> case do + {:ok, _success} -> :ok + {:error, error} -> IO.inspect(error) + end + """) + + position = %{"line" => 2, "character" => 13} + + assert %WorkspaceEdit{changes: %{^uri => [edit = %TextEdit{range: range}]}} = + Pipe.to(%{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 == 7 + end + + test "handles nested calls in conditionals" do + uri = "my_app.ex" + + text = + String.split( + """ + defmodule MyApp do + def check(result) do + case parse_result(result) do + {:ok, _success} -> :ok + {:error, error} -> IO.inspect(error) + end + end + end + """, + "\n" + ) + + position = %{"line" => 2, "character" => 5} - setup :with_lsp + expected_edit = + String.trim_trailing(""" + parse_result(result) + |> case do + {:ok, _success} -> :ok + {:error, error} -> IO.inspect(error) + end + """) + + assert %WorkspaceEdit{changes: %{^uri => [edit = %TextEdit{range: range}]}} = + Pipe.to(%{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 == 7 + end - setup context do - assert :ok == notify(context.client, %{method: "initialized", jsonrpc: "2.0", params: %{}}) - assert_is_ready(context, "my_proj") - assert_compiled(context, "my_proj") - assert_notification "$/progress", %{"value" => %{"kind" => "end", "message" => "Finished indexing!"}} + test "another case" do + uri = "my_app.ex" - did_open(context.client, context.foo_path, context.foo) - did_open(context.client, context.bar_path, context.bar) - context + text = + String.split( + """ + Enum.map( + NextLS.ASTHelpers.Variables.list_variable_references(file, {line, col}), + fn {_name, {startl..endl, startc..endc}} -> + [file, startl, endl, startc, endc] + end + ) + """, + "\n" + ) + + expected_edit = + String.trim_trailing(""" + NextLS.ASTHelpers.Variables.list_variable_references(file, {line, col}) + |> Enum.map(fn {_name, {startl..endl, startc..endc}} -> + [file, startl, endl, startc, endc] + end) + """) + + line = 0 + position = %{"line" => line, "character" => 5} + + assert %WorkspaceEdit{changes: %{^uri => [edit = %TextEdit{range: range}]}} = + Pipe.to(%{uri: uri, text: text, position: position}) + + assert edit.new_text == expected_edit + assert range.start.line == line + assert range.start.character == 0 + assert range.end.line == 5 + assert range.end.character == 1 + end end - test "transforms nested function expressions to pipes", %{client: client, foo_path: foo} do - foo_uri = uri(foo) - id = 1 - - request client, %{ - method: "workspace/executeCommand", - id: id, - jsonrpc: "2.0", - params: %{ - command: "to-pipe", - arguments: [%{uri: foo_uri, position: %{line: 2, character: 19}}] - } - } - - assert_request(client, "workspace/applyEdit", 500, fn params -> - assert %{"edit" => edit, "label" => "Pipe"} = params - - assert %{ - "changes" => %{ - ^foo_uri => [%{"newText" => text, "range" => range}] + 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} + + assert %WorkspaceEdit{changes: %{^uri => [edit = %TextEdit{range: range}]}} = + Pipe.from(%{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 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}]}} = + Pipe.from(%{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}]}} = + Pipe.from(%{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" => 2, "character" => 5} + expected_line = Enum.at(text, 3) + + assert %WorkspaceEdit{changes: %{^uri => [edit = %TextEdit{range: range}]}} = + Pipe.from(%{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 "handles broken code" do + uri = "my_app.ex" + + text = + String.split( + """ + defmodule MyApp do + def to_list(map) do + + map + |> Enum.to_list() + end + """, + "\n" + ) + + position = %{"line" => 3, "character" => 5} + + assert %GenLSP.Structures.WorkspaceEdit{ + changes: %{ + "my_app.ex" => [ + %GenLSP.Structures.TextEdit{ + new_text: "Enum.to_list(map)", + range: %GenLSP.Structures.Range{ + end: %GenLSP.Structures.Position{character: 21, line: 4}, + start: %GenLSP.Structures.Position{character: 4, line: 3} + } + } + ] } - } = edit + } = Pipe.from(%{uri: uri, text: text, position: position}) + end + + test "we handle schematic errors" do + assert %GenLSP.ErrorResponse{code: @parse_error_code, message: message} = Pipe.from(%{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" - expected = "Map.new() |> Enum.to_list()" - assert text == expected - assert range["start"] == %{"character" => 4, "line" => 2} - assert range["end"] == %{"character" => 27, "line" => 2} - end) + 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" => 2, "character" => 5} + + expected_line = Enum.at(text, 5) + + assert %WorkspaceEdit{changes: %{^uri => [edit = %TextEdit{range: range}]}} = + Pipe.from(%{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 diff --git a/test/next_ls/commands/to_pipe_test.exs b/test/next_ls/commands/to_pipe_test.exs deleted file mode 100644 index 4f762cb8..00000000 --- a/test/next_ls/commands/to_pipe_test.exs +++ /dev/null @@ -1,372 +0,0 @@ -defmodule NextLS.Commands.ToPipeTest do - use ExUnit.Case, async: true - - alias GenLSP.Structures.TextEdit - alias GenLSP.Structures.WorkspaceEdit - alias NextLS.Commands.ToPipe - - @parse_error_code -32_700 - - describe "to-pipe" do - test "works on one liners" do - uri = "my_app.ex" - - text = - String.split( - """ - defmodule MyApp do - def to_list(map) do - Enum.to_list(map) - end - end - """, - "\n" - ) - - expected_edit = "map |> Enum.to_list()" - - line = 2 - position = %{"line" => line, "character" => 19} - - assert %WorkspaceEdit{changes: %{^uri => [edit = %TextEdit{range: range}]}} = - ToPipe.run(%{uri: uri, text: text, position: position}) - - assert edit.new_text == expected_edit - assert range.start.line == line - assert range.start.character == 4 - assert range.end.line == line - assert range.end.character == 21 - end - - test "works on one liners with imports" do - uri = "my_app.ex" - - text = - String.split( - """ - defmodule MyApp do - import Enum - - def to_list(map) do - to_list(map) - end - end - """, - "\n" - ) - - line = 4 - position = %{"line" => line, "character" => 5} - expected_edit = "map |> to_list()" - - assert %WorkspaceEdit{changes: %{^uri => [edit = %TextEdit{range: range}]}} = - ToPipe.run(%{uri: uri, text: text, position: position}) - - assert edit.new_text == expected_edit - assert range.start.line == line - assert range.start.character == 4 - assert range.end.line == line - assert range.end.character == 16 - end - - test "works on one liners with nested function calls" do - uri = "my_app.ex" - - text = - String.split( - """ - defmodule MyApp do - def to_list(map) do - to_list(Map.new()) - end - end - """, - "\n" - ) - - expected_edit = "Map.new() |> to_list()" - - line = 2 - position = %{"line" => line, "character" => 10} - - assert %WorkspaceEdit{changes: %{^uri => [edit = %TextEdit{range: range}]}} = - ToPipe.run(%{uri: uri, text: text, position: position}) - - assert edit.new_text == expected_edit - assert range.start.line == line - assert range.start.character == 4 - assert range.end.line == line - assert range.end.character == 22 - end - - test "works on one liners with nested function calls with qualified calls" do - uri = "my_app.ex" - - text = - String.split( - """ - defmodule MyApp do - def to_list(map) do - Enum.to_list(Map.new()) - end - end - """, - "\n" - ) - - expected_edit = - String.trim_trailing(""" - Map.new() |> Enum.to_list() - """) - - line = 2 - position = %{"line" => line, "character" => 7} - - assert %WorkspaceEdit{changes: %{^uri => [edit = %TextEdit{range: range}]}} = - ToPipe.run(%{uri: uri, text: text, position: position}) - - assert edit.new_text == expected_edit - assert range.start.line == line - assert range.start.character == 4 - assert range.end.line == 2 - assert range.end.character == 27 - end - - @test_scenarios [ - {6, "to_list(Map.new)", "Map.new() |> to_list()"}, - {6, "to_list(a, b, c)", "a |> to_list(b, c)"}, - {10, "Foo.Bar.baz(foo, bar)", "foo |> Foo.Bar.baz(bar)"}, - {10, "Foo.Bar.baz(foo, bar, Map.new())", "foo |> Foo.Bar.baz(bar, Map.new())"} - ] - - test "small test scenarios work" do - uri = "foo.ex" - position = %{"line" => 0, "character" => 0} - - Enum.each(@test_scenarios, fn {character, to_transform, expected} -> - dbg(to_transform) - position = %{position | "character" => character} - - assert %WorkspaceEdit{changes: %{^uri => [edit]}} = - ToPipe.run(%{uri: uri, text: [to_transform], position: position}) - - assert edit.new_text == expected - end) - end - - test "handles broken code" do - uri = "my_app.ex" - - text = - String.split( - """ - defmodule MyApp do - def bad_ast(map) do - Enum.to_list(foo - end - end - """, - "\n" - ) - - position = %{"line" => 2, "character" => 15} - - assert %WorkspaceEdit{ - change_annotations: nil, - changes: %{ - "my_app.ex" => [ - %TextEdit{ - new_text: "foo |> Enum.to_list()", - range: %GenLSP.Structures.Range{ - end: %GenLSP.Structures.Position{character: 20, line: 2}, - start: %GenLSP.Structures.Position{character: 4, line: 2} - } - } - ] - }, - document_changes: nil - } = - ToPipe.run(%{uri: uri, text: text, position: position}) - end - - test "handles bad cursor position" do - uri = "my_app.ex" - - text = - String.split( - """ - foo = [:one, two] - """, - "\n" - ) - - position = %{"line" => 0, "character" => 5} - - assert %GenLSP.ErrorResponse{code: @parse_error_code, message: message} = - ToPipe.run(%{uri: uri, text: text, position: position}) - - assert message =~ "could not find an argument to extract at the cursor position" - end - - test "handles schematic errors" do - assert %GenLSP.ErrorResponse{code: @parse_error_code, message: message} = ToPipe.run(%{bad_arg: :is_very_bad}) - - assert message =~ "position: \"expected a map\"" - end - - test "handles an expression on multiple lines" do - uri = "my_app.ex" - - text = - String.split( - """ - defmodule MyApp do - def all_odd?(map) do - Enum.all?(map, fn {x, y} -> - Integer.is_odd(y) - end) - end - end - """, - "\n" - ) - - expected_edit = - String.trim_trailing(""" - map - |> Enum.all?(fn {x, y} -> - Integer.is_odd(y) - end) - """) - - position = %{"line" => 2, "character" => 15} - - assert %WorkspaceEdit{changes: %{^uri => [edit = %TextEdit{range: range}]}} = - ToPipe.run(%{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 == 4 - assert range.end.character == 8 - end - - test "handles piping into a case/if/unless" do - uri = "my_app.ex" - - text = - String.split( - """ - defmodule MyApp do - def check(result) do - case result do - {:ok, _success} -> :ok - {:error, error} -> IO.inspect(error) - end - end - end - """, - "\n" - ) - - expected_edit = - String.trim_trailing(""" - result - |> case do - {:ok, _success} -> :ok - {:error, error} -> IO.inspect(error) - end - """) - - position = %{"line" => 2, "character" => 13} - - assert %WorkspaceEdit{changes: %{^uri => [edit = %TextEdit{range: range}]}} = - ToPipe.run(%{uri: uri, text: text, position: position}) - - dbg(edit.new_text) - 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 == 7 - end - - test "handles nested calls in conditionals" do - uri = "my_app.ex" - - text = - String.split( - """ - defmodule MyApp do - def check(result) do - case parse_result(result) do - {:ok, _success} -> :ok - {:error, error} -> IO.inspect(error) - end - end - end - """, - "\n" - ) - - position = %{"line" => 2, "character" => 5} - - expected_edit = - String.trim_trailing(""" - parse_result(result) - |> case do - {:ok, _success} -> :ok - {:error, error} -> IO.inspect(error) - end - """) - - assert %WorkspaceEdit{changes: %{^uri => [edit = %TextEdit{range: range}]}} = - ToPipe.run(%{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 == 7 - end - - test "another case" do - uri = "my_app.ex" - - text = - String.split( - """ - Enum.map( - NextLS.ASTHelpers.Variables.list_variable_references(file, {line, col}), - fn {_name, {startl..endl, startc..endc}} -> - [file, startl, endl, startc, endc] - end - ) - """, - "\n" - ) - - expected_edit = - String.trim_trailing(""" - NextLS.ASTHelpers.Variables.list_variable_references(file, {line, col}) - |> Enum.map(fn {_name, {startl..endl, startc..endc}} -> - [file, startl, endl, startc, endc] - end) - """) - - line = 0 - position = %{"line" => line, "character" => 5} - - assert %WorkspaceEdit{changes: %{^uri => [edit = %TextEdit{range: range}]}} = - ToPipe.run(%{uri: uri, text: text, position: position}) - - dbg(range) - - assert edit.new_text == expected_edit - assert range.start.line == line - assert range.start.character == 0 - assert range.end.line == 5 - assert range.end.character == 1 - end - end -end diff --git a/test/next_ls/extensions/credo_extension_test.exs b/test/next_ls/extensions/credo_extension_test.exs index 6dc848f6..928a3000 100644 --- a/test/next_ls/extensions/credo_extension_test.exs +++ b/test/next_ls/extensions/credo_extension_test.exs @@ -28,6 +28,35 @@ defmodule NextLS.CredoExtensionTest do end """) + credo = Path.join(cwd, "my_proj/.credo.exs") + + File.write!(credo, ~S""" + %{ + configs: [ + %{ + name: "default", + files: %{ + included: ["lib/", "test/"], + excluded: [~r"/_build/", ~r"/deps/", ~r"/node_modules/"] + }, + plugins: [], + requires: [], + strict: false, + parse_timeout: 5000, + color: true, + checks: %{ + enabled: [ + {Credo.Check.Readability.ModuleDoc, []}, + {Credo.Check.Warning.Dbg, []}, + {Credo.Check.Warning.IoInspect, []}, + ], + disabled: [] + } + } + ] + } + """) + [foo: foo] end diff --git a/test/next_ls/pipe_test.exs b/test/next_ls/pipe_test.exs new file mode 100644 index 00000000..8d7ee811 --- /dev/null +++ b/test/next_ls/pipe_test.exs @@ -0,0 +1,115 @@ +defmodule NextLS.PipeTest do + use ExUnit.Case, async: true + + import GenLSP.Test + import NextLS.Support.Utils + + @moduletag :tmp_dir + @moduletag root_paths: ["my_proj"] + + setup %{tmp_dir: tmp_dir} do + File.mkdir_p!(Path.join(tmp_dir, "my_proj/lib")) + File.write!(Path.join(tmp_dir, "my_proj/mix.exs"), mix_exs()) + + cwd = Path.join(tmp_dir, "my_proj") + + foo_path = Path.join(cwd, "lib/foo.ex") + + foo = """ + defmodule Foo do + def to_list() do + Enum.to_list(Map.new()) + end + end + """ + + File.write!(foo_path, foo) + + bar_path = Path.join(cwd, "lib/bar.ex") + + bar = """ + defmodule Bar do + def to_list() do + Map.new() |> Enum.to_list() + end + end + """ + + File.write!(bar_path, bar) + + [foo: foo, foo_path: foo_path, bar: bar, bar_path: bar_path] + end + + setup :with_lsp + + setup context do + assert :ok == notify(context.client, %{method: "initialized", jsonrpc: "2.0", params: %{}}) + assert_is_ready(context, "my_proj") + assert_compiled(context, "my_proj") + assert_notification "$/progress", %{"value" => %{"kind" => "end", "message" => "Finished indexing!"}} + + did_open(context.client, context.foo_path, context.foo) + did_open(context.client, context.bar_path, context.bar) + context + end + + test "transforms nested function expressions to pipes", %{client: client, foo_path: foo} do + foo_uri = uri(foo) + id = 1 + + request client, %{ + method: "workspace/executeCommand", + id: id, + jsonrpc: "2.0", + params: %{ + command: "to-pipe", + arguments: [%{uri: foo_uri, position: %{line: 2, character: 19}}] + } + } + + assert_request(client, "workspace/applyEdit", 500, fn params -> + assert %{"edit" => edit, "label" => "Pipe"} = params + + assert %{ + "changes" => %{ + ^foo_uri => [%{"newText" => text, "range" => range}] + } + } = edit + + expected = "Map.new() |> Enum.to_list()" + assert text == expected + assert range["start"] == %{"character" => 4, "line" => 2} + assert range["end"] == %{"character" => 27, "line" => 2} + end) + end + + test "transforms pipes to function expressions", %{client: client, bar_path: bar} do + bar_uri = uri(bar) + id = 2 + + request client, %{ + method: "workspace/executeCommand", + id: id, + jsonrpc: "2.0", + params: %{ + command: "from-pipe", + arguments: [%{uri: bar_uri, position: %{line: 2, character: 9}}] + } + } + + assert_request(client, "workspace/applyEdit", 500, fn params -> + assert %{"edit" => edit, "label" => "Pipe"} = params + + assert %{ + "changes" => %{ + ^bar_uri => [%{"newText" => text, "range" => range}] + } + } = edit + + expected = "Enum.to_list(Map.new())" + assert text == expected + assert range["start"] == %{"character" => 4, "line" => 2} + assert range["end"] == %{"character" => 31, "line" => 2} + end) + end +end