Skip to main content

src/greenwood.erl

-module(greenwood).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/greenwood.gleam").
-export([node/2, node_with_trivia/3, node_element/2, node_element_with_trivia/3, token/2, token_element/2, implicit_token/1, implicit_token_element/1, fold/3, fold_with_depth/3, each_with_depth/2, each/2, visitor/0, on_token/2, on_enter_node/2, on_exit_node/2, on_trivia/2, trailing_trivia/1, leading_trivia/1, traverse/3, map_children/2, map_tree_up/2, map_tree_down/2, find_child/2, filter_children/2, find_descendant/2, replace_child/3, replace_first/3, insert_before/3, insert_after/3, remove_children/2, append_child/2, prepend_child/2, set_leading_trivia/2, set_trailing_trivia/2, all_trivia/1, zip/1, down/1, down_where/2, left/1, left_where/2, right/1, right_where/2, up/1, up_n/2, left_n/2, left_n_where/3, right_n/2, right_n_where/3, set_focus/2, map_focus/2, unzip/1]).
-export_type([token/1, trivia/1, node_/1, element/1, zipper/1, crumb/1, traverse_action/1, visitor/2]).

-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.

?MODULEDOC(
    " Greenwood: a generic trivia-preserving concrete syntax tree\n"
    "\n"
    " Greenwood provides an immutable concrete syntax tree parameterized by node\n"
    " and token `kind`, with associated trivia and structural transformation\n"
    " primitives. Greenwood syntax trees are format-agnostic and parsers supply\n"
    " their own `kind` types.\n"
    "\n"
    " ### Function Groups\n"
    "\n"
    " There are six types of functions in the Greenwood interface:\n"
    "\n"
    " - `builder`: build a Greenwood tree\n"
    " - `query`: interrogate a Greenwood tree\n"
    " - `transformer`: transform a Greenwood tree\n"
    " - `traversal`: traverse a Greenwood tree\n"
    " - `trivia`: manage trivia on Greenwood tree node(s)\n"
    " - `cursor`: navigate and edit a Greenwood tree with a movable cursor,\n"
    "   provided by `greenwood/zipper`\n"
    "\n"
    " Each function and type in the Greenwood interface is marked with the\n"
    " appropriate interface group or groups.\n"
    "\n"
    " ### `Node`s and `Token`s\n"
    "\n"
    " Greenwood distinguishes between `Node` elements (which may carry child\n"
    " nodes or tokens) and `Token` elements (which carry text). Both may carry\n"
    " meaning, but `Token` elements are used to represent leaf elements and the\n"
    " text associated with them.\n"
    "\n"
    " ### Trivia\n"
    "\n"
    " In syntax tree terminology, \"trivia\" refers to source text that has no\n"
    " semantic meaning to the language but matters to humans: whitespace,\n"
    " comments, blank lines, and sometimes preprocessor directives. A pure AST\n"
    " discards trivia entirely, but a CST must track it.\n"
    "\n"
    " Greenwood implements a hybrid concrete syntax tree supporting\n"
    " [Roslyn][dn]-style Trivia annotations (attached trivia) or\n"
    " [Rowan][rust]-style green tree child tokens (inline trivia), depending on\n"
    " the choices made by the parser.\n"
    "\n"
    " - Attached trivia (Roslyn-style) is where each node carries leading and\n"
    "   trailing trivia tokens. A comment above a function \"belongs to\" that\n"
    "   function node. This makes it easy to move a node and have its comments\n"
    "   follow. Greenwood supports this with `Trivia(leading, trailing)`.\n"
    "\n"
    " - Inline trivia (Rowan-style) tokens are siblings in the children list,\n"
    "   with no special attachment. The tree is uniform but the parser (or\n"
    "   a later pass) must decide ownership when moving nodes. Greenwood supports\n"
    "   this with `Bare` trivia markers.\n"
    "\n"
    " ### Concrete and Abstract Syntax Trees\n"
    "\n"
    " A concrete syntax tree is a tree representation of the actual tokens for\n"
    " a language, whereas abstract syntax trees are simplified forms discarding\n"
    " anything not necessary for resolution of the language into meaning.\n"
    "\n"
    " As an example, consider what the abstract syntax tree for the following\n"
    " Gleam code might look like:\n"
    "\n"
    " ```gleam\n"
    " /// Tail-recursive Fibonacci sequence implementation\n"
    " pub fn fib(n: Int) -> Int {\n"
    "   case n {\n"
    "     0 | 1 -> n\n"
    "     _ -> fib(n - 1) + fib(n - 2)\n"
    "   }\n"
    " }\n"
    " ```\n"
    "\n"
    " This would produce an executable abstract syntax tree like:\n"
    "\n"
    " ```gleam\n"
    " Function(\n"
    "  name: \"fib\", publicity: Public,\n"
    "  parameters: [Parameter(name: \"n\", type: Int)],\n"
    "  return_type: Int,\n"
    "  body: Case(\n"
    "    subjects: [Variable(\"n\")],\n"
    "    clauses: [\n"
    "      Clause(patterns: [Int(0), Int(1)], body: Variable(\"n\")),\n"
    "      Clause(patterns: [Discard], body: BinOp(\n"
    "        Add,\n"
    "        Call(\"fib\", [BinOp(Sub, Variable(\"n\"), Int(1))]),\n"
    "        Call(\"fib\", [BinOp(Sub, Variable(\"n\"), Int(2))]),\n"
    "      )),\n"
    "    ],\n"
    "  ),\n"
    " )\n"
    " ```\n"
    "\n"
    " This could be transformed, but when rendered back to Gleam, the result\n"
    " would _drop_ the leading documentation comment. Comparatively, the concrete\n"
    " syntax tree is about the form of the text and _keeps_ the leading\n"
    " documentation comment. Every single run of contiguous whitespace is\n"
    " accounted for. Everything is assigned a meaning for preservation.\n"
    "\n"
    " ```gleam\n"
    " Node(\n"
    "   kind: Function,\n"
    "   trivia: Trivia(leading: [\n"
    "     Token(\n"
    "      DocComment,\n"
    "      \"/// Tail-recursive Fibonacci sequence implementation\",\n"
    "     ),\n"
    "     Token(Newline, \"\\n\"),\n"
    "   ]),\n"
    "   children: [\n"
    "     Token(Pub, \"pub\"),\n"
    "     Token(Whitespace, \" \"),\n"
    "     Token(Fn, \"fn\"),\n"
    "     Token(Whitespace, \" \"),\n"
    "     Token(Name, \"fib\"),\n"
    "     Token(LeftParen, \"(\"),\n"
    "     Token(Name, \"n\"),\n"
    "     Token(Colon, \":\"),\n"
    "     Token(Whitespace, \" \"),\n"
    "     Token(UpperName, \"Int\"),\n"
    "     Token(RightParen, \")\"),\n"
    "     Token(Whitespace, \" \"),\n"
    "     Token(Arrow, \"->\"),\n"
    "     Token(Whitespace, \" \"),\n"
    "     Token(UpperName, \"Int\"),\n"
    "     Token(Whitespace, \" \"),\n"
    "     Token(LeftBrace, \"{\"),\n"
    "     Token(Newline, \"\\n\"),\n"
    "     Token(Whitespace, \"  \"),\n"
    "     Node(Case, [\n"
    "       Token(Case, \"case\"),\n"
    "       Token(Whitespace, \" \"),\n"
    "       Token(Name, \"n\"),\n"
    "       Token(Whitespace, \" \"),\n"
    "       Token(LeftBrace, \"{\"),\n"
    "       Token(Newline, \"\\n\"),\n"
    "       ...every token including whitespace, pipes, arrows...\n"
    "     ], Bare),\n"
    "     Token(Newline, \"\\n\"),\n"
    "     Token(RightBrace, \"}\"),\n"
    "     Token(Newline, \"\\n\"),\n"
    "   ],\n"
    " )\n"
    " ```\n"
    "\n"
    " The differences are vast: the abstract syntax tree tells you what the code\n"
    " means, but the concrete syntax tree tells you what the source looks like.\n"
    " The source can be reconstructed byte-for-byte from the concrete syntax\n"
    " tree.\n"
    "\n"
    " This makes a concrete syntax tree useful for editors, language server\n"
    " implementations, and for edit-safe transformations.\n"
    "\n"
    " > For sufficiently distinct nodes, it is possible to design the syntax tree\n"
    " > to be somewhere between a concrete tree and an abstract tree. As an\n"
    " > example, `LeftBrace` nodes always have only the text `\"{\"`, so the\n"
    " > representation could be `Token(LeftBrace, \"\")`.\n"
    " >\n"
    " > Greenwood v2 will support this use case explicitly with a new `Token`\n"
    " > variant, `ImplicitToken`. The current version provides constructor\n"
    " > functions to assist with this migration: `implicit_token` and\n"
    " > `implicit_token_element`.\n"
    "\n"
    " [dn]: https://github.com/dotnet/roslyn/blob/main/docs/wiki/Roslyn-Overview.md#syntax-trivia\n"
    " [rust]: https://github.com/rust-analyzer/rowan\n"
).

