Module

incremental

Incremental re-parsing for Patitas ASTs.

When a user edits one paragraph, only that paragraph's region needs re-parsing. This module accepts a previous Document AST plus the new source text and an edit range, then:

  1. Identifies which top-level blocks overlap the edit range.
  2. Determines the minimal region of source that must be re-parsed.
  3. Re-parses only that region.
  4. Splices the new blocks into the existing AST.

The result is a new Document that is structurally and offset-wise equivalent to a full re-parse, computed in O(change) rather than O(document). Re-parsed blocks have their full node tree (the top-level blocklocation and every descendant inline/nested location, plusFencedCode source_start/source_end) shifted to absolute offsets, and blocks after the edit are shifted by the edit delta. This keeps source maps, LSP positions, and diagnostics accurate without a full re-parse.

Custom registries:

``parse_incremental`` accepts ``directive_registry`` so the re-parsed
region honors custom directives exactly like ``parse()``.  Inline
roles are resolved at *render* time, not parse time, so there is no
parse-time role registry -- this mirrors ``parse()``'s own signature.

Fallback:

If the edit cannot be handled incrementally (e.g., the region
detection fails or the partial re-parse raises), falls back to a
full re-parse.  This guarantees correctness at all times.  If the
fallback full re-parse *also* fails, the error is propagated (with
any original partial-parse error chained as ``__cause__``) rather
than silently swallowed.

Thread Safety:

``parse_incremental`` is a pure function -- safe to call from any thread.

Functions

parse_incremental 7 Document
Parse only the edited region and splice into the existing AST.
def parse_incremental(new_source: str, previous: Document, edit_start: int, edit_end: int, new_length: int, *, source_file: str | None = None, directive_registry: DirectiveRegistry | None = None) -> Document
Parameters
Name Type Description
new_source str

The complete new source text (after the edit).

previous Document

The Document AST from before the edit.

edit_start int

Offset in the OLD source where the edit begins.

edit_end int

Offset in the OLD source where the edit ends (i.e., the old text from edit_start..edit_end was replaced).

new_length int

Length of the replacement text in the new source. The replaced region in new_source isnew_source[edit_start : edit_start + new_length].

source_file str | None

Optional source file path for location tracking.

Default:None
directive_registry DirectiveRegistry | None

Custom directive registry (uses defaults if None), matching :func:patitas.parse. The re-parsed region honors it exactly like a full parse.

Default:None
Returns
Document
_find_affected_range 3 tuple[int | None, int | …
Find the range of block indices affected by an edit. A block is affected if it…
def _find_affected_range(blocks: Sequence[Block], edit_start: int, edit_end: int) -> tuple[int | None, int | None]

Find the range of block indices affected by an edit.

A block is affected if its source range overlaps [edit_start, edit_end). If no blocks overlap, expands to the surrounding blocks to handle boundary changes (e.g., merging two paragraphs).

Returns (first_affected, last_affected) or (None, None).

Parameters
Name Type Description
blocks Sequence[Block]
edit_start int
edit_end int
Returns
tuple[int | None, int | None]
_parse_region 4 tuple[tuple[Block, ...] …
Parse a source region and adjust locations to absolute offsets. Returns ``(blo…
def _parse_region(source: str, offset: int, source_file: str | None, directive_registry: DirectiveRegistry | None) -> tuple[tuple[Block, ...] | None, Exception | None]

Parse a source region and adjust locations to absolute offsets.

Returns(blocks, None) on success or (None, error)when the partial parse raises. Returning the captured exception (instead of swallowing it) lets the caller chain it onto a fallback failure so errors are never fully opaque.

Parameters
Name Type Description
source str
offset int
source_file str | None
directive_registry DirectiveRegistry | None
Returns
tuple[tuple[Block, ...] | None, Exception | None]
_shift_node 2 N
Return a copy of ``node`` with its entire subtree shifted by ``delta``. Shifts…
def _shift_node(node: N, delta: int) -> N

Return a copy of node with its entire subtree shifted by delta.

Shifts location (offset/end_offset) on every node, FencedCode source_start/source_end (which index into source text), and recurses into every child node held in dataclass fields (single nodes and tuples of nodes, including tuples-of-tuples such as Table rows).

Usesdataclasses.replacefor clean frozen-dataclass copying.

Parameters
Name Type Description
node N
delta int
Returns
N
_shift_field_value 2 object
Recursively shift any nodes reachable through a dataclass field value. Handles…
def _shift_field_value(value: object, delta: int) -> object

Recursively shift any nodes reachable through a dataclass field value.

Handles single nodes and (possibly nested) tuples of nodes. Non-node values (strings, ints, options objects, ...) are returned unchanged so the caller can skip them via identity comparison.

Parameters
Name Type Description
value object
delta int
Returns
object
_shift_block_offset 2 Block
Shift a block and its entire subtree by ``delta``. Thin wrapper over `_shift_n…
def _shift_block_offset(block: Block, delta: int) -> Block

Shift a block and its entire subtree bydelta.

Thin wrapper over _shift_node() kept for the call-site/test surface. Unlike earlier versions, this adjusts all descendant locations (inline and nested), not just the top-level block location, so incremental results match a full parse.

Parameters
Name Type Description
block Block
delta int
Returns
Block
_adjust_offsets 2 tuple[Block, ...]
Shift location offsets of all blocks (and their subtrees) by delta.
def _adjust_offsets(blocks: Sequence[Block], delta: int) -> tuple[Block, ...]
Parameters
Name Type Description
blocks Sequence[Block]
delta int
Returns
tuple[Block, ...]
_full_parse 4 Document
Fall back to a full re-parse. If the full re-parse itself fails, the error is …
def _full_parse(source: str, source_file: str | None, directive_registry: DirectiveRegistry | None = None, cause: Exception | None = None) -> Document

Fall back to a full re-parse.

If the full re-parse itself fails, the error is not swallowed: it propagates, with any earlier partial-parse error chained as __cause__so the recoverable-vs-fatal distinction stays visible.

Parameters
Name Type Description
source str
source_file str | None
directive_registry DirectiveRegistry | None Default:None
cause Exception | None Default:None
Returns
Document