Incremental Parsing: Reparse the Paragraph, Not the Document
Reference: Wagner, Tim A. & Graham, Susan L., Efficient and flexible incremental parsing — ACM Transactions on Programming Languages and Systems 20(5), 980–1013, September 1998
An incremental parser keeps the syntax tree it built last time, applies your edit to it, and reuses every node the edit did not touch. The cost of a keystroke tracks the size of the edit, not the size of the document — which is how a 50,000-word note stays as cheap to type in as a short one.
That is the idea in one paragraph. The rest of this post is the part that is harder to find written down: what the reuse mechanism actually looks like in the two systems that ship it, what an editor does with the answer, and where the model breaks on Markdown specifically. Markdown has a token that re-scopes every block after it — three backticks — and the code-editor literature never discusses it as a performance problem.
This post also ships its own measurement, with the harness printed in full, and reports the number that undercuts the drama: block-level scanning of a 50,000-word note takes about four tenths of a millisecond, which is not what makes an editor stutter. The cost model is still the right model. It is just not the right villain.
What does a keystroke actually cost in an editor?
A naive editor re-parses the entire document after every keystroke. That work grows with the document, not with the change, so the per-character cost of typing rises as the note gets longer. The parse itself is only the first bill; highlighting and re-rendering are charged on top of it.
The loop is usually about four lines, and every one of them is proportional to the file:
// The naive loop: every keystroke pays for the whole document.
editor.on('input', () => {
const tree = parse(editor.value); // O(document)
paint(highlight(tree)); // O(document)
});
For a shopping list this is free. For a research note that has been growing for two years it is a tax collected on every character, and the tax rate rises with the note. The standard the tooling world set for itself is explicit about the cadence: tree-sitter's design goals require it to be "Fast enough to parse on every keystroke in a text editor" and "Robust enough to provide useful results even in the presence of syntax errors." 1
The second goal is not a nicety. A document in the middle of an edit is usually broken — an open bracket, a half-typed fence — and a parser that refuses to produce a tree until the text is valid gives the editor nothing to highlight at precisely the moment the user is looking at it.
The interesting question is not "is parsing slow?" It is "what is the parse proportional to?" A cost that scales with the document punishes exactly the users who invested the most in it. A cost that scales with the edit does not. Everything below is about moving from the first to the second, and then about the case where you cannot.
What is incremental parsing?
Incremental parsing means keeping the parse tree from the previous version of the document, telling it where the text changed, and rebuilding only the parts whose meaning the change could have altered. Everything else is reused by reference. The technique comes out of programming-language tooling, and it is decades old.
Laurence Tratt, Professor of Software Development at King's College London, gives the cleanest one-sentence definition: "The basic idea of incremental parsing is to allow people to edit programs as if they were sequences of UTF-8 characters but to maintain and update a parse tree in the background." 2
He adds the constraint that makes it usable in a live editor rather than a structure editor: "Crucially, that parse tree can be arbitrarily “broken” when the user’s input is not syntactically correct, so users aren’t constrained by the tree’s existence." 2
The originators are Tim A. Wagner and Susan L. Graham, whose 1998 paper in ACM Transactions on Programming Languages and Systems is the foundation the modern implementations descend from. Their abstract opens by clearing the field: "Previously published algorithms for LR(k) incremental parsing are inefficient, unnecessarily restrictive, and in some cases incorrect. We present a simple algorithm based on parsing LR(k) sentential forms that can incrementally parse an arbitrary number of textual and/or structural modifications in optimal time and with no storage overhead." 3
The paper also states the reason reuse is not merely an optimization: "The reuse of nonterminal nodes by the parser is essential both in achieving overall environment performance and in maintaining user annotations." 3 Reused nodes keep their identity, so anything the environment attached to them survives the edit.
The lineage is not our reconstruction; both modern implementations state it themselves. Tree-sitter's documentation lists, under the heading "The design of Tree-sitter was greatly influenced by the following research papers:", both Wagner's Berkeley thesis and the TOPLAS paper. 4 Lezer's author, Marijn Haverbeke, writes that "This system's approach is heavily influenced by tree-sitter, a similar system written in C and Rust, and several papers by Tim Wagner and Susan Graham on incremental parsing". 5
Wagner and Graham are the source. Tree-sitter and Lezer are implementations of the idea, and Max Brunsfeld is credited as tree-sitter's author, not as the inventor of incremental parsing.
How does tree-sitter reuse the old tree?
Tree-sitter asks for two things: the byte range the edit replaced, and the old tree. It shifts the ranges of existing nodes so they stay aligned with the new text, then re-parses with the old tree passed in as a reference — producing a new tree that shares structure with the old one instead of rebuilding it from scratch.
The documentation states the use case plainly: "In applications like text editors, you often need to re-parse a file after its source code has changed. Tree-sitter is designed to support this use case efficiently." 6 The procedure has two steps, and the first is bookkeeping rather than parsing: "First, you must edit the syntax tree, which adjusts the ranges of its nodes so that they stay in sync with the code." 6
An edit is described as a struct, and the shape of that struct is the whole cost argument in miniature — it carries the change, not the document:
/**
* A summary of a change to a text document.
*/
typedef struct TSInputEdit {
uint32_t start_byte;
uint32_t old_end_byte;
uint32_t new_end_byte;
TSPoint start_point;
TSPoint old_end_point;
TSPoint new_end_point;
} TSInputEdit;
void ts_tree_edit(TSTree *self, const TSInputEdit *edit);
Three byte offsets and three (row, column) points. Where the change started, where the old text ended, where the new text ends. Nothing about how big the file is. 7
The second step is the reuse itself: "Then, you can call ts_parser_parse again, passing in the old tree. This will create a new tree that internally shares structure with the old tree." 6 Structure sharing is the mechanism doing the work — unchanged subtrees are not copied into the new tree, they are pointed at by it.
Lezer, the parser system behind CodeMirror 6, reaches the same place through a different door. Marijn Haverbeke describes it as a system that "can be used incrementally, meaning it can efficiently re-parse a document that is slightly changed compared to some previous version given the parse for the old version." 5
The reuse unit there is a fragment cache rather than a whole previous tree: "the parser allows you to provide a cache of tree fragments, which hold information about trees produced by previous parses, annotated with information about the document changes that happened in the meantime," and "The parser will, when possible, reuse nodes from this cache rather than re-parsing the parts of the document they cover." 5
Lezer also names why the reuse stays cheap on long documents: "Because the syntax tree represents sequences of matches of repeat operators (specified in the grammar notation with + and *) as balanced sub-trees, the cost of re-matching unchanged parts of the document is low, and you can quickly create a new tree even for a huge document." 5
And it states a browser-specific trade-off that a native library does not have to make — it "generates more compact in-memory trees, to avoid putting too much pressure on the user's machine." 5 In a tab, the tree you keep around is a cost too, not just an asset.
How does the editor know what to redraw?
Reparsing cheaply is only half the win. The editor still has to decide what to repaint, and repainting the whole document undoes the saving. Tree-sitter answers this with a function that diffs the old tree against the new one and hands back the ranges whose syntactic structure actually changed.
The declaration and its doc comment are both in the C header — the API is real, but it is documented in lib/include/tree_sitter/api.h rather than on the parsing docs page:
/**
* The returned ranges indicate areas where the hierarchical structure of syntax
* nodes (from root to leaf) has changed between the old and new trees. Characters
* outside these ranges have identical ancestor nodes in both trees.
*
* Note that the returned ranges may be slightly larger than the exact changed areas,
* but Tree-sitter attempts to make them as small as possible.
*/
TSRange *ts_tree_get_changed_ranges(
const TSTree *old_tree,
const TSTree *new_tree,
uint32_t *length
);
Read the second sentence of that comment carefully: "Characters outside these ranges have identical ancestor nodes in both trees." 7 That is a guarantee an editor can act on. Every character the function did not report still has the same chain of parents it had before the keystroke, so its highlighting, folding, and indentation are still correct and do not need to be recomputed.
The honest caveat is in the same comment: "Note that the returned ranges may be slightly larger than the exact changed areas, but Tree-sitter attempts to make them as small as possible." 7 It over-reports rather than under-reports, because a missed range would be a rendering bug and a slightly wide one is only a little extra work.
This is the API that turns "reparse cheaply" into "redraw cheaply." Without it, a fast incremental parse still ends at a full repaint, and the user feels the repaint.
What does the edit loop look like end to end?
The loop has five moves: capture the edit as byte offsets, shift the old tree's ranges, reparse against that tree, ask which ranges changed, redraw only those. One branch decides everything — whether the edit altered a token that controls block structure, because that is the case where the cheap path does not apply.
flowchart TD
A[Keystroke in<br/>paragraph 400] --> B[Edit descriptor<br/>start / old end / new end]
B --> C[Shift node ranges<br/>in the previous tree]
C --> D{Did the edit change<br/>a block token?}
D -->|No| E[Reparse the edited block<br/>reuse subtrees by reference]
D -->|Yes| F[Invalidate to the end of<br/>the containing block]
E --> G[Changed ranges]
F --> G
G --> H[Redraw only<br/>those ranges]
Figure: the incremental edit loop. A keystroke becomes an edit descriptor of three byte offsets, which shifts the ranges of the previous tree. The reparse then branches: an ordinary edit reparses one block and reuses the rest of the tree by reference, while an edit that changes a block-structural token invalidates everything to the end of the containing block. Both paths converge on a set of changed ranges, and the editor redraws only those.
Two properties of that loop are worth naming. First, the expensive-looking arrow — "reuse subtrees by reference" — is genuinely cheap in tree-sitter's implementation, because trees are reference-counted rather than copied: "Internally, copying a syntax tree just entails incrementing an atomic reference count." 6
A tree cheap enough to copy is a tree cheap enough to hand to another thread, which is the bridge to the sibling question of where this work runs. Shrink the work first, with the cost model in this post; then move what remains off the main thread, as covered in keeping a note editor at 60fps with Web Workers. Doing it in the other order means paying transfer costs to relocate work you could have deleted.
Second, the diamond in the middle is not a rare edge case in Markdown. It is three backticks.
Why does an unclosed code fence break the model?
Because a fence is a block-structural token with unbounded reach. CommonMark specifies that an unclosed fenced code block swallows every line after it to the end of the containing block or document — so typing three backticks in paragraph 12 of a 400-paragraph note re-scopes all 388 paragraphs below it, and the edit stops being local.
The spec is unambiguous. Section 4.5 says the content of the block "consists of all subsequent lines, until a closing code fence of the same type as the code block began with", and then covers the half-typed case explicitly: "If the end of the containing block (or document) is reached and no closing code fence has been found, the code block contains all of the lines after the opening code fence until the end of the containing block (or document)." 8
The same section removes the one guard rail you might hope for: "A fenced code block may interrupt a paragraph, and does not require a blank line either before or after." 8 There is no blank-line ceremony to warn the parser. One character mid-sentence flips the meaning of the rest of the note.
Notes on the migration.
```
Everything from here down is now inside a code block,
including this heading:
## Not a heading anymore
...and it stays that way until a closing fence appears
or the document ends.
The moment the user types the third backtick — before typing anything else, before closing it — every block below changes meaning. Structurally that keystroke is not a paragraph edit. It is a document edit.
This is not a Markdown quirk; it is the general worst case of incremental parsing, wearing a Markdown costume. Haverbeke names the same class of hazard in Lezer's own documentation: "This isn't bulletproof, though—even a tiny document change, if it changes the meaning of the stuff that comes after it, can require a big part of the document to be re-parsed. An example would be adding or removing a block comment opening marker." 5
Swap "block comment opening marker" for "opening code fence" and you have the Markdown editor's version, with the difference that Markdown users type fences constantly and deliberately, whereas programmers open block comments rarely.
Two consequences follow for anyone building this. The recovery cost tracks the tail after the edit, not the edit, so the same keystroke is cheap at the bottom of a note and expensive at the top.
And the invalidation boundary is the containing block, not the document — a fence opened inside a blockquote or a list item is bounded by that container, which is a meaningful saving on a structured note. Correctness questions around these same constructs — where parsers genuinely disagree about what a document means — are a separate problem covered in the Markdown edge cases that break editors. This post is about what they cost.
How much does the full scan actually cost?
Less than the framing suggests, and saying so first is the point. A dependency-free block scanner written for this post walks a 50,000-word Markdown document in about 0.41 milliseconds on Node v20.19.5, while the same scan restricted to the edited paragraph takes 0.0008 milliseconds. The shape of the curve is the finding, not the absolute numbers.
The harness models the leaf-block phase of a CommonMark-ish parse — the pass that decides where paragraphs, headings, quotes, lists, and fences begin and end. That is precisely the phase an edit invalidates, and it is small enough to print in full:
// Minimal CommonMark-ish BLOCK scanner: line -> block boundaries.
// Not a full parser; it models the leaf-block phase, which is the part
// an edit invalidates. No dependencies. Node >= 18.
function scanBlocks(lines, from = 0, to = lines.length) {
const blocks = []; let i = from; let fenceChar = null, fenceLen = 0;
let cur = null;
const push = () => { if (cur) { blocks.push(cur); cur = null; } };
for (; i < to; i++) {
const line = lines[i];
const m = /^ {0,3}(`{3,}|~{3,})(.*)$/.exec(line);
if (fenceChar) {
cur.end = i;
if (m && m[1][0] === fenceChar && m[1].length >= fenceLen &&
m[2].trim() === '') { fenceChar = null; push(); }
continue;
}
if (m) { push(); fenceChar = m[1][0]; fenceLen = m[1].length;
cur = { type: 'fence', start: i, end: i }; continue; }
if (/^ {0,3}#{1,6}(\s|$)/.test(line)) {
push(); blocks.push({ type: 'heading', start: i, end: i }); continue;
}
if (/^ {0,3}>/.test(line)) {
if (!cur || cur.type !== 'quote') { push(); cur = { type: 'quote', start: i, end: i }; }
else cur.end = i;
continue;
}
if (/^ {0,3}([-*+]|\d{1,9}[.)])(\s|$)/.test(line)) {
if (!cur || cur.type !== 'list') { push(); cur = { type: 'list', start: i, end: i }; }
else cur.end = i;
continue;
}
if (line.trim() === '') { push(); continue; }
if (!cur || cur.type !== 'para') { push(); cur = { type: 'para', start: i, end: i }; }
else cur.end = i;
}
push();
return blocks;
}
The driver builds synthetic documents of 500, 5,000, and 50,000 words, edits the middle paragraph, and times three variants: scanBlocks(L) for the full document, scanBlocks(L, target.start, target.end + 1) for the edit-scoped rescan, and a third run after replacing the edited line with an opening fence, which forces a rescan from the edit to the end of the document. Medians of 400 iterations, timed with process.hrtime.bigint() on Node v20.19.5. 9
| Document | Lines | Full-document block scan | Edit-scoped rescan | Unclosed-fence rescan | Full ÷ scoped |
|---|---|---|---|---|---|
| 500 words | 58 | 0.0084 ms | 0.0004 ms | 0.0012 ms | 19× |
| 5,000 words | 570 | 0.0338 ms | 0.0005 ms | 0.0050 ms | 73× |
| 50,000 words | 5,696 | 0.4061 ms | 0.0008 ms | 0.0545 ms | 542× |
Three readings, in order of importance.
The full scan grows with the document: 0.0084 to 0.4061 milliseconds across a 100-fold increase in size. The edit-scoped rescan is effectively flat: 0.0004 to 0.0008 milliseconds across the same range. That is the thesis of this post, measured — cost tracks the edit, not the note. The ratio widens from 19× to 542× purely because the numerator keeps growing while the denominator does not.
The fence column is the middle column made expensive. At 50,000 words it costs 0.0545 milliseconds, about 68× the edit-scoped rescan, because it has to walk the tail after the edit. Edit near the end of a note and it is nearly free; open a fence in the first paragraph and you are paying close to full-document price.
And the number that keeps this post honest: 0.4061 milliseconds is about 2.4% of a 16.7-millisecond frame budget. Block scanning alone does not make a note editor stutter at 50,000 words. Anyone with a profiler would say so within a minute, so it is better said here.
What the table does not show matters as much as what it does. It measures a simplified leaf-block scanner written for this post — not tree-sitter and not Lezer. It demonstrates the cost shape, not either library's performance, and it should never be read as a comparison between them.
It is one machine, one Node version, one synthetic corpus: order of magnitude, not constant. Inline parsing, tree allocation, and rendering are all excluded. And there is no cold-start column, which flatters incremental parsing, because a real incremental parser must first build and then retain the previous tree. The same in-browser measurement discipline — publish the harness, state the machine, refuse to generalise from one run — is the one used in the in-browser full-text search benchmark.
When is incremental parsing not worth it?
When the document is small, when the parser is cold, and when the real cost is somewhere else. At 500 words the full scan costs 8 microseconds, so the bookkeeping of edits, fragment caches, and tree retention can genuinely cost more than it saves. Incremental parsing is a good default with a known worst case, not a guarantee.
Take the three cases in turn. On a short note, the machinery loses on its own terms: you are maintaining a tree, allocating edit descriptors, and diffing ranges to avoid work that measured 8 microseconds. On a cold start there is no previous tree at all, so the first parse is a full parse plus the cost of building a structure you intend to keep — an incremental parser is not faster on the first pass, only on the second and every one after.
And the fence case is the reminder that the worst case is real: Haverbeke's warning that "This isn't bulletproof, though—even a tiny document change, if it changes the meaning of the stuff that comes after it, can require a big part of the document to be re-parsed" 5 is a property of the technique, not a defect in an implementation.
The third case is the one most likely to apply to a browser editor, and the measurement above is what points at it. If a long note feels sluggish, block structure is almost certainly not the culprit at 0.4 milliseconds. Suspect the work that happens after the parse: inline parsing of every emphasis run and link in the visible region, syntax highlighting, and DOM reconciliation across thousands of nodes, none of which the table above measures.
Those are frame-budget-scale costs, and the fix for them is a different one — keeping long tasks off the thread that draws the screen, which is the 60fps Web Worker problem.
The correct order still holds. Shrink the work, then move it. A parse you eliminated costs nothing to transfer, and cutting a workload by two orders of magnitude before relocating it is strictly better than paying postMessage to relocate the whole thing. But do not sell the shrink as the cure for lag it was not causing.
Frequently Asked Questions
How do code editors stay fast in large files?
By making the parse proportional to the edit instead of the file. The parser keeps the previous syntax tree, applies a byte-range edit to it, reparses only what the change could have affected, and reuses the rest by reference. Tree-sitter's stated design goal is to be fast enough to parse on every keystroke. 1
Why does my editor lag in long documents?
Rarely because of block parsing. In the harness above, a full block scan of a 50,000-word note took 0.4061 milliseconds — roughly 2.4% of a 16.7-millisecond frame. Suspect inline parsing, syntax highlighting, and DOM reconciliation first, and profile before optimising the parser you assumed was guilty.
What is incremental parsing?
Laurence Tratt's definition is the cleanest: "The basic idea of incremental parsing is to allow people to edit programs as if they were sequences of UTF-8 characters but to maintain and update a parse tree in the background." 2 The formal treatment is Wagner and Graham's 1998 TOPLAS paper, which parses modifications "in optimal time and with no storage overhead." 3
Is tree-sitter faster than a normal parser?
That is the wrong axis. It is faster per edit on a document you have already parsed, because the new tree "internally shares structure with the old tree." 6 On a cold start it is not faster — it must build and then retain the previous tree, so on small documents the bookkeeping can cost more than the saving.
Why does an unclosed code block break the rest of my Markdown?
Because CommonMark says it should. Section 4.5: "If the end of the containing block (or document) is reached and no closing code fence has been found, the code block contains all of the lines after the opening code fence until the end of the containing block (or document)." 8 A fence may also interrupt a paragraph with no blank line. 8
What is the difference between tree-sitter and Lezer?
Let Lezer describe it. Its approach is "heavily influenced by tree-sitter, a similar system written in C and Rust", and it differs by generating "more compact in-memory trees, to avoid putting too much pressure on the user's machine." 5 That memory trade-off is the one that matters most when the parser runs in a browser tab.
How do I know which part of the document changed after a parse?
Tree-sitter's ts_tree_get_changed_ranges compares the old and new trees. Its documentation guarantees that "Characters outside these ranges have identical ancestor nodes in both trees", while warning that returned ranges "may be slightly larger than the exact changed areas". 7 That guarantee is what lets an editor repaint a region instead of a document.
An editor that scales is not the one that parses faster. It is the one that parses less, knows exactly when it cannot, and tells the renderer the truth about which lines moved.
MNMNOTE keeps notes as plain Markdown on your own device, working offline by default — mnmnote.com.
[
{
"@context": "https://schema.org",
"@type": "Article",
"headline": "Incremental Parsing: Reparse the Paragraph, Not the Document",
"description": "Incremental parsing keeps the previous syntax tree, applies the edit to it, and reuses everything that did not move — so the cost of a keystroke tracks the size of the edit, not the size of the note. Markdown has one hazard that breaks the rule: an unclosed code fence.",
"articleSection": "Engineering",
"datePublished": "2026-07-28",
"dateModified": "2026-07-28",
"author": {
"@type": "Organization",
"name": "MNMNOTE",
"url": "https://mnmnote.com"
},
"publisher": {
"@type": "Organization",
"name": "MNMNOTE",
"url": "https://mnmnote.com"
},
"mainEntityOfPage": {
"@type": "WebPage",
"@id": "https://blog.mnmnote.com/posts/reparse-the-paragraph-not-the-document"
},
"keywords": "incremental parsing, tree-sitter, Lezer, CommonMark, Markdown editor performance"
},
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{
"@type": "ListItem",
"position": 1,
"name": "Blog",
"item": "https://blog.mnmnote.com/"
},
{
"@type": "ListItem",
"position": 2,
"name": "Engineering",
"item": "https://blog.mnmnote.com/category/engineering"
},
{
"@type": "ListItem",
"position": 3,
"name": "Incremental Parsing: Reparse the Paragraph, Not the Document",
"item": "https://blog.mnmnote.com/posts/reparse-the-paragraph-not-the-document"
}
]
},
{
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": [
{
"@type": "Question",
"name": "How do code editors stay fast in large files?",
"acceptedAnswer": {
"@type": "Answer",
"text": "By making the parse proportional to the edit instead of the file. The parser keeps the previous syntax tree, applies a byte-range edit to it, reparses only what the change could have affected, and reuses the rest by reference. Tree-sitter's stated design goal is to be fast enough to parse on every keystroke."
}
},
{
"@type": "Question",
"name": "Why does my editor lag in long documents?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Rarely because of block parsing. In our harness, a full block scan of a 50,000-word note took 0.4061 milliseconds, roughly 2.4% of a 16.7-millisecond frame. Suspect inline parsing, syntax highlighting, and DOM reconciliation first, and profile before optimising the parser you assumed was guilty."
}
},
{
"@type": "Question",
"name": "What is incremental parsing?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Laurence Tratt defines it as allowing people to edit programs as if they were sequences of UTF-8 characters while a parse tree is maintained and updated in the background. The formal treatment is Wagner and Graham's 1998 TOPLAS paper, which parses modifications in optimal time and with no storage overhead."
}
},
{
"@type": "Question",
"name": "Is tree-sitter faster than a normal parser?",
"acceptedAnswer": {
"@type": "Answer",
"text": "That is the wrong axis. It is faster per edit on a document you have already parsed, because the new tree internally shares structure with the old tree. On a cold start it is not faster, since it must build and retain the previous tree, so on small documents the bookkeeping can cost more than the saving."
}
},
{
"@type": "Question",
"name": "Why does an unclosed code block break the rest of my Markdown?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Because CommonMark 0.31.2 section 4.5 specifies it. If the end of the containing block or document is reached and no closing code fence has been found, the code block contains all of the lines after the opening fence until that end. A fence may also interrupt a paragraph with no blank line."
}
},
{
"@type": "Question",
"name": "What is the difference between tree-sitter and Lezer?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Lezer describes its own approach as heavily influenced by tree-sitter, a similar system written in C and Rust, and says it differs by generating more compact in-memory trees to avoid putting too much pressure on the user's machine. That memory trade-off matters most when the parser runs in a browser tab."
}
},
{
"@type": "Question",
"name": "How do I know which part of the document changed after a parse?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Tree-sitter's ts_tree_get_changed_ranges compares the old and new trees. Its documentation guarantees that characters outside the returned ranges have identical ancestor nodes in both trees, while warning that the ranges may be slightly larger than the exact changed areas. That guarantee lets an editor repaint a region instead of a document."
}
}
]
}
]
Footnotes
-
Tree-sitter design goals, project README and documentation introduction. Accessed 2026-07-28; README pinned to commit
4deef2dso the quoted lines cannot drift. https://tree-sitter.github.io/tree-sitter/ (archived https://web.archive.org/web/20260728013026/https://tree-sitter.github.io/tree-sitter/) · https://github.com/tree-sitter/tree-sitter/blob/4deef2d5ef0d2dc738289f50622211a24ff7d9a0/README.md ↩ ↩2 -
Laurence Tratt, "Structured Editing and Incremental Parsing", tratt.net, 27 November 2024. Accessed 2026-07-28. https://tratt.net/laurie/blog/2024/structured_editing_and_incremental_parsing.html (archived https://web.archive.org/web/20260723192326/https://tratt.net/laurie/blog/2024/structured_editing_and_incremental_parsing.html) ↩ ↩2 ↩3
-
Wagner, Tim A. and Graham, Susan L., "Efficient and flexible incremental parsing", ACM Transactions on Programming Languages and Systems 20(5), pp. 980–1013, September 1998. DOI 10.1145/293677.293678 (volume, issue, pages and month confirmed against the Crossref registry deposit; the author-copy PDF carries a stale pre-publication footer reading "Vol. 20, No. 2, March 1998"). Accessed 2026-07-28. https://doi.org/10.1145/293677.293678 · author copy: https://harmonia.cs.berkeley.edu/papers/twagner-parsing.pdf (archived https://web.archive.org/web/20260429161114/https://harmonia.cs.berkeley.edu/papers/twagner-parsing.pdf) ↩ ↩2 ↩3
-
Tree-sitter documentation, "The design of Tree-sitter was greatly influenced by the following research papers", listing Wagner's Practical Algorithms for Incremental Software Development Environments and Efficient and Flexible Incremental Parsing. Accessed 2026-07-28. https://tree-sitter.github.io/tree-sitter/ ↩
-
Marijn Haverbeke, Lezer System Guide (Overview and Incremental Parsing sections). Accessed 2026-07-28. https://lezer.codemirror.net/docs/guide/ (archived https://web.archive.org/web/20260630172930/https://lezer.codemirror.net/docs/guide/) ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8
-
Tree-sitter documentation, "Advanced Parsing" (Editing and Concurrency sections). Accessed 2026-07-28. https://tree-sitter.github.io/tree-sitter/using-parsers/3-advanced-parsing.html (archived https://web.archive.org/web/20260728013047/https://tree-sitter.github.io/tree-sitter/using-parsers/3-advanced-parsing.html) ↩ ↩2 ↩3 ↩4 ↩5
-
Tree-sitter C API header,
lib/include/tree_sitter/api.h—TSInputEdit,ts_tree_edit, andts_tree_get_changed_rangeswith its doc comment. Accessed 2026-07-28; pinned to commit4deef2dso the quoted declarations cannot drift. https://github.com/tree-sitter/tree-sitter/blob/4deef2d5ef0d2dc738289f50622211a24ff7d9a0/lib/include/tree_sitter/api.h ↩ ↩2 ↩3 ↩4 -
CommonMark Spec version 0.31.2 (2024-01-28), §4.5 "Fenced code blocks". Accessed 2026-07-28. https://spec.commonmark.org/0.31.2/ (archived https://web.archive.org/web/20260728013151/https://spec.commonmark.org/0.31.2/) ↩ ↩2 ↩3 ↩4
-
Own measurement, 2026-07-28. Dependency-free block-scanner harness (source printed above) on Node v20.19.5, medians of 400 iterations per cell timed with
process.hrtime.bigint(), synthetic documents of 500 / 5,000 / 50,000 words. One machine, one runtime, one corpus. ↩