-type token(DKT) :: {token, DKT, binary()}.

-type trivia(DKU) :: {trivia, list(token(DKU)), list(token(DKU))} | bare.

-type node_(DKV) :: {node, DKV, list(element(DKV)), trivia(DKV)}.

-type element(DKW) :: {node_element, node_(DKW)} | {token_element, token(DKW)}.

-type zipper(DKX) :: {zipper, node_(DKX), list(crumb(DKX))}.

-type crumb(DKY) :: {crumb,
        DKY,
        trivia(DKY),
        list(element(DKY)),
        list(element(DKY))}.

-type traverse_action(DKZ) :: {continue, DKZ} | {skip, DKZ} | {stop, DKZ}.

-type visitor(DLA, DLB) :: {visitor,
        gleam@option:option(fun((DLB, token(DLA)) -> traverse_action(DLB))),
        gleam@option:option(fun((DLB, node_(DLA)) -> traverse_action(DLB))),
        gleam@option:option(fun((DLB, node_(DLA)) -> traverse_action(DLB))),
        gleam@option:option(fun((DLB, token(DLA)) -> traverse_action(DLB)))}.

-file("src/greenwood.gleam", 319).
?DOC(
    " Create a node without Trivia.\n"
    "\n"
    " This is the same as `Node(kind:, children:, trivia: Bare)`.\n"
    "\n"
    " `builder`\n"
).
-spec node(DLC, list(element(DLC))) -> node_(DLC).
node(Kind, Children) ->
    {node, Kind, Children, bare}.

-file("src/greenwood.gleam", 331).
?DOC(
    " Create a node with Trivia.\n"
    "\n"
    " This is the same as `Node(kind:, children:, trivia:)`.\n"
    "\n"
    " `builder`\n"
).
-spec node_with_trivia(DLG, list(element(DLG)), trivia(DLG)) -> node_(DLG).
node_with_trivia(Kind, Children, Trivia) ->
    {node, Kind, Children, Trivia}.

-file("src/greenwood.gleam", 343).
?DOC(
    " Create a node element without Trivia. This is the same as\n"
    " `NodeElement(Node(kind:, children:, trivia: Bare))`.\n"
    "\n"
    " `builder`\n"
).
-spec node_element(DLL, list(element(DLL))) -> element(DLL).
node_element(Kind, Children) ->
    {node_element, {node, Kind, Children, bare}}.

-file("src/greenwood.gleam", 354).
?DOC(
    " Create a node element with Trivia. This is the same as\n"
    " `NodeElement(Node(kind:, children:, trivia:))`.\n"
    "\n"
    " `builder`\n"
).
-spec node_element_with_trivia(DLP, list(element(DLP)), trivia(DLP)) -> element(DLP).
node_element_with_trivia(Kind, Children, Trivia) ->
    {node_element, {node, Kind, Children, Trivia}}.

-file("src/greenwood.gleam", 365).
?DOC(
    " Create a token. This is the same as `Token(kind:, text:)`.\n"
    "\n"
    " `builder`\n"
).
-spec token(DLU, binary()) -> token(DLU).
token(Kind, Text) ->
    {token, Kind, Text}.

-file("src/greenwood.gleam", 373).
?DOC(
    " Create a token element. This is the same as `TokenElement(Token(kind:,\n"
    " text:))`.\n"
    "\n"
    " `builder`\n"
).
-spec token_element(DLW, binary()) -> element(DLW).
token_element(Kind, Text) ->
    {token_element, {token, Kind, Text}}.

-file("src/greenwood.gleam", 383).
?DOC(
    " Create an implicit token.\n"
    "\n"
    " In greenwood v1, this is the same as `Token(kind:, text: \"\")`. In greenwood\n"
    " v2, it will be `ImplicitToken(kind:)`.\n"
    "\n"
    " `builder`\n"
).
-spec implicit_token(DLY) -> token(DLY).
implicit_token(Kind) ->
    {token, Kind, <<""/utf8>>}.

-file("src/greenwood.gleam", 393).
?DOC(
    " Create an implicit token element.\n"
    "\n"
    " In greenwood v1, this is the same as `TokenElement(Token(kind:, text: \"\"))`.\n"
    " In greenwood v2, it will be `TokenElement(ImplicitToken(kind:))`.\n"
    "\n"
    " `builder`\n"
).
-spec implicit_token_element(DMA) -> element(DMA).
implicit_token_element(Kind) ->
    {token_element, {token, Kind, <<""/utf8>>}}.

-file("src/greenwood.gleam", 1239).
-spec do_fold(list(element(DYS)), DYV, fun((DYV, element(DYS)) -> DYV)) -> DYV.
do_fold(Children, Acc, F) ->
    gleam@list:fold(
        Children,
        Acc,
        fun(Acc@1, El) ->
            Acc@2 = F(Acc@1, El),
            case El of
                {node_element, Child} ->
                    do_fold(erlang:element(3, Child), Acc@2, F);

                {token_element, _} ->
                    Acc@2
            end
        end
    ).

