diff --git a/lib/sourceror/zipper.ex b/lib/sourceror/zipper.ex index 58d69bc..759a238 100644 --- a/lib/sourceror/zipper.ex +++ b/lib/sourceror/zipper.ex @@ -477,6 +477,105 @@ defmodule Sourceror.Zipper do defp into(%Z{path: nil} = zipper, %Z{path: path, supertree: supertree}), do: %{zipper | path: path, supertree: supertree} + @doc """ + Matches and moves to the location of a `__cursor__` in provided source code. + + Use `__cursor__()` to match a cursor in the provided source code. Use `__` to skip any code at a point. + + For example: + + ```elixir + zipper = + \"\"\" + if true do + 10 + end + \"\"\" + |> Sourceror.Zipper.zip() + + pattern = + \"\"\" + if __ do + __cursor__ + end + \"\"\" + + zipper + |> Zipper.move_to_cursor(pattern) + |> Zipper.subtree() + |> Zipper.node() + # => 10 + ``` + """ + @spec move_to_cursor(t(), String.t() | t()) :: t() | nil + def move_to_cursor(%Z{} = zipper, pattern) when is_binary(pattern) do + pattern + |> Sourceror.parse_string!() + |> zip() + |> then(&do_move_to_cursor(zipper, &1)) + end + + def move_to_cursor(%Z{} = zipper, %Z{} = pattern_zipper) do + do_move_to_cursor(zipper, pattern_zipper) + end + + defp do_move_to_cursor(%Z{} = zipper, %Z{} = pattern_zipper) do + cond do + is_cursor?(pattern_zipper |> subtree() |> node()) -> + zipper + + match_type = zippers_match(zipper, pattern_zipper) -> + move = + case match_type do + :skip -> &skip/1 + :next -> &next/1 + end + + with zipper when not is_nil(zipper) <- move.(zipper), + pattern_zipper when not is_nil(pattern_zipper) <- move.(pattern_zipper) do + do_move_to_cursor(zipper, pattern_zipper) + end + + true -> + nil + end + end + + defp is_cursor?({:__cursor__, _, []}), do: true + defp is_cursor?(_other), do: false + + defp zippers_match(zipper, pattern_zipper) do + zipper_node = + zipper + |> subtree() + |> node() + + pattern_node = + pattern_zipper + |> subtree() + |> node() + + case {zipper_node, pattern_node} do + {_, {:__, _, _}} -> + :skip + + {{call, _, _}, {call, _, _}} -> + :next + + {{_, _}, {_, _}} -> + :next + + {same, same} -> + :next + + {left, right} when is_list(left) and is_list(right) -> + :next + + _ -> + false + end + end + @doc """ Returns a `zipper` to the `node` that satisfies the `predicate` function, or `nil` if none is found. diff --git a/test/zipper_test.exs b/test/zipper_test.exs index f67e2fb..ee2ba51 100644 --- a/test/zipper_test.exs +++ b/test/zipper_test.exs @@ -739,4 +739,82 @@ defmodule SourcerorTest.ZipperTest do """ end end + + describe "move_to_cursor/2" do + test "if the cursor is top level, it matches everything" do + code = + """ + if foo == :bar do + IO.puts("Hello") + end + """ + |> Sourceror.parse_string!() + |> Z.zip() + + seek = """ + __cursor__() + """ + + assert code == Z.move_to_cursor(code, seek) + end + + test "if the cursor is inside of a block" do + code = + """ + if foo == :bar do + IO.puts("Hello") + end + """ + |> Sourceror.parse_string!() + |> Z.zip() + + seek = """ + if foo == :bar do + __cursor__() + end + """ + + assert new_zipper = Z.move_to_cursor(code, seek) + + assert "IO.puts(\"Hello\")" == + new_zipper |> Z.subtree() |> Z.node() |> Sourceror.to_string() + end + + test "a really complicated example" do + code = + """ + defmodule Foo do + @foo File.read!("foo.txt") + + case @foo do + "foo" -> + 10 + + "bar" -> + 20 + end + end + """ + |> Sourceror.parse_string!() + |> Z.zip() + + seek = """ + defmodule Foo do + @foo File.read!("foo.txt") + + case @foo do + __ -> + __ + + "bar" -> + __cursor__() + end + end + """ + + assert new_zipper = Z.move_to_cursor(code, seek) + + assert "20" == new_zipper |> Z.subtree() |> Z.node() |> Sourceror.to_string() + end + end end