defmodule Localize.Mf2.TreeSitter.Node do @moduledoc """ A single node in a tree-sitter MF2 parse tree. Nodes are value structs that hold the opaque NIF node handle plus the tree's source binary, so that span-addressing (`text/1`) is a pure Elixir operation and does not round-trip through the NIF. Child traversal (`children/1`, `named_children/1`) is implemented in Elixir on top of indexed NIF accessors. Each accessor call is O(1). """ alias Localize.Mf2.TreeSitter.Nif @enforce_keys [:handle, :source] defstruct [:handle, :source] @typedoc """ A tree-sitter node. Opaque — the `:handle` field is a NIF resource handle whose internal representation is unspecified. Always interact with a node via the functions on this module rather than pattern-matching on its fields. """ @opaque t :: %__MODULE__{handle: Nif.node_handle(), source: binary()} @type point :: {row :: non_neg_integer(), column :: non_neg_integer()} @type range :: {point(), point()} @doc "Return the node's grammar symbol name (e.g. `\"variable\"`)." @spec type(t()) :: binary() def type(%__MODULE__{handle: h}), do: Nif.node_type(h) @doc "Return the node's byte offset in the source." @spec start_byte(t()) :: non_neg_integer() def start_byte(%__MODULE__{handle: h}), do: Nif.node_start_byte(h) @doc "Return the byte offset just past the node's last byte." @spec end_byte(t()) :: non_neg_integer() def end_byte(%__MODULE__{handle: h}), do: Nif.node_end_byte(h) @doc "Return the node's `{row, column}` start position." @spec start_point(t()) :: point() def start_point(%__MODULE__{handle: h}), do: Nif.node_start_point(h) @doc "Return the node's `{row, column}` end position." @spec end_point(t()) :: point() def end_point(%__MODULE__{handle: h}), do: Nif.node_end_point(h) @doc "Return `{start_point, end_point}` for the node." @spec range(t()) :: range() def range(%__MODULE__{} = node), do: {start_point(node), end_point(node)} @doc "Whether the node is a named grammar node (not anonymous punctuation)." @spec named?(t()) :: boolean() def named?(%__MODULE__{handle: h}), do: Nif.node_is_named(h) @doc """ Whether the node or any descendant is an `ERROR` / `MISSING` node. This is the LSP-relevant predicate: after parsing partially invalid input, tree-sitter's GLR recovery produces a tree with `ERROR` / `MISSING` nodes at the failure points rather than aborting. Use this to decide whether to surface diagnostics. """ @spec has_error?(t()) :: boolean() def has_error?(%__MODULE__{handle: h}), do: Nif.node_has_error(h) @doc "Return the total number of children (named and anonymous)." @spec child_count(t()) :: non_neg_integer() def child_count(%__MODULE__{handle: h}), do: Nif.node_child_count(h) @doc "Return the number of named children." @spec named_child_count(t()) :: non_neg_integer() def named_child_count(%__MODULE__{handle: h}), do: Nif.node_named_child_count(h) @doc "Return the child at `index`, or `nil` if the index is out of range." @spec child(t(), non_neg_integer()) :: t() | nil def child(%__MODULE__{handle: h, source: src}, index) do case Nif.node_child(h, index) do nil -> nil child_handle -> %__MODULE__{handle: child_handle, source: src} end end @doc "Return the named child at `index`, or `nil` if the index is out of range." @spec named_child(t(), non_neg_integer()) :: t() | nil def named_child(%__MODULE__{handle: h, source: src}, index) do case Nif.node_named_child(h, index) do nil -> nil child_handle -> %__MODULE__{handle: child_handle, source: src} end end @doc "Return all children (named and anonymous) as a list." @spec children(t()) :: [t()] def children(%__MODULE__{} = node) do for i <- 0..(child_count(node) - 1)//1, do: child(node, i) end @doc "Return only named children as a list." @spec named_children(t()) :: [t()] def named_children(%__MODULE__{} = node) do for i <- 0..(named_child_count(node) - 1)//1, do: named_child(node, i) end @doc """ Return the slice of the source binary spanned by the node. ### Examples iex> {:ok, tree} = Localize.Mf2.TreeSitter.parse("hello {$name}") iex> root = Localize.Mf2.TreeSitter.root(tree) iex> Localize.Mf2.TreeSitter.Node.text(root) "hello {$name}" """ @spec text(t()) :: binary() def text(%__MODULE__{source: src} = node) do start_b = start_byte(node) end_b = end_byte(node) binary_part(src, start_b, end_b - start_b) end @doc "Return the parent node, or `nil` if this is the root." @spec parent(t()) :: t() | nil def parent(%__MODULE__{handle: h, source: src}) do case Nif.node_parent(h) do nil -> nil handle -> %__MODULE__{handle: handle, source: src} end end @doc "Return the next sibling (including anonymous nodes), or `nil`." @spec next_sibling(t()) :: t() | nil def next_sibling(%__MODULE__{handle: h, source: src}) do case Nif.node_next_sibling(h) do nil -> nil handle -> %__MODULE__{handle: handle, source: src} end end @doc "Return the previous sibling (including anonymous nodes), or `nil`." @spec previous_sibling(t()) :: t() | nil def previous_sibling(%__MODULE__{handle: h, source: src}) do case Nif.node_prev_sibling(h) do nil -> nil handle -> %__MODULE__{handle: handle, source: src} end end @doc "Return the next named sibling, or `nil`." @spec next_named_sibling(t()) :: t() | nil def next_named_sibling(%__MODULE__{handle: h, source: src}) do case Nif.node_next_named_sibling(h) do nil -> nil handle -> %__MODULE__{handle: handle, source: src} end end @doc "Return the previous named sibling, or `nil`." @spec previous_named_sibling(t()) :: t() | nil def previous_named_sibling(%__MODULE__{handle: h, source: src}) do case Nif.node_prev_named_sibling(h) do nil -> nil handle -> %__MODULE__{handle: handle, source: src} end end @doc """ Return the smallest node that spans the given byte range. Includes anonymous nodes. Useful for answering "what is under the cursor?" — pass the cursor's start and end byte offsets (equal for a caret, different for a selection). ### Examples iex> {:ok, tree} = Localize.Mf2.TreeSitter.parse("hello {$name}") iex> root = Localize.Mf2.TreeSitter.root(tree) iex> node = Localize.Mf2.TreeSitter.Node.descendant_for_byte_range(root, 8, 8) iex> Localize.Mf2.TreeSitter.Node.type(node) "name" """ @spec descendant_for_byte_range(t(), non_neg_integer(), non_neg_integer()) :: t() | nil def descendant_for_byte_range(%__MODULE__{handle: h, source: src}, start_byte, end_byte) do case Nif.node_descendant_for_byte_range(h, start_byte, end_byte) do nil -> nil handle -> %__MODULE__{handle: handle, source: src} end end @doc """ Like `descendant_for_byte_range/3` but restricted to named nodes. """ @spec named_descendant_for_byte_range(t(), non_neg_integer(), non_neg_integer()) :: t() | nil def named_descendant_for_byte_range(%__MODULE__{handle: h, source: src}, start_byte, end_byte) do case Nif.node_named_descendant_for_byte_range(h, start_byte, end_byte) do nil -> nil handle -> %__MODULE__{handle: handle, source: src} end end @doc """ Whether this exact node is an `ERROR` node. Non-recursive — unlike `has_error?/1`, this returns `false` for a well-formed node whose descendants contain errors. Used together with tree traversal to collect the precise ranges of parse failures for editor diagnostics. """ @spec error?(t()) :: boolean() def error?(%__MODULE__{handle: h}), do: Nif.node_is_error(h) @doc """ Whether this node is `MISSING`. A missing node is synthesised by tree-sitter's error recovery when a required child was absent in the input (e.g. the `}}` that closes a quoted pattern). Missing nodes have zero-byte span at the point where the parser expected the token. """ @spec missing?(t()) :: boolean() def missing?(%__MODULE__{handle: h}), do: Nif.node_is_missing(h) @doc "Return the node's S-expression form (see `Tree.to_sexp/1`)." @spec to_sexp(t()) :: binary() def to_sexp(%__MODULE__{handle: h}), do: Nif.node_to_sexp(h) defimpl Inspect do def inspect(%Localize.Mf2.TreeSitter.Node{} = node, _opts) do "#Localize.Mf2.TreeSitter.Node<" <> Localize.Mf2.TreeSitter.Node.type(node) <> " " <> inspect(Localize.Mf2.TreeSitter.Node.range(node)) <> ">" end end end