@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.
@@ -9,7 +9,10 @@ import {
9
9
  ICellModel,
10
10
  MarkdownCell
11
11
  } from '@jupyterlab/cells';
12
- import { CodeMirrorEditor } from '@jupyterlab/codemirror';
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
- fromCursor = false
314
+ options?: IHighlightAdjacentMatchOptions
311
315
  ): Promise<ISearchMatch | undefined> {
312
- const match = await this._stepNext(false, loop, fromCursor);
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
- // If we are searching in selection we do not want to show the first
386
- // "current" closest to cursor as depending on which way the user
387
- // dragged the selection it would be the first or last match.
388
- const firstMatchAfterCursor = !(
389
- this._onSelection && this._selectionSearchMode === 'text'
390
- );
391
- await this.highlightNext(false, firstMatchAfterCursor);
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
- fromCursor = false
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, fromCursor)
643
- : await searchEngine.highlightNext(false, fromCursor);
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, fromCursor)
671
- : await searchEngine.highlightNext(false, fromCursor);
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._activeCellChangedFinished = new PromiseDelegate();
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
- const previouslyProviderCell =
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
- previouslyProviderCell &&
694
- this.widget.content.isSelectedOrActive(previouslyProviderCell);
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._activeCellChangedFinished) {
849
+ if (this._delayedActiveCellChangeHandler !== null) {
785
850
  // Avoid race condition due to `activeCellChanged` and `selectionChanged`
786
- // signals firing in short sequence, with handling of the former having
787
- // potential to undo selection set by the latter.
788
- await this._activeCellChangedFinished.promise;
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
- private _activeCellChangedFinished: PromiseDelegate<void> | undefined;
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._updateForDeferMode();
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 === 'defer' && !this.isDisposed) {
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._updateForDeferMode(
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 _updateForDeferMode(
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
- cell.dataset.windowedListIndex = `${cellIdx}`;
879
- this.layout.insertWidget(cellIdx, cell);
880
- await cell.ready;
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
- if (activeCell.editor?.hasFocus() === false) {
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 {