@difizen/libro-search 0.0.2-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1 -0
  3. package/es/abstract-search-provider.d.ts +113 -0
  4. package/es/abstract-search-provider.d.ts.map +1 -0
  5. package/es/abstract-search-provider.js +126 -0
  6. package/es/index.d.ts +10 -0
  7. package/es/index.d.ts.map +1 -0
  8. package/es/index.js +9 -0
  9. package/es/index.less +107 -0
  10. package/es/libro-cell-search-provider.d.ts +10 -0
  11. package/es/libro-cell-search-provider.d.ts.map +1 -0
  12. package/es/libro-cell-search-provider.js +54 -0
  13. package/es/libro-search-engine-html.d.ts +3 -0
  14. package/es/libro-search-engine-html.d.ts.map +1 -0
  15. package/es/libro-search-engine-html.js +59 -0
  16. package/es/libro-search-engine-text.d.ts +10 -0
  17. package/es/libro-search-engine-text.d.ts.map +1 -0
  18. package/es/libro-search-engine-text.js +32 -0
  19. package/es/libro-search-generic-provider.d.ts +107 -0
  20. package/es/libro-search-generic-provider.d.ts.map +1 -0
  21. package/es/libro-search-generic-provider.js +467 -0
  22. package/es/libro-search-manager.d.ts +22 -0
  23. package/es/libro-search-manager.d.ts.map +1 -0
  24. package/es/libro-search-manager.js +102 -0
  25. package/es/libro-search-model.d.ts +111 -0
  26. package/es/libro-search-model.d.ts.map +1 -0
  27. package/es/libro-search-model.js +395 -0
  28. package/es/libro-search-protocol.d.ts +176 -0
  29. package/es/libro-search-protocol.d.ts.map +1 -0
  30. package/es/libro-search-protocol.js +36 -0
  31. package/es/libro-search-provider.d.ts +138 -0
  32. package/es/libro-search-provider.d.ts.map +1 -0
  33. package/es/libro-search-provider.js +759 -0
  34. package/es/libro-search-utils.d.ts +25 -0
  35. package/es/libro-search-utils.d.ts.map +1 -0
  36. package/es/libro-search-utils.js +85 -0
  37. package/es/libro-search-view.d.ts +59 -0
  38. package/es/libro-search-view.d.ts.map +1 -0
  39. package/es/libro-search-view.js +455 -0
  40. package/es/module.d.ts +4 -0
  41. package/es/module.d.ts.map +1 -0
  42. package/es/module.js +35 -0
  43. package/package.json +66 -0
  44. package/src/abstract-search-provider.ts +160 -0
  45. package/src/index.less +107 -0
  46. package/src/index.ts +9 -0
  47. package/src/libro-cell-search-provider.ts +39 -0
  48. package/src/libro-search-engine-html.ts +74 -0
  49. package/src/libro-search-engine-text.ts +34 -0
  50. package/src/libro-search-generic-provider.ts +303 -0
  51. package/src/libro-search-manager.ts +86 -0
  52. package/src/libro-search-model.ts +266 -0
  53. package/src/libro-search-protocol.ts +209 -0
  54. package/src/libro-search-provider.ts +507 -0
  55. package/src/libro-search-utils.spec.ts +37 -0
  56. package/src/libro-search-utils.ts +83 -0
  57. package/src/libro-search-view.tsx +404 -0
  58. package/src/module.ts +59 -0