-file("src/greenwood.gleam", 403).
?DOC(
    " Recursively fold over all elements depth-first.\n"
    "\n"
    " If you only need to fold over a node's immediate children, consider using\n"
    " `node.children |> list.fold(...)` directly.\n"
    "\n"
    " `traversal`\n"
).
-spec fold(node_(DMC), DME, fun((DME, element(DMC)) -> DME)) -> DME.
fold(Node, Acc, F) ->
    do_fold(erlang:element(3, Node), Acc, F).

-file("src/greenwood.gleam", 1253).
-spec do_fold_with_depth(
    list(element(DYX)),
    DZA,
    fun((DZA, element(DYX), integer()) -> DZA),
    integer()
) -> DZA.
do_fold_with_depth(Children, Acc, F, Depth) ->
    gleam@list:fold(
        Children,
        Acc,
        fun(Acc@1, El) ->
            Acc@2 = F(Acc@1, El, Depth),
            case El of
                {node_element, Child} ->
                    do_fold_with_depth(
                        erlang:element(3, Child),
                        Acc@2,
                        F,
                        Depth + 1
                    );

                {token_element, _} ->
                    Acc@2
            end
        end
    ).

-file("src/greenwood.gleam", 415).
?DOC(
    " Recursively fold over all elements depth-first, with depth. Depth `0` is the\n"
    " immediate children of the provided `node` (e.g., `node.children`).\n"
    "\n"
    " `traversal`\n"
).
-spec fold_with_depth(
    node_(DMG),
    DMI,
    fun((DMI, element(DMG), integer()) -> DMI)
) -> DMI.
fold_with_depth(Node, Acc, F) ->
    do_fold_with_depth(erlang:element(3, Node), Acc, F, 0).

-file("src/greenwood.gleam", 1269).
-spec do_each_with_depth(
    list(element(DZC)),
    fun((element(DZC), integer()) -> nil),
    integer()
) -> nil.
do_each_with_depth(Children, F, Depth) ->
    gleam@list:each(
        Children,
        fun(El) ->
            F(El, Depth),
            case El of
                {node_element, Child} ->
                    do_each_with_depth(erlang:element(3, Child), F, Depth + 1);

                {token_element, _} ->
                    nil
            end
        end
    ).

-file("src/greenwood.gleam", 426).
?DOC(
    " Recursively traverse all elements for side effects, with depth.\n"
    "\n"
    " `traversal`\n"
).
-spec each_with_depth(node_(DMK), fun((element(DMK), integer()) -> nil)) -> nil.
each_with_depth(Node, F) ->
    do_each_with_depth(erlang:element(3, Node), F, 0).

-file("src/greenwood.gleam", 1283).
-spec do_each(list(element(DZG)), fun((element(DZG)) -> nil)) -> nil.
do_each(Children, F) ->
    gleam@list:each(
        Children,
        fun(El) ->
            F(El),
            case El of
                {node_element, Child} ->
                    do_each(erlang:element(3, Child), F);

                {token_element, _} ->
                    nil
            end
        end
    ).

-file("src/greenwood.gleam", 436).
?DOC(
    " Recursively traverse all elements for side effects.\n"
    "\n"
    " `traversal`\n"
).
-spec each(node_(DMN), fun((element(DMN)) -> nil)) -> nil.
each(Node, F) ->
    do_each(erlang:element(3, Node), F).

-file("src/greenwood.gleam", 443).
?DOC(
    " Create a visitor with all callbacks set to `None` (no-op, always continues).\n"
    "\n"
    " `traversal`\n"
).
-spec visitor() -> visitor(any(), any()).
visitor() ->
    {visitor, none, none, none, none}.

-file("src/greenwood.gleam", 450).
?DOC(
    " Set the token callback on a visitor.\n"
    "\n"
    " `traversal`\n"
).
-spec on_token(
    visitor(DMU, DMV),
    fun((DMV, token(DMU)) -> traverse_action(DMV))
) -> visitor(DMU, DMV).
on_token(V, F) ->
    {visitor,
        {some, F},
        erlang:element(3, V),
        erlang:element(4, V),
        erlang:element(5, V)}.

-file("src/greenwood.gleam", 460).
?DOC(
    " Set the enter_node callback on a visitor.\n"
    "\n"
    " `traversal`\n"
).
-spec on_enter_node(
    visitor(DNC, DND),
    fun((DND, node_(DNC)) -> traverse_action(DND))
) -> visitor(DNC, DND).
on_enter_node(V, F) ->
    {visitor,
        erlang:element(2, V),
        {some, F},
        erlang:element(4, V),
        erlang:element(5, V)}.

-file("src/greenwood.gleam", 470).
?DOC(
    " Set the exit_node callback on a visitor.\n"
    "\n"
    " `traversal`\n"
).
-spec on_exit_node(
    visitor(DNK, DNL),
    fun((DNL, node_(DNK)) -> traverse_action(DNL))
) -> visitor(DNK, DNL).
on_exit_node(V, F) ->
    {visitor,
        erlang:element(2, V),
        erlang:element(3, V),
        {some, F},
        erlang:element(5, V)}.

-file("src/greenwood.gleam", 480).
?DOC(
    " Set the trivia callback on a visitor.\n"
    "\n"
    " `traversal`\n"
).
-spec on_trivia(
    visitor(DNS, DNT),
    fun((DNT, token(DNS)) -> traverse_action(DNT))
) -> visitor(DNS, DNT).
on_trivia(V, F) ->
    {visitor,
        erlang:element(2, V),
        erlang:element(3, V),
        erlang:element(4, V),
        {some, F}}.

-file("src/greenwood.gleam", 746).
?DOC(
    " Get trailing trivia tokens from a node.\n"
    "\n"
    " `trivia`\n"
).
-spec trailing_trivia(node_(DRH)) -> list(token(DRH)).
trailing_trivia(Node) ->
    case erlang:element(4, Node) of
        {trivia, _, Trailing} ->
            Trailing;

        bare ->
            []
    end.

-file("src/greenwood.gleam", 1382).
-spec do_traverse_trivia(
    list(token(EAM)),
    EAP,
    fun((EAP, token(EAM)) -> traverse_action(EAP))
) -> traverse_action(EAP).
do_traverse_trivia(Trivia, Acc, F) ->
    case Trivia of
        [] ->
            {continue, Acc};

        [Tok | Rest] ->
            case F(Acc, Tok) of
                {stop, _} = Stop ->
                    Stop;

                {continue, Acc@1} ->
                    do_traverse_trivia(Rest, Acc@1, F);

                {skip, Acc@1} ->
                    do_traverse_trivia(Rest, Acc@1, F)
            end
    end.

