feat(html): a text editor that owns the edit - #875
Merged
Conversation
andiwand
force-pushed
the
feat/text-edit-paragraphs
branch
from
September 10, 2026 12:56
4210894 to
5e1f351
Compare
andiwand
force-pushed
the
feat/text-edit-browser
branch
from
September 10, 2026 12:56
22f260f to
9ac0584
Compare
andiwand
force-pushed
the
feat/text-edit-paragraphs
branch
2 times, most recently
from
September 10, 2026 13:44
267399e to
7c538eb
Compare
The editor cancels what the browser was about to do and splices the page itself, so the markup stays what the renderer wrote and every change has an operation behind it. Reading the run back afterwards only worked while an edit stayed inside one run: contenteditable's answer to a selection spanning two paragraphs is browser-specific and maps onto nothing in the element tree. So a reader can now type, replace and delete across runs and across paragraphs, press Enter to split a paragraph and keep the formatting on both sides, press Backspace at the start of one to merge it into the paragraph before, and paste plain text over as many lines as it holds. Undo and redo come with it, because cancelling every edit leaves the browser's own stack empty. Each step holds the operations it puts on the wire and the two halves of taking it back, so `canUndo` and the chord agree at last and a host's undo button is live. Two things the browser does not always state, and what the editor does about them. A delete whose range it left out is one character, or the paragraph boundary the caret stands at - but a word or line delete is not guessed at, because guessing where a word ends takes away text the reader did not name. And a composition cannot be cancelled at all, so the editor lets it finish and reads the run back on `compositionend`. `insertText` gains a `parent` form for the paragraph Enter just made, which holds no run to sit beside; `Document::append_text` is the same in C++. The check page drives all of it; `defaultPrevented` no longer says whether an edit was taken, because the editor cancels either way, so the checks read the refusal channel instead. 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-browser
branch
from
September 10, 2026 13:52
9ac0584 to
5ec412a
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
Fourth of five; #874 is merged, so this now sits on main. See
docs/design/document-editing.mddecisions 6 and 6b.What this one does
The editor cancels what the browser was about to do and splices the page
itself, so the markup stays what the renderer wrote and every change has an
operation behind it.
Reading the run back afterwards only worked while an edit stayed inside one
run. Contenteditable's answer to a selection spanning two paragraphs is
browser-specific — a
<div>wrapper here, a merged<b>there — and maps ontonothing in the element tree.
So a reader can now:
Undo and redo are the editor's
Cancelling every edit leaves the browser's own stack empty, so undo had to come
with this. Each step holds the operations it puts on the wire and the two
halves of taking it back, so
canUndoand the chord agree at last and a host'sundo button is live. One
beforeinputis one step.Two things the browser does not always state
key names — or, at the start of a paragraph, the boundary itself, which
merges and takes no character. A word or line delete is not guessed at:
guessing where a word ends takes away text the reader did not name.
the run back on
compositionend. With the page as the model there is nothingto reconcile — the run's text is the operation.
insertTextgains aparentform for the paragraph Enter just made, whichholds no run to sit beside;
Document::append_textis the same in C++, on thedocument rather than on a handle like the other structural edits.
Checks
test/browser/text/tests.html— 48 checks, drivingbeforeinputand assertingboth the page and the log.
defaultPreventedno longer says whether an editwas taken, because the editor cancels either way, so the checks read the
refusal channel instead. Run headless:
test/browser/text/servethen Chrome--headless --dump-dom.