1 |
|
2 |
|
3 |
|
4 | import { Dialog, showDialog } from '@jupyterlab/apputils';
|
5 | import {
|
6 | CellSearchProvider,
|
7 | CodeCell,
|
8 | createCellSearchProvider,
|
9 | ICellModel,
|
10 | MarkdownCell
|
11 | } from '@jupyterlab/cells';
|
12 | import { IHighlightAdjacentMatchOptions } from '@jupyterlab/codemirror';
|
13 | import { CodeEditor } from '@jupyterlab/codeeditor';
|
14 | import { IChangedArgs } from '@jupyterlab/coreutils';
|
15 | import {
|
16 | IFilter,
|
17 | IFilters,
|
18 | IReplaceOptions,
|
19 | IReplaceOptionsSupport,
|
20 | ISearchMatch,
|
21 | ISearchProvider,
|
22 | SearchProvider,
|
23 | SelectionState
|
24 | } from '@jupyterlab/documentsearch';
|
25 | import { IObservableList, IObservableMap } from '@jupyterlab/observables';
|
26 | import { ITranslator, nullTranslator } from '@jupyterlab/translation';
|
27 | import { ArrayExt } from '@lumino/algorithm';
|
28 | import { Widget } from '@lumino/widgets';
|
29 | import { CellList } from './celllist';
|
30 | import { NotebookPanel } from './panel';
|
31 | import { Notebook } from './widget';
|
32 |
|
33 |
|
34 |
|
35 |
|
36 | export class NotebookSearchProvider extends SearchProvider<NotebookPanel> {
|
37 | |
38 |
|
39 |
|
40 |
|
41 |
|
42 |
|
43 | constructor(
|
44 | widget: NotebookPanel,
|
45 | protected translator: ITranslator = nullTranslator
|
46 | ) {
|
47 | super(widget);
|
48 |
|
49 | this._handleHighlightsAfterActiveCellChange =
|
50 | this._handleHighlightsAfterActiveCellChange.bind(this);
|
51 | this.widget.model!.cells.changed.connect(this._onCellsChanged, this);
|
52 | this.widget.content.activeCellChanged.connect(
|
53 | this._onActiveCellChanged,
|
54 | this
|
55 | );
|
56 | this.widget.content.selectionChanged.connect(
|
57 | this._onCellSelectionChanged,
|
58 | this
|
59 | );
|
60 | this.widget.content.stateChanged.connect(
|
61 | this._onNotebookStateChanged,
|
62 | this
|
63 | );
|
64 | this._observeActiveCell();
|
65 | this._filtersChanged.connect(this._setEnginesSelectionSearchMode, this);
|
66 | }
|
67 |
|
68 | private _onNotebookStateChanged(_: Notebook, args: IChangedArgs<any>) {
|
69 | if (args.name === 'mode') {
|
70 |
|
71 | window.setTimeout(() => {
|
72 | if (
|
73 | args.newValue === 'command' &&
|
74 | document.activeElement?.closest('.jp-DocumentSearch-overlay')
|
75 | ) {
|
76 |
|
77 | return;
|
78 | }
|
79 | this._updateSelectionMode();
|
80 | this._filtersChanged.emit();
|
81 | }, 0);
|
82 | }
|
83 | }
|
84 |
|
85 | |
86 |
|
87 |
|
88 |
|
89 |
|
90 |
|
91 | static isApplicable(domain: Widget): domain is NotebookPanel {
|
92 |
|
93 |
|
94 | return domain instanceof NotebookPanel;
|
95 | }
|
96 |
|
97 | |
98 |
|
99 |
|
100 |
|
101 |
|
102 |
|
103 |
|
104 |
|
105 |
|
106 |
|
107 |
|
108 |
|
109 | static createNew(
|
110 | widget: NotebookPanel,
|
111 | translator?: ITranslator
|
112 | ): ISearchProvider {
|
113 | return new NotebookSearchProvider(widget, translator);
|
114 | }
|
115 |
|
116 | |
117 |
|
118 |
|
119 | get currentMatchIndex(): number | null {
|
120 | let agg = 0;
|
121 | let found = false;
|
122 | for (let idx = 0; idx < this._searchProviders.length; idx++) {
|
123 | const provider = this._searchProviders[idx];
|
124 | if (this._currentProviderIndex == idx) {
|
125 | const localMatch = provider.currentMatchIndex;
|
126 | if (localMatch === null) {
|
127 | return null;
|
128 | }
|
129 | agg += localMatch;
|
130 | found = true;
|
131 | break;
|
132 | } else {
|
133 | agg += provider.matchesCount;
|
134 | }
|
135 | }
|
136 | return found ? agg : null;
|
137 | }
|
138 |
|
139 | |
140 |
|
141 |
|
142 | get matchesCount(): number | null {
|
143 | return this._searchProviders.reduce(
|
144 | (sum, provider) => (sum += provider.matchesCount),
|
145 | 0
|
146 | );
|
147 | }
|
148 |
|
149 | |
150 |
|
151 |
|
152 |
|
153 |
|
154 | get isReadOnly(): boolean {
|
155 | return this.widget?.content.model?.readOnly ?? false;
|
156 | }
|
157 |
|
158 | |
159 |
|
160 |
|
161 | get replaceOptionsSupport(): IReplaceOptionsSupport {
|
162 | return {
|
163 | preserveCase: true
|
164 | };
|
165 | }
|
166 |
|
167 | getSelectionState(): SelectionState {
|
168 | const cellMode = this._selectionSearchMode === 'cells';
|
169 | const selectedCount = cellMode ? this._selectedCells : this._selectedLines;
|
170 | return selectedCount > 1
|
171 | ? 'multiple'
|
172 | : selectedCount === 1 && !cellMode
|
173 | ? 'single'
|
174 | : 'none';
|
175 | }
|
176 |
|
177 | |
178 |
|
179 |
|
180 |
|
181 |
|
182 |
|
183 |
|
184 |
|
185 |
|
186 |
|
187 |
|
188 | dispose(): void {
|
189 | if (this.isDisposed) {
|
190 | return;
|
191 | }
|
192 |
|
193 | this.widget.content.activeCellChanged.disconnect(
|
194 | this._onActiveCellChanged,
|
195 | this
|
196 | );
|
197 |
|
198 | this.widget.model?.cells.changed.disconnect(this._onCellsChanged, this);
|
199 |
|
200 | this.widget.content.stateChanged.disconnect(
|
201 | this._onNotebookStateChanged,
|
202 | this
|
203 | );
|
204 | this.widget.content.selectionChanged.disconnect(
|
205 | this._onCellSelectionChanged,
|
206 | this
|
207 | );
|
208 | this._stopObservingLastCell();
|
209 |
|
210 | super.dispose();
|
211 |
|
212 | const index = this.widget.content.activeCellIndex;
|
213 | this.endQuery()
|
214 | .then(() => {
|
215 | if (!this.widget.isDisposed) {
|
216 | this.widget.content.activeCellIndex = index;
|
217 | }
|
218 | })
|
219 | .catch(reason => {
|
220 | console.error(`Fail to end search query in notebook:\n${reason}`);
|
221 | });
|
222 | }
|
223 |
|
224 | |
225 |
|
226 |
|
227 |
|
228 |
|
229 | getFilters(): { [key: string]: IFilter } {
|
230 | const trans = this.translator.load('jupyterlab');
|
231 |
|
232 | return {
|
233 | output: {
|
234 | title: trans.__('Search Cell Outputs'),
|
235 | description: trans.__('Search in the cell outputs.'),
|
236 | default: false,
|
237 | supportReplace: false
|
238 | },
|
239 | selection: {
|
240 | title:
|
241 | this._selectionSearchMode === 'cells'
|
242 | ? trans._n(
|
243 | 'Search in %1 Selected Cell',
|
244 | 'Search in %1 Selected Cells',
|
245 | this._selectedCells
|
246 | )
|
247 | : trans._n(
|
248 | 'Search in %1 Selected Line',
|
249 | 'Search in %1 Selected Lines',
|
250 | this._selectedLines
|
251 | ),
|
252 | description: trans.__(
|
253 | 'Search only in the selected cells or text (depending on edit/command mode).'
|
254 | ),
|
255 | default: false,
|
256 | supportReplace: true
|
257 | }
|
258 | };
|
259 | }
|
260 |
|
261 | |
262 |
|
263 |
|
264 |
|
265 |
|
266 |
|
267 | private _updateSelectionMode() {
|
268 | if (this._selectionLock) {
|
269 | return;
|
270 | }
|
271 | this._selectionSearchMode =
|
272 | this._selectedCells === 1 &&
|
273 | this.widget.content.mode === 'edit' &&
|
274 | this._selectedLines !== 0
|
275 | ? 'text'
|
276 | : 'cells';
|
277 | }
|
278 |
|
279 | |
280 |
|
281 |
|
282 |
|
283 |
|
284 |
|
285 | getInitialQuery(): string {
|
286 |
|
287 | return window.getSelection()?.toString() || '';
|
288 | }
|
289 |
|
290 | |
291 |
|
292 |
|
293 | async clearHighlight(): Promise<void> {
|
294 | this._selectionLock = true;
|
295 | if (
|
296 | this._currentProviderIndex !== null &&
|
297 | this._currentProviderIndex < this._searchProviders.length
|
298 | ) {
|
299 | await this._searchProviders[this._currentProviderIndex].clearHighlight();
|
300 | this._currentProviderIndex = null;
|
301 | }
|
302 | this._selectionLock = false;
|
303 | }
|
304 |
|
305 | |
306 |
|
307 |
|
308 |
|
309 |
|
310 |
|
311 |
|
312 | async highlightNext(
|
313 | loop: boolean = true,
|
314 | options?: IHighlightAdjacentMatchOptions
|
315 | ): Promise<ISearchMatch | undefined> {
|
316 | const match = await this._stepNext(false, loop, options);
|
317 | return match ?? undefined;
|
318 | }
|
319 |
|
320 | |
321 |
|
322 |
|
323 |
|
324 |
|
325 |
|
326 |
|
327 | async highlightPrevious(
|
328 | loop: boolean = true,
|
329 | options?: IHighlightAdjacentMatchOptions
|
330 | ): Promise<ISearchMatch | undefined> {
|
331 | const match = await this._stepNext(true, loop, options);
|
332 | return match ?? undefined;
|
333 | }
|
334 |
|
335 | |
336 |
|
337 |
|
338 |
|
339 |
|
340 |
|
341 |
|
342 | async startQuery(
|
343 | query: RegExp,
|
344 | filters: IFilters | undefined
|
345 | ): Promise<void> {
|
346 | if (!this.widget) {
|
347 | return;
|
348 | }
|
349 | await this.endQuery();
|
350 | this._searchActive = true;
|
351 | let cells = this.widget.content.widgets;
|
352 |
|
353 | this._query = query;
|
354 | this._filters = {
|
355 | output: false,
|
356 | selection: false,
|
357 | ...(filters ?? {})
|
358 | };
|
359 |
|
360 | this._onSelection = this._filters.selection;
|
361 |
|
362 | const currentProviderIndex = this.widget.content.activeCellIndex;
|
363 |
|
364 |
|
365 | this._searchProviders = await Promise.all(
|
366 | cells.map(async (cell, index) => {
|
367 | const cellSearchProvider = createCellSearchProvider(cell);
|
368 |
|
369 | await cellSearchProvider.setIsActive(
|
370 | !this._filters!.selection ||
|
371 | this.widget.content.isSelectedOrActive(cell)
|
372 | );
|
373 |
|
374 | if (
|
375 | this._onSelection &&
|
376 | this._selectionSearchMode === 'text' &&
|
377 | index === currentProviderIndex
|
378 | ) {
|
379 | if (this._textSelection) {
|
380 | await cellSearchProvider.setSearchSelection(this._textSelection);
|
381 | }
|
382 | }
|
383 |
|
384 | await cellSearchProvider.startQuery(query, this._filters);
|
385 |
|
386 | return cellSearchProvider;
|
387 | })
|
388 | );
|
389 | this._currentProviderIndex = currentProviderIndex;
|
390 |
|
391 |
|
392 |
|
393 |
|
394 |
|
395 |
|
396 |
|
397 |
|
398 | await this.highlightNext(true, {
|
399 | from: 'selection-start',
|
400 | scroll: false,
|
401 | select: false
|
402 | });
|
403 |
|
404 | return Promise.resolve();
|
405 | }
|
406 |
|
407 | |
408 |
|
409 |
|
410 | async endQuery(): Promise<void> {
|
411 | await Promise.all(
|
412 | this._searchProviders.map(provider => {
|
413 | return provider.endQuery().then(() => {
|
414 | provider.dispose();
|
415 | });
|
416 | })
|
417 | );
|
418 |
|
419 | this._searchActive = false;
|
420 | this._searchProviders.length = 0;
|
421 | this._currentProviderIndex = null;
|
422 | }
|
423 |
|
424 | |
425 |
|
426 |
|
427 |
|
428 |
|
429 |
|
430 |
|
431 |
|
432 | async replaceCurrentMatch(
|
433 | newText: string,
|
434 | loop = true,
|
435 | options?: IReplaceOptions
|
436 | ): Promise<boolean> {
|
437 | let replaceOccurred = false;
|
438 |
|
439 | const unrenderMarkdownCell = async (
|
440 | highlightNext = false
|
441 | ): Promise<void> => {
|
442 |
|
443 | const activeCell = this.widget?.content.activeCell;
|
444 | if (
|
445 | activeCell?.model.type === 'markdown' &&
|
446 | (activeCell as MarkdownCell).rendered
|
447 | ) {
|
448 | (activeCell as MarkdownCell).rendered = false;
|
449 | if (highlightNext) {
|
450 | await this.highlightNext(loop);
|
451 | }
|
452 | }
|
453 | };
|
454 |
|
455 | if (this._currentProviderIndex !== null) {
|
456 | await unrenderMarkdownCell();
|
457 |
|
458 | const searchEngine = this._searchProviders[this._currentProviderIndex];
|
459 | replaceOccurred = await searchEngine.replaceCurrentMatch(
|
460 | newText,
|
461 | false,
|
462 | options
|
463 | );
|
464 | if (searchEngine.currentMatchIndex === null) {
|
465 |
|
466 | await this.highlightNext(loop);
|
467 | }
|
468 | }
|
469 |
|
470 |
|
471 |
|
472 | await unrenderMarkdownCell(true);
|
473 | return replaceOccurred;
|
474 | }
|
475 |
|
476 | |
477 |
|
478 |
|
479 |
|
480 |
|
481 |
|
482 |
|
483 | async replaceAllMatches(
|
484 | newText: string,
|
485 | options?: IReplaceOptions
|
486 | ): Promise<boolean> {
|
487 | const replacementOccurred = await Promise.all(
|
488 | this._searchProviders.map(provider => {
|
489 | return provider.replaceAllMatches(newText, options);
|
490 | })
|
491 | );
|
492 | return replacementOccurred.includes(true);
|
493 | }
|
494 |
|
495 | async validateFilter(name: string, value: boolean): Promise<boolean> {
|
496 | if (name !== 'output') {
|
497 |
|
498 | return value;
|
499 | }
|
500 |
|
501 |
|
502 | if (
|
503 | value &&
|
504 | this.widget.content.widgets.some(
|
505 | w => w instanceof CodeCell && w.isPlaceholder()
|
506 | )
|
507 | ) {
|
508 | const trans = this.translator.load('jupyterlab');
|
509 |
|
510 | const reply = await showDialog({
|
511 | title: trans.__('Confirmation'),
|
512 | body: trans.__(
|
513 | 'Searching outputs is expensive and requires to first rendered all outputs. Are you sure you want to search in the cell outputs?'
|
514 | ),
|
515 | buttons: [
|
516 | Dialog.cancelButton({ label: trans.__('Cancel') }),
|
517 | Dialog.okButton({ label: trans.__('Ok') })
|
518 | ]
|
519 | });
|
520 | if (reply.button.accept) {
|
521 | this.widget.content.widgets.forEach((w, i) => {
|
522 | if (w instanceof CodeCell && w.isPlaceholder()) {
|
523 | this.widget.content.renderCellOutputs(i);
|
524 | }
|
525 | });
|
526 | } else {
|
527 | return false;
|
528 | }
|
529 | }
|
530 |
|
531 | return value;
|
532 | }
|
533 |
|
534 | private _addCellProvider(index: number) {
|
535 | const cell = this.widget.content.widgets[index];
|
536 | const cellSearchProvider = createCellSearchProvider(cell);
|
537 |
|
538 | ArrayExt.insert(this._searchProviders, index, cellSearchProvider);
|
539 |
|
540 | void cellSearchProvider
|
541 | .setIsActive(
|
542 | !(this._filters?.selection ?? false) ||
|
543 | this.widget.content.isSelectedOrActive(cell)
|
544 | )
|
545 | .then(() => {
|
546 | if (this._searchActive) {
|
547 | void cellSearchProvider.startQuery(this._query, this._filters);
|
548 | }
|
549 | });
|
550 | }
|
551 |
|
552 | private _removeCellProvider(index: number) {
|
553 | const provider = ArrayExt.removeAt(this._searchProviders, index);
|
554 | provider?.dispose();
|
555 | }
|
556 |
|
557 | private async _onCellsChanged(
|
558 | cells: CellList,
|
559 | changes: IObservableList.IChangedArgs<ICellModel>
|
560 | ): Promise<void> {
|
561 | switch (changes.type) {
|
562 | case 'add':
|
563 | changes.newValues.forEach((model, index) => {
|
564 | this._addCellProvider(changes.newIndex + index);
|
565 | });
|
566 | break;
|
567 | case 'move':
|
568 | ArrayExt.move(
|
569 | this._searchProviders,
|
570 | changes.oldIndex,
|
571 | changes.newIndex
|
572 | );
|
573 | break;
|
574 | case 'remove':
|
575 | for (let index = 0; index < changes.oldValues.length; index++) {
|
576 | this._removeCellProvider(changes.oldIndex);
|
577 | }
|
578 | break;
|
579 | case 'set':
|
580 | changes.newValues.forEach((model, index) => {
|
581 | this._addCellProvider(changes.newIndex + index);
|
582 | this._removeCellProvider(changes.newIndex + index + 1);
|
583 | });
|
584 |
|
585 | break;
|
586 | }
|
587 | this._stateChanged.emit();
|
588 | }
|
589 |
|
590 | private async _stepNext(
|
591 | reverse = false,
|
592 | loop = false,
|
593 | options?: IHighlightAdjacentMatchOptions
|
594 | ): Promise<ISearchMatch | null> {
|
595 | const activateNewMatch = async (match: ISearchMatch) => {
|
596 | const shouldScroll = options?.scroll ?? true;
|
597 | if (!shouldScroll) {
|
598 |
|
599 | return;
|
600 | }
|
601 |
|
602 | this._selectionLock = true;
|
603 | if (this.widget.content.activeCellIndex !== this._currentProviderIndex!) {
|
604 | this.widget.content.activeCellIndex = this._currentProviderIndex!;
|
605 | }
|
606 | if (this.widget.content.activeCellIndex === -1) {
|
607 | console.warn('No active cell (no cells or no model), aborting search');
|
608 | this._selectionLock = false;
|
609 | return;
|
610 | }
|
611 | const activeCell = this.widget.content.activeCell!;
|
612 |
|
613 | if (!activeCell.inViewport) {
|
614 | try {
|
615 | await this.widget.content.scrollToItem(this._currentProviderIndex!);
|
616 | } catch (error) {
|
617 |
|
618 | }
|
619 | }
|
620 |
|
621 |
|
622 | if (activeCell.inputHidden) {
|
623 | activeCell.inputHidden = false;
|
624 | }
|
625 |
|
626 | if (!activeCell.inViewport) {
|
627 | this._selectionLock = false;
|
628 |
|
629 | return;
|
630 | }
|
631 |
|
632 | await activeCell.ready;
|
633 | const editor = activeCell.editor!;
|
634 | editor.revealPosition(editor.getPositionAt(match.position)!);
|
635 | this._selectionLock = false;
|
636 | };
|
637 |
|
638 | if (this._currentProviderIndex === null) {
|
639 | this._currentProviderIndex = this.widget.content.activeCellIndex;
|
640 | }
|
641 |
|
642 |
|
643 |
|
644 |
|
645 | if (reverse && this.widget.content.mode === 'command') {
|
646 | const searchEngine = this._searchProviders[this._currentProviderIndex];
|
647 | const currentMatch = searchEngine.getCurrentMatch();
|
648 | if (!currentMatch) {
|
649 | this._currentProviderIndex -= 1;
|
650 | }
|
651 | if (loop) {
|
652 | this._currentProviderIndex =
|
653 | (this._currentProviderIndex + this._searchProviders.length) %
|
654 | this._searchProviders.length;
|
655 | }
|
656 | }
|
657 |
|
658 | const startIndex = this._currentProviderIndex;
|
659 | do {
|
660 | const searchEngine = this._searchProviders[this._currentProviderIndex];
|
661 |
|
662 | const match = reverse
|
663 | ? await searchEngine.highlightPrevious(false, options)
|
664 | : await searchEngine.highlightNext(false, options);
|
665 |
|
666 | if (match) {
|
667 | await activateNewMatch(match);
|
668 | return match;
|
669 | } else {
|
670 | this._currentProviderIndex =
|
671 | this._currentProviderIndex + (reverse ? -1 : 1);
|
672 |
|
673 | if (loop) {
|
674 | this._currentProviderIndex =
|
675 | (this._currentProviderIndex + this._searchProviders.length) %
|
676 | this._searchProviders.length;
|
677 | }
|
678 | }
|
679 | } while (
|
680 | loop
|
681 | ?
|
682 | this._currentProviderIndex !== startIndex
|
683 | : 0 <= this._currentProviderIndex &&
|
684 | this._currentProviderIndex < this._searchProviders.length
|
685 | );
|
686 |
|
687 | if (loop) {
|
688 |
|
689 | const searchEngine = this._searchProviders[startIndex];
|
690 | const match = reverse
|
691 | ? await searchEngine.highlightPrevious(false, options)
|
692 | : await searchEngine.highlightNext(false, options);
|
693 | if (match) {
|
694 | await activateNewMatch(match);
|
695 | return match;
|
696 | }
|
697 | }
|
698 |
|
699 | this._currentProviderIndex = null;
|
700 | return null;
|
701 | }
|
702 |
|
703 | private async _onActiveCellChanged() {
|
704 | if (this._delayedActiveCellChangeHandler !== null) {
|
705 |
|
706 |
|
707 | clearTimeout(this._delayedActiveCellChangeHandler);
|
708 | this._delayedActiveCellChangeHandler = null;
|
709 | }
|
710 |
|
711 | if (this.widget.content.activeCellIndex !== this._currentProviderIndex) {
|
712 |
|
713 |
|
714 |
|
715 |
|
716 |
|
717 | this._delayedActiveCellChangeHandler = window.setTimeout(() => {
|
718 | this.delayedActiveCellChangeHandlerReady =
|
719 | this._handleHighlightsAfterActiveCellChange();
|
720 | }, 0);
|
721 | }
|
722 | this._observeActiveCell();
|
723 | }
|
724 |
|
725 | private async _handleHighlightsAfterActiveCellChange() {
|
726 | if (this._onSelection) {
|
727 | const previousProviderCell =
|
728 | this._currentProviderIndex !== null &&
|
729 | this._currentProviderIndex < this.widget.content.widgets.length
|
730 | ? this.widget.content.widgets[this._currentProviderIndex]
|
731 | : null;
|
732 |
|
733 | const previousProviderInCurrentSelection =
|
734 | previousProviderCell &&
|
735 | this.widget.content.isSelectedOrActive(previousProviderCell);
|
736 |
|
737 | if (!previousProviderInCurrentSelection) {
|
738 | await this._updateCellSelection();
|
739 |
|
740 | await this.clearHighlight();
|
741 |
|
742 |
|
743 |
|
744 | this._currentProviderIndex = this.widget.content.activeCellIndex;
|
745 | }
|
746 | }
|
747 |
|
748 | await this._ensureCurrentMatch();
|
749 | }
|
750 |
|
751 | |
752 |
|
753 |
|
754 |
|
755 | private async _ensureCurrentMatch() {
|
756 | if (this._currentProviderIndex !== null) {
|
757 | const searchEngine = this._searchProviders[this._currentProviderIndex];
|
758 | if (!searchEngine) {
|
759 |
|
760 | return;
|
761 | }
|
762 | const currentMatch = searchEngine.getCurrentMatch();
|
763 | if (!currentMatch && this.matchesCount) {
|
764 |
|
765 |
|
766 | await this.highlightNext(true, {
|
767 | from: 'start',
|
768 | scroll: false,
|
769 | select: false
|
770 | });
|
771 | }
|
772 | }
|
773 | }
|
774 |
|
775 | private _observeActiveCell() {
|
776 | const editor = this.widget.content.activeCell?.editor;
|
777 | if (!editor) {
|
778 | return;
|
779 | }
|
780 | this._stopObservingLastCell();
|
781 |
|
782 | editor.model.selections.changed.connect(this._setSelectedLines, this);
|
783 | this._editorSelectionsObservable = editor.model.selections;
|
784 | }
|
785 |
|
786 | private _stopObservingLastCell() {
|
787 | if (this._editorSelectionsObservable) {
|
788 | this._editorSelectionsObservable.changed.disconnect(
|
789 | this._setSelectedLines,
|
790 | this
|
791 | );
|
792 | }
|
793 | }
|
794 |
|
795 | private _setSelectedLines() {
|
796 | const editor = this.widget.content.activeCell?.editor;
|
797 | if (!editor) {
|
798 | return;
|
799 | }
|
800 |
|
801 | const selection = editor.getSelection();
|
802 | const { start, end } = selection;
|
803 |
|
804 | const newLines =
|
805 | end.line === start.line && end.column === start.column
|
806 | ? 0
|
807 | : end.line - start.line + 1;
|
808 |
|
809 | this._textSelection = selection;
|
810 |
|
811 | if (newLines !== this._selectedLines) {
|
812 | this._selectedLines = newLines;
|
813 | this._updateSelectionMode();
|
814 | }
|
815 | this._filtersChanged.emit();
|
816 | }
|
817 |
|
818 | private _textSelection: CodeEditor.IRange | null = null;
|
819 |
|
820 | |
821 |
|
822 |
|
823 | private async _setEnginesSelectionSearchMode() {
|
824 | let textMode: boolean;
|
825 |
|
826 | if (!this._onSelection) {
|
827 |
|
828 | textMode = false;
|
829 | } else {
|
830 |
|
831 |
|
832 |
|
833 | textMode = this._selectionSearchMode === 'text';
|
834 | }
|
835 |
|
836 | if (this._selectionLock) {
|
837 | return;
|
838 | }
|
839 |
|
840 |
|
841 | await Promise.all(
|
842 | this._searchProviders.map((provider, index) => {
|
843 | const isCurrent = this.widget.content.activeCellIndex === index;
|
844 | provider.setProtectSelection(isCurrent && this._onSelection);
|
845 | return provider.setSearchSelection(
|
846 | isCurrent && textMode ? this._textSelection : null
|
847 | );
|
848 | })
|
849 | );
|
850 | }
|
851 |
|
852 | private async _onCellSelectionChanged() {
|
853 | if (this._delayedActiveCellChangeHandler !== null) {
|
854 |
|
855 |
|
856 |
|
857 | clearTimeout(this._delayedActiveCellChangeHandler);
|
858 | this._delayedActiveCellChangeHandler = null;
|
859 | }
|
860 | await this._updateCellSelection();
|
861 | if (this._currentProviderIndex === null) {
|
862 |
|
863 | const firstSelectedCellIndex = this.widget.content.widgets.findIndex(
|
864 | cell => this.widget.content.isSelectedOrActive(cell)
|
865 | );
|
866 | this._currentProviderIndex = firstSelectedCellIndex;
|
867 | }
|
868 | await this._ensureCurrentMatch();
|
869 | }
|
870 |
|
871 | private async _updateCellSelection() {
|
872 | const cells = this.widget.content.widgets;
|
873 | let selectedCells = 0;
|
874 | await Promise.all(
|
875 | cells.map(async (cell, index) => {
|
876 | const provider = this._searchProviders[index];
|
877 | const isSelected = this.widget.content.isSelectedOrActive(cell);
|
878 | if (isSelected) {
|
879 | selectedCells += 1;
|
880 | }
|
881 | if (provider && this._onSelection) {
|
882 | await provider.setIsActive(isSelected);
|
883 | }
|
884 | })
|
885 | );
|
886 |
|
887 | if (selectedCells !== this._selectedCells) {
|
888 | this._selectedCells = selectedCells;
|
889 | this._updateSelectionMode();
|
890 | }
|
891 |
|
892 | this._filtersChanged.emit();
|
893 | }
|
894 |
|
895 |
|
896 | protected delayedActiveCellChangeHandlerReady: Promise<void>;
|
897 | private _currentProviderIndex: number | null = null;
|
898 | private _delayedActiveCellChangeHandler: number | null = null;
|
899 | private _filters: IFilters | undefined;
|
900 | private _onSelection = false;
|
901 | private _selectedCells: number = 1;
|
902 | private _selectedLines: number = 0;
|
903 | private _query: RegExp | null = null;
|
904 | private _searchProviders: CellSearchProvider[] = [];
|
905 | private _editorSelectionsObservable: IObservableMap<
|
906 | CodeEditor.ITextSelection[]
|
907 | > | null = null;
|
908 | private _selectionSearchMode: 'cells' | 'text' = 'cells';
|
909 | private _selectionLock: boolean = false;
|
910 | private _searchActive: boolean = false;
|
911 | }
|