Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,24 +14,9 @@
* limitations under the License.
*/

import type { DocumentDataModel, ICommandInfo, IDrawingParam, ITransformState } from '@univerjs/core';
import type { IRichTextEditingMutationParams } from '@univerjs/docs';
import type { DocumentDataModel, ICommandInfo, IDrawingParam, IExecutionOptions, ITransformState } from '@univerjs/core';
import type { Documents, DocumentSkeleton, IDocsCustomBlockRenderViewport, IDocsTableRenderViewport, IDocumentSkeletonHeaderFooter, IDocumentSkeletonPage, IDocumentSkeletonRow, IDocumentSkeletonTable, Image, IRenderContext, IRenderModule } from '@univerjs/engine-render';
import {
AlignTypeH,
AlignTypeV,
BooleanNumber,
Disposable,
fromEventSubject,
ICommandService,
Inject,
IUniverInstanceService,
LifecycleService,
LifecycleStages,
ObjectRelativeFromH,
ObjectRelativeFromV,
PositionedObjectLayoutType,
} from '@univerjs/core';
import { AlignTypeH, AlignTypeV, BooleanNumber, Disposable, fromEventSubject, ICommandService, Inject, IUniverInstanceService, LifecycleService, LifecycleStages, ObjectRelativeFromH, ObjectRelativeFromV, PositionedObjectLayoutType } from '@univerjs/core';
import { DocSkeletonManagerService, RichTextEditingMutation } from '@univerjs/docs';
import { IEditorService, SetDocZoomRatioOperation } from '@univerjs/docs-ui';
import { IDrawingManagerService } from '@univerjs/drawing';
Expand Down Expand Up @@ -222,8 +207,18 @@ function hasHorizontalTableViewport(viewport: IDocsTableRenderViewport | null |
(viewport.leadingInsetLeft ?? 0) + viewport.contentWidth + (viewport.trailingInsetRight ?? 0) > viewport.viewportWidth;
}

function getCommandUnitId(command: ICommandInfo): string | undefined {
const { params } = command;
if (params == null || !('unitId' in params) || typeof params.unitId !== 'string') {
return;
}

return params.unitId;
}

