Packages

Elixir bindings to the tree-sitter-mf2 grammar. Incremental, error-recovering CST parser for ICU MessageFormat 2 (MF2) messages suitable for editor tooling and LSP use.

Current section

Files

Jump to
localize_mf2_treesitter lib localize mf2 tree_sitter node.ex
Raw

lib/localize/mf2/tree_sitter/node.ex

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