Skip to content

feat: implement undo/redo functionality#11

Merged
seancdavis merged 5 commits intomainfrom
feat/implement-undo-redo-functionality
Mar 10, 2026
Merged

feat: implement undo/redo functionality#11
seancdavis merged 5 commits intomainfrom
feat/implement-undo-redo-functionality

Conversation

@rocktree29
Copy link
Contributor

Summary

Add undo/redo support to the text editor using standard keyboard shortcuts, with intelligent history grouping that matches the behavior of editors like VS Code.

Changes

  • useUndoHistory.ts — new hook managing a history stack with commit, scheduleCommit, undo, and redo operations
  • Editor.tsx — integrates undo/redo: intercepts Cmd/Ctrl+Z and Cmd/Ctrl+Shift+Z, wraps shortcut actions and pastes as discrete undo groups, and debounces regular typing into logical chunks
  • Editor.undo.test.tsx — 8 tests covering undo/redo of typed chunks, multi-group navigation, mid-type undo, bold shortcut undo, paste undo, redo, and redo-stack clearing

PRD Requirements

  • Cmd+Z / Ctrl+Z undoes the most recent history group
  • Cmd+Shift+Z / Ctrl+Shift+Z redoes the last undone group
  • Continuous typing is grouped by a 500ms debounce — undo reverses a chunk, not a character
  • Cmd+B (bold) and other shortcut actions are pushed as discrete undo groups
  • Paste is committed as its own undo group via onPaste interception
  • Redo restores content correctly after undo
  • New edits after undo clear the redo stack

Testing

All 40 tests pass (npm test in packages/editor). New tests cover each acceptance criterion using @testing-library/react with vi.useFakeTimers() to control debounce timing.

Closes #7

Add undo/redo support with Cmd+Z / Ctrl+Z (undo) and Cmd+Shift+Z / Ctrl+Shift+Z (redo).

- useUndoHistory hook manages a history stack with debounced grouping (500ms) for regular typing
- Shortcut actions (Cmd+B, Tab, Enter, etc.) are committed as discrete undo groups immediately
- Paste operations are committed as their own undo group via onPaste interception
- New edits after an undo clear the redo stack (standard behavior)
- Uncommitted typing is flushed to history before undo so it can be redone

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@netlify
Copy link

netlify bot commented Mar 10, 2026

Deploy Preview for rt-ash canceled.

Name Link
🔨 Latest commit f38219e
🔍 Latest deploy log https://app.netlify.com/projects/rt-ash/deploys/69b04f29ca9f150008ce55da

@rocktree29
Copy link
Contributor Author

QA Review (Round 1) — CHANGES_REQUESTED

Reviewer: Codex (automated)

Summary

Undo/redo is partially implemented, but key PRD behaviors are not fully met: redo shortcut handling is fragile, redo clearing after undo is delayed for typed edits, and shortcut-boundary grouping is incomplete for non-paste/native edit shortcuts.

Issues

  • [major] packages/editor/src/Editor.tsx: Undo/redo detection is case-sensitive (e.key === 'z' at lines 100 and 115). For Ctrl/Cmd+Shift+Z, browsers commonly emit key: 'Z', so redo may not trigger. Normalize key comparisons (e.g., e.key.toLowerCase() === 'z') for both undo and redo paths.
  • [major] packages/editor/src/useUndoHistory.ts: Redo stack is only cleared on commit() (line 27), but normal typing uses debounced scheduleCommit() (line 37). After undo, a user can type and press redo before debounce fires, and stale redo is still available, violating the requirement that any new edit after undo clears redo. Clear forward history immediately on first post-undo edit (e.g., a dedicated markEdited()/clearRedo() call from onChange before debounced commit).
  • [major] packages/editor/src/Editor.tsx: The PRD requires any keyboard shortcut that modifies content to be tracked as an immediate discrete undo group. Current code only does this for internal shortcuts (Cmd/Ctrl+B, Cmd/Ctrl+I, Tab/Shift+Tab/Enter via apply) and paste (onPaste). Other shortcut-driven edits (e.g., Cmd/Ctrl+X cut, OS/browser transform shortcuts) fall through to debounced typing history. Add explicit boundary commits for additional shortcut edit events (at least onCut, and preferably beforeinput/inputType-based handling) so each shortcut edit is its own undo group.
  • [minor] packages/editor/src/__tests__/Editor.undo.test.tsx: Tests do not cover several acceptance-critical paths: macOS metaKey bindings, uppercase Z redo key events, immediate redo-disable after undo+new typed edit (without waiting debounce), and selection restoration after undoing Cmd/Ctrl+B. Add targeted tests for these cases to prevent regressions.

- Normalize key comparison with .toLowerCase() so Ctrl/Cmd+Shift+Z works
  regardless of whether the browser emits 'z' or 'Z'