export class DocDrawingTransformUpdateController extends Disposable implements IRenderModule {
private _liquid = new Liquid();
private _changesetDrawingRefreshScheduled = false;

constructor(
private readonly _context: IRenderContext<DocumentDataModel>,
Expand Down Expand Up @@ -274,35 +269,62 @@ export class DocDrawingTransformUpdateController extends Disposable implements I
const updateCommandList = [RichTextEditingMutation.id, SetDocZoomRatioOperation.id];

this.disposeWithMe(
this._commandService.onCommandExecuted((command: ICommandInfo) => {
this._commandService.onCommandExecuted((command: ICommandInfo, options?: IExecutionOptions) => {
if (updateCommandList.includes(command.id)) {
const params = command.params as IRichTextEditingMutationParams;
const { unitId: commandUnitId } = params;
const commandUnitId = getCommandUnitId(command);

const { unitId, mainComponent } = this._context;
const { unitId } = this._context;

if (commandUnitId !== unitId) {
return;
}

const skeleton = this._docSkeletonManagerService.getSkeleton();

if (skeleton == null) {
return;
}

// TODO: @JOCS, Do not use unitId to check if it's need to render images or isEditor. maybe need a config?
if (this._editorService.isEditor(unitId)) {
mainComponent?.makeDirty();
// TODO(@ai-review): Confirm the second microtask always follows the coalesced Doc layout refresh.
if (command.id === RichTextEditingMutation.id && options?.fromChangeset) {
this._scheduleChangesetDrawingRefresh();
return;
}

this._refreshDrawing(skeleton);
this._refreshCurrentDrawing();
}
})
);
}

private _scheduleChangesetDrawingRefresh(): void {
if (this._changesetDrawingRefreshScheduled) {
return;
}

this._changesetDrawingRefreshScheduled = true;
queueMicrotask(() => {
queueMicrotask(() => {
this._changesetDrawingRefreshScheduled = false;
if (this._disposed) {
return;
}

this._refreshCurrentDrawing();
});
});
}

private _refreshCurrentDrawing(): void {
const skeleton = this._docSkeletonManagerService.getSkeleton();
if (skeleton == null) {
return;
}

const { unitId, mainComponent } = this._context;
// TODO: @JOCS, Do not use unitId to check if it's need to render images or isEditor. maybe need a config?
if (this._editorService.isEditor(unitId)) {
mainComponent?.makeDirty();
return;
}

this._refreshDrawing(skeleton);
}

private _initTransformRefresh() {
this.disposeWithMe(
merge(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
* limitations under the License.
*/

import type { ICommandInfo } from '@univerjs/core';
import type { ICommandInfo, IExecutionOptions } from '@univerjs/core';
import type { IRichTextEditingMutationParams } from '@univerjs/docs';
import { DOCS_NORMAL_EDITOR_UNIT_ID_KEY, DocumentFlavor } from '@univerjs/core';
import { RichTextEditingMutation } from '@univerjs/docs';
import { Subject } from 'rxjs';
Expand All @@ -24,6 +25,17 @@ import { DocRenderController } from '../render-controllers/doc.render-controller

const mockScrollBarProps = vi.hoisted(() => [] as unknown[]);

function createRichTextMutation(): ICommandInfo<IRichTextEditingMutationParams> {
return {
id: RichTextEditingMutation.id,
params: {
unitId: 'doc-unit',
actions: [],
textRanges: null,
},
};
}

vi.mock('@univerjs/engine-render', async (importOriginal) => {
const actual = await importOriginal<typeof import('@univerjs/engine-render')>();
const PageLayoutType = {
Expand Down Expand Up @@ -106,7 +118,7 @@ function createControllerFixture(options?: {
unitId?: string;
}) {
mockScrollBarProps.length = 0;
const commandCallbacks: Array<(command: ICommandInfo) => void> = [];
const commandCallbacks: Array<(command: ICommandInfo, options?: IExecutionOptions) => void> = [];
const darkMode$ = new Subject<boolean>();
const canvasElement = { style: {} as Record<string, string> };
const canvasColorService = {
Expand Down Expand Up @@ -255,14 +267,26 @@ describe('doc render controller', () => {
it('refreshes page layout and selection after rich text mutations resize the document', () => {
const { commandCallbacks, pageLayoutService, selectionManager } = createControllerFixture();

commandCallbacks[0]({
id: RichTextEditingMutation.id,
params: {
unitId: 'doc-unit',
actions: [],
},
} as unknown as ICommandInfo);
commandCallbacks[0](createRichTextMutation());

expect(pageLayoutService.calculatePagePosition).toHaveBeenCalledTimes(1);
expect(selectionManager.refreshSelection).toHaveBeenCalledTimes(1);
});

// TODO(@ai-review): Confirm this test continues to model SnapshotService's synchronous changeset replay boundary.
it('coalesces synchronous changeset mutations into one document render', async () => {
const { commandCallbacks, pageLayoutService, selectionManager, skeletonManager } = createControllerFixture();
const mutation = createRichTextMutation();

commandCallbacks[0](mutation, { fromChangeset: true });
commandCallbacks[0](mutation, { fromChangeset: true });
commandCallbacks[0](mutation, { fromChangeset: true });

expect(skeletonManager.getSkeleton().calculate).not.toHaveBeenCalled();

await Promise.resolve();

expect(skeletonManager.getSkeleton().calculate).toHaveBeenCalledTimes(1);
expect(pageLayoutService.calculatePagePosition).toHaveBeenCalledTimes(1);
expect(selectionManager.refreshSelection).toHaveBeenCalledTimes(1);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@
* limitations under the License.
*/

import type { DocumentDataModel, EventState, ICommandInfo, Nullable } from '@univerjs/core';
import type { IRichTextEditingMutationParams } from '@univerjs/docs';
import type { DocumentDataModel, EventState, ICommandInfo, IExecutionOptions, Nullable } from '@univerjs/core';
import type { DocumentSkeleton, IDocumentSkeletonPage, IRenderContext, IRenderModule, IWheelEvent } from '@univerjs/engine-render';
import { DocumentFlavor, ICommandService, Inject, isInternalEditorID, IUniverInstanceService, RxDisposable, ThemeService, UniverInstanceType } from '@univerjs/core';
import { DocSelectionManagerService, DocSkeletonManagerService, RichTextEditingMutation } from '@univerjs/docs';
Expand All @@ -28,7 +27,18 @@ import { DocViewScaleService } from '../../services/doc-view-scale';
import { IEditorService } from '../../services/editor/editor-manager.service';
import { DocSelectionRenderService } from '../../services/selection/doc-selection-render.service';

function getCommandUnitId(command: ICommandInfo): string | undefined {
const { params } = command;
if (params == null || !('unitId' in params) || typeof params.unitId !== 'string') {
return;
}

return params.unitId;
}

export class DocRenderController extends RxDisposable implements IRenderModule {
private _changesetRenderScheduled = false;

constructor(
private readonly _context: IRenderContext<DocumentDataModel>,
@ICommandService private readonly _commandService: ICommandService,
Expand Down Expand Up @@ -220,17 +230,42 @@ export class DocRenderController extends RxDisposable implements IRenderModule {
private _initCommandListener() {
const updateCommandList = [RichTextEditingMutation.id];

this.disposeWithMe(this._commandService.onCommandExecuted((command: ICommandInfo) => {
// TODO@Jocs: performance, only update the skeleton when the command is related to the current unit.
if (updateCommandList.includes(command.id)) {
const params = command.params as IRichTextEditingMutationParams;
const { unitId } = params;
this.disposeWithMe(this._commandService.onCommandExecuted((command: ICommandInfo, options?: IExecutionOptions) => {
if (!updateCommandList.includes(command.id)) {
return;
}

this.reRender(unitId);
const unitId = getCommandUnitId(command);
if (unitId !== this._context.unitId) {
return;
}

// TODO(@ai-review): Verify this coalescing remains correct if changeset replay later yields between mutations.
if (options?.fromChangeset) {
this._scheduleChangesetRender();
return;
}

this.reRender(unitId);
}));
}

private _scheduleChangesetRender(): void {
if (this._changesetRenderScheduled) {
return;
}

this._changesetRenderScheduled = true;
queueMicrotask(() => {
this._changesetRenderScheduled = false;
if (this._disposed) {
return;
}

this.reRender(this._context.unitId);
});
}

private _initThemeListener() {
this.disposeWithMe(this._themeService.darkMode$.pipe(takeUntil(this.dispose$)).subscribe(() => {
this._syncCanvasBackground();
Expand Down