Skip to content

Suggest mode: inline text and formatting suggestions via markers#79714

Open
adamsilverstein wants to merge 75 commits into
suggest/corefrom
suggest/inline
Open

Suggest mode: inline text and formatting suggestions via markers#79714
adamsilverstein wants to merge 75 commits into
suggest/corefrom
suggest/inline

Conversation

@adamsilverstein

@adamsilverstein adamsilverstein commented Jun 30, 2026

Copy link
Copy Markdown
Member

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-markers primitive (the same mechanism behind inline Notes):

  • inline-markers/ primitive + inline-suggestions/ format, operations, and
    pure plan/diff engines (reconcile-edit.js, reconcile-format.js).
  • Typing, word/line deletion, cut, and single-line paste are caught on
    beforeinput / cut / paste by the suggestion keyboards, which
    preventDefault and write markers directly (no onChange).
  • The remaining onChange seams (committed IME composition, autocorrect /
    insertReplacementText, drag-drop, multi-line paste) are diffed into markers
    by a singleton content reconciler.
  • Formatting toggles (toolbar, shortcut, link popover) are detected at the
    block's setAttributes seam and handed to a singleton format handler that
    wraps the run in one format-type marker (Google Docs model: the text shows
    once, styled in place).
  • Both singletons serialize their note-then-marker writes per block through a
    shared write queue and re-validate live content around the async note POST.
  • block-suggestions.php strips un-accepted markers type-aware on the front end.
  • Mounts + the core/suggestion format registration are added inside the
    experiment 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-markers refactor of the Notes hooks/utils is intentionally dropped (the
suggestion code uses the inline-markers primitive directly).

Testing

  1. On top of Suggest mode: core subsystem (intent modes, overlay capture, structural + attribute suggestions) #79713, enable the Suggestion Mode experiment and switch the editor
    to Suggesting via the more menu.
  2. Type into a paragraph → the typed run appears as an addition marker.
  3. Select a word and delete or cut it → it becomes a deletion marker.
  4. Select a word and press Cmd/Ctrl+B → the run wraps in a
    single format marker, shown bold in place.
  5. Change a heading's level via the block switcher → the block shows the
    pending-suggestion bracket (attribute path), and auto-save records it.
  6. Confirm undo/redo and (in an RTC session) a second peer both see the markers
    as ordinary content.

Combined Playground preview: #78994.

Automated coverage

  • Unit: suggestion-mode, inline-suggestions (engines), inline-markers, and
    attribute-suggestions suites — 451 tests.
  • E2E: suggestion-mode.spec.js golden paths and the
    suggestion-mode-overlay-retirement.spec.js safety net, which asserts the
    core invariant directly: a block never carries both an inline marker and an
    overlay content diff.

@github-actions

github-actions Bot commented Jun 30, 2026

Copy link
Copy Markdown

Size Change: +9.59 kB (+0.12%)

Total Size: 7.7 MB

📦 View Changed
Filename Size Change
build/scripts/editor/index.min.js 513 kB +9.05 kB (+1.79%)
build/styles/block-editor/content-rtl.css 6.16 kB +138 B (+2.29%)
build/styles/block-editor/content-rtl.min.css 4.61 kB +132 B (+2.95%)
build/styles/block-editor/content.css 6.16 kB +136 B (+2.26%)
build/styles/block-editor/content.min.css 4.6 kB +133 B (+2.98%)

compressed-size-action

@adamsilverstein adamsilverstein added the [Type] Feature New feature to highlight in changelogs. label Jun 30, 2026
@adamsilverstein adamsilverstein self-assigned this Jun 30, 2026
@adamsilverstein adamsilverstein added the [Status] In Progress Tracking issues with work in progress label Jun 30, 2026
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.
@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown

Flaky tests detected in 2e22f19.
Some tests passed with failed attempts. The failures may not be related to this commit but are still reported for visibility. See the documentation for more information.

🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/28953203014
📝 Reported issues:

…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.
@adamsilverstein adamsilverstein changed the title Suggest mode: inline text suggestions via the marker primitive Suggest mode: inline text and formatting suggestions via markers Jul 2, 2026
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Package] Block editor /packages/block-editor [Package] Editor /packages/editor [Status] In Progress Tracking issues with work in progress [Type] Feature New feature to highlight in changelogs.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant