diff --git a/packages/engine-formula/src/engine/dependency/formula-dependency.ts b/packages/engine-formula/src/engine/dependency/formula-dependency.ts index 4dc1782de7db..fc8eb16d76c0 100644 --- a/packages/engine-formula/src/engine/dependency/formula-dependency.ts +++ b/packages/engine-formula/src/engine/dependency/formula-dependency.ts @@ -58,6 +58,8 @@ export interface IFormulaDependencyGenerator { generate(): Promise; } +export type { IFormulaDependencyTree }; + export const IFormulaDependencyGenerator = createIdentifier('engine-formula.dependency-generator'); export class FormulaDependencyGenerator extends Disposable { diff --git a/packages/engine-formula/src/services/calculate-formula.service.ts b/packages/engine-formula/src/services/calculate-formula.service.ts index 3588033e364c..dbe0e8f4c1af 100644 --- a/packages/engine-formula/src/services/calculate-formula.service.ts +++ b/packages/engine-formula/src/services/calculate-formula.service.ts @@ -23,9 +23,10 @@ import type { IRuntimeUnitDataType, IUnitExcludedCell, } from '../basics/common'; - import type { IUniverEngineFormulaConfig } from '../controller/config.schema'; + import type { LexerNode } from '../engine/analysis/lexer-node'; +import type { IFormulaDependencyTree } from '../engine/dependency/formula-dependency'; import type { FunctionVariantType } from '../engine/reference-object/base-reference-object'; import type { IAllRuntimeData, IExecutionInProgressParams } from './runtime.service'; import { @@ -64,6 +65,7 @@ export interface ICalculateFormulaService { setRuntimeFeatureCellData(featureId: string, featureData: IRuntimeUnitDataType): void; setRuntimeFeatureRange(featureId: string, featureRange: IFeatureDirtyRangeType): void; execute(formulaDatasetConfig: IFormulaDatasetConfig): Promise; + generateDependencyTrees(formulaDatasetConfig: IFormulaDatasetConfig): Promise; stopFormulaExecution(): void; calculate(formulaString: string, transformSuffix?: boolean): void; } @@ -363,6 +365,37 @@ export class CalculateFormulaService extends Disposable implements ICalculateFor return this._runtimeService.getAllRuntimeData(); } + async generateDependencyTrees(formulaDatasetConfig: IFormulaDatasetConfig): Promise { + this._currentConfigService.load(formulaDatasetConfig); + + this._runtimeService.reset(); + + const normalTrees = await this._generateTrees(false); + + const executeState = this._runtimeService.getAllRuntimeData(); + + const { dirtyRanges, excludedCell } = this._getArrayFormulaDirtyRangeAndExcludedRange( + executeState.arrayFormulaRange, + executeState.runtimeFeatureRange + ); + + let arrayTrees: IFormulaDependencyTree[] = []; + + if (dirtyRanges && dirtyRanges.length > 0) { + this._currentConfigService.loadDirtyRangesAndExcludedCell(dirtyRanges, excludedCell); + + arrayTrees = await this._generateTrees(true); + } + + return [...normalTrees, ...arrayTrees]; + } + + private async _generateTrees(isArrayFormulaState: boolean): Promise { + const treeList = (await this._formulaDependencyGenerator.generate()).reverse(); + + return treeList; + } + calculate(formulaString: string, transformSuffix: boolean = true) { // TODO how to observe @alex // this.getObserver('onBeforeFormulaCalculateObservable')?.notifyObservers(formulaString); diff --git a/packages/sheets-formula/src/facade/__tests__/create-test-bed.ts b/packages/sheets-formula/src/facade/__tests__/create-test-bed.ts index 174f6459a083..d1d068cb264b 100644 --- a/packages/sheets-formula/src/facade/__tests__/create-test-bed.ts +++ b/packages/sheets-formula/src/facade/__tests__/create-test-bed.ts @@ -48,8 +48,10 @@ import enUS from '@univerjs/sheets/locale/en-US'; import zhCN from '@univerjs/sheets/locale/zh-CN'; import '@univerjs/sheets/facade'; +import '@univerjs/engine-formula/facade'; import '../f-workbook'; import '../f-range'; +import '../f-formula'; function getTestWorkbookDataDemo(): IWorkbookData { return { diff --git a/packages/sheets-formula/src/facade/__tests__/f-formula.test.ts b/packages/sheets-formula/src/facade/__tests__/f-formula.test.ts new file mode 100644 index 000000000000..3919d2958b89 --- /dev/null +++ b/packages/sheets-formula/src/facade/__tests__/f-formula.test.ts @@ -0,0 +1,49 @@ +/** + * 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 { Mocked } from 'vitest'; + +import type { IFormulaCellAndFeatureItem } from '../../services/remote/remote-formula-dependency-generator.service'; +import { describe, expect, it, vi } from 'vitest'; +import { IRemoteFormulaDependencyGenerator } from '../../services/remote/remote-formula-dependency-generator.service'; +import { createFacadeTestBed } from './create-test-bed'; + +describe('FFormula', () => { + describe('getFormulaCellsAndFeatures', () => { + it('should return formula cells and features from dependency tree', async () => { + const testBed = createFacadeTestBed(); + const mockDependencyGenerator: Mocked = { + generate: vi.fn(), + }; + + testBed.injector.add([IRemoteFormulaDependencyGenerator, { useValue: mockDependencyGenerator }]); + + const mockTrees: Array = [ + { unitId: 'unit1', subUnitId: 'sheet1', row: 0, column: 0 }, + { unitId: 'unit1', subUnitId: 'sheet1', featureId: 'feature1' }, + ]; + mockDependencyGenerator.generate.mockResolvedValue(mockTrees); + + const fFormula = testBed.univerAPI.getFormula(); + const result = await fFormula.getFormulaCellsAndFeatures(); + + expect(result).toEqual([ + { unitId: 'unit1', subUnitId: 'sheet1', row: 0, column: 0 }, + { unitId: 'unit1', subUnitId: 'sheet1', featureId: 'feature1' }, + ]); + }); + }); +}); diff --git a/packages/sheets-formula/src/facade/f-formula.ts b/packages/sheets-formula/src/facade/f-formula.ts index 1aba2e27edea..8f65f8395982 100644 --- a/packages/sheets-formula/src/facade/f-formula.ts +++ b/packages/sheets-formula/src/facade/f-formula.ts @@ -14,14 +14,14 @@ * limitations under the License. */ -import type { IDisposable, ILocales } from '@univerjs/core'; +import type { IDisposable, ILocales, IUnitRange } from '@univerjs/core'; import type { IFunctionInfo } from '@univerjs/engine-formula'; -import type { CalculationMode, IRegisterAsyncFunction, IRegisterFunction, ISingleFunctionRegisterParams, IUniverSheetsFormulaBaseConfig } from '@univerjs/sheets-formula'; +import type { CalculationMode, IFormulaCellAndFeatureItem, IRegisterAsyncFunction, IRegisterFunction, ISingleFunctionRegisterParams, IUniverSheetsFormulaBaseConfig } from '@univerjs/sheets-formula'; import { debounce, IConfigService, ILogService, LifecycleService, LifecycleStages } from '@univerjs/core'; import { SetFormulaCalculationStartMutation } from '@univerjs/engine-formula'; import { FFormula } from '@univerjs/engine-formula/facade'; -import { IRegisterFunctionService, PLUGIN_CONFIG_KEY_BASE, RegisterFunctionService } from '@univerjs/sheets-formula'; +import { IRegisterFunctionService, IRemoteFormulaDependencyGenerator, PLUGIN_CONFIG_KEY_BASE, RegisterFunctionService } from '@univerjs/sheets-formula'; /** * @ignore @@ -39,6 +39,20 @@ export interface IFFormulaSheetsMixin { */ setInitialFormulaComputing(calculationMode: CalculationMode): void; + /** + * Get the list of formula cells and feature IDs from the dependency tree. + * @param {IUnitRange} [range] - Optional range to filter the results. + * @returns {Promise>} An array of objects containing unitId, subUnitId, and either cell coordinates or featureId. + * + * @example + * ```ts + * const formulaEngine = univerAPI.getFormula(); + * const cellsAndFeatures = await formulaEngine.getFormulaCellsAndFeatures(); + * console.log(cellsAndFeatures); + * ``` + */ + getFormulaCellsAndFeatures(range?: IUnitRange): Promise>; + /** * Register a custom synchronous formula function. * @param {string} name - The name of the function to register. This will be used in formulas (e.g., =MYFUNC()). @@ -319,6 +333,11 @@ export class FFormulaSheetsMixin extends FFormula implements IFFormulaSheetsMixi config.initialFormulaComputing = calculationMode; } + override async getFormulaCellsAndFeatures(range?: IUnitRange): Promise> { + const dependencyGenerator = this._injector.get(IRemoteFormulaDependencyGenerator); + return dependencyGenerator.generate(range); + } + override registerFunction(name: string, func: IRegisterFunction): IDisposable; override registerFunction(name: string, func: IRegisterFunction, description: string): IDisposable; override registerFunction( diff --git a/packages/sheets-formula/src/index.ts b/packages/sheets-formula/src/index.ts index e3fabcceee23..2e4b06946d07 100644 --- a/packages/sheets-formula/src/index.ts +++ b/packages/sheets-formula/src/index.ts @@ -32,5 +32,6 @@ export type { IRegisterFunctionParams, IUnregisterFunctionParams } from './servi export { RegisterFunctionService } from './services/register-function.service'; export { IRegisterFunctionService } from './services/register-function.service'; export { RegisterOtherFormulaService } from './services/register-other-formula.service'; +export { type IFormulaCellAndFeatureItem, IRemoteFormulaDependencyGenerator, RemoteFormulaDependencyGeneratorService } from './services/remote/remote-formula-dependency-generator.service'; export { IRemoteRegisterFunctionService, RemoteRegisterFunctionService } from './services/remote/remote-register-function.service'; export { calculateFormula } from './util/calculate'; diff --git a/packages/sheets-formula/src/plugin.ts b/packages/sheets-formula/src/plugin.ts index c979ebffc51f..6d5fd303f702 100644 --- a/packages/sheets-formula/src/plugin.ts +++ b/packages/sheets-formula/src/plugin.ts @@ -38,6 +38,7 @@ import { DescriptionService, IDescriptionService } from './services/description. import { FormulaRefRangeService } from './services/formula-ref-range.service'; import { IRegisterFunctionService, RegisterFunctionService } from './services/register-function.service'; import { RegisterOtherFormulaService } from './services/register-other-formula.service'; +import { IRemoteFormulaDependencyGenerator, RemoteFormulaDependencyGeneratorService, RemoteFormulaDependencyGeneratorServiceName } from './services/remote/remote-formula-dependency-generator.service'; import { IRemoteRegisterFunctionService, RemoteRegisterFunctionService, RemoteRegisterFunctionServiceName } from './services/remote/remote-register-function.service'; @DependentOn(UniverFormulaEnginePlugin) @@ -67,6 +68,12 @@ export class UniverRemoteSheetsFormulaPlugin extends Plugin { RemoteRegisterFunctionServiceName, fromModule(this._injector.get(RemoteRegisterFunctionService)) ); + + this._injector.add([RemoteFormulaDependencyGeneratorService]); + this._injector.get(IRPCChannelService).registerChannel( + RemoteFormulaDependencyGeneratorServiceName, + fromModule(this._injector.get(RemoteFormulaDependencyGeneratorService)) + ); } } @@ -114,6 +121,10 @@ export class UniverSheetsFormulaPlugin extends Plugin { dependencies.push([IRemoteRegisterFunctionService, { useFactory: () => toModule(rpcChannelService.requestChannel(RemoteRegisterFunctionServiceName)), }]); + + dependencies.push([IRemoteFormulaDependencyGenerator, { + useFactory: () => toModule(rpcChannelService.requestChannel(RemoteFormulaDependencyGeneratorServiceName)), + }]); } dependencies.forEach((dependency) => j.add(dependency)); diff --git a/packages/sheets-formula/src/services/remote/remote-formula-dependency-generator.service.ts b/packages/sheets-formula/src/services/remote/remote-formula-dependency-generator.service.ts new file mode 100644 index 000000000000..5ab28ef729f7 --- /dev/null +++ b/packages/sheets-formula/src/services/remote/remote-formula-dependency-generator.service.ts @@ -0,0 +1,87 @@ +/** + * 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 { IUnitRange } from '@univerjs/core'; +import { createIdentifier, Inject, isFormulaId, isFormulaString } from '@univerjs/core'; +import { FormulaDataModel, ICalculateFormulaService, IFormulaCurrentConfigService } from '@univerjs/engine-formula'; + +export interface IFormulaCellAndFeatureItem { + unitId: string; + subUnitId: string; + row?: number; + column?: number; + featureId?: string; + formula?: string; + formulaId?: string; +} + +export interface IRemoteFormulaDependencyGenerator { + generate(range?: IUnitRange): Promise>; +} + +export const RemoteFormulaDependencyGeneratorServiceName = 'sheets-formula.remote-formula-dependency-generator.service'; +export const IRemoteFormulaDependencyGenerator = createIdentifier(RemoteFormulaDependencyGeneratorServiceName); + +/** + * This class should resident in the main process. + */ +export class RemoteFormulaDependencyGeneratorService implements IRemoteFormulaDependencyGenerator { + constructor( + @ICalculateFormulaService private readonly _calculateFormulaService: ICalculateFormulaService, + @Inject(FormulaDataModel) private readonly _formulaDataModel: FormulaDataModel, + @IFormulaCurrentConfigService private readonly _currentConfigService: IFormulaCurrentConfigService + ) {} + + async generate(range?: IUnitRange): Promise> { + const formulaData = this._formulaDataModel.getFormulaData(); + const arrayFormulaCellData = this._formulaDataModel.getArrayFormulaCellData(); + const arrayFormulaRange = this._formulaDataModel.getArrayFormulaRange(); + + const formulaDatasetConfig = { + formulaData, + arrayFormulaCellData, + arrayFormulaRange, + forceCalculate: false, + dirtyRanges: range ? [range] : [], + dirtyNameMap: {}, + dirtyDefinedNameMap: {}, + dirtyUnitFeatureMap: {}, + dirtyUnitOtherFormulaMap: {}, + clearDependencyTreeCache: {}, + maxIteration: 1, + rowData: undefined, + }; + + const trees = await this._calculateFormulaService.generateDependencyTrees(formulaDatasetConfig); + + const result: Array = []; + for (let i = 0; i < trees.length; i++) { + const tree = trees[i]; + if ((isFormulaString(tree.formula) || isFormulaId(tree.formulaId)) || tree.featureId != null) { + result.push({ + unitId: tree.unitId, + subUnitId: tree.subUnitId, + row: tree.row, + column: tree.column, + featureId: tree.featureId || undefined, + formula: tree.formula || undefined, + formulaId: tree.formulaId || undefined, + }); + } + } + return result; + } +}