- Accept both metaKey and ctrlKey as the modifier so macOS Cmd shortcuts
  work in all environments
- Clear redo stack immediately in scheduleCommit so typing after undo
  disables redo before the debounce fires
- Add onCut handler that commits a boundary entry, making cut its own
  undo group (mirrors the existing onPaste handling)
- Include selection state in the commit duplicate-entry check so
  pre-shortcut selection is preserved for correct restoration on undo
- Add tests: metaKey bindings, uppercase Z redo, immediate redo-clear
  after undo+new edit, selection restoration after Ctrl+B

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@rocktree29
Copy link
Contributor Author

QA Review (Round 2) — APPROVED

Reviewer: Codex (automated)

Summary

The implementation in packages/editor satisfies the PRD for undo/redo: required shortcuts are implemented for macOS/Windows/Linux, typing is grouped via debounce (500ms), shortcut-based edits (including Cmd/Ctrl+B and paste) are tracked as discrete undo groups, redo works correctly, and redo history is cleared on new edits after undo. Tests in Editor.undo.test.tsx cover all acceptance criteria directly or equivalently.

Issues

No issues found.

@seancdavis
Copy link
Member

Fix conflicts and then we are good to go.

rocktree29 and others added 2 commits March 10, 2026 11:15
- Resolve import conflict: combine applyLink/applyLinkPaste from main
  with useUndoHistory from undo/redo branch
- Merge duplicate handlePaste definitions: unify link-paste handling
  (from main) with undo history tracking (from undo/redo branch)
- Guard e.clipboardData?.getData() against missing clipboardData
  to fix test regression
- Retain onCut handler and undo/redo keyboard shortcuts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@rocktree29
Copy link
Contributor Author

QA Review (Round 3) — CHANGES_REQUESTED

Reviewer: Codex (automated)

Summary

Undo/redo core behavior is largely implemented correctly (including Cmd/Ctrl+Z, Cmd/Ctrl+Shift+Z, debounce grouping, redo clearing, and Cmd/Ctrl+B + paste coverage), but one PRD requirement is not fully met for keyboard-shortcut-driven content edits beyond explicitly handled shortcuts.

Issues

  • [major] packages/editor/src/Editor.tsx: The PRD requires that any keyboard shortcut that modifies content be recorded as an immediate discrete undo group (no debounce). In handleChange, all non-paste changes go through debounced history.scheduleCommit(...), so native keyboard editing shortcuts that mutate text (for example, Ctrl/Alt+Backspace-style word deletion and other browser/OS editing shortcuts not intercepted in onKeyDown) are grouped as typing instead of committed immediately. This can merge distinct shortcut actions into one undo chunk and violates the shortcut-boundary requirement. Fix by detecting shortcut-originated input in onChange (e.g., via InputEvent.inputType/beforeinput metadata or an explicit shortcut-action flag) and calling immediate history.commit for those changes.
  • [minor] packages/editor/src/__tests__/Editor.undo.test.tsx: Tests validate Ctrl/Cmd+Z, Ctrl/Cmd+Shift+Z, Cmd/Ctrl+B, and paste undo/redo, but they do not cover the broader PRD requirement that other keyboard shortcuts which modify content are discrete undo groups. Add tests for at least one additional keyboard-driven mutation path outside the explicit custom handlers (or a generalized shortcut-originated change path) to prevent regressions.

@seancdavis
Copy link
Member

Just needed a QA reset. Go again!

Detect OS/browser editing shortcuts (deleteWordBackward, deleteWordForward,
deleteSoftLine*, deleteHardLine*, deleteByDrag) via onBeforeInput and commit
the pre-change state immediately, so each shortcut action forms its own undo
group instead of being merged with surrounding typing via the debounce.

Add tests covering Ctrl+Backspace and Ctrl+Delete word-deletion paths.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@rocktree29
Copy link
Contributor Author

QA Review (Round 1) — APPROVED

Reviewer: Codex (automated)

Summary

The implementation satisfies the PRD for undo/redo: Cmd/Ctrl+Z and Cmd/Ctrl+Shift+Z are wired correctly, typing is grouped via 500ms debounce, shortcut-driven edits (including Cmd/Ctrl+B and paste flows) are tracked as discrete history groups, redo is cleared after new edits post-undo, and tests cover the required behaviors including selection restoration for bold undo. No blocking correctness, security, quality, or performance issues were found in the reviewed code.

Issues

No issues found.

@seancdavis seancdavis merged commit bc26a24 into main Mar 10, 2026
4 checks passed
@seancdavis seancdavis deleted the feat/implement-undo-redo-functionality branch March 10, 2026 17:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement undo/redo functionality

2 participants