-file("src/greenwood.gleam", 1347).
-spec traverse_trivia(
    list(token(DZX)),
    visitor(DZX, EAA),
    EAA,
    fun((EAA) -> traverse_action(EAA))
) -> traverse_action(EAA).
traverse_trivia(Trivia, Visitor, Acc, Callback) ->
    Result = case erlang:element(5, Visitor) of
        none ->
            {continue, Acc};

        {some, F} ->
            do_traverse_trivia(Trivia, Acc, F)
    end,
    case Result of
        {stop, _} = Stop ->
            Stop;

        {continue, Acc@1} ->
            Callback(Acc@1);

        {skip, Acc@1} ->
            Callback(Acc@1)
    end.

-file("src/greenwood.gleam", 1364).
-spec enter_node(
    node_(EAF),
    visitor(EAF, EAH),
    EAH,
    fun((EAH) -> traverse_action(EAH))
) -> traverse_action(EAH).
enter_node(Node, Visitor, Acc, Callback) ->
    Result = case erlang:element(3, Visitor) of
        none ->
            {continue, Acc};

        {some, F} ->
            F(Acc, Node)
    end,
    case Result of
        {stop, _} = Stop ->
            Stop;

        {skip, Acc@1} ->
            {continue, Acc@1};

        {continue, Acc@2} ->
            Callback(Acc@2)
    end.

-file("src/greenwood.gleam", 736).
?DOC(
    " Get leading trivia tokens from a node.\n"
    "\n"
    " `trivia`\n"
).
-spec leading_trivia(node_(DRD)) -> list(token(DRD)).
leading_trivia(Node) ->
    case erlang:element(4, Node) of
        {trivia, Leading, _} ->
            Leading;

        bare ->
            []
    end.

-file("src/greenwood.gleam", 1321).
-spec do_traverse_children(list(element(DZQ)), DZT, visitor(DZQ, DZT)) -> traverse_action(DZT).
do_traverse_children(Children, Acc, Visitor) ->
    case Children of
        [] ->
            {continue, Acc};

        [El | Rest] ->
            Result = case El of
                {token_element, Tok} ->
                    case erlang:element(2, Visitor) of
                        none ->
                            {continue, Acc};

                        {some, F} ->
                            F(Acc, Tok)
                    end;

                {node_element, Child} ->
                    do_traverse_node(Child, Acc, Visitor)
            end,
            case Result of
                {stop, _} = Stop ->
                    Stop;

                {continue, Acc@1} ->
                    do_traverse_children(Rest, Acc@1, Visitor);

                {skip, Acc@1} ->
                    do_traverse_children(Rest, Acc@1, Visitor)
            end
    end.

-file("src/greenwood.gleam", 1293).
-spec do_traverse_node(node_(DZK), DZM, visitor(DZK, DZM)) -> traverse_action(DZM).
do_traverse_node(Node, Acc, Visitor) ->
    traverse_trivia(
        leading_trivia(Node),
        Visitor,
        Acc,
        fun(Acc@1) ->
            enter_node(
                Node,
                Visitor,
                Acc@1,
                fun(Acc@2) ->
                    case do_traverse_children(
                        erlang:element(3, Node),
                        Acc@2,
                        Visitor
                    ) of
                        {stop, _} = Stop ->
                            Stop;

                        {continue, Acc@3} ->
                            traverse_trivia(
                                trailing_trivia(Node),
                                Visitor,
                                Acc@3,
                                fun(Acc@4) -> case erlang:element(4, Visitor) of
                                        none ->
                                            {continue, Acc@4};

                                        {some, F} ->
                                            F(Acc@4, Node)
                                    end end
                            );

                        {skip, Acc@3} ->
                            traverse_trivia(
                                trailing_trivia(Node),
                                Visitor,
                                Acc@3,
                                fun(Acc@4) -> case erlang:element(4, Visitor) of
                                        none ->
                                            {continue, Acc@4};

                                        {some, F} ->
                                            F(Acc@4, Node)
                                    end end
                            )
                    end
                end
            )
        end
    ).

-file("src/greenwood.gleam", 503).
?DOC(
    " Traverse a tree with a visitor, folding an accumulator through callbacks.\n"
    "\n"
    " Visits the full tree including the root node in the following traversal\n"
    " order:\n"
    "\n"
    " 1. Leading trivia (if `trivia` callback set)\n"
    " 2. `enter_node`\n"
    " 3. Children (tokens invoke `token`; nodes recurse)\n"
    " 4. Trailing trivia (if `trivia` callback set)\n"
    " 5. `exit_node`\n"
    "\n"
    " `Stop` from any callback halts immediately. `Skip` from `enter_node` skips\n"
    " children and exit for that node. In all other cases `Skip` behaves like\n"
    " `Continue`.\n"
    "\n"
    " `traversal`\n"
).
-spec traverse(node_(DOA), DOC, visitor(DOA, DOC)) -> DOC.
traverse(Node, Acc, Visitor) ->
    case do_traverse_node(Node, Acc, Visitor) of
        {continue, A} ->
            A;

        {skip, A@1} ->
            A@1;

        {stop, A@2} ->
            A@2
    end.

-file("src/greenwood.gleam", 518).
?DOC(
    " Map over immediate children of a node.\n"
    "\n"
    " `transformer`\n"
).
-spec map_children(node_(DOF), fun((element(DOF)) -> element(DOF))) -> node_(DOF).
map_children(Node, F) ->
    {node,
        erlang:element(2, Node),
        gleam@list:map(erlang:element(3, Node), F),
        erlang:element(4, Node)}.

-file("src/greenwood.gleam", 537).
?DOC(
    " Recursively map all elements leaf nodes first. Child nodes are processed\n"
    " before sibling nodes.\n"
    "\n"
    " ```\n"
    "       A        ← processed last\n"
    "      / \\\n"
    "     B   C      ← processed second\n"
    "    / \\\n"
    "   D   E        ← processed first\n"
    " ```\n"
    "\n"
    " `transformer`\n"
).
-spec map_tree_up(node_(DOK), fun((element(DOK)) -> element(DOK))) -> node_(DOK).
map_tree_up(Node, F) ->
    Children = gleam@list:map(erlang:element(3, Node), fun(El) -> case El of
                {node_element, Child} ->
                    F({node_element, map_tree_up(Child, F)});

                {token_element, _} ->
                    F(El)
            end end),
    {node, erlang:element(2, Node), Children, erlang:element(4, Node)}.

-file("src/greenwood.gleam", 567).
?DOC(
    " Recursively map all elements root node first. Child nodes are processed\n"
    " before sibling nodes.\n"
    "\n"
    " Unlike `map_tree_up`, this runs the mapping function over the root node\n"
    " first and recurses into the _result_ of the mapping function. It is\n"
    " therefore possible to affect the next iteration with the mapping function.\n"
    "\n"
    " ```\n"
    "       A        ← processed first\n"
    "      / \\\n"
    "     B′  C′     ← processed second\n"
    "    / \\\n"
    "   D″  E″       ← processed last\n"
    " ```\n"
    "\n"
    " `transformer`\n"
).
-spec map_tree_down(node_(DOP), fun((element(DOP)) -> element(DOP))) -> node_(DOP).
map_tree_down(Node, F) ->
    Children = gleam@list:map(
        erlang:element(3, Node),
        fun(El) ->
            Mapped = F(El),
            case Mapped of
                {node_element, Child} ->
                    {node_element, map_tree_down(Child, F)};

                {token_element, _} ->
                    Mapped
            end
        end
    ),
    {node, erlang:element(2, Node), Children, erlang:element(4, Node)}.

-file("src/greenwood.gleam", 585).
?DOC(
    " Find the first immediate child element matching a predicate.\n"
    "\n"
    " `query`\n"
).
-spec find_child(node_(DOU), fun((element(DOU)) -> boolean())) -> gleam@option:option(element(DOU)).
find_child(Node, Predicate) ->
    _pipe = gleam@list:find(erlang:element(3, Node), Predicate),
    gleam@option:from_result(_pipe).

-file("src/greenwood.gleam", 595).
?DOC(
    " Find all immediate child elements matching a predicate.\n"
    "\n"
    " `query`\n"
).
-spec filter_children(node_(DOZ), fun((element(DOZ)) -> boolean())) -> list(element(DOZ)).
filter_children(Node, Predicate) ->
    gleam@list:filter(erlang:element(3, Node), Predicate).

-file("src/greenwood.gleam", 961).
-spec do_find_descendant(list(element(DUB)), fun((element(DUB)) -> boolean())) -> gleam@option:option(element(DUB)).
do_find_descendant(Elements, Predicate) ->
    case Elements of
        [] ->
            none;

        [El | Rest] ->
            gleam@bool:guard(Predicate(El), {some, El}, fun() -> case El of
                        {node_element, Child} ->
                            _pipe = do_find_descendant(
                                erlang:element(3, Child),
                                Predicate
                            ),
                            gleam@option:lazy_or(
                                _pipe,
                                fun() -> do_find_descendant(Rest, Predicate) end
                            );

                        {token_element, _} ->
                            do_find_descendant(Rest, Predicate)
                    end end)
    end.

-file("src/greenwood.gleam", 606).
?DOC(
    " Find the first descendant matching a predicate. Searches from root to leaves\n"
    " returning the shallowest match.\n"
    "\n"
    " `query`\n"
).
-spec find_descendant(node_(DPE), fun((element(DPE)) -> boolean())) -> gleam@option:option(element(DPE)).
find_descendant(Node, Predicate) ->
    do_find_descendant(erlang:element(3, Node), Predicate).

-file("src/greenwood.gleam", 616).
?DOC(
    " Replace a child at a given index.\n"
    "\n"
    " `transformer`\n"
).
-spec replace_child(node_(DPJ), integer(), element(DPJ)) -> node_(DPJ).
replace_child(Node, Index, Element) ->
    Children = gleam@list:index_map(
        erlang:element(3, Node),
        fun(El, I) ->
            gleam@bool:guard(I =:= Index, Element, fun() -> El end)
        end
    ),
    {node, erlang:element(2, Node), Children, erlang:element(4, Node)}.

-file("src/greenwood.gleam", 980).
-spec do_replace_first(
    list(element(DUH)),
    fun((element(DUH)) -> boolean()),
    fun((element(DUH)) -> element(DUH)),
    list(element(DUH))
) -> list(element(DUH)).
do_replace_first(Elements, Predicate, Replacement, Acc) ->
    case Elements of
        [] ->
            lists:reverse(Acc);

        [El | Rest] ->
            gleam@bool:guard(
                Predicate(El),
                lists:append(lists:reverse(Acc), [Replacement(El) | Rest]),
                fun() ->
                    do_replace_first(Rest, Predicate, Replacement, [El | Acc])
                end
            )
    end.

-file("src/greenwood.gleam", 632).
?DOC(
    " Replace the first child matching a predicate.\n"
    "\n"
    " `transformer`\n"
).
-spec replace_first(
    node_(DPN),
    fun((element(DPN)) -> boolean()),
    fun((element(DPN)) -> element(DPN))
) -> node_(DPN).
replace_first(Node, Predicate, Replacement) ->
    Children = do_replace_first(
        erlang:element(3, Node),
        Predicate,
        Replacement,
        []
    ),
    {node, erlang:element(2, Node), Children, erlang:element(4, Node)}.

-file("src/greenwood.gleam", 998).
-spec do_insert_before(
    list(element(DUR)),
    fun((element(DUR)) -> boolean()),
    element(DUR),
    list(element(DUR))
) -> list(element(DUR)).
do_insert_before(Elements, Predicate, Element, Acc) ->
    case Elements of
        [] ->
            lists:reverse(Acc);

        [El | Rest] ->
            gleam@bool:guard(
                Predicate(El),
                lists:append(lists:reverse(Acc), [Element, El | Rest]),
                fun() ->
                    do_insert_before(Rest, Predicate, Element, [El | Acc])
                end
            )
    end.

-file("src/greenwood.gleam", 644).
?DOC(
    " Insert an element before the first child matching a predicate.\n"
    "\n"
    " `transformer`\n"
).
-spec insert_before(node_(DPT), fun((element(DPT)) -> boolean()), element(DPT)) -> node_(DPT).
insert_before(Node, Predicate, Element) ->
    Children = do_insert_before(erlang:element(3, Node), Predicate, Element, []),
    {node, erlang:element(2, Node), Children, erlang:element(4, Node)}.

-file("src/greenwood.gleam", 1016).
-spec do_insert_after(
    list(element(DVA)),
    fun((element(DVA)) -> boolean()),
    element(DVA),
    list(element(DVA))
) -> list(element(DVA)).
do_insert_after(Elements, Predicate, Element, Acc) ->
    case Elements of
        [] ->
            lists:reverse(Acc);

        [El | Rest] ->
            gleam@bool:guard(
                Predicate(El),
                lists:append(lists:reverse(Acc), [El, Element | Rest]),
                fun() ->
                    do_insert_after(Rest, Predicate, Element, [El | Acc])
                end
            )
    end.

-file("src/greenwood.gleam", 656).
?DOC(
    " Insert an element after the first child matching a predicate.\n"
    "\n"
    " `transformer`\n"
).
-spec insert_after(node_(DPY), fun((element(DPY)) -> boolean()), element(DPY)) -> node_(DPY).
insert_after(Node, Predicate, Element) ->
    Children = do_insert_after(erlang:element(3, Node), Predicate, Element, []),
    {node, erlang:element(2, Node), Children, erlang:element(4, Node)}.

-file("src/greenwood.gleam", 668).
?DOC(
    " Remove all children matching a predicate.\n"
    "\n"
    " `transformer`\n"
).
-spec remove_children(node_(DQD), fun((element(DQD)) -> boolean())) -> node_(DQD).
remove_children(Node, Predicate) ->
    {node,
        erlang:element(2, Node),
        gleam@list:filter(
            erlang:element(3, Node),
            fun(El) -> not Predicate(El) end
        ),
        erlang:element(4, Node)}.

-file("src/greenwood.gleam", 678).
?DOC(
    " Append a child to the end of a node's children.\n"
    "\n"
    " `transformer`\n"
).
-spec append_child(node_(DQH), element(DQH)) -> node_(DQH).
append_child(Node, Element) ->
    {node,
        erlang:element(2, Node),
        lists:append(erlang:element(3, Node), [Element]),
        erlang:element(4, Node)}.

-file("src/greenwood.gleam", 688).
?DOC(
    " Prepend a child to the beginning of a node's children.\n"
    "\n"
    " `transformer`\n"
).
-spec prepend_child(node_(DQL), element(DQL)) -> node_(DQL).
prepend_child(Node, Element) ->
    {node,
        erlang:element(2, Node),
        [Element | erlang:element(3, Node)],
        erlang:element(4, Node)}.

-file("src/greenwood.gleam", 698).
?DOC(
    " Attach leading trivia to a node.\n"
    "\n"
    " `trivia`\n"
).
-spec set_leading_trivia(node_(DQP), list(token(DQP))) -> node_(DQP).
set_leading_trivia(Node, Tokens) ->
    New_trivia = case erlang:element(4, Node) of
        {trivia, _, T} ->
            {trivia, Tokens, T};

        bare ->
            {trivia, Tokens, []}
    end,
    {node, erlang:element(2, Node), erlang:element(3, Node), New_trivia}.

-file("src/greenwood.gleam", 712).
?DOC(
    " Attach trailing trivia to a node.\n"
    "\n"
    " `trivia`\n"
).
-spec set_trailing_trivia(node_(DQU), list(token(DQU))) -> node_(DQU).
set_trailing_trivia(Node, Tokens) ->
    New_trivia = case erlang:element(4, Node) of
        {trivia, L, _} ->
            {trivia, L, Tokens};

        bare ->
            {trivia, [], Tokens}
    end,
    {node, erlang:element(2, Node), erlang:element(3, Node), New_trivia}.

-file("src/greenwood.gleam", 726).
?DOC(
    " Get all trivia tokens (leading then trailing) from a node.\n"
    "\n"
    " `trivia`\n"
).
-spec all_trivia(node_(DQZ)) -> list(token(DQZ)).
all_trivia(Node) ->
    case erlang:element(4, Node) of
        {trivia, Leading, Trailing} ->
            lists:append(Leading, Trailing);

        bare ->
            []
    end.

-file("src/greenwood.gleam", 758).
?DOC(
    " Create a zipper focused on the root node.\n"
    "\n"
    " `cursor`\n"
    "\n"
    " _Deprecated_: Use `greenwood/zipper.zip` instead.\n"
).
-spec zip(node_(DRL)) -> zipper(DRL).
zip(Root) ->
    {zipper, Root, []}.

-file("src/greenwood.gleam", 1170).
?DOC(
    " `left` is accumulated nearest-first (reversed from source order) so that\n"
    " `left`/`right` cursor moves are O(1) and `up` can rebuild children with a\n"
    " single `list.reverse`.\n"
).
-spec split_at_node(
    list(element(DXB)),
    fun((node_(DXB)) -> boolean()),
    list(element(DXB))
) -> gleam@option:option({list(element(DXB)), node_(DXB), list(element(DXB))}).
split_at_node(Elements, Predicate, Left) ->
    case Elements of
        [] ->
            none;

        [{node_element, N} | Rest] ->
            gleam@bool:guard(
                Predicate(N),
                {some, {Left, N, Rest}},
                fun() ->
                    split_at_node(Rest, Predicate, [{node_element, N} | Left])
                end
            );

        [Other | Rest@1] ->
            split_at_node(Rest@1, Predicate, [Other | Left])
    end.

-file("src/greenwood.gleam", 1152).
-spec do_down(
    node_(DWS),
    fun((node_(DWS)) -> boolean()),
    list(crumb(DWS)),
    list(element(DWS))
) -> gleam@option:option(zipper(DWS)).
do_down(Parent, Predicate, Crumbs, Left) ->
    case split_at_node(erlang:element(3, Parent), Predicate, Left) of
        {some, {Left@1, Child, Right}} ->
            Crumb = {crumb,
                erlang:element(2, Parent),
                erlang:element(4, Parent),
                Left@1,
                Right},
            {some, {zipper, Child, [Crumb | Crumbs]}};

        none ->
            none
    end.

-file("src/greenwood.gleam", 767).
?DOC(
    " Move focus to the first child that is a Node.\n"
    "\n"
    " `cursor`\n"
    "\n"
    " _Deprecated_: Use `greenwood/zipper.down` instead.\n"
).
-spec down(zipper(DRO)) -> gleam@option:option(zipper(DRO)).
down(Zipper) ->
    do_down(
        erlang:element(2, Zipper),
        fun(_) -> true end,
        erlang:element(3, Zipper),
        []
    ).

-file("src/greenwood.gleam", 776).
?DOC(
    " Move focus to the first child Node matching a predicate.\n"
    "\n"
    " `cursor`\n"
    "\n"
    " _Deprecated_: Use `greenwood/zipper.down_where` instead.\n"
).
-spec down_where(zipper(DRS), fun((node_(DRS)) -> boolean())) -> gleam@option:option(zipper(DRS)).
down_where(Zipper, Predicate) ->
    do_down(erlang:element(2, Zipper), Predicate, erlang:element(3, Zipper), []).

-file("src/greenwood.gleam", 1189).
?DOC(
    " Walk `elements` (the crumb's nearest-first `left` list) looking for the\n"
    " first matching Node. Elements skipped along the way are prepended to\n"
    " `right` so they end up positioned between the new focus and the prior\n"
    " focus in source order.\n"
).
-spec scan_left(
    list(element(DXN)),
    fun((node_(DXN)) -> boolean()),
    list(element(DXN))
) -> gleam@option:option({list(element(DXN)), node_(DXN), list(element(DXN))}).
scan_left(Elements, Predicate, Right) ->
    case Elements of
        [] ->
            none;

        [{node_element, N} | Rest] ->
            gleam@bool:guard(
                Predicate(N),
                {some, {Rest, N, Right}},
                fun() ->
                    scan_left(Rest, Predicate, [{node_element, N} | Right])
                end
            );

        [Other | Rest@1] ->
            scan_left(Rest@1, Predicate, [Other | Right])
    end.

