Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.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 { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js';
8
import { Schemas } from '../../../../../base/common/network.js';
9
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
10
import { DiffElementCellViewModelBase, getFormattedOutputJSON, OutputComparison, outputEqual, OUTPUT_EDITOR_HEIGHT_MAGIC, PropertyFoldingState, SideBySideDiffElementViewModel, SingleSideDiffElementViewModel, DiffElementPlaceholderViewModel, IDiffElementViewModelBase, NotebookDocumentMetadataViewModel } from './diffElementViewModel.js';
11
import { CellDiffSideBySideRenderTemplate, CellDiffSingleSideRenderTemplate, DiffSide, DIFF_CELL_MARGIN, INotebookTextDiffEditor, NOTEBOOK_DIFF_CELL_INPUT, NOTEBOOK_DIFF_CELL_PROPERTY, NOTEBOOK_DIFF_CELL_PROPERTY_EXPANDED, CellDiffPlaceholderRenderTemplate, IDiffCellMarginOverlay, NOTEBOOK_DIFF_CELL_IGNORE_WHITESPACE, NotebookDocumentDiffElementRenderTemplate, NOTEBOOK_DIFF_METADATA } from './notebookDiffEditorBrowser.js';
12
import { CodeEditorWidget, ICodeEditorWidgetOptions } from '../../../../../editor/browser/widget/codeEditor/codeEditorWidget.js';
13
import { IModelService } from '../../../../../editor/common/services/model.js';
14
import { ILanguageService } from '../../../../../editor/common/languages/language.js';
15
import { CellEditType, CellUri, NotebookCellMetadata } from '../../common/notebookCommon.js';
16
import { ToolBar } from '../../../../../base/browser/ui/toolbar/toolbar.js';
17
import { IContextMenuService } from '../../../../../platform/contextview/browser/contextView.js';
18
import { IMenu, IMenuService, MenuId, MenuItemAction } from '../../../../../platform/actions/common/actions.js';
19
import { IKeybindingService } from '../../../../../platform/keybinding/common/keybinding.js';
20
import { INotificationService } from '../../../../../platform/notification/common/notification.js';
21
import { getFlatActionBarActions } from '../../../../../platform/actions/browser/menuEntryActionViewItem.js';
22
import { IContextKey, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js';
23
import { CodiconActionViewItem } from '../view/cellParts/cellActionView.js';
24
import { collapsedIcon, expandedIcon } from '../notebookIcons.js';
25
import { OutputContainer } from './diffElementOutputs.js';
26
import { EditorExtensionsRegistry } from '../../../../../editor/browser/editorExtensions.js';
27
import { ContextMenuController } from '../../../../../editor/contrib/contextmenu/browser/contextmenu.js';
28
import { SnippetController2 } from '../../../../../editor/contrib/snippet/browser/snippetController2.js';
29
import { SuggestController } from '../../../../../editor/contrib/suggest/browser/suggestController.js';
30
import { MenuPreventer } from '../../../codeEditor/browser/menuPreventer.js';
31
import { SelectionClipboardContributionID } from '../../../codeEditor/browser/selectionClipboard.js';
32
import { TabCompletionController } from '../../../snippets/browser/tabCompletion.js';
33
import { renderIcon, renderLabelWithIcons } from '../../../../../base/browser/ui/iconLabel/iconLabels.js';
34
import * as editorCommon from '../../../../../editor/common/editorCommon.js';
35
import { ITextModelService } from '../../../../../editor/common/services/resolverService.js';
36
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
37
import { IThemeService } from '../../../../../platform/theme/common/themeService.js';
38
import { WorkbenchToolBar } from '../../../../../platform/actions/browser/toolbar.js';
39
import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js';
40
import { fixedDiffEditorOptions, fixedEditorOptions, getEditorPadding } from './diffCellEditorOptions.js';
41
import { AccessibilityVerbositySettingId } from '../../../accessibility/browser/accessibilityConfiguration.js';
42
import { IAccessibilityService } from '../../../../../platform/accessibility/common/accessibility.js';
43
import { DiffEditorWidget } from '../../../../../editor/browser/widget/diffEditor/diffEditorWidget.js';
44
import { ICommandService } from '../../../../../platform/commands/common/commands.js';
45
import { DiffNestedCellViewModel } from './diffNestedCellViewModel.js';
46
import { localize } from '../../../../../nls.js';
47
import { Emitter } from '../../../../../base/common/event.js';
48
import { ITextResourceConfigurationService } from '../../../../../editor/common/services/textResourceConfiguration.js';
49
import { getFormattedMetadataJSON } from '../../common/model/notebookCellTextModel.js';
50
import { IDiffEditorOptions } from '../../../../../editor/common/config/editorOptions.js';
51
import { getUnchangedRegionSettings } from './unchangedEditorRegions.js';
52
53
export function getOptimizedNestedCodeEditorWidgetOptions(): ICodeEditorWidgetOptions {
54
return {
55
isSimpleWidget: false,
56
contributions: EditorExtensionsRegistry.getSomeEditorContributions([
57
MenuPreventer.ID,
58
SelectionClipboardContributionID,
59
ContextMenuController.ID,
60
SuggestController.ID,
61
SnippetController2.ID,
62
TabCompletionController.ID,
63
])
64
};
65
}
66
67
export class CellDiffPlaceholderElement extends Disposable {
68
constructor(
69
placeholder: DiffElementPlaceholderViewModel,
70
templateData: CellDiffPlaceholderRenderTemplate,
71
) {
72
super();
73
templateData.body.classList.remove('left', 'right', 'full');
74
const text = (placeholder.hiddenCells.length === 1) ?
75
localize('hiddenCell', '{0} hidden cell', placeholder.hiddenCells.length) :
76
localize('hiddenCells', '{0} hidden cells', placeholder.hiddenCells.length);
77
templateData.placeholder.innerText = text;
78
79
this._register(DOM.addDisposableListener(templateData.placeholder, 'dblclick', (e: MouseEvent) => {
80
if (e.button !== 0) {
81
return;
82
}
83
e.preventDefault();
84
placeholder.showHiddenCells();
85
}));
86
this._register(templateData.marginOverlay.onAction(() => placeholder.showHiddenCells()));
87
templateData.marginOverlay.show();
88
}
89
}
90
91
class PropertyHeader extends Disposable {
92
protected _foldingIndicator!: HTMLElement;
93
protected _statusSpan!: HTMLElement;
94
protected _description!: HTMLElement;
95
protected _toolbar!: WorkbenchToolBar;
96
protected _menu!: IMenu;
97
protected _propertyExpanded?: IContextKey<boolean>;
98
protected _propertyChanged?: IContextKey<boolean>;
99
100
constructor(
101
readonly cell: IDiffElementViewModelBase,
102
readonly propertyHeaderContainer: HTMLElement,
103
readonly notebookEditor: INotebookTextDiffEditor,
104
readonly accessor: {
105
updateInfoRendering: (renderOutput: boolean) => void;
106
checkIfModified: () => false | { reason: string | undefined };
107
getFoldingState: () => PropertyFoldingState;
108
updateFoldingState: (newState: PropertyFoldingState) => void;
109
unChangedLabel: string;
110
changedLabel: string;
111
prefix: string;
112
menuId: MenuId;
113
},
114
@IContextMenuService private readonly contextMenuService: IContextMenuService,
115
@IKeybindingService private readonly keybindingService: IKeybindingService,
116
@ICommandService private readonly commandService: ICommandService,
117
@INotificationService private readonly notificationService: INotificationService,
118
@IMenuService private readonly menuService: IMenuService,
119
@IContextKeyService private readonly contextKeyService: IContextKeyService,
120
@IThemeService private readonly themeService: IThemeService,
121
@ITelemetryService private readonly telemetryService: ITelemetryService,
122
@IAccessibilityService private readonly accessibilityService: IAccessibilityService
123
) {
124
super();
125
}
126
127
buildHeader(): void {
128
this._foldingIndicator = DOM.append(this.propertyHeaderContainer, DOM.$('.property-folding-indicator'));
129
this._foldingIndicator.classList.add(this.accessor.prefix);
130
const metadataStatus = DOM.append(this.propertyHeaderContainer, DOM.$('div.property-status'));
131
this._statusSpan = DOM.append(metadataStatus, DOM.$('span'));
132
this._description = DOM.append(metadataStatus, DOM.$('span.property-description'));
133
134
const cellToolbarContainer = DOM.append(this.propertyHeaderContainer, DOM.$('div.property-toolbar'));
135
this._toolbar = this._register(new WorkbenchToolBar(cellToolbarContainer, {
136
actionViewItemProvider: (action, options) => {
137
if (action instanceof MenuItemAction) {
138
const item = new CodiconActionViewItem(action, { hoverDelegate: options.hoverDelegate }, this.keybindingService, this.notificationService, this.contextKeyService, this.themeService, this.contextMenuService, this.accessibilityService);
139
return item;
140
}
141
142
return undefined;
143
}
144
}, this.menuService, this.contextKeyService, this.contextMenuService, this.keybindingService, this.commandService, this.telemetryService));
145
this._toolbar.context = this.cell;
146
147
const scopedContextKeyService = this.contextKeyService.createScoped(cellToolbarContainer);
148
this._register(scopedContextKeyService);
149
this._propertyChanged = NOTEBOOK_DIFF_CELL_PROPERTY.bindTo(scopedContextKeyService);
150
this._propertyExpanded = NOTEBOOK_DIFF_CELL_PROPERTY_EXPANDED.bindTo(scopedContextKeyService);
151
152
this._menu = this._register(this.menuService.createMenu(this.accessor.menuId, scopedContextKeyService));
153
this._register(this._menu.onDidChange(() => this.updateMenu()));
154
155
this._register(this.notebookEditor.onMouseUp(e => {
156
if (!e.event.target || e.target !== this.cell) {
157
return;
158
}
159
160
const target = e.event.target as HTMLElement;
161
162
if (
163
target === this.propertyHeaderContainer ||
164
target === this._foldingIndicator || this._foldingIndicator.contains(target) ||
165
target === metadataStatus || metadataStatus.contains(target)
166
) {
167
const oldFoldingState = this.accessor.getFoldingState();
168
this.accessor.updateFoldingState(oldFoldingState === PropertyFoldingState.Expanded ? PropertyFoldingState.Collapsed : PropertyFoldingState.Expanded);
169
this._updateFoldingIcon();
170
this.accessor.updateInfoRendering(this.cell.renderOutput);
171
}
172
}));
173
174
this.refresh();
175
this.accessor.updateInfoRendering(this.cell.renderOutput);
176
}
177
refresh() {
178
this.updateMenu();
179
this._updateFoldingIcon();
180
181
const metadataChanged = this.accessor.checkIfModified();
182
if (this._propertyChanged) {
183
this._propertyChanged.set(!!metadataChanged);
184
}
185
if (metadataChanged) {
186
this._statusSpan.textContent = this.accessor.changedLabel;
187
this._statusSpan.style.fontWeight = 'bold';
188
if (metadataChanged.reason) {
189
this._description.textContent = metadataChanged.reason;
190
}
191
this.propertyHeaderContainer.classList.add('modified');
192
} else {
193
this._statusSpan.textContent = this.accessor.unChangedLabel;
194
this._statusSpan.style.fontWeight = 'normal';
195
this._description.textContent = '';
196
this.propertyHeaderContainer.classList.remove('modified');
197
}
198
}
199
200
private updateMenu() {
201
const metadataChanged = this.accessor.checkIfModified();
202
if (metadataChanged) {
203
const actions = getFlatActionBarActions(this._menu.getActions({ shouldForwardArgs: true }));
204
this._toolbar.setActions(actions);
205
} else {
206
this._toolbar.setActions([]);
207
}
208
}
209
210
private _updateFoldingIcon() {
211
if (this.accessor.getFoldingState() === PropertyFoldingState.Collapsed) {
212
DOM.reset(this._foldingIndicator, renderIcon(collapsedIcon));
213
this._propertyExpanded?.set(false);
214
} else {
215
DOM.reset(this._foldingIndicator, renderIcon(expandedIcon));
216
this._propertyExpanded?.set(true);
217
}
218
219
}
220
}
221
222
interface IDiffElementLayoutState {
223
outerWidth?: boolean;
224
editorHeight?: boolean;
225
metadataEditor?: boolean;
226
metadataHeight?: boolean;
227
outputTotalHeight?: boolean;
228
}
229
230
231
export class NotebookDocumentMetadataElement extends Disposable {
232
private readonly _editor: DiffEditorWidget;
233
private _editorViewStateChanged: boolean;
234
private _toolbar!: ToolBar;
235
private readonly _cellHeaderContainer: HTMLElement;
236
private readonly _editorContainer: HTMLElement;
237
private _cellHeader!: PropertyHeader;
238
private _diffEditorContainer!: HTMLElement;
239
240
constructor(
241
readonly notebookEditor: INotebookTextDiffEditor,
242
readonly viewModel: NotebookDocumentMetadataViewModel,
243
readonly templateData: NotebookDocumentDiffElementRenderTemplate,
244
@IInstantiationService private readonly instantiationService: IInstantiationService,
245
@ITextModelService private readonly textModelService: ITextModelService,
246
@IMenuService private readonly menuService: IMenuService,
247
@IContextKeyService private readonly contextKeyService: IContextKeyService,
248
@ITextResourceConfigurationService private readonly textConfigurationService: ITextResourceConfigurationService,
249
@IConfigurationService private readonly configurationService: IConfigurationService,
250
) {
251
super();
252
this._editor = templateData.sourceEditor;
253
this._cellHeaderContainer = this.templateData.cellHeaderContainer;
254
this._editorContainer = this.templateData.editorContainer;
255
this._diffEditorContainer = this.templateData.diffEditorContainer;
256
257
this._editorViewStateChanged = false;
258
// init
259
this._register(viewModel.onDidLayoutChange(e => {
260
this.layout(e);
261
this.updateBorders();
262
}));
263
this.buildBody();
264
this.updateBorders();
265
}
266
267
buildBody(): void {
268
const body = this.templateData.body;
269
body.classList.remove('full');
270
body.classList.add('full');
271
272
this.updateSourceEditor();
273
274
if (this.viewModel instanceof NotebookDocumentMetadataViewModel) {
275
this._register(this.viewModel.modifiedMetadata.onDidChange(e => {
276
this._cellHeader.refresh();
277
}));
278
}
279
}
280
protected layoutNotebookCell() {
281
this.notebookEditor.layoutNotebookCell(
282
this.viewModel,
283
this.viewModel.layoutInfo.totalHeight
284
);
285
}
286
287
updateBorders() {
288
this.templateData.leftBorder.style.height = `${this.viewModel.layoutInfo.totalHeight - 32}px`;
289
this.templateData.rightBorder.style.height = `${this.viewModel.layoutInfo.totalHeight - 32}px`;
290
this.templateData.bottomBorder.style.top = `${this.viewModel.layoutInfo.totalHeight - 32}px`;
291
}
292
updateSourceEditor(): void {
293
this._cellHeaderContainer.style.display = 'flex';
294
this._cellHeaderContainer.innerText = '';
295
this._editorContainer.classList.add('diff');
296
297
const updateSourceEditor = () => {
298
if (this.viewModel.cellFoldingState === PropertyFoldingState.Collapsed) {
299
this._editorContainer.style.display = 'none';
300
this.viewModel.editorHeight = 0;
301
return;
302
}
303
304
const lineHeight = this.notebookEditor.getLayoutInfo().fontInfo.lineHeight || 17;
305
const editorHeight = this.viewModel.layoutInfo.editorHeight !== 0 ? this.viewModel.layoutInfo.editorHeight : this.viewModel.computeInputEditorHeight(lineHeight);
306
307
this._editorContainer.style.height = `${editorHeight}px`;
308
this._editorContainer.style.display = 'block';
309
310
const contentHeight = this._editor.getContentHeight();
311
if (contentHeight >= 0) {
312
this.viewModel.editorHeight = contentHeight;
313
}
314
return editorHeight;
315
};
316
const renderSourceEditor = () => {
317
const editorHeight = updateSourceEditor();
318
if (!editorHeight) {
319
return;
320
}
321
322
// If there is only 1 line, then ensure we have the necessary padding to display the button for whitespaces.
323
// E.g. assume we have a cell with 1 line and we add some whitespace,
324
// Then diff editor displays the button `Show Whitespace Differences`, however with 12 paddings on the top, the
325
// button can get cut off.
326
const lineCount = this.viewModel.modifiedMetadata.textBuffer.getLineCount();
327
const options: IDiffEditorOptions = {
328
padding: getEditorPadding(lineCount)
329
};
330
const unchangedRegions = this._register(getUnchangedRegionSettings(this.configurationService));
331
if (unchangedRegions.options.enabled) {
332
options.hideUnchangedRegions = unchangedRegions.options;
333
}
334
this._editor.updateOptions(options);
335
this._register(unchangedRegions.onDidChangeEnablement(() => {
336
options.hideUnchangedRegions = unchangedRegions.options;
337
this._editor.updateOptions(options);
338
}));
339
this._editor.layout({
340
width: this.notebookEditor.getLayoutInfo().width - 2 * DIFF_CELL_MARGIN,
341
height: editorHeight
342
});
343
this._register(this._editor.onDidContentSizeChange((e) => {
344
if (this.viewModel.cellFoldingState === PropertyFoldingState.Expanded && e.contentHeightChanged && this.viewModel.layoutInfo.editorHeight !== e.contentHeight) {
345
this.viewModel.editorHeight = e.contentHeight;
346
}
347
}));
348
this._initializeSourceDiffEditor();
349
};
350
351
this._cellHeader = this._register(this.instantiationService.createInstance(
352
PropertyHeader,
353
this.viewModel,
354
this._cellHeaderContainer,
355
this.notebookEditor,
356
{
357
updateInfoRendering: () => renderSourceEditor(),
358
checkIfModified: () => {
359
return this.viewModel.originalMetadata.getHash() !== this.viewModel.modifiedMetadata.getHash() ? { reason: undefined } : false;
360
},
361
getFoldingState: () => this.viewModel.cellFoldingState,
362
updateFoldingState: (state) => this.viewModel.cellFoldingState = state,
363
unChangedLabel: 'Notebook Metadata',
364
changedLabel: 'Notebook Metadata changed',
365
prefix: 'metadata',
366
menuId: MenuId.NotebookDiffDocumentMetadata
367
}
368
));
369
this._cellHeader.buildHeader();
370
renderSourceEditor();
371
372
const scopedContextKeyService = this.contextKeyService.createScoped(this.templateData.inputToolbarContainer);
373
this._register(scopedContextKeyService);
374
const inputChanged = NOTEBOOK_DIFF_METADATA.bindTo(scopedContextKeyService);
375
inputChanged.set(this.viewModel.originalMetadata.getHash() !== this.viewModel.modifiedMetadata.getHash());
376
377
this._toolbar = this.templateData.toolbar;
378
379
this._toolbar.context = this.viewModel;
380
381
const refreshToolbar = () => {
382
const hasChanges = this.viewModel.originalMetadata.getHash() !== this.viewModel.modifiedMetadata.getHash();
383
inputChanged.set(hasChanges);
384
385
if (hasChanges) {
386
const menu = this.menuService.getMenuActions(MenuId.NotebookDiffDocumentMetadata, scopedContextKeyService, { shouldForwardArgs: true });
387
const actions = getFlatActionBarActions(menu);
388
this._toolbar.setActions(actions);
389
} else {
390
this._toolbar.setActions([]);
391
}
392
};
393
394
this._register(this.viewModel.modifiedMetadata.onDidChange(() => {
395
refreshToolbar();
396
}));
397
refreshToolbar();
398
}
399
400
private async _initializeSourceDiffEditor() {
401
const [originalRef, modifiedRef] = await Promise.all([
402
this.textModelService.createModelReference(this.viewModel.originalMetadata.uri),
403
this.textModelService.createModelReference(this.viewModel.modifiedMetadata.uri)]);
404
405
if (this._store.isDisposed) {
406
originalRef.dispose();
407
modifiedRef.dispose();
408
return;
409
}
410
411
this._register(originalRef);
412
this._register(modifiedRef);
413
414
const vm = this._register(this._editor.createViewModel({
415
original: originalRef.object.textEditorModel,
416
modified: modifiedRef.object.textEditorModel,
417
}));
418
419
// Reduces flicker (compute this before setting the model)
420
// Else when the model is set, the height of the editor will be x, after diff is computed, then height will be y.
421
// & that results in flicker.
422
await vm.waitForDiff();
423
this._editor.setModel(vm);
424
425
const handleViewStateChange = () => {
426
this._editorViewStateChanged = true;
427
};
428
429
const handleScrollChange = (e: editorCommon.IScrollEvent) => {
430
if (e.scrollTopChanged || e.scrollLeftChanged) {
431
this._editorViewStateChanged = true;
432
}
433
};
434
435
this.updateEditorOptionsForWhitespace();
436
this._register(this._editor.getOriginalEditor().onDidChangeCursorSelection(handleViewStateChange));
437
this._register(this._editor.getOriginalEditor().onDidScrollChange(handleScrollChange));
438
this._register(this._editor.getModifiedEditor().onDidChangeCursorSelection(handleViewStateChange));
439
this._register(this._editor.getModifiedEditor().onDidScrollChange(handleScrollChange));
440
441
const editorViewState = this.viewModel.getSourceEditorViewState() as editorCommon.IDiffEditorViewState | null;
442
if (editorViewState) {
443
this._editor.restoreViewState(editorViewState);
444
}
445
446
const contentHeight = this._editor.getContentHeight();
447
this.viewModel.editorHeight = contentHeight;
448
}
449
private updateEditorOptionsForWhitespace() {
450
const editor = this._editor;
451
const uri = editor.getModel()?.modified.uri || editor.getModel()?.original.uri;
452
if (!uri) {
453
return;
454
}
455
const ignoreTrimWhitespace = this.textConfigurationService.getValue<boolean>(uri, 'diffEditor.ignoreTrimWhitespace');
456
editor.updateOptions({ ignoreTrimWhitespace });
457
458
this._register(this.textConfigurationService.onDidChangeConfiguration(e => {
459
if (e.affectsConfiguration(uri, 'diffEditor') &&
460
e.affectedKeys.has('diffEditor.ignoreTrimWhitespace')) {
461
const ignoreTrimWhitespace = this.textConfigurationService.getValue<boolean>(uri, 'diffEditor.ignoreTrimWhitespace');
462
editor.updateOptions({ ignoreTrimWhitespace });
463
}
464
}));
465
}
466
layout(state: IDiffElementLayoutState) {
467
DOM.scheduleAtNextAnimationFrame(DOM.getWindow(this._diffEditorContainer), () => {
468
if (state.editorHeight) {
469
this._editorContainer.style.height = `${this.viewModel.layoutInfo.editorHeight}px`;
470
this._editor.layout({
471
width: this._editor.getViewWidth(),
472
height: this.viewModel.layoutInfo.editorHeight
473
});
474
}
475
476
if (state.outerWidth) {
477
this._editorContainer.style.height = `${this.viewModel.layoutInfo.editorHeight}px`;
478
this._editor.layout();
479
}
480
481
this.layoutNotebookCell();
482
});
483
}
484
485
override dispose() {
486
this._editor.setModel(null);
487
488
if (this._editorViewStateChanged) {
489
this.viewModel.saveSpirceEditorViewState(this._editor.saveViewState());
490
}
491
492
super.dispose();
493
}
494
}
495
496
497
abstract class AbstractElementRenderer extends Disposable {
498
protected readonly _metadataLocalDisposable = this._register(new DisposableStore());
499
protected readonly _outputLocalDisposable = this._register(new DisposableStore());
500
protected _ignoreMetadata: boolean = false;
501
protected _ignoreOutputs: boolean = false;
502
protected _cellHeaderContainer!: HTMLElement;
503
protected _editorContainer!: HTMLElement;
504
protected _cellHeader!: PropertyHeader;
505
protected _metadataHeaderContainer!: HTMLElement;
506
protected _metadataHeader!: PropertyHeader;
507
protected _metadataInfoContainer!: HTMLElement;
508
protected _metadataEditorContainer?: HTMLElement;
509
protected readonly _metadataEditorDisposeStore!: DisposableStore;
510
protected _metadataEditor?: CodeEditorWidget | DiffEditorWidget;
511
512
protected _outputHeaderContainer!: HTMLElement;
513
protected _outputHeader!: PropertyHeader;
514
protected _outputInfoContainer!: HTMLElement;
515
protected _outputEditorContainer?: HTMLElement;
516
protected _outputViewContainer?: HTMLElement;
517
protected _outputLeftContainer?: HTMLElement;
518
protected _outputRightContainer?: HTMLElement;
519
protected _outputMetadataContainer?: HTMLElement;
520
protected _outputEmptyElement?: HTMLElement;
521
protected _outputLeftView?: OutputContainer;
522
protected _outputRightView?: OutputContainer;
523
protected readonly _outputEditorDisposeStore!: DisposableStore;
524
protected _outputEditor?: CodeEditorWidget | DiffEditorWidget;
525
protected _outputMetadataEditor?: DiffEditorWidget;
526
527
protected _diffEditorContainer!: HTMLElement;
528
protected _diagonalFill?: HTMLElement;
529
protected _isDisposed: boolean;
530
531
constructor(
532
readonly notebookEditor: INotebookTextDiffEditor,
533
readonly cell: DiffElementCellViewModelBase,
534
readonly templateData: CellDiffSingleSideRenderTemplate | CellDiffSideBySideRenderTemplate,
535
readonly style: 'left' | 'right' | 'full',
536
protected readonly instantiationService: IInstantiationService,
537
protected readonly languageService: ILanguageService,
538
protected readonly modelService: IModelService,
539
protected readonly textModelService: ITextModelService,
540
protected readonly contextMenuService: IContextMenuService,
541
protected readonly keybindingService: IKeybindingService,
542
protected readonly notificationService: INotificationService,
543
protected readonly menuService: IMenuService,
544
protected readonly contextKeyService: IContextKeyService,
545
protected readonly configurationService: IConfigurationService,
546
protected readonly textConfigurationService: ITextResourceConfigurationService
547
) {
548
super();
549
// init
550
this._isDisposed = false;
551
this._metadataEditorDisposeStore = this._register(new DisposableStore());
552
this._outputEditorDisposeStore = this._register(new DisposableStore());
553
this._register(cell.onDidLayoutChange(e => {
554
this.layout(e);
555
}));
556
this._register(cell.onDidLayoutChange(e => this.updateBorders()));
557
this.init();
558
this.buildBody();
559
560
this._register(cell.onDidStateChange(() => {
561
this.updateOutputRendering(this.cell.renderOutput);
562
}));
563
}
564
565
abstract init(): void;
566
abstract styleContainer(container: HTMLElement): void;
567
abstract _buildOutput(): void;
568
abstract _disposeOutput(): void;
569
abstract _buildMetadata(): void;
570
abstract _disposeMetadata(): void;
571
572
buildBody(): void {
573
const body = this.templateData.body;
574
this._diffEditorContainer = this.templateData.diffEditorContainer;
575
body.classList.remove('left', 'right', 'full');
576
switch (this.style) {
577
case 'left':
578
body.classList.add('left');
579
break;
580
case 'right':
581
body.classList.add('right');
582
break;
583
default:
584
body.classList.add('full');
585
break;
586
}
587
588
this.styleContainer(this._diffEditorContainer);
589
this.updateSourceEditor();
590
if (this.cell.modified) {
591
this._register(this.cell.modified.textModel.onDidChangeContent(() => this._cellHeader.refresh()));
592
}
593
594
this._ignoreMetadata = this.configurationService.getValue('notebook.diff.ignoreMetadata');
595
if (this._ignoreMetadata) {
596
this._disposeMetadata();
597
} else {
598
this._buildMetadata();
599
}
600
601
this._ignoreOutputs = this.configurationService.getValue<boolean>('notebook.diff.ignoreOutputs') || !!(this.notebookEditor.textModel?.transientOptions.transientOutputs);
602
if (this._ignoreOutputs) {
603
this._disposeOutput();
604
} else {
605
this._buildOutput();
606
}
607
608
this._register(this.configurationService.onDidChangeConfiguration(e => {
609
let metadataLayoutChange = false;
610
let outputLayoutChange = false;
611
if (e.affectsConfiguration('notebook.diff.ignoreMetadata')) {
612
const newValue = this.configurationService.getValue<boolean>('notebook.diff.ignoreMetadata');
613
614
if (newValue !== undefined && this._ignoreMetadata !== newValue) {
615
this._ignoreMetadata = newValue;
616
617
this._metadataLocalDisposable.clear();
618
if (this.configurationService.getValue('notebook.diff.ignoreMetadata')) {
619
this._disposeMetadata();
620
} else {
621
this.cell.metadataStatusHeight = 25;
622
this._buildMetadata();
623
this.updateMetadataRendering();
624
metadataLayoutChange = true;
625
}
626
}
627
}
628
629
if (e.affectsConfiguration('notebook.diff.ignoreOutputs')) {
630
const newValue = this.configurationService.getValue<boolean>('notebook.diff.ignoreOutputs');
631
632
if (newValue !== undefined && this._ignoreOutputs !== (newValue || this.notebookEditor.textModel?.transientOptions.transientOutputs)) {
633
this._ignoreOutputs = newValue || !!(this.notebookEditor.textModel?.transientOptions.transientOutputs);
634
635
this._outputLocalDisposable.clear();
636
if (this._ignoreOutputs) {
637
this._disposeOutput();
638
this.cell.layoutChange();
639
} else {
640
this.cell.outputStatusHeight = 25;
641
this._buildOutput();
642
outputLayoutChange = true;
643
}
644
}
645
}
646
647
if (metadataLayoutChange || outputLayoutChange) {
648
this.layout({ metadataHeight: metadataLayoutChange, outputTotalHeight: outputLayoutChange });
649
}
650
}));
651
}
652
653
updateMetadataRendering() {
654
if (this.cell.metadataFoldingState === PropertyFoldingState.Expanded) {
655
// we should expand the metadata editor
656
this._metadataInfoContainer.style.display = 'block';
657
658
if (!this._metadataEditorContainer || !this._metadataEditor) {
659
// create editor
660
this._metadataEditorContainer = DOM.append(this._metadataInfoContainer, DOM.$('.metadata-editor-container'));
661
this._buildMetadataEditor();
662
} else {
663
this.cell.metadataHeight = this._metadataEditor.getContentHeight();
664
}
665
} else {
666
// we should collapse the metadata editor
667
this._metadataInfoContainer.style.display = 'none';
668
// this._metadataEditorDisposeStore.clear();
669
this.cell.metadataHeight = 0;
670
}
671
}
672
673
updateOutputRendering(renderRichOutput: boolean) {
674
if (this.cell.outputFoldingState === PropertyFoldingState.Expanded) {
675
this._outputInfoContainer.style.display = 'block';
676
if (renderRichOutput) {
677
this._hideOutputsRaw();
678
this._buildOutputRendererContainer();
679
this._showOutputsRenderer();
680
this._showOutputsEmptyView();
681
} else {
682
this._hideOutputsRenderer();
683
this._buildOutputRawContainer();
684
this._showOutputsRaw();
685
}
686
} else {
687
this._outputInfoContainer.style.display = 'none';
688
689
this._hideOutputsRaw();
690
this._hideOutputsRenderer();
691
this._hideOutputsEmptyView();
692
}
693
}
694
695
private _buildOutputRawContainer() {
696
if (!this._outputEditorContainer) {
697
this._outputEditorContainer = DOM.append(this._outputInfoContainer, DOM.$('.output-editor-container'));
698
this._buildOutputEditor();
699
}
700
}
701
702
private _showOutputsRaw() {
703
if (this._outputEditorContainer) {
704
this._outputEditorContainer.style.display = 'block';
705
this.cell.rawOutputHeight = this._outputEditor!.getContentHeight();
706
}
707
}
708
709
private _showOutputsEmptyView() {
710
this.cell.layoutChange();
711
}
712
713
protected _hideOutputsRaw() {
714
if (this._outputEditorContainer) {
715
this._outputEditorContainer.style.display = 'none';
716
this.cell.rawOutputHeight = 0;
717
}
718
}
719
720
protected _hideOutputsEmptyView() {
721
this.cell.layoutChange();
722
}
723
724
abstract _buildOutputRendererContainer(): void;
725
abstract _hideOutputsRenderer(): void;
726
abstract _showOutputsRenderer(): void;
727
728
private _applySanitizedMetadataChanges(currentMetadata: NotebookCellMetadata, newMetadata: any) {
729
const result: { [key: string]: any } = {};
730
try {
731
const newMetadataObj = JSON.parse(newMetadata);
732
const keys = new Set([...Object.keys(newMetadataObj)]);
733
for (const key of keys) {
734
switch (key as keyof NotebookCellMetadata) {
735
case 'inputCollapsed':
736
case 'outputCollapsed':
737
// boolean
738
if (typeof newMetadataObj[key] === 'boolean') {
739
result[key] = newMetadataObj[key];
740
} else {
741
result[key] = currentMetadata[key as keyof NotebookCellMetadata];
742
}
743
break;
744
745
default:
746
result[key] = newMetadataObj[key];
747
break;
748
}
749
}
750
751
const index = this.notebookEditor.textModel!.cells.indexOf(this.cell.modified!.textModel);
752
753
if (index < 0) {
754
return;
755
}
756
757
this.notebookEditor.textModel!.applyEdits([
758
{ editType: CellEditType.Metadata, index, metadata: result }
759
], true, undefined, () => undefined, undefined, true);
760
} catch {
761
}
762
}
763
764
private async _buildMetadataEditor() {
765
this._metadataEditorDisposeStore.clear();
766
767
if (this.cell instanceof SideBySideDiffElementViewModel) {
768
this._metadataEditor = this.instantiationService.createInstance(DiffEditorWidget, this._metadataEditorContainer!, {
769
...fixedDiffEditorOptions,
770
overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode(),
771
readOnly: false,
772
originalEditable: false,
773
ignoreTrimWhitespace: false,
774
automaticLayout: false,
775
dimension: {
776
height: this.cell.layoutInfo.metadataHeight,
777
width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), true, true)
778
}
779
}, {
780
originalEditor: getOptimizedNestedCodeEditorWidgetOptions(),
781
modifiedEditor: getOptimizedNestedCodeEditorWidgetOptions()
782
});
783
784
const unchangedRegions = this._register(getUnchangedRegionSettings(this.configurationService));
785
if (unchangedRegions.options.enabled) {
786
this._metadataEditor.updateOptions({ hideUnchangedRegions: unchangedRegions.options });
787
}
788
this._metadataEditorDisposeStore.add(unchangedRegions.onDidChangeEnablement(() => {
789
if (this._metadataEditor) {
790
this._metadataEditor.updateOptions({ hideUnchangedRegions: unchangedRegions.options });
791
}
792
}));
793
794
795
this.layout({ metadataHeight: true });
796
this._metadataEditorDisposeStore.add(this._metadataEditor);
797
798
this._metadataEditorContainer?.classList.add('diff');
799
800
const [originalMetadataModel, modifiedMetadataModel] = await Promise.all([
801
this.textModelService.createModelReference(CellUri.generateCellPropertyUri(this.cell.originalDocument.uri, this.cell.original.handle, Schemas.vscodeNotebookCellMetadata)),
802
this.textModelService.createModelReference(CellUri.generateCellPropertyUri(this.cell.modifiedDocument.uri, this.cell.modified.handle, Schemas.vscodeNotebookCellMetadata))
803
]);
804
805
if (this._isDisposed) {
806
originalMetadataModel.dispose();
807
modifiedMetadataModel.dispose();
808
return;
809
}
810
811
this._metadataEditorDisposeStore.add(originalMetadataModel);
812
this._metadataEditorDisposeStore.add(modifiedMetadataModel);
813
const vm = this._metadataEditor.createViewModel({
814
original: originalMetadataModel.object.textEditorModel,
815
modified: modifiedMetadataModel.object.textEditorModel
816
});
817
this._metadataEditor.setModel(vm);
818
// Reduces flicker (compute this before setting the model)
819
// Else when the model is set, the height of the editor will be x, after diff is computed, then height will be y.
820
// & that results in flicker.
821
await vm.waitForDiff();
822
823
if (this._isDisposed) {
824
return;
825
}
826
827
this.cell.metadataHeight = this._metadataEditor.getContentHeight();
828
829
this._metadataEditorDisposeStore.add(this._metadataEditor.onDidContentSizeChange((e) => {
830
if (e.contentHeightChanged && this.cell.metadataFoldingState === PropertyFoldingState.Expanded) {
831
this.cell.metadataHeight = e.contentHeight;
832
}
833
}));
834
835
let respondingToContentChange = false;
836
837
this._metadataEditorDisposeStore.add(modifiedMetadataModel.object.textEditorModel.onDidChangeContent(() => {
838
respondingToContentChange = true;
839
const value = modifiedMetadataModel.object.textEditorModel.getValue();
840
this._applySanitizedMetadataChanges(this.cell.modified!.metadata, value);
841
this._metadataHeader.refresh();
842
respondingToContentChange = false;
843
}));
844
845
this._metadataEditorDisposeStore.add(this.cell.modified.textModel.onDidChangeMetadata(() => {
846
if (respondingToContentChange) {
847
return;
848
}
849
850
const modifiedMetadataSource = getFormattedMetadataJSON(this.notebookEditor.textModel?.transientOptions.transientCellMetadata, this.cell.modified?.metadata || {}, this.cell.modified?.language, true);
851
modifiedMetadataModel.object.textEditorModel.setValue(modifiedMetadataSource);
852
}));
853
854
return;
855
} else {
856
this._metadataEditor = this.instantiationService.createInstance(CodeEditorWidget, this._metadataEditorContainer!, {
857
...fixedEditorOptions,
858
dimension: {
859
width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, true),
860
height: this.cell.layoutInfo.metadataHeight
861
},
862
overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode(),
863
readOnly: false,
864
allowVariableLineHeights: false
865
}, {});
866
this.layout({ metadataHeight: true });
867
this._metadataEditorDisposeStore.add(this._metadataEditor);
868
869
const mode = this.languageService.createById('jsonc');
870
const originalMetadataSource = getFormattedMetadataJSON(this.notebookEditor.textModel?.transientOptions.transientCellMetadata,
871
this.cell.type === 'insert'
872
? this.cell.modified!.metadata || {}
873
: this.cell.original!.metadata || {}, undefined, true);
874
const uri = this.cell.type === 'insert'
875
? this.cell.modified!.uri
876
: this.cell.original!.uri;
877
const handle = this.cell.type === 'insert'
878
? this.cell.modified!.handle
879
: this.cell.original!.handle;
880
881
const modelUri = CellUri.generateCellPropertyUri(uri, handle, Schemas.vscodeNotebookCellMetadata);
882
const metadataModel = this.modelService.createModel(originalMetadataSource, mode, modelUri, false);
883
this._metadataEditor.setModel(metadataModel);
884
this._metadataEditorDisposeStore.add(metadataModel);
885
886
this.cell.metadataHeight = this._metadataEditor.getContentHeight();
887
888
this._metadataEditorDisposeStore.add(this._metadataEditor.onDidContentSizeChange((e) => {
889
if (e.contentHeightChanged && this.cell.metadataFoldingState === PropertyFoldingState.Expanded) {
890
this.cell.metadataHeight = e.contentHeight;
891
}
892
}));
893
}
894
}
895
896
private _buildOutputEditor() {
897
this._outputEditorDisposeStore.clear();
898
899
if ((this.cell.type === 'modified' || this.cell.type === 'unchanged') && !this.notebookEditor.textModel!.transientOptions.transientOutputs) {
900
const originalOutputsSource = getFormattedOutputJSON(this.cell.original?.outputs || []);
901
const modifiedOutputsSource = getFormattedOutputJSON(this.cell.modified?.outputs || []);
902
if (originalOutputsSource !== modifiedOutputsSource) {
903
const mode = this.languageService.createById('json');
904
const originalModel = this.modelService.createModel(originalOutputsSource, mode, undefined, true);
905
const modifiedModel = this.modelService.createModel(modifiedOutputsSource, mode, undefined, true);
906
this._outputEditorDisposeStore.add(originalModel);
907
this._outputEditorDisposeStore.add(modifiedModel);
908
909
const lineHeight = this.notebookEditor.getLayoutInfo().fontInfo.lineHeight || 17;
910
const lineCount = Math.max(originalModel.getLineCount(), modifiedModel.getLineCount());
911
this._outputEditor = this.instantiationService.createInstance(DiffEditorWidget, this._outputEditorContainer!, {
912
...fixedDiffEditorOptions,
913
overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode(),
914
readOnly: true,
915
ignoreTrimWhitespace: false,
916
automaticLayout: false,
917
dimension: {
918
height: Math.min(OUTPUT_EDITOR_HEIGHT_MAGIC, this.cell.layoutInfo.rawOutputHeight || lineHeight * lineCount),
919
width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, true)
920
},
921
accessibilityVerbose: this.configurationService.getValue<boolean>(AccessibilityVerbositySettingId.DiffEditor) ?? false
922
}, {
923
originalEditor: getOptimizedNestedCodeEditorWidgetOptions(),
924
modifiedEditor: getOptimizedNestedCodeEditorWidgetOptions()
925
});
926
this._outputEditorDisposeStore.add(this._outputEditor);
927
928
this._outputEditorContainer?.classList.add('diff');
929
930
this._outputEditor.setModel({
931
original: originalModel,
932
modified: modifiedModel
933
});
934
this._outputEditor.restoreViewState(this.cell.getOutputEditorViewState() as editorCommon.IDiffEditorViewState);
935
936
this.cell.rawOutputHeight = this._outputEditor.getContentHeight();
937
938
this._outputEditorDisposeStore.add(this._outputEditor.onDidContentSizeChange((e) => {
939
if (e.contentHeightChanged && this.cell.outputFoldingState === PropertyFoldingState.Expanded) {
940
this.cell.rawOutputHeight = e.contentHeight;
941
}
942
}));
943
944
this._outputEditorDisposeStore.add(this.cell.modified!.textModel.onDidChangeOutputs(() => {
945
const modifiedOutputsSource = getFormattedOutputJSON(this.cell.modified?.outputs || []);
946
modifiedModel.setValue(modifiedOutputsSource);
947
this._outputHeader.refresh();
948
}));
949
950
return;
951
}
952
}
953
954
this._outputEditor = this.instantiationService.createInstance(CodeEditorWidget, this._outputEditorContainer!, {
955
...fixedEditorOptions,
956
dimension: {
957
width: Math.min(OUTPUT_EDITOR_HEIGHT_MAGIC, this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, this.cell.type === 'unchanged' || this.cell.type === 'modified') - 32),
958
height: this.cell.layoutInfo.rawOutputHeight
959
},
960
overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode(),
961
allowVariableLineHeights: false
962
}, {});
963
this._outputEditorDisposeStore.add(this._outputEditor);
964
965
const mode = this.languageService.createById('json');
966
const originaloutputSource = getFormattedOutputJSON(
967
this.notebookEditor.textModel!.transientOptions.transientOutputs
968
? []
969
: this.cell.type === 'insert'
970
? this.cell.modified?.outputs || []
971
: this.cell.original?.outputs || []);
972
const outputModel = this.modelService.createModel(originaloutputSource, mode, undefined, true);
973
this._outputEditorDisposeStore.add(outputModel);
974
this._outputEditor.setModel(outputModel);
975
this._outputEditor.restoreViewState(this.cell.getOutputEditorViewState());
976
977
this.cell.rawOutputHeight = this._outputEditor.getContentHeight();
978
979
this._outputEditorDisposeStore.add(this._outputEditor.onDidContentSizeChange((e) => {
980
if (e.contentHeightChanged && this.cell.outputFoldingState === PropertyFoldingState.Expanded) {
981
this.cell.rawOutputHeight = e.contentHeight;
982
}
983
}));
984
}
985
986
protected layoutNotebookCell() {
987
this.notebookEditor.layoutNotebookCell(
988
this.cell,
989
this.cell.layoutInfo.totalHeight
990
);
991
}
992
993
updateBorders() {
994
this.templateData.leftBorder.style.height = `${this.cell.layoutInfo.totalHeight - 32}px`;
995
this.templateData.rightBorder.style.height = `${this.cell.layoutInfo.totalHeight - 32}px`;
996
this.templateData.bottomBorder.style.top = `${this.cell.layoutInfo.totalHeight - 32}px`;
997
}
998
999
override dispose() {
1000
if (this._outputEditor) {
1001
this.cell.saveOutputEditorViewState(this._outputEditor.saveViewState());
1002
}
1003
1004
if (this._metadataEditor) {
1005
this.cell.saveMetadataEditorViewState(this._metadataEditor.saveViewState());
1006
}
1007
1008
this._metadataEditorDisposeStore.dispose();
1009
this._outputEditorDisposeStore.dispose();
1010
1011
this._isDisposed = true;
1012
super.dispose();
1013
}
1014
1015
abstract updateSourceEditor(): void;
1016
abstract layout(state: IDiffElementLayoutState): void;
1017
}
1018
1019
abstract class SingleSideDiffElement extends AbstractElementRenderer {
1020
protected _editor!: CodeEditorWidget;
1021
override readonly cell: SingleSideDiffElementViewModel;
1022
override readonly templateData: CellDiffSingleSideRenderTemplate;
1023
abstract get nestedCellViewModel(): DiffNestedCellViewModel;
1024
abstract get readonly(): boolean;
1025
constructor(
1026
notebookEditor: INotebookTextDiffEditor,
1027
cell: SingleSideDiffElementViewModel,
1028
templateData: CellDiffSingleSideRenderTemplate,
1029
style: 'left' | 'right' | 'full',
1030
instantiationService: IInstantiationService,
1031
languageService: ILanguageService,
1032
modelService: IModelService,
1033
textModelService: ITextModelService,
1034
contextMenuService: IContextMenuService,
1035
keybindingService: IKeybindingService,
1036
notificationService: INotificationService,
1037
menuService: IMenuService,
1038
contextKeyService: IContextKeyService,
1039
configurationService: IConfigurationService,
1040
textConfigurationService: ITextResourceConfigurationService
1041
) {
1042
super(
1043
notebookEditor,
1044
cell,
1045
templateData,
1046
style,
1047
instantiationService,
1048
languageService,
1049
modelService,
1050
textModelService,
1051
contextMenuService,
1052
keybindingService,
1053
notificationService,
1054
menuService,
1055
contextKeyService,
1056
configurationService,
1057
textConfigurationService
1058
);
1059
this.cell = cell;
1060
this.templateData = templateData;
1061
1062
this.updateBorders();
1063
}
1064
1065
init() {
1066
this._diagonalFill = this.templateData.diagonalFill;
1067
}
1068
1069
override buildBody() {
1070
const body = this.templateData.body;
1071
this._diffEditorContainer = this.templateData.diffEditorContainer;
1072
body.classList.remove('left', 'right', 'full');
1073
switch (this.style) {
1074
case 'left':
1075
body.classList.add('left');
1076
break;
1077
case 'right':
1078
body.classList.add('right');
1079
break;
1080
default:
1081
body.classList.add('full');
1082
break;
1083
}
1084
1085
this.styleContainer(this._diffEditorContainer);
1086
this.updateSourceEditor();
1087
1088
if (this.configurationService.getValue('notebook.diff.ignoreMetadata')) {
1089
this._disposeMetadata();
1090
} else {
1091
this._buildMetadata();
1092
}
1093
1094
if (this.configurationService.getValue('notebook.diff.ignoreOutputs') || this.notebookEditor.textModel?.transientOptions.transientOutputs) {
1095
this._disposeOutput();
1096
} else {
1097
this._buildOutput();
1098
}
1099
1100
this._register(this.configurationService.onDidChangeConfiguration(e => {
1101
let metadataLayoutChange = false;
1102
let outputLayoutChange = false;
1103
if (e.affectsConfiguration('notebook.diff.ignoreMetadata')) {
1104
this._metadataLocalDisposable.clear();
1105
if (this.configurationService.getValue('notebook.diff.ignoreMetadata')) {
1106
this._disposeMetadata();
1107
} else {
1108
this.cell.metadataStatusHeight = 25;
1109
this._buildMetadata();
1110
this.updateMetadataRendering();
1111
metadataLayoutChange = true;
1112
}
1113
}
1114
1115
if (e.affectsConfiguration('notebook.diff.ignoreOutputs')) {
1116
this._outputLocalDisposable.clear();
1117
if (this.configurationService.getValue('notebook.diff.ignoreOutputs') || this.notebookEditor.textModel?.transientOptions.transientOutputs) {
1118
this._disposeOutput();
1119
} else {
1120
this.cell.outputStatusHeight = 25;
1121
this._buildOutput();
1122
outputLayoutChange = true;
1123
}
1124
}
1125
1126
if (metadataLayoutChange || outputLayoutChange) {
1127
this.layout({ metadataHeight: metadataLayoutChange, outputTotalHeight: outputLayoutChange });
1128
}
1129
}));
1130
}
1131
1132
override updateSourceEditor(): void {
1133
this._cellHeaderContainer = this.templateData.cellHeaderContainer;
1134
this._cellHeaderContainer.style.display = 'flex';
1135
this._cellHeaderContainer.innerText = '';
1136
this._editorContainer = this.templateData.editorContainer;
1137
this._editorContainer.classList.add('diff');
1138
1139
const renderSourceEditor = () => {
1140
if (this.cell.cellFoldingState === PropertyFoldingState.Collapsed) {
1141
this._editorContainer.style.display = 'none';
1142
this.cell.editorHeight = 0;
1143
return;
1144
}
1145
const lineHeight = this.notebookEditor.getLayoutInfo().fontInfo.lineHeight || 17;
1146
const editorHeight = this.cell.computeInputEditorHeight(lineHeight);
1147
1148
this._editorContainer.style.height = `${editorHeight}px`;
1149
this._editorContainer.style.display = 'block';
1150
1151
if (this._editor) {
1152
const contentHeight = this._editor.getContentHeight();
1153
if (contentHeight >= 0) {
1154
this.cell.editorHeight = contentHeight;
1155
}
1156
return;
1157
}
1158
1159
this._editor = this.templateData.sourceEditor;
1160
this._editor.layout(
1161
{
1162
width: (this.notebookEditor.getLayoutInfo().width - 2 * DIFF_CELL_MARGIN) / 2 - 18,
1163
height: editorHeight
1164
}
1165
);
1166
this._editor.updateOptions({ readOnly: this.readonly });
1167
this.cell.editorHeight = editorHeight;
1168
1169
this._register(this._editor.onDidContentSizeChange((e) => {
1170
if (this.cell.cellFoldingState === PropertyFoldingState.Expanded && e.contentHeightChanged && this.cell.layoutInfo.editorHeight !== e.contentHeight) {
1171
this.cell.editorHeight = e.contentHeight;
1172
}
1173
}));
1174
this._initializeSourceDiffEditor(this.nestedCellViewModel);
1175
};
1176
1177
this._cellHeader = this._register(this.instantiationService.createInstance(
1178
PropertyHeader,
1179
this.cell,
1180
this._cellHeaderContainer,
1181
this.notebookEditor,
1182
{
1183
updateInfoRendering: () => renderSourceEditor(),
1184
checkIfModified: () => ({ reason: undefined }),
1185
getFoldingState: () => this.cell.cellFoldingState,
1186
updateFoldingState: (state) => this.cell.cellFoldingState = state,
1187
unChangedLabel: 'Input',
1188
changedLabel: 'Input',
1189
prefix: 'input',
1190
menuId: MenuId.NotebookDiffCellInputTitle
1191
}
1192
));
1193
this._cellHeader.buildHeader();
1194
renderSourceEditor();
1195
1196
this._initializeSourceDiffEditor(this.nestedCellViewModel);
1197
}
1198
protected calculateDiagonalFillHeight() {
1199
return this.cell.layoutInfo.cellStatusHeight + this.cell.layoutInfo.editorHeight + this.cell.layoutInfo.editorMargin + this.cell.layoutInfo.metadataStatusHeight + this.cell.layoutInfo.metadataHeight + this.cell.layoutInfo.outputTotalHeight + this.cell.layoutInfo.outputStatusHeight;
1200
}
1201
1202
private async _initializeSourceDiffEditor(modifiedCell: DiffNestedCellViewModel) {
1203
const modifiedRef = await this.textModelService.createModelReference(modifiedCell.uri);
1204
1205
if (this._isDisposed) {
1206
return;
1207
}
1208
1209
const modifiedTextModel = modifiedRef.object.textEditorModel;
1210
this._register(modifiedRef);
1211
1212
this._editor!.setModel(modifiedTextModel);
1213
1214
const editorViewState = this.cell.getSourceEditorViewState() as editorCommon.IDiffEditorViewState | null;
1215
if (editorViewState) {
1216
this._editor!.restoreViewState(editorViewState);
1217
}
1218
1219
const contentHeight = this._editor!.getContentHeight();
1220
this.cell.editorHeight = contentHeight;
1221
const height = `${this.calculateDiagonalFillHeight()}px`;
1222
if (this._diagonalFill!.style.height !== height) {
1223
this._diagonalFill!.style.height = height;
1224
}
1225
}
1226
1227
_disposeMetadata() {
1228
this.cell.metadataStatusHeight = 0;
1229
this.cell.metadataHeight = 0;
1230
this.templateData.cellHeaderContainer.style.display = 'none';
1231
this.templateData.metadataHeaderContainer.style.display = 'none';
1232
this.templateData.metadataInfoContainer.style.display = 'none';
1233
this._metadataEditor = undefined;
1234
}
1235
1236
_buildMetadata() {
1237
this._metadataHeaderContainer = this.templateData.metadataHeaderContainer;
1238
this._metadataInfoContainer = this.templateData.metadataInfoContainer;
1239
this._metadataHeaderContainer.style.display = 'flex';
1240
this._metadataInfoContainer.style.display = 'block';
1241
this._metadataHeaderContainer.innerText = '';
1242
this._metadataInfoContainer.innerText = '';
1243
1244
this._metadataHeader = this.instantiationService.createInstance(
1245
PropertyHeader,
1246
this.cell,
1247
this._metadataHeaderContainer,
1248
this.notebookEditor,
1249
{
1250
updateInfoRendering: this.updateMetadataRendering.bind(this),
1251
checkIfModified: () => {
1252
return this.cell.checkMetadataIfModified();
1253
},
1254
getFoldingState: () => {
1255
return this.cell.metadataFoldingState;
1256
},
1257
updateFoldingState: (state) => {
1258
this.cell.metadataFoldingState = state;
1259
},
1260
unChangedLabel: 'Metadata',
1261
changedLabel: 'Metadata changed',
1262
prefix: 'metadata',
1263
menuId: MenuId.NotebookDiffCellMetadataTitle
1264
}
1265
);
1266
this._metadataLocalDisposable.add(this._metadataHeader);
1267
this._metadataHeader.buildHeader();
1268
}
1269
1270
_buildOutput() {
1271
this.templateData.outputHeaderContainer.style.display = 'flex';
1272
this.templateData.outputInfoContainer.style.display = 'block';
1273
1274
this._outputHeaderContainer = this.templateData.outputHeaderContainer;
1275
this._outputInfoContainer = this.templateData.outputInfoContainer;
1276
1277
this._outputHeaderContainer.innerText = '';
1278
this._outputInfoContainer.innerText = '';
1279
1280
this._outputHeader = this.instantiationService.createInstance(
1281
PropertyHeader,
1282
this.cell,
1283
this._outputHeaderContainer,
1284
this.notebookEditor,
1285
{
1286
updateInfoRendering: this.updateOutputRendering.bind(this),
1287
checkIfModified: () => {
1288
return this.cell.checkIfOutputsModified();
1289
},
1290
getFoldingState: () => {
1291
return this.cell.outputFoldingState;
1292
},
1293
updateFoldingState: (state) => {
1294
this.cell.outputFoldingState = state;
1295
},
1296
unChangedLabel: 'Outputs',
1297
changedLabel: 'Outputs changed',
1298
prefix: 'output',
1299
menuId: MenuId.NotebookDiffCellOutputsTitle
1300
}
1301
);
1302
this._outputLocalDisposable.add(this._outputHeader);
1303
this._outputHeader.buildHeader();
1304
}
1305
1306
_disposeOutput() {
1307
this._hideOutputsRaw();
1308
this._hideOutputsRenderer();
1309
this._hideOutputsEmptyView();
1310
1311
this.cell.rawOutputHeight = 0;
1312
this.cell.outputMetadataHeight = 0;
1313
this.cell.outputStatusHeight = 0;
1314
this.templateData.outputHeaderContainer.style.display = 'none';
1315
this.templateData.outputInfoContainer.style.display = 'none';
1316
this._outputViewContainer = undefined;
1317
}
1318
}
1319
export class DeletedElement extends SingleSideDiffElement {
1320
constructor(
1321
notebookEditor: INotebookTextDiffEditor,
1322
cell: SingleSideDiffElementViewModel,
1323
templateData: CellDiffSingleSideRenderTemplate,
1324
@ILanguageService languageService: ILanguageService,
1325
@IModelService modelService: IModelService,
1326
@ITextModelService textModelService: ITextModelService,
1327
@IInstantiationService instantiationService: IInstantiationService,
1328
@IContextMenuService contextMenuService: IContextMenuService,
1329
@IKeybindingService keybindingService: IKeybindingService,
1330
@INotificationService notificationService: INotificationService,
1331
@IMenuService menuService: IMenuService,
1332
@IContextKeyService contextKeyService: IContextKeyService,
1333
@IConfigurationService configurationService: IConfigurationService,
1334
@ITextResourceConfigurationService textConfigurationService: ITextResourceConfigurationService,
1335
) {
1336
super(notebookEditor, cell, templateData, 'left', instantiationService, languageService, modelService, textModelService, contextMenuService, keybindingService, notificationService, menuService, contextKeyService, configurationService, textConfigurationService);
1337
}
1338
1339
get nestedCellViewModel() {
1340
return this.cell.original!;
1341
}
1342
get readonly() {
1343
return true;
1344
}
1345
1346
styleContainer(container: HTMLElement) {
1347
container.classList.remove('inserted');
1348
container.classList.add('removed');
1349
}
1350
1351
layout(state: IDiffElementLayoutState) {
1352
DOM.scheduleAtNextAnimationFrame(DOM.getWindow(this._diffEditorContainer), () => {
1353
if ((state.editorHeight || state.outerWidth) && this._editor) {
1354
this._editorContainer.style.height = `${this.cell.layoutInfo.editorHeight}px`;
1355
this._editor.layout({
1356
width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, false),
1357
height: this.cell.layoutInfo.editorHeight
1358
});
1359
}
1360
1361
if (state.outerWidth && this._editor) {
1362
this._editorContainer.style.height = `${this.cell.layoutInfo.editorHeight}px`;
1363
this._editor.layout();
1364
}
1365
1366
if (state.metadataHeight || state.outerWidth) {
1367
this._metadataEditor?.layout({
1368
width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, false),
1369
height: this.cell.layoutInfo.metadataHeight
1370
});
1371
}
1372
1373
if (state.outputTotalHeight || state.outerWidth) {
1374
this._outputEditor?.layout({
1375
width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, false),
1376
height: this.cell.layoutInfo.outputTotalHeight
1377
});
1378
}
1379
1380
if (this._diagonalFill) {
1381
this._diagonalFill.style.height = `${this.calculateDiagonalFillHeight()}px`;
1382
}
1383
1384
this.layoutNotebookCell();
1385
});
1386
}
1387
1388
1389
_buildOutputRendererContainer() {
1390
if (!this._outputViewContainer) {
1391
this._outputViewContainer = DOM.append(this._outputInfoContainer, DOM.$('.output-view-container'));
1392
this._outputEmptyElement = DOM.append(this._outputViewContainer, DOM.$('.output-empty-view'));
1393
const span = DOM.append(this._outputEmptyElement, DOM.$('span'));
1394
span.innerText = 'No outputs to render';
1395
1396
if (!this.cell.original?.outputs.length) {
1397
this._outputEmptyElement.style.display = 'block';
1398
} else {
1399
this._outputEmptyElement.style.display = 'none';
1400
}
1401
1402
this.cell.layoutChange();
1403
1404
this._outputLeftView = this.instantiationService.createInstance(OutputContainer, this.notebookEditor, this.notebookEditor.textModel!, this.cell, this.cell.original!, DiffSide.Original, this._outputViewContainer);
1405
this._register(this._outputLeftView);
1406
this._outputLeftView.render();
1407
1408
const removedOutputRenderListener = this.notebookEditor.onDidDynamicOutputRendered(e => {
1409
if (e.cell.uri.toString() === this.cell.original!.uri.toString()) {
1410
this.notebookEditor.deltaCellOutputContainerClassNames(DiffSide.Original, this.cell.original!.id, ['nb-cellDeleted'], []);
1411
removedOutputRenderListener.dispose();
1412
}
1413
});
1414
1415
this._register(removedOutputRenderListener);
1416
}
1417
1418
this._outputViewContainer.style.display = 'block';
1419
}
1420
1421
_decorate() {
1422
this.notebookEditor.deltaCellOutputContainerClassNames(DiffSide.Original, this.cell.original!.id, ['nb-cellDeleted'], []);
1423
}
1424
1425
_showOutputsRenderer() {
1426
if (this._outputViewContainer) {
1427
this._outputViewContainer.style.display = 'block';
1428
1429
this._outputLeftView?.showOutputs();
1430
this._decorate();
1431
}
1432
}
1433
1434
_hideOutputsRenderer() {
1435
if (this._outputViewContainer) {
1436
this._outputViewContainer.style.display = 'none';
1437
1438
this._outputLeftView?.hideOutputs();
1439
}
1440
}
1441
1442
override dispose() {
1443
if (this._editor) {
1444
this.cell.saveSpirceEditorViewState(this._editor.saveViewState());
1445
}
1446
1447
super.dispose();
1448
}
1449
}
1450
1451
export class InsertElement extends SingleSideDiffElement {
1452
constructor(
1453
notebookEditor: INotebookTextDiffEditor,
1454
cell: SingleSideDiffElementViewModel,
1455
templateData: CellDiffSingleSideRenderTemplate,
1456
@IInstantiationService instantiationService: IInstantiationService,
1457
@ILanguageService languageService: ILanguageService,
1458
@IModelService modelService: IModelService,
1459
@ITextModelService textModelService: ITextModelService,
1460
@IContextMenuService contextMenuService: IContextMenuService,
1461
@IKeybindingService keybindingService: IKeybindingService,
1462
@INotificationService notificationService: INotificationService,
1463
@IMenuService menuService: IMenuService,
1464
@IContextKeyService contextKeyService: IContextKeyService,
1465
@IConfigurationService configurationService: IConfigurationService,
1466
@ITextResourceConfigurationService textConfigurationService: ITextResourceConfigurationService,
1467
) {
1468
super(notebookEditor, cell, templateData, 'right', instantiationService, languageService, modelService, textModelService, contextMenuService, keybindingService, notificationService, menuService, contextKeyService, configurationService, textConfigurationService);
1469
}
1470
get nestedCellViewModel() {
1471
return this.cell.modified!;
1472
}
1473
get readonly() {
1474
return false;
1475
}
1476
1477
styleContainer(container: HTMLElement): void {
1478
container.classList.remove('removed');
1479
container.classList.add('inserted');
1480
}
1481
1482
_buildOutputRendererContainer() {
1483
if (!this._outputViewContainer) {
1484
this._outputViewContainer = DOM.append(this._outputInfoContainer, DOM.$('.output-view-container'));
1485
this._outputEmptyElement = DOM.append(this._outputViewContainer, DOM.$('.output-empty-view'));
1486
this._outputEmptyElement.innerText = 'No outputs to render';
1487
1488
if (!this.cell.modified?.outputs.length) {
1489
this._outputEmptyElement.style.display = 'block';
1490
} else {
1491
this._outputEmptyElement.style.display = 'none';
1492
}
1493
1494
this.cell.layoutChange();
1495
1496
this._outputRightView = this.instantiationService.createInstance(OutputContainer, this.notebookEditor, this.notebookEditor.textModel!, this.cell, this.cell.modified!, DiffSide.Modified, this._outputViewContainer);
1497
this._register(this._outputRightView);
1498
this._outputRightView.render();
1499
1500
const insertOutputRenderListener = this.notebookEditor.onDidDynamicOutputRendered(e => {
1501
if (e.cell.uri.toString() === this.cell.modified!.uri.toString()) {
1502
this.notebookEditor.deltaCellOutputContainerClassNames(DiffSide.Modified, this.cell.modified!.id, ['nb-cellAdded'], []);
1503
insertOutputRenderListener.dispose();
1504
}
1505
});
1506
this._register(insertOutputRenderListener);
1507
}
1508
1509
this._outputViewContainer.style.display = 'block';
1510
}
1511
1512
_decorate() {
1513
this.notebookEditor.deltaCellOutputContainerClassNames(DiffSide.Modified, this.cell.modified!.id, ['nb-cellAdded'], []);
1514
}
1515
1516
_showOutputsRenderer() {
1517
if (this._outputViewContainer) {
1518
this._outputViewContainer.style.display = 'block';
1519
this._outputRightView?.showOutputs();
1520
this._decorate();
1521
}
1522
}
1523
1524
_hideOutputsRenderer() {
1525
if (this._outputViewContainer) {
1526
this._outputViewContainer.style.display = 'none';
1527
this._outputRightView?.hideOutputs();
1528
}
1529
}
1530
1531
layout(state: IDiffElementLayoutState) {
1532
DOM.scheduleAtNextAnimationFrame(DOM.getWindow(this._diffEditorContainer), () => {
1533
if ((state.editorHeight || state.outerWidth) && this._editor) {
1534
this._editorContainer.style.height = `${this.cell.layoutInfo.editorHeight}px`;
1535
this._editor.layout({
1536
width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, false),
1537
height: this.cell.layoutInfo.editorHeight
1538
});
1539
}
1540
1541
if (state.outerWidth && this._editor) {
1542
this._editorContainer.style.height = `${this.cell.layoutInfo.editorHeight}px`;
1543
this._editor.layout();
1544
}
1545
1546
if (state.metadataHeight || state.outerWidth) {
1547
this._metadataEditor?.layout({
1548
width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, true),
1549
height: this.cell.layoutInfo.metadataHeight
1550
});
1551
}
1552
1553
if (state.outputTotalHeight || state.outerWidth) {
1554
this._outputEditor?.layout({
1555
width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, false),
1556
height: this.cell.layoutInfo.outputTotalHeight
1557
});
1558
}
1559
1560
this.layoutNotebookCell();
1561
1562
if (this._diagonalFill) {
1563
this._diagonalFill.style.height = `${this.calculateDiagonalFillHeight()}px`;
1564
}
1565
});
1566
}
1567
1568
override dispose() {
1569
if (this._editor) {
1570
this.cell.saveSpirceEditorViewState(this._editor.saveViewState());
1571
}
1572
1573
super.dispose();
1574
}
1575
}
1576
1577
export class ModifiedElement extends AbstractElementRenderer {
1578
private _editor?: DiffEditorWidget;
1579
private _editorViewStateChanged: boolean;
1580
protected _toolbar!: ToolBar;
1581
protected _menu!: IMenu;
1582
1583
override readonly cell: SideBySideDiffElementViewModel;
1584
override readonly templateData: CellDiffSideBySideRenderTemplate;
1585
1586
constructor(
1587
notebookEditor: INotebookTextDiffEditor,
1588
cell: SideBySideDiffElementViewModel,
1589
templateData: CellDiffSideBySideRenderTemplate,
1590
@IInstantiationService instantiationService: IInstantiationService,
1591
@ILanguageService languageService: ILanguageService,
1592
@IModelService modelService: IModelService,
1593
@ITextModelService textModelService: ITextModelService,
1594
@IContextMenuService contextMenuService: IContextMenuService,
1595
@IKeybindingService keybindingService: IKeybindingService,
1596
@INotificationService notificationService: INotificationService,
1597
@IMenuService menuService: IMenuService,
1598
@IContextKeyService contextKeyService: IContextKeyService,
1599
@IConfigurationService configurationService: IConfigurationService,
1600
@ITextResourceConfigurationService textConfigurationService: ITextResourceConfigurationService,
1601
) {
1602
super(notebookEditor, cell, templateData, 'full', instantiationService, languageService, modelService, textModelService, contextMenuService, keybindingService, notificationService, menuService, contextKeyService, configurationService, textConfigurationService);
1603
this.cell = cell;
1604
this.templateData = templateData;
1605
this._editorViewStateChanged = false;
1606
1607
this.updateBorders();
1608
}
1609
1610
init() { }
1611
styleContainer(container: HTMLElement): void {
1612
container.classList.remove('inserted', 'removed');
1613
}
1614
1615
override buildBody(): void {
1616
super.buildBody();
1617
if (this.cell.displayIconToHideUnmodifiedCells) {
1618
this._register(this.templateData.marginOverlay.onAction(() => this.cell.hideUnchangedCells()));
1619
this.templateData.marginOverlay.show();
1620
} else {
1621
this.templateData.marginOverlay.hide();
1622
}
1623
}
1624
_disposeMetadata() {
1625
this.cell.metadataStatusHeight = 0;
1626
this.cell.metadataHeight = 0;
1627
this.templateData.metadataHeaderContainer.style.display = 'none';
1628
this.templateData.metadataInfoContainer.style.display = 'none';
1629
this._metadataEditor = undefined;
1630
}
1631
1632
_buildMetadata() {
1633
this._metadataHeaderContainer = this.templateData.metadataHeaderContainer;
1634
this._metadataInfoContainer = this.templateData.metadataInfoContainer;
1635
this._metadataHeaderContainer.style.display = 'flex';
1636
this._metadataInfoContainer.style.display = 'block';
1637
1638
this._metadataHeaderContainer.innerText = '';
1639
this._metadataInfoContainer.innerText = '';
1640
1641
this._metadataHeader = this.instantiationService.createInstance(
1642
PropertyHeader,
1643
this.cell,
1644
this._metadataHeaderContainer,
1645
this.notebookEditor,
1646
{
1647
updateInfoRendering: this.updateMetadataRendering.bind(this),
1648
checkIfModified: () => {
1649
return this.cell.checkMetadataIfModified();
1650
},
1651
getFoldingState: () => {
1652
return this.cell.metadataFoldingState;
1653
},
1654
updateFoldingState: (state) => {
1655
this.cell.metadataFoldingState = state;
1656
},
1657
unChangedLabel: 'Metadata',
1658
changedLabel: 'Metadata changed',
1659
prefix: 'metadata',
1660
menuId: MenuId.NotebookDiffCellMetadataTitle
1661
}
1662
);
1663
this._metadataLocalDisposable.add(this._metadataHeader);
1664
this._metadataHeader.buildHeader();
1665
}
1666
1667
_disposeOutput() {
1668
this._hideOutputsRaw();
1669
this._hideOutputsRenderer();
1670
this._hideOutputsEmptyView();
1671
1672
this.cell.rawOutputHeight = 0;
1673
this.cell.outputMetadataHeight = 0;
1674
this.cell.outputStatusHeight = 0;
1675
this.templateData.outputHeaderContainer.style.display = 'none';
1676
this.templateData.outputInfoContainer.style.display = 'none';
1677
this._outputViewContainer = undefined;
1678
}
1679
1680
_buildOutput() {
1681
this.templateData.outputHeaderContainer.style.display = 'flex';
1682
this.templateData.outputInfoContainer.style.display = 'block';
1683
1684
this._outputHeaderContainer = this.templateData.outputHeaderContainer;
1685
this._outputInfoContainer = this.templateData.outputInfoContainer;
1686
this._outputHeaderContainer.innerText = '';
1687
this._outputInfoContainer.innerText = '';
1688
1689
if (this.cell.checkIfOutputsModified()) {
1690
this._outputInfoContainer.classList.add('modified');
1691
} else {
1692
this._outputInfoContainer.classList.remove('modified');
1693
}
1694
1695
this._outputHeader = this.instantiationService.createInstance(
1696
PropertyHeader,
1697
this.cell,
1698
this._outputHeaderContainer,
1699
this.notebookEditor,
1700
{
1701
updateInfoRendering: this.updateOutputRendering.bind(this),
1702
checkIfModified: () => {
1703
return this.cell.checkIfOutputsModified();
1704
},
1705
getFoldingState: () => {
1706
return this.cell.outputFoldingState;
1707
},
1708
updateFoldingState: (state) => {
1709
this.cell.outputFoldingState = state;
1710
},
1711
unChangedLabel: 'Outputs',
1712
changedLabel: 'Outputs changed',
1713
prefix: 'output',
1714
menuId: MenuId.NotebookDiffCellOutputsTitle
1715
}
1716
);
1717
this._outputLocalDisposable.add(this._outputHeader);
1718
this._outputHeader.buildHeader();
1719
}
1720
1721
_buildOutputRendererContainer() {
1722
if (!this._outputViewContainer) {
1723
this._outputViewContainer = DOM.append(this._outputInfoContainer, DOM.$('.output-view-container'));
1724
this._outputEmptyElement = DOM.append(this._outputViewContainer, DOM.$('.output-empty-view'));
1725
this._outputEmptyElement.innerText = 'No outputs to render';
1726
1727
if (!this.cell.checkIfOutputsModified() && this.cell.modified.outputs.length === 0) {
1728
this._outputEmptyElement.style.display = 'block';
1729
} else {
1730
this._outputEmptyElement.style.display = 'none';
1731
}
1732
1733
this.cell.layoutChange();
1734
1735
this._register(this.cell.modified.textModel.onDidChangeOutputs(() => {
1736
// currently we only allow outputs change to the modified cell
1737
if (!this.cell.checkIfOutputsModified() && this.cell.modified.outputs.length === 0) {
1738
this._outputEmptyElement!.style.display = 'block';
1739
} else {
1740
this._outputEmptyElement!.style.display = 'none';
1741
}
1742
this._decorate();
1743
}));
1744
1745
this._outputLeftContainer = DOM.append(this._outputViewContainer, DOM.$('.output-view-container-left'));
1746
this._outputRightContainer = DOM.append(this._outputViewContainer, DOM.$('.output-view-container-right'));
1747
this._outputMetadataContainer = DOM.append(this._outputViewContainer, DOM.$('.output-view-container-metadata'));
1748
1749
const outputModified = this.cell.checkIfOutputsModified();
1750
const outputMetadataChangeOnly = outputModified
1751
&& outputModified.kind === OutputComparison.Metadata
1752
&& this.cell.original.outputs.length === 1
1753
&& this.cell.modified.outputs.length === 1
1754
&& outputEqual(this.cell.original.outputs[0], this.cell.modified.outputs[0]) === OutputComparison.Metadata;
1755
1756
if (outputModified && !outputMetadataChangeOnly) {
1757
const originalOutputRenderListener = this.notebookEditor.onDidDynamicOutputRendered(e => {
1758
if (e.cell.uri.toString() === this.cell.original.uri.toString() && this.cell.checkIfOutputsModified()) {
1759
this.notebookEditor.deltaCellOutputContainerClassNames(DiffSide.Original, this.cell.original.id, ['nb-cellDeleted'], []);
1760
originalOutputRenderListener.dispose();
1761
}
1762
});
1763
1764
const modifiedOutputRenderListener = this.notebookEditor.onDidDynamicOutputRendered(e => {
1765
if (e.cell.uri.toString() === this.cell.modified.uri.toString() && this.cell.checkIfOutputsModified()) {
1766
this.notebookEditor.deltaCellOutputContainerClassNames(DiffSide.Modified, this.cell.modified.id, ['nb-cellAdded'], []);
1767
modifiedOutputRenderListener.dispose();
1768
}
1769
});
1770
1771
this._register(originalOutputRenderListener);
1772
this._register(modifiedOutputRenderListener);
1773
}
1774
1775
// We should use the original text model here
1776
this._outputLeftView = this.instantiationService.createInstance(OutputContainer, this.notebookEditor, this.notebookEditor.textModel!, this.cell, this.cell.original, DiffSide.Original, this._outputLeftContainer);
1777
this._outputLeftView.render();
1778
this._register(this._outputLeftView);
1779
this._outputRightView = this.instantiationService.createInstance(OutputContainer, this.notebookEditor, this.notebookEditor.textModel!, this.cell, this.cell.modified, DiffSide.Modified, this._outputRightContainer);
1780
this._outputRightView.render();
1781
this._register(this._outputRightView);
1782
1783
if (outputModified && !outputMetadataChangeOnly) {
1784
this._decorate();
1785
}
1786
1787
if (outputMetadataChangeOnly) {
1788
1789
this._outputMetadataContainer.style.top = `${this.cell.layoutInfo.rawOutputHeight}px`;
1790
// single output, metadata change, let's render a diff editor for metadata
1791
this._outputMetadataEditor = this.instantiationService.createInstance(DiffEditorWidget, this._outputMetadataContainer, {
1792
...fixedDiffEditorOptions,
1793
overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode(),
1794
readOnly: true,
1795
ignoreTrimWhitespace: false,
1796
automaticLayout: false,
1797
dimension: {
1798
height: OUTPUT_EDITOR_HEIGHT_MAGIC,
1799
width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, true)
1800
}
1801
}, {
1802
originalEditor: getOptimizedNestedCodeEditorWidgetOptions(),
1803
modifiedEditor: getOptimizedNestedCodeEditorWidgetOptions()
1804
});
1805
1806
this._register(this._outputMetadataEditor);
1807
const originalOutputMetadataSource = JSON.stringify(this.cell.original.outputs[0].metadata ?? {}, undefined, '\t');
1808
const modifiedOutputMetadataSource = JSON.stringify(this.cell.modified.outputs[0].metadata ?? {}, undefined, '\t');
1809
1810
const mode = this.languageService.createById('json');
1811
const originalModel = this.modelService.createModel(originalOutputMetadataSource, mode, undefined, true);
1812
const modifiedModel = this.modelService.createModel(modifiedOutputMetadataSource, mode, undefined, true);
1813
1814
this._outputMetadataEditor.setModel({
1815
original: originalModel,
1816
modified: modifiedModel
1817
});
1818
1819
this.cell.outputMetadataHeight = this._outputMetadataEditor.getContentHeight();
1820
1821
this._register(this._outputMetadataEditor.onDidContentSizeChange((e) => {
1822
this.cell.outputMetadataHeight = e.contentHeight;
1823
}));
1824
}
1825
}
1826
1827
this._outputViewContainer.style.display = 'block';
1828
}
1829
1830
_decorate() {
1831
if (this.cell.checkIfOutputsModified()) {
1832
this.notebookEditor.deltaCellOutputContainerClassNames(DiffSide.Original, this.cell.original.id, ['nb-cellDeleted'], []);
1833
this.notebookEditor.deltaCellOutputContainerClassNames(DiffSide.Modified, this.cell.modified.id, ['nb-cellAdded'], []);
1834
} else {
1835
this.notebookEditor.deltaCellOutputContainerClassNames(DiffSide.Original, this.cell.original.id, [], ['nb-cellDeleted']);
1836
this.notebookEditor.deltaCellOutputContainerClassNames(DiffSide.Modified, this.cell.modified.id, [], ['nb-cellAdded']);
1837
}
1838
}
1839
1840
_showOutputsRenderer() {
1841
if (this._outputViewContainer) {
1842
this._outputViewContainer.style.display = 'block';
1843
1844
this._outputLeftView?.showOutputs();
1845
this._outputRightView?.showOutputs();
1846
this._outputMetadataEditor?.layout({
1847
width: this._editor?.getViewWidth() || this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, true),
1848
height: this.cell.layoutInfo.outputMetadataHeight
1849
});
1850
1851
this._decorate();
1852
}
1853
}
1854
1855
_hideOutputsRenderer() {
1856
if (this._outputViewContainer) {
1857
this._outputViewContainer.style.display = 'none';
1858
1859
this._outputLeftView?.hideOutputs();
1860
this._outputRightView?.hideOutputs();
1861
}
1862
}
1863
1864
updateSourceEditor(): void {
1865
this._cellHeaderContainer = this.templateData.cellHeaderContainer;
1866
this._cellHeaderContainer.style.display = 'flex';
1867
this._cellHeaderContainer.innerText = '';
1868
const modifiedCell = this.cell.modified;
1869
this._editorContainer = this.templateData.editorContainer;
1870
this._editorContainer.classList.add('diff');
1871
1872
const renderSourceEditor = () => {
1873
if (this.cell.cellFoldingState === PropertyFoldingState.Collapsed) {
1874
this._editorContainer.style.display = 'none';
1875
this.cell.editorHeight = 0;
1876
return;
1877
}
1878
1879
const lineCount = modifiedCell.textModel.textBuffer.getLineCount();
1880
const lineHeight = this.notebookEditor.getLayoutInfo().fontInfo.lineHeight || 17;
1881
const editorHeight = this.cell.layoutInfo.editorHeight !== 0 ? this.cell.layoutInfo.editorHeight : this.cell.computeInputEditorHeight(lineHeight);
1882
1883
this._editorContainer.style.height = `${editorHeight}px`;
1884
this._editorContainer.style.display = 'block';
1885
1886
if (this._editor) {
1887
const contentHeight = this._editor.getContentHeight();
1888
if (contentHeight >= 0) {
1889
this.cell.editorHeight = contentHeight;
1890
}
1891
return;
1892
}
1893
1894
this._editor = this.templateData.sourceEditor;
1895
// If there is only 1 line, then ensure we have the necessary padding to display the button for whitespaces.
1896
// E.g. assume we have a cell with 1 line and we add some whitespace,
1897
// Then diff editor displays the button `Show Whitespace Differences`, however with 12 paddings on the top, the
1898
// button can get cut off.
1899
const options: IDiffEditorOptions = {
1900
padding: getEditorPadding(lineCount)
1901
};
1902
const unchangedRegions = this._register(getUnchangedRegionSettings(this.configurationService));
1903
if (unchangedRegions.options.enabled) {
1904
options.hideUnchangedRegions = unchangedRegions.options;
1905
}
1906
this._editor.updateOptions(options);
1907
this._register(unchangedRegions.onDidChangeEnablement(() => {
1908
options.hideUnchangedRegions = unchangedRegions.options;
1909
this._editor?.updateOptions(options);
1910
}));
1911
this._editor.layout({
1912
width: this.notebookEditor.getLayoutInfo().width - 2 * DIFF_CELL_MARGIN,
1913
height: editorHeight
1914
});
1915
this._register(this._editor.onDidContentSizeChange((e) => {
1916
if (this.cell.cellFoldingState === PropertyFoldingState.Expanded && e.contentHeightChanged && this.cell.layoutInfo.editorHeight !== e.contentHeight) {
1917
this.cell.editorHeight = e.contentHeight;
1918
}
1919
}));
1920
this._initializeSourceDiffEditor();
1921
};
1922
1923
this._cellHeader = this._register(this.instantiationService.createInstance(
1924
PropertyHeader,
1925
this.cell,
1926
this._cellHeaderContainer,
1927
this.notebookEditor,
1928
{
1929
updateInfoRendering: () => renderSourceEditor(),
1930
checkIfModified: () => {
1931
return this.cell.modified?.textModel.getTextBufferHash() !== this.cell.original?.textModel.getTextBufferHash() ? { reason: undefined } : false;
1932
},
1933
getFoldingState: () => this.cell.cellFoldingState,
1934
updateFoldingState: (state) => this.cell.cellFoldingState = state,
1935
unChangedLabel: 'Input',
1936
changedLabel: 'Input changed',
1937
prefix: 'input',
1938
menuId: MenuId.NotebookDiffCellInputTitle
1939
}
1940
));
1941
this._cellHeader.buildHeader();
1942
renderSourceEditor();
1943
1944
const scopedContextKeyService = this.contextKeyService.createScoped(this.templateData.inputToolbarContainer);
1945
this._register(scopedContextKeyService);
1946
const inputChanged = NOTEBOOK_DIFF_CELL_INPUT.bindTo(scopedContextKeyService);
1947
inputChanged.set(this.cell.modified.textModel.getTextBufferHash() !== this.cell.original.textModel.getTextBufferHash());
1948
1949
const ignoreWhitespace = NOTEBOOK_DIFF_CELL_IGNORE_WHITESPACE.bindTo(scopedContextKeyService);
1950
const ignore = this.textConfigurationService.getValue<boolean>(this.cell.modified.uri, 'diffEditor.ignoreTrimWhitespace');
1951
ignoreWhitespace.set(ignore);
1952
1953
this._toolbar = this.templateData.toolbar;
1954
1955
this._toolbar.context = this.cell;
1956
1957
const refreshToolbar = () => {
1958
const ignore = this.textConfigurationService.getValue<boolean>(this.cell.modified.uri, 'diffEditor.ignoreTrimWhitespace');
1959
ignoreWhitespace.set(ignore);
1960
const hasChanges = this.cell.modified.textModel.getTextBufferHash() !== this.cell.original.textModel.getTextBufferHash();
1961
inputChanged.set(hasChanges);
1962
1963
if (hasChanges) {
1964
const menu = this.menuService.getMenuActions(MenuId.NotebookDiffCellInputTitle, scopedContextKeyService, { shouldForwardArgs: true });
1965
const actions = getFlatActionBarActions(menu);
1966
this._toolbar.setActions(actions);
1967
} else {
1968
this._toolbar.setActions([]);
1969
}
1970
};
1971
1972
this._register(this.cell.modified.textModel.onDidChangeContent(() => refreshToolbar()));
1973
this._register(this.textConfigurationService.onDidChangeConfiguration(e => {
1974
if (e.affectsConfiguration(this.cell.modified.uri, 'diffEditor') &&
1975
e.affectedKeys.has('diffEditor.ignoreTrimWhitespace')) {
1976
refreshToolbar();
1977
}
1978
}));
1979
refreshToolbar();
1980
}
1981
1982
private async _initializeSourceDiffEditor() {
1983
const [originalRef, modifiedRef] = await Promise.all([
1984
this.textModelService.createModelReference(this.cell.original.uri),
1985
this.textModelService.createModelReference(this.cell.modified.uri)]);
1986
this._register(originalRef);
1987
this._register(modifiedRef);
1988
1989
if (this._isDisposed) {
1990
originalRef.dispose();
1991
modifiedRef.dispose();
1992
return;
1993
}
1994
1995
const vm = this._register(this._editor!.createViewModel({
1996
original: originalRef.object.textEditorModel,
1997
modified: modifiedRef.object.textEditorModel,
1998
}));
1999
2000
// Reduces flicker (compute this before setting the model)
2001
// Else when the model is set, the height of the editor will be x, after diff is computed, then height will be y.
2002
// & that results in flicker.
2003
await vm.waitForDiff();
2004
this._editor!.setModel(vm);
2005
2006
const handleViewStateChange = () => {
2007
this._editorViewStateChanged = true;
2008
};
2009
2010
const handleScrollChange = (e: editorCommon.IScrollEvent) => {
2011
if (e.scrollTopChanged || e.scrollLeftChanged) {
2012
this._editorViewStateChanged = true;
2013
}
2014
};
2015
2016
this.updateEditorOptionsForWhitespace();
2017
this._register(this._editor!.getOriginalEditor().onDidChangeCursorSelection(handleViewStateChange));
2018
this._register(this._editor!.getOriginalEditor().onDidScrollChange(handleScrollChange));
2019
this._register(this._editor!.getModifiedEditor().onDidChangeCursorSelection(handleViewStateChange));
2020
this._register(this._editor!.getModifiedEditor().onDidScrollChange(handleScrollChange));
2021
2022
const editorViewState = this.cell.getSourceEditorViewState() as editorCommon.IDiffEditorViewState | null;
2023
if (editorViewState) {
2024
this._editor!.restoreViewState(editorViewState);
2025
}
2026
2027
const contentHeight = this._editor!.getContentHeight();
2028
this.cell.editorHeight = contentHeight;
2029
}
2030
private updateEditorOptionsForWhitespace() {
2031
const editor = this._editor;
2032
if (!editor) {
2033
return;
2034
}
2035
const uri = editor.getModel()?.modified.uri || editor.getModel()?.original.uri;
2036
if (!uri) {
2037
return;
2038
}
2039
const ignoreTrimWhitespace = this.textConfigurationService.getValue<boolean>(uri, 'diffEditor.ignoreTrimWhitespace');
2040
editor.updateOptions({ ignoreTrimWhitespace });
2041
2042
this._register(this.textConfigurationService.onDidChangeConfiguration(e => {
2043
if (e.affectsConfiguration(uri, 'diffEditor') &&
2044
e.affectedKeys.has('diffEditor.ignoreTrimWhitespace')) {
2045
const ignoreTrimWhitespace = this.textConfigurationService.getValue<boolean>(uri, 'diffEditor.ignoreTrimWhitespace');
2046
editor.updateOptions({ ignoreTrimWhitespace });
2047
}
2048
}));
2049
}
2050
layout(state: IDiffElementLayoutState) {
2051
DOM.scheduleAtNextAnimationFrame(DOM.getWindow(this._diffEditorContainer), () => {
2052
if (state.editorHeight && this._editor) {
2053
this._editorContainer.style.height = `${this.cell.layoutInfo.editorHeight}px`;
2054
this._editor.layout({
2055
width: this._editor!.getViewWidth(),
2056
height: this.cell.layoutInfo.editorHeight
2057
});
2058
}
2059
2060
if (state.outerWidth && this._editor) {
2061
this._editorContainer.style.height = `${this.cell.layoutInfo.editorHeight}px`;
2062
this._editor.layout();
2063
}
2064
2065
if (state.metadataHeight || state.outerWidth) {
2066
if (this._metadataEditorContainer) {
2067
this._metadataEditorContainer.style.height = `${this.cell.layoutInfo.metadataHeight}px`;
2068
this._metadataEditor?.layout({
2069
width: this._editor?.getViewWidth() || this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, true),
2070
height: this.cell.layoutInfo.metadataHeight
2071
});
2072
}
2073
}
2074
2075
if (state.outputTotalHeight || state.outerWidth) {
2076
if (this._outputEditorContainer) {
2077
this._outputEditorContainer.style.height = `${this.cell.layoutInfo.outputTotalHeight}px`;
2078
this._outputEditor?.layout({
2079
width: this._editor?.getViewWidth() || this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, true),
2080
height: this.cell.layoutInfo.outputTotalHeight
2081
});
2082
}
2083
2084
if (this._outputMetadataContainer) {
2085
this._outputMetadataContainer.style.height = `${this.cell.layoutInfo.outputMetadataHeight}px`;
2086
this._outputMetadataContainer.style.top = `${this.cell.layoutInfo.outputTotalHeight - this.cell.layoutInfo.outputMetadataHeight}px`;
2087
this._outputMetadataEditor?.layout({
2088
width: this._editor?.getViewWidth() || this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, true),
2089
height: this.cell.layoutInfo.outputMetadataHeight
2090
});
2091
}
2092
}
2093
2094
this.layoutNotebookCell();
2095
});
2096
}
2097
2098
override dispose() {
2099
// The editor isn't disposed yet, it can be re-used.
2100
// However the model can be disposed before the editor & that causes issues.
2101
if (this._editor) {
2102
this._editor.setModel(null);
2103
}
2104
2105
if (this._editor && this._editorViewStateChanged) {
2106
this.cell.saveSpirceEditorViewState(this._editor.saveViewState());
2107
}
2108
2109
super.dispose();
2110
}
2111
}
2112
2113
2114
export class CollapsedCellOverlayWidget extends Disposable implements IDiffCellMarginOverlay {
2115
private readonly _nodes = DOM.h('div.diff-hidden-cells', [
2116
DOM.h('div.center@content', { style: { display: 'flex' } }, [
2117
DOM.$('a', {
2118
title: localize('showUnchangedCells', 'Show Unchanged Cells'),
2119
role: 'button',
2120
onclick: () => { this._action.fire(); }
2121
},
2122
...renderLabelWithIcons('$(unfold)'))]
2123
),
2124
]);
2125
2126
private readonly _action = this._register(new Emitter<void>());
2127
public readonly onAction = this._action.event;
2128
constructor(
2129
private readonly container: HTMLElement
2130
) {
2131
super();
2132
2133
this._nodes.root.style.display = 'none';
2134
container.appendChild(this._nodes.root);
2135
}
2136
2137
public show() {
2138
this._nodes.root.style.display = 'block';
2139
}
2140
2141
public hide() {
2142
this._nodes.root.style.display = 'none';
2143
}
2144
2145
public override dispose() {
2146
this.hide();
2147
this.container.removeChild(this._nodes.root);
2148
DOM.reset(this._nodes.root);
2149
super.dispose();
2150
}
2151
}
2152
2153
export class UnchangedCellOverlayWidget extends Disposable implements IDiffCellMarginOverlay {
2154
private readonly _nodes = DOM.h('div.diff-hidden-cells', [
2155
DOM.h('div.center@content', { style: { display: 'flex' } }, [
2156
DOM.$('a', {
2157
title: localize('hideUnchangedCells', 'Hide Unchanged Cells'),
2158
role: 'button',
2159
onclick: () => { this._action.fire(); }
2160
},
2161
...renderLabelWithIcons('$(fold)')
2162
),
2163
]
2164
),
2165
]);
2166
2167
private readonly _action = this._register(new Emitter<void>());
2168
public readonly onAction = this._action.event;
2169
constructor(
2170
private readonly container: HTMLElement
2171
) {
2172
super();
2173
2174
this._nodes.root.style.display = 'none';
2175
container.appendChild(this._nodes.root);
2176
}
2177
2178
public show() {
2179
this._nodes.root.style.display = 'block';
2180
}
2181
2182
public hide() {
2183
this._nodes.root.style.display = 'none';
2184
}
2185
public override dispose() {
2186
this.hide();
2187
this.container.removeChild(this._nodes.root);
2188
DOM.reset(this._nodes.root);
2189
super.dispose();
2190
}
2191
}
2192
2193