Suggest mode: inline suggestions on the #78218 marker primitive#79282
Draft
adamsilverstein wants to merge 29 commits into
Draft
Suggest mode: inline suggestions on the #78218 marker primitive#79282adamsilverstein wants to merge 29 commits into
adamsilverstein wants to merge 29 commits into
Conversation
|
Size Change: +4.33 kB (+0.06%) Total Size: 7.52 MB 📦 View Changed
|
This was referenced Jun 17, 2026
094a10a to
ed4a4da
Compare
2bfac09 to
d42e861
Compare
Generalize the inline-note marker helpers (#78218) into a format-agnostic primitive under editor/components/inline-markers/ so both Notes and the upcoming inline Suggestions can anchor ranges to edit-surviving <mark> markers and derive offsets on read. - findMarkerRange: parameterize findNoteRange over format type, id attribute, and an optional quick-reject token. The single offset-resolution point and the intended CRDT swap point. - wrapInlineMarker / readInlineSelection / reconcileMarkerRemoval / useAnnotateRanges: generalize the wrap, selection-read, removal-decision, and annotation-decoration helpers; parameterize the annotation source. Refactor Notes to consume the primitive with no behavior change: the Notes-specific _wp_note_selection meta fallback stays in the collab-sidebar layer, so the primitive itself derives strictly from the in-content marker (no stored offsets). All 90 existing collab-sidebar tests pass unchanged; 24 standalone primitive tests added. Part of Phase 0 of the inline-suggestions plan.
Build the inline-suggestion marker layer on the shared inline-markers primitive (Option B: suggested inline changes live as marked text in block content). - core/suggestion rich-text format, serializing as <mark class="wp-suggestion" data-suggestion-id data-suggestion-type data-author>. The distinct wp-suggestion class never collides with wp-note or a user/core/text-color <mark>; data-author makes per-author attribution structural. findSuggestionRange resolves a marker's live range by id via the primitive (offsets derived on read). - useAnnotateSuggestions decorates ranges under the core-suggestion annotation source, independent of Notes' core-note source. - A type-aware render_block strip (gutenberg_strip_inline_suggestion_markers): a deletion unwraps but keeps its text (removed only on accept); an addition removes wrapper *and* text, so un-accepted proposed content never reaches the public front end. Overlapping byte ranges are merged so a deletion nested in an addition can't corrupt offsets. Raw post_content keeps the markers for reload. The format edit is inert for now; the interactive suggest-delete/add entry points and overlay wiring come with the suggest-mode integration phases. Tests: 6 format unit tests; 13 PHP strip cases (del keeps text, add drops text, wrappers removed, unrelated/note marks untouched, del-in-add and add-in-del nesting). Part of Phase 1 of the inline-suggestions plan.
Register the core/suggestion marker format so rich-text can round-trip a suggestion <mark> in block content and the annotations API can decorate it. Idempotent and guarded against duplicate registration the same way core/note is, using the rich-text store's getFormatType selector. Phase 1 defined the format but never registered it, so nothing could yet round-trip or decorate a marker; this is the foundation the Phase 2 wiring builds on.
Call registerSuggestionFormat() once when the editor provider module loads, alongside registerSuggestionOverlayFilter(). The format is intent-agnostic and idempotent, so registering it globally is safe and makes suggestion markers available to rich-text and the annotations API throughout the editor.
Add SuggestionAnnotations, the suggestion-mode counterpart to Notes' useAnnotateBlocks. It reads the post's unresolved suggestion threads, re-derives each core/suggestion marker's live range from the linked block's current content via findSuggestionRange (offsets are never stored, so a marker survives unrelated edits elsewhere in the block), and decorates the ranges through the annotations API. Mounted in the editor provider so markers are visible to everyone viewing the post, in any intent. The pure suggestionAnnotations helper is unit-tested. Adds a minimal canvas style keyed off the marker's data-suggestion-type; full author tinting and Revisions-aligned visuals follow in the plan's Phase 5. Phase 1 left the format unregistered with no decoration consumer; this is the decoration half of Phase 2 Slice A (registration landed in 649b29b).
Pure RichTextData transforms for resolving an inline deletion suggestion against a block attribute: - acceptInlineDeletion: remove both the marked text and its marker (the proposed removal becomes permanent). - rejectInlineDeletion: drop only the marker, keeping the text (the existing content is preserved). Both resolve the marker's range from the in-content marker on read via findSuggestionRange, so they stay correct after unrelated edits elsewhere in the value, and both no-op on non-rich-text values or a missing marker. These are the value-level half of Phase 2; the provider apply/reject wiring and the suggest-deletion action build on them. 9 unit tests, including the marker-shifted-by-an-unrelated-edit case.
Wire the inline-suggestion op type into the provider's apply/reject paths, sitting alongside the attribute-set and structural branches: - findInlineOp locates an inline-suggestion op in a payload. - applySuggestion resolves the marker by comment id and rewrites the one marked attribute: a deletion drops the marked text with its marker, an addition unwraps the marker so the text becomes permanent. The write bypasses the suggest-mode interceptor and rolls the attribute back if the status save fails. - rejectSuggestion restores the pre-suggestion content: a deletion keeps the text and drops the marker, an addition removes the proposed text. Resolves the target block via the same metadata.noteId scan apply uses. Inline ops carry no stored offsets; the range is re-derived from the in-content marker on read (acceptInlineDeletion/rejectInlineDeletion). Part of Phase 2; the suggest-deletion action that creates these markers follows. 3 findInlineOp unit tests; all 234 suggestion-mode + inline-suggestion tests pass.
Complete the inline-deletion loop with the creation entry point. In the suggest intent, a non-collapsed rich-text selection shows a Suggest deletion control (the core/suggestion format's edit). Clicking it persists a note-type suggestion carrying an inline-suggestion op, then wraps the selected range in a <mark class="wp-suggestion" data-suggestion-type="del"> keyed by the new comment id. The wrap is written straight to the block attribute with an interceptor bypass so the marker lands in content (where it syncs and survives reload) instead of the overlay. - registerSuggestionFormat now accepts an optional edit, so the generic inline-suggestions primitive stays UI-free while suggest mode supplies the marker-creating control. - INLINE_OP_TYPE is exported for the control to build the op. - buildSuggestionMarkerAttributes pins the marker attribute contract (id/type/author) in one tested place. Completes Phase 2 (deletions): create wraps a del marker, decoration shows it, apply removes text+marker, reject keeps text. 236 unit tests pass; the toolbar control itself needs e2e/manual verification in a running editor.
The suggestion-annotation ranges were memoized on [threads, getBlockAttributes], but getBlockAttributes is a stable selector reference, so the memo only recomputed when the thread list changed - never when block content changed. The suggest-delete flow creates the comment first (threads update, marker not yet in content -> no range) and then writes the marker into content (content changes, threads do not -> memo never reruns), so the decoration did not appear until an unrelated threads update or a remount (toggling the code editor). Re-derive the ranges inside useSelect via a reactive signature so they recompute when a tracked block's content changes, mirroring the moveSignature pattern in overlay-context.js. Notes is unaffected: it keeps a stored-offset fallback, while suggestion markers are content-only and must be re-derived reactively.
…button In Suggest mode every ordinary edit is already a suggestion, so deleting selected text should mark it as a proposed deletion - not require a special control. The Phase 2 'Suggest deletion' toolbar button was the wrong user flow. Remove the button (SuggestDeletionFormatEdit) and register the core/suggestion format inert. Add SuggestionDeletionKeyboard: a capture-phase keydown interceptor (mounted in Suggest mode) that, on Backspace/Delete over a non-collapsed selection inside a single rich-text attribute, prevents the removal and wraps the range in an in-content <mark data-suggestion-type="del"> marker keyed to a fresh suggestion note. Capture phase + preventDefault makes RichText's own delete handler bail (it checks event.defaultPrevented) and blocks the browser's contentEditable removal. The marker format, operations, accept/reject, decoration, and PHP strip are unchanged - only the creation trigger moved. Collapsed-cursor deletion and typing-over-selection still fall through to the existing path; they are handled when additions land (Phase 3).
Additions are the mirror of deletions: accepting an addition keeps the proposed text and drops the marker (== rejecting a deletion); rejecting an addition removes the text with its marker (== accepting a deletion). Factor the two underlying transforms into removeMarkedRange and unwrapMarker primitives and expose four clearly-named operations (accept/reject x deletion/addition) plus insertInlineAddition, a pure, id-agnostic helper that inserts proposed text wrapped in an add marker (collapsed range = caret insert, non-collapsed = type-over). Rewire the provider's inline apply/reject to the explicit addition functions instead of inverting the deletion ones, so the add path reads as what it does. Behaviour is unchanged; add unit tests covering the addition transforms, the add/del symmetry, and insertInlineAddition (insert, type-over, append, reversibility). This completes the accept/reject/strip half of additions. The creation trigger (typing in Suggest mode -> add marker, with run coalescing) is the remaining net-new piece and reuses the deletion interception path.
Verified in wp-env that the keydown approach let partial-selection deletes fall through: the marker never landed in content and the edit went into the old overlay path instead (text removed, suggest-mode-only, gone in edit mode). Browsers apply a selection delete through beforeinput, where preventDefault reliably cancels the edit; a keydown preventDefault does not. Listen for beforeinput (capture) on every candidate document (top doc + same-origin iframes, so the canvas iframe is covered), match any delete input type over a non-collapsed inline selection in a contentEditable, cancel it, and wrap the range in the in-content del marker. Confirmed: the <mark class="wp-suggestion" data-suggestion-type="del"> now lands in content and renders its strikethrough in both suggest and edit mode.
…kers The runtime annotation decoration nests a <mark class="annotation-text"> inside each inline suggestion marker. Suggestion threads are note-type comments, so NoteHighlightStyles tints that inner mark with the author's color by id, painting a translucent highlight box whose rounded edges read as stray vertical bars around the run and any trailing space. The marker's own strikethrough/underline is the intended visual, so the box is redundant; neutralize it with an !important rule that beats the per-thread id-specificity rule. Per-author tinting returns on the marker itself in the Phase 5 visual work.
In Suggest mode, typing now lands proposed text as an in-content core/suggestion add marker rather than as permanent content, mirroring the deletion trigger. A new SuggestionAdditionKeyboard intercepts beforeinput insertText (capture phase) at a collapsed caret, cancels the native insertion, and writes the marked text itself, advancing the caret via selectionChange. A contiguous run of typing grows a single marker: the first character opens the suggestion note (async; characters typed in that window buffer and flush together once the id resolves), and each subsequent character re-stamps the whole marker span so it stays one <mark> rather than fragmenting per keystroke (growInlineAddition). readInlineCaret reads a collapsed caret from selection state (readInlineSelection rejects one). Type-over (typing with a selection), IME composition, and paste are left to the existing path for now. Verified live in wp-env: typing produces a single add marker keyed to one note, rendered underlined/green, caret tracking correctly.
Typing with a non-collapsed selection in Suggest mode now proposes deleting the selected text (a del marker) and adding the typed replacement (an add run at the selection end), as two independent notes, instead of falling through to the overlay diff path. The addition keyboard reads the caret with readInlineCaret (collapsed or range): a range opens a deletion note, wraps the selection, then opens an addition note and inserts the replacement right after it (wrapping the selection in the del marker leaves the text length unchanged, so the selection end is still the insertion point). An early mid-creation guard buffers characters typed while either note request is in flight, regardless of selection state, so a fast type-over flushes cleanly once the ids land. Verified live: type-over a word produces adjacent del+add markers keyed to two notes.
A simple single-line paste in Suggest mode now becomes an add-marker suggestion like typed text. Paste is handled on the `paste` event in capture phase, ahead of the editor's own paste pipeline, and cancelled there (stopImmediatePropagation) — intercepting it at `beforeinput` `insertFromPaste` instead raced with the editor's paste handler and left an orphaned note while the marker write was clobbered. Multi-line / block-level paste is left to the editor's pipeline (no preventDefault). Typing, type-over, and paste now share one `insertText` router (buffer in-flight / grow contiguous / start a new run) over a `beginInsertion` helper that opens the note(s) and writes the marker(s), removing the duplicated del+add and single-add sequences. Verified live: paste yields one add marker keyed to one note, and continued typing grows the same marker.
Backspace and Delete at a caret (no selection) in Suggest mode now mark a character for deletion instead of removing it. Backspace marks the character before the caret and moves left; Delete marks the one after and holds the caret. Repeating in the same direction grows a single del marker (re-wrapping the whole span keeps it one <mark>); the first keystroke opens the note and repeats during that window are counted and applied on flush. A target character already inside a suggestion marker, and word/line deletes, fall through to the default path. This closes the last inline-text gap: typing, selection-delete, type-over, paste, and now single-character delete all produce markers, so the overlay diff path is reached only by formatting changes. Verified live: Backspace grows one del marker leftward; Delete marks forward as its own run.
Inline suggestion markers now carry their author's color. A new
SuggestionAuthorColors component injects one
`.wp-suggestion[data-author="N"]{--suggestion-author-color:<color>}`
rule per distinct author with an inline-suggestion thread, via
useStyleOverride so it reaches the iframed canvas. The marker rules in
content-suggestion.scss already consume that custom property (with the
semantic red/green as the fallback), so the strikethrough/underline keeps
conveying delete-vs-add while the color conveys who — the Google-Docs
model, and the #77869 Q1 authorship-follows-the-mark request.
The tint is keyed off the marker's own data-author attribute, so it
survives reload and a reviewer's view and no longer depends on a block
also carrying a structural suggestion (which was the only thing setting
the custom property before). Verified live: a clean deletion with no
structural marker tints to the author color (#D94145 for user 1).
Update the suggestions architecture doc for Option B: inline text changes (typing, delete, type-over, paste, collapsed-cursor delete) now live as anchored core/suggestion <mark> markers in block content, re-resolved on read via findMarkerRange, rather than whole-attribute overlay diffs. Adds an Inline suggestion markers section (marker shape, edit-driven creation table, decoration, type-aware PHP strip, accept/ reject, per-author tint), notes the overlay path now serves only non-text attribute and inline-formatting changes, lists the new files and the inline-markers/ + inline-suggestions/ directories, and updates the Overview and the resolved sub-attribute-anchoring limitation.
Rewrite the add/delete golden-path e2e tests for the Option B marker model and add type-over and collapsed-cursor-delete cases. Typing and deleting now produce in-content core/suggestion <mark> markers (visible immediately, no blur), so the tests assert mark.wp-suggestion[data-suggestion-type] and that the proposed text round-trips through the serialized post. The style (bold) golden path is unchanged — inline formatting still uses the overlay <ins>/<del> diff.
Mark phases 0-4 + Phase 5 tinting + Phase 6 docs/e2e complete; note the deferred follow-ups (formatting/IME/multi-line paste as markers, Revisions visual alignment, inverse marker-removal cleanup, undo/RTC e2e).
d42e861 to
64fe549
Compare
… Format Typing an addition (or deleting a selection) in Suggest mode produces an 'inline-suggestion' operation. Unlike an 'attribute-set' op it carries no before/after text — the proposed words live in the in-content marker, so the payload only records the op type and suggestion kind. summarizeOperations had no branch for it and fell through to the generic attribute handler, labelling the sidebar note 'Format: content' instead of describing the edit. Resolve the marker's text from the linked block's live content (the same way the canvas decoration derives the marker's range) and feed it into the summary so the note reads like a Google Docs review note: Add: "new text" or Delete: "old text". - inline-markers: add findMarkerText alongside findMarkerRange, sharing a single parse via parseMarkerValue/rangeInRecord. - inline-suggestions: add findSuggestionText wrapper. - suggestion-actions: resolve inline-suggestion text reactively (so the summary tracks the marker as contiguous typing grows it) before handing enriched operations to the pure summarizeOperations. - summarizeOperations: render inline-suggestion ops as Add:/Delete:, falling back to the attribute label only when the marker can no longer be resolved. Adds unit coverage for findMarkerText and the new summary branch, plus an e2e test reproducing the report: type before a word in Suggest mode and confirm the note summarizes 'Add: new text' rather than 'Format: content'. (cherry picked from commit 6c7f6f2)
Port the Option B assertion updates and the auto-save race fix from the combined branch. The 'auto-saves a content edit as a suggestion' test still carried the pre-Option-B expectation that a typed addition stays out of the serialized post, then waited for a /wp/v2/comments POST. Under the inline- marker model typing routes through SuggestionAdditionKeyboard, which bypasses the overlay interceptor: the addition round-trips inside a wp-suggestion add marker and its note POST fires during typing, so waitForResponse races it. Assert the race-free way instead: the in-content add marker is only written once its note id resolves, so a populated data-suggestion-id proves the suggestion persisted. Mirrors the golden-path inline tests.
…o try/inline-suggestions-phase2
…o try/inline-suggestions-phase2
…t Format: Typing only whitespace in Suggest mode produces an inline-suggestion op whose resolved text is all spaces. summarizeOperations required that text to survive a trim() and then ran it through ellipsize(), which collapses and trims whitespace — so a pure-whitespace edit resolved to an empty quote and fell through to the generic "Format: content" label. Render the marker text verbatim via a new clampText() helper that caps length without collapsing whitespace, and only fall back to the attribute label when no text resolves (non-string or empty op.text, i.e. the marker was edited away). A single added space now reads Add: " ". Covered by a unit case in suggestion-summary and an e2e regression in suggestion-mode.spec.js.
Member
Author
|
Fixed in |
adamsilverstein
added a commit
that referenced
this pull request
Jun 24, 2026
…tent Clicking the default block appender inserts an unmodified default paragraph via insertDefaultBlock. In Suggest mode that empty block incorrectly became an Insert block suggestion on its own. Guard the store interceptor's new-block branch with isUnmodifiedDefaultBlock so the insertion is only recorded once the block has content, and skip without recording a snapshot so a later fire re-enters the branch. Ports the fix already present on the combined branch (#78994) and the structural stack (#77971) to the inline-suggestions base so #79282 matches. Adds the matching e2e regression test.
…s-phase2 Cascades the trunk catch-up and the empty-inserted-block guard up from the base branch. Conflict resolutions mirror the combined branch (#78994): - lib/compat/wordpress-7.1/block-suggestions.php: union of the base's suggestion meta registration and this branch's inline-marker render-strip (meta + strip). - lib/load.php: add the 7.1 comment controller require. - editor CHANGELOG: keep the suggest-mode New Features and Bug Fixes entries above trunk's released 14.49.0 / 14.48.1 headers. - provider.js: point the payload-size doc reference at block-suggestions.php (where the meta now lives). After this merge the suggestion feature footprint is identical to the combined branch.
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.
What
Builds the inline-suggestion layer of Suggest mode on top of the reusable inline-
<mark>marker primitive from #78218 (Option B: suggested inline changes live as marked text in block content, not in an ephemeral overlay). This is the implementation of the plan indocs/superpowers/plans/2026-06-16-suggest-mode-inline-markers.mdand supersedes the bespokeinline-formats.jsdiff layer from #77869.It resolves Riad's "brittle before/after baseline" objection (#73411) and structurally dissolves the multi-author inline collision (#79220): each inline suggestion is an independent, author-tagged
<mark>resolved on read viafindMarkerRange, never a stored offset.Stacking
try/inline-suggestions-baseis the integration point that already has both (the Phase-0 merge of Notes: inline (partial-text) notes via hybrid marker + strip-on-render approach #78218 into the combined branch), so this PR's diff is only the inline-suggestion layer.What's here
inline-markers/primitive extracted from Notes (findMarkerRange,wrapInlineMarker,readInlineSelection,readInlineCaret,reconcileMarkerRemoval,useAnnotateRanges); Notes refactored to consume it with no behavior change.core/suggestionmarker format (<mark class="wp-suggestion" data-suggestion-id data-suggestion-type="del|add" data-author>), runtime-decorated via the annotations API, with a type-aware PHP render-strip (delkeeps text/drops wrapper;adddrops wrapper + text until accepted).delmarker (beforeinputinterception).addmarker; contiguous typing grows one marker.delon the selection +addfor the replacement.addmarker (handled on thepasteevent ahead of the editor's paste pipeline).acceptInlineDeletion/rejectInlineDeletion/acceptInlineAddition/rejectInlineAddition) wired throughprovider.js.Still on the overlay/diff path by design
Inline formatting changes (bold/italic/link) change a property of existing text, not add/remove it, so they keep using the existing overlay diff (
inline-formats.js). Collapsed-cursor single-char deletion and IME composition are follow-ups.Testing
render_blockstrip (del keeps text, add drops text, wrappers removed, raw/REST retains markers).🤖 Generated with Claude Code