Skip to content

[lexical-history] Feature: Add canUndo and canRedo ReadonlySignals to HistoryExtension output#8465

Merged
etrepum merged 9 commits into
facebook:mainfrom
etrepum:claude/add-undo-redo-signals-xmrwy
May 7, 2026
Merged

[lexical-history] Feature: Add canUndo and canRedo ReadonlySignals to HistoryExtension output#8465
etrepum merged 9 commits into
facebook:mainfrom
etrepum:claude/add-undo-redo-signals-xmrwy

Conversation

@etrepum

@etrepum etrepum commented May 5, 2026

Copy link
Copy Markdown
Collaborator

Description

Adds canUndo: ReadonlySignal<boolean> and canRedo: ReadonlySignal<boolean> to HistoryExtension's output, kept in sync via CAN_UNDO_COMMAND and CAN_REDO_COMMAND listeners. Signals reset to false when the extension is disabled.

The CAN_UNDO_COMMAND and CAN_REDO_COMMAND are hard to keep in sync because it's tricky to read the initial state of them, it's better to have signals that directly expose the correct values.

Test plan

New unit tests

claude added 4 commits May 5, 2026 18:55
Adds `canUndo: ReadonlySignal<boolean>` and `canRedo: ReadonlySignal<boolean>`
to `HistoryExtension`'s output, kept in sync via `CAN_UNDO_COMMAND` and
`CAN_REDO_COMMAND` listeners. Signals reset to `false` when the extension is
disabled.

https://claude.ai/code/session_01XeSGvEsnDrdbyLJBGUNYT8
- Convert HistoryExtensionOutput from type alias to exported interface with
  JSDoc on every field
- Add internal HistoryExtensionInit interface; writable Signal<boolean> pairs
  are created in the new `init` phase (one pair per editor, no singleton
  mutation) and accessed by both `build` and `register` via getInitResult()
- `build` exposes them as ReadonlySignal<boolean> via computed() — no `as`
  casts needed anywhere
- `register` reads canUndo/canRedo from historyState stacks directly
  (undoStack.length > 0 / redoStack.length > 0) on CAN_UNDO_COMMAND /
  CAN_REDO_COMMAND; the commands are still used as efficient change triggers
  but the signal value is derived from the authoritative HistoryState source
- Import computed and signal from @lexical/extension
- Update Flow types (LexicalHistory.js.flow) to reflect the new output
  interface and import ReadonlySignal / Signal
- Add six unit tests covering initial state, edit, undo, redo, clear, and
  new-edit-clears-redo scenarios

https://claude.ai/code/session_01XeSGvEsnDrdbyLJBGUNYT8
…ests

When historyState is reassigned (e.g. SharedHistoryExtension inheriting the
parent's state) the effect re-runs but previously no command was dispatched,
leaving canUndo/canRedo stale. Fix: read undoStack/redoStack lengths at the
start of the effect so signals are always in sync with whatever historyState
is currently assigned.

Add three tests for the pre-populated case:
- Initialised with a non-empty undoStack via createInitialHistoryState
- Initialised with a non-empty redoStack via createInitialHistoryState
- historyState signal reassigned at runtime to a populated HistoryState

https://claude.ai/code/session_01XeSGvEsnDrdbyLJBGUNYT8
@vercel

vercel Bot commented May 5, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
lexical Ready Ready Preview, Comment May 5, 2026 9:28pm
lexical-playground Ready Ready Preview, Comment May 5, 2026 9:28pm

Request Review

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label May 5, 2026
- HistoryExtensionOutput: put canRedo before canUndo (sort-keys-fix rule)
- Flow type: same key order fix
- build(): expand namedSignals({...}) onto multiple lines (was 95 chars)
- Tests: put createInitialHistoryState before delay in configExtension
  objects (sort-keys-fix rule)

https://claude.ai/code/session_01XeSGvEsnDrdbyLJBGUNYT8
…a callback

registerHistory now accepts an optional callback that is invoked once on
registration and again after every mutation of historyState (push, pop, clear).
HistoryExtension uses this callback to keep canUndo/canRedo signals in sync
with the current HistoryState, so the signals are computed directly from the
authoritative source with no dependency on commands.

Removes the previous CAN_UNDO_COMMAND/CAN_REDO_COMMAND listeners. The callback
fires on initialization, which also covers the SharedHistoryExtension case
where historyState may be pre-populated when the child editor is created.

Tests:
- Two updates are required to put an entry on undoStack (first sets current,
  second pushes). makeEditorWithOneUndoEntry helper does both updates.
- The "new edit clears redoStack after undo" test wraps the UNDO dispatch in
  editor.update() so the HISTORIC_TAG from undo's setEditorState does not
  leak into the subsequent edit (which would otherwise discard it).

Verified locally with pnpm run test-unit and pnpm run ci-check (tsc,
tsc-extension, tsc-website, flow, prettier, lint) -- all pass.

https://claude.ai/code/session_01XeSGvEsnDrdbyLJBGUNYT8
…g\` in tests

- Move onHistoryStateChange invocation out of the UNDO/REDO/CLEAR_EDITOR/
  CLEAR_HISTORY command handlers and into the undo / redo / clearHistory
  helper functions themselves, alongside the mutation. The command handlers
  are now thin pass-throughs containing no signal-related code.
- Wrap the canUndo / canRedo writes in HistoryExtension's callback (and the
  disabled-reset path) in batch() so subscribers to both signals are only
  notified once per change.
- Switch all editor variables in the new tests to \`using\` declarations so
  editors are eagerly disposed at end of scope, matching the pattern used by
  other lexical-extension / lexical-react unit tests.

Verified locally with pnpm run test-unit (3045 passed) and pnpm run ci-check
(tsc, tsc-extension, tsc-website, flow, prettier, lint -- all pass).

https://claude.ai/code/session_01XeSGvEsnDrdbyLJBGUNYT8
… effect

These reads are outside any reactive context, so peek() is the appropriate
accessor — it returns the current value without establishing a subscription.

The single remaining .value occurrence is the writable assignment
\`dep.output.historyState.value = populated\` which must stay as a setter.

Verified locally with pnpm run test-unit and pnpm run ci-check.

https://claude.ai/code/session_01XeSGvEsnDrdbyLJBGUNYT8
- Stop wrapping canUndo/canRedo in computed() inside HistoryExtension.build.
  Signal<T> already extends ReadonlySignal<T>, so the explicit
  HistoryExtensionOutput return-type annotation is enough to expose them as
  readonly to consumers — the computed() layer added nothing.
- Drop the now-unused \`computed\` import from @lexical/extension.
- Hoist a single syncFromHistoryState callback in HistoryExtension.register
  that accepts \`HistoryState | null\`. Passing null resets both signals
  (used in the disabled branch); passing a HistoryState derives them from
  its stacks (used by registerHistory on init and after every mutation).
  Eliminates the duplicated batch/reset block.

Verified locally with pnpm run test-unit and pnpm run ci-check.

https://claude.ai/code/session_01XeSGvEsnDrdbyLJBGUNYT8
@etrepum etrepum added the extended-tests Run extended e2e tests on a PR label May 5, 2026
@etrepum etrepum added this pull request to the merge queue May 7, 2026
Merged via the queue into facebook:main with commit c3a9ab5 May 7, 2026
44 checks passed
@etrepum etrepum deleted the claude/add-undo-redo-signals-xmrwy branch May 19, 2026 17:55
@etrepum etrepum mentioned this pull request May 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. extended-tests Run extended e2e tests on a PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants