feat(document): a text edit that spans several runs - #873
Merged
Conversation
andiwand
force-pushed
the
feat/text-edit-by-id
branch
from
September 10, 2026 12:56
b7195cf to
12310b4
Compare
andiwand
force-pushed
the
feat/text-edit-runs
branch
from
September 10, 2026 12:56
0fb6032 to
315dcd7
Compare
andiwand
force-pushed
the
feat/text-edit-runs
branch
3 times, most recently
from
September 10, 2026 13:19
5c7adcb to
d0a4632
Compare
`insertText` puts a run beside one that is there, in the same parent so it carries the same style, and `removeElement` takes an element and its subtree away. With `setText` on each end, a selection that starts in one run and ends in another replays as the two ends rewritten and what lay between removed. `Document::remove`, `Document::insert_text_before` and `insert_text_after` are the same three in the C++ API. They sit on the document rather than on a handle because a handle says what an element holds and the document says what the tree holds - an `Element` is immutable, and restructuring the tree through one would leave a handle naming something unreachable. Each refuses an element of another document. An operation that creates an element states a negative id for it, and a later operation names it by the same number, so replay stays a pure function of the log. The shared registry grows the links a structural edit needs - `insert_sibling_after`, `insert_sibling_before` and `unlink_child`. Until now nothing built a tree except a parser reading forward, so `append_child` was the whole surface. An unlinked element keeps its id and stops being reachable, so an id an edit already handed out never names something else. `xml::TreeEditor` is where the dom half lives, over any registry whose elements carry a `pugi::xml_node`. odf, ooxml text and ooxml presentation all resolve an id to a node, splice the subtree and fix up the links; only the tag names differ, and those come from the nodes. Each engine writes its own text nodes and nothing else. Fixes a `.docx` edit dropping the space at either end of a run: the `w:t` now states `xml:space="preserve"` where the text needs it. A lone space is part of a `string` token, so the token type never said one was there. Verified with headless LibreOffice on the saved package. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KKFKbUVCYF2VhujdmjhhPW
andiwand
force-pushed
the
feat/text-edit-runs
branch
from
September 10, 2026 13:27
d0a4632 to
07199f0
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🤖 Generated with Claude Code
Second of five; #872 is merged, so this now sits on main. See
docs/design/document-editing.mdfor the decisions.What this one does
insertTextputs a run beside one that is there, in the same parent so itcarries the same style, and
removeElementtakes an element and its subtreeaway. With
setTexton each end, a selection that starts in one run and endsin another replays as the two ends rewritten and what lay between removed.
Document::remove,Document::insert_text_beforeandinsert_text_afterarethe same three in the C++ API. They sit on the document rather than on a
handle: a handle says what an element holds, the document says what the tree
holds. An
Elementis an immutable handle, so restructuring the tree throughone would leave a handle naming something unreachable. Each refuses an element
of another document.
Created elements
An operation that creates one states a negative id for it, and a later
operation names it by the same number. Replay keeps a per-log map. That keeps
replay a pure function of the log — returning minted ids to the browser is the
round trip architecture A exists to avoid. A sign rather than a reserved range,
because odf already spends the top bit of
ElementIdentifieron a positionalcell id.
The registry grows the links a structural edit needs
insert_sibling_after,insert_sibling_beforeandunlink_child. Until nownothing built a tree except a parser reading forward, so
append_childwas thewhole surface. An unlinked element keeps its id and stops being reachable, so
an id an edit already handed out never names something else.
xml::TreeEditoris where the dom half lives, over any registry whose elementscarry a
pugi::xml_node. odf, ooxml text and ooxml presentation all resolve anid to a node, splice the subtree and fix up the links; only the tag names
differ, and those come from the nodes.
Read-only engines take a default that throws, rather than eight identical
bodies.
Fix
A
.docxedit dropped the space at either end of a run: thew:tnow statesxml:space="preserve"where the text needs it ([ECMA-376] Part 1 17.3.3.31). Alone space is part of a
stringtoken, so the token type never said one wasthere. Verified with headless LibreOffice on the saved package.