diff --git a/examples/src/sheets/main.ts b/examples/src/sheets/main.ts index f866fd8613..736a6659a2 100644 --- a/examples/src/sheets/main.ts +++ b/examples/src/sheets/main.ts @@ -56,6 +56,7 @@ import '@univerjs/sheets-hyper-link-ui/facade'; import '@univerjs/sheets-thread-comment/facade'; import '@univerjs/sheets-conditional-formatting/facade'; +import '@univerjs/sheets-find-replace/facade'; import '@univerjs/sheets-drawing-ui/facade'; import '../global.css'; diff --git a/packages/find-replace/src/index.ts b/packages/find-replace/src/index.ts index cca2896bff..ad2fa409bd 100644 --- a/packages/find-replace/src/index.ts +++ b/packages/find-replace/src/index.ts @@ -24,9 +24,11 @@ export type { IFindMoveParams, IFindQuery, IFindReplaceProvider, + IFindReplaceState, IReplaceAllResult, } from './services/find-replace.service'; -export { FindBy, FindDirection, FindModel, FindScope, IFindReplaceService } from './services/find-replace.service'; + +export { createInitFindReplaceState, FindBy, FindDirection, FindModel, FindReplaceModel, FindReplaceState, FindScope, IFindReplaceService } from './services/find-replace.service'; // #region - all commands diff --git a/packages/find-replace/src/services/find-replace.service.ts b/packages/find-replace/src/services/find-replace.service.ts index 5a94a52d71..458d608206 100644 --- a/packages/find-replace/src/services/find-replace.service.ts +++ b/packages/find-replace/src/services/find-replace.service.ts @@ -15,9 +15,9 @@ */ import type { IDisposable, Nullable } from '@univerjs/core'; +import type { Observable } from 'rxjs'; import { createIdentifier, Disposable, DisposableCollection, IContextService, Inject, Injector, IUniverInstanceService, toDisposable } from '@univerjs/core'; import { RENDER_RAW_FORMULA_KEY } from '@univerjs/engine-render'; -import type { Observable } from 'rxjs'; import { BehaviorSubject, combineLatest, debounceTime, Subject, throttleTime } from 'rxjs'; import { FIND_REPLACE_REPLACE_REVEALED } from './context-keys'; @@ -155,6 +155,8 @@ export interface IFindReplaceService { replace(): Promise; replaceAll(): Promise; + + getProviders(): Set; } export const IFindReplaceService = createIdentifier('find-replace.service'); @@ -363,7 +365,7 @@ export class FindReplaceModel extends Disposable { this._state.changeState({ matchesPosition: index + 1 }); } - moveToNextMatch(): void { + moveToNextMatch() { if (!this._matchingModel) { return; } @@ -372,13 +374,14 @@ export class FindReplaceModel extends Disposable { const nextMatch = this._matchingModel.moveToNextMatch({ loop: loopInCurrentUnit }); if (nextMatch) { this._markMatch(nextMatch); + return nextMatch; } else { const currentModelIndex = this._findModels.findIndex((m) => m === this._matchingModel); - this._moveToNextUnitMatch(currentModelIndex); + return this._moveToNextUnitMatch(currentModelIndex); } } - private _moveToNextUnitMatch(startingIndex: number): void { + private _moveToNextUnitMatch(startingIndex: number) { const l = this._findModels.length; for (let i = (startingIndex + 1) % l; i !== startingIndex;) { const nextPositionModel = this._findModels[i]; @@ -386,7 +389,7 @@ export class FindReplaceModel extends Disposable { if (nextMatch) { this._matchingModel = nextPositionModel; this._markMatch(nextMatch); - return; + return nextMatch; } i = (i + 1) % l; @@ -395,10 +398,11 @@ export class FindReplaceModel extends Disposable { if (this._matchingModel) { const nextMatch = this._matchingModel.moveToNextMatch({ ignoreSelection: true }); if (nextMatch) this._markMatch(nextMatch); + return nextMatch; } } - moveToPreviousMatch(): void { + moveToPreviousMatch() { if (!this._matchingModel) { return; } @@ -409,6 +413,7 @@ export class FindReplaceModel extends Disposable { const index = this._matches.findIndex((value) => value === nextMatch); this.currentMatch$.next(nextMatch); this._state.changeState({ matchesPosition: index + 1 }); + return nextMatch; } else { const l = this._findModels.length; const currentModelIndex = this._findModels.findIndex((m) => m === this._matchingModel); @@ -418,7 +423,7 @@ export class FindReplaceModel extends Disposable { if (nextMatch) { this._matchingModel = nextPositionModel; this._markMatch(nextMatch); - return; + return nextMatch; } i = (i - 1) % l; @@ -426,6 +431,7 @@ export class FindReplaceModel extends Disposable { const nextMatch = this._matchingModel.moveToPreviousMatch({ ignoreSelection: true }); if (nextMatch) this._markMatch(nextMatch); + return nextMatch; } } @@ -494,7 +500,7 @@ export interface IFindReplaceState { findBy: FindBy; } -function createInitFindReplaceState(): IFindReplaceState { +export function createInitFindReplaceState(): IFindReplaceState { return { caseSensitive: false, findBy: FindBy.VALUE, @@ -698,6 +704,10 @@ export class FindReplaceService extends Disposable implements IFindReplaceServic this._focusSignal$.complete(); } + getProviders(): Set { + return this._providers; + } + getCurrentMatch(): Nullable { return this._model?.getCurrentMatch(); } diff --git a/packages/sheets-find-replace/package.json b/packages/sheets-find-replace/package.json index 7a3a058690..f8d4e3ae66 100644 --- a/packages/sheets-find-replace/package.json +++ b/packages/sheets-find-replace/package.json @@ -23,7 +23,8 @@ "exports": { ".": "./src/index.ts", "./*": "./src/*", - "./locale/*": "./src/locale/*.ts" + "./locale/*": "./src/locale/*.ts", + "./facade": "./src/facade/index.ts" }, "main": "./src/index.ts", "types": "./lib/types/index.d.ts", diff --git a/packages/sheets-find-replace/src/facade/f-text-finder.ts b/packages/sheets-find-replace/src/facade/f-text-finder.ts new file mode 100644 index 0000000000..6a5f5a1e6c --- /dev/null +++ b/packages/sheets-find-replace/src/facade/f-text-finder.ts @@ -0,0 +1,292 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { IRange, Nullable, Workbook } from '@univerjs/core'; +import type { IFindComplete, IFindMatch, IFindReplaceState } from '@univerjs/find-replace'; +import { Disposable, Inject, Injector, IUniverInstanceService } from '@univerjs/core'; +import { createInitFindReplaceState, FindBy, FindReplaceModel, FindReplaceState, IFindReplaceService } from '@univerjs/find-replace'; +import { FRange } from '@univerjs/sheets/facade'; + +export interface IFTextFinder { + /** + * get all the matched range in the univer + * @returns all the matched range + * @throws if the find operation is not completed + * + * @example + * ```typescript + * const textFinder = await univerAPI.createTextFinderAsync('hello'); + * const ranges = textFinder.findAll(); + * ranges.forEach((range) => { + * console.log(range.getA1Notation()); + * }); + * ``` + */ + findAll(): FRange[]; + /** + * find the next matched range in the univer + * @returns the next matched range + * @throws if the find operation is not completed + * @returns null if no more match + * + * @example + * ```typescript + * const textFinder = await univerAPI.createTextFinderAsync('hello'); + * const range = textFinder.findNext(); + * if (range) { + * console.log(range.getA1Notation()); + * } + * ``` + */ + findNext(): Nullable; + /** + * find the previous matched range in the univer + * @returns the previous matched range + * @throws if the find operation is not completed + * @returns null if no more match + * + * @example + * ```typescript + * const textFinder = await univerAPI.createTextFinderAsync('hello'); + * const range = textFinder.findPrevious(); + * if (range) { + * console.log(range.getA1Notation()); + * } + * ``` + */ + findPrevious(): Nullable; + /** + * get the current matched range in the univer + * @returns the current matched range + * @throws if the find operation is not completed + * + * @example + * ```typescript + * const textFinder = await univerAPI.createTextFinderAsync('hello'); + * const range = textFinder.getCurrentMatch(); + * if (range) { + * console.log(range.getA1Notation()); + * } + * ``` + */ + getCurrentMatch(): Nullable; + + /** + * set the match case option + * @param {boolean} matchCase whether to match case + * @returns text-finder instance + * + * @example + * ```typescript + * const textFinder = await univerAPI.createTextFinderAsync('hello'); + * await textFinder.matchCaseAsync(true); + * ``` + */ + matchCaseAsync(matchCase: boolean): Promise; + /** + * set the match entire cell option + * @param {boolean} matchEntireCell whether to match entire cell + * @returns text-finder instance + * + * @example + * ```typescript + * const textFinder = await univerAPI.createTextFinderAsync('hello'); + * await textFinder.matchEntireCellAsync(true); + * ``` + */ + matchEntireCellAsync(matchEntireCell: boolean): Promise; + /** + * set the match formula text option + * @param {boolean} matchFormulaText whether to match formula text + * @returns text-finder instance + * + * @example + * ```typescript + * const textFinder = await univerAPI.createTextFinderAsync('hello'); + * await textFinder.matchFormulaTextAsync(true); + * ``` + */ + matchFormulaTextAsync(matchFormulaText: boolean): Promise; + /** + * replace all the matched text with the given text + * @param {string} replaceText the text to replace + * @returns the number of replaced text + * @throws if the find operation is not completed + * + * @example + * ```typescript + * const textFinder = await univerAPI.createTextFinderAsync('hello'); + * const replacedCount = await textFinder.replaceAllWithAsync('world'); + * console.log(replacedCount); + * ``` + */ + replaceAllWithAsync(replaceText: string): Promise; + /** + * replace the current matched text with the given text + * @param {string} replaceText the text to replace + * @returns whether the replace is successful + * @throws if the find operation is not completed + * + * @example + * ```typescript + * const textFinder = await univerAPI.createTextFinderAsync('hello'); + * const replaced = await textFinder.replaceWithAsync('world'); + * console.log(replaced); + * ``` + */ + replaceWithAsync(replaceText: string): Promise; + /** + * ensure the find operation is completed + * @returns the find complete result + * + * @example + * ```typescript + * const textFinder = await univerAPI.createTextFinderAsync('hello'); + * const complete = await textFinder.ensureCompleteAsync(); + * console.log(complete); + * ``` + */ + ensureCompleteAsync(): Promise>; +} + +/** + * This interface class provides methods to find and replace text in the univer. + */ +export class FTextFinder extends Disposable implements IFTextFinder { + private readonly _state = new FindReplaceState(); + private _model: Nullable; + private _complete: Nullable; + constructor( + _initialState: Partial, + @Inject(Injector) private readonly _injector: Injector, + @IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService, + @IFindReplaceService private readonly _findReplaceService: IFindReplaceService + ) { + super(); + const providers = this._findReplaceService.getProviders(); + this._model = this._injector.createInstance(FindReplaceModel, this._state, providers); + const newState = { + ...createInitFindReplaceState(), + ..._initialState, + }; + this._state.changeState(newState); + } + + findAll(): FRange[] { + if (!this._state.findCompleted || !this._complete) { + return []; + } + return this._complete.results.map((result) => { + return this._findMatchToFRange(result); + }); + } + + findNext(): Nullable { + if (!this._state.findCompleted || !this._complete) { + return null; + } + const match = this._model?.moveToNextMatch(); + if (!match) { + return null; + } + return this._findMatchToFRange(match); + } + + findPrevious(): Nullable { + const match = this._model?.moveToPreviousMatch(); + if (!match) { + return null; + } + return this._findMatchToFRange(match); + } + + getCurrentMatch(): Nullable { + if (!this._state.findCompleted || !this._complete) { + throw new Error('Find operation is not completed.'); + } + const match = this._model?.currentMatch$.value; + if (!match) { + return null; + } + return this._findMatchToFRange(match); + } + + async matchCaseAsync(matchCase: boolean): Promise { + this._state.changeState({ caseSensitive: matchCase }); + return new Promise((resolve) => { + const subscribe = this._state.stateUpdates$.subscribe((state) => { + if (state.findCompleted === true) { + subscribe.unsubscribe(); + resolve(this); + } + }); + }); + } + + async matchEntireCellAsync(matchEntireCell: boolean): Promise { + this._state.changeState({ matchesTheWholeCell: matchEntireCell }); + return new Promise((resolve) => { + const subscribe = this._state.stateUpdates$.subscribe((state) => { + if (state.findCompleted === true) { + subscribe.unsubscribe(); + resolve(this); + } + }); + }); + } + + async matchFormulaTextAsync(matchFormulaText: boolean): Promise { + this._state.changeState({ findBy: matchFormulaText ? FindBy.FORMULA : FindBy.VALUE }); + return new Promise((resolve) => { + const subscribe = this._state.stateUpdates$.subscribe((state) => { + if (state.findCompleted === true) { + subscribe.unsubscribe(); + resolve(this); + } + }); + }); + } + + async replaceAllWithAsync(replaceText: string): Promise { + await this._state.changeState({ replaceRevealed: true, replaceString: replaceText }); + const res = (await this._model?.replaceAll())?.success ?? 0; + this._state.changeState({ replaceRevealed: false }); + return res; + } + + async replaceWithAsync(replaceText: string): Promise { + await this._state.changeState({ replaceRevealed: true, replaceString: replaceText }); + await this._model?.replace(); + this._state.changeState({ replaceRevealed: false }); + return true; + } + + async ensureCompleteAsync(): Promise> { + if (!this._state.findCompleted || !this._complete) { + this._complete = await this._model?.start(); + } + return this._complete; + }; + + private _findMatchToFRange(match: IFindMatch): FRange { + const { unitId } = match; + const { subUnitId, range } = match.range as { subUnitId: string; range: IRange }; + const workbook = this._univerInstanceService.getUnit(unitId) as Workbook; + const worksheet = workbook.getSheetBySheetId(subUnitId)!; + return this._injector.createInstance(FRange, workbook, worksheet, range); + } +} + diff --git a/packages/sheets-find-replace/src/facade/f-univer.ts b/packages/sheets-find-replace/src/facade/f-univer.ts new file mode 100644 index 0000000000..99aa55569c --- /dev/null +++ b/packages/sheets-find-replace/src/facade/f-univer.ts @@ -0,0 +1,51 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { IFindReplaceState } from '@univerjs/find-replace'; +import { FUniver } from '@univerjs/core'; +import { FTextFinder } from './f-text-finder'; + +export interface IFUniverFindReplaceMixin { + /** + * Create a text-finder for the current univer. + * + * @param {string} text - The text to find. + * + * @returns {Promise} A promise that resolves to the text-finder instance. + * + * @example + * ```typescript + * const textFinder = await univerAPI.createTextFinderAsync('Hello'); + * ``` + */ + createTextFinderAsync(text: string): Promise; + +} + +export class FUniverFindReplaceMixin extends FUniver implements IFUniverFindReplaceMixin { + override async createTextFinderAsync(text: string): Promise { + const state: Partial = { findString: text }; + const textFinder = this._injector.createInstance(FTextFinder, state); + await textFinder.ensureCompleteAsync(); + return textFinder; + } +} + +FUniver.extend(FUniverFindReplaceMixin); +declare module '@univerjs/core' { + // eslint-disable-next-line ts/naming-convention + interface FUniver extends IFUniverFindReplaceMixin {} +} diff --git a/packages/sheets-find-replace/src/facade/index.ts b/packages/sheets-find-replace/src/facade/index.ts new file mode 100644 index 0000000000..419b917d82 --- /dev/null +++ b/packages/sheets-find-replace/src/facade/index.ts @@ -0,0 +1,21 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import './f-text-finder'; +import './f-univer'; + +export type * from './f-text-finder'; +export type * from './f-univer';