From 6fb7f51ea244f46c7d8005a6ce30bc272a4e0abf Mon Sep 17 00:00:00 2001 From: Saul-Mirone Date: Mon, 12 May 2025 01:50:57 +0000 Subject: [PATCH] refactor(editor): history as a store extension (#12214) ## Summary by CodeRabbit - **Refactor** - Improved history and undo/redo management across the app by introducing a dedicated history extension. Undo/redo operations now use a more focused undo manager, resulting in clearer and more consistent behavior. - **Documentation** - Updated API documentation to reflect changes in history management, including revised method signatures and removal of outdated event references. --- .../affine/blocks/code/src/code-block.ts | 2 +- .../affine/blocks/list/src/list-block.ts | 2 +- .../blocks/paragraph/src/paragraph-block.ts | 2 +- .../fragments/doc-title/src/doc-title.ts | 2 +- .../src/services/notification-service.ts | 8 +- .../src/services/toolbar-service/context.ts | 2 +- .../api/@blocksuite/store/classes/Store.md | 4 +- .../store/interfaces/StoreSlots.md | 8 -- .../extension/history/history-extension.ts | 79 +++++++++++++++++++ .../store/src/extension/history/index.ts | 1 + .../framework/store/src/extension/index.ts | 1 + .../selection/selection-extension.ts | 10 ++- .../framework/store/src/model/store/store.ts | 75 +++++------------- .../_common/components/starter-debug-menu.ts | 2 +- .../src/blocksuite/initialization/index.ts | 2 +- .../database-properties/cells/rich-text.tsx | 2 +- 16 files changed, 124 insertions(+), 78 deletions(-) create mode 100644 blocksuite/framework/store/src/extension/history/history-extension.ts create mode 100644 blocksuite/framework/store/src/extension/history/index.ts diff --git a/blocksuite/affine/blocks/code/src/code-block.ts b/blocksuite/affine/blocks/code/src/code-block.ts index b75821fb7..12fa4efa8 100644 --- a/blocksuite/affine/blocks/code/src/code-block.ts +++ b/blocksuite/affine/blocks/code/src/code-block.ts @@ -411,7 +411,7 @@ export class CodeBlockComponent extends CaptionedBlockComponent })} .yText=${this.model.props.text.yText} .inlineEventSource=${this.topContenteditableElement ?? nothing} - .undoManager=${this.store.history} + .undoManager=${this.store.history.undoManager} .attributesSchema=${this.inlineManager.getSchema()} .attributeRenderer=${this.inlineManager.getRenderer()} .readonly=${this.store.readonly} diff --git a/blocksuite/affine/blocks/list/src/list-block.ts b/blocksuite/affine/blocks/list/src/list-block.ts index 73585d14a..1d83b5db7 100644 --- a/blocksuite/affine/blocks/list/src/list-block.ts +++ b/blocksuite/affine/blocks/list/src/list-block.ts @@ -185,7 +185,7 @@ export class ListBlockComponent extends CaptionedBlockComponent this._viewport} .readonly=${this.doc.readonly} .enableFormat=${false} diff --git a/blocksuite/affine/shared/src/services/notification-service.ts b/blocksuite/affine/shared/src/services/notification-service.ts index 7f7dd4b20..da269d88d 100644 --- a/blocksuite/affine/shared/src/services/notification-service.ts +++ b/blocksuite/affine/shared/src/services/notification-service.ts @@ -89,14 +89,14 @@ function notifyWithUndoActionImpl( options.abort?.addEventListener('abort', abort); const clearOnClose = () => { - store.history.off('stack-item-added', addHandler); - store.history.off('stack-item-popped', popHandler); + store.history.undoManager.off('stack-item-added', addHandler); + store.history.undoManager.off('stack-item-popped', popHandler); disposable.unsubscribe(); options.abort?.removeEventListener('abort', abort); }; - const addHandler = store.history.on('stack-item-added', abort); - const popHandler = store.history.on('stack-item-popped', abort); + const addHandler = store.history.undoManager.on('stack-item-added', abort); + const popHandler = store.history.undoManager.on('stack-item-popped', abort); const disposable = provider .get(EditorLifeCycleExtension) .slots.unmounted.subscribe(() => abort()); diff --git a/blocksuite/affine/shared/src/services/toolbar-service/context.ts b/blocksuite/affine/shared/src/services/toolbar-service/context.ts index 01b6c159e..a6690ab1e 100644 --- a/blocksuite/affine/shared/src/services/toolbar-service/context.ts +++ b/blocksuite/affine/shared/src/services/toolbar-service/context.ts @@ -59,7 +59,7 @@ abstract class ToolbarContextBase { } get history() { - return this.store.history; + return this.store.history.undoManager; } get view() { diff --git a/blocksuite/docs/api/@blocksuite/store/classes/Store.md b/blocksuite/docs/api/@blocksuite/store/classes/Store.md index 06a94561e..e37ee4454 100644 --- a/blocksuite/docs/api/@blocksuite/store/classes/Store.md +++ b/blocksuite/docs/api/@blocksuite/store/classes/Store.md @@ -725,13 +725,13 @@ Check if the store can undo #### Get Signature -> **get** **history**(): `UndoManager` +> **get** **history**(): `HistoryExtension` Get the Y.UndoManager instance for current store. ##### Returns -`UndoManager` +`HistoryExtension` *** diff --git a/blocksuite/docs/api/@blocksuite/store/interfaces/StoreSlots.md b/blocksuite/docs/api/@blocksuite/store/interfaces/StoreSlots.md index 5a6a74c03..d2251384c 100644 --- a/blocksuite/docs/api/@blocksuite/store/interfaces/StoreSlots.md +++ b/blocksuite/docs/api/@blocksuite/store/interfaces/StoreSlots.md @@ -32,14 +32,6 @@ The payload can have three types: *** -### historyUpdated - -> **historyUpdated**: `Subject`\<`void`\> - -This fires when the history is updated. - -*** - ### ready > **ready**: `Subject`\<`void`\> diff --git a/blocksuite/framework/store/src/extension/history/history-extension.ts b/blocksuite/framework/store/src/extension/history/history-extension.ts new file mode 100644 index 000000000..c0c71e932 --- /dev/null +++ b/blocksuite/framework/store/src/extension/history/history-extension.ts @@ -0,0 +1,79 @@ +import { signal } from '@preact/signals-core'; +import { Subject } from 'rxjs'; +import * as Y from 'yjs'; + +import type { Store } from '../../model'; +import { StoreExtension } from '../store-extension'; + +export class HistoryExtension extends StoreExtension { + static override readonly key = 'history'; + + private readonly _history!: Y.UndoManager; + + private readonly _canRedo = signal(false); + + private readonly _canUndo = signal(false); + + readonly onUpdated = new Subject(); + + constructor(store: Store) { + super(store); + + this._history = new Y.UndoManager([this.store.doc.yBlocks], { + trackedOrigins: new Set([this.store.doc.spaceDoc.clientID]), + }); + } + + private readonly _updateCanUndoRedoSignals = () => { + const canRedo = this._history.canRedo(); + const canUndo = this._history.canUndo(); + if (this._canRedo.peek() !== canRedo) { + this._canRedo.value = canRedo; + } + if (this._canUndo.peek() !== canUndo) { + this._canUndo.value = canUndo; + } + }; + + get canRedo() { + return this._canRedo.peek(); + } + + get canUndo() { + return this._canUndo.peek(); + } + + get canRedo$() { + return this._canRedo; + } + + get canUndo$() { + return this._canUndo; + } + + get undoManager() { + return this._history; + } + + override loaded() { + this._updateCanUndoRedoSignals(); + this._history.on('stack-cleared', this._historyObserver); + this._history.on('stack-item-added', this._historyObserver); + this._history.on('stack-item-popped', this._historyObserver); + this._history.on('stack-item-updated', this._historyObserver); + } + + private readonly _historyObserver = () => { + this._updateCanUndoRedoSignals(); + this.onUpdated.next(); + }; + + override disposed() { + super.disposed(); + this._history.off('stack-cleared', this._historyObserver); + this._history.off('stack-item-added', this._historyObserver); + this._history.off('stack-item-popped', this._historyObserver); + this._history.off('stack-item-updated', this._historyObserver); + this.onUpdated.complete(); + } +} diff --git a/blocksuite/framework/store/src/extension/history/index.ts b/blocksuite/framework/store/src/extension/history/index.ts new file mode 100644 index 000000000..1c8e4c1fd --- /dev/null +++ b/blocksuite/framework/store/src/extension/history/index.ts @@ -0,0 +1 @@ +export * from './history-extension'; diff --git a/blocksuite/framework/store/src/extension/index.ts b/blocksuite/framework/store/src/extension/index.ts index bfd36d2c5..2a5881538 100644 --- a/blocksuite/framework/store/src/extension/index.ts +++ b/blocksuite/framework/store/src/extension/index.ts @@ -1,4 +1,5 @@ export * from './extension'; +export * from './history'; export * from './schema'; export * from './selection'; export * from './store-extension'; diff --git a/blocksuite/framework/store/src/extension/selection/selection-extension.ts b/blocksuite/framework/store/src/extension/selection/selection-extension.ts index d4126ae7f..5de62997a 100644 --- a/blocksuite/framework/store/src/extension/selection/selection-extension.ts +++ b/blocksuite/framework/store/src/extension/selection/selection-extension.ts @@ -94,8 +94,14 @@ export class StoreSelectionExtension extends StoreExtension { } ); - this.store.history.on('stack-item-added', this._itemAdded); - this.store.history.on('stack-item-popped', this._itemPopped); + this.store.history.undoManager.on('stack-item-added', this._itemAdded); + this.store.history.undoManager.on('stack-item-popped', this._itemPopped); + } + + override disposed() { + super.disposed(); + this.store.history.undoManager.off('stack-item-added', this._itemAdded); + this.store.history.undoManager.off('stack-item-popped', this._itemPopped); } get value() { diff --git a/blocksuite/framework/store/src/model/store/store.ts b/blocksuite/framework/store/src/model/store/store.ts index a0771b90d..c81180f4a 100644 --- a/blocksuite/framework/store/src/model/store/store.ts +++ b/blocksuite/framework/store/src/model/store/store.ts @@ -9,6 +9,7 @@ import type { ExtensionType } from '../../extension/extension.js'; import { BlockSchemaIdentifier, type Doc, + HistoryExtension, StoreExtensionIdentifier, StoreSelectionExtension, } from '../../extension/index.js'; @@ -163,10 +164,6 @@ export type StoreSlots = { * */ blockUpdated: Subject; - /** - * This fires when the history is updated. - */ - historyUpdated: Subject; /** @internal */ yBlockUpdated: Subject< | { @@ -182,7 +179,7 @@ export type StoreSlots = { >; }; -const internalExtensions = [StoreSelectionExtension]; +const internalExtensions = [StoreSelectionExtension, HistoryExtension]; /** * Core store class that manages blocks and their lifecycle in BlockSuite @@ -231,10 +228,6 @@ export class Store { private readonly _schema: Schema; - private readonly _canRedo = signal(false); - - private readonly _canUndo = signal(false); - /** * Get the id of the store. * @@ -310,7 +303,7 @@ export class Store { if (this.readonly) { return false; } - return this._canRedo.peek(); + return this._history.canRedo; } /** @@ -322,7 +315,7 @@ export class Store { if (this.readonly) { return false; } - return this._canUndo.peek(); + return this._history.canUndo; } /** @@ -330,35 +323,35 @@ export class Store { * * @category History */ - undo() { + undo = () => { if (this.readonly) { console.error('cannot undo in readonly mode'); return; } - this._history.undo(); - } + this._history.undoManager.undo(); + }; /** * Redo the last undone transaction. * * @category History */ - redo() { + redo = () => { if (this.readonly) { console.error('cannot undo in readonly mode'); return; } - this._history.redo(); - } + this._history.undoManager.redo(); + }; /** * Reset the history of the store. * * @category History */ - resetHistory() { - return this._history.clear(); - } + resetHistory = () => { + return this._history.undoManager.clear(); + }; /** * Execute a transaction. @@ -374,7 +367,7 @@ export class Store { * @category History */ transact(fn: () => void, shouldTransact: boolean = this._shouldTransact) { - const spaceDoc = this.doc.spaceDoc; + const spaceDoc = this._doc.spaceDoc; spaceDoc.transact( () => { try { @@ -425,9 +418,9 @@ export class Store { * * @category History */ - captureSync() { - this._history.stopCapturing(); - } + captureSync = () => { + this._history.undoManager.stopCapturing(); + }; /** * Get the {@link Workspace} instance for current store. @@ -555,7 +548,9 @@ export class Store { private _isDisposed = false; - private readonly _history!: Y.UndoManager; + private get _history() { + return this._provider.get(HistoryExtension); + } /** * @internal @@ -568,7 +563,6 @@ export class Store { rootAdded: new Subject(), rootDeleted: new Subject(), blockUpdated: new Subject(), - historyUpdated: new Subject(), yBlockUpdated: new Subject(), }; this._schema = new Schema(); @@ -609,35 +603,9 @@ export class Store { this._onBlockAdded(id, false, true); }); - this._history = new Y.UndoManager([this._yBlocks], { - trackedOrigins: new Set([this.doc.spaceDoc.clientID]), - }); - - this._updateCanUndoRedoSignals(); - this._history.on('stack-cleared', this._historyObserver); - this._history.on('stack-item-added', this._historyObserver); - this._history.on('stack-item-popped', this._historyObserver); - this._history.on('stack-item-updated', this._historyObserver); - this._subscribeToSlots(); } - private readonly _updateCanUndoRedoSignals = () => { - const canRedo = this._history.canRedo(); - const canUndo = this._history.canUndo(); - if (this._canRedo.peek() !== canRedo) { - this._canRedo.value = canRedo; - } - if (this._canUndo.peek() !== canUndo) { - this._canUndo.value = canUndo; - } - }; - - private readonly _historyObserver = () => { - this._updateCanUndoRedoSignals(); - this.slots.historyUpdated.next(); - }; - private readonly _subscribeToSlots = () => { this.disposableGroup.add( this.slots.yBlockUpdated.subscribe(({ type, id, isLocal }) => { @@ -1270,14 +1238,13 @@ export class Store { this._provider.getAll(StoreExtensionIdentifier).forEach(ext => { ext.disposed(); }); - if (this.doc.ready) { + if (this._doc.ready) { this._yBlocks.unobserveDeep(this._handleYEvents); } this.slots.ready.complete(); this.slots.rootAdded.complete(); this.slots.rootDeleted.complete(); this.slots.blockUpdated.complete(); - this.slots.historyUpdated.complete(); this.slots.yBlockUpdated.complete(); this.disposableGroup.dispose(); this._isDisposed = true; diff --git a/blocksuite/playground/apps/_common/components/starter-debug-menu.ts b/blocksuite/playground/apps/_common/components/starter-debug-menu.ts index 25d29e58b..729489032 100644 --- a/blocksuite/playground/apps/_common/components/starter-debug-menu.ts +++ b/blocksuite/playground/apps/_common/components/starter-debug-menu.ts @@ -746,7 +746,7 @@ export class StarterDebugMenu extends ShadowlessElement { } override firstUpdated() { - this.doc.slots.historyUpdated.subscribe(() => { + this.doc.history.onUpdated.subscribe(() => { this._canUndo = this.doc.canUndo; this._canRedo = this.doc.canRedo; }); diff --git a/packages/frontend/core/src/blocksuite/initialization/index.ts b/packages/frontend/core/src/blocksuite/initialization/index.ts index 0127099d7..b3dd10a6e 100644 --- a/packages/frontend/core/src/blocksuite/initialization/index.ts +++ b/packages/frontend/core/src/blocksuite/initialization/index.ts @@ -56,6 +56,6 @@ export function initDocFromProps( paragraphId: paragraphBlockId, surfaceId, }); - doc.history.clear(); + doc.history.undoManager.clear(); }); } diff --git a/packages/frontend/core/src/modules/doc-info/views/database-properties/cells/rich-text.tsx b/packages/frontend/core/src/modules/doc-info/views/database-properties/cells/rich-text.tsx index c2dc6be1f..4c7c12e4c 100644 --- a/packages/frontend/core/src/modules/doc-info/views/database-properties/cells/rich-text.tsx +++ b/packages/frontend/core/src/modules/doc-info/views/database-properties/cells/rich-text.tsx @@ -31,7 +31,7 @@ const renderRichText = ({ const richText = new RichText(); richText.yText = text; - richText.undoManager = doc.history; + richText.undoManager = doc.history.undoManager; richText.readonly = doc.readonly; richText.attributesSchema = inlineManager.getSchema() as any; richText.attributeRenderer = inlineManager.getRenderer();