perf(sheets): replace object matrix queries with range operations (#7470)

This commit is contained in:
wpxp123456
2026-08-11 10:54:23 +08:00
committed by GitHub
parent 8a25f7602e
commit 24233500a6
17 changed files with 1002 additions and 918 deletions
@@ -21,7 +21,6 @@ test('cells rendering after scrolling', async () => {
await page.waitForTimeout(1000);
const canvas = page.locator(SHEET_MAIN_CANVAS_ID);
// TODO(@ai-review): Verify fixed-count wheel input and stable scroll frames eliminate CI flakiness without hiding merged-cell repaint regressions.
await canvas.evaluate(async (element: HTMLCanvasElement) => {
const scroll = async (deltaY: number) => {
for (let elapsed = 0; elapsed < 1000; elapsed += 30) {
@@ -19,7 +19,6 @@ import { describe, expect, it } from 'vitest';
import { Range } from '../../sheets/range';
import { AbsoluteRefType } from '../../sheets/typedef';
import { ObjectMatrix } from '../object-matrix';
import { multiSubtractMultiRanges } from '../object-matrix-query';
import { Rectangle } from '../rectangle';
function rangesToMatrix(ranges: IRange[]) {
@@ -34,6 +33,13 @@ function rangesToMatrix(ranges: IRange[]) {
return matrix.getMatrix();
}
function subtractedRangesToMatrix(ranges: IRange[], rangesToSubtract: IRange[]) {
const matrix = new ObjectMatrix<number>();
ranges.forEach((range) => Range.foreach(range, (row, col) => matrix.setValue(row, col, 1)));
rangesToSubtract.forEach((range) => Range.foreach(range, (row, col) => matrix.realDeleteValue(row, col)));
return matrix.getMatrix();
}
const cellToRange = (row: number, col: number) => ({ startRow: row, endRow: row, startColumn: col, endColumn: col } as IRange);
describe('test "Rectangle"', () => {
it('test "subtract"', () => {
@@ -134,8 +140,7 @@ describe('multiSubtractMulti', () => {
const ranges2: IRange[] = [
{ startColumn: 2, endColumn: 4, startRow: 2, endRow: 4 },
];
const expected: IRange[] = multiSubtractMultiRanges(ranges1, ranges2);
expect(rangesToMatrix(Rectangle.subtractMulti(ranges1, ranges2))).toEqual(rangesToMatrix(expected));
expect(rangesToMatrix(Rectangle.subtractMulti(ranges1, ranges2))).toEqual(subtractedRangesToMatrix(ranges1, ranges2));
});
it('should handle subtracting multiple ranges from a single range', () => {
@@ -146,8 +151,7 @@ describe('multiSubtractMulti', () => {
{ startColumn: 2, endColumn: 5, startRow: 2, endRow: 5 },
{ startColumn: 6, endColumn: 8, startRow: 6, endRow: 8 },
];
const expected: IRange[] = multiSubtractMultiRanges(ranges1, ranges2);
expect(rangesToMatrix(Rectangle.subtractMulti(ranges1, ranges2))).toEqual(rangesToMatrix(expected));
expect(rangesToMatrix(Rectangle.subtractMulti(ranges1, ranges2))).toEqual(subtractedRangesToMatrix(ranges1, ranges2));
});
it('should handle non-overlapping subtraction ranges', () => {
@@ -157,8 +161,7 @@ describe('multiSubtractMulti', () => {
const ranges2: IRange[] = [
{ startColumn: 6, endColumn: 8, startRow: 6, endRow: 8 },
];
const expected: IRange[] = multiSubtractMultiRanges(ranges1, ranges2);
expect(rangesToMatrix(Rectangle.subtractMulti(ranges1, ranges2))).toEqual(rangesToMatrix(expected));
expect(rangesToMatrix(Rectangle.subtractMulti(ranges1, ranges2))).toEqual(subtractedRangesToMatrix(ranges1, ranges2));
});
it('should handle subtraction ranges that completely overlap', () => {
@@ -168,8 +171,7 @@ describe('multiSubtractMulti', () => {
const ranges2: IRange[] = [
{ startColumn: 1, endColumn: 5, startRow: 1, endRow: 5 },
];
const expected: IRange[] = multiSubtractMultiRanges(ranges1, ranges2);
expect(rangesToMatrix(Rectangle.subtractMulti(ranges1, ranges2))).toEqual(rangesToMatrix(expected));
expect(rangesToMatrix(Rectangle.subtractMulti(ranges1, ranges2))).toEqual(subtractedRangesToMatrix(ranges1, ranges2));
});
it('should handle empty ranges', () => {
@@ -177,8 +179,7 @@ describe('multiSubtractMulti', () => {
const ranges2: IRange[] = [
{ startColumn: 2, endColumn: 4, startRow: 2, endRow: 4 },
];
const expected: IRange[] = multiSubtractMultiRanges(ranges1, ranges2);
expect(rangesToMatrix(Rectangle.subtractMulti(ranges1, ranges2))).toEqual(rangesToMatrix(expected));
expect(rangesToMatrix(Rectangle.subtractMulti(ranges1, ranges2))).toEqual(subtractedRangesToMatrix(ranges1, ranges2));
});
it('should handle empty subtraction ranges', () => {
@@ -186,7 +187,6 @@ describe('multiSubtractMulti', () => {
{ startColumn: 1, endColumn: 5, startRow: 1, endRow: 5 },
];
const ranges2: IRange[] = [];
const expected: IRange[] = multiSubtractMultiRanges(ranges1, ranges2);
expect(rangesToMatrix(Rectangle.subtractMulti(ranges1, ranges2))).toEqual(rangesToMatrix(expected));
expect(rangesToMatrix(Rectangle.subtractMulti(ranges1, ranges2))).toEqual(subtractedRangesToMatrix(ranges1, ranges2));
});
});
-1
View File
@@ -45,7 +45,6 @@ export {
numfmt,
} from './numfmt';
export * from './object-matrix';
export { queryObjectMatrix } from './object-matrix-query';
export * from './random-id';
export { moveRangeByOffset, splitIntoGrid } from './range';
export * from './rectangle';
@@ -1,123 +0,0 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 { IRange } from '../sheets/typedef';
import type { Nullable } from './types';
import { Range } from '../sheets/range';
import { ObjectMatrix } from './object-matrix';
import { Rectangle } from './rectangle';
function maximalRectangle<T>(matrix: T[][], match: (val: T) => boolean) {
if (matrix.length === 0 || matrix[0].length === 0) return null;
const heights = new Array(matrix[0].length).fill(0);
let maxArea = 0;
let maxRect = null;
for (let row = 0; row < matrix.length; row++) {
for (let col = 0; col < matrix[0].length; col++) {
heights[col] = match(matrix[row][col]) ? heights[col] + 1 : 0;
}
const areaWithRect = largestRectangleArea(heights);
if (areaWithRect.area > maxArea) {
maxArea = areaWithRect.area;
// Adjust the rectangle's top row to the current row minus the height plus one
maxRect = {
startColumn: areaWithRect.start,
startRow: row - areaWithRect.height + 1,
endColumn: areaWithRect.end,
endRow: row,
};
}
}
return maxRect;
}
function largestRectangleArea(heights: number[]) {
const stack: number[] = [];
let maxArea = 0;
let maxRect = { area: 0, height: 0, start: 0, end: 0 };
let index = 0;
while (index < heights.length) {
if (stack.length === 0 || heights[index] >= heights[stack[stack.length - 1]]) {
stack.push(index++);
} else {
const height = heights[stack.pop()!];
const width = stack.length === 0 ? index : index - stack[stack.length - 1] - 1;
if (height * width > maxArea) {
maxArea = height * width;
maxRect = { area: maxArea, height, start: stack.length === 0 ? 0 : stack[stack.length - 1] + 1, end: index - 1 };
}
}
}
while (stack.length > 0) {
const height = heights[stack.pop()!];
const width = stack.length === 0 ? index : index - stack[stack.length - 1] - 1;
if (height * width > maxArea) {
maxArea = height * width;
maxRect = { area: maxArea, height, start: stack.length === 0 ? 0 : stack[stack.length - 1] + 1, end: index - 1 };
}
}
return maxRect;
}
function resetMatrix<T>(matrix: Nullable<T>[][], range: IRange) {
Range.foreach(range, (row, col) => {
matrix[row][col] = undefined;
});
}
/**
* @deprecated this function could cause memory out of use in large range.
*/
export function queryObjectMatrix<T>(matrix: ObjectMatrix<T>, match: (value: T) => boolean) {
const arrayMatrix = matrix.toFullArray();
const results: IRange[] = [];
while (true) {
const rectangle = maximalRectangle(arrayMatrix, match);
if (!rectangle) {
break;
}
results.push(rectangle);
resetMatrix(arrayMatrix, rectangle);
}
return results;
}
export function multiSubtractMultiRanges(ranges1: IRange[], ranges2: IRange[]): IRange[] {
const matrix = new ObjectMatrix<number>();
ranges1.forEach((range) => {
Range.foreach(range, (row, col) => {
matrix.setValue(row, col, 1);
});
});
ranges2.forEach((range) => {
Range.foreach(range, (row, col) => {
matrix.setValue(row, col, 0);
});
});
return Rectangle.mergeRanges(queryObjectMatrix(matrix, (value) => value === 1));
}
@@ -14,7 +14,9 @@
* limitations under the License.
*/
import type { Workbook } from '@univerjs/core';
import type { IConditionFormattingRule } from '@univerjs/sheets-conditional-formatting';
import { IUniverInstanceService, Range, UniverInstanceType } from '@univerjs/core';
import {
AddConditionalRuleMutation,
CFRuleType,
@@ -24,16 +26,19 @@ import {
SetConditionalRuleMutation,
} from '@univerjs/sheets-conditional-formatting';
import { COPY_TYPE, PREDEFINED_HOOK_NAME_PASTE } from '@univerjs/sheets-ui';
import { afterEach, describe, expect, it } from 'vitest';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { createCfUiTestBed } from '../../__tests__/create-cf-ui-test-bed';
import { ConditionalFormattingCopyPasteController } from '../cf.copy-paste.controller';
describe('ConditionalFormattingCopyPasteController', () => {
afterEach(() => {
// each test disposes its own univer instance
vi.restoreAllMocks();
});
it('creates copy-paste mutations from the real rule model and view model', async () => {
vi.spyOn(Range, 'foreach').mockImplementation(() => {
throw new Error('must not enumerate cells');
});
const testBed = createCfUiTestBed();
testBed.injector.add([ConditionalFormattingCopyPasteController]);
testBed.injector.get(ConditionalFormattingCopyPasteController);
@@ -145,4 +150,234 @@ describe('ConditionalFormattingCopyPasteController', () => {
testBed.univer.dispose();
});
it('keeps identical overlapping source rules distinct when copying', async () => {
const testBed = createCfUiTestBed();
testBed.injector.add([ConditionalFormattingCopyPasteController]);
testBed.injector.get(ConditionalFormattingCopyPasteController);
const config: IConditionFormattingRule['rule'] = {
type: CFRuleType.highlightCell,
subType: CFSubRuleType.text,
operator: CFTextOperator.notContainsText,
value: 'A1',
style: { bg: { rgb: '#0f0' } },
};
for (const cfId of ['cf-source-1', 'cf-source-2']) {
await testBed.commandService.executeCommand(AddConditionalRuleMutation.id, {
unitId: testBed.unitId,
subUnitId: testBed.subUnitId,
rule: {
cfId,
ranges: [{ startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 }],
stopIfTrue: false,
rule: config,
},
});
}
const hook = testBed.getClipboardHook()!;
hook.onBeforeCopy(testBed.unitId, testBed.subUnitId, {
startRow: 0,
endRow: 0,
startColumn: 0,
endColumn: 0,
});
const result = hook.onPasteCells(
{ unitId: testBed.unitId, subUnitId: testBed.subUnitId, range: { rows: [0], cols: [0] } },
{ unitId: testBed.unitId, subUnitId: testBed.subUnitId, range: { rows: [1], cols: [1] } },
null,
{ copyType: COPY_TYPE.COPY, pasteType: PREDEFINED_HOOK_NAME_PASTE.DEFAULT_PASTE }
);
const copiedRuleIds = result.redos.flatMap((mutation) => {
if (mutation.id !== AddConditionalRuleMutation.id && mutation.id !== SetConditionalRuleMutation.id) {
return [];
}
const rule = (mutation.params as { rule: IConditionFormattingRule }).rule;
return rule.ranges.some((range) => range.startRow <= 1 && range.endRow >= 1 && range.startColumn <= 1 && range.endColumn >= 1)
? [rule.cfId]
: [];
});
expect(new Set(copiedRuleIds)).toEqual(new Set(['cf-source-1', 'cf-source-2']));
testBed.univer.dispose();
});
it('does not reuse a target rule with different stop-if-true semantics', async () => {
const testBed = createCfUiTestBed();
const workbook = testBed.get(IUniverInstanceService).getUnit<Workbook>(testBed.unitId, UniverInstanceType.UNIVER_SHEET)!;
workbook.addWorksheet('sheet2', 1, { id: 'sheet2', name: 'sheet2' });
testBed.injector.add([ConditionalFormattingCopyPasteController]);
testBed.injector.get(ConditionalFormattingCopyPasteController);
const config: IConditionFormattingRule['rule'] = {
type: CFRuleType.highlightCell,
subType: CFSubRuleType.text,
operator: CFTextOperator.notContainsText,
value: 'A1',
style: { bg: { rgb: '#0f0' } },
};
await testBed.commandService.executeCommand(AddConditionalRuleMutation.id, {
unitId: testBed.unitId,
subUnitId: testBed.subUnitId,
rule: {
cfId: 'cf-source',
ranges: [{ startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 }],
stopIfTrue: true,
rule: config,
},
});
await testBed.commandService.executeCommand(AddConditionalRuleMutation.id, {
unitId: testBed.unitId,
subUnitId: 'sheet2',
rule: {
cfId: 'cf-target',
ranges: [{ startRow: 2, endRow: 2, startColumn: 2, endColumn: 2 }],
stopIfTrue: false,
rule: config,
},
});
const hook = testBed.getClipboardHook()!;
hook.onBeforeCopy(testBed.unitId, testBed.subUnitId, {
startRow: 0,
endRow: 0,
startColumn: 0,
endColumn: 0,
});
const result = hook.onPasteCells(
{ unitId: testBed.unitId, subUnitId: testBed.subUnitId, range: { rows: [0], cols: [0] } },
{ unitId: testBed.unitId, subUnitId: 'sheet2', range: { rows: [1], cols: [1] } },
null,
{ copyType: COPY_TYPE.COPY, pasteType: PREDEFINED_HOOK_NAME_PASTE.DEFAULT_PASTE }
);
const addedRule = result.redos.find((mutation) => mutation.id === AddConditionalRuleMutation.id)?.params as { rule: IConditionFormattingRule } | undefined;
expect(addedRule?.rule.stopIfTrue).toBe(true);
testBed.univer.dispose();
});
it.each([
{ name: 'matching semantics', targetValue: 'source' },
{ name: 'different semantics', targetValue: 'target' },
])('isolates source and target rules with colliding IDs during cross-sheet cut: $name', async ({ targetValue }) => {
const testBed = createCfUiTestBed();
const workbook = testBed.get(IUniverInstanceService).getUnit<Workbook>(testBed.unitId, UniverInstanceType.UNIVER_SHEET)!;
workbook.addWorksheet('sheet2', 1, { id: 'sheet2', name: 'sheet2' });
testBed.injector.add([ConditionalFormattingCopyPasteController]);
testBed.injector.get(ConditionalFormattingCopyPasteController);
const createRule = (value: string, ranges: IConditionFormattingRule['ranges']): IConditionFormattingRule => ({
cfId: 'cf-collision',
ranges,
stopIfTrue: false,
rule: {
type: CFRuleType.highlightCell,
subType: CFSubRuleType.text,
operator: CFTextOperator.notContainsText,
value,
style: { bg: { rgb: value === 'source' ? '#0f0' : '#f00' } },
},
});
const sourceRule = createRule('source', [{ startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 }]);
const targetRule = createRule(targetValue, [
{ startRow: 1, endRow: 1, startColumn: 1, endColumn: 1 },
{ startRow: 2, endRow: 2, startColumn: 2, endColumn: 2 },
]);
await testBed.commandService.executeCommand(AddConditionalRuleMutation.id, {
unitId: testBed.unitId,
subUnitId: testBed.subUnitId,
rule: sourceRule,
});
await testBed.commandService.executeCommand(AddConditionalRuleMutation.id, {
unitId: testBed.unitId,
subUnitId: 'sheet2',
rule: targetRule,
});
const hook = testBed.getClipboardHook()!;
hook.onBeforeCopy(testBed.unitId, testBed.subUnitId, {
startRow: 0,
endRow: 0,
startColumn: 0,
endColumn: 0,
});
const result = hook.onPasteCells(
{ unitId: testBed.unitId, subUnitId: testBed.subUnitId, range: { rows: [0], cols: [0] } },
{ unitId: testBed.unitId, subUnitId: 'sheet2', range: { rows: [1], cols: [1] } },
null,
{ copyType: COPY_TYPE.CUT, pasteType: PREDEFINED_HOOK_NAME_PASTE.DEFAULT_PASTE }
) as { redos: Array<{ id: string; params: object }>; undos: Array<{ id: string; params: object }> };
for (const mutation of result.redos) {
await testBed.commandService.executeCommand(mutation.id, mutation.params);
}
expect(testBed.ruleModel.getRule(testBed.unitId, testBed.subUnitId, 'cf-collision')).toBeFalsy();
expect(testBed.ruleModel.getRule(testBed.unitId, 'sheet2', 'cf-collision')?.ranges).toEqual([
{ startRow: 2, endRow: 2, startColumn: 2, endColumn: 2 },
]);
const copiedRules = testBed.ruleModel.getSubunitRules(testBed.unitId, 'sheet2')?.filter((rule) => (
rule.cfId !== 'cf-collision' && rule.rule.type === CFRuleType.highlightCell && 'value' in rule.rule && rule.rule.value === 'source'
));
expect(copiedRules).toHaveLength(1);
expect(copiedRules?.[0].ranges).toEqual([{ startRow: 1, endRow: 1, startColumn: 1, endColumn: 1 }]);
for (const mutation of result.undos) {
await testBed.commandService.executeCommand(mutation.id, mutation.params);
}
expect(testBed.ruleModel.getRule(testBed.unitId, testBed.subUnitId, 'cf-collision')).toEqual(sourceRule);
expect(testBed.ruleModel.getSubunitRules(testBed.unitId, 'sheet2')).toEqual([targetRule]);
testBed.univer.dispose();
});
it('preserves source rule priority when copying across worksheets', async () => {
const testBed = createCfUiTestBed();
const workbook = testBed.get(IUniverInstanceService).getUnit<Workbook>(testBed.unitId, UniverInstanceType.UNIVER_SHEET)!;
workbook.addWorksheet('sheet2', 1, { id: 'sheet2', name: 'sheet2' });
testBed.injector.add([ConditionalFormattingCopyPasteController]);
testBed.injector.get(ConditionalFormattingCopyPasteController);
for (const value of ['low', 'high']) {
await testBed.commandService.executeCommand(AddConditionalRuleMutation.id, {
unitId: testBed.unitId,
subUnitId: testBed.subUnitId,
rule: {
cfId: `cf-${value}`,
ranges: [{ startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 }],
stopIfTrue: false,
rule: {
type: CFRuleType.highlightCell,
subType: CFSubRuleType.text,
operator: CFTextOperator.notContainsText,
value,
style: { bg: { rgb: value === 'high' ? '#0f0' : '#f00' } },
},
},
});
}
const hook = testBed.getClipboardHook()!;
hook.onBeforeCopy(testBed.unitId, testBed.subUnitId, {
startRow: 0,
endRow: 0,
startColumn: 0,
endColumn: 0,
});
const result = hook.onPasteCells(
{ unitId: testBed.unitId, subUnitId: testBed.subUnitId, range: { rows: [0], cols: [0] } },
{ unitId: testBed.unitId, subUnitId: 'sheet2', range: { rows: [1], cols: [1] } },
null,
{ copyType: COPY_TYPE.COPY, pasteType: PREDEFINED_HOOK_NAME_PASTE.DEFAULT_PASTE }
) as { redos: Array<{ id: string; params: object }>; undos: Array<{ id: string; params: object }> };
for (const mutation of result.redos) {
await testBed.commandService.executeCommand(mutation.id, mutation.params);
}
const copiedValues = testBed.ruleModel.getSubunitRules(testBed.unitId, 'sheet2')?.map((rule) => (
rule.rule.type === CFRuleType.highlightCell && 'value' in rule.rule ? rule.rule.value : undefined
));
expect(copiedValues).toEqual(['high', 'low']);
for (const mutation of result.undos) {
await testBed.commandService.executeCommand(mutation.id, mutation.params);
}
expect(testBed.ruleModel.getSubunitRules(testBed.unitId, 'sheet2')).toEqual([]);
testBed.univer.dispose();
});
});
@@ -17,12 +17,11 @@
import type { IMutationInfo, IRange, Workbook } from '@univerjs/core';
import type { IDiscreteRange, ISheetAutoFillHook } from '@univerjs/sheets';
import type { IDeleteConditionalRuleMutationParams, ISetConditionalRuleMutationParams } from '@univerjs/sheets-conditional-formatting';
import { Disposable, Inject, Injector, IUniverInstanceService, Range, Rectangle, UniverInstanceType } from '@univerjs/core';
import { Disposable, getIntersectRange, Inject, Injector, IUniverInstanceService, Rectangle, UniverInstanceType } from '@univerjs/core';
import { AUTO_FILL_APPLY_TYPE, AutoFillTools, IAutoFillService } from '@univerjs/sheets';
import {
ConditionalFormattingRangeTransformService,
ConditionalFormattingRuleModel,
ConditionalFormattingViewModel,
DeleteConditionalRuleMutation,
DeleteConditionalRuleMutationUndoFactory,
SetConditionalRuleMutation,
@@ -42,7 +41,6 @@ export class ConditionalFormattingAutoFillController extends Disposable {
@Inject(IUniverInstanceService) private _univerInstanceService: IUniverInstanceService,
@Inject(IAutoFillService) private _autoFillService: IAutoFillService,
@Inject(ConditionalFormattingRuleModel) private _conditionalFormattingRuleModel: ConditionalFormattingRuleModel,
@Inject(ConditionalFormattingViewModel) private _conditionalFormattingViewModel: ConditionalFormattingViewModel,
@Inject(ConditionalFormattingRangeTransformService) private _conditionalFormattingRangeTransformService: ConditionalFormattingRangeTransformService
) {
super();
@@ -54,117 +52,6 @@ export class ConditionalFormattingAutoFillController extends Disposable {
private _initAutoFill() {
const noopReturnFunc = () => ({ redos: [], undos: [] });
const loopFunc = (
sourceStartCell: { row: number; col: number },
targetStartCell: { row: number; col: number },
relativeRange: IRange,
rangeMap: Map<string, IRange[]>,
rangeDeltaMap: Map<string, IRangeDelta>,
mapFunc: (row: number, col: number) => ({ row: number; col: number })
) => {
const unitId = this._univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!.getUnitId();
const subUnitId = this._univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!.getActiveSheet()?.getSheetId();
if (!unitId || !subUnitId) {
return;
}
const getRangeDelta = (cfId: string) => {
let rangeDelta = rangeDeltaMap.get(cfId);
if (!rangeDelta) {
rangeDelta = { add: [], remove: [] };
rangeDeltaMap.set(cfId, rangeDelta);
}
return rangeDelta;
};
const ensureRuleRanges = (cfId: string) => {
if (rangeMap.has(cfId)) {
return true;
}
const rule = this._conditionalFormattingRuleModel.getRule(unitId, subUnitId, cfId);
if (!rule) {
return false;
}
rangeMap.set(cfId, rule.ranges);
return true;
};
const sourceRange = {
startRow: sourceStartCell.row,
startColumn: sourceStartCell.col,
endColumn: sourceStartCell.col,
endRow: sourceStartCell.row,
};
const targetRange = {
startRow: targetStartCell.row,
startColumn: targetStartCell.col,
endColumn: targetStartCell.col,
endRow: targetStartCell.row,
};
Range.foreach(relativeRange, (row, col) => {
const sourcePositionRange = Rectangle.getPositionRange(
{
startRow: row,
startColumn: col,
endColumn: col,
endRow: row,
},
sourceRange
);
const targetPositionRange = Rectangle.getPositionRange(
{
startRow: row,
startColumn: col,
endColumn: col,
endRow: row,
},
targetRange
);
const { row: sourceRow, col: sourceCol } = mapFunc(sourcePositionRange.startRow, sourcePositionRange.startColumn);
const sourceCellCf = this._conditionalFormattingViewModel.getCellCfs(
unitId,
subUnitId,
sourceRow,
sourceCol
);
const { row: targetRow, col: targetCol } = mapFunc(targetPositionRange.startRow, targetPositionRange.startColumn);
const targetCellCf = this._conditionalFormattingViewModel.getCellCfs(
unitId,
subUnitId,
targetRow,
targetCol
);
if (targetCellCf) {
targetCellCf.forEach((cf) => {
if (!ensureRuleRanges(cf.cfId)) {
return;
}
getRangeDelta(cf.cfId).remove.push({
startRow: targetRow,
endRow: targetRow,
startColumn: targetCol,
endColumn: targetCol,
});
});
}
if (sourceCellCf) {
sourceCellCf.forEach((cf) => {
if (!ensureRuleRanges(cf.cfId)) {
return;
}
getRangeDelta(cf.cfId).add.push({
startRow: targetRow,
endRow: targetRow,
startColumn: targetCol,
endColumn: targetCol,
});
});
}
});
};
const generalApplyFunc = (sourceRange: IDiscreteRange, targetRange: IDiscreteRange) => {
const unitId = this._univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)?.getUnitId();
const subUnitId = this._univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)?.getActiveSheet()?.getSheetId();
@@ -177,18 +64,51 @@ export class ConditionalFormattingAutoFillController extends Disposable {
return noopReturnFunc();
}
const virtualRange = virtualizeDiscreteRanges([sourceRange, targetRange]);
const [vSourceRange, vTargetRange] = virtualRange.ranges;
const { mapFunc } = virtualRange;
const sourceStartCell = {
row: vSourceRange.startRow,
col: vSourceRange.startColumn,
};
const virtualization = virtualizeDiscreteRanges([sourceRange, targetRange]);
const [vSourceRange, vTargetRange] = virtualization.ranges;
const repeats = AutoFillTools.getAutoFillRepeatRange(vSourceRange, vTargetRange);
repeats.forEach((repeat) => {
loopFunc(sourceStartCell, repeat.repeatStartCell, repeat.relativeRange, rangeMap, rangeDeltaMap, mapFunc);
const targetRanges = repeats.flatMap((repeat) => virtualization.mapRange(Rectangle.getPositionRange(repeat.relativeRange, {
startRow: repeat.repeatStartCell.row,
endRow: repeat.repeatStartCell.row,
startColumn: repeat.repeatStartCell.col,
endColumn: repeat.repeatStartCell.col,
})));
const getRangeDelta = (cfId: string) => {
let rangeDelta = rangeDeltaMap.get(cfId);
if (!rangeDelta) {
rangeDelta = { add: [], remove: [] };
rangeDeltaMap.set(cfId, rangeDelta);
}
return rangeDelta;
};
const rules = this._conditionalFormattingRuleModel.getSubunitRules(unitId, subUnitId) ?? [];
rules.forEach((rule) => {
if (Rectangle.doAnyRangesIntersect(rule.ranges, targetRanges)) {
rangeMap.set(rule.cfId, rule.ranges);
getRangeDelta(rule.cfId).remove.push(...targetRanges);
}
const sourceRanges = rule.ranges.flatMap((range) => {
const projected = virtualization.projectRange(range);
const intersected = projected && getIntersectRange(projected, vSourceRange);
return intersected ? [Rectangle.getRelativeRange(intersected, vSourceRange)] : [];
});
const additions = repeats.flatMap((repeat) => sourceRanges.flatMap((range) => {
const copiedRange = getIntersectRange(range, repeat.relativeRange);
return copiedRange
? virtualization.mapRange(Rectangle.getPositionRange(copiedRange, {
startRow: repeat.repeatStartCell.row,
endRow: repeat.repeatStartCell.row,
startColumn: repeat.repeatStartCell.col,
endColumn: repeat.repeatStartCell.col,
}))
: [];
}));
if (additions.length) {
rangeMap.set(rule.cfId, rule.ranges);
getRangeDelta(rule.cfId).add.push(...additions);
}
});
rangeDeltaMap.forEach((rangeDelta, cfId) => {
const ranges = rangeMap.get(cfId);
@@ -17,7 +17,6 @@
import type { IRange, Nullable } from '@univerjs/core';
import type {
IAddConditionalRuleMutationParams,
IConditionalFormattingRuleConfig,
IConditionFormattingRule,
IDeleteConditionalRuleMutationParams,
ISetConditionalRuleMutationParams,
@@ -28,10 +27,7 @@ import {
Inject,
Injector,
IUniverInstanceService,
ObjectMatrix,
Range,
Rectangle,
Tools,
} from '@univerjs/core';
import {
getSheetCommandTarget,
@@ -42,7 +38,6 @@ import {
AddConditionalRuleMutationUndoFactory,
ConditionalFormattingRangeTransformService,
ConditionalFormattingRuleModel,
ConditionalFormattingViewModel,
DeleteConditionalRuleMutation,
DeleteConditionalRuleMutationUndoFactory,
SetConditionalRuleMutation,
@@ -52,11 +47,11 @@ import {
import { COPY_TYPE, getRepeatRange, ISheetClipboardService, PREDEFINED_HOOK_NAME_PASTE, virtualizeDiscreteRanges } from '@univerjs/sheets-ui';
interface ICopyInfoType {
matrix: ObjectMatrix<string[]>;
rules: Map<string, IRange[]>;
info: {
unitId: string;
subUnitId: string;
cfMap: Record<string, IConditionalFormattingRuleConfig>;
cfMap: Record<string, Pick<IConditionFormattingRule, 'rule' | 'stopIfTrue'>>;
};
}
@@ -73,7 +68,6 @@ export class ConditionalFormattingCopyPasteController extends Disposable {
@Inject(ISheetClipboardService) private _sheetClipboardService: ISheetClipboardService,
@Inject(ConditionalFormattingRuleModel) private _conditionalFormattingRuleModel: ConditionalFormattingRuleModel,
@Inject(Injector) private _injector: Injector,
@Inject(ConditionalFormattingViewModel) private _conditionalFormattingViewModel: ConditionalFormattingViewModel,
@Inject(IUniverInstanceService) private _univerInstanceService: IUniverInstanceService,
@Inject(ConditionalFormattingRangeTransformService) private _conditionalFormattingRangeTransformService: ConditionalFormattingRangeTransformService
) {
@@ -98,10 +92,10 @@ export class ConditionalFormattingCopyPasteController extends Disposable {
}
private _collectConditionalRule(unitId: string, subUnitId: string, range: IRange) {
const matrix = new ObjectMatrix<string[]>();
const cfMap: Record<string, IConditionalFormattingRuleConfig> = {};
const rules = new Map<string, IRange[]>();
const cfMap: Record<string, Pick<IConditionFormattingRule, 'rule' | 'stopIfTrue'>> = {};
this._copyInfo = {
matrix,
rules,
info: {
unitId,
subUnitId,
@@ -115,28 +109,25 @@ export class ConditionalFormattingCopyPasteController extends Disposable {
if (!discreteRange) {
return;
}
const { rows, cols } = discreteRange;
const cfIdSet: Set<string> = new Set();
rows.forEach((row, rowIndex) => {
cols.forEach((col, colIndex) => {
const cellCfList = this._conditionalFormattingViewModel.getCellCfs(unitId, subUnitId, row, col);
if (!cellCfList) {
return;
}
cellCfList.forEach((item) => cfIdSet.add(item.cfId));
matrix.setValue(rowIndex, colIndex, cellCfList.map((item) => item.cfId));
const { projectRange } = virtualizeDiscreteRanges([discreteRange]);
this._conditionalFormattingRuleModel.getSubunitRules(unitId, subUnitId)?.forEach((rule) => {
const projectedRanges = rule.ranges.flatMap((ruleRange) => {
const projected = projectRange(ruleRange);
return projected ? [projected] : [];
});
});
cfIdSet.forEach((cfId) => {
const rule = this._conditionalFormattingRuleModel.getRule(unitId, subUnitId, cfId);
if (rule) {
cfMap[cfId] = rule.rule;
if (projectedRanges.length) {
rules.set(rule.cfId, projectedRanges.length > 1 ? Rectangle.mergeRanges(projectedRanges) : projectedRanges);
cfMap[rule.cfId] = { rule: rule.rule, stopIfTrue: rule.stopIfTrue };
}
});
}
// eslint-disable-next-line max-lines-per-function
private _generateConditionalFormattingMutations(pasteFrom: ISheetDiscreteRangeLocation, pasteTo: ISheetDiscreteRangeLocation, payload: ICopyPastePayload) {
const copyInfo = this._copyInfo;
if (!copyInfo) {
return { redos: [], undos: [] };
}
const { unitId: copyUnitId, subUnitId: copySubUnitId, range: copyRange } = pasteFrom;
const { unitId: pastedUnitId, subUnitId: pastedSubUnitId, range: pastedRange } = pasteTo;
const { copyType = COPY_TYPE.COPY } = payload;
@@ -152,147 +143,112 @@ export class ConditionalFormattingCopyPasteController extends Disposable {
return { redos: [], undos: [] };
}
const { ranges: [vCopyRange, vPastedRange], mapFunc } = virtualizeDiscreteRanges([copyRange, pastedRange]);
const repeatRange = getRepeatRange(vCopyRange, vPastedRange, true);
const effectedConditionalFormattingRuleRanges: Record<string, {
const sourceVirtualization = virtualizeDiscreteRanges([copyRange]);
const sourceVirtualRange = sourceVirtualization.ranges[0];
const targetVirtualization = virtualizeDiscreteRanges([pastedRange]);
const targetVirtualRange = targetVirtualization.ranges[0];
const repeatRange = getRepeatRange(sourceVirtualRange, targetVirtualRange, true);
const targetRanges = targetVirtualization.mapRange(targetVirtualRange);
const isSameSheet = pastedUnitId === copyUnitId && pastedSubUnitId === copySubUnitId;
const effectedConditionalFormattingRuleRanges = new Map<string, {
cfId: string;
unitId: string;
subUnitId: string;
ranges: IRange[];
add: IRange[];
remove: IRange[];
}> = {};
}>();
const getEffectKey = (unitId: string, subUnitId: string, cfId: string) => JSON.stringify([unitId, subUnitId, cfId]);
// 1. delete the conditional formatting rules in the pasted range.
Range.foreach(vPastedRange, (row, col) => {
const { row: realRow, col: realCol } = mapFunc(row, col);
const cellCfList = this._conditionalFormattingViewModel.getCellCfs(pastedUnitId, pastedSubUnitId, realRow, realCol);
if (cellCfList) {
cellCfList.forEach((item) => {
if (!effectedConditionalFormattingRuleRanges[item.cfId]) {
const rule = this._conditionalFormattingRuleModel.getRule(pastedUnitId, pastedSubUnitId, item.cfId);
if (!rule) {
return;
}
effectedConditionalFormattingRuleRanges[item.cfId] = {
unitId: pastedUnitId,
subUnitId: pastedSubUnitId,
ranges: rule.ranges,
add: [],
remove: [],
};
}
const current = effectedConditionalFormattingRuleRanges[item.cfId];
current.remove.push({
startRow: realRow,
endRow: realRow,
startColumn: realCol,
endColumn: realCol,
});
});
this._conditionalFormattingRuleModel.getSubunitRules(pastedUnitId, pastedSubUnitId)?.forEach((rule) => {
if (!Rectangle.doAnyRangesIntersect(rule.ranges, targetRanges)) {
return;
}
effectedConditionalFormattingRuleRanges.set(getEffectKey(pastedUnitId, pastedSubUnitId, rule.cfId), {
cfId: rule.cfId,
unitId: pastedUnitId,
subUnitId: pastedSubUnitId,
ranges: rule.ranges,
add: [],
remove: targetRanges,
});
});
// 2. if it is cut from another worksheet, need to delete the conditional formatting rules in the copy range.
if (copyType === COPY_TYPE.CUT && (pastedUnitId !== copyUnitId || pastedSubUnitId !== copySubUnitId)) {
Range.foreach(vCopyRange, (row, col) => {
const { row: realRow, col: realCol } = mapFunc(row, col);
const cellCfList = this._conditionalFormattingViewModel.getCellCfs(copyUnitId, copySubUnitId, realRow, realCol);
if (cellCfList) {
cellCfList.forEach((item) => {
if (!effectedConditionalFormattingRuleRanges[item.cfId]) {
const rule = this._conditionalFormattingRuleModel.getRule(copyUnitId, copySubUnitId, item.cfId);
if (!rule) {
return;
}
effectedConditionalFormattingRuleRanges[item.cfId] = {
unitId: copyUnitId,
subUnitId: copySubUnitId,
ranges: rule.ranges,
add: [],
remove: [],
};
}
const current = effectedConditionalFormattingRuleRanges[item.cfId];
current.remove.push({
startRow: realRow,
endRow: realRow,
startColumn: realCol,
endColumn: realCol,
});
});
const sourceRanges = sourceVirtualization.mapRange(sourceVirtualRange);
copyInfo.rules.forEach((_ranges, cfId) => {
const rule = this._conditionalFormattingRuleModel.getRule(copyUnitId, copySubUnitId, cfId);
if (!rule) {
return;
}
effectedConditionalFormattingRuleRanges.set(getEffectKey(copyUnitId, copySubUnitId, cfId), {
cfId,
unitId: copyUnitId,
subUnitId: copySubUnitId,
ranges: rule.ranges,
add: [],
remove: sourceRanges,
});
});
}
const { matrix, info } = this._copyInfo as ICopyInfoType;
const waitAddRule: IConditionFormattingRule[] = [];
const { rules, info } = copyInfo;
const waitAddRule = new Map<string, IConditionFormattingRule>();
const cacheCfIdMap: Record<string, IConditionFormattingRule> = {};
// 3. generate the new conditional formatting rules based on the copy range's conditional formatting rules and the paste position.
const getCurrentSheetCfRule = (copyRangeCfId: string) => {
const oldRule = info?.cfMap[copyRangeCfId];
const targetRule = [...(this._conditionalFormattingRuleModel.getSubunitRules(pastedUnitId, pastedSubUnitId) || []), ...waitAddRule].find((rule) => {
return Tools.diffValue(rule.rule, oldRule);
});
if (targetRule) {
cacheCfIdMap[copyRangeCfId] = targetRule;
return targetRule;
} else {
const rule: IConditionFormattingRule = {
rule: oldRule,
cfId: this._conditionalFormattingRuleModel.createCfId(pastedUnitId, pastedSubUnitId),
ranges: [],
stopIfTrue: false,
};
cacheCfIdMap[copyRangeCfId] = rule;
waitAddRule.push(rule);
return rule;
if (isSameSheet) {
const rule = this._conditionalFormattingRuleModel.getRule(pastedUnitId, pastedSubUnitId, copyRangeCfId);
if (rule) {
cacheCfIdMap[copyRangeCfId] = rule;
return rule;
}
}
const rule: IConditionFormattingRule = {
rule: oldRule.rule,
cfId: this._conditionalFormattingRuleModel.createCfId(pastedUnitId, pastedSubUnitId),
ranges: [],
stopIfTrue: oldRule.stopIfTrue,
};
cacheCfIdMap[copyRangeCfId] = rule;
waitAddRule.set(rule.cfId, rule);
return rule;
};
repeatRange.forEach((item) => {
matrix &&
matrix.forValue((row, col, copyRangeCfIdList) => {
const range = Rectangle.getPositionRange(
{
startRow: row,
endRow: row,
startColumn: col,
endColumn: col,
},
item.startRange
);
const { row: _row, col: _col } = mapFunc(range.startRow, range.startColumn);
copyRangeCfIdList.forEach((cfId) => {
const rule = cacheCfIdMap[cfId] || getCurrentSheetCfRule(cfId);
if (!effectedConditionalFormattingRuleRanges[rule.cfId]) {
effectedConditionalFormattingRuleRanges[rule.cfId] = {
unitId: pastedUnitId,
subUnitId: pastedSubUnitId,
ranges: rule.ranges,
add: [],
remove: [],
};
}
const current = effectedConditionalFormattingRuleRanges[rule.cfId];
current.add.push({
startRow: _row,
endRow: _row,
startColumn: _col,
endColumn: _col,
});
});
const sourceRuleEntries = Array.from(rules.entries());
if (!isSameSheet) {
// AddRule prepends, so emit lower-priority cross-sheet clones first.
sourceRuleEntries.reverse();
}
sourceRuleEntries.forEach(([cfId, sourceRanges]) => {
const rule = cacheCfIdMap[cfId] || getCurrentSheetCfRule(cfId);
const effectKey = getEffectKey(pastedUnitId, pastedSubUnitId, rule.cfId);
if (!effectedConditionalFormattingRuleRanges.has(effectKey)) {
effectedConditionalFormattingRuleRanges.set(effectKey, {
cfId: rule.cfId,
unitId: pastedUnitId,
subUnitId: pastedSubUnitId,
ranges: rule.ranges,
add: [],
remove: [],
});
}
const current = effectedConditionalFormattingRuleRanges.get(effectKey)!;
current.add.push(...repeatRange.flatMap((item) => sourceRanges.flatMap((sourceRange) => (
targetVirtualization.mapRange(Rectangle.getPositionRange(sourceRange, item.startRange))
))));
});
const redos = [];
const undos = [];
for (const cfId in effectedConditionalFormattingRuleRanges) {
const { unitId, subUnitId, ranges: sourceRanges, add, remove } = effectedConditionalFormattingRuleRanges[cfId];
for (const effect of effectedConditionalFormattingRuleRanges.values()) {
const { cfId, unitId, subUnitId, ranges: sourceRanges, add, remove } = effect;
const ranges = this._conditionalFormattingRangeTransformService.applyRangeDelta(sourceRanges, remove, add);
if (!ranges.length) {
@@ -303,14 +259,15 @@ export class ConditionalFormattingCopyPasteController extends Disposable {
};
redos.push({ id: DeleteConditionalRuleMutation.id, params: deleteParams });
undos.push(...DeleteConditionalRuleMutationUndoFactory(this._injector, deleteParams));
continue;
}
if (waitAddRule.some((rule) => rule.cfId === cfId)) {
const rule = waitAddRule.find((rule) => rule.cfId === cfId) as IConditionFormattingRule;
const waitAdd = waitAddRule.get(cfId);
if (waitAdd) {
const addParams: IAddConditionalRuleMutationParams = {
unitId: pastedUnitId,
subUnitId: pastedSubUnitId,
rule: { ...rule, ranges },
rule: { ...waitAdd, ranges },
};
redos.push({ id: AddConditionalRuleMutation.id, params: addParams });
undos.push(AddConditionalRuleMutationUndoFactory(this._injector, addParams));
@@ -17,18 +17,18 @@
import type { IMutationInfo, IRange, Nullable, Workbook } from '@univerjs/core';
import type {
IAddConditionalRuleMutationParams,
IConditionFormattingRule,
IDeleteConditionalRuleMutationParams,
ISetConditionalRuleMutationParams,
} from '@univerjs/sheets-conditional-formatting';
import type { IFormatPainterHook } from '@univerjs/sheets-ui';
import { Disposable, Inject, Injector, IUniverInstanceService, Range, Rectangle, Tools, UniverInstanceType } from '@univerjs/core';
import { Disposable, getIntersectRange, Inject, Injector, IUniverInstanceService, Rectangle, Tools, UniverInstanceType } from '@univerjs/core';
import { SheetsSelectionsService } from '@univerjs/sheets';
import {
AddConditionalRuleMutation,
AddConditionalRuleMutationUndoFactory,
ConditionalFormattingRangeTransformService,
ConditionalFormattingRuleModel,
ConditionalFormattingViewModel,
DeleteConditionalRuleMutation,
DeleteConditionalRuleMutationUndoFactory,
SetConditionalRuleMutation,
@@ -101,7 +101,6 @@ export class ConditionalFormattingPainterController extends Disposable {
@Inject(IFormatPainterService) private _formatPainterService: IFormatPainterService,
@Inject(SheetsSelectionsService) private _sheetsSelectionsService: SheetsSelectionsService,
@Inject(ConditionalFormattingRuleModel) private _conditionalFormattingRuleModel: ConditionalFormattingRuleModel,
@Inject(ConditionalFormattingViewModel) private _conditionalFormattingViewModel: ConditionalFormattingViewModel,
@Inject(ConditionalFormattingRangeTransformService) private _conditionalFormattingRangeTransformService: ConditionalFormattingRangeTransformService
) {
@@ -114,108 +113,6 @@ export class ConditionalFormattingPainterController extends Disposable {
private _initFormattingPainter() {
const noopReturnFunc = () => ({ redos: [], undos: [] });
const loopFunc = (
sourceStartCell: { row: number; col: number },
targetStartCell: { row: number; col: number },
relativeRange: IRange,
rangeMap: Map<string, IRange[]>,
rangeDeltaMap: Map<string, IRangeDelta>,
config: {
targetUnitId: string;
targetSubUnitId: string;
}
) => {
const { unitId: sourceUnitId, subUnitId: sourceSubUnitId } = this._painterConfig!;
const { targetUnitId, targetSubUnitId } = config;
const getRangeDelta = (cfId: string) => {
let rangeDelta = rangeDeltaMap.get(cfId);
if (!rangeDelta) {
rangeDelta = { add: [], remove: [] };
rangeDeltaMap.set(cfId, rangeDelta);
}
return rangeDelta;
};
const sourceRange = {
startRow: sourceStartCell.row,
startColumn: sourceStartCell.col,
endColumn: sourceStartCell.col,
endRow: sourceStartCell.row,
};
const targetRange = {
startRow: targetStartCell.row,
startColumn: targetStartCell.col,
endColumn: targetStartCell.col,
endRow: targetStartCell.row,
};
Range.foreach(relativeRange, (row, col) => {
const sourcePositionRange = Rectangle.getPositionRange(
{
startRow: row,
startColumn: col,
endColumn: col,
endRow: row,
},
sourceRange
);
const targetPositionRange = Rectangle.getPositionRange(
{
startRow: row,
startColumn: col,
endColumn: col,
endRow: row,
},
targetRange
);
const sourceCellCf = this._conditionalFormattingViewModel.getCellCfs(
sourceUnitId,
sourceSubUnitId,
sourcePositionRange.startRow,
sourcePositionRange.startColumn
);
const targetCellCf = this._conditionalFormattingViewModel.getCellCfs(
targetUnitId,
targetSubUnitId,
targetPositionRange.startRow,
targetPositionRange.startColumn
);
if (targetCellCf) {
targetCellCf.forEach((cf) => {
if (!rangeMap.has(cf.cfId)) {
const rule = this._conditionalFormattingRuleModel.getRule(targetUnitId, targetSubUnitId, cf.cfId);
if (!rule) {
return;
}
rangeMap.set(cf.cfId, rule.ranges);
}
getRangeDelta(cf.cfId).remove.push({
startRow: targetPositionRange.startRow,
endRow: targetPositionRange.startRow,
startColumn: targetPositionRange.startColumn,
endColumn: targetPositionRange.startColumn,
});
});
}
if (sourceCellCf) {
sourceCellCf.forEach((cf) => {
if (!rangeMap.has(cf.cfId)) {
return;
}
getRangeDelta(cf.cfId).add.push({
startRow: targetPositionRange.startRow,
endRow: targetPositionRange.startRow,
startColumn: targetPositionRange.startColumn,
endColumn: targetPositionRange.startColumn,
});
});
}
});
};
// eslint-disable-next-line max-lines-per-function
const generalApplyFunc = (targetUnitId: string, targetSubUnitId: string, targetRange: IRange) => {
const { range: sourceRange, unitId: sourceUnitId, subUnitId: sourceSubUnitId } = this._painterConfig!;
@@ -228,23 +125,53 @@ export class ConditionalFormattingPainterController extends Disposable {
if (!targetUnitId || !targetSubUnitId || !sourceUnitId || !sourceSubUnitId) {
return noopReturnFunc();
}
const ruleList = this._conditionalFormattingRuleModel.getSubunitRules(sourceUnitId, sourceSubUnitId) ?? [];
ruleList?.forEach((rule) => {
const { ranges, cfId } = rule;
if (ranges.some((range) => Rectangle.intersects(sourceRange, range))) {
rangeMap.set(cfId, isSkipSheet ? [] : ranges);
const repeats = repeatByRange(sourceRange, targetRange);
const targetRanges = repeats.map((repeat) => Rectangle.getPositionRange(repeat.repeatRelativeRange, repeat.startRange));
const getRangeDelta = (cfId: string) => {
let rangeDelta = rangeDeltaMap.get(cfId);
if (!rangeDelta) {
rangeDelta = { add: [], remove: [] };
rangeDeltaMap.set(cfId, rangeDelta);
}
return rangeDelta;
};
const targetRuleList = this._conditionalFormattingRuleModel.getSubunitRules(targetUnitId, targetSubUnitId) ?? [];
const waitAddRule = new Map<string, IConditionFormattingRule>();
targetRuleList.forEach((rule) => {
if (Rectangle.doAnyRangesIntersect(rule.ranges, targetRanges)) {
rangeMap.set(rule.cfId, rule.ranges);
getRangeDelta(rule.cfId).remove.push(...targetRanges);
}
});
const sourceStartCell = {
row: sourceRange.startRow,
col: sourceRange.startColumn,
};
const repeats = repeatByRange(sourceRange, targetRange);
repeats.forEach((repeat) => {
loopFunc(sourceStartCell, { row: repeat.startRange.startRow, col: repeat.startRange.startColumn }, repeat.repeatRelativeRange, rangeMap, rangeDeltaMap, { targetUnitId, targetSubUnitId });
const sourceRuleList = this._conditionalFormattingRuleModel.getSubunitRules(sourceUnitId, sourceSubUnitId) ?? [];
const sourceRules = isSkipSheet ? [...sourceRuleList].reverse() : sourceRuleList;
sourceRules.forEach((rule) => {
const sourceRanges = rule.ranges.flatMap((range) => {
const intersected = getIntersectRange(range, sourceRange);
return intersected ? [Rectangle.getRelativeRange(intersected, sourceRange)] : [];
});
const additions = repeats.flatMap((repeat) => sourceRanges.flatMap((range) => {
const copiedRange = getIntersectRange(range, repeat.repeatRelativeRange);
return copiedRange ? [Rectangle.getPositionRange(copiedRange, repeat.startRange)] : [];
}));
if (additions.length) {
let targetCfId = rule.cfId;
if (isSkipSheet) {
targetCfId = this._conditionalFormattingRuleModel.createCfId(targetUnitId, targetSubUnitId);
waitAddRule.set(targetCfId, {
...Tools.deepClone(rule),
cfId: targetCfId,
ranges: [],
});
rangeMap.set(targetCfId, []);
}
if (!rangeMap.has(targetCfId)) {
rangeMap.set(targetCfId, rule.ranges);
}
getRangeDelta(targetCfId).add.push(...additions);
}
});
rangeDeltaMap.forEach((rangeDelta, cfId) => {
const ranges = rangeMap.get(cfId);
@@ -283,25 +210,21 @@ export class ConditionalFormattingPainterController extends Disposable {
undos.push(...DeleteConditionalRuleMutationUndoFactory(this._injector, params));
}
} else {
const rule = this._conditionalFormattingRuleModel.getRule(targetUnitId, targetSubUnitId, cfId);
if (!rule) {
const waitAdd = waitAddRule.get(cfId);
if (waitAdd) {
if (ranges.length) {
const sourceRule = this._conditionalFormattingRuleModel.getRule(sourceUnitId, sourceSubUnitId, cfId);
if (sourceRule) {
const params: IAddConditionalRuleMutationParams = {
unitId: targetUnitId,
subUnitId: targetSubUnitId,
rule: {
...Tools.deepClone(sourceRule),
cfId: this._conditionalFormattingRuleModel.createCfId(targetUnitId, targetSubUnitId),
ranges,
},
};
redos.push({ id: AddConditionalRuleMutation.id, params });
undos.push(AddConditionalRuleMutationUndoFactory(this._injector, params));
}
const params: IAddConditionalRuleMutationParams = {
unitId: targetUnitId,
subUnitId: targetSubUnitId,
rule: { ...waitAdd, ranges },
};
redos.push({ id: AddConditionalRuleMutation.id, params });
undos.push(AddConditionalRuleMutationUndoFactory(this._injector, params));
}
} else {
return;
}
const rule = this._conditionalFormattingRuleModel.getRule(targetUnitId, targetSubUnitId, cfId);
if (rule) {
if (ranges.length) {
const params: ISetConditionalRuleMutationParams = {
unitId: targetUnitId,
@@ -0,0 +1,107 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 { IWorkbookData, Workbook } from '@univerjs/core';
import type { ISheetClipboardHook } from '@univerjs/sheets-ui';
import {
Disposable,
ICommandService,
ILogService,
IUniverInstanceService,
LocaleService,
LocaleType,
LogLevel,
toDisposable,
Tools,
Univer,
UniverInstanceType,
} from '@univerjs/core';
import { UniverDataValidationPlugin } from '@univerjs/data-validation';
import { IAutoFillService, UniverSheetsPlugin } from '@univerjs/sheets';
import { SheetDataValidationModel, UniverSheetsDataValidationPlugin } from '@univerjs/sheets-data-validation';
import { ISheetClipboardService } from '@univerjs/sheets-ui';
import enUS from '@univerjs/sheets/locale/en-US';
const TEST_WORKBOOK_DATA: IWorkbookData = {
id: 'test',
appVersion: '3.0.0-alpha',
locale: LocaleType.EN_US,
name: '',
sheetOrder: ['sheet1', 'sheet2'],
styles: {},
sheets: {
sheet1: {
id: 'sheet1',
name: 'Sheet1',
rowCount: 200_000,
columnCount: 2_000,
cellData: {},
},
sheet2: {
id: 'sheet2',
name: 'Sheet2',
rowCount: 100,
columnCount: 100,
cellData: {},
},
},
};
class TestSheetClipboardService extends Disposable {
private _hooks: ISheetClipboardHook[] = [];
addClipboardHook(hook: ISheetClipboardHook) {
this._hooks.push(hook);
return toDisposable(() => {
this._hooks = this._hooks.filter((item) => item !== hook);
});
}
getHooks() {
return this._hooks;
}
}
export function createDvUiTestBed() {
const univer = new Univer();
const injector = univer.__getInjector();
const get = injector.get.bind(injector);
univer.registerPlugin(UniverDataValidationPlugin);
univer.registerPlugin(UniverSheetsPlugin, { notExecuteFormula: true });
univer.registerPlugin(UniverSheetsDataValidationPlugin);
const workbook = univer.createUnit<IWorkbookData, Workbook>(UniverInstanceType.UNIVER_SHEET, Tools.deepClone(TEST_WORKBOOK_DATA));
get(IUniverInstanceService).focusUnit(workbook.getUnitId());
get(ILogService).setLogLevel(LogLevel.SILENT);
get(LocaleService).load({ enUS });
get(LocaleService).setLocale(LocaleType.EN_US);
injector.add([ISheetClipboardService, { useClass: TestSheetClipboardService as never }]);
const clipboardService = get(ISheetClipboardService) as unknown as TestSheetClipboardService;
return {
univer,
injector,
workbook,
commandService: get(ICommandService),
dataValidationModel: get(SheetDataValidationModel),
autoFillService: get(IAutoFillService),
getClipboardHook: () => clipboardService.getHooks()[0],
unitId: 'test',
subUnitId: 'sheet1',
};
}
@@ -14,120 +14,122 @@
* limitations under the License.
*/
import { DataValidationType } from '@univerjs/core';
import { AUTO_FILL_APPLY_TYPE, AutoFillTools } from '@univerjs/sheets';
import { DATA_VALIDATION_PLUGIN_NAME, getDataValidationDiffMutations } from '@univerjs/sheets-data-validation';
import { virtualizeDiscreteRanges } from '@univerjs/sheets-ui';
import { describe, expect, it, vi } from 'vitest';
import type { ISheetAutoFillHook } from '@univerjs/sheets';
import { DataValidationType, Direction, Range } from '@univerjs/core';
import { AddDataValidationMutation } from '@univerjs/data-validation';
import { AUTO_FILL_APPLY_TYPE } from '@univerjs/sheets';
import { DATA_VALIDATION_PLUGIN_NAME } from '@univerjs/sheets-data-validation';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { createDvUiTestBed } from '../../__tests__/create-dv-ui-test-bed';
import { DataValidationAutoFillController } from '../dv-auto-fill.controller';
vi.mock('@univerjs/sheets-ui', async (importActual) => {
const actual = await importActual<typeof import('@univerjs/sheets-ui')>();
return {
...actual,
virtualizeDiscreteRanges: vi.fn(() => ({
ranges: [
{ startRow: 0, startColumn: 0, endRow: 0, endColumn: 0 },
{ startRow: 1, startColumn: 1, endRow: 1, endColumn: 1 },
],
mapFunc: (row: number, col: number) => ({ row, col }),
})),
};
});
vi.mock('@univerjs/sheets-data-validation', async (importActual) => {
const actual = await importActual<typeof import('@univerjs/sheets-data-validation')>();
return {
...actual,
getDataValidationDiffMutations: vi.fn(() => ({ redoMutations: ['redo-dv'], undoMutations: ['undo-dv'] })),
};
});
vi.mock('@univerjs/sheets', async (importActual) => {
const actual = await importActual<typeof import('@univerjs/sheets')>();
return {
...actual,
AutoFillTools: {
...actual.AutoFillTools,
getAutoFillRepeatRange: vi.fn(() => [{
repeatStartCell: { row: 1, col: 1 },
relativeRange: { startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 },
}]),
},
};
});
describe('DataValidationAutoFillController', () => {
it('registers a hook and disables series fill when checkbox validation exists in source cells', () => {
let hook: { id: string; onBeforeFillData: (location: { unitId: string; subUnitId: string; source: { rows: number[]; cols: number[] } }) => void } | undefined;
const autoFillService = {
addHook: vi.fn((registeredHook) => {
hook = registeredHook;
return { dispose: vi.fn() };
}),
setDisableApplyType: vi.fn(),
};
let testBed: ReturnType<typeof createDvUiTestBed>;
let hook: ISheetAutoFillHook;
const controller = new DataValidationAutoFillController(
autoFillService as never,
{
getRuleByLocation: vi.fn(() => ({ type: DataValidationType.CHECKBOX })),
} as never,
{} as never
);
beforeEach(() => {
testBed = createDvUiTestBed();
testBed.injector.add([DataValidationAutoFillController]);
testBed.injector.get(DataValidationAutoFillController);
const registeredHook = testBed.autoFillService.getAllHooks().find((item) => item.id === DATA_VALIDATION_PLUGIN_NAME);
if (!registeredHook) {
throw new Error('Data validation autofill hook was not registered');
}
hook = registeredHook;
});
expect(controller).toBeTruthy();
expect(hook?.id).toBe(DATA_VALIDATION_PLUGIN_NAME);
afterEach(() => {
vi.restoreAllMocks();
testBed.univer.dispose();
});
hook!.onBeforeFillData({
unitId: 'book-1',
subUnitId: 'sheet-1',
source: { rows: [0], cols: [0] },
it('disables series fill for checkbox rules without querying each source cell', async () => {
await testBed.commandService.executeCommand(AddDataValidationMutation.id, {
unitId: testBed.unitId,
subUnitId: testBed.subUnitId,
rule: {
uid: 'checkbox-rule',
type: DataValidationType.CHECKBOX,
ranges: [{ startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 }],
},
});
vi.spyOn(testBed.dataValidationModel, 'getRuleByLocation').mockImplementation(() => {
throw new Error('must not query validation by cell');
});
expect(autoFillService.setDisableApplyType).toHaveBeenCalledWith(AUTO_FILL_APPLY_TYPE.SERIES, true);
hook.onBeforeFillData?.({
unitId: testBed.unitId,
subUnitId: testBed.subUnitId,
source: {
rows: Array.from({ length: 100_000 }, (_, index) => index),
cols: Array.from({ length: 1_000 }, (_, index) => index),
},
target: { rows: [100_000], cols: [0] },
}, Direction.DOWN);
expect(testBed.autoFillService.menu.find((item) => item.value === AUTO_FILL_APPLY_TYPE.SERIES)?.disable).toBe(true);
});
it('builds diff mutations for copy and format autofill, and skips unsupported apply types', () => {
let hook: { onFillData: (location: { unitId: string; subUnitId: string; source: { rows: number[]; cols: number[]; startRow: number; endRow: number; startColumn: number; endColumn: number }; target: { rows: number[]; cols: number[]; startRow: number; endRow: number; startColumn: number; endColumn: number } }, direction: unknown, applyType: AUTO_FILL_APPLY_TYPE) => { redos: unknown[]; undos: unknown[] } } | undefined;
const ruleMatrixCopy = {
addRangeRules: vi.fn(),
diff: vi.fn(() => 'diffs'),
};
it('ignores checkbox rules that only cover filtered-out source rows', async () => {
await testBed.commandService.executeCommand(AddDataValidationMutation.id, {
unitId: testBed.unitId,
subUnitId: testBed.subUnitId,
rule: {
uid: 'filtered-checkbox-rule',
type: DataValidationType.CHECKBOX,
ranges: [{ startRow: 1, endRow: 1, startColumn: 0, endColumn: 0 }],
},
});
vi.spyOn(testBed.dataValidationModel, 'getRuleByLocation').mockImplementation(() => {
throw new Error('must not query validation by cell');
});
const controller = new DataValidationAutoFillController(
{
addHook: vi.fn((registeredHook) => {
hook = registeredHook;
return { dispose: vi.fn() };
}),
setDisableApplyType: vi.fn(),
} as never,
{
getRuleByLocation: vi.fn(() => null),
getRuleObjectMatrix: vi.fn(() => ({ clone: vi.fn(() => ruleMatrixCopy) })),
getRuleIdByLocation: vi.fn(() => 'rule-1'),
getRules: vi.fn(() => ['existing-rule']),
} as never,
{} as never
);
hook.onBeforeFillData?.({
unitId: testBed.unitId,
subUnitId: testBed.subUnitId,
source: { rows: [0, 2], cols: [0] },
target: { rows: [3], cols: [0] },
}, Direction.DOWN);
expect(controller).toBeTruthy();
expect(testBed.autoFillService.menu.find((item) => item.value === AUTO_FILL_APPLY_TYPE.SERIES)?.disable).toBe(false);
});
it('executes and undoes real validation mutations without enumerating autofill cells', async () => {
await testBed.commandService.executeCommand(AddDataValidationMutation.id, {
unitId: testBed.unitId,
subUnitId: testBed.subUnitId,
rule: {
uid: 'decimal-rule',
type: DataValidationType.DECIMAL,
formula1: '1',
ranges: [{ startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 }],
},
});
const foreach = vi.spyOn(Range, 'foreach').mockImplementation(() => {
throw new Error('must not enumerate cells');
});
const location = {
unitId: 'book-1',
subUnitId: 'sheet-1',
source: { rows: [0], cols: [0], startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 },
target: { rows: [1], cols: [1], startRow: 1, endRow: 1, startColumn: 1, endColumn: 1 },
unitId: testBed.unitId,
subUnitId: testBed.subUnitId,
source: { rows: [0], cols: [0] },
target: { rows: [1], cols: [0] },
};
const result = hook!.onFillData(location, 'down', AUTO_FILL_APPLY_TYPE.ONLY_FORMAT);
expect(vi.mocked(virtualizeDiscreteRanges)).toHaveBeenCalledWith([location.source, location.target]);
expect(vi.mocked(AutoFillTools.getAutoFillRepeatRange)).toHaveBeenCalled();
expect(ruleMatrixCopy.addRangeRules).toHaveBeenCalledWith([{ id: 'rule-1', ranges: [{ startRow: 1, endRow: 1, startColumn: 1, endColumn: 1 }] }]);
expect(vi.mocked(getDataValidationDiffMutations)).toHaveBeenCalledWith('book-1', 'sheet-1', 'diffs', {}, 'patched', true);
expect(result).toEqual({ redos: ['redo-dv'], undos: ['undo-dv'] });
const result = hook.onFillData?.(location, Direction.DOWN, AUTO_FILL_APPLY_TYPE.COPY);
if (!result) {
throw new Error('Data validation autofill did not return mutations');
}
foreach.mockRestore();
for (const mutation of result.redos) {
await testBed.commandService.executeCommand(mutation.id, mutation.params);
}
expect(testBed.dataValidationModel.getRuleByLocation(testBed.unitId, testBed.subUnitId, 0, 0)?.uid).toBe('decimal-rule');
expect(testBed.dataValidationModel.getRuleByLocation(testBed.unitId, testBed.subUnitId, 1, 0)?.uid).toBe('decimal-rule');
expect(hook!.onFillData(location, 'down', AUTO_FILL_APPLY_TYPE.NO_FORMAT)).toEqual({ redos: [], undos: [] });
for (const mutation of result.undos) {
await testBed.commandService.executeCommand(mutation.id, mutation.params);
}
expect(testBed.dataValidationModel.getRuleByLocation(testBed.unitId, testBed.subUnitId, 0, 0)?.uid).toBe('decimal-rule');
expect(testBed.dataValidationModel.getRuleByLocation(testBed.unitId, testBed.subUnitId, 1, 0)).toBeUndefined();
});
});
@@ -14,8 +14,9 @@
* limitations under the License.
*/
import type { IRange } from '@univerjs/core';
import type { IAutoFillLocation, ISheetAutoFillHook } from '@univerjs/sheets';
import { DataValidationType, Disposable, Inject, Injector, ObjectMatrix, queryObjectMatrix, Range, Rectangle } from '@univerjs/core';
import { DataValidationType, Disposable, getIntersectRange, Inject, Injector, Rectangle } from '@univerjs/core';
import { AUTO_FILL_APPLY_TYPE, AutoFillTools, IAutoFillService } from '@univerjs/sheets';
import { DATA_VALIDATION_PLUGIN_NAME, getDataValidationDiffMutations, SheetDataValidationModel } from '@univerjs/sheets-data-validation';
import { virtualizeDiscreteRanges } from '@univerjs/sheets-ui';
@@ -42,58 +43,44 @@ export class DataValidationAutoFillController extends Disposable {
const virtualRange = virtualizeDiscreteRanges([sourceRange, targetRange]);
const [vSourceRange, vTargetRange] = virtualRange.ranges;
const { mapFunc } = virtualRange;
const sourceStartCell = {
row: vSourceRange.startRow,
col: vSourceRange.startColumn,
};
const { mapRange, projectRange } = virtualRange;
const repeats = AutoFillTools.getAutoFillRepeatRange(vSourceRange, vTargetRange);
const additionMatrix = new ObjectMatrix();
const additionRules = new Set<string>();
repeats.forEach((repeat) => {
const targetStartCell = repeat.repeatStartCell;
const relativeRange = repeat.relativeRange;
const sourceRange = {
startRow: sourceStartCell.row,
startColumn: sourceStartCell.col,
endColumn: sourceStartCell.col,
endRow: sourceStartCell.row,
};
const targetRange = {
startRow: targetStartCell.row,
startColumn: targetStartCell.col,
endColumn: targetStartCell.col,
endRow: targetStartCell.row,
};
Range.foreach(relativeRange, (row, col) => {
const sourcePositionRange = Rectangle.getPositionRange(
{
startRow: row,
startColumn: col,
endColumn: col,
endRow: row,
},
sourceRange
);
const { row: sourceRow, col: sourceCol } = mapFunc(sourcePositionRange.startRow, sourcePositionRange.startColumn);
// if ruleId exists, set more dv rules, if not, clear dv rules.
const ruleId = this._sheetDataValidationModel.getRuleIdByLocation(unitId, subUnitId, sourceRow, sourceCol) || '';
const targetPositionRange = Rectangle.getPositionRange(
{
startRow: row,
startColumn: col,
endColumn: col,
endRow: row,
},
targetRange
);
const { row: targetRow, col: targetCol } = mapFunc(targetPositionRange.startRow, targetPositionRange.startColumn);
const additionsByRuleId = new Map<string, IRange[]>();
additionsByRuleId.set('', repeats.flatMap((repeat) => mapRange(Rectangle.getPositionRange(repeat.relativeRange, {
startRow: repeat.repeatStartCell.row,
endRow: repeat.repeatStartCell.row,
startColumn: repeat.repeatStartCell.col,
endColumn: repeat.repeatStartCell.col,
}))));
additionMatrix.setValue(targetRow, targetCol, ruleId);
additionRules.add(ruleId);
this._sheetDataValidationModel.getRules(unitId, subUnitId).forEach((rule) => {
const relativeSourceRanges = rule.ranges.flatMap((range) => {
const projected = projectRange(range);
const intersected = projected && getIntersectRange(projected, vSourceRange);
return intersected
? [Rectangle.getRelativeRange(intersected, vSourceRange)]
: [];
});
const targetRanges = repeats.flatMap((repeat) => relativeSourceRanges.flatMap((sourceRange) => {
const copiedRange = getIntersectRange(sourceRange, repeat.relativeRange);
if (!copiedRange) {
return [];
}
return mapRange(Rectangle.getPositionRange(copiedRange, {
startRow: repeat.repeatStartCell.row,
endRow: repeat.repeatStartCell.row,
startColumn: repeat.repeatStartCell.col,
endColumn: repeat.repeatStartCell.col,
}));
}));
if (targetRanges.length) {
additionsByRuleId.set(rule.uid, targetRanges);
}
});
const additions = Array.from(additionRules).map((id) => ({ id, ranges: queryObjectMatrix(additionMatrix, (value) => value === id) }));
const additions = Array.from(additionsByRuleId, ([id, ranges]) => ({
id,
ranges: ranges.length > 1 ? Rectangle.mergeRanges(ranges) : ranges,
}));
ruleMatrixCopy.addRangeRules(additions);
const diffs = ruleMatrixCopy.diff(this._sheetDataValidationModel.getRules(unitId, subUnitId));
const { redoMutations, undoMutations } = getDataValidationDiffMutations(unitId, subUnitId, diffs, this._injector, 'patched', applyType === AUTO_FILL_APPLY_TYPE.ONLY_FORMAT);
@@ -106,14 +93,12 @@ export class DataValidationAutoFillController extends Disposable {
id: DATA_VALIDATION_PLUGIN_NAME,
onBeforeFillData: (location) => {
const { source: sourceRange, unitId, subUnitId } = location;
for (const row of sourceRange.rows) {
for (const col of sourceRange.cols) {
const dv = this._sheetDataValidationModel.getRuleByLocation(unitId, subUnitId, row, col);
if (dv && dv.type === DataValidationType.CHECKBOX) {
this._autoFillService.setDisableApplyType(AUTO_FILL_APPLY_TYPE.SERIES, true);
return;
}
}
const { projectRange } = virtualizeDiscreteRanges([sourceRange]);
const hasCheckbox = this._sheetDataValidationModel.getRules(unitId, subUnitId).some((rule) => (
rule.type === DataValidationType.CHECKBOX && rule.ranges.some((range) => projectRange(range) !== null)
));
if (hasCheckbox) {
this._autoFillService.setDisableApplyType(AUTO_FILL_APPLY_TYPE.SERIES, true);
}
},
onFillData: (location, direction, applyType) => {
@@ -16,13 +16,13 @@
import type { IRange, ISheetDataValidationRule, Nullable } from '@univerjs/core';
import type { ICopyPastePayload, IPasteHookValueType, ISheetDiscreteRangeLocation } from '@univerjs/sheets-ui';
import { Disposable, Inject, Injector, IUniverInstanceService, ObjectMatrix, queryObjectMatrix, Rectangle } from '@univerjs/core';
import { Disposable, Inject, Injector, IUniverInstanceService, Rectangle } from '@univerjs/core';
import { getSheetCommandTarget, rangeToDiscreteRange } from '@univerjs/sheets';
import { DATA_VALIDATION_PLUGIN_NAME, getDataValidationDiffMutations, SheetDataValidationModel } from '@univerjs/sheets-data-validation';
import { COPY_TYPE, getRepeatRange, ISheetClipboardService, PREDEFINED_HOOK_NAME_PASTE, virtualizeDiscreteRanges } from '@univerjs/sheets-ui';
interface ICopyInfoType {
matrix: ObjectMatrix<string>;
rules: Map<string, IRange[]>;
unitId: string;
subUnitId: string;
}
@@ -61,11 +61,11 @@ export class DataValidationCopyPasteController extends Disposable {
}
private _collect(unitId: string, subUnitId: string, range: IRange) {
const matrix = new ObjectMatrix<string>();
const rules = new Map<string, IRange[]>();
this._copyInfo = {
unitId,
subUnitId,
matrix,
rules,
};
const discreteRange = this._injector.invoke((accessor) => {
@@ -74,17 +74,24 @@ export class DataValidationCopyPasteController extends Disposable {
if (!discreteRange) {
return;
}
const { rows, cols } = discreteRange;
rows.forEach((row, rowIndex) => {
cols.forEach((col, colIndex) => {
const ruleId = this._sheetDataValidationModel.getRuleIdByLocation(unitId, subUnitId, row, col);
matrix.setValue(rowIndex, colIndex, ruleId ?? '');
const { projectRange } = virtualizeDiscreteRanges([discreteRange]);
this._sheetDataValidationModel.getRules(unitId, subUnitId).forEach((rule) => {
const projectedRanges = rule.ranges.flatMap((ruleRange) => {
const projected = projectRange(ruleRange);
return projected ? [projected] : [];
});
if (projectedRanges.length) {
rules.set(rule.uid, projectedRanges.length > 1 ? Rectangle.mergeRanges(projectedRanges) : projectedRanges);
}
});
}
// eslint-disable-next-line max-lines-per-function
private _generateMutations(pasteFrom: ISheetDiscreteRangeLocation, pasteTo: ISheetDiscreteRangeLocation, payload: ICopyPastePayload) {
const copyInfo = this._copyInfo;
if (!copyInfo) {
return { redos: [], undos: [] };
}
const { unitId: copyUnitId, subUnitId: copySubUnitId, range: copyRange } = pasteFrom;
const { unitId: pastedUnitId, subUnitId: pastedSubUnitId, range: pastedRange } = pasteTo;
const { copyType = COPY_TYPE.COPY } = payload;
@@ -100,41 +107,29 @@ export class DataValidationCopyPasteController extends Disposable {
return { redos: [], undos: [] };
}
const sourceVirtualRange = virtualizeDiscreteRanges([copyRange]).ranges[0];
const targetVirtualization = virtualizeDiscreteRanges([pastedRange]);
const targetVirtualRange = targetVirtualization.ranges[0];
const repeatRange = getRepeatRange(sourceVirtualRange, targetVirtualRange, true);
const clearTargetRanges = targetVirtualization.mapRange(targetVirtualRange);
const getTargetRanges = (sourceRanges: IRange[]) => repeatRange.flatMap(({ startRange }) => sourceRanges.flatMap((sourceRange) => (
targetVirtualization.mapRange(Rectangle.getPositionRange(sourceRange, startRange))
)));
if (pastedUnitId !== copyUnitId || pastedSubUnitId !== copySubUnitId) {
const ruleMatrix = this._sheetDataValidationModel.getRuleObjectMatrix(pastedUnitId, pastedSubUnitId).clone();
const additionMatrix = new ObjectMatrix();
const addRules = new Set<string>();
const { ranges: [vCopyRange, vPastedRange], mapFunc } = virtualizeDiscreteRanges([copyRange, pastedRange]);
const repeatRange = getRepeatRange(vCopyRange, vPastedRange, true);
const additionRules: Map<string, ISheetDataValidationRule> = new Map();
const additions = [{ id: '', ranges: clearTargetRanges }];
repeatRange.forEach(({ startRange }) => {
this._copyInfo?.matrix.forValue((row, col, ruleId) => {
const range = Rectangle.getPositionRange(
{
startRow: row,
endRow: row,
startColumn: col,
endColumn: col,
},
startRange
);
const transformedRuleId = `${copySubUnitId}-${ruleId}`;
const oldRule = this._sheetDataValidationModel.getRuleById(copyUnitId, copySubUnitId, ruleId);
if (!this._sheetDataValidationModel.getRuleById(pastedUnitId, pastedSubUnitId, transformedRuleId) && oldRule) {
additionRules.set(transformedRuleId, { ...oldRule, uid: transformedRuleId });
}
const { row: startRow, col: startColumn } = mapFunc(range.startRow, range.startColumn);
addRules.add(transformedRuleId);
additionMatrix.setValue(startRow, startColumn, transformedRuleId);
});
copyInfo.rules.forEach((ranges, ruleId) => {
const transformedRuleId = `${copySubUnitId}-${ruleId}`;
const oldRule = this._sheetDataValidationModel.getRuleById(copyUnitId, copySubUnitId, ruleId);
if (!this._sheetDataValidationModel.getRuleById(pastedUnitId, pastedSubUnitId, transformedRuleId) && oldRule) {
additionRules.set(transformedRuleId, { ...oldRule, uid: transformedRuleId });
}
additions.push({ id: transformedRuleId, ranges: getTargetRanges(ranges) });
});
const additions = Array.from(addRules).map((id) => ({ id, ranges: queryObjectMatrix(additionMatrix, (value) => value === id) }));
ruleMatrix.addRangeRules(additions);
const { redoMutations, undoMutations } = getDataValidationDiffMutations(
@@ -149,17 +144,10 @@ export class DataValidationCopyPasteController extends Disposable {
if (copyType === COPY_TYPE.CUT) {
// Delete rules in copy range
const copySheetRuleMatrix = this._sheetDataValidationModel.getRuleObjectMatrix(copyUnitId, copySubUnitId).clone();
const deleteRangeStartCell = mapFunc(vCopyRange.startRow, vCopyRange.startColumn);
const deleteRangeEndCell = mapFunc(vCopyRange.endRow, vCopyRange.endColumn);
copySheetRuleMatrix.addRangeRules([
{
id: '',
ranges: [{
startRow: deleteRangeStartCell.row,
endRow: deleteRangeEndCell.row,
startColumn: deleteRangeStartCell.col,
endColumn: deleteRangeEndCell.col,
}],
ranges: virtualizeDiscreteRanges([copyRange]).mapRange(sourceVirtualRange),
},
]);
const { redoMutations: cutRedos, undoMutations: cutUndos } = getDataValidationDiffMutations(
@@ -180,31 +168,10 @@ export class DataValidationCopyPasteController extends Disposable {
};
} else {
const ruleMatrix = this._sheetDataValidationModel.getRuleObjectMatrix(copyUnitId, copySubUnitId).clone();
const additionMatrix = new ObjectMatrix();
const additionRules = new Set<string>();
const { ranges: [vCopyRange, vPastedRange], mapFunc } = virtualizeDiscreteRanges([copyRange, pastedRange]);
const repeatRange = getRepeatRange(vCopyRange, vPastedRange, true);
repeatRange.forEach(({ startRange }) => {
this._copyInfo?.matrix.forValue((row, col, ruleId) => {
const range = Rectangle.getPositionRange(
{
startRow: row,
endRow: row,
startColumn: col,
endColumn: col,
},
startRange
);
const { row: startRow, col: startColumn } = mapFunc(range.startRow, range.startColumn);
additionMatrix.setValue(startRow, startColumn, ruleId);
additionRules.add(ruleId);
});
});
const additions = Array.from(additionRules).map((id) => ({ id, ranges: queryObjectMatrix(additionMatrix, (value) => value === id) }));
const additions = [
{ id: '', ranges: clearTargetRanges },
...Array.from(copyInfo.rules, ([id, ranges]) => ({ id, ranges: getTargetRanges(ranges) })),
];
ruleMatrix.addRangeRules(additions);
const { redoMutations, undoMutations } = getDataValidationDiffMutations(
pastedUnitId,
@@ -38,4 +38,25 @@ describe('range-tools', () => {
expect(mapFunc(0, 0)).toEqual({ row: 5, col: 10 });
expect(mapFunc(2, 2)).toEqual({ row: 9, col: 30 });
});
it('maps rectangles between virtual and discrete coordinates', () => {
const result = virtualizeDiscreteRanges([{
rows: [5, 6, 9],
cols: [10, 11, 20],
}]);
expect(result).toHaveProperty('mapRange');
expect(result.mapRange({ startRow: 0, endRow: 2, startColumn: 0, endColumn: 2 })).toEqual([
{ startRow: 5, endRow: 6, startColumn: 10, endColumn: 11 },
{ startRow: 5, endRow: 6, startColumn: 20, endColumn: 20 },
{ startRow: 9, endRow: 9, startColumn: 10, endColumn: 11 },
{ startRow: 9, endRow: 9, startColumn: 20, endColumn: 20 },
]);
expect(result.projectRange({ startRow: 6, endRow: 9, startColumn: 11, endColumn: 20 })).toEqual({
startRow: 1,
endRow: 2,
startColumn: 1,
endColumn: 2,
});
});
});
@@ -17,9 +17,58 @@
import type { IRange } from '@univerjs/core';
import type { IDiscreteRange } from '@univerjs/sheets';
interface ILine {
start: number;
end: number;
}
function groupConsecutive(values: number[], start: number, end: number): ILine[] {
const groups: ILine[] = [];
for (let index = start; index <= end; index++) {
const value = values[index];
const previous = groups[groups.length - 1];
if (previous && value === previous.end + 1) {
previous.end = value;
} else {
groups.push({ start: value, end: value });
}
}
return groups;
}
function projectLine(values: number[], start: number, end: number): ILine | null {
let low = 0;
let high = values.length;
while (low < high) {
const middle = Math.floor((low + high) / 2);
if (values[middle] < start) {
low = middle + 1;
} else {
high = middle;
}
}
const first = low;
if (first === values.length || values[first] > end) {
return null;
}
high = values.length;
while (low < high) {
const middle = Math.floor((low + high) / 2);
if (values[middle] <= end) {
low = middle + 1;
} else {
high = middle;
}
}
return { start: first, end: low - 1 };
}
export function virtualizeDiscreteRanges(ranges: IDiscreteRange[]): {
ranges: IRange[];
mapFunc: (row: number, col: number) => { row: number; col: number };
mapRange: (range: IRange) => IRange[];
projectRange: (range: IRange) => IRange | null;
} {
let totalRows: number[] = [];
let totalCols: number[] = [];
@@ -51,5 +100,22 @@ export function virtualizeDiscreteRanges(ranges: IDiscreteRange[]): {
col: totalCols[col],
}
),
mapRange: (range) => {
const rowGroups = groupConsecutive(totalRows, range.startRow, range.endRow);
const columnGroups = groupConsecutive(totalCols, range.startColumn, range.endColumn);
return rowGroups.flatMap((row) => columnGroups.map((column) => ({
startRow: row.start,
endRow: row.end,
startColumn: column.start,
endColumn: column.end,
})));
},
projectRange: (range) => {
const row = projectLine(totalRows, range.startRow, range.endRow);
const column = projectLine(totalCols, range.startColumn, range.endColumn);
return row && column
? { startRow: row.start, endRow: row.end, startColumn: column.start, endColumn: column.end }
: null;
},
};
}
@@ -23,8 +23,8 @@ import type {
} from '../../../basics';
import type { IMoveRangeMutationParams } from '../../../commands/mutations/move-range.mutation';
import type { IMoveColumnsMutationParams } from '../../../commands/mutations/move-rows-cols.mutation';
import { Direction, IUniverInstanceService, MAX_COLUMN_COUNT, MAX_ROW_COUNT, RANGE_TYPE } from '@univerjs/core';
import { describe, expect, it } from 'vitest';
import { Direction, IUniverInstanceService, MAX_COLUMN_COUNT, MAX_ROW_COUNT, Range, RANGE_TYPE } from '@univerjs/core';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { InsertColMutation, InsertRowMutation } from '../../../commands/mutations/insert-row-col.mutation';
import { MoveRangeMutation } from '../../../commands/mutations/move-range.mutation';
import { MoveColsMutation } from '../../../commands/mutations/move-rows-cols.mutation';
@@ -71,6 +71,18 @@ const formatRanges = (ranges: IRange[]) =>
.map((range) => [range.startRow, range.endRow, range.startColumn, range.endColumn] as const)
.sort((prev, aft) => prev[0] - aft[0] || prev[2] - aft[2] || prev[1] - aft[1] || prev[3] - aft[3]);
const formatCoveredCells = (ranges: IRange[]) => {
const cells = new Set<string>();
ranges.forEach((range) => {
for (let row = range.startRow; row <= range.endRow; row++) {
for (let column = range.startColumn; column <= range.endColumn; column++) {
cells.add(`${row}:${column}`);
}
}
});
return Array.from(cells).sort();
};
const selectionDeps = (range?: IRange) => ({
selectionManagerService: {
getCurrentSelections: () => range ? [{ range }] : [],
@@ -78,6 +90,50 @@ const selectionDeps = (range?: IRange) => ({
} as any);
describe('ref range util behavior coverage', () => {
afterEach(() => vi.restoreAllMocks());
it('transforms large ranges without enumerating individual cells', () => {
const foreach = vi.spyOn(Range, 'foreach').mockImplementation(() => {
throw new Error('must not enumerate cells');
});
const targetRange = r(0, MAX_ROW_COUNT - 1, 0, MAX_COLUMN_COUNT - 1);
expect(() => handleMoveRowsCommon({
id: EffectRefRangId.MoveRowsCommandId,
params: { fromRange: r(2, 3, 0, MAX_COLUMN_COUNT - 1), toRange: r(8, 9, 0, MAX_COLUMN_COUNT - 1) },
}, targetRange)).not.toThrow();
expect(() => handleMoveColsCommon({
id: EffectRefRangId.MoveColsCommandId,
params: { fromRange: r(0, MAX_ROW_COUNT - 1, 2, 3), toRange: r(0, MAX_ROW_COUNT - 1, 8, 9) },
}, targetRange)).not.toThrow();
expect(() => handleReorderRangeCommon({
id: EffectRefRangId.ReorderRangeCommandId,
params: { unitId: 'unit', subUnitId: 'sheet', range: r(2, 3, 0, MAX_COLUMN_COUNT - 1), order: { 2: 3, 3: 2 } },
}, targetRange)).not.toThrow();
expect(() => handleMoveRangeCommon({
id: EffectRefRangId.MoveRangeCommandId,
params: { fromRange: r(2, 3, 2, 3), toRange: r(8, 9, 8, 9) },
}, targetRange)).not.toThrow();
expect(() => handleInsertRangeMoveDownCommon({
id: EffectRefRangId.InsertRangeMoveDownCommandId,
params: { range: r(2, 3, 2, 3) },
}, targetRange)).not.toThrow();
expect(() => handleInsertRangeMoveRightCommon({
id: EffectRefRangId.InsertRangeMoveRightCommandId,
params: { range: r(2, 3, 2, 3) },
}, targetRange)).not.toThrow();
expect(() => handleDeleteRangeMoveLeftCommon({
id: EffectRefRangId.DeleteRangeMoveLeftCommandId,
params: { range: r(2, 3, 2, 3) },
}, targetRange)).not.toThrow();
expect(() => handleDeleteRangeMoveUpCommon({
id: EffectRefRangId.DeleteRangeMoveUpCommandId,
params: { range: r(2, 3, 2, 3) },
}, targetRange)).not.toThrow();
foreach.mockRestore();
});
describe('range type normalization', () => {
it('expands typed and inferred column/row/all ranges for internal calculation', () => {
expect(handleRangeTypeInput(r(Number.NaN, Number.NaN, 2, 4))).toEqual(r(0, MAX_ROW_COUNT - 1, 2, 4));
@@ -182,19 +238,19 @@ describe('ref range util behavior coverage', () => {
});
it('removes deleted cells and moves the trailing cells into the gap', () => {
expect(formatRanges(handleDeleteRangeMoveLeftCommon({
expect(formatCoveredCells(handleDeleteRangeMoveLeftCommon({
id: EffectRefRangId.DeleteRangeMoveLeftCommandId,
params: { range: r(1, 2, 2, 3) },
}, r(0, 3, 1, 5)))).toEqual(formatRanges([
}, r(0, 3, 1, 5)))).toEqual(formatCoveredCells([
r(0, 3, 1, 3),
r(0, 0, 4, 5),
r(3, 3, 4, 5),
]));
expect(formatRanges(handleDeleteRangeMoveUpCommon({
expect(formatCoveredCells(handleDeleteRangeMoveUpCommon({
id: EffectRefRangId.DeleteRangeMoveUpCommandId,
params: { range: r(2, 3, 1, 2) },
}, r(1, 5, 0, 3)))).toEqual(formatRanges([
}, r(1, 5, 0, 3)))).toEqual(formatCoveredCells([
r(1, 3, 0, 3),
r(4, 5, 0, 0),
r(4, 5, 3, 3),
@@ -293,10 +349,10 @@ describe('ref range util behavior coverage', () => {
});
it('uses common range transformations for split range operations', () => {
expect(formatRanges(handleCommonDefaultRangeChangeWithEffectRefCommands(r(0, 3, 1, 5), {
expect(formatCoveredCells(handleCommonDefaultRangeChangeWithEffectRefCommands(r(0, 3, 1, 5), {
id: EffectRefRangId.DeleteRangeMoveLeftCommandId,
params: { range: r(1, 2, 2, 3) },
}))).toEqual(formatRanges([
}))).toEqual(formatCoveredCells([
r(0, 3, 1, 3),
r(0, 0, 4, 5),
r(3, 3, 4, 5),
@@ -66,6 +66,18 @@ const countRange = ([a, b, c, d]: readonly [number, number, number, number]) =>
const formatRanges = (ranges: IRange[]) => ranges.map((range) => [range.startRow, range.endRow, range.startColumn, range.endColumn] as const).sort((prev, aft) => countRange(prev) - countRange(aft));
const formatCoveredCells = (ranges: IRange[]) => {
const cells = new Set<string>();
ranges.forEach((range) => {
for (let row = range.startRow; row <= range.endRow; row++) {
for (let column = range.startColumn; column <= range.endColumn; column++) {
cells.add(`${row}:${column}`);
}
}
});
return Array.from(cells).sort();
};
describe('test ref-range move', () => {
describe('range type and effect-range helpers', () => {
it('normalizes row, column, all, and NaN range coordinates', () => {
@@ -535,7 +547,7 @@ describe('test ref-range move', () => {
targetRange1_2
);
expect(resRange1_1).toEqual(
expect(formatRanges(resRange1_1)).toEqual(formatRanges(
[
{
endColumn: 10,
@@ -550,8 +562,8 @@ describe('test ref-range move', () => {
startRow: 2,
},
]
);
expect(resRange1_2).toEqual([
));
expect(formatRanges(resRange1_2)).toEqual(formatRanges([
{
endColumn: 10,
endRow: 22,
@@ -564,7 +576,7 @@ describe('test ref-range move', () => {
startColumn: 0,
startRow: 6,
},
]);
]));
});
});
});
@@ -1621,8 +1633,8 @@ describe('test ref-range move', () => {
}]
)
);
expect(formatRanges(res2)).toEqual(
formatRanges(
expect(formatCoveredCells(res2)).toEqual(
formatCoveredCells(
[{
endColumn: 10,
endRow: 10,
+95 -137
View File
@@ -44,7 +44,7 @@ import type {
IRemoveRowColCommand,
IReorderRangeCommand,
} from './type';
import { Direction, getIntersectRange, IUniverInstanceService, MAX_COLUMN_COUNT, MAX_ROW_COUNT, mergeIntervals, ObjectMatrix, queryObjectMatrix, Range, RANGE_TYPE, Rectangle } from '@univerjs/core';
import { Direction, getIntersectRange, IUniverInstanceService, MAX_COLUMN_COUNT, MAX_ROW_COUNT, mergeIntervals, ObjectMatrix, RANGE_TYPE, Rectangle } from '@univerjs/core';
import { DeleteRangeMoveLeftCommand } from '../../commands/commands/delete-range-move-left.command';
import { DeleteRangeMoveUpCommand } from '../../commands/commands/delete-range-move-up.command';
import { InsertRangeMoveDownCommand } from '../../commands/commands/insert-range-move-down.command';
@@ -120,6 +120,67 @@ interface ILine {
start: number;
end: number;
}
interface ILineTransform extends ILine {
offset: number;
}
function mergeAndSortRanges(ranges: IRange[]): IRange[] {
const merged = ranges.length > 1 ? Rectangle.mergeRanges(ranges) : ranges;
return sortRanges(merged);
}
function sortRanges(ranges: IRange[]): IRange[] {
return ranges.sort((a, b) => a.startRow - b.startRow || a.startColumn - b.startColumn || a.endRow - b.endRow || a.endColumn - b.endColumn);
}
function translateRange(range: IRange, rowOffset: number, columnOffset: number): IRange {
return {
startRow: range.startRow + rowOffset,
endRow: range.endRow + rowOffset,
startColumn: range.startColumn + columnOffset,
endColumn: range.endColumn + columnOffset,
};
}
function moveRangeAlongAxis(targetRange: IRange, from: number, count: number, to: number, isRow: boolean): IRange[] {
let transforms: ILineTransform[];
if (from > to) {
transforms = [
{ start: Number.NEGATIVE_INFINITY, end: to - 1, offset: 0 },
{ start: to, end: from - 1, offset: count },
{ start: from, end: from + count - 1, offset: to - from },
{ start: from + count, end: Number.POSITIVE_INFINITY, offset: 0 },
];
} else {
if (from + count > to) {
throw new Error('Invalid move operation');
}
transforms = [
{ start: Number.NEGATIVE_INFINITY, end: from - 1, offset: 0 },
{ start: from, end: from + count - 1, offset: to - from - count },
{ start: from + count, end: to - 1, offset: -count },
{ start: to, end: Number.POSITIVE_INFINITY, offset: 0 },
];
}
const rangeStart = isRow ? targetRange.startRow : targetRange.startColumn;
const rangeEnd = isRow ? targetRange.endRow : targetRange.endColumn;
const ranges = transforms.flatMap(({ start, end, offset }) => {
const intersectStart = Math.max(rangeStart, start);
const intersectEnd = Math.min(rangeEnd, end);
if (intersectStart > intersectEnd) {
return [];
}
const range = isRow
? { startRow: intersectStart, endRow: intersectEnd, startColumn: targetRange.startColumn, endColumn: targetRange.endColumn }
: { startRow: targetRange.startRow, endRow: targetRange.endRow, startColumn: intersectStart, endColumn: intersectEnd };
return [translateRange(range, isRow ? offset : 0, isRow ? 0 : offset)];
});
return mergeAndSortRanges(ranges);
}
/**
* see docs/tldr/ref-range/move-rows-cols.tldr
*/
@@ -276,17 +337,7 @@ export const handleMoveRowsCommon = (params: IMoveRowsCommand, targetRange: IRan
const count = fromRange.endRow - fromRange.startRow + 1;
const toRow = toRange.startRow;
const matrix = new ObjectMatrix();
Range.foreach(targetRange, (row, col) => {
matrix.setValue(row, col, 1);
});
matrix.moveRows(fromRow, count, toRow);
// TODO@zhangw try to remove queryObjectMatrix, this could case memory out of use in large range.
const res = queryObjectMatrix(matrix, (value) => value === 1);
return res;
return moveRangeAlongAxis(targetRange, fromRow, count, toRow, true);
};
export const handleReorderRangeCommon = (param: IReorderRangeCommand, targetRange: IRange) => {
@@ -294,26 +345,25 @@ export const handleReorderRangeCommon = (param: IReorderRangeCommand, targetRang
if (!range || !order) {
return [targetRange];
}
const matrix = new ObjectMatrix();
Range.foreach(targetRange, (row, col) => {
matrix.setValue(row, col, 1);
});
const overwrittenRows: IRange[] = [];
const additions: IRange[] = [];
const startColumn = Math.max(range.startColumn, targetRange.startColumn);
const endColumn = Math.min(range.endColumn, targetRange.endColumn);
const cacheMatrix = new ObjectMatrix();
Range.foreach(range, (row, col) => {
if (Object.prototype.hasOwnProperty.call(order, row)) {
const targetRow = order[row];
const cloneCell = matrix.getValue(targetRow, col) ?? 0;
cacheMatrix.setValue(row, col, cloneCell);
Object.keys(order).forEach((key) => {
const row = Number(key);
if (row < range.startRow || row > range.endRow) {
return;
}
overwrittenRows.push({ startRow: row, endRow: row, startColumn: range.startColumn, endColumn: range.endColumn });
const sourceRow = order[row];
if (sourceRow >= targetRange.startRow && sourceRow <= targetRange.endRow && startColumn <= endColumn) {
additions.push({ startRow: row, endRow: row, startColumn, endColumn });
}
});
cacheMatrix.forValue((row, col, cellData) => {
matrix.setValue(row, col, cellData);
});
// TODO@zhangw try to remove queryObjectMatrix, this could case memory out of use in large range.
const res = queryObjectMatrix(matrix, (value) => value === 1);
return res;
return mergeAndSortRanges([...Rectangle.subtractMulti([targetRange], overwrittenRows), ...additions]);
};
export const handleMoveCols = (params: IMoveColsCommand, targetRange: IRange): IOperator[] => {
@@ -355,15 +405,7 @@ export const handleMoveColsCommon = (params: IMoveColsCommand, targetRange: IRan
const count = fromRange.endColumn - fromRange.startColumn + 1;
const toCol = toRange.startColumn;
const matrix = new ObjectMatrix();
Range.foreach(targetRange, (row, col) => {
matrix.setValue(row, col, 1);
});
matrix.moveColumns(fromCol, count, toCol);
// TODO@zhangw try to remove queryObjectMatrix, this could case memory out of use in large range.
return queryObjectMatrix(matrix, (value) => value === 1);
return moveRangeAlongAxis(targetRange, fromCol, count, toCol, false);
};
export const handleMoveRange = (param: IMoveRangeCommand, targetRange: IRange) => {
@@ -414,41 +456,13 @@ export const handleMoveRangeCommon = (param: IMoveRangeCommand, targetRange: IRa
return [positionRange];
}
const matrix = new ObjectMatrix();
Range.foreach(targetRange, (row, col) => {
matrix.setValue(row, col, 1);
});
const fromMatrix = new ObjectMatrix();
const loopFromRange = getIntersectRange(fromRange, targetRange);
loopFromRange && Range.foreach(loopFromRange, (row, col) => {
if (matrix.getValue(row, col)) {
matrix.setValue(row, col, undefined);
fromMatrix.setValue(row, col, 1);
}
});
const columnOffset = toRange.startColumn - fromRange.startColumn;
const rowOffset = toRange.startRow - fromRange.startRow;
const movedRange = getIntersectRange(fromRange, targetRange);
const remainingRanges = Rectangle.subtractMulti([targetRange], [fromRange, toRange]);
const translatedRanges = movedRange ? [translateRange(movedRange, rowOffset, columnOffset)] : [];
const loopToRange = {
startColumn: toRange.startColumn - columnOffset,
endColumn: toRange.endColumn - columnOffset,
startRow: toRange.startRow - rowOffset,
endRow: toRange.endRow - rowOffset,
};
loopToRange && Range.foreach(loopToRange, (row, col) => {
const targetRow = row + rowOffset;
const targetCol = col + columnOffset;
matrix.setValue(targetRow, targetCol, fromMatrix.getValue(row, col) ?? 0);
});
// TODO@zhangw try to remove queryObjectMatrix, this could case memory out of use in large range.
const res = queryObjectMatrix(matrix, (value) => value === 1);
return res;
return mergeAndSortRanges([...remainingRanges, ...translatedRanges]);
};
// see docs/tldr/ref-range/remove-rows-cols.tldr
@@ -769,19 +783,7 @@ export const handleInsertRangeMoveDownCommon = (param: IInsertRangeMoveDownComma
return [targetRange];
}
const matrix = new ObjectMatrix<number>();
noMoveRanges.forEach((noMoveRange) => {
Range.foreach(noMoveRange, (row, col) => {
matrix.setValue(row, col, 1);
});
});
targetMoveRange && Range.foreach(targetMoveRange, (row, col) => {
matrix.setValue(row + moveCount, col, 1);
});
// TODO@zhangw try to remove queryObjectMatrix, this could case memory out of use in large range.
return queryObjectMatrix(matrix, (v) => v === 1);
return sortRanges([...noMoveRanges, translateRange(targetMoveRange, moveCount, 0)]);
};
export const handleInsertRangeMoveRight = (param: IInsertRangeMoveRightCommand, targetRange: IRange) => {
@@ -820,19 +822,7 @@ export const handleInsertRangeMoveRightCommon = (param: IInsertRangeMoveRightCom
return [targetRange];
}
const matrix = new ObjectMatrix<number>();
noMoveRanges.forEach((noMoveRange) => {
Range.foreach(noMoveRange, (row, col) => {
matrix.setValue(row, col, 1);
});
});
targetMoveRange && Range.foreach(targetMoveRange, (row, col) => {
matrix.setValue(row, col + moveCount, 1);
});
// TODO@zhangw try to remove queryObjectMatrix, this could case memory out of use in large range.
return queryObjectMatrix(matrix, (v) => v === 1);
return sortRanges([...noMoveRanges, translateRange(targetMoveRange, 0, moveCount)]);
};
export const handleDeleteRangeMoveLeft = (param: IDeleteRangeMoveLeftCommand, targetRange: IRange) => {
@@ -872,34 +862,18 @@ export const handleDeleteRangeMoveLeftCommon = (param: IDeleteRangeMoveLeftComma
};
const moveCount = range.endColumn - range.startColumn + 1;
// this range need delete
const targetDeleteRange = getIntersectRange(range, targetRange);
const noMoveRanges = Rectangle.subtract(targetRange, rightRange);
const targetMoveRange = getIntersectRange(rightRange, targetRange);
if (!targetDeleteRange && !targetMoveRange) {
if (!targetMoveRange) {
return [targetRange];
}
const matrix = new ObjectMatrix<number>();
const movedRanges = targetMoveRange
? Rectangle.subtract(targetMoveRange, range).map((target) => translateRange(target, 0, -moveCount))
: [];
targetMoveRange && Range.foreach(targetMoveRange, (row, col) => {
matrix.setValue(row, col - moveCount, 1);
});
targetDeleteRange && Range.foreach(targetDeleteRange, (row, col) => {
matrix.setValue(row, col - moveCount, 0);
});
noMoveRanges.forEach((noMoveRange) => {
Range.foreach(noMoveRange, (row, col) => {
matrix.setValue(row, col, 1);
});
});
// TODO@zhangw try to remove queryObjectMatrix, this could case memory out of use in large range.
return queryObjectMatrix(matrix, (v) => v === 1);
return mergeAndSortRanges([...noMoveRanges, ...movedRanges]);
};
export const handleDeleteRangeMoveUp = (param: IDeleteRangeMoveUpCommand, targetRange: IRange) => {
@@ -935,34 +909,18 @@ export const handleDeleteRangeMoveUpCommon = (param: IDeleteRangeMoveUpCommand,
};
const moveCount = range.endRow - range.startRow + 1;
// this range need delete
const targetDeleteRange = getIntersectRange(range, targetRange);
const noMoveRanges = Rectangle.subtract(targetRange, bottomRange);
const targetMoveRange = getIntersectRange(bottomRange, targetRange);
if (!targetDeleteRange && !targetMoveRange) {
if (!targetMoveRange) {
return [targetRange];
}
const matrix = new ObjectMatrix<number>();
const movedRanges = targetMoveRange
? Rectangle.subtract(targetMoveRange, range).map((target) => translateRange(target, -moveCount, 0))
: [];
targetMoveRange && Range.foreach(targetMoveRange, (row, col) => {
matrix.setValue(row - moveCount, col, 1);
});
targetDeleteRange && Range.foreach(targetDeleteRange, (row, col) => {
matrix.setValue(row - moveCount, col, 0);
});
noMoveRanges.forEach((noMoveRange) => {
Range.foreach(noMoveRange, (row, col) => {
matrix.setValue(row, col, 1);
});
});
// TODO@zhangw try to remove queryObjectMatrix, this could case memory out of use in large range.
return queryObjectMatrix(matrix, (v) => v === 1);
return mergeAndSortRanges([...noMoveRanges, ...movedRanges]);
};
export const handleRemoveRowCommon = (param: IRemoveRowColCommandInterceptParams, targetRange: IRange) => {