@jupyterlab/notebook 4.0.0-rc.0 → 4.0.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.
- package/lib/actions.js +5 -0
- package/lib/actions.js.map +1 -1
- package/lib/panel.d.ts +9 -0
- package/lib/panel.js +18 -0
- package/lib/panel.js.map +1 -1
- package/lib/searchprovider.d.ts +11 -3
- package/lib/searchprovider.js +94 -26
- package/lib/searchprovider.js.map +1 -1
- package/lib/widget.d.ts +2 -0
- package/lib/widget.js +36 -8
- package/lib/widget.js.map +1 -1
- package/package.json +20 -20
- package/src/actions.tsx +5 -0
- package/src/panel.ts +21 -0
- package/src/searchprovider.ts +106 -30
- package/src/widget.ts +51 -8
package/src/searchprovider.ts
CHANGED
|
@@ -9,7 +9,10 @@ import {
|
|
|
9
9
|
ICellModel,
|
|
10
10
|
MarkdownCell
|
|
11
11
|
} from '@jupyterlab/cells';
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
CodeMirrorEditor,
|
|
14
|
+
IHighlightAdjacentMatchOptions
|
|
15
|
+
} from '@jupyterlab/codemirror';
|
|
13
16
|
import { CodeEditor } from '@jupyterlab/codeeditor';
|
|
14
17
|
import { IChangedArgs } from '@jupyterlab/coreutils';
|
|
15
18
|
import {
|
|
@@ -24,7 +27,6 @@ import {
|
|
|
24
27
|
import { IObservableList, IObservableMap } from '@jupyterlab/observables';
|
|
25
28
|
import { ITranslator, nullTranslator } from '@jupyterlab/translation';
|
|
26
29
|
import { ArrayExt } from '@lumino/algorithm';
|
|
27
|
-
import { PromiseDelegate } from '@lumino/coreutils';
|
|
28
30
|
import { Widget } from '@lumino/widgets';
|
|
29
31
|
import { CellList } from './celllist';
|
|
30
32
|
import { NotebookPanel } from './panel';
|
|
@@ -46,6 +48,8 @@ export class NotebookSearchProvider extends SearchProvider<NotebookPanel> {
|
|
|
46
48
|
) {
|
|
47
49
|
super(widget);
|
|
48
50
|
|
|
51
|
+
this._handleHighlightsAfterActiveCellChange =
|
|
52
|
+
this._handleHighlightsAfterActiveCellChange.bind(this);
|
|
49
53
|
this.widget.model!.cells.changed.connect(this._onCellsChanged, this);
|
|
50
54
|
this.widget.content.activeCellChanged.connect(
|
|
51
55
|
this._onActiveCellChanged,
|
|
@@ -307,9 +311,9 @@ export class NotebookSearchProvider extends SearchProvider<NotebookPanel> {
|
|
|
307
311
|
*/
|
|
308
312
|
async highlightNext(
|
|
309
313
|
loop: boolean = true,
|
|
310
|
-
|
|
314
|
+
options?: IHighlightAdjacentMatchOptions
|
|
311
315
|
): Promise<ISearchMatch | undefined> {
|
|
312
|
-
const match = await this._stepNext(false, loop,
|
|
316
|
+
const match = await this._stepNext(false, loop, options);
|
|
313
317
|
return match ?? undefined;
|
|
314
318
|
}
|
|
315
319
|
|
|
@@ -321,9 +325,10 @@ export class NotebookSearchProvider extends SearchProvider<NotebookPanel> {
|
|
|
321
325
|
* @returns The previous match if available.
|
|
322
326
|
*/
|
|
323
327
|
async highlightPrevious(
|
|
324
|
-
loop: boolean = true
|
|
328
|
+
loop: boolean = true,
|
|
329
|
+
options?: IHighlightAdjacentMatchOptions
|
|
325
330
|
): Promise<ISearchMatch | undefined> {
|
|
326
|
-
const match = await this._stepNext(true, loop);
|
|
331
|
+
const match = await this._stepNext(true, loop, options);
|
|
327
332
|
return match ?? undefined;
|
|
328
333
|
}
|
|
329
334
|
|
|
@@ -382,13 +387,18 @@ export class NotebookSearchProvider extends SearchProvider<NotebookPanel> {
|
|
|
382
387
|
);
|
|
383
388
|
this._currentProviderIndex = currentProviderIndex;
|
|
384
389
|
|
|
385
|
-
//
|
|
386
|
-
//
|
|
387
|
-
//
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
390
|
+
// We do not want to show the first "current" closest to cursor as depending
|
|
391
|
+
// on which way the user dragged the selection it would be:
|
|
392
|
+
// - the first or last match when searching in selection
|
|
393
|
+
// - the next match when starting search using ctrl + f
|
|
394
|
+
// `scroll` and `select` are disabled because `startQuery` is also used as
|
|
395
|
+
// "restartQuery" after each text change and if those were enabled, we would
|
|
396
|
+
// steal the cursor.
|
|
397
|
+
await this.highlightNext(true, {
|
|
398
|
+
from: 'selection-start',
|
|
399
|
+
scroll: false,
|
|
400
|
+
select: false
|
|
401
|
+
});
|
|
392
402
|
|
|
393
403
|
return Promise.resolve();
|
|
394
404
|
}
|
|
@@ -570,14 +580,21 @@ export class NotebookSearchProvider extends SearchProvider<NotebookPanel> {
|
|
|
570
580
|
|
|
571
581
|
break;
|
|
572
582
|
}
|
|
583
|
+
this._stateChanged.emit();
|
|
573
584
|
}
|
|
574
585
|
|
|
575
586
|
private async _stepNext(
|
|
576
587
|
reverse = false,
|
|
577
588
|
loop = false,
|
|
578
|
-
|
|
589
|
+
options?: IHighlightAdjacentMatchOptions
|
|
579
590
|
): Promise<ISearchMatch | null> {
|
|
580
591
|
const activateNewMatch = async (match: ISearchMatch) => {
|
|
592
|
+
const shouldScroll = options?.scroll ?? true;
|
|
593
|
+
if (!shouldScroll) {
|
|
594
|
+
// do not activate the match if scrolling was disabled
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
|
|
581
598
|
this._selectionLock = true;
|
|
582
599
|
if (this.widget.content.activeCellIndex !== this._currentProviderIndex!) {
|
|
583
600
|
this.widget.content.activeCellIndex = this._currentProviderIndex!;
|
|
@@ -639,8 +656,8 @@ export class NotebookSearchProvider extends SearchProvider<NotebookPanel> {
|
|
|
639
656
|
const searchEngine = this._searchProviders[this._currentProviderIndex];
|
|
640
657
|
|
|
641
658
|
const match = reverse
|
|
642
|
-
? await searchEngine.highlightPrevious(false,
|
|
643
|
-
: await searchEngine.highlightNext(false,
|
|
659
|
+
? await searchEngine.highlightPrevious(false, options)
|
|
660
|
+
: await searchEngine.highlightNext(false, options);
|
|
644
661
|
|
|
645
662
|
if (match) {
|
|
646
663
|
await activateNewMatch(match);
|
|
@@ -667,8 +684,8 @@ export class NotebookSearchProvider extends SearchProvider<NotebookPanel> {
|
|
|
667
684
|
// try the first provider again
|
|
668
685
|
const searchEngine = this._searchProviders[startIndex];
|
|
669
686
|
const match = reverse
|
|
670
|
-
? await searchEngine.highlightPrevious(false,
|
|
671
|
-
: await searchEngine.highlightNext(false,
|
|
687
|
+
? await searchEngine.highlightPrevious(false, options)
|
|
688
|
+
: await searchEngine.highlightNext(false, options);
|
|
672
689
|
if (match) {
|
|
673
690
|
await activateNewMatch(match);
|
|
674
691
|
return match;
|
|
@@ -680,28 +697,75 @@ export class NotebookSearchProvider extends SearchProvider<NotebookPanel> {
|
|
|
680
697
|
}
|
|
681
698
|
|
|
682
699
|
private async _onActiveCellChanged() {
|
|
683
|
-
this.
|
|
700
|
+
if (this._delayedActiveCellChangeHandler !== null) {
|
|
701
|
+
// Prevent handler from running twice if active cell is changed twice
|
|
702
|
+
// within the same task of the event loop.
|
|
703
|
+
clearTimeout(this._delayedActiveCellChangeHandler);
|
|
704
|
+
this._delayedActiveCellChangeHandler = null;
|
|
705
|
+
}
|
|
684
706
|
|
|
685
707
|
if (this.widget.content.activeCellIndex !== this._currentProviderIndex) {
|
|
686
|
-
|
|
708
|
+
// At this time we cannot handle the change of active cell, because
|
|
709
|
+
// `activeCellChanged` is also emitted in the middle of cell selection
|
|
710
|
+
// change, and if selection is getting extended, we do not want to clear
|
|
711
|
+
// highlights just to re-apply them shortly after, which has side effects
|
|
712
|
+
// impacting the functionality and performance.
|
|
713
|
+
this._delayedActiveCellChangeHandler = setTimeout(() => {
|
|
714
|
+
this.delayedActiveCellChangeHandlerReady =
|
|
715
|
+
this._handleHighlightsAfterActiveCellChange();
|
|
716
|
+
}, 0);
|
|
717
|
+
}
|
|
718
|
+
this._observeActiveCell();
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
private async _handleHighlightsAfterActiveCellChange() {
|
|
722
|
+
if (this._onSelection) {
|
|
723
|
+
const previousProviderCell =
|
|
687
724
|
this._currentProviderIndex !== null &&
|
|
688
725
|
this._currentProviderIndex < this.widget.content.widgets.length
|
|
689
726
|
? this.widget.content.widgets[this._currentProviderIndex]
|
|
690
727
|
: null;
|
|
691
728
|
|
|
692
729
|
const previousProviderInCurrentSelection =
|
|
693
|
-
|
|
694
|
-
this.widget.content.isSelectedOrActive(
|
|
730
|
+
previousProviderCell &&
|
|
731
|
+
this.widget.content.isSelectedOrActive(previousProviderCell);
|
|
695
732
|
|
|
696
733
|
if (!previousProviderInCurrentSelection) {
|
|
697
734
|
await this._updateCellSelection();
|
|
698
735
|
// Clear highlight from previous provider
|
|
699
736
|
await this.clearHighlight();
|
|
737
|
+
// If we are searching in all cells, we should not change the active
|
|
738
|
+
// provider when switching active cell to preserve current match;
|
|
739
|
+
// if we are searching within selected cells we should update
|
|
740
|
+
this._currentProviderIndex = this.widget.content.activeCellIndex;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
await this._ensureCurrentMatch();
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* If there are results but no match is designated as current,
|
|
749
|
+
* mark a result as current and highlight it.
|
|
750
|
+
*/
|
|
751
|
+
private async _ensureCurrentMatch() {
|
|
752
|
+
if (this._currentProviderIndex !== null) {
|
|
753
|
+
const searchEngine = this._searchProviders[this._currentProviderIndex];
|
|
754
|
+
if (!searchEngine) {
|
|
755
|
+
// This can happen when `startQuery()` has not finished yet.
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
const currentMatch = searchEngine.getCurrentMatch();
|
|
759
|
+
if (!currentMatch && this.matchesCount) {
|
|
760
|
+
// Select a match as current by highlighting next (with looping) from
|
|
761
|
+
// the selection start, to prevent "current" match from jumping around.
|
|
762
|
+
await this.highlightNext(true, {
|
|
763
|
+
from: 'start',
|
|
764
|
+
scroll: false,
|
|
765
|
+
select: false
|
|
766
|
+
});
|
|
700
767
|
}
|
|
701
|
-
this._currentProviderIndex = this.widget.content.activeCellIndex;
|
|
702
768
|
}
|
|
703
|
-
this._observeActiveCell();
|
|
704
|
-
this._activeCellChangedFinished.resolve();
|
|
705
769
|
}
|
|
706
770
|
|
|
707
771
|
private _observeActiveCell() {
|
|
@@ -773,6 +837,7 @@ export class NotebookSearchProvider extends SearchProvider<NotebookPanel> {
|
|
|
773
837
|
await Promise.all(
|
|
774
838
|
this._searchProviders.map((provider, index) => {
|
|
775
839
|
const isCurrent = this.widget.content.activeCellIndex === index;
|
|
840
|
+
provider.setProtectSelection(isCurrent && this._onSelection);
|
|
776
841
|
return provider.setSearchSelection(
|
|
777
842
|
isCurrent && textMode ? this._textSelection : null
|
|
778
843
|
);
|
|
@@ -781,13 +846,22 @@ export class NotebookSearchProvider extends SearchProvider<NotebookPanel> {
|
|
|
781
846
|
}
|
|
782
847
|
|
|
783
848
|
private async _onCellSelectionChanged() {
|
|
784
|
-
if (this.
|
|
849
|
+
if (this._delayedActiveCellChangeHandler !== null) {
|
|
785
850
|
// Avoid race condition due to `activeCellChanged` and `selectionChanged`
|
|
786
|
-
// signals firing in short sequence
|
|
787
|
-
// potential to undo selection set by the latter.
|
|
788
|
-
|
|
851
|
+
// signals firing in short sequence when selection gets extended, with
|
|
852
|
+
// handling of the former having potential to undo selection set by the latter.
|
|
853
|
+
clearTimeout(this._delayedActiveCellChangeHandler);
|
|
854
|
+
this._delayedActiveCellChangeHandler = null;
|
|
789
855
|
}
|
|
790
856
|
await this._updateCellSelection();
|
|
857
|
+
if (this._currentProviderIndex === null) {
|
|
858
|
+
// For consistency we set the first cell in selection as current provider.
|
|
859
|
+
const firstSelectedCellIndex = this.widget.content.widgets.findIndex(
|
|
860
|
+
cell => this.widget.content.isSelectedOrActive(cell)
|
|
861
|
+
);
|
|
862
|
+
this._currentProviderIndex = firstSelectedCellIndex;
|
|
863
|
+
}
|
|
864
|
+
await this._ensureCurrentMatch();
|
|
791
865
|
}
|
|
792
866
|
|
|
793
867
|
private async _updateCellSelection() {
|
|
@@ -814,8 +888,10 @@ export class NotebookSearchProvider extends SearchProvider<NotebookPanel> {
|
|
|
814
888
|
this._filtersChanged.emit();
|
|
815
889
|
}
|
|
816
890
|
|
|
817
|
-
|
|
891
|
+
// used for testing only
|
|
892
|
+
protected delayedActiveCellChangeHandlerReady: Promise<void>;
|
|
818
893
|
private _currentProviderIndex: number | null = null;
|
|
894
|
+
private _delayedActiveCellChangeHandler: number | null = null;
|
|
819
895
|
private _filters: IFilters | undefined;
|
|
820
896
|
private _onSelection = false;
|
|
821
897
|
private _selectedCells: number = 1;
|
package/src/widget.ts
CHANGED
|
@@ -518,7 +518,7 @@ export class StaticNotebook extends WindowedList {
|
|
|
518
518
|
*/
|
|
519
519
|
protected onUpdateRequest(msg: Message): void {
|
|
520
520
|
if (this.notebookConfig.windowingMode === 'defer') {
|
|
521
|
-
void this.
|
|
521
|
+
void this._runOnIdleTime();
|
|
522
522
|
} else {
|
|
523
523
|
super.onUpdateRequest(msg);
|
|
524
524
|
}
|
|
@@ -797,7 +797,7 @@ export class StaticNotebook extends WindowedList {
|
|
|
797
797
|
}
|
|
798
798
|
|
|
799
799
|
private _scheduleCellRenderOnIdle() {
|
|
800
|
-
if (this.notebookConfig.windowingMode
|
|
800
|
+
if (this.notebookConfig.windowingMode !== 'none' && !this.isDisposed) {
|
|
801
801
|
if (!this._idleCallBack) {
|
|
802
802
|
this._idleCallBack = requestIdleCallback(
|
|
803
803
|
(deadline: IdleDeadline) => {
|
|
@@ -805,7 +805,7 @@ export class StaticNotebook extends WindowedList {
|
|
|
805
805
|
|
|
806
806
|
// In case of timeout, render for some time even if it means freezing the UI
|
|
807
807
|
// This avoids the cells to never be loaded.
|
|
808
|
-
void this.
|
|
808
|
+
void this._runOnIdleTime(
|
|
809
809
|
deadline.didTimeout
|
|
810
810
|
? MAXIMUM_TIME_REMAINING
|
|
811
811
|
: deadline.timeRemaining()
|
|
@@ -864,7 +864,7 @@ export class StaticNotebook extends WindowedList {
|
|
|
864
864
|
}
|
|
865
865
|
}
|
|
866
866
|
|
|
867
|
-
private async
|
|
867
|
+
private async _runOnIdleTime(
|
|
868
868
|
remainingTime: number = MAXIMUM_TIME_REMAINING
|
|
869
869
|
): Promise<void> {
|
|
870
870
|
const startTime = Date.now();
|
|
@@ -875,9 +875,14 @@ export class StaticNotebook extends WindowedList {
|
|
|
875
875
|
) {
|
|
876
876
|
const cell = this.cellsArray[cellIdx];
|
|
877
877
|
if (cell.isPlaceholder()) {
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
878
|
+
switch (this.notebookConfig.windowingMode) {
|
|
879
|
+
case 'defer':
|
|
880
|
+
await this._updateForDeferMode(cell, cellIdx);
|
|
881
|
+
break;
|
|
882
|
+
case 'full':
|
|
883
|
+
this._renderCSSAndJSOutputs(cell, cellIdx);
|
|
884
|
+
break;
|
|
885
|
+
}
|
|
881
886
|
}
|
|
882
887
|
cellIdx++;
|
|
883
888
|
}
|
|
@@ -892,6 +897,43 @@ export class StaticNotebook extends WindowedList {
|
|
|
892
897
|
}
|
|
893
898
|
}
|
|
894
899
|
|
|
900
|
+
private async _updateForDeferMode(
|
|
901
|
+
cell: Cell<ICellModel>,
|
|
902
|
+
cellIdx: number
|
|
903
|
+
): Promise<void> {
|
|
904
|
+
cell.dataset.windowedListIndex = `${cellIdx}`;
|
|
905
|
+
this.layout.insertWidget(cellIdx, cell);
|
|
906
|
+
await cell.ready;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
private _renderCSSAndJSOutputs(
|
|
910
|
+
cell: Cell<ICellModel>,
|
|
911
|
+
cellIdx: number
|
|
912
|
+
): void {
|
|
913
|
+
// Only render cell with text/html outputs containing scripts or/and styles
|
|
914
|
+
// Note:
|
|
915
|
+
// We don't need to render JavaScript mimetype outputs because they get
|
|
916
|
+
// directly evaluate without adding DOM elements (see @jupyterlab/javascript-extension)
|
|
917
|
+
if (cell instanceof CodeCell) {
|
|
918
|
+
for (
|
|
919
|
+
let outputIdx = 0;
|
|
920
|
+
outputIdx < (cell.model.outputs?.length ?? 0);
|
|
921
|
+
outputIdx++
|
|
922
|
+
) {
|
|
923
|
+
const output = cell.model.outputs.get(outputIdx);
|
|
924
|
+
const html = (output.data['text/html'] as string) ?? '';
|
|
925
|
+
if (
|
|
926
|
+
html.match(
|
|
927
|
+
/(<style[^>]*>[^<]*<\/style[^>]*>|<script[^>]*>.*?<\/script[^>]*>)/gims
|
|
928
|
+
)
|
|
929
|
+
) {
|
|
930
|
+
this.renderCellOutputs(cellIdx);
|
|
931
|
+
break;
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
|
|
895
937
|
/**
|
|
896
938
|
* Apply updated notebook settings.
|
|
897
939
|
*/
|
|
@@ -2034,7 +2076,8 @@ export class Notebook extends StaticNotebook {
|
|
|
2034
2076
|
private _ensureFocus(force = false): void {
|
|
2035
2077
|
const activeCell = this.activeCell;
|
|
2036
2078
|
if (this.mode === 'edit' && activeCell) {
|
|
2037
|
-
|
|
2079
|
+
// Test for !== true to cover hasFocus is false and editor is not yet rendered.
|
|
2080
|
+
if (activeCell.editor?.hasFocus() !== true) {
|
|
2038
2081
|
if (activeCell.inViewport) {
|
|
2039
2082
|
activeCell.editor?.focus();
|
|
2040
2083
|
} else {
|