feat: add find replace facade api (#4430)

This commit is contained in:
YuHong
2025-01-06 14:48:37 +08:00
committed by GitHub
parent 59a1774a6f
commit e34c664213
7 changed files with 388 additions and 10 deletions
+1
View File
@@ -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';
+3 -1
View File
@@ -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
@@ -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<boolean>;
replaceAll(): Promise<IReplaceAllResult>;
getProviders(): Set<IFindReplaceProvider>;
}
export const IFindReplaceService = createIdentifier<IFindReplaceService>('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<IFindReplaceProvider> {
return this._providers;
}
getCurrentMatch(): Nullable<IFindMatch> {
return this._model?.getCurrentMatch();
}
+2 -1
View File
@@ -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",
@@ -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<FRange>;
/**
* 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<FRange>;
/**
* 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<FRange>;
/**
* 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<IFTextFinder>;
/**
* 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<IFTextFinder>;
/**
* 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<IFTextFinder>;
/**
* 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<number>;
/**
* 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<boolean>;
/**
* 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<Nullable<IFindComplete>>;
}
/**
* 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<FindReplaceModel>;
private _complete: Nullable<IFindComplete>;
constructor(
_initialState: Partial<IFindReplaceState>,
@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<FRange> {
if (!this._state.findCompleted || !this._complete) {
return null;
}
const match = this._model?.moveToNextMatch();
if (!match) {
return null;
}
return this._findMatchToFRange(match);
}
findPrevious(): Nullable<FRange> {
const match = this._model?.moveToPreviousMatch();
if (!match) {
return null;
}
return this._findMatchToFRange(match);
}
getCurrentMatch(): Nullable<FRange> {
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<IFTextFinder> {
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<IFTextFinder> {
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<IFTextFinder> {
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<number> {
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<boolean> {
await this._state.changeState({ replaceRevealed: true, replaceString: replaceText });
await this._model?.replace();
this._state.changeState({ replaceRevealed: false });
return true;
}
async ensureCompleteAsync(): Promise<Nullable<IFindComplete>> {
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);
}
}
@@ -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<FTextFinder | null>} A promise that resolves to the text-finder instance.
*
* @example
* ```typescript
* const textFinder = await univerAPI.createTextFinderAsync('Hello');
* ```
*/
createTextFinderAsync(text: string): Promise<FTextFinder | null>;
}
export class FUniverFindReplaceMixin extends FUniver implements IFUniverFindReplaceMixin {
override async createTextFinderAsync(text: string): Promise<FTextFinder | null> {
const state: Partial<IFindReplaceState> = { 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 {}
}
@@ -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';