@jupyterlab/notebook 4.7.0-alpha.0 → 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/toc.js +13 -8
- package/lib/toc.js.map +1 -1
- package/lib/widget.d.ts +34 -2
- package/lib/widget.js +152 -18
- package/lib/widget.js.map +1 -1
- package/lib/widgetfactory.js +1 -4
- package/lib/widgetfactory.js.map +1 -1
- package/package.json +33 -33
- 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/toc.ts +17 -7
- package/src/widget.ts +164 -20
- package/src/widgetfactory.ts +1 -3
- 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/toc.ts
CHANGED
|
@@ -244,8 +244,10 @@ export class NotebookToCModel extends TableOfContentsModel<
|
|
|
244
244
|
*/
|
|
245
245
|
protected async getHeadings(): Promise<INotebookHeading[] | null> {
|
|
246
246
|
const cells = this.widget.content.widgets;
|
|
247
|
+
const currentCells = new Set(cells);
|
|
247
248
|
const headings: INotebookHeading[] = [];
|
|
248
249
|
const documentLevels = new Array<number>();
|
|
250
|
+
const cellToHeadingIndex = new WeakMap<Cell, number>();
|
|
249
251
|
|
|
250
252
|
// Generate headings by iterating through all notebook cells...
|
|
251
253
|
for (let i = 0; i < cells.length; i++) {
|
|
@@ -306,12 +308,15 @@ export class NotebookToCModel extends TableOfContentsModel<
|
|
|
306
308
|
}
|
|
307
309
|
|
|
308
310
|
if (headings.length > 0) {
|
|
309
|
-
|
|
310
|
-
} else {
|
|
311
|
-
// If no headings were found, remove the cell from the map
|
|
312
|
-
this._cellToHeadingIndex.delete(cell);
|
|
311
|
+
cellToHeadingIndex.set(cell, headings.length - 1);
|
|
313
312
|
}
|
|
314
313
|
}
|
|
314
|
+
|
|
315
|
+
this._cellToHeadingIndex = cellToHeadingIndex;
|
|
316
|
+
this._runningCells = this._runningCells.filter(cell =>
|
|
317
|
+
currentCells.has(cell)
|
|
318
|
+
);
|
|
319
|
+
this._errorCells = this._errorCells.filter(cell => currentCells.has(cell));
|
|
315
320
|
this.updateRunningStatus(headings);
|
|
316
321
|
return Promise.resolve(headings);
|
|
317
322
|
}
|
|
@@ -329,7 +334,8 @@ export class NotebookToCModel extends TableOfContentsModel<
|
|
|
329
334
|
): boolean {
|
|
330
335
|
return (
|
|
331
336
|
super.isHeadingEqual(heading1, heading2) &&
|
|
332
|
-
heading1.cellRef === heading2.cellRef
|
|
337
|
+
heading1.cellRef === heading2.cellRef &&
|
|
338
|
+
heading1.isRunning === heading2.isRunning
|
|
333
339
|
);
|
|
334
340
|
}
|
|
335
341
|
|
|
@@ -463,11 +469,15 @@ export class NotebookToCModel extends TableOfContentsModel<
|
|
|
463
469
|
}
|
|
464
470
|
|
|
465
471
|
protected updateRunningStatus(headings: INotebookHeading[]): void {
|
|
472
|
+
headings.forEach(heading => {
|
|
473
|
+
heading.isRunning = RunningStatus.Idle;
|
|
474
|
+
});
|
|
475
|
+
|
|
466
476
|
// Update isRunning
|
|
467
477
|
this._runningCells.forEach((cell, index) => {
|
|
468
478
|
const headingIndex = this._cellToHeadingIndex.get(cell);
|
|
469
479
|
if (headingIndex !== undefined) {
|
|
470
|
-
const heading =
|
|
480
|
+
const heading = headings[headingIndex];
|
|
471
481
|
// Running is prioritized over Scheduled, so if a heading is
|
|
472
482
|
// running don't change status
|
|
473
483
|
if (heading.isRunning !== RunningStatus.Running) {
|
|
@@ -480,7 +490,7 @@ export class NotebookToCModel extends TableOfContentsModel<
|
|
|
480
490
|
this._errorCells.forEach((cell, index) => {
|
|
481
491
|
const headingIndex = this._cellToHeadingIndex.get(cell);
|
|
482
492
|
if (headingIndex !== undefined) {
|
|
483
|
-
const heading =
|
|
493
|
+
const heading = headings[headingIndex];
|
|
484
494
|
// Running and Scheduled are prioritized over Error, so only if
|
|
485
495
|
// a heading is idle will it be set to Error
|
|
486
496
|
if (heading.isRunning === RunningStatus.Idle) {
|
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 = '[ ]';
|
|
@@ -2883,6 +2895,10 @@ export class Notebook extends StaticNotebook {
|
|
|
2883
2895
|
* Ensure that the notebook has proper focus.
|
|
2884
2896
|
*/
|
|
2885
2897
|
private _ensureFocus(force = false): void {
|
|
2898
|
+
const activeElement = document.activeElement;
|
|
2899
|
+
if (!force && activeElement && !this.node.contains(activeElement)) {
|
|
2900
|
+
return;
|
|
2901
|
+
}
|
|
2886
2902
|
// No-op is the footer has the focus.
|
|
2887
2903
|
const footer = (this.layout as NotebookWindowedLayout).footer;
|
|
2888
2904
|
if (footer && document.activeElement === footer.node) {
|
|
@@ -2918,16 +2934,84 @@ export class Notebook extends StaticNotebook {
|
|
|
2918
2934
|
}
|
|
2919
2935
|
}
|
|
2920
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
|
+
|
|
2921
3005
|
/**
|
|
2922
3006
|
* Find the cell index containing the target html element.
|
|
2923
3007
|
*
|
|
2924
3008
|
* #### Notes
|
|
2925
3009
|
* Returns -1 if the cell is not found.
|
|
2926
3010
|
*/
|
|
2927
|
-
private _findCell(node:
|
|
3011
|
+
private _findCell(node: Element): number {
|
|
2928
3012
|
// Trace up the DOM hierarchy to find the root cell node.
|
|
2929
3013
|
// Then find the corresponding child and select it.
|
|
2930
|
-
let n:
|
|
3014
|
+
let n: Element | null = node;
|
|
2931
3015
|
while (n && n !== this.node) {
|
|
2932
3016
|
if (n.classList.contains(NB_CELL_CLASS)) {
|
|
2933
3017
|
const i = ArrayExt.findFirstIndex(
|
|
@@ -3161,18 +3245,22 @@ export class Notebook extends StaticNotebook {
|
|
|
3161
3245
|
if (targetArea === 'notebook') {
|
|
3162
3246
|
this.deselectAll();
|
|
3163
3247
|
} else if (targetArea === 'prompt' || targetArea === 'cell') {
|
|
3164
|
-
// We don't want to prevent the default selection behavior
|
|
3165
|
-
// if there is currently text selected in an output.
|
|
3166
|
-
const hasSelection = (window.getSelection() ?? '').toString() !== '';
|
|
3167
3248
|
if (
|
|
3168
3249
|
button === 0 &&
|
|
3169
3250
|
shiftKey &&
|
|
3170
|
-
!
|
|
3171
|
-
|
|
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)
|
|
3172
3255
|
) {
|
|
3173
3256
|
// Prevent browser selecting text in prompt or output
|
|
3174
3257
|
event.preventDefault();
|
|
3175
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
|
+
|
|
3176
3264
|
// Shift-click - extend selection
|
|
3177
3265
|
try {
|
|
3178
3266
|
this.extendContiguousSelectionTo(index);
|
|
@@ -3470,7 +3558,7 @@ export class Notebook extends StaticNotebook {
|
|
|
3470
3558
|
const executionCount = (activeCell.model as ICodeCellModel)
|
|
3471
3559
|
.executionCount;
|
|
3472
3560
|
countString = ' ';
|
|
3473
|
-
if (executionCount) {
|
|
3561
|
+
if (executionCount !== null) {
|
|
3474
3562
|
countString = executionCount.toString();
|
|
3475
3563
|
}
|
|
3476
3564
|
} else {
|
|
@@ -3825,6 +3913,62 @@ namespace Private {
|
|
|
3825
3913
|
}
|
|
3826
3914
|
}
|
|
3827
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
|
+
|
|
3828
3972
|
/**
|
|
3829
3973
|
* Create a cell drag image.
|
|
3830
3974
|
*/
|
package/src/widgetfactory.ts
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
// Copyright (c) Jupyter Development Team.
|
|
2
2
|
// Distributed under the terms of the Modified BSD License.
|
|
3
|
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
4
|
-
|
|
5
3
|
import type { IEditorMimeTypeService } from '@jupyterlab/codeeditor';
|
|
6
4
|
import type { DocumentRegistry } from '@jupyterlab/docregistry';
|
|
7
5
|
import { ABCWidgetFactory } from '@jupyterlab/docregistry';
|
|
@@ -82,7 +80,7 @@ export class NotebookWidgetFactory extends ABCWidgetFactory<
|
|
|
82
80
|
context: DocumentRegistry.IContext<INotebookModel>,
|
|
83
81
|
source?: NotebookPanel
|
|
84
82
|
): NotebookPanel {
|
|
85
|
-
const translator =
|
|
83
|
+
const translator = this.translator;
|
|
86
84
|
const kernelHistory = new NotebookHistory({
|
|
87
85
|
sessionContext: context.sessionContext,
|
|
88
86
|
translator: translator
|