-file("src/greenwood.gleam", 1092).
-spec do_left_where(zipper(DWE), fun((node_(DWE)) -> boolean())) -> gleam@option:option(zipper(DWE)).
do_left_where(Zipper, Predicate) ->
    case erlang:element(3, Zipper) of
        [] ->
            none;

        [Crumb | Rest] ->
            case scan_left(
                erlang:element(4, Crumb),
                Predicate,
                [{node_element, erlang:element(2, Zipper)} |
                    erlang:element(5, Crumb)]
            ) of
                {some, {New_left, New_focus, New_right}} ->
                    New_crumb = {crumb,
                        erlang:element(2, Crumb),
                        erlang:element(3, Crumb),
                        New_left,
                        New_right},
                    {some, {zipper, New_focus, [New_crumb | Rest]}};

                none ->
                    none
            end
    end.

-file("src/greenwood.gleam", 1088).
-spec do_left(zipper(DWA)) -> gleam@option:option(zipper(DWA)).
do_left(Zipper) ->
    do_left_where(Zipper, fun(_) -> true end).

-file("src/greenwood.gleam", 790).
?DOC(
    " Move focus to the nearest sibling Node to the left of the focus.\n"
    "\n"
    " Returns `None` if the focus is the root or has no Node sibling to the left.\n"
    "\n"
    " `cursor`\n"
    "\n"
    " _Deprecated_: Use `greenwood/zipper.left` instead.\n"
).
-spec left(zipper(DRX)) -> gleam@option:option(zipper(DRX)).
left(Zipper) ->
    do_left(Zipper).

-file("src/greenwood.gleam", 802).
?DOC(
    " Move focus to the nearest sibling Node to the left matching a predicate.\n"
    "\n"
    " Token siblings (whitespace, comments stored inline) are skipped and remain\n"
    " in place between the old and new focus.\n"
    "\n"
    " `cursor`\n"
    "\n"
    " _Deprecated_: Use `greenwood/zipper.left_where` instead.\n"
).
-spec left_where(zipper(DSB), fun((node_(DSB)) -> boolean())) -> gleam@option:option(zipper(DSB)).
left_where(Zipper, Predicate) ->
    do_left_where(Zipper, Predicate).

-file("src/greenwood.gleam", 1208).
?DOC(
    " Walk `elements` (the crumb's source-order `right` list) looking for the\n"
    " first matching Node. Elements skipped along the way are prepended to\n"
    " `left` (which is maintained nearest-first) so they end up positioned\n"
    " between the prior focus and the new focus in source order.\n"
).
-spec scan_right(
    list(element(DXZ)),
    fun((node_(DXZ)) -> boolean()),
    list(element(DXZ))
) -> gleam@option:option({list(element(DXZ)), node_(DXZ), list(element(DXZ))}).
scan_right(Elements, Predicate, Left) ->
    case Elements of
        [] ->
            none;

        [{node_element, N} | Rest] ->
            gleam@bool:guard(
                Predicate(N),
                {some, {Left, N, Rest}},
                fun() ->
                    scan_right(Rest, Predicate, [{node_element, N} | Left])
                end
            );

        [Other | Rest@1] ->
            scan_right(Rest@1, Predicate, [Other | Left])
    end.

-file("src/greenwood.gleam", 1124).
-spec do_right_where(zipper(DWN), fun((node_(DWN)) -> boolean())) -> gleam@option:option(zipper(DWN)).
do_right_where(Zipper, Predicate) ->
    case erlang:element(3, Zipper) of
        [] ->
            none;

        [Crumb | Rest] ->
            case scan_right(
                erlang:element(5, Crumb),
                Predicate,
                [{node_element, erlang:element(2, Zipper)} |
                    erlang:element(4, Crumb)]
            ) of
                {some, {New_left, New_focus, New_right}} ->
                    New_crumb = {crumb,
                        erlang:element(2, Crumb),
                        erlang:element(3, Crumb),
                        New_left,
                        New_right},
                    {some, {zipper, New_focus, [New_crumb | Rest]}};

                none ->
                    none
            end
    end.

-file("src/greenwood.gleam", 1120).
-spec do_right(zipper(DWJ)) -> gleam@option:option(zipper(DWJ)).
do_right(Zipper) ->
    do_right_where(Zipper, fun(_) -> true end).

-file("src/greenwood.gleam", 816).
?DOC(
    " Move focus to the nearest sibling Node to the right of the focus.\n"
    "\n"
    " Returns `None` if the focus is the root or has no Node sibling to the right.\n"
    "\n"
    " `cursor`\n"
    "\n"
    " _Deprecated_: Use `greenwood/zipper.right` instead.\n"
).
-spec right(zipper(DSG)) -> gleam@option:option(zipper(DSG)).
right(Zipper) ->
    do_right(Zipper).

-file("src/greenwood.gleam", 828).
?DOC(
    " Move focus to the nearest sibling Node to the right matching a predicate.\n"
    "\n"
    " Token siblings (whitespace, comments stored inline) are skipped and remain\n"
    " in place between the old and new focus.\n"
    "\n"
    " `cursor`\n"
    "\n"
    " _Deprecated_: Use `greenwood/zipper.right_where` instead.\n"
).
-spec right_where(zipper(DSK), fun((node_(DSK)) -> boolean())) -> gleam@option:option(zipper(DSK)).
right_where(Zipper, Predicate) ->
    do_right_where(Zipper, Predicate).

-file("src/greenwood.gleam", 1073).
-spec do_up(zipper(DVW)) -> gleam@option:option(zipper(DVW)).
do_up(Zipper) ->
    case erlang:element(3, Zipper) of
        [] ->
            none;

        [Crumb | Rest] ->
            Children = lists:append(
                lists:reverse(erlang:element(4, Crumb)),
                [{node_element, erlang:element(2, Zipper)} |
                    erlang:element(5, Crumb)]
            ),
            Parent = {node,
                erlang:element(2, Crumb),
                Children,
                erlang:element(3, Crumb)},
            {some, {zipper, Parent, Rest}}
    end.

-file("src/greenwood.gleam", 840).
?DOC(
    " Move focus back up to the parent.\n"
    "\n"
    " `cursor`\n"
    "\n"
    " _Deprecated_: Use `greenwood/zipper.up` instead.\n"
).
-spec up(zipper(DSP)) -> gleam@option:option(zipper(DSP)).
up(Zipper) ->
    do_up(Zipper).

-file("src/greenwood.gleam", 1223).
-spec repeat_move(
    zipper(DYL),
    integer(),
    fun((zipper(DYL)) -> gleam@option:option(zipper(DYL)))
) -> gleam@option:option(zipper(DYL)).
repeat_move(Zipper, N, Move) ->
    case N of
        _ when N < 0 ->
            none;

        0 ->
            {some, Zipper};

        _ ->
            case Move(Zipper) of
                {some, Z} ->
                    repeat_move(Z, N - 1, Move);

                none ->
                    none
            end
    end.

-file("src/greenwood.gleam", 852).
?DOC(
    " Move focus up `n` parents. Strict: returns `None` if `n` is negative or if\n"
    " the cursor would move above the root.\n"
    "\n"
    " `up_n(z, by: 0)` returns `Some(z)`.\n"
    "\n"
    " `cursor`\n"
    "\n"
    " _Deprecated_: Use `greenwood/zipper.up_n` instead.\n"
).
-spec up_n(zipper(DST), integer()) -> gleam@option:option(zipper(DST)).
up_n(Zipper, N) ->
    repeat_move(Zipper, N, fun do_up/1).

