diff --git a/packages/super-editor/src/core/extensions/keymap-history.test.js b/packages/super-editor/src/core/extensions/keymap-history.test.js index c9afa2420..419902518 100644 --- a/packages/super-editor/src/core/extensions/keymap-history.test.js +++ b/packages/super-editor/src/core/extensions/keymap-history.test.js @@ -1,4 +1,5 @@ import { describe, it, expect, afterEach } from 'vitest'; +import { TextSelection } from 'prosemirror-state'; import { closeHistory, undoDepth } from 'prosemirror-history'; import { initTestEditor } from '@tests/helpers/helpers.js'; import { handleEnter, handleBackspace, handleDelete } from './keymap.js'; @@ -100,6 +101,29 @@ describe('keymap history grouping', () => { expect(editor.state.doc.textContent).toBe('hello'); }); + it('collapses selection after undo so layout does not treat it as active range', () => { + ({ editor } = initTestEditor({ mode: 'text', content: '

Hello world

' })); + + // Select "Hello" + const from = 1; + const to = 6; + const sel = TextSelection.create(editor.state.doc, from, to); + editor.view.dispatch(editor.state.tr.setSelection(sel)); + + expect(editor.state.selection.from).toBe(from); + expect(editor.state.selection.to).toBe(to); + expect(editor.state.selection.empty).toBe(false); + + // Simple edit to create an undo step + editor.view.dispatch(editor.state.tr.insertText('!', to)); + + // Undo should both revert the content change and collapse selection + editor.commands.undo(); + + const selectionAfterUndo = editor.state.selection; + expect(selectionAfterUndo.empty).toBe(true); + }); + it('closeHistory before deletion creates its own undo step', () => { ({ editor } = initTestEditor({ mode: 'text', content: '

' })); diff --git a/packages/super-editor/src/extensions/custom-selection/custom-selection.js b/packages/super-editor/src/extensions/custom-selection/custom-selection.js index 23c1ab9a0..f6111437d 100644 --- a/packages/super-editor/src/extensions/custom-selection/custom-selection.js +++ b/packages/super-editor/src/extensions/custom-selection/custom-selection.js @@ -378,6 +378,14 @@ export const CustomSelection = Extension.create({ skipFocusReset: false, }), ); + + // Also clear editor-level preserved selection snapshots so that + // subsequent commands (linked styles, mark commands, etc.) don't + // resurrect an old selection after history undo/redo. + this.editor.setOptions({ + preservedSelection: null, + lastSelection: null, + }); } }, }, diff --git a/packages/super-editor/src/extensions/history/history.js b/packages/super-editor/src/extensions/history/history.js index cbc3ddf1a..b69c97c4c 100644 --- a/packages/super-editor/src/extensions/history/history.js +++ b/packages/super-editor/src/extensions/history/history.js @@ -1,7 +1,40 @@ // @ts-nocheck +import { TextSelection } from 'prosemirror-state'; import { history, redo as originalRedo, undo as originalUndo } from 'prosemirror-history'; import { undo as yUndo, redo as yRedo, yUndoPlugin } from 'y-prosemirror'; import { Extension } from '@core/Extension.js'; +import { CustomSelectionPluginKey } from '../custom-selection/custom-selection.js'; + +function createHistoryDispatch(editor, dispatch) { + if (!dispatch) return dispatch; + + return (historyTr) => { + let cleared = historyTr.setMeta(CustomSelectionPluginKey, { + focused: false, + preservedSelection: null, + showVisualSelection: false, + skipFocusReset: false, + }); + + const sel = cleared.selection; + if (sel && sel instanceof TextSelection && !sel.empty) { + const headPos = typeof sel.head === 'number' ? sel.head : sel.to; + try { + const collapsed = TextSelection.create(cleared.doc, headPos); + cleared = cleared.setSelection(collapsed); + } catch { + // Ignore collapse failures and fall back to original selection + } + } + + editor.setOptions({ + preservedSelection: null, + lastSelection: null, + }); + + dispatch(cleared); + }; +} /** * Configuration options for History @@ -55,7 +88,8 @@ export const History = Extension.create({ return yUndo(state); } tr.setMeta('inputType', 'historyUndo'); - return originalUndo(state, dispatch); + const wrappedDispatch = createHistoryDispatch(this.editor, dispatch); + return originalUndo(state, wrappedDispatch); }, /** @@ -71,7 +105,8 @@ export const History = Extension.create({ return yRedo(state); } tr.setMeta('inputType', 'historyRedo'); - return originalRedo(state, dispatch); + const wrappedDispatch = createHistoryDispatch(this.editor, dispatch); + return originalRedo(state, wrappedDispatch); }, }; },