@jupyterlab/notebook 4.7.0-alpha.1 → 4.7.0-alpha.2
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 +37 -5
- package/lib/actions.js.map +1 -1
- package/lib/executionindicator.d.ts +9 -0
- package/lib/executionindicator.js +36 -7
- package/lib/executionindicator.js.map +1 -1
- package/lib/notebooklspadapter.js +34 -9
- package/lib/notebooklspadapter.js.map +1 -1
- package/lib/panel.js +5 -2
- package/lib/panel.js.map +1 -1
- package/lib/searchprovider.d.ts +1 -0
- package/lib/searchprovider.js +19 -12
- package/lib/searchprovider.js.map +1 -1
- package/lib/testutils.js +9 -2
- package/lib/testutils.js.map +1 -1
- package/lib/widget.d.ts +34 -2
- package/lib/widget.js +148 -18
- package/lib/widget.js.map +1 -1
- package/package.json +21 -21
- package/src/actions.tsx +45 -5
- package/src/executionindicator.tsx +42 -9
- package/src/notebooklspadapter.ts +34 -9
- package/src/panel.ts +8 -2
- package/src/searchprovider.ts +21 -13
- package/src/testutils.ts +9 -1
- package/src/widget.ts +160 -20
- package/style/base.css +36 -0
|
@@ -154,7 +154,9 @@ export function ExecutionIndicatorComponent(
|
|
|
154
154
|
return reactElement('busy', progressBar(percentage), [
|
|
155
155
|
<span key={0}>
|
|
156
156
|
{trans.__(
|
|
157
|
-
|
|
157
|
+
'Executed %1/%2 cells',
|
|
158
|
+
executedCellNumber,
|
|
159
|
+
scheduledCellNumber
|
|
158
160
|
)}
|
|
159
161
|
</span>,
|
|
160
162
|
<span key={1}>
|
|
@@ -230,6 +232,22 @@ export class ExecutionIndicator extends VDomRenderer<ExecutionIndicator.Model> {
|
|
|
230
232
|
this.addClass('jp-mod-highlighted');
|
|
231
233
|
}
|
|
232
234
|
|
|
235
|
+
/**
|
|
236
|
+
* Dispose the widget and its model.
|
|
237
|
+
*
|
|
238
|
+
* `VDomRenderer.dispose` drops its reference to the model without
|
|
239
|
+
* disposing it; undisposed, the model stays connected to the session
|
|
240
|
+
* context (which outlives this widget) and its current notebook stays
|
|
241
|
+
* reachable. The model is owned here: it is created in the constructor.
|
|
242
|
+
*/
|
|
243
|
+
dispose(): void {
|
|
244
|
+
if (this.isDisposed) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
this.model?.dispose();
|
|
248
|
+
super.dispose();
|
|
249
|
+
}
|
|
250
|
+
|
|
233
251
|
/**
|
|
234
252
|
* Render the execution status item.
|
|
235
253
|
*/
|
|
@@ -378,13 +396,17 @@ export namespace ExecutionIndicator {
|
|
|
378
396
|
this
|
|
379
397
|
);
|
|
380
398
|
|
|
399
|
+
// The cleanup handlers are registered with this model as receiver:
|
|
400
|
+
// without one they would sit on the context until it is disposed,
|
|
401
|
+
// keeping the notebook captured by this scope reachable even after
|
|
402
|
+
// the model itself is disposed and cleared.
|
|
381
403
|
context.disposed.connect(ctx => {
|
|
382
404
|
ctx.connectionStatusChanged.disconnect(
|
|
383
405
|
contextConnectionStatusChanged,
|
|
384
406
|
this
|
|
385
407
|
);
|
|
386
408
|
ctx.statusChanged.disconnect(contextStatusChanged, this);
|
|
387
|
-
});
|
|
409
|
+
}, this);
|
|
388
410
|
const handleKernelMsg = (
|
|
389
411
|
sender: Kernel.IKernelConnection,
|
|
390
412
|
msg: Kernel.IAnyMessageArgs
|
|
@@ -413,9 +435,10 @@ export namespace ExecutionIndicator {
|
|
|
413
435
|
this._startTimer(nb);
|
|
414
436
|
}
|
|
415
437
|
};
|
|
416
|
-
context.session?.kernel?.anyMessage.connect(handleKernelMsg);
|
|
417
|
-
context.session?.kernel?.disposed.connect(
|
|
418
|
-
kernel.anyMessage.disconnect(handleKernelMsg)
|
|
438
|
+
context.session?.kernel?.anyMessage.connect(handleKernelMsg, this);
|
|
439
|
+
context.session?.kernel?.disposed.connect(
|
|
440
|
+
kernel => kernel.anyMessage.disconnect(handleKernelMsg, this),
|
|
441
|
+
this
|
|
419
442
|
);
|
|
420
443
|
const kernelChangedSlot = (
|
|
421
444
|
_: ISessionContext,
|
|
@@ -429,13 +452,14 @@ export namespace ExecutionIndicator {
|
|
|
429
452
|
this._resetTime(state);
|
|
430
453
|
this.stateChanged.emit(void 0);
|
|
431
454
|
if (kernelData.newValue) {
|
|
432
|
-
kernelData.newValue.anyMessage.connect(handleKernelMsg);
|
|
455
|
+
kernelData.newValue.anyMessage.connect(handleKernelMsg, this);
|
|
433
456
|
}
|
|
434
457
|
}
|
|
435
458
|
};
|
|
436
|
-
context.kernelChanged.connect(kernelChangedSlot);
|
|
437
|
-
context.disposed.connect(
|
|
438
|
-
ctx.kernelChanged.disconnect(kernelChangedSlot)
|
|
459
|
+
context.kernelChanged.connect(kernelChangedSlot, this);
|
|
460
|
+
context.disposed.connect(
|
|
461
|
+
ctx => ctx.kernelChanged.disconnect(kernelChangedSlot, this),
|
|
462
|
+
this
|
|
439
463
|
);
|
|
440
464
|
}
|
|
441
465
|
}
|
|
@@ -583,6 +607,15 @@ export namespace ExecutionIndicator {
|
|
|
583
607
|
* @param data - the state to be updated.
|
|
584
608
|
*/
|
|
585
609
|
private _tick(data: IExecutionState): void {
|
|
610
|
+
if (this.isDisposed) {
|
|
611
|
+
// The slots that would clear this interval are disconnected when the
|
|
612
|
+
// model is disposed, so the interval has to terminate itself
|
|
613
|
+
// (`_notebookExecutionProgress` is a `WeakMap`, so `dispose` cannot
|
|
614
|
+
// enumerate the states to clear them there).
|
|
615
|
+
clearInterval(data.interval);
|
|
616
|
+
clearTimeout(data.timeout);
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
586
619
|
data.totalTime += 1;
|
|
587
620
|
this.stateChanged.emit(void 0);
|
|
588
621
|
}
|
|
@@ -32,15 +32,28 @@ export class NotebookAdapter extends WidgetLSPAdapter<NotebookPanel> {
|
|
|
32
32
|
this.editor = editorWidget.content;
|
|
33
33
|
this._cellToEditor = new WeakMap();
|
|
34
34
|
this.isReady = this.isReady.bind(this);
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
const sessionContextReady = this.widget.context.sessionContext.ready;
|
|
36
|
+
const connectionManagerReady = this.connectionManager.ready;
|
|
37
|
+
const adapter: { current: NotebookAdapter | null } = { current: this };
|
|
38
|
+
this.disposed.connect(() => {
|
|
39
|
+
adapter.current = null;
|
|
40
|
+
});
|
|
41
|
+
void Promise.all([sessionContextReady, connectionManagerReady])
|
|
39
42
|
.then(async () => {
|
|
40
|
-
|
|
41
|
-
|
|
43
|
+
const currentAdapter = adapter.current;
|
|
44
|
+
if (!currentAdapter) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
await currentAdapter.initOnceReady();
|
|
48
|
+
currentAdapter._readyDelegate.resolve();
|
|
42
49
|
})
|
|
43
|
-
.catch(
|
|
50
|
+
.catch(reason => {
|
|
51
|
+
const currentAdapter = adapter.current;
|
|
52
|
+
if (!currentAdapter || currentAdapter.isDisposed) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
console.error(reason);
|
|
56
|
+
});
|
|
44
57
|
}
|
|
45
58
|
|
|
46
59
|
/**
|
|
@@ -185,8 +198,14 @@ export class NotebookAdapter extends WidgetLSPAdapter<NotebookPanel> {
|
|
|
185
198
|
try {
|
|
186
199
|
// note: we need to wait until ready before updating language info
|
|
187
200
|
const oldLanguageInfo = this._languageInfo;
|
|
188
|
-
await untilReady(this.isReady, -1);
|
|
201
|
+
await untilReady(() => this.isDisposed || this.isReady(), -1);
|
|
202
|
+
if (this.isDisposed) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
189
205
|
await this._updateLanguageInfo();
|
|
206
|
+
if (this.isDisposed) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
190
209
|
const newLanguageInfo = this._languageInfo;
|
|
191
210
|
if (
|
|
192
211
|
oldLanguageInfo?.name != newLanguageInfo.name ||
|
|
@@ -350,8 +369,14 @@ export class NotebookAdapter extends WidgetLSPAdapter<NotebookPanel> {
|
|
|
350
369
|
* connect various signals.
|
|
351
370
|
*/
|
|
352
371
|
protected async initOnceReady(): Promise<void> {
|
|
353
|
-
await untilReady(this.isReady
|
|
372
|
+
await untilReady(() => this.isDisposed || this.isReady(), -1);
|
|
373
|
+
if (this.isDisposed) {
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
354
376
|
await this._updateLanguageInfo();
|
|
377
|
+
if (this.isDisposed) {
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
355
380
|
this.initVirtual();
|
|
356
381
|
|
|
357
382
|
// connect the document, but do not open it as the adapter will handle this
|
package/src/panel.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { isMarkdownCellModel } from '@jupyterlab/cells';
|
|
|
6
6
|
import { PageConfig } from '@jupyterlab/coreutils';
|
|
7
7
|
import type { DocumentRegistry } from '@jupyterlab/docregistry';
|
|
8
8
|
import { DocumentWidget } from '@jupyterlab/docregistry';
|
|
9
|
+
import type * as nbformat from '@jupyterlab/nbformat';
|
|
9
10
|
import type { Kernel, KernelMessage, Session } from '@jupyterlab/services';
|
|
10
11
|
import type { ITranslator } from '@jupyterlab/translation';
|
|
11
12
|
import { Token } from '@lumino/coreutils';
|
|
@@ -250,10 +251,15 @@ export class NotebookPanel extends DocumentWidget<Notebook, INotebookModel> {
|
|
|
250
251
|
if (this.isDisposed) {
|
|
251
252
|
return;
|
|
252
253
|
}
|
|
254
|
+
const oldSpec = this.model!.getMetadata(
|
|
255
|
+
'kernelspec'
|
|
256
|
+
) as nbformat.IKernelspecMetadata;
|
|
257
|
+
const preserved = oldSpec?.name === kernel.name ? oldSpec : undefined;
|
|
253
258
|
this.model!.setMetadata('kernelspec', {
|
|
254
259
|
name: kernel.name,
|
|
255
|
-
display_name:
|
|
256
|
-
|
|
260
|
+
display_name:
|
|
261
|
+
spec?.display_name ?? preserved?.display_name ?? kernel.name,
|
|
262
|
+
language: spec?.language ?? preserved?.language
|
|
257
263
|
});
|
|
258
264
|
}
|
|
259
265
|
|
package/src/searchprovider.ts
CHANGED
|
@@ -24,6 +24,7 @@ import type { IObservableList, IObservableMap } from '@jupyterlab/observables';
|
|
|
24
24
|
import type { ITranslator } from '@jupyterlab/translation';
|
|
25
25
|
import { nullTranslator } from '@jupyterlab/translation';
|
|
26
26
|
import { ArrayExt } from '@lumino/algorithm';
|
|
27
|
+
import { Debouncer } from '@lumino/polling';
|
|
27
28
|
import type { Widget } from '@lumino/widgets';
|
|
28
29
|
import type { CellList } from './celllist';
|
|
29
30
|
import { NotebookPanel } from './panel';
|
|
@@ -66,18 +67,9 @@ export class NotebookSearchProvider extends SearchProvider<NotebookPanel> {
|
|
|
66
67
|
|
|
67
68
|
private _onNotebookStateChanged(_: Notebook, args: IChangedArgs<unknown>) {
|
|
68
69
|
if (args.name === 'mode') {
|
|
69
|
-
//
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
args.newValue === 'command' &&
|
|
73
|
-
document.activeElement?.closest('.jp-DocumentSearch-overlay')
|
|
74
|
-
) {
|
|
75
|
-
// Do not request updating mode when user switched focus to search overlay.
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
this._updateSelectionMode();
|
|
79
|
-
this._filtersChanged.emit();
|
|
80
|
-
}, 0);
|
|
70
|
+
// Debounce to ensure that `document.activeElement` settled, and to keep
|
|
71
|
+
// only the most recent mode transition.
|
|
72
|
+
void this._modeChangeDebouncer.invoke();
|
|
81
73
|
}
|
|
82
74
|
}
|
|
83
75
|
|
|
@@ -206,6 +198,8 @@ export class NotebookSearchProvider extends SearchProvider<NotebookPanel> {
|
|
|
206
198
|
);
|
|
207
199
|
this._stopObservingLastCell();
|
|
208
200
|
|
|
201
|
+
this._modeChangeDebouncer.dispose();
|
|
202
|
+
|
|
209
203
|
super.dispose();
|
|
210
204
|
|
|
211
205
|
const index = this.widget.content.activeCellIndex;
|
|
@@ -794,7 +788,8 @@ export class NotebookSearchProvider extends SearchProvider<NotebookPanel> {
|
|
|
794
788
|
return;
|
|
795
789
|
}
|
|
796
790
|
const currentMatch = searchEngine.getCurrentMatch();
|
|
797
|
-
|
|
791
|
+
const matchesCount = this.matchesCount;
|
|
792
|
+
if (!currentMatch && matchesCount !== null && matchesCount > 0) {
|
|
798
793
|
// Select a match as current by highlighting next (with looping) from
|
|
799
794
|
// the selection start, to prevent "current" match from jumping around.
|
|
800
795
|
await this.highlightNext(true, {
|
|
@@ -930,6 +925,19 @@ export class NotebookSearchProvider extends SearchProvider<NotebookPanel> {
|
|
|
930
925
|
protected delayedActiveCellChangeHandlerReady: Promise<void>;
|
|
931
926
|
private _currentProviderIndex: number | null = null;
|
|
932
927
|
private _delayedActiveCellChangeHandler: number | null = null;
|
|
928
|
+
private _modeChangeDebouncer = new Debouncer(() => {
|
|
929
|
+
// The mode can change again before this handler runs; check the
|
|
930
|
+
// current mode because this is what `_updateSelectionMode()` acts on.
|
|
931
|
+
if (
|
|
932
|
+
this.widget.content.mode === 'command' &&
|
|
933
|
+
document.activeElement?.closest('.jp-DocumentSearch-overlay')
|
|
934
|
+
) {
|
|
935
|
+
// Do not request updating mode when user switched focus to search overlay.
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
this._updateSelectionMode();
|
|
939
|
+
this._filtersChanged.emit();
|
|
940
|
+
}, 0);
|
|
933
941
|
private _filters: IFilters | undefined;
|
|
934
942
|
private _onSelection = false;
|
|
935
943
|
private _selectedCells: number = 1;
|
package/src/testutils.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import type { ISessionContext } from '@jupyterlab/apputils';
|
|
6
6
|
import {
|
|
7
7
|
Clipboard,
|
|
8
|
+
Sanitizer,
|
|
8
9
|
SessionContextDialogs,
|
|
9
10
|
SystemClipboard
|
|
10
11
|
} from '@jupyterlab/apputils';
|
|
@@ -202,10 +203,17 @@ export namespace NBTestUtils {
|
|
|
202
203
|
kernelHistory: new NotebookHistory({ sessionContext: sessionContext })
|
|
203
204
|
}
|
|
204
205
|
: {};
|
|
206
|
+
// Render textual outputs synchronously in tests: the incremental
|
|
207
|
+
// (animation-frame based) pipeline bails out immediately for hosts that
|
|
208
|
+
// are not attached to the document, which is the norm in unit tests that
|
|
209
|
+
// never call `Widget.attach`.
|
|
210
|
+
const sanitizer = new Sanitizer();
|
|
211
|
+
sanitizer.setIncrementalAutolink(false);
|
|
205
212
|
return new Notebook({
|
|
206
213
|
rendermime: new RenderMimeRegistry({
|
|
207
214
|
markdownParser: parser,
|
|
208
|
-
initialFactories: standardRendererFactories
|
|
215
|
+
initialFactories: standardRendererFactories,
|
|
216
|
+
sanitizer
|
|
209
217
|
}),
|
|
210
218
|
contentFactory: createNotebookFactory(),
|
|
211
219
|
mimeTypeService,
|
package/src/widget.ts
CHANGED
|
@@ -386,6 +386,12 @@ export class StaticNotebook extends WindowedList<NotebookViewModel> {
|
|
|
386
386
|
this._contentVisibilityObserver = null;
|
|
387
387
|
}
|
|
388
388
|
super.dispose();
|
|
389
|
+
// Dispose cells that windowing modes may have detached from the layout.
|
|
390
|
+
for (const cell of this.cellsArray) {
|
|
391
|
+
cell.dispose();
|
|
392
|
+
}
|
|
393
|
+
this.cellsArray.length = 0;
|
|
394
|
+
this.viewModel.dispose();
|
|
389
395
|
}
|
|
390
396
|
|
|
391
397
|
protected onBeforeDetach(msg: Message): void {
|
|
@@ -620,7 +626,7 @@ export class StaticNotebook extends WindowedList<NotebookViewModel> {
|
|
|
620
626
|
for (const cell of cells) {
|
|
621
627
|
this._insertCell(++index, cell);
|
|
622
628
|
}
|
|
623
|
-
this.
|
|
629
|
+
this._syncCellTrust();
|
|
624
630
|
newValue.cells.changed.connect(this._onCellsChanged, this);
|
|
625
631
|
newValue.metadataChanged.connect(this.onMetadataChanged, this);
|
|
626
632
|
newValue.contentChanged.connect(this.onModelContentChanged, this);
|
|
@@ -682,7 +688,7 @@ export class StaticNotebook extends WindowedList<NotebookViewModel> {
|
|
|
682
688
|
this.addHeader();
|
|
683
689
|
}
|
|
684
690
|
|
|
685
|
-
this.
|
|
691
|
+
this._syncCellTrust();
|
|
686
692
|
this.update();
|
|
687
693
|
}
|
|
688
694
|
|
|
@@ -710,7 +716,7 @@ export class StaticNotebook extends WindowedList<NotebookViewModel> {
|
|
|
710
716
|
widget.addClass(NB_CELL_CLASS);
|
|
711
717
|
|
|
712
718
|
ArrayExt.insert(this.cellsArray, index, widget);
|
|
713
|
-
this.
|
|
719
|
+
this._syncCellTrust(widget);
|
|
714
720
|
this.onCellInserted(index, widget);
|
|
715
721
|
|
|
716
722
|
this._scheduleCellRenderOnIdle();
|
|
@@ -821,8 +827,8 @@ export class StaticNotebook extends WindowedList<NotebookViewModel> {
|
|
|
821
827
|
widget.dispose();
|
|
822
828
|
}
|
|
823
829
|
|
|
824
|
-
private
|
|
825
|
-
// Note: this returns false in a notebook without
|
|
830
|
+
private _shouldTrustCell(): boolean {
|
|
831
|
+
// Note: this returns false in a notebook without trusted code cells;
|
|
826
832
|
// This is intended since only Code cells carry trust status on disk.
|
|
827
833
|
if (!this._notebookModel) {
|
|
828
834
|
return false;
|
|
@@ -839,13 +845,19 @@ export class StaticNotebook extends WindowedList<NotebookViewModel> {
|
|
|
839
845
|
return hasCodeCell;
|
|
840
846
|
}
|
|
841
847
|
|
|
842
|
-
private
|
|
843
|
-
const trusted = this.
|
|
848
|
+
private _syncCellTrust(cell?: Cell): void {
|
|
849
|
+
const trusted = this._shouldTrustCell();
|
|
844
850
|
const trustHandler = this.rendermime.trustHandler;
|
|
845
851
|
if (!trustHandler) {
|
|
846
852
|
return;
|
|
847
853
|
}
|
|
848
854
|
|
|
855
|
+
if (trusted) {
|
|
856
|
+
trustHandler.markTrusted(this.node);
|
|
857
|
+
} else {
|
|
858
|
+
trustHandler.unmarkTrusted(this.node);
|
|
859
|
+
}
|
|
860
|
+
|
|
849
861
|
const cells = cell ? [cell] : this.widgets;
|
|
850
862
|
for (const widget of cells) {
|
|
851
863
|
if (!(widget instanceof MarkdownCell)) {
|
|
@@ -864,7 +876,7 @@ export class StaticNotebook extends WindowedList<NotebookViewModel> {
|
|
|
864
876
|
args: IChangedArgs<any>
|
|
865
877
|
): void {
|
|
866
878
|
if (args.name === 'trusted') {
|
|
867
|
-
this.
|
|
879
|
+
this._syncCellTrust();
|
|
868
880
|
}
|
|
869
881
|
}
|
|
870
882
|
|
|
@@ -970,7 +982,7 @@ export class StaticNotebook extends WindowedList<NotebookViewModel> {
|
|
|
970
982
|
|
|
971
983
|
private _scheduleCellRenderOnIdle() {
|
|
972
984
|
if (this.notebookConfig.windowingMode !== 'none' && !this.isDisposed) {
|
|
973
|
-
if (
|
|
985
|
+
if (this._idleCallBack === null) {
|
|
974
986
|
this._idleCallBack = requestIdleCallback(
|
|
975
987
|
(deadline: IdleDeadline) => {
|
|
976
988
|
this._idleCallBack = null;
|
|
@@ -1073,7 +1085,7 @@ export class StaticNotebook extends WindowedList<NotebookViewModel> {
|
|
|
1073
1085
|
this._scheduleCellRenderOnIdle();
|
|
1074
1086
|
}
|
|
1075
1087
|
} else {
|
|
1076
|
-
if (this._idleCallBack) {
|
|
1088
|
+
if (this._idleCallBack !== null) {
|
|
1077
1089
|
window.cancelIdleCallback(this._idleCallBack);
|
|
1078
1090
|
this._idleCallBack = null;
|
|
1079
1091
|
}
|
|
@@ -1206,7 +1218,7 @@ export class StaticNotebook extends WindowedList<NotebookViewModel> {
|
|
|
1206
1218
|
this._contentVisibilityObserver!.observe(cell.node);
|
|
1207
1219
|
});
|
|
1208
1220
|
});
|
|
1209
|
-
});
|
|
1221
|
+
}, this);
|
|
1210
1222
|
}
|
|
1211
1223
|
|
|
1212
1224
|
protected cellsArray: Array<Cell>;
|
|
@@ -1678,7 +1690,7 @@ class ScrollbarItem implements WindowedList.IRenderer.IScrollbarItem {
|
|
|
1678
1690
|
state = 'error';
|
|
1679
1691
|
} else if (model.executionState == 'running') {
|
|
1680
1692
|
content = '[*]';
|
|
1681
|
-
} else if (model.executionCount) {
|
|
1693
|
+
} else if (model.executionCount !== null) {
|
|
1682
1694
|
content = `[${model.executionCount}]`;
|
|
1683
1695
|
} else {
|
|
1684
1696
|
content = '[ ]';
|
|
@@ -2922,16 +2934,84 @@ export class Notebook extends StaticNotebook {
|
|
|
2922
2934
|
}
|
|
2923
2935
|
}
|
|
2924
2936
|
|
|
2937
|
+
/**
|
|
2938
|
+
* Find the cell containing a node, traversing open shadow roots.
|
|
2939
|
+
*
|
|
2940
|
+
* Returned cell is `null` if the node is not in a cell of this notebook.
|
|
2941
|
+
*/
|
|
2942
|
+
private _cellContaining(node: Node | null): Cell | null {
|
|
2943
|
+
let current = node;
|
|
2944
|
+
while (current) {
|
|
2945
|
+
const element =
|
|
2946
|
+
current instanceof Element ? current : current.parentElement;
|
|
2947
|
+
if (element) {
|
|
2948
|
+
const index = this._findCell(element);
|
|
2949
|
+
if (index !== -1) {
|
|
2950
|
+
return this.widgets[index];
|
|
2951
|
+
}
|
|
2952
|
+
}
|
|
2953
|
+
const root = (element ?? current).getRootNode();
|
|
2954
|
+
current = root instanceof ShadowRoot ? root.host : null;
|
|
2955
|
+
}
|
|
2956
|
+
return null;
|
|
2957
|
+
}
|
|
2958
|
+
|
|
2959
|
+
/**
|
|
2960
|
+
* Whether a shift-click on `target` should be left to the browser to handle
|
|
2961
|
+
* as an extension of the current text selection, rather than be turned into
|
|
2962
|
+
* a cell range selection.
|
|
2963
|
+
*
|
|
2964
|
+
* This is the case when the click lands in a region of a cell whose text the
|
|
2965
|
+
* browser selects - the output area of a code cell, or the rendered input of
|
|
2966
|
+
* a rendered markdown cell - and the current selection starts in such a
|
|
2967
|
+
* region too; see https://github.com/jupyterlab/jupyterlab/issues/4800. A
|
|
2968
|
+
* click on a cell prompt, on cell chrome, or on a cell without such a region
|
|
2969
|
+
* (an unrendered markdown cell, a code cell without outputs) always selects
|
|
2970
|
+
* cells.
|
|
2971
|
+
*
|
|
2972
|
+
* #### Notes
|
|
2973
|
+
* Only the anchor of the selection is considered, because a shift-click
|
|
2974
|
+
* moves the focus and leaves the anchor in place. The anchor may sit in a
|
|
2975
|
+
* different cell than the one clicked, which is what allows a selection to
|
|
2976
|
+
* be extended across the outputs of several cells; and disregarding the
|
|
2977
|
+
* focus keeps this working when a drag overshot the edge of an output and
|
|
2978
|
+
* left the focus outside of it.
|
|
2979
|
+
*
|
|
2980
|
+
* This trade-off between extending the text selection and extending the cell
|
|
2981
|
+
* selection can be adjusted in the future. For now the text selection wins to
|
|
2982
|
+
* preserve the behaviour from before refactor that users may be accustomed to.
|
|
2983
|
+
*/
|
|
2984
|
+
private _isExtendingTextSelection(
|
|
2985
|
+
selection: Selection | null,
|
|
2986
|
+
targetCell: Cell,
|
|
2987
|
+
target: Node
|
|
2988
|
+
): boolean {
|
|
2989
|
+
if (!selection || selection.isCollapsed) {
|
|
2990
|
+
return false;
|
|
2991
|
+
}
|
|
2992
|
+
if (!Private.isInTextRegion(targetCell, target)) {
|
|
2993
|
+
return false;
|
|
2994
|
+
}
|
|
2995
|
+
const { anchorNode } = selection;
|
|
2996
|
+
// The selection usually starts in the very cell which was clicked, which
|
|
2997
|
+
// can be checked without having to locate the cell of the anchor.
|
|
2998
|
+
if (Private.isInTextRegion(targetCell, anchorNode)) {
|
|
2999
|
+
return true;
|
|
3000
|
+
}
|
|
3001
|
+
const anchorCell = this._cellContaining(anchorNode);
|
|
3002
|
+
return !!anchorCell && Private.isInTextRegion(anchorCell, anchorNode);
|
|
3003
|
+
}
|
|
3004
|
+
|
|
2925
3005
|
/**
|
|
2926
3006
|
* Find the cell index containing the target html element.
|
|
2927
3007
|
*
|
|
2928
3008
|
* #### Notes
|
|
2929
3009
|
* Returns -1 if the cell is not found.
|
|
2930
3010
|
*/
|
|
2931
|
-
private _findCell(node:
|
|
3011
|
+
private _findCell(node: Element): number {
|
|
2932
3012
|
// Trace up the DOM hierarchy to find the root cell node.
|
|
2933
3013
|
// Then find the corresponding child and select it.
|
|
2934
|
-
let n:
|
|
3014
|
+
let n: Element | null = node;
|
|
2935
3015
|
while (n && n !== this.node) {
|
|
2936
3016
|
if (n.classList.contains(NB_CELL_CLASS)) {
|
|
2937
3017
|
const i = ArrayExt.findFirstIndex(
|
|
@@ -3165,18 +3245,22 @@ export class Notebook extends StaticNotebook {
|
|
|
3165
3245
|
if (targetArea === 'notebook') {
|
|
3166
3246
|
this.deselectAll();
|
|
3167
3247
|
} else if (targetArea === 'prompt' || targetArea === 'cell') {
|
|
3168
|
-
// We don't want to prevent the default selection behavior
|
|
3169
|
-
// if there is currently text selected in an output.
|
|
3170
|
-
const hasSelection = (window.getSelection() ?? '').toString() !== '';
|
|
3171
3248
|
if (
|
|
3172
3249
|
button === 0 &&
|
|
3173
3250
|
shiftKey &&
|
|
3174
|
-
!
|
|
3175
|
-
|
|
3251
|
+
!['INPUT', 'OPTION'].includes(target.tagName) &&
|
|
3252
|
+
// We don't want to prevent the default selection behavior when the
|
|
3253
|
+
// user is extending a text selection into the clicked region.
|
|
3254
|
+
!this._isExtendingTextSelection(window.getSelection(), widget, target)
|
|
3176
3255
|
) {
|
|
3177
3256
|
// Prevent browser selecting text in prompt or output
|
|
3178
3257
|
event.preventDefault();
|
|
3179
3258
|
|
|
3259
|
+
// Preventing the default also stops the browser from collapsing any
|
|
3260
|
+
// text selection which is already there; drop it explicitly so that
|
|
3261
|
+
// stale highlighted text is not left over the new cell selection.
|
|
3262
|
+
window.getSelection()?.removeAllRanges();
|
|
3263
|
+
|
|
3180
3264
|
// Shift-click - extend selection
|
|
3181
3265
|
try {
|
|
3182
3266
|
this.extendContiguousSelectionTo(index);
|
|
@@ -3474,7 +3558,7 @@ export class Notebook extends StaticNotebook {
|
|
|
3474
3558
|
const executionCount = (activeCell.model as ICodeCellModel)
|
|
3475
3559
|
.executionCount;
|
|
3476
3560
|
countString = ' ';
|
|
3477
|
-
if (executionCount) {
|
|
3561
|
+
if (executionCount !== null) {
|
|
3478
3562
|
countString = executionCount.toString();
|
|
3479
3563
|
}
|
|
3480
3564
|
} else {
|
|
@@ -3829,6 +3913,62 @@ namespace Private {
|
|
|
3829
3913
|
}
|
|
3830
3914
|
}
|
|
3831
3915
|
|
|
3916
|
+
/**
|
|
3917
|
+
* Check whether `node` is `parent` or a descendant of it.
|
|
3918
|
+
*
|
|
3919
|
+
* Unlike `Node.contains()` this traverses out of open shadow roots, so a
|
|
3920
|
+
* node rendered by a widget library which attaches a shadow root (such as
|
|
3921
|
+
* Panel or Bokeh) is still recognised as belonging to its host subtree.
|
|
3922
|
+
*/
|
|
3923
|
+
function containsDeep(parent: Node, node: Node | null): boolean {
|
|
3924
|
+
let current = node;
|
|
3925
|
+
while (current) {
|
|
3926
|
+
if (parent.contains(current)) {
|
|
3927
|
+
return true;
|
|
3928
|
+
}
|
|
3929
|
+
const root = current.getRootNode();
|
|
3930
|
+
current = root instanceof ShadowRoot ? root.host : null;
|
|
3931
|
+
}
|
|
3932
|
+
return false;
|
|
3933
|
+
}
|
|
3934
|
+
|
|
3935
|
+
/**
|
|
3936
|
+
* Whether a cell exposes an output area.
|
|
3937
|
+
*
|
|
3938
|
+
* #### Notes
|
|
3939
|
+
* This is a structural check rather than `instanceof CodeCell` so that it
|
|
3940
|
+
* still holds when a federated extension loads its own copy of
|
|
3941
|
+
* `@jupyterlab/cells`.
|
|
3942
|
+
*/
|
|
3943
|
+
function hasOutputArea(cell: Cell): cell is CodeCell {
|
|
3944
|
+
return 'outputArea' in cell;
|
|
3945
|
+
}
|
|
3946
|
+
|
|
3947
|
+
/**
|
|
3948
|
+
* The regions of a cell whose text the browser, rather than the notebook,
|
|
3949
|
+
* is responsible for selecting: the output area of a code cell and the
|
|
3950
|
+
* rendered input of a cell which renders it, such as a rendered markdown
|
|
3951
|
+
* cell.
|
|
3952
|
+
*/
|
|
3953
|
+
function textRegionsOf(cell: Cell): Node[] {
|
|
3954
|
+
const regions: Node[] = [];
|
|
3955
|
+
if (hasOutputArea(cell)) {
|
|
3956
|
+
regions.push(cell.outputArea.node);
|
|
3957
|
+
}
|
|
3958
|
+
const rendered = cell.inputArea?.renderedInput;
|
|
3959
|
+
if (rendered) {
|
|
3960
|
+
regions.push(rendered.node);
|
|
3961
|
+
}
|
|
3962
|
+
return regions;
|
|
3963
|
+
}
|
|
3964
|
+
|
|
3965
|
+
/**
|
|
3966
|
+
* Whether a node is in a region of a cell whose text the browser selects.
|
|
3967
|
+
*/
|
|
3968
|
+
export function isInTextRegion(cell: Cell, node: Node | null): boolean {
|
|
3969
|
+
return textRegionsOf(cell).some(region => containsDeep(region, node));
|
|
3970
|
+
}
|
|
3971
|
+
|
|
3832
3972
|
/**
|
|
3833
3973
|
* Create a cell drag image.
|
|
3834
3974
|
*/
|
package/style/base.css
CHANGED
|
@@ -281,6 +281,42 @@
|
|
|
281
281
|
white-space: normal;
|
|
282
282
|
}
|
|
283
283
|
|
|
284
|
+
.jp-CellIdField {
|
|
285
|
+
padding: 12px 0;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.jp-CellIdField label {
|
|
289
|
+
display: block;
|
|
290
|
+
line-height: 1.4;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
.jp-CellIdField-row {
|
|
294
|
+
display: flex;
|
|
295
|
+
gap: 4px;
|
|
296
|
+
margin-top: 4px;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.jp-CellIdField .jp-CellIdField-input {
|
|
300
|
+
flex: 1 1 auto;
|
|
301
|
+
width: 100%;
|
|
302
|
+
min-width: 0;
|
|
303
|
+
font-family: var(--jp-code-font-family);
|
|
304
|
+
font-size: var(--jp-code-font-size);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
.jp-CellIdField .jp-CellIdField-button {
|
|
308
|
+
flex: 0 0 28px;
|
|
309
|
+
width: 28px;
|
|
310
|
+
padding: 2px;
|
|
311
|
+
margin: 0;
|
|
312
|
+
color: var(--jp-ui-font-color2);
|
|
313
|
+
transition: color 120ms ease;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
.jp-CellIdField .jp-CellIdField-button.jp-mod-copied {
|
|
317
|
+
color: var(--jp-success-color1);
|
|
318
|
+
}
|
|
319
|
+
|
|
284
320
|
.jp-MetadataEditorTool {
|
|
285
321
|
flex-direction: column;
|
|
286
322
|
padding: 12px 0;
|