-file("src/greenwood.gleam", 865).
?DOC(
    " Move focus `n` sibling Nodes to the left. Strict: returns `None` if the\n"
    " move cannot be completed in full.\n"
    "\n"
    " `left_n(z, by: 0)` returns `Some(z)`. Negative `n` flips direction:\n"
    " `left_n(z, by: -n)` is equivalent to `right_n(z, by: n)`.\n"
    "\n"
    " `cursor`\n"
    "\n"
    " _Deprecated_: Use `greenwood/zipper.left_n` instead.\n"
).
-spec left_n(zipper(DSX), integer()) -> gleam@option:option(zipper(DSX)).
left_n(Zipper, N) ->
    gleam@bool:guard(
        N < 0,
        repeat_move(Zipper, - N, fun do_right/1),
        fun() -> repeat_move(Zipper, N, fun do_left/1) end
    ).

-file("src/greenwood.gleam", 1057).
-spec do_right_n_where(zipper(DVR), integer(), fun((node_(DVR)) -> boolean())) -> gleam@option:option(zipper(DVR)).
do_right_n_where(Zipper, N, Predicate) ->
    case N of
        0 ->
            {some, Zipper};

        _ when N < 0 ->
            do_left_n_where(Zipper, - N, Predicate);

        _ ->
            case do_right_where(Zipper, Predicate) of
                {some, Z} ->
                    do_right_n_where(Z, N - 1, Predicate);

                none ->
                    none
            end
    end.

-file("src/greenwood.gleam", 1041).
-spec do_left_n_where(zipper(DVM), integer(), fun((node_(DVM)) -> boolean())) -> gleam@option:option(zipper(DVM)).
do_left_n_where(Zipper, N, Predicate) ->
    case N of
        0 ->
            {some, Zipper};

        _ when N < 0 ->
            do_right_n_where(Zipper, - N, Predicate);

        _ ->
            case do_left_where(Zipper, Predicate) of
                {some, Z} ->
                    do_left_n_where(Z, N - 1, Predicate);

                none ->
                    none
            end
    end.

-file("src/greenwood.gleam", 884).
?DOC(
    " Move focus `n` sibling Nodes to the left where those nodes match\n"
    " a predicate. Strict: returns `None` if the move cannot be completed in full.\n"
    "\n"
    " Token siblings (whitespace, comments stored inline) are skipped and remain\n"
    " in place between the old and new focus.\n"
    "\n"
    " `left_n_where(zipper:, by: 0, predicate:)` returns `Some(zipper)`. Negative\n"
    " `n` flips direction: `left_n_where(zipper:, by: -n, predicate:)` is\n"
    " equivalent to `right_n_where(zipper:, by: n, predicate:)`\n"
    "\n"
    " `cursor`\n"
    "\n"
    " _Deprecated_: Use `greenwood/zipper.left_n_where` instead.\n"
).
-spec left_n_where(zipper(DTB), integer(), fun((node_(DTB)) -> boolean())) -> gleam@option:option(zipper(DTB)).
left_n_where(Zipper, N, Predicate) ->
    do_left_n_where(Zipper, N, Predicate).

-file("src/greenwood.gleam", 901).
?DOC(
    " Move focus `n` sibling Nodes to the right. Strict: returns `None` if the\n"
    " move cannot be completed in full.\n"
    "\n"
    " `right_n(z, by: 0)` returns `Some(z)`. Negative `n` flips direction:\n"
    " `right_n(z, by: -n)` is equivalent to `left_n(z, by: n)`.\n"
    "\n"
    " `cursor`\n"
    "\n"
    " _Deprecated_: Use `greenwood/zipper.right_n` instead.\n"
).
-spec right_n(zipper(DTG), integer()) -> gleam@option:option(zipper(DTG)).
right_n(Zipper, N) ->
    gleam@bool:guard(
        N < 0,
        repeat_move(Zipper, - N, fun do_left/1),
        fun() -> repeat_move(Zipper, N, fun do_right/1) end
    ).

-file("src/greenwood.gleam", 920).
?DOC(
    " Move focus `n` sibling Nodes to the right where those nodes match\n"
    " a predicate. Strict: returns `None` if the move cannot be completed in full.\n"
    "\n"
    " Token siblings (whitespace, comments stored inline) are skipped and remain\n"
    " in place between the old and new focus.\n"
    "\n"
    " `right_n_where(zipper:, by: 0, predicate:)` returns `Some(zipper)`. Negative\n"
    " `n` flips direction: `right_n_where(zipper:, by: -n, predicate:)` is\n"
    " equivalent to `left_n_where(zipper:, by: n, predicate:)`\n"
    "\n"
    " `cursor`\n"
    "\n"
    " _Deprecated_: Use `greenwood/zipper.right_n_where` instead.\n"
).
-spec right_n_where(zipper(DTK), integer(), fun((node_(DTK)) -> boolean())) -> gleam@option:option(zipper(DTK)).
right_n_where(Zipper, N, Predicate) ->
    do_right_n_where(Zipper, N, Predicate).

-file("src/greenwood.gleam", 933).
?DOC(
    " Replace the focused node and return the updated zipper.\n"
    "\n"
    " `cursor`\n"
    "\n"
    " _Deprecated_: Use `greenwood/zipper.set_focus` instead.\n"
).
-spec set_focus(zipper(DTP), node_(DTP)) -> zipper(DTP).
set_focus(Zipper, Node) ->
    {zipper, Node, erlang:element(3, Zipper)}.

-file("src/greenwood.gleam", 945).
?DOC(
    " Apply a transform to the focused node.\n"
    "\n"
    " `cursor`\n"
    "\n"
    " _Deprecated_: Use `greenwood/zipper.map_focus` instead.\n"
).
-spec map_focus(zipper(DTT), fun((node_(DTT)) -> node_(DTT))) -> zipper(DTT).
map_focus(Zipper, F) ->
    {zipper, F(erlang:element(2, Zipper)), erlang:element(3, Zipper)}.

-file("src/greenwood.gleam", 1034).
-spec do_unzip(zipper(DVJ)) -> node_(DVJ).
do_unzip(Zipper) ->
    case do_up(Zipper) of
        {some, Parent_zipper} ->
            do_unzip(Parent_zipper);

        none ->
            erlang:element(2, Zipper)
    end.

-file("src/greenwood.gleam", 957).
?DOC(
    " Reconstruct the full tree from a zipper by moving up to the root.\n"
    "\n"
    " `cursor`\n"
    "\n"
    " _Deprecated_: Use `greenwood/zipper.unzip` instead.\n"
).
-spec unzip(zipper(DTY)) -> node_(DTY).
unzip(Zipper) ->
    do_unzip(Zipper).