Suggest mode: inline text and formatting suggestions via markers#79714
Open
adamsilverstein wants to merge 75 commits into
Open
Suggest mode: inline text and formatting suggestions via markers#79714adamsilverstein wants to merge 75 commits into
adamsilverstein wants to merge 75 commits into
Conversation
This was referenced Jun 30, 2026
|
Size Change: +9.59 kB (+0.12%) Total Size: 7.7 MB 📦 View Changed
|
86a3ed3 to
61b9e76
Compare
61b9e76 to
45a17fd
Compare
Stacks the inline (in-text) suggestion layer on the Suggest mode core. Inline add/delete/replace suggestions live in block content as <mark class="wp-suggestion"> markers, built on the reusable inline-markers primitive (shared with inline Notes), rather than an ephemeral overlay: - New inline-markers/ primitive and inline-suggestions/ format + operations. - SuggestionAdditionKeyboard / SuggestionDeletionKeyboard capture typing and deletion as add/delete markers, with per-author tinting (suggestion-author- colors). annotate-suggestions renders them on the canvas. - block-suggestions.php strips un-accepted markers on the front end. - Mounts and the core/suggestion format registration are added inside the experiment gate from the core branch. Notes keeps trunk's self-contained inline implementation; the feature's earlier inline-markers refactor of the Notes hooks/utils is intentionally dropped (the suggestion code uses the inline-markers primitive directly). Replaces #79282.
45a17fd to
6b82582
Compare
This was referenced Jun 30, 2026
|
Flaky tests detected in 2e22f19. 🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/28953203014
|
…safety net Local reproduction (probe-instrumented build) showed the multi-line paste never reaches the content reconciler: the paste pipeline commits the merged value to the block-editor store directly rather than through the block's setAttributes prop, so the store interceptor captures it - reverting the store to baseline and diverting the pasted value into the attribute overlay as a whole-attribute suggestion. The e2e asserted aspirational reconciler behavior (inline add markers) and failed on every CI attempt. Pin what actually holds: the paste is never committed raw (store and serialized content stay at baseline), never rendered as an overlay inline diff, the suggester sees their text live via the overlay merge with the attribute-pending treatment, and no inline markers appear. Update the architecture doc's seam tables to match, and note the marker-conversion follow-up.
The addition and deletion keyboards derived marker offsets from the
block-editor store selection (readInlineCaret/readInlineSelection). The
store selection is synced from the DOM asynchronously: after any marker
write re-renders RichText (e.g. the format keyboard on primary+b)
followed by caret movement, a fast typist's beforeinput fires before the
store's selectionChange sync, so the marker landed at stale offsets.
That corrupts content - e.g. typing ' more' after End landed the add
marker mid-word at the stale offset, splitting an existing format
marker into two mark fragments and dropping the leading space.
Fix: keep the store selection only for block/attribute identification
(the isEventTargetSelectedRichText guard already validates the event
target against it) and derive START/END offsets from the DOM at input
time via a new readEventRange( event ) helper in keyboard-target.js:
- event.getTargetRanges()[0] (Input Events spec: the exact range the
input will affect) when available;
- otherwise the live DOM selection (paste/cut expose no target ranges;
deletion passes preferTargetRanges: false because a delete's target
range is pre-expanded to the removed text, while collapsed-delete run
growth needs the triggering caret).
The range maps to rich-text offsets with the same call RichText and the
writing-flow selection observer use for DOM ranges:
create( { element, range, __unstableIsEditableTree: true } ). create()
only reads the four container/offset properties, so a StaticRange works
interchangeably with a live Range. When no DOM range resolves the
keyboards fall back to the store offsets (previous behavior), and
multi-block/cross-attribute selections still bail to the native path.
Grapheme clamping still applies on top of the DOM offsets.
…rlay-retire # Conflicts: # packages/editor/src/components/suggestion-mode/suggestion-deletion-keyboard.js
The format keyboard's marker write is asynchronous (a note POST precedes it). Writing the marker re-renders RichText, which re-applies the STORE selection to the DOM — but the user may have moved the caret during that gap, and the store's selectionchange sync lags the DOM. The restore then clobbered the live caret, re-selecting the formatted run, so a fast typist's next keystroke typed over the marker instead of inserting at their caret. Add readLiveInlineSelection( clientId, attributeKey ) to keyboard-target.js: it maps the live DOM selection to rich-text offsets (same create()-based mapping as readEventRange) only when it sits inside the given block's rich text, and the format keyboard now dispatches selectionChange with those offsets alongside its write so the re-render restores the caret to where the user actually is. A format plan never changes the text, so the DOM offsets map 1:1 onto the marked value. When the live selection is elsewhere (sidebar focus, no selection) the store selection is left alone, as before.
Reproduces the store-selection-lag corruption fixed on suggest/inline: format marker on 'world' (primary+b), collapse the caret, then an immediate ' more'. Before the fix the add marker landed at stale store offsets, splitting the format marker into two fragments mid-word and dropping the leading space. Asserts the add marker holds exactly ' more', the paragraph reads 'Hello world more', and the format marker stays one unfragmented 'world' span (exactly two markers on the block). Also switch the caret collapse in this test and the invariant test from End to ArrowRight: on macOS Chromium, End is a no-op on a non-collapsed selection (it only moves a collapsed caret), so End left 'world' selected and the typing became a type-over of the format marker — the reason the invariant test failed on macOS while CI (Linux, where End collapses) passed. ArrowRight collapses the DOM selection synchronously on every platform while the store selection sync lags, so the test deterministically exercises the stale-offset window the fix closes.
# Conflicts: # packages/editor/src/components/suggestion-mode/with-suggestion-overlay.js
…marker path Typing into a freshly-inserted paragraph opened an inline "Add" note on the first keystroke, whose metadata.noteId write then made the deferred empty block look modified — so the interceptor registered a second, structural "Insert block" note for the same action (two notes to review for one edit). The addition and deletion keyboards now fall through to the native edit when the caret sits inside a block that is a pending insertion (or is nested in one, or is a deferred empty placeholder about to become one). The native edit writes through to the real block — the overlay HOC and store interceptor recognize both states since the suggest/core side of this fix — so the whole block stays ONE "Insert block" suggestion. Adds an e2e regression test asserting a single "Insert block:" note and no inline add marker when typing into a new paragraph in Suggest mode.
…rlay-retire # Conflicts: # packages/editor/src/components/suggestion-mode/overlay-context.js # packages/editor/src/components/suggestion-mode/with-suggestion-overlay.js
Brings in the PR1 slim-down (word-diff.js extraction replacing the unused SuggestionDiff component). Doc conflicts resolved to the marker-era text; the wrapper-level author-color paragraph is preserved since the HOC still applies it.
The review-flows spec from the core layer covers structural and attribute suggestions; this layer adds the inline capture paths, so extend the spec with accept/reject coverage for the marker kinds: a typed addition (unwrapped on accept, text dropped on reject), a deletion (text removed on accept, restored on reject), and a bold format run (formatting kept on accept, original run restored on reject), asserting the canvas, the serialized content, and the note's resolved state for each.
…istence Close three coverage gaps in the user flows: a single-line paste becomes an add marker; creating a suggestion switches an open settings sidebar to All notes; a typed inline addition survives save + reload and can still be accepted.
# Conflicts: # packages/editor/src/components/provider/index.js # packages/editor/src/components/suggestion-mode/index.js
Extend the note garbage collector with the inline anchor kind: a suggestion note whose marker has left block content (Ctrl+Z, or deleting the marked text outside Suggest mode) is trashed, and an inline marker restored by redo gets its note back — keeping every marker resolvable. Type-over replacements now compose the deletion marker and the addition run into a single content write. Written separately they occupied two undo levels, so Ctrl+Z peeled off the proposed text but left the strikethrough behind; one gesture must withdraw as one unit.
Add the inline kinds to the undo spec — typed additions, suggested deletions, type-over replacements, and format suggestions all revert with their note removed, and repeated undo withdraws suggestions newest-first across kinds. The older typed-marker undo test now expects redo to restore the marker (with its note), which the undo guard and note garbage collector made possible; it previously pinned the orphaning behavior as a known limitation.
…ng instructions Audit every bullet of the testing instructions on the tracking issue and add e2e tests for the inline-layer items that had no coverage: - Repeated Backspace grows a single del marker (one note, not one per keystroke), and the note summarizes as "Delete:". - Backspace over an emoji ZWJ sequence marks the whole grapheme. - Cut still copies: pasting elsewhere reproduces the cut text. - Italic and link toggles each wrap the run in one format marker. - A committed IME composition becomes an add marker via the content reconciler, driven through CDP (Input.imeSetComposition), which can compose where Playwright's keyboard API cannot. - A delete straddling an existing marker never corrupts it: no nested marks, the straddling edit falls back to an attribute suggestion, and the original suggestion serializes intact. - A closed sidebar stays closed when a suggestion is created (the auto-switch only retargets an already-open non-notes sidebar). - With multiple suggesters, markers carry data-author and resolve distinct per-author tints. - Front-end strip is type-aware end to end: pending additions never render, pending deletions and format runs render their real text, and no marker wrappers leak.
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
The inline (in-text) suggestion layer for Suggest mode, stacked on the core
(#79713). Replaces #79282 and absorbs the former overlay-retirement PR (#79752),
so the overlay never renders inline content diffs at any point in the stack.
Map and rationale: #73411.
Every inline text and formatting change becomes a
<mark class="wp-suggestion">marker in block content by construction — edit-resilient, per-author, and
re-resolved against current content on read — built on the shared
inline-markersprimitive (the same mechanism behind inline Notes):inline-markers/primitive +inline-suggestions/format, operations, andpure plan/diff engines (
reconcile-edit.js,reconcile-format.js).beforeinput/cut/pasteby the suggestion keyboards, whichpreventDefaultand write markers directly (noonChange).onChangeseams (committed IME composition, autocorrect /insertReplacementText, drag-drop, multi-line paste) are diffed into markersby a singleton content reconciler.
block's
setAttributesseam and handed to a singleton format handler thatwraps the run in one
format-type marker (Google Docs model: the text showsonce, styled in place).
shared write queue and re-validate live content around the async note POST.
block-suggestions.phpstrips un-accepted markers type-aware on the front end.core/suggestionformat registration are added inside theexperiment gate from Suggest mode: core subsystem (intent modes, overlay capture, structural + attribute suggestions) #79713.
Primitive attribute changes (heading level, alignment, color) still route
through #79713's overlay, which serves purely as the clean-value store that
auto-save reads to build the suggestion's operations — it never renders inline
content diffs. A rare ambiguous content edit (e.g. one straddling an existing
marker) falls back to that overlay path too: rendered plain-merged with the
block bracket, still stored and saved as a suggestion.
Notes keeps trunk's self-contained inline implementation; the feature's earlier
inline-markersrefactor of the Notes hooks/utils is intentionally dropped (thesuggestion code uses the
inline-markersprimitive directly).Testing
to Suggesting via the more menu.
single format marker, shown bold in place.
pending-suggestion bracket (attribute path), and auto-save records it.
as ordinary content.
Combined Playground preview: #78994.
Automated coverage
suggestion-mode,inline-suggestions(engines),inline-markers, andattribute-suggestionssuites — 451 tests.suggestion-mode.spec.jsgolden paths and thesuggestion-mode-overlay-retirement.spec.jssafety net, which asserts thecore invariant directly: a block never carries both an inline marker and an
overlay content diff.