0005 Suggestions
On this page
Suggestion mode records edits as proposals that people accept or reject. It must work for any command (typing, paste, AI, input rules, lists), in standalone editors and under real-time collaboration, and resolve on a server without a browser.
Decision
Commands are not rewritten one by one. Every content transaction runs its normal handler, and a general engine filter, transaction.rewrite, replaces the handler's operations with an equivalent proposal before normalisation.
Storage in the document. Inline proposals are marks on text leaves (
suggestionInsert,suggestionDelete,suggestionFormat) with id, author id, name snapshot and time. Structural proposals are parts of asuggestionannotation. Marks follow their text through splits, joins, copy and collaboration with no anchor mapping; annotations carry what marks cannot (boundaries, block types, wrappers, moves).Rewrite by replay. The rewriter replays the handler's operations on a scratch draft of the base state. Inserted content is applied and marked. Deleted text stays in place, hidden from the handler's coordinates, and is marked for removal. Elements the handler deleted stay as "ghosts" with a
deleteNodepart. Joins are not applied; the removed boundary becomes ajoinpart. Splits, type changes, wraps, unwraps and moves are applied and record what reverts them. Positions are mapped from the handler's view to the scratch draft through per-leaf runs (base, new, hidden) and through children lists with ghosts skipped and joined blocks appended.Own work is not proposed twice. Deleting the author's own proposed text, block or paragraph break removes it outright; reverting one's own wrap or type change drops the part.
Ordering. Parts carry a document-wide sequence number. Accepting applies them in order; rejecting reverts them newest first, so restructuring by several suggestions unwinds correctly.
Refuse rather than half-track. Changes that cannot be represented (code blocks, whole-document replacement, normaliser restructuring) fail with
capability-unavailableand a message.Mode per person, in the document. Each person's suggestion mode is an annotation. The authority executes commands with the authenticated actor, reads the same annotation and produces the same operations the client predicted; ids come from the command-scoped allocator.
Engine changes
TransactionDraft.reset()returns a draft to its base state.The
transaction.rewritefilter receives{ operations, selection }with the draft, origin and base state, and may return replacement operations (applied to the reset draft), a selection, stored marks, or anerrorthat fails the transaction.transaction.allowmay refuse with{ code, message }instead offalse, so a refusal can explain itself.
Alternatives
Per-command suggestion variants. Every plugin would need suggestion-aware code, and paste, AI and input rules would be missed.
Diffing snapshots. A diff guesses intent (which "a" was typed), loses ids and cannot tell a move from a delete and insert.
Range annotations for inline text. Ranges need mapping through every transaction and break on splits and joins; marks already move with text and serialise to
<ins>/<del>.Server-side conversion. Would need suggestion code in the authority. Rewriting inside the transaction keeps the authority generic.
Consequences
Undo of a suggested edit removes the proposal (the rewritten operations are what history records).
Comments anchored to text suggested for deletion stay attached, because deleted text is never removed from the document until accepted.
HTML carries inline suggestions; structural parts need the JSON document.
Rewriting costs a replay of the transaction's operations on a scratch draft, only while suggesting.