Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import * as DOM from '../../../../base/browser/dom.js';
7
import { IActionViewItem } from '../../../../base/browser/ui/actionbar/actionbar.js';
8
import { IAction, toAction } from '../../../../base/common/actions.js';
9
import { timeout } from '../../../../base/common/async.js';
10
import { CancellationToken } from '../../../../base/common/cancellation.js';
11
import { Emitter, Event } from '../../../../base/common/event.js';
12
import { DisposableStore, MutableDisposable } from '../../../../base/common/lifecycle.js';
13
import { extname, isEqual } from '../../../../base/common/resources.js';
14
import { URI } from '../../../../base/common/uri.js';
15
import { generateUuid } from '../../../../base/common/uuid.js';
16
import { ITextResourceConfigurationService } from '../../../../editor/common/services/textResourceConfiguration.js';
17
import { localize } from '../../../../nls.js';
18
import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
19
import { IEditorOptions } from '../../../../platform/editor/common/editor.js';
20
import { ByteSize, FileOperationError, FileOperationResult, IFileService, TooLargeFileOperationError } from '../../../../platform/files/common/files.js';
21
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
22
import { IStorageService } from '../../../../platform/storage/common/storage.js';
23
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
24
import { IThemeService } from '../../../../platform/theme/common/themeService.js';
25
import { Selection } from '../../../../editor/common/core/selection.js';
26
import { EditorPane } from '../../../browser/parts/editor/editorPane.js';
27
import { DEFAULT_EDITOR_ASSOCIATION, EditorPaneSelectionChangeReason, EditorPaneSelectionCompareResult, EditorResourceAccessor, IEditorMemento, IEditorOpenContext, IEditorPane, IEditorPaneScrollPosition, IEditorPaneSelection, IEditorPaneSelectionChangeEvent, IEditorPaneWithScrolling, createEditorOpenError, createTooLargeFileError, isEditorOpenError } from '../../../common/editor.js';
28
import { EditorInput } from '../../../common/editor/editorInput.js';
29
import { SELECT_KERNEL_ID } from './controller/coreActions.js';
30
import { INotebookEditorOptions, INotebookEditorPane, INotebookEditorViewState } from './notebookBrowser.js';
31
import { IBorrowValue, INotebookEditorService } from './services/notebookEditorService.js';
32
import { NotebookEditorWidget } from './notebookEditorWidget.js';
33
import { NotebooKernelActionViewItem } from './viewParts/notebookKernelView.js';
34
import { NotebookTextModel } from '../common/model/notebookTextModel.js';
35
import { CellKind, NOTEBOOK_EDITOR_ID, NotebookWorkingCopyTypeIdentifier } from '../common/notebookCommon.js';
36
import { NotebookEditorInput } from '../common/notebookEditorInput.js';
37
import { NotebookPerfMarks } from '../common/notebookPerformance.js';
38
import { GroupsOrder, IEditorGroup, IEditorGroupsService } from '../../../services/editor/common/editorGroupsService.js';
39
import { IEditorService } from '../../../services/editor/common/editorService.js';
40
import { IEditorProgressService } from '../../../../platform/progress/common/progress.js';
41
import { InstallRecommendedExtensionAction } from '../../extensions/browser/extensionsActions.js';
42
import { INotebookService } from '../common/notebookService.js';
43
import { IExtensionsWorkbenchService } from '../../extensions/common/extensions.js';
44
import { EnablementState } from '../../../services/extensionManagement/common/extensionManagement.js';
45
import { IWorkingCopyBackupService } from '../../../services/workingCopy/common/workingCopyBackup.js';
46
import { streamToBuffer } from '../../../../base/common/buffer.js';
47
import { ILogService } from '../../../../platform/log/common/log.js';
48
import { IPreferencesService } from '../../../services/preferences/common/preferences.js';
49
import { IActionViewItemOptions } from '../../../../base/browser/ui/actionbar/actionViewItems.js';
50
import { StopWatch } from '../../../../base/common/stopwatch.js';
51
import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js';
52
53
const NOTEBOOK_EDITOR_VIEW_STATE_PREFERENCE_KEY = 'NotebookEditorViewState';
54
55
export class NotebookEditor extends EditorPane implements INotebookEditorPane, IEditorPaneWithScrolling {
56
static readonly ID: string = NOTEBOOK_EDITOR_ID;
57
58
private readonly _editorMemento: IEditorMemento<INotebookEditorViewState>;
59
private readonly _groupListener = this._register(new DisposableStore());
60
private readonly _widgetDisposableStore: DisposableStore = this._register(new DisposableStore());
61
private _widget: IBorrowValue<NotebookEditorWidget> = { value: undefined };
62
private _rootElement!: HTMLElement;
63
private _pagePosition?: { readonly dimension: DOM.Dimension; readonly position: DOM.IDomPosition };
64
65
private readonly _inputListener = this._register(new MutableDisposable());
66
67
// override onDidFocus and onDidBlur to be based on the NotebookEditorWidget element
68
private readonly _onDidFocusWidget = this._register(new Emitter<void>());
69
override get onDidFocus(): Event<void> { return this._onDidFocusWidget.event; }
70
private readonly _onDidBlurWidget = this._register(new Emitter<void>());
71
override get onDidBlur(): Event<void> { return this._onDidBlurWidget.event; }
72
73
private readonly _onDidChangeModel = this._register(new Emitter<void>());
74
readonly onDidChangeModel: Event<void> = this._onDidChangeModel.event;
75
76
private readonly _onDidChangeSelection = this._register(new Emitter<IEditorPaneSelectionChangeEvent>());
77
readonly onDidChangeSelection = this._onDidChangeSelection.event;
78
79
protected readonly _onDidChangeScroll = this._register(new Emitter<void>());
80
readonly onDidChangeScroll = this._onDidChangeScroll.event;
81
82
constructor(
83
group: IEditorGroup,
84
@ITelemetryService telemetryService: ITelemetryService,
85
@IThemeService themeService: IThemeService,
86
@IInstantiationService private readonly _instantiationService: IInstantiationService,
87
@IStorageService storageService: IStorageService,
88
@IEditorService private readonly _editorService: IEditorService,
89
@IEditorGroupsService private readonly _editorGroupService: IEditorGroupsService,
90
@INotebookEditorService private readonly _notebookWidgetService: INotebookEditorService,
91
@IContextKeyService private readonly _contextKeyService: IContextKeyService,
92
@IFileService private readonly _fileService: IFileService,
93
@ITextResourceConfigurationService configurationService: ITextResourceConfigurationService,
94
@IEditorProgressService private readonly _editorProgressService: IEditorProgressService,
95
@INotebookService private readonly _notebookService: INotebookService,
96
@IExtensionsWorkbenchService private readonly _extensionsWorkbenchService: IExtensionsWorkbenchService,
97
@IWorkingCopyBackupService private readonly _workingCopyBackupService: IWorkingCopyBackupService,
98
@ILogService private readonly logService: ILogService,
99
@IPreferencesService private readonly _preferencesService: IPreferencesService
100
) {
101
super(NotebookEditor.ID, group, telemetryService, themeService, storageService);
102
this._editorMemento = this.getEditorMemento<INotebookEditorViewState>(_editorGroupService, configurationService, NOTEBOOK_EDITOR_VIEW_STATE_PREFERENCE_KEY);
103
104
this._register(this._fileService.onDidChangeFileSystemProviderCapabilities(e => this._onDidChangeFileSystemProvider(e.scheme)));
105
this._register(this._fileService.onDidChangeFileSystemProviderRegistrations(e => this._onDidChangeFileSystemProvider(e.scheme)));
106
}
107
108
private _onDidChangeFileSystemProvider(scheme: string): void {
109
if (this.input instanceof NotebookEditorInput && this.input.resource?.scheme === scheme) {
110
this._updateReadonly(this.input);
111
}
112
}
113
114
private _onDidChangeInputCapabilities(input: NotebookEditorInput): void {
115
if (this.input === input) {
116
this._updateReadonly(input);
117
}
118
}
119
120
private _updateReadonly(input: NotebookEditorInput): void {
121
this._widget.value?.setOptions({ isReadOnly: !!input.isReadonly() });
122
}
123
124
get textModel(): NotebookTextModel | undefined {
125
return this._widget.value?.textModel;
126
}
127
128
override get minimumWidth(): number { return 220; }
129
override get maximumWidth(): number { return Number.POSITIVE_INFINITY; }
130
131
// these setters need to exist because this extends from EditorPane
132
override set minimumWidth(value: number) { /*noop*/ }
133
override set maximumWidth(value: number) { /*noop*/ }
134
135
//#region Editor Core
136
override get scopedContextKeyService(): IContextKeyService | undefined {
137
return this._widget.value?.scopedContextKeyService;
138
}
139
140
protected createEditor(parent: HTMLElement): void {
141
this._rootElement = DOM.append(parent, DOM.$('.notebook-editor'));
142
this._rootElement.id = `notebook-editor-element-${generateUuid()}`;
143
}
144
145
override getActionViewItem(action: IAction, options: IActionViewItemOptions): IActionViewItem | undefined {
146
if (action.id === SELECT_KERNEL_ID) {
147
// this is being disposed by the consumer
148
return this._register(this._instantiationService.createInstance(NotebooKernelActionViewItem, action, this, options));
149
}
150
return undefined;
151
}
152
153
override getControl(): NotebookEditorWidget | undefined {
154
return this._widget.value;
155
}
156
157
override setVisible(visible: boolean): void {
158
super.setVisible(visible);
159
if (!visible) {
160
this._widget.value?.onWillHide();
161
}
162
}
163
164
protected override setEditorVisible(visible: boolean): void {
165
super.setEditorVisible(visible);
166
this._groupListener.clear();
167
this._groupListener.add(this.group.onWillCloseEditor(e => this._saveEditorViewState(e.editor)));
168
this._groupListener.add(this.group.onDidModelChange(() => {
169
if (this._editorGroupService.activeGroup !== this.group) {
170
this._widget?.value?.updateEditorFocus();
171
}
172
}));
173
174
if (!visible) {
175
this._saveEditorViewState(this.input);
176
if (this.input && this._widget.value) {
177
// the widget is not transfered to other editor inputs
178
this._widget.value.onWillHide();
179
}
180
}
181
}
182
183
override focus() {
184
super.focus();
185
this._widget.value?.focus();
186
}
187
188
override hasFocus(): boolean {
189
const value = this._widget.value;
190
if (!value) {
191
return false;
192
}
193
194
return !!value && (DOM.isAncestorOfActiveElement(value.getDomNode() || DOM.isAncestorOfActiveElement(value.getOverflowContainerDomNode())));
195
}
196
197
override async setInput(input: NotebookEditorInput, options: INotebookEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken, noRetry?: boolean): Promise<void> {
198
try {
199
let perfMarksCaptured = false;
200
const fileOpenMonitor = timeout(10000);
201
fileOpenMonitor.then(() => {
202
perfMarksCaptured = true;
203
this._handlePerfMark(perf, input);
204
});
205
206
const perf = new NotebookPerfMarks();
207
perf.mark('startTime');
208
209
this._inputListener.value = input.onDidChangeCapabilities(() => this._onDidChangeInputCapabilities(input));
210
211
this._widgetDisposableStore.clear();
212
213
// there currently is a widget which we still own so
214
// we need to hide it before getting a new widget
215
this._widget.value?.onWillHide();
216
217
this._widget = <IBorrowValue<NotebookEditorWidget>>this._instantiationService.invokeFunction(this._notebookWidgetService.retrieveWidget, this.group.id, input, undefined, this._pagePosition?.dimension, this.window);
218
219
if (this._rootElement && this._widget.value!.getDomNode()) {
220
this._rootElement.setAttribute('aria-flowto', this._widget.value!.getDomNode().id || '');
221
DOM.setParentFlowTo(this._widget.value!.getDomNode(), this._rootElement);
222
}
223
224
this._widgetDisposableStore.add(this._widget.value!.onDidChangeModel(() => this._onDidChangeModel.fire()));
225
this._widgetDisposableStore.add(this._widget.value!.onDidChangeActiveCell(() => this._onDidChangeSelection.fire({ reason: EditorPaneSelectionChangeReason.USER })));
226
227
if (this._pagePosition) {
228
this._widget.value!.layout(this._pagePosition.dimension, this._rootElement, this._pagePosition.position);
229
}
230
231
// only now `setInput` and yield/await. this is AFTER the actual widget is ready. This is very important
232
// so that others synchronously receive a notebook editor with the correct widget being set
233
await super.setInput(input, options, context, token);
234
const model = await input.resolve(options, perf);
235
perf.mark('inputLoaded');
236
237
// Check for cancellation
238
if (token.isCancellationRequested) {
239
return undefined;
240
}
241
242
// The widget has been taken away again. This can happen when the tab has been closed while
243
// loading was in progress, in particular when open the same resource as different view type.
244
// When this happen, retry once
245
if (!this._widget.value) {
246
if (noRetry) {
247
return undefined;
248
}
249
return this.setInput(input, options, context, token, true);
250
}
251
252
if (model === null) {
253
const knownProvider = this._notebookService.getViewTypeProvider(input.viewType);
254
255
if (!knownProvider) {
256
throw new Error(localize('fail.noEditor', "Cannot open resource with notebook editor type '{0}', please check if you have the right extension installed and enabled.", input.viewType));
257
}
258
259
await this._extensionsWorkbenchService.whenInitialized;
260
const extensionInfo = this._extensionsWorkbenchService.local.find(e => e.identifier.id === knownProvider);
261
262
throw createEditorOpenError(new Error(localize('fail.noEditor.extensionMissing', "Cannot open resource with notebook editor type '{0}', please check if you have the right extension installed and enabled.", input.viewType)), [
263
toAction({
264
id: 'workbench.notebook.action.installOrEnableMissing', label:
265
extensionInfo
266
? localize('notebookOpenEnableMissingViewType', "Enable extension for '{0}'", input.viewType)
267
: localize('notebookOpenInstallMissingViewType', "Install extension for '{0}'", input.viewType)
268
, run: async () => {
269
const d = this._notebookService.onAddViewType(viewType => {
270
if (viewType === input.viewType) {
271
// serializer is registered, try to open again
272
this._editorService.openEditor({ resource: input.resource });
273
d.dispose();
274
}
275
});
276
const extensionInfo = this._extensionsWorkbenchService.local.find(e => e.identifier.id === knownProvider);
277
278
try {
279
if (extensionInfo) {
280
await this._extensionsWorkbenchService.setEnablement(extensionInfo, extensionInfo.enablementState === EnablementState.DisabledWorkspace ? EnablementState.EnabledWorkspace : EnablementState.EnabledGlobally);
281
} else {
282
await this._instantiationService.createInstance(InstallRecommendedExtensionAction, knownProvider).run();
283
}
284
} catch (ex) {
285
this.logService.error(`Failed to install or enable extension ${knownProvider}`, ex);
286
d.dispose();
287
}
288
}
289
}),
290
toAction({
291
id: 'workbench.notebook.action.openAsText', label: localize('notebookOpenAsText', "Open As Text"), run: async () => {
292
const backup = await this._workingCopyBackupService.resolve({ resource: input.resource, typeId: NotebookWorkingCopyTypeIdentifier.create(input.viewType) });
293
if (backup) {
294
// with a backup present, we must resort to opening the backup contents
295
// as untitled text file to not show the wrong data to the user
296
const contents = await streamToBuffer(backup.value);
297
this._editorService.openEditor({ resource: undefined, contents: contents.toString() });
298
} else {
299
// without a backup present, we can open the original resource
300
this._editorService.openEditor({ resource: input.resource, options: { override: DEFAULT_EDITOR_ASSOCIATION.id, pinned: true } });
301
}
302
}
303
})
304
], { allowDialog: true });
305
306
}
307
308
this._widgetDisposableStore.add(model.notebook.onDidChangeContent(() => this._onDidChangeSelection.fire({ reason: EditorPaneSelectionChangeReason.EDIT })));
309
310
const viewState = options?.viewState ?? this._loadNotebookEditorViewState(input);
311
312
// We might be moving the notebook widget between groups, and these services are tied to the group
313
this._widget.value.setParentContextKeyService(this._contextKeyService);
314
this._widget.value.setEditorProgressService(this._editorProgressService);
315
316
await this._widget.value.setModel(model.notebook, viewState, perf);
317
const isReadOnly = !!input.isReadonly();
318
await this._widget.value.setOptions({ ...options, isReadOnly });
319
this._widgetDisposableStore.add(this._widget.value.onDidFocusWidget(() => this._onDidFocusWidget.fire()));
320
this._widgetDisposableStore.add(this._widget.value.onDidBlurWidget(() => this._onDidBlurWidget.fire()));
321
322
this._widgetDisposableStore.add(this._editorGroupService.createEditorDropTarget(this._widget.value.getDomNode(), {
323
containsGroup: (group) => this.group.id === group.id
324
}));
325
326
this._widgetDisposableStore.add(this._widget.value.onDidScroll(() => { this._onDidChangeScroll.fire(); }));
327
328
perf.mark('editorLoaded');
329
330
fileOpenMonitor.cancel();
331
if (perfMarksCaptured) {
332
return;
333
}
334
335
this._handlePerfMark(perf, input, model.notebook);
336
this._onDidChangeControl.fire();
337
} catch (e) {
338
this.logService.warn('NotebookEditorWidget#setInput failed', e);
339
if (isEditorOpenError(e)) {
340
throw e;
341
}
342
343
// Handle case where a file is too large to open without confirmation
344
if ((<FileOperationError>e).fileOperationResult === FileOperationResult.FILE_TOO_LARGE) {
345
let message: string;
346
if (e instanceof TooLargeFileOperationError) {
347
message = localize('notebookTooLargeForHeapErrorWithSize', "The notebook is not displayed in the notebook editor because it is very large ({0}).", ByteSize.formatSize(e.size));
348
} else {
349
message = localize('notebookTooLargeForHeapErrorWithoutSize', "The notebook is not displayed in the notebook editor because it is very large.");
350
}
351
352
throw createTooLargeFileError(this.group, input, options, message, this._preferencesService);
353
}
354
355
const error = createEditorOpenError(e instanceof Error ? e : new Error((e ? e.message : '')), [
356
toAction({
357
id: 'workbench.notebook.action.openInTextEditor', label: localize('notebookOpenInTextEditor', "Open in Text Editor"), run: async () => {
358
const activeEditorPane = this._editorService.activeEditorPane;
359
if (!activeEditorPane) {
360
return;
361
}
362
363
const activeEditorResource = EditorResourceAccessor.getCanonicalUri(activeEditorPane.input);
364
if (!activeEditorResource) {
365
return;
366
}
367
368
if (activeEditorResource.toString() === input.resource?.toString()) {
369
// Replace the current editor with the text editor
370
return this._editorService.openEditor({
371
resource: activeEditorResource,
372
options: {
373
override: DEFAULT_EDITOR_ASSOCIATION.id,
374
pinned: true // new file gets pinned by default
375
}
376
});
377
}
378
379
return;
380
}
381
})
382
], { allowDialog: true });
383
384
throw error;
385
}
386
}
387
388
private _handlePerfMark(perf: NotebookPerfMarks, input: NotebookEditorInput, notebook?: NotebookTextModel) {
389
const perfMarks = perf.value;
390
391
type WorkbenchNotebookOpenClassification = {
392
owner: 'rebornix';
393
comment: 'The notebook file open metrics. Used to get a better understanding of the performance of notebook file opening';
394
scheme: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'File system provider scheme for the notebook resource' };
395
ext: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'File extension for the notebook resource' };
396
viewType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The view type of the notebook editor' };
397
extensionActivated: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Extension activation time for the resource opening' };
398
inputLoaded: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Editor Input loading time for the resource opening' };
399
webviewCommLoaded: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Webview initialization time for the resource opening' };
400
customMarkdownLoaded: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Custom markdown loading time for the resource opening' };
401
editorLoaded: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Overall editor loading time for the resource opening' };
402
codeCellCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Total number of code cell' };
403
mdCellCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Total number of markdown cell' };
404
outputCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Total number of cell outputs' };
405
outputBytes: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Total number of bytes for all outputs' };
406
codeLength: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Length of text in all code cells' };
407
markdownLength: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Length of text in all markdown cells' };
408
notebookStatsLoaded: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Time for generating the notebook level information for telemetry' };
409
};
410
411
type WorkbenchNotebookOpenEvent = {
412
scheme: string;
413
ext: string;
414
viewType: string;
415
extensionActivated: number;
416
inputLoaded: number;
417
webviewCommLoaded: number;
418
customMarkdownLoaded: number | undefined;
419
editorLoaded: number;
420
codeCellCount: number | undefined;
421
mdCellCount: number | undefined;
422
outputCount: number | undefined;
423
outputBytes: number | undefined;
424
codeLength: number | undefined;
425
markdownLength: number | undefined;
426
notebookStatsLoaded: number | undefined;
427
};
428
429
const startTime = perfMarks['startTime'];
430
const extensionActivated = perfMarks['extensionActivated'];
431
const inputLoaded = perfMarks['inputLoaded'];
432
const webviewCommLoaded = perfMarks['webviewCommLoaded'];
433
const customMarkdownLoaded = perfMarks['customMarkdownLoaded'];
434
const editorLoaded = perfMarks['editorLoaded'];
435
436
let extensionActivationTimespan = -1;
437
let inputLoadingTimespan = -1;
438
let webviewCommLoadingTimespan = -1;
439
let customMarkdownLoadingTimespan = -1;
440
let editorLoadingTimespan = -1;
441
442
if (startTime !== undefined && extensionActivated !== undefined) {
443
extensionActivationTimespan = extensionActivated - startTime;
444
445
if (inputLoaded !== undefined) {
446
inputLoadingTimespan = inputLoaded - extensionActivated;
447
}
448
449
if (webviewCommLoaded !== undefined) {
450
webviewCommLoadingTimespan = webviewCommLoaded - extensionActivated;
451
452
}
453
454
if (customMarkdownLoaded !== undefined) {
455
customMarkdownLoadingTimespan = customMarkdownLoaded - startTime;
456
}
457
458
if (editorLoaded !== undefined) {
459
editorLoadingTimespan = editorLoaded - startTime;
460
}
461
}
462
463
// Notebook information
464
let codeCellCount: number | undefined = undefined;
465
let mdCellCount: number | undefined = undefined;
466
let outputCount: number | undefined = undefined;
467
let outputBytes: number | undefined = undefined;
468
let codeLength: number | undefined = undefined;
469
let markdownLength: number | undefined = undefined;
470
let notebookStatsLoaded: number | undefined = undefined;
471
if (notebook) {
472
const stopWatch = new StopWatch();
473
for (const cell of notebook.cells) {
474
if (cell.cellKind === CellKind.Code) {
475
codeCellCount = (codeCellCount || 0) + 1;
476
codeLength = (codeLength || 0) + cell.getTextLength();
477
outputCount = (outputCount || 0) + cell.outputs.length;
478
outputBytes = (outputBytes || 0) + cell.outputs.reduce((prev, cur) => prev + cur.outputs.reduce((size, item) => size + item.data.byteLength, 0), 0);
479
} else {
480
mdCellCount = (mdCellCount || 0) + 1;
481
markdownLength = (codeLength || 0) + cell.getTextLength();
482
}
483
}
484
notebookStatsLoaded = stopWatch.elapsed();
485
}
486
487
this.logService.trace(`[NotebookEditor] open notebook perf ${notebook?.uri.toString() ?? ''} - extensionActivation: ${extensionActivationTimespan}, inputLoad: ${inputLoadingTimespan}, webviewComm: ${webviewCommLoadingTimespan}, customMarkdown: ${customMarkdownLoadingTimespan}, editorLoad: ${editorLoadingTimespan}`);
488
489
this.telemetryService.publicLog2<WorkbenchNotebookOpenEvent, WorkbenchNotebookOpenClassification>('notebook/editorOpenPerf', {
490
scheme: input.resource.scheme,
491
ext: extname(input.resource),
492
viewType: input.viewType,
493
extensionActivated: extensionActivationTimespan,
494
inputLoaded: inputLoadingTimespan,
495
webviewCommLoaded: webviewCommLoadingTimespan,
496
customMarkdownLoaded: customMarkdownLoadingTimespan,
497
editorLoaded: editorLoadingTimespan,
498
codeCellCount,
499
mdCellCount,
500
outputCount,
501
outputBytes,
502
codeLength,
503
markdownLength,
504
notebookStatsLoaded
505
});
506
}
507
508
override clearInput(): void {
509
this._inputListener.clear();
510
511
if (this._widget.value) {
512
this._saveEditorViewState(this.input);
513
this._widget.value.onWillHide();
514
}
515
super.clearInput();
516
}
517
518
override setOptions(options: INotebookEditorOptions | undefined): void {
519
this._widget.value?.setOptions(options);
520
super.setOptions(options);
521
}
522
523
protected override saveState(): void {
524
this._saveEditorViewState(this.input);
525
super.saveState();
526
}
527
528
override getViewState(): INotebookEditorViewState | undefined {
529
const input = this.input;
530
if (!(input instanceof NotebookEditorInput)) {
531
return undefined;
532
}
533
534
this._saveEditorViewState(input);
535
return this._loadNotebookEditorViewState(input);
536
}
537
538
getSelection(): IEditorPaneSelection | undefined {
539
if (this._widget.value) {
540
const activeCell = this._widget.value.getActiveCell();
541
if (activeCell) {
542
const cellUri = activeCell.uri;
543
return new NotebookEditorSelection(cellUri, activeCell.getSelections());
544
}
545
}
546
547
return undefined;
548
}
549
550
getScrollPosition(): IEditorPaneScrollPosition {
551
const widget = this.getControl();
552
if (!widget) {
553
throw new Error('Notebook widget has not yet been initialized');
554
}
555
556
return {
557
scrollTop: widget.scrollTop,
558
scrollLeft: 0,
559
};
560
}
561
562
setScrollPosition(scrollPosition: IEditorPaneScrollPosition): void {
563
const editor = this.getControl();
564
if (!editor) {
565
throw new Error('Control has not yet been initialized');
566
}
567
568
editor.setScrollTop(scrollPosition.scrollTop);
569
}
570
571
private _saveEditorViewState(input: EditorInput | undefined): void {
572
if (this._widget.value && input instanceof NotebookEditorInput) {
573
if (this._widget.value.isDisposed) {
574
return;
575
}
576
577
const state = this._widget.value.getEditorViewState();
578
this._editorMemento.saveEditorState(this.group, input.resource, state);
579
}
580
}
581
582
private _loadNotebookEditorViewState(input: NotebookEditorInput): INotebookEditorViewState | undefined {
583
const result = this._editorMemento.loadEditorState(this.group, input.resource);
584
if (result) {
585
return result;
586
}
587
// when we don't have a view state for the group/input-tuple then we try to use an existing
588
// editor for the same resource.
589
for (const group of this._editorGroupService.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE)) {
590
if (group.activeEditorPane !== this && group.activeEditorPane instanceof NotebookEditor && group.activeEditor?.matches(input)) {
591
return group.activeEditorPane._widget.value?.getEditorViewState();
592
}
593
}
594
return;
595
}
596
597
layout(dimension: DOM.Dimension, position: DOM.IDomPosition): void {
598
this._rootElement.classList.toggle('mid-width', dimension.width < 1000 && dimension.width >= 600);
599
this._rootElement.classList.toggle('narrow-width', dimension.width < 600);
600
this._pagePosition = { dimension, position };
601
602
if (!this._widget.value || !(this.input instanceof NotebookEditorInput)) {
603
return;
604
}
605
606
if (this.input.resource.toString() !== this.textModel?.uri.toString() && this._widget.value?.hasModel()) {
607
// input and widget mismatch
608
// this happens when
609
// 1. open document A, pin the document
610
// 2. open document B
611
// 3. close document B
612
// 4. a layout is triggered
613
return;
614
}
615
616
if (this.isVisible()) {
617
this._widget.value.layout(dimension, this._rootElement, position);
618
}
619
}
620
621
//#endregion
622
}
623
624
class NotebookEditorSelection implements IEditorPaneSelection {
625
626
constructor(
627
private readonly cellUri: URI,
628
private readonly selections: Selection[]
629
) { }
630
631
compare(other: IEditorPaneSelection): EditorPaneSelectionCompareResult {
632
if (!(other instanceof NotebookEditorSelection)) {
633
return EditorPaneSelectionCompareResult.DIFFERENT;
634
}
635
636
if (isEqual(this.cellUri, other.cellUri)) {
637
return EditorPaneSelectionCompareResult.IDENTICAL;
638
}
639
640
return EditorPaneSelectionCompareResult.DIFFERENT;
641
}
642
643
restore(options: IEditorOptions): INotebookEditorOptions {
644
const notebookOptions: INotebookEditorOptions = {
645
cellOptions: {
646
resource: this.cellUri,
647
options: {
648
selection: this.selections[0]
649
}
650
}
651
};
652
653
Object.assign(notebookOptions, options);
654
655
return notebookOptions;
656
}
657
658
log(): string {
659
return this.cellUri.fragment;
660
}
661
}
662
663
export function isNotebookContainingCellEditor(editor: IEditorPane | undefined, codeEditor: ICodeEditor): boolean {
664
if (editor?.getId() === NotebookEditor.ID) {
665
const notebookWidget = editor.getControl() as NotebookEditorWidget;
666
if (notebookWidget) {
667
for (const [_, editor] of notebookWidget.codeEditors) {
668
if (editor === codeEditor) {
669
return true;
670
}
671
}
672
}
673
}
674
return false;
675
}
676
677