Skip to content

Commit

Permalink
improvement: add move_to_cursor/2 (#147)
Browse files Browse the repository at this point in the history
* improvement: add move_to_cursor/2

* use `__cursor__` and `...`

* use `__` instead of `...` and use `skip` to skip subtrees.
  • Loading branch information
zachdaniel committed Jun 25, 2024
1 parent 5dd072f commit b353d74
Show file tree
Hide file tree
Showing 2 changed files with 177 additions and 0 deletions.
99 changes: 99 additions & 0 deletions lib/sourceror/zipper.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
78 changes: 78 additions & 0 deletions test/zipper_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit b353d74

Please sign in to comment.