@@ -0,0 +1,209 @@
1
+ import type { CellView } from '@difizen/libro-core';
2
+ import type { Disposable, Event } from '@difizen/mana-app';
3
+ import type { View } from '@difizen/mana-app';
4
+ import { Syringe } from '@difizen/mana-app';
5
+
6
+ /**
7
+ * Base search match interface
8
+ */
9
+ export interface SearchMatch {
10
+ /**
11
+ * Text of the exact match itself
12
+ */
13
+ readonly text: string;
14
+
15
+ /**
16
+ * Start location of the match (in a text, this is the column)
17
+ */
18
+ position: number;
19
+ }
20
+
21
+ /**
22
+ * HTML search match interface
23
+ */
24
+ export interface HTMLSearchMatch extends SearchMatch {
25
+ /**
26
+ * Node containing the match
27
+ */
28
+ readonly node: Text;
29
+ }
30
+
31
+ /**
32
+ * Filter interface
33
+ */
34
+ export interface SearchFilter {
35
+ /**
36
+ * Filter title
37
+ */
38
+ title: string;
39
+ /**
40
+ * Filter description
41
+ */
42
+ description: string;
43
+ /**
44
+ * Default value
45
+ */
46
+ default: boolean;
47
+ /**
48
+ * Does the filter support replace?
49
+ */
50
+ supportReplace: boolean;
51
+ }
52
+ /**
53
+ * Type of filters
54
+ *
55
+ */
56
+ export interface SearchFilters {
57
+ searchCellOutput: boolean;
58
+ onlySearchSelectedCells: boolean;
59
+ }
60
+
61
+ /**
62
+ * Base search provider interface
63
+ *
64
+ * #### Notes
65
+ * It is implemented by subprovider like searching on a single cell.
66
+ */
67
+ export interface BaseSearchProvider extends Disposable {
68
+ /**
69
+ * Get an initial query value if applicable so that it can be entered
70
+ * into the search box as an initial query
71
+ *
72
+ * @returns Initial value used to populate the search box.
73
+ */
74
+ getInitialQuery?(): string;
75
+ /**
76
+ * Start a search
77
+ *
78
+ * @param query Regular expression to test for
79
+ * @param filters Filters to apply when searching
80
+ */
81
+ startQuery(query: RegExp, filters?: SearchFilters): Promise<void>;
82
+
83
+ /**
84
+ * Stop a search and clear any internal state of the provider
85
+ */
86
+ endQuery(): Promise<void>;
87
+
88
+ /**
89
+ * Clear currently highlighted match.
90
+ */
91
+ clearHighlight(): Promise<void>;
92
+
93
+ /**
94
+ * Highlight the next match
95
+ *
96
+ * @param loop Whether to loop within the matches list.
97
+ *
98
+ * @returns The next match if it exists
99
+ */
100
+ highlightNext(loop?: boolean): Promise<SearchMatch | undefined>;
101
+
102
+ /**
103
+ * Highlight the previous match
104
+ *
105
+ * @param loop Whether to loop within the matches list.
106
+ *
107
+ * @returns The previous match if it exists.
108
+ */
109
+ highlightPrevious(loop?: boolean): Promise<SearchMatch | undefined>;
110
+
111
+ /**
112
+ * Replace the currently selected match with the provided text
113
+ * and highlight the next match.
114
+ *
115
+ * @param newText The replacement text
116
+ * @param loop Whether to loop within the matches list.
117
+ *
118
+ * @returns A promise that resolves with a boolean indicating whether a replace occurred.
119
+ */
120
+ replaceCurrentMatch(newText: string, loop?: boolean): Promise<boolean>;
121
+
122
+ /**
123
+ * Replace all matches in the widget with the provided text
124
+ *
125
+ * @param newText The replacement text.
126
+ *
127
+ * @returns A promise that resolves with a boolean indicating whether a replace occurred.
128
+ */
129
+ replaceAllMatches(newText: string): Promise<boolean>;
130
+
131
+ /**
132
+ * Signal indicating that something in the search has changed, so the UI should update
133
+ */
134
+ readonly stateChanged: Event<void>;
135
+
136
+ /**
137
+ * The current index of the selected match.
138
+ */
139
+ readonly currentMatchIndex: number | undefined;
140
+
141
+ /**
142
+ * The number of matches.
143
+ */
144
+ readonly matchesCount: number | undefined;
145
+ }
146
+
147
+ /**
148
+ * Search provider interface
149
+ */
150
+ export interface SearchProvider extends BaseSearchProvider {
151
+ /**
152
+ * Set to true if the widget under search is read-only, false
153
+ * if it is editable. Will be used to determine whether to show
154
+ * the replace option.
155
+ */
156
+ readonly isReadOnly: boolean;
157
+
158
+ /**
159
+ * Get the filters definition for the given provider.
160
+ *
161
+ * @returns The filters definition.
162
+ *
163
+ * ### Notes
164
+ * TODO For now it only supports boolean filters (represented with checkboxes)
165
+ */
166
+ getFilters?(): Record<string, SearchFilter>;
167
+
168
+ /**
169
+ * Validate a new filter value for the widget.
170
+ *
171
+ * @param name The filter name
172
+ * @param value The filter value candidate
173
+ *
174
+ * @returns The valid filter value
175
+ */
176
+ validateFilter?(name: string, value: boolean): Promise<boolean>;
177
+ }
178
+
179
+ // export const SearchProvider = Syringe.defineToken('SearchProvider');
180
+
181
+ export interface CellSearchProvider extends BaseSearchProvider {
182
+ isActive: boolean;
183
+ }
184
+
185
+ export interface CellSearchProviderContribution {
186
+ canHandle: (cell: CellView) => number;
187
+ factory: (cell: CellView) => CellSearchProvider;
188
+ getInitialQuery?: (cell: CellView) => string;
189
+ }
190
+
191
+ export const CellSearchProviderContribution = Syringe.defineToken(
192
+ 'CellSearchProviderContribution',
193
+ );
194
+
195
+ export const LIBRO_SEARCH_FOUND_CLASSES = [
196
+ 'cm-string',
197
+ 'cm-overlay',
198
+ 'cm-searching',
199
+ 'libro-searching',
200
+ ];
201
+ export const LIBRO_SEARCH_SELECTED_CLASSES = [
202
+ 'CodeMirror-selectedtext',
203
+ 'libro-selectedtext',
204
+ ];
205
+
206
+ export interface SearchProviderOption {
207
+ view: View;
208
+ }
209
+ export const SearchProviderOption = Symbol('SearchProviderOption');
@@ -0,0 +1,507 @@
1
+ import type { CellView } from '@difizen/libro-core';
2
+ import { LibroView } from '@difizen/libro-core';
3
+ import { inject, prop, transient, watch, equals } from '@difizen/mana-app';
4
+ import { Deferred, DisposableCollection } from '@difizen/mana-app';
5
+ import { l10n } from '@difizen/mana-l10n';
6
+
7
+ import { AbstractSearchProvider } from './abstract-search-provider.js';
8
+ import { LibroCellSearchProvider } from './libro-cell-search-provider.js';
9
+ import type {
10
+ CellSearchProvider,
11
+ SearchFilter,
12
+ SearchMatch,
13
+ SearchFilters,
14
+ } from './libro-search-protocol.js';
15
+ import { SearchProviderOption } from './libro-search-protocol.js';
16
+
17
+ export type LibroSearchProviderFactory = (
18
+ option: SearchProviderOption,
19
+ ) => LibroSearchProvider;
20
+ export const LibroSearchProviderFactory = Symbol('LibroSearchProviderFactory');
21
+ /**
22
+ * Libro view search provider
23
+ */
24
+ @transient()
25
+ export class LibroSearchProvider extends AbstractSearchProvider {
26
+ @inject(LibroCellSearchProvider) libroCellSearchProvider: LibroCellSearchProvider;
27
+ protected cellsChangeDeferred: Deferred<void> | undefined;
28
+
29
+ protected toDispose = new DisposableCollection();
30
+ @prop() protected currentProviderIndex: number | undefined = undefined;
31
+ @prop() searchCellOutput = true;
32
+ @prop() protected onlySearchSelectedCells = false;
33
+ @prop() replaceMode = false;
34
+
35
+ protected get filters(): SearchFilters {
36
+ return {
37
+ searchCellOutput: this.searchCellOutput && !this.replaceMode,
38
+ onlySearchSelectedCells: this.onlySearchSelectedCells,
39
+ };
40
+ }
41
+
42
+ protected query: RegExp | undefined = undefined;
43
+ @prop() protected searchProviders: (CellSearchProvider | undefined)[] = [];
44
+ @prop() protected providerMap = new Map<string, CellSearchProvider>();
45
+ protected documentHasChanged = false;
46
+ protected override view: LibroView;
47
+
48
+ updateSearchCellOutput(value: boolean): void {
49
+ this.searchCellOutput = value;
50
+ this.filters.searchCellOutput = value;
51
+ }
52
+ /**
53
+ * @param option Provide the view to search in
54
+ */
55
+ constructor(@inject(SearchProviderOption) option: SearchProviderOption) {
56
+ super(option);
57
+ this.view = option.view as LibroView;
58
+ this.toDispose.push(watch(this.view.model, 'active', this.onActiveCellChanged));
59
+ this.toDispose.push(watch(this.view.model, 'cells', this.onCellsChanged));
60
+ }
61
+
62
+ protected getProvider = (cell: CellView) => {
63
+ return this.providerMap.get(cell.id);
64
+ };
65
+ /**
66
+ * Report whether or not this provider has the ability to search on the given object
67
+ *
68
+ * @param domain Widget to test
69
+ * @returns Search ability
70
+ */
71
+ static isApplicable(domain: LibroView): domain is LibroView {
72
+ // check to see if the CMSearchProvider can search on the
73
+ // first cell, false indicates another editor is present
74
+ return domain instanceof LibroView;
75
+ }
76
+
77
+ /**
78
+ * The current index of the selected match.
79
+ */
80
+ override get currentMatchIndex(): number | undefined {
81
+ let agg = 0;
82
+ let found = false;
83
+ for (let idx = 0; idx < this.searchProviders.length; idx++) {
84
+ const provider = this.searchProviders[idx];
85
+ const localMatch = provider?.currentMatchIndex;
86
+ if (localMatch !== undefined) {
87
+ agg += localMatch;
88
+ found = true;
89
+ break;
90
+ } else {
91
+ agg += provider?.matchesCount ?? 0;
92
+ }
93
+ }
94
+ return found ? agg : undefined;
95
+ }
96
+
97
+ /**
98
+ * The number of matches.
99
+ */
100
+ override get matchesCount(): number | undefined {
101
+ const count = this.view.model.cells.reduce((sum, cell) => {
102
+ const provider = this.getProvider(cell);
103
+ sum += provider?.matchesCount || 0;
104
+ return sum;
105
+ }, 0);
106
+ if (count === 0) {
107
+ return undefined;
108
+ }
109
+ return count;
110
+ }
111
+
112
+ /**
113
+ * Set to true if the widget under search is read-only, false
114
+ * if it is editable. Will be used to determine whether to show
115
+ * the replace option.
116
+ */
117
+ get isReadOnly(): boolean {
118
+ return this.view?.model?.readOnly ?? false;
119
+ }
120
+
121
+ /**
122
+ * Dispose of the resources held by the search provider.
123
+ *
124
+ * #### Notes
125
+ * If the object's `dispose` method is called more than once, all
126
+ * calls made after the first will be a no-op.
127
+ *
128
+ * #### Undefined Behavior
129
+ * It is undefined behavior to use any functionality of the object
130
+ * after it has been disposed unless otherwise explicitly noted.
131
+ */
132
+ override dispose(): void {
133
+ if (this.isDisposed) {
134
+ return;
135
+ }
136
+ this.toDispose.dispose();
137
+ this.providerMap.clear();
138
+ super.dispose();
139
+
140
+ // const index = this.view.model.active;
141
+ this.endQuery()
142
+ .then(() => {
143
+ if (!this.view.isDisposed) {
144
+ // this.view.model.active = index;
145
+ // TODO: should active cell?
146
+ }
147
+ return;
148
+ })
149
+ .catch((reason) => {
150
+ console.error(`Fail to end search query in notebook:\n${reason}`);
151
+ });
152
+ }
153
+
154
+ /**
155
+ * Get the filters for the given provider.
156
+ *
157
+ * @returns The filters.
158
+ */
159
+ override getFilters(): Record<string, SearchFilter> {
160
+ return {
161
+ output: {
162
+ title: l10n.t('在 Output 中查找'),
163
+ description: l10n.t('在 Output 中查找'),
164
+ default: false,
165
+ supportReplace: false,
166
+ },
167
+ selectedCells: {
168
+ title: l10n.t('仅在选中 cell 中查找'),
169
+ description: l10n.t('仅在选中 cell 中查找'),
170
+ default: false,
171
+ supportReplace: true,
172
+ },
173
+ };
174
+ }
175
+
176
+ /**
177
+ * Get an initial query value if applicable so that it can be entered
178
+ * into the search box as an initial query
179
+ *
180
+ * @returns Initial value used to populate the search box.
181
+ */
182
+ override getInitialQuery = (): string => {
183
+ const activeCell = this.view.model.active;
184
+ if (activeCell) {
185
+ return this.libroCellSearchProvider.getInitialQuery(activeCell);
186
+ }
187
+ return '';
188
+ };
189
+
190
+ /**
191
+ * Clear currently highlighted match.
192
+ */
193
+ clearHighlight = async (): Promise<void> => {
194
+ if (this.currentProviderIndex !== undefined) {
195
+ await this.searchProviders[this.currentProviderIndex]?.clearHighlight();
196
+ this.currentProviderIndex = undefined;
197
+ }
198
+ };
199
+
200
+ /**
201
+ * Highlight the next match.
202
+ *
203
+ * @param loop Whether to loop within the matches list.
204
+ *
205
+ * @returns The next match if available.
206
+ */
207
+ highlightNext = async (loop = true): Promise<SearchMatch | undefined> => {
208
+ const match = await this.stepNext(false, loop);
209
+ return match ?? undefined;
210
+ };
211
+
212
+ /**
213
+ * Highlight the previous match.
214
+ *
215
+ * @param loop Whether to loop within the matches list.
216
+ *
217
+ * @returns The previous match if available.
218
+ */
219
+ highlightPrevious = async (loop = true): Promise<SearchMatch | undefined> => {
220
+ const match = await this.stepNext(true, loop);
221
+ return match ?? undefined;
222
+ };
223
+
224
+ /**
225
+ * Search for a regular expression with optional filters.
226
+ *
227
+ * @param query A regular expression to test for
228
+ * @param filters Filter parameters to pass to provider
229
+ *
230
+ */
231
+ startQuery = async (
232
+ query: RegExp,
233
+ _filters?: SearchFilters,
234
+ highlightNext = true,
235
+ ): Promise<void> => {
236
+ if (!this.view) {
237
+ return;
238
+ }
239
+ await this.endQuery();
240
+ const cells = this.view.model.cells;
241
+
242
+ this.query = query;
243
+
244
+ // TODO: support selected cells
245
+ if (this.filters?.onlySearchSelectedCells) {
246
+ // watch(this.view.model, 'selections', this._onSelectionChanged);
247
+ // this.view.model.selectionChanged.connect(this._onSelectionChanged, this);
248
+ }
249
+ // For each cell, create a search provider
250
+ this.searchProviders = await Promise.all(
251
+ cells.map(async (cell) => {
252
+ let cellSearchProvider;
253
+ if (this.providerMap.has(cell.id)) {
254
+ cellSearchProvider = this.providerMap.get(cell.id);
255
+ } else {
256
+ cellSearchProvider =
257
+ this.libroCellSearchProvider.createCellSearchProvider(cell);
258
+ }
259
+ if (cellSearchProvider) {
260
+ this.providerMap.set(cell.id, cellSearchProvider);
261
+ }
262
+ // cellSearchProvider.stateChanged(this._onSearchProviderChanged);
263
+ // await cellSearchProvider.setIsActive(
264
+ // !this._filters!.selectedCells || this.widget.content.isSelectedOrActive(cell),
265
+ // );
266
+ await cellSearchProvider?.startQuery(query, this.filters);
267
+ return cellSearchProvider;
268
+ }),
269
+ );
270
+ this.currentProviderIndex = this.getActiveIndex();
271
+
272
+ if (!this.documentHasChanged && highlightNext) {
273
+ await this.highlightNext(false);
274
+ }
275
+ this.documentHasChanged = false;
276
+
277
+ return Promise.resolve();
278
+ };
279
+
280
+ /**
281
+ * Stop the search and clear all internal state.
282
+ */
283
+ endQuery = async (): Promise<void> => {
284
+ await Promise.all(
285
+ this.searchProviders.map((provider) => {
286
+ // provider?.stateChanged(this._onSearchProviderChanged);
287
+ return provider?.endQuery();
288
+ }),
289
+ );
290
+ this.searchProviders.length = 0;
291
+ this.currentProviderIndex = undefined;
292
+ };
293
+
294
+ /**
295
+ * Replace the currently selected match with the provided text
296
+ *
297
+ * @param newText The replacement text.
298
+ * @param loop Whether to loop within the matches list.
299
+ *
300
+ * @returns A promise that resolves with a boolean indicating whether a replace occurred.
301
+ */
302
+ replaceCurrentMatch = async (newText: string, loop = true): Promise<boolean> => {
303
+ let replaceOccurred = false;
304
+ // TODO: makrdown unrendered
305
+ // const unrenderMarkdownCell = async (highlightNext = false): Promise<void> => {
306
+ // // Unrendered markdown cell
307
+ // const activeCell = this.view?.model.active;
308
+ // if (activeCell?.model.type === 'markdown' && (activeCell as MarkdownCell).rendered) {
309
+ // (activeCell as MarkdownCell).rendered = false;
310
+ // if (highlightNext) {
311
+ // await this.highlightNext(loop);
312
+ // }
313
+ // }
314
+ // };
315
+
316
+ if (this.currentProviderIndex !== undefined) {
317
+ // await unrenderMarkdownCell();
318
+
319
+ const searchEngine = this.searchProviders[this.currentProviderIndex];
320
+ replaceOccurred = !!(await searchEngine?.replaceCurrentMatch(newText));
321
+ }
322
+
323
+ await this.highlightNext(loop);
324
+ // Force highlighting the first hit in the unrendered cell
325
+ // await unrenderMarkdownCell(true);
326
+ return replaceOccurred;
327
+ };
328
+
329
+ /**
330
+ * Replace all matches in the notebook with the provided text
331
+ *
332
+ * @param newText The replacement text.
333
+ *
334
+ * @returns A promise that resolves with a boolean indicating whether a replace occurred.
335
+ */
336
+ replaceAllMatches = async (newText: string): Promise<boolean> => {
337
+ const replacementOccurred = await Promise.all(
338
+ this.searchProviders.map((provider) => {
339
+ return provider?.replaceAllMatches(newText);
340
+ }),
341
+ );
342
+ return replacementOccurred.includes(true);
343
+ };
344
+
345
+ protected addCellProvider = (index: number) => {
346
+ const cell = this.view.model.cells[index];
347
+ const cellSearchProvider =
348
+ this.libroCellSearchProvider.createCellSearchProvider(cell);
349
+ const current = this.searchProviders.slice();
350
+ current.splice(index, 0, cellSearchProvider);
351
+ this.searchProviders = current;
352
+ if (cellSearchProvider) {
353
+ cellSearchProvider.stateChanged(this.onSearchProviderChanged);
354
+ if (this.query) {
355
+ cellSearchProvider.startQuery(this.query, this.filters);
356
+ }
357
+ }
358
+ // void cellSearchProvider
359
+ // .setIsActive(
360
+ // !(this._filters?.selectedCells ?? false) || this.widget.content.isSelectedOrActive(cell),
361
+ // )
362
+ // .then(() => {
363
+ // void cellSearchProvider.startQuery(this._query, this._filters);
364
+ // });
365
+ };
366
+
367
+ protected removeCellProvider = (index: number) => {
368
+ const current = this.searchProviders.slice();
369
+ const provider = current.slice(index, 1)[0];
370
+ provider?.dispose();
371
+ };
372
+
373
+ protected doCellsChanged = async (): Promise<void> => {
374
+ if (this.query) {
375
+ this.startQuery(this.query);
376
+ } else {
377
+ this.endQuery();
378
+ }
379
+ this.onSearchProviderChanged();
380
+ this.cellsChangeDeferred = undefined;
381
+ };
382
+ protected onCellsChanged = async (): Promise<void> => {
383
+ if (!this.cellsChangeDeferred) {
384
+ this.cellsChangeDeferred = new Deferred();
385
+ this.cellsChangeDeferred.promise.then(this.doCellsChanged).catch(() => {
386
+ //
387
+ });
388
+ this.cellsChangeDeferred.resolve();
389
+ }
390
+ };
391
+
392
+ protected getActiveIndex = (): number | undefined => {
393
+ if (!this.view.activeCell) {
394
+ return undefined;
395
+ }
396
+ const index = this.view.model.cells.findIndex((cell) =>
397
+ equals(cell, this.view.activeCell),
398
+ );
399
+ if (index < 0) {
400
+ return undefined;
401
+ }
402
+ return index;
403
+ };
404
+ protected stepNext = async (
405
+ reverse = false,
406
+ loop = false,
407
+ ): Promise<SearchMatch | undefined> => {
408
+ const activateNewMatch = async () => {
409
+ // if (this.getActiveIndex() !== this._currentProviderIndex!) {
410
+ // this.widget.content.activeCellIndex = this._currentProviderIndex!;
411
+ // }
412
+ // const activeCell = this.view.activeCell;
413
+ // if (!activeCell.inViewport) {
414
+ // try {
415
+ // if (this.view.activeCell) {
416
+ // this.view.model.scrollToView(this.view.activeCell);
417
+ // }
418
+ // } catch (error) {
419
+ // // no-op
420
+ // }
421
+ // }
422
+ // // Unhide cell
423
+ // if (activeCell.inputHidden) {
424
+ // activeCell.inputHidden = false;
425
+ // }
426
+ // if (!activeCell.inViewport) {
427
+ // // It will not be possible the cell is not in the view
428
+ // return;
429
+ // }
430
+ // await activeCell.ready;
431
+ // const editor = activeCell.editor! as CodeMirrorEditor;
432
+ // editor.revealSelection(editor.getSelection());
433
+ };
434
+ if (this.currentProviderIndex === undefined) {
435
+ this.currentProviderIndex = this.getActiveIndex()!;
436
+ }
437
+ const startIndex = this.currentProviderIndex;
438
+ do {
439
+ const searchEngine = this.searchProviders[this.currentProviderIndex];
440
+ const match = reverse
441
+ ? await searchEngine?.highlightPrevious()
442
+ : await searchEngine?.highlightNext();
443
+ if (match) {
444
+ await activateNewMatch();
445
+ return match;
446
+ } else {
447
+ this.currentProviderIndex = this.currentProviderIndex + (reverse ? -1 : 1);
448
+ if (loop) {
449
+ // We loop on all cells, not hit found
450
+ if (this.currentProviderIndex === startIndex) {
451
+ break;
452
+ }
453
+ this.currentProviderIndex =
454
+ (this.currentProviderIndex + this.searchProviders.length) %
455
+ this.searchProviders.length;
456
+ }
457
+ }
458
+ } while (
459
+ 0 <= this.currentProviderIndex &&
460
+ this.currentProviderIndex < this.searchProviders.length
461
+ );
462
+
463
+ if (loop) {
464
+ // Search a last time in the first provider as it may contain more
465
+ // than one matches
466
+ const searchEngine = this.searchProviders[this.currentProviderIndex];
467
+ const match = reverse
468
+ ? await searchEngine?.highlightPrevious()
469
+ : await searchEngine?.highlightNext();
470
+
471
+ if (match) {
472
+ await activateNewMatch();
473
+ return match;
474
+ }
475
+ }
476
+
477
+ this.currentProviderIndex = undefined;
478
+ return undefined;
479
+ };
480
+
481
+ protected onActiveCellChanged = async () => {
482
+ await this._onSelectionChanged();
483
+
484
+ if (this.getActiveIndex() !== this.currentProviderIndex) {
485
+ await this.clearHighlight();
486
+ }
487
+ };
488
+
489
+ protected onSearchProviderChanged = () => {
490
+ // Don't highlight the next occurrence when the query
491
+ // follows a document change
492
+ this.documentHasChanged = true;
493
+ this._stateChanged.fire();
494
+ };
495
+
496
+ protected _onSelectionChanged = async () => {
497
+ // if (this.onlySelectedCells) {
498
+ // const cells = this.widget.content.widgets;
499
+ // await Promise.all(
500
+ // this._searchProviders.map((provider, index) =>
501
+ // provider.setIsActive(this.widget.content.isSelectedOrActive(cells[index])),
502
+ // ),
503
+ // );
504
+ // this._onSearchProviderChanged();
505
+ // }
506
+ };
507
+ }