From a2916c74d875b7cf741aaf53f20b02ef7589515a Mon Sep 17 00:00:00 2001 From: hexf00 Date: Tue, 27 Aug 2024 21:23:01 +0800 Subject: [PATCH] feat(facade): add redo undo facade hooks --- packages/facade/src/apis/f-hooks.ts | 32 ++++- packages/facade/src/apis/facade.ts | 13 +- .../hooks/__tests__/f-undoredo-hooks.spec.ts | 111 ++++++++++++++++++ .../facade/src/apis/hooks/f-undoredo-hooks.ts | 94 +++++++++++++++ 4 files changed, 243 insertions(+), 7 deletions(-) create mode 100644 packages/facade/src/apis/hooks/__tests__/f-undoredo-hooks.spec.ts create mode 100644 packages/facade/src/apis/hooks/f-undoredo-hooks.ts diff --git a/packages/facade/src/apis/f-hooks.ts b/packages/facade/src/apis/f-hooks.ts index 5b9cf92d88ae..90ed43f0d657 100644 --- a/packages/facade/src/apis/f-hooks.ts +++ b/packages/facade/src/apis/f-hooks.ts @@ -14,12 +14,14 @@ * limitations under the License. */ -import { Inject, LifecycleService, LifecycleStages, toDisposable } from '@univerjs/core'; +import { Inject, Injector, LifecycleService, LifecycleStages, toDisposable } from '@univerjs/core'; import type { IDisposable } from '@univerjs/core'; import { filter } from 'rxjs'; +import { FUndoRedoHooks } from './hooks/f-undoredo-hooks'; export class FHooks { constructor( + @Inject(Injector) protected readonly _injector: Injector, @Inject(LifecycleService) private readonly _lifecycleService: LifecycleService ) { // empty @@ -60,4 +62,32 @@ export class FHooks { onSteady(callback: () => void): IDisposable { return toDisposable(this._lifecycleService.lifecycle$.pipe(filter((lifecycle) => lifecycle === LifecycleStages.Steady)).subscribe(callback)); } + + /** + * Hook that fires before an undo operation is executed. + * @param callback Function to be called when the event is triggered + * @returns A disposable object that can be used to unsubscribe from the event + */ + beforeUndo = FUndoRedoHooks.beforeUndo.bind(this); + + /** + * Hook that fires after an undo operation is executed. + * @param callback Function to be called when the event is triggered + * @returns A disposable object that can be used to unsubscribe from the event + */ + afterUndo = FUndoRedoHooks.afterUndo.bind(this); + + /** + * Hook that fires before a redo operation is executed. + * @param callback Function to be called when the event is triggered + * @returns A disposable object that can be used to unsubscribe from the event + */ + beforeRedo = FUndoRedoHooks.beforeRedo.bind(this); + + /** + * Hook that fires after a redo operation is executed. + * @param callback Function to be called when the event is triggered + * @returns A disposable object that can be used to unsubscribe from the event + */ + afterRedo = FUndoRedoHooks.afterRedo.bind(this); } diff --git a/packages/facade/src/apis/facade.ts b/packages/facade/src/apis/facade.ts index 9227a7014032..be6f75f6fb0f 100644 --- a/packages/facade/src/apis/facade.ts +++ b/packages/facade/src/apis/facade.ts @@ -33,6 +33,7 @@ import { Injector, IUniverInstanceService, Quantity, + RedoCommand, toDisposable, UndoCommand, Univer, UniverInstanceType, WrapStrategy, @@ -71,9 +72,7 @@ export class FUniver { constructor( @Inject(Injector) protected readonly _injector: Injector, @IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService, - @ICommandService private readonly _commandService: ICommandService, - @ISocketService private readonly _ws: ISocketService, - @IRenderManagerService private readonly _renderManagerService: IRenderManagerService + @ICommandService private readonly _commandService: ICommandService ) { this._initialize(); } @@ -328,7 +327,7 @@ export class FUniver { * @returns {Promise} redo result */ redo(): Promise { - return this._commandService.executeCommand(UndoCommand.id); + return this._commandService.executeCommand(RedoCommand.id); } // #endregion @@ -384,7 +383,8 @@ export class FUniver { * @returns {ISocket} WebSocket instance */ createSocket(url: string): ISocket { - const ws = this._ws.createSocket(url); + const wsService = this._injector.get(ISocketService); + const ws = wsService.createSocket(url); if (!ws) { throw new Error('[WebSocketService]: failed to create socket!'); @@ -421,7 +421,8 @@ export class FUniver { * @returns {Nullable} The render component. */ private _getSheetRenderComponent(unitId: string, viewKey: SHEET_VIEW_KEY): Nullable { - const render = this._renderManagerService.getRenderById(unitId); + const renderManagerService = this._injector.get(IRenderManagerService); + const render = renderManagerService.getRenderById(unitId); if (!render) { throw new Error('Render not found'); } diff --git a/packages/facade/src/apis/hooks/__tests__/f-undoredo-hooks.spec.ts b/packages/facade/src/apis/hooks/__tests__/f-undoredo-hooks.spec.ts new file mode 100644 index 000000000000..ec944bb05197 --- /dev/null +++ b/packages/facade/src/apis/hooks/__tests__/f-undoredo-hooks.spec.ts @@ -0,0 +1,111 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { type Injector, IUndoRedoService, IUniverInstanceService, Univer, UniverInstanceType } from '@univerjs/core'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { UniverSheetsPlugin } from '@univerjs/sheets'; +import { UniverRenderEnginePlugin } from '@univerjs/engine-render'; +import { FUniver } from '../../facade'; +import type { FWorksheet } from '../../sheets/f-worksheet'; + +function createUnitTestBed(): { + univer: Univer; + get: Injector['get']; + univerAPI: FUniver; + injector: Injector; +} { + const univer = new Univer(); + const injector = univer.__getInjector(); + univer.registerPlugin(UniverSheetsPlugin); + univer.registerPlugin(UniverRenderEnginePlugin); + + const sheet = univer.createUnit(UniverInstanceType.UNIVER_SHEET, {}); + const univerInstanceService = injector.get(IUniverInstanceService); + univerInstanceService.focusUnit(sheet.getUnitId()); + const univerAPI = FUniver.newAPI(univer); + + return { + univer, + get: injector.get.bind(injector), + univerAPI, + injector, + }; +} + +describe('Test Undo Redo Hooks', () => { + let get: Injector['get']; + let univerAPI: FUniver; + + beforeEach(() => { + const testBed = createUnitTestBed(); + get = testBed.get; + univerAPI = testBed.univerAPI; + }); + + it('undoredo normal case', async () => { + const sheet = univerAPI.getActiveWorkbook()?.getActiveSheet() as FWorksheet; + expect(sheet).not.toBeUndefined(); + const range = sheet.getRange(0, 0); + const emptyFlag = ''; + const text = 'Hello World'; + await range.setValue(emptyFlag); + await range.setValue(text); + univerAPI.getHooks().beforeUndo(() => { + expect(range.getValue()).toBe(text); + }); + univerAPI.getHooks().afterUndo(() => { + expect(range.getValue()).toBe(emptyFlag); + }); + univerAPI.getHooks().beforeRedo(() => { + expect(range.getValue()).toBe(emptyFlag); + }); + univerAPI.getHooks().afterRedo(() => { + expect(range.getValue()).toBe(text); + }); + expect(range.getValue()).toBe(text); + await univerAPI.undo(); + expect(range.getValue()).toBe(emptyFlag); + await univerAPI.redo(); + expect(range.getValue()).toBe(text); + }); + + it('undoredo edge case', async () => { + const sheet = univerAPI.getActiveWorkbook()?.getActiveSheet() as FWorksheet; + expect(sheet).not.toBeUndefined(); + + // manually construct undo redo service + get(IUndoRedoService); + + const beforeUndoFn = vi.fn(); + const afterUndoFn = vi.fn(); + const beforeRedoFn = vi.fn(); + const afterRedoFn = vi.fn(); + + univerAPI.getHooks().beforeUndo(beforeUndoFn); + univerAPI.getHooks().afterUndo(afterUndoFn); + univerAPI.getHooks().beforeRedo(beforeRedoFn); + univerAPI.getHooks().afterRedo(afterRedoFn); + + await univerAPI.undo(); + await univerAPI.redo(); + + expect(beforeUndoFn).toHaveBeenCalledTimes(0); + expect(afterUndoFn).toHaveBeenCalledTimes(0); + expect(beforeRedoFn).toHaveBeenCalledTimes(0); + expect(afterRedoFn).toHaveBeenCalledTimes(0); + }); +}); + diff --git a/packages/facade/src/apis/hooks/f-undoredo-hooks.ts b/packages/facade/src/apis/hooks/f-undoredo-hooks.ts new file mode 100644 index 000000000000..21efe97f5928 --- /dev/null +++ b/packages/facade/src/apis/hooks/f-undoredo-hooks.ts @@ -0,0 +1,94 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { IDisposable, IUndoRedoItem } from '@univerjs/core'; +import { ICommandService, IUndoRedoService, RedoCommand, UndoCommand } from '@univerjs/core'; +import type { FHooks } from '../f-hooks'; + +export const FUndoRedoHooks = { + /** + * Hook that fires before an undo operation is executed. + * @param callback Function to be called when the event is triggered + * @returns A disposable object that can be used to unsubscribe from the event + */ + beforeUndo(this: FHooks, callback: (action: IUndoRedoItem) => void): IDisposable { + const commandService = this._injector.get(ICommandService); + + return commandService.beforeCommandExecuted((command) => { + if (command.id === UndoCommand.id) { + const undoredoService = this._injector.get(IUndoRedoService); + const action = undoredoService.pitchTopUndoElement(); + if (action) { + callback(action); + } + } + }); + }, + /** + * Hook that fires after an undo operation is executed. + * @param callback Function to be called when the event is triggered + * @returns A disposable object that can be used to unsubscribe from the event + */ + afterUndo(this: FHooks, callback: (action: IUndoRedoItem) => void): IDisposable { + const commandService = this._injector.get(ICommandService); + + return commandService.onCommandExecuted((command) => { + if (command.id === UndoCommand.id) { + const undoredoService = this._injector.get(IUndoRedoService); + const action = undoredoService.pitchTopUndoElement(); + if (action) { + callback(action); + } + } + }); + }, + /** + * Hook that fires before a redo operation is executed. + * @param callback Function to be called when the event is triggered + * @returns A disposable object that can be used to unsubscribe from the event + */ + beforeRedo(this: FHooks, callback: (action: IUndoRedoItem) => void): IDisposable { + const commandService = this._injector.get(ICommandService); + + return commandService.beforeCommandExecuted((command) => { + if (command.id === RedoCommand.id) { + const undoredoService = this._injector.get(IUndoRedoService); + const action = undoredoService.pitchTopRedoElement(); + if (action) { + callback(action); + } + } + }); + }, + /** + * Hook that fires after a redo operation is executed. + * @param callback Function to be called when the event is triggered + * @returns A disposable object that can be used to unsubscribe from the event + */ + afterRedo(this: FHooks, callback: (action: IUndoRedoItem) => void): IDisposable { + const commandService = this._injector.get(ICommandService); + + return commandService.onCommandExecuted((command) => { + if (command.id === RedoCommand.id) { + const undoredoService = this._injector.get(IUndoRedoService); + const action = undoredoService.pitchTopRedoElement(); + if (action) { + callback(action); + } + } + }); + }, +};