mirror of
https://github.com/dream-num/univer.git
synced 2026-09-24 15:32:18 +08:00
feat: add find replace facade api (#4430)
This commit is contained in:
@@ -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';
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
Reference in New Issue
Block a user