Incremental Parsing
The incremental API reparses a document after a byte-range edit. Its signature is the long-term contract: today it performs a deterministic full reparse of the edited source; future versions may reuse unaffected subtrees from the previous tree, but the result is guaranteed byte-identical to a full parse either way — your code never needs to care which strategy ran.
LexEdit
Section titled “LexEdit”type LexEdit = { readonly start: number; // byte offset into the PREVIOUS source readonly end: number; // half-open: bytes [start, end) are replaced readonly replacement: Uint8Array;};- Insertion:
start === end, non-emptyreplacement. - Deletion:
start < end, emptyreplacement(new Uint8Array(0)). - Replacement: both.
Offsets are byte offsets into the previous tree’s source, not character offsets. If your editor works in UTF-16 code units, convert before building the edit.
reparse through a handle (never throws)
Section titled “reparse through a handle (never throws)”import { createParser } from '@lanexio/parser';import { markdownGrammar } from '@lanexio/parser-grammar-markdown';
const parser = await createParser(markdownGrammar);
let tree = parser.parse('# Hello\n\nWorld.');
// Replace "Hello" (bytes 2..7) with "Goodbye"tree = parser.reparse(tree, { start: 2, end: 7, replacement: new TextEncoder().encode('Goodbye'),});
// Chain edits — each reparse returns a fresh tree over the edited sourcetree = parser.reparse(tree, { start: 0, end: 0, replacement: new TextEncoder().encode('> ') });parser.reparse preserves Never-Throw Integrity: a malformed edit (out-of-bounds or inverted range) or any internal failure returns an error tree built over the previous source rather than throwing.
applyEdit — the pure half (throws on bad ranges)
Section titled “applyEdit — the pure half (throws on bad ranges)”If you want loud validation instead of an error tree, apply the edit yourself and parse the result:
import { applyEdit, LexEditError, LexEditErrorCode } from '@lanexio/parser';import { parseMarkdown } from '@lanexio/parser-grammar-markdown';
try { const nextSource = applyEdit(tree.source, edit); // pure function, allocates the new buffer tree = parseMarkdown(nextSource, { gfm: true });} catch (err) { if (err instanceof LexEditError) { // err.code is one of: // LexEditErrorCode.RangeOutOfBounds — start < 0 or end > source.byteLength // LexEditErrorCode.RangeInverted — end < start }}This split is deliberate: applyEdit validates at the boundary and throws typed errors (your bug), while the grammar’s parse of the edited content stays never-throwing (the document’s problem).
Editor integration pattern
Section titled “Editor integration pattern”import { createParser } from '@lanexio/parser';import { htmlGrammar } from '@lanexio/parser-grammar-html';
const parser = await createParser(htmlGrammar);let tree = parser.parse(initialText);
function onEditorChange(byteStart: number, byteEnd: number, insertedText: string) { tree = parser.reparse(tree, { start: byteStart, end: byteEnd, replacement: new TextEncoder().encode(insertedText), });
// Position-based lookups for the UI: const nodeAtCursor = tree.root.descendantForRange([byteStart, byteStart]); highlight(nodeAtCursor);}descendantForRange and the cursor’s gotoDescendantForRange give O(depth) lookups from byte position to the deepest enclosing node — the building block for hover, selection expansion, and folding. See Tree Traversal.
Determinism
Section titled “Determinism”For a given previous source and edit, reparse is fully deterministic and equals parse(applyEdit(previousSource, edit)) byte-for-byte at the tree-buffer level. Snapshot tests can rely on buffer equality:
import { applyEdit } from '@lanexio/parser';
const viaReparse = parser.reparse(tree, edit);const viaFull = parser.parse(applyEdit(tree.source, edit));// new Uint8Array(viaReparse.buffer) equals new Uint8Array(viaFull.buffer)