Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/notebook/browser/diff/diffElementViewModel.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 { Emitter } from '../../../../../base/common/event.js';
7
import { hash } from '../../../../../base/common/hash.js';
8
import { Disposable } from '../../../../../base/common/lifecycle.js';
9
import { URI } from '../../../../../base/common/uri.js';
10
import { DiffEditorWidget } from '../../../../../editor/browser/widget/diffEditor/diffEditorWidget.js';
11
import { FontInfo } from '../../../../../editor/common/config/fontInfo.js';
12
import * as editorCommon from '../../../../../editor/common/editorCommon.js';
13
import { getEditorPadding } from './diffCellEditorOptions.js';
14
import { DiffNestedCellViewModel } from './diffNestedCellViewModel.js';
15
import { NotebookDiffEditorEventDispatcher, NotebookDiffViewEventType } from './eventDispatcher.js';
16
import { CellDiffViewModelLayoutChangeEvent, DIFF_CELL_MARGIN, DiffSide, IDiffElementLayoutInfo } from './notebookDiffEditorBrowser.js';
17
import { CellLayoutState, IGenericCellViewModel } from '../notebookBrowser.js';
18
import { NotebookLayoutInfo } from '../notebookViewEvents.js';
19
import { getFormattedMetadataJSON, NotebookCellTextModel } from '../../common/model/notebookCellTextModel.js';
20
import { NotebookTextModel } from '../../common/model/notebookTextModel.js';
21
import { CellUri, ICellOutput, INotebookTextModel, IOutputDto, IOutputItemDto } from '../../common/notebookCommon.js';
22
import { INotebookService } from '../../common/notebookService.js';
23
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
24
import { Schemas } from '../../../../../base/common/network.js';
25
import { IDiffEditorHeightCalculatorService } from './editorHeightCalculator.js';
26
import { NotebookDocumentMetadataTextModel } from '../../common/model/notebookMetadataTextModel.js';
27
28
const PropertyHeaderHeight = 25;
29
30
// From `.monaco-editor .diff-hidden-lines .center` in src/vs/editor/browser/widget/diffEditor/style.css
31
export const HeightOfHiddenLinesRegionInDiffEditor = 24;
32
33
export const DefaultLineHeight = 17;
34
35
export enum PropertyFoldingState {
36
Expanded,
37
Collapsed
38
}
39
40
export const OUTPUT_EDITOR_HEIGHT_MAGIC = 1440;
41
42
type ILayoutInfoDelta0 = { [K in keyof IDiffElementLayoutInfo]?: number; };
43
interface ILayoutInfoDelta extends ILayoutInfoDelta0 {
44
rawOutputHeight?: number;
45
recomputeOutput?: boolean;
46
}
47
48
export type IDiffElementViewModelBase = DiffElementCellViewModelBase | DiffElementPlaceholderViewModel | NotebookDocumentMetadataViewModel;
49
50
export abstract class DiffElementViewModelBase extends Disposable {
51
protected _layoutInfoEmitter = this._register(new Emitter<CellDiffViewModelLayoutChangeEvent>());
52
onDidLayoutChange = this._layoutInfoEmitter.event;
53
abstract renderOutput: boolean;
54
constructor(
55
public readonly mainDocumentTextModel: INotebookTextModel,
56
public readonly editorEventDispatcher: NotebookDiffEditorEventDispatcher,
57
public readonly initData: {
58
metadataStatusHeight: number;
59
outputStatusHeight: number;
60
fontInfo: FontInfo | undefined;
61
}
62
) {
63
super();
64
65
this._register(this.editorEventDispatcher.onDidChangeLayout(e => this._layoutInfoEmitter.fire({ outerWidth: true })));
66
}
67
68
abstract layoutChange(): void;
69
abstract getHeight(lineHeight: number): number;
70
abstract get totalHeight(): number;
71
}
72
73
export class DiffElementPlaceholderViewModel extends DiffElementViewModelBase {
74
readonly type: 'placeholder' = 'placeholder';
75
public hiddenCells: DiffElementCellViewModelBase[] = [];
76
protected _unfoldHiddenCells = this._register(new Emitter<void>());
77
onUnfoldHiddenCells = this._unfoldHiddenCells.event;
78
79
public renderOutput: boolean = false;
80
constructor(
81
mainDocumentTextModel: INotebookTextModel,
82
editorEventDispatcher: NotebookDiffEditorEventDispatcher,
83
initData: {
84
metadataStatusHeight: number;
85
outputStatusHeight: number;
86
fontInfo: FontInfo | undefined;
87
}
88
) {
89
super(mainDocumentTextModel, editorEventDispatcher, initData);
90
91
}
92
get totalHeight() {
93
return 24 + (2 * DIFF_CELL_MARGIN);
94
}
95
getHeight(_: number): number {
96
return this.totalHeight;
97
}
98
override layoutChange(): void {
99
//
100
}
101
showHiddenCells() {
102
this._unfoldHiddenCells.fire();
103
}
104
}
105
106
107
export class NotebookDocumentMetadataViewModel extends DiffElementViewModelBase {
108
public readonly originalMetadata: NotebookDocumentMetadataTextModel;
109
public readonly modifiedMetadata: NotebookDocumentMetadataTextModel;
110
public cellFoldingState: PropertyFoldingState;
111
protected _layoutInfo!: IDiffElementLayoutInfo;
112
public renderOutput: boolean = false;
113
set editorHeight(height: number) {
114
this._layout({ editorHeight: height });
115
}
116
117
get editorHeight() {
118
throw new Error('Use Cell.layoutInfo.editorHeight');
119
}
120
121
set editorMargin(margin: number) {
122
this._layout({ editorMargin: margin });
123
}
124
125
get editorMargin() {
126
throw new Error('Use Cell.layoutInfo.editorMargin');
127
}
128
get layoutInfo(): IDiffElementLayoutInfo {
129
return this._layoutInfo;
130
}
131
132
get totalHeight() {
133
return this.layoutInfo.totalHeight;
134
}
135
136
private _sourceEditorViewState: editorCommon.ICodeEditorViewState | editorCommon.IDiffEditorViewState | null = null;
137
constructor(
138
public readonly originalDocumentTextModel: INotebookTextModel,
139
public readonly modifiedDocumentTextModel: INotebookTextModel,
140
public readonly type: 'unchangedMetadata' | 'modifiedMetadata',
141
editorEventDispatcher: NotebookDiffEditorEventDispatcher,
142
initData: {
143
metadataStatusHeight: number;
144
outputStatusHeight: number;
145
fontInfo: FontInfo | undefined;
146
},
147
notebookService: INotebookService,
148
private readonly editorHeightCalculator: IDiffEditorHeightCalculatorService
149
) {
150
super(originalDocumentTextModel, editorEventDispatcher, initData);
151
152
const cellStatusHeight = PropertyHeaderHeight;
153
this._layoutInfo = {
154
width: 0,
155
editorHeight: 0,
156
editorMargin: 0,
157
metadataHeight: 0,
158
cellStatusHeight,
159
metadataStatusHeight: 0,
160
rawOutputHeight: 0,
161
outputTotalHeight: 0,
162
outputStatusHeight: 0,
163
outputMetadataHeight: 0,
164
bodyMargin: 32,
165
totalHeight: 82 + cellStatusHeight + 0,
166
layoutState: CellLayoutState.Uninitialized
167
};
168
169
this.cellFoldingState = type === 'modifiedMetadata' ? PropertyFoldingState.Expanded : PropertyFoldingState.Collapsed;
170
this.originalMetadata = this._register(new NotebookDocumentMetadataTextModel(originalDocumentTextModel));
171
this.modifiedMetadata = this._register(new NotebookDocumentMetadataTextModel(modifiedDocumentTextModel));
172
}
173
174
public async computeHeights() {
175
if (this.type === 'unchangedMetadata') {
176
this.editorHeight = this.editorHeightCalculator.computeHeightFromLines(this.originalMetadata.textBuffer.getLineCount());
177
} else {
178
const original = this.originalMetadata.uri;
179
const modified = this.modifiedMetadata.uri;
180
this.editorHeight = await this.editorHeightCalculator.diffAndComputeHeight(original, modified);
181
}
182
}
183
184
layoutChange() {
185
this._layout({ recomputeOutput: true });
186
}
187
188
protected _layout(delta: ILayoutInfoDelta) {
189
const width = delta.width !== undefined ? delta.width : this._layoutInfo.width;
190
const editorHeight = delta.editorHeight !== undefined ? delta.editorHeight : this._layoutInfo.editorHeight;
191
const editorMargin = delta.editorMargin !== undefined ? delta.editorMargin : this._layoutInfo.editorMargin;
192
const cellStatusHeight = delta.cellStatusHeight !== undefined ? delta.cellStatusHeight : this._layoutInfo.cellStatusHeight;
193
const bodyMargin = delta.bodyMargin !== undefined ? delta.bodyMargin : this._layoutInfo.bodyMargin;
194
195
const totalHeight = editorHeight
196
+ editorMargin
197
+ cellStatusHeight
198
+ bodyMargin;
199
200
const newLayout: IDiffElementLayoutInfo = {
201
width: width,
202
editorHeight: editorHeight,
203
editorMargin: editorMargin,
204
metadataHeight: 0,
205
cellStatusHeight,
206
metadataStatusHeight: 0,
207
outputTotalHeight: 0,
208
outputStatusHeight: 0,
209
bodyMargin: bodyMargin,
210
rawOutputHeight: 0,
211
outputMetadataHeight: 0,
212
totalHeight: totalHeight,
213
layoutState: CellLayoutState.Measured
214
};
215
216
let somethingChanged = false;
217
218
const changeEvent: CellDiffViewModelLayoutChangeEvent = {};
219
220
if (newLayout.width !== this._layoutInfo.width) {
221
changeEvent.width = true;
222
somethingChanged = true;
223
}
224
225
if (newLayout.editorHeight !== this._layoutInfo.editorHeight) {
226
changeEvent.editorHeight = true;
227
somethingChanged = true;
228
}
229
230
if (newLayout.editorMargin !== this._layoutInfo.editorMargin) {
231
changeEvent.editorMargin = true;
232
somethingChanged = true;
233
}
234
235
if (newLayout.cellStatusHeight !== this._layoutInfo.cellStatusHeight) {
236
changeEvent.cellStatusHeight = true;
237
somethingChanged = true;
238
}
239
240
if (newLayout.bodyMargin !== this._layoutInfo.bodyMargin) {
241
changeEvent.bodyMargin = true;
242
somethingChanged = true;
243
}
244
245
if (newLayout.totalHeight !== this._layoutInfo.totalHeight) {
246
changeEvent.totalHeight = true;
247
somethingChanged = true;
248
}
249
250
if (somethingChanged) {
251
this._layoutInfo = newLayout;
252
this._fireLayoutChangeEvent(changeEvent);
253
}
254
}
255
256
getHeight(lineHeight: number) {
257
if (this._layoutInfo.layoutState === CellLayoutState.Uninitialized) {
258
const editorHeight = this.cellFoldingState === PropertyFoldingState.Collapsed ? 0 : this.computeInputEditorHeight(lineHeight);
259
return this._computeTotalHeight(editorHeight);
260
} else {
261
return this._layoutInfo.totalHeight;
262
}
263
}
264
265
private _computeTotalHeight(editorHeight: number) {
266
const totalHeight = editorHeight
267
+ this._layoutInfo.editorMargin
268
+ this._layoutInfo.metadataHeight
269
+ this._layoutInfo.cellStatusHeight
270
+ this._layoutInfo.metadataStatusHeight
271
+ this._layoutInfo.outputTotalHeight
272
+ this._layoutInfo.outputStatusHeight
273
+ this._layoutInfo.outputMetadataHeight
274
+ this._layoutInfo.bodyMargin;
275
276
return totalHeight;
277
}
278
279
public computeInputEditorHeight(_lineHeight: number): number {
280
return this.editorHeightCalculator.computeHeightFromLines(Math.max(this.originalMetadata.textBuffer.getLineCount(), this.modifiedMetadata.textBuffer.getLineCount()));
281
}
282
283
private _fireLayoutChangeEvent(state: CellDiffViewModelLayoutChangeEvent) {
284
this._layoutInfoEmitter.fire(state);
285
this.editorEventDispatcher.emit([{ type: NotebookDiffViewEventType.CellLayoutChanged, source: this._layoutInfo }]);
286
}
287
288
getComputedCellContainerWidth(layoutInfo: NotebookLayoutInfo, diffEditor: boolean, fullWidth: boolean) {
289
if (fullWidth) {
290
return layoutInfo.width - 2 * DIFF_CELL_MARGIN + (diffEditor ? DiffEditorWidget.ENTIRE_DIFF_OVERVIEW_WIDTH : 0) - 2;
291
}
292
293
return (layoutInfo.width - 2 * DIFF_CELL_MARGIN + (diffEditor ? DiffEditorWidget.ENTIRE_DIFF_OVERVIEW_WIDTH : 0)) / 2 - 18 - 2;
294
}
295
296
getSourceEditorViewState(): editorCommon.ICodeEditorViewState | editorCommon.IDiffEditorViewState | null {
297
return this._sourceEditorViewState;
298
}
299
300
saveSpirceEditorViewState(viewState: editorCommon.ICodeEditorViewState | editorCommon.IDiffEditorViewState | null) {
301
this._sourceEditorViewState = viewState;
302
}
303
}
304
305
306
export abstract class DiffElementCellViewModelBase extends DiffElementViewModelBase {
307
public cellFoldingState: PropertyFoldingState;
308
public metadataFoldingState: PropertyFoldingState;
309
public outputFoldingState: PropertyFoldingState;
310
protected _stateChangeEmitter = this._register(new Emitter<{ renderOutput: boolean }>());
311
onDidStateChange = this._stateChangeEmitter.event;
312
protected _layoutInfo!: IDiffElementLayoutInfo;
313
314
public displayIconToHideUnmodifiedCells?: boolean;
315
private _hideUnchangedCells = this._register(new Emitter<void>());
316
public onHideUnchangedCells = this._hideUnchangedCells.event;
317
318
hideUnchangedCells() {
319
this._hideUnchangedCells.fire();
320
}
321
set rawOutputHeight(height: number) {
322
this._layout({ rawOutputHeight: Math.min(OUTPUT_EDITOR_HEIGHT_MAGIC, height) });
323
}
324
325
get rawOutputHeight() {
326
throw new Error('Use Cell.layoutInfo.rawOutputHeight');
327
}
328
329
set outputStatusHeight(height: number) {
330
this._layout({ outputStatusHeight: height });
331
}
332
333
get outputStatusHeight() {
334
throw new Error('Use Cell.layoutInfo.outputStatusHeight');
335
}
336
337
set outputMetadataHeight(height: number) {
338
this._layout({ outputMetadataHeight: height });
339
}
340
341
get outputMetadataHeight() {
342
throw new Error('Use Cell.layoutInfo.outputStatusHeight');
343
}
344
345
set editorHeight(height: number) {
346
this._layout({ editorHeight: height });
347
}
348
349
get editorHeight() {
350
throw new Error('Use Cell.layoutInfo.editorHeight');
351
}
352
353
set editorMargin(margin: number) {
354
this._layout({ editorMargin: margin });
355
}
356
357
get editorMargin() {
358
throw new Error('Use Cell.layoutInfo.editorMargin');
359
}
360
361
set metadataStatusHeight(height: number) {
362
this._layout({ metadataStatusHeight: height });
363
}
364
365
get metadataStatusHeight() {
366
throw new Error('Use Cell.layoutInfo.outputStatusHeight');
367
}
368
369
set metadataHeight(height: number) {
370
this._layout({ metadataHeight: height });
371
}
372
373
get metadataHeight() {
374
throw new Error('Use Cell.layoutInfo.metadataHeight');
375
}
376
377
private _renderOutput = true;
378
379
set renderOutput(value: boolean) {
380
this._renderOutput = value;
381
this._layout({ recomputeOutput: true });
382
this._stateChangeEmitter.fire({ renderOutput: this._renderOutput });
383
}
384
385
get renderOutput() {
386
return this._renderOutput;
387
}
388
389
get layoutInfo(): IDiffElementLayoutInfo {
390
return this._layoutInfo;
391
}
392
393
get totalHeight() {
394
return this.layoutInfo.totalHeight;
395
}
396
397
protected get ignoreOutputs() {
398
return this.configurationService.getValue<boolean>('notebook.diff.ignoreOutputs') || !!(this.mainDocumentTextModel?.transientOptions.transientOutputs);
399
}
400
401
protected get ignoreMetadata() {
402
return this.configurationService.getValue<boolean>('notebook.diff.ignoreMetadata');
403
}
404
405
private _sourceEditorViewState: editorCommon.ICodeEditorViewState | editorCommon.IDiffEditorViewState | null = null;
406
private _outputEditorViewState: editorCommon.ICodeEditorViewState | editorCommon.IDiffEditorViewState | null = null;
407
private _metadataEditorViewState: editorCommon.ICodeEditorViewState | editorCommon.IDiffEditorViewState | null = null;
408
public readonly original: DiffNestedCellViewModel | undefined;
409
410
public readonly modified: DiffNestedCellViewModel | undefined;
411
constructor(
412
mainDocumentTextModel: INotebookTextModel,
413
original: NotebookCellTextModel | undefined,
414
modified: NotebookCellTextModel | undefined,
415
readonly type: 'unchanged' | 'insert' | 'delete' | 'modified',
416
editorEventDispatcher: NotebookDiffEditorEventDispatcher,
417
initData: {
418
metadataStatusHeight: number;
419
outputStatusHeight: number;
420
fontInfo: FontInfo | undefined;
421
},
422
notebookService: INotebookService,
423
public readonly index: number,
424
private readonly configurationService: IConfigurationService,
425
public readonly diffEditorHeightCalculator: IDiffEditorHeightCalculatorService
426
) {
427
super(mainDocumentTextModel, editorEventDispatcher, initData);
428
this.original = original ? this._register(new DiffNestedCellViewModel(original, notebookService)) : undefined;
429
this.modified = modified ? this._register(new DiffNestedCellViewModel(modified, notebookService)) : undefined;
430
const editorHeight = this._estimateEditorHeight(initData.fontInfo);
431
const cellStatusHeight = PropertyHeaderHeight;
432
this._layoutInfo = {
433
width: 0,
434
editorHeight: editorHeight,
435
editorMargin: 0,
436
metadataHeight: 0,
437
cellStatusHeight,
438
metadataStatusHeight: this.ignoreMetadata ? 0 : PropertyHeaderHeight,
439
rawOutputHeight: 0,
440
outputTotalHeight: 0,
441
outputStatusHeight: this.ignoreOutputs ? 0 : PropertyHeaderHeight,
442
outputMetadataHeight: 0,
443
bodyMargin: 32,
444
totalHeight: 82 + cellStatusHeight + editorHeight,
445
layoutState: CellLayoutState.Uninitialized
446
};
447
448
this.cellFoldingState = modified?.getTextBufferHash() !== original?.getTextBufferHash() ? PropertyFoldingState.Expanded : PropertyFoldingState.Collapsed;
449
this.metadataFoldingState = PropertyFoldingState.Collapsed;
450
this.outputFoldingState = PropertyFoldingState.Collapsed;
451
}
452
453
layoutChange() {
454
this._layout({ recomputeOutput: true });
455
}
456
457
private _estimateEditorHeight(fontInfo: FontInfo | undefined) {
458
const lineHeight = fontInfo?.lineHeight ?? 17;
459
460
switch (this.type) {
461
case 'unchanged':
462
case 'insert':
463
{
464
const lineCount = this.modified!.textModel.textBuffer.getLineCount();
465
const editorHeight = lineCount * lineHeight + getEditorPadding(lineCount).top + getEditorPadding(lineCount).bottom;
466
return editorHeight;
467
}
468
case 'delete':
469
case 'modified':
470
{
471
const lineCount = this.original!.textModel.textBuffer.getLineCount();
472
const editorHeight = lineCount * lineHeight + getEditorPadding(lineCount).top + getEditorPadding(lineCount).bottom;
473
return editorHeight;
474
}
475
}
476
}
477
478
protected _layout(delta: ILayoutInfoDelta) {
479
const width = delta.width !== undefined ? delta.width : this._layoutInfo.width;
480
const editorHeight = delta.editorHeight !== undefined ? delta.editorHeight : this._layoutInfo.editorHeight;
481
const editorMargin = delta.editorMargin !== undefined ? delta.editorMargin : this._layoutInfo.editorMargin;
482
const metadataHeight = delta.metadataHeight !== undefined ? delta.metadataHeight : this._layoutInfo.metadataHeight;
483
const cellStatusHeight = delta.cellStatusHeight !== undefined ? delta.cellStatusHeight : this._layoutInfo.cellStatusHeight;
484
const metadataStatusHeight = delta.metadataStatusHeight !== undefined ? delta.metadataStatusHeight : this._layoutInfo.metadataStatusHeight;
485
const rawOutputHeight = delta.rawOutputHeight !== undefined ? delta.rawOutputHeight : this._layoutInfo.rawOutputHeight;
486
const outputStatusHeight = delta.outputStatusHeight !== undefined ? delta.outputStatusHeight : this._layoutInfo.outputStatusHeight;
487
const bodyMargin = delta.bodyMargin !== undefined ? delta.bodyMargin : this._layoutInfo.bodyMargin;
488
const outputMetadataHeight = delta.outputMetadataHeight !== undefined ? delta.outputMetadataHeight : this._layoutInfo.outputMetadataHeight;
489
const outputHeight = this.ignoreOutputs ? 0 : (delta.recomputeOutput || delta.rawOutputHeight !== undefined || delta.outputMetadataHeight !== undefined) ? this._getOutputTotalHeight(rawOutputHeight, outputMetadataHeight) : this._layoutInfo.outputTotalHeight;
490
491
const totalHeight = editorHeight
492
+ editorMargin
493
+ cellStatusHeight
494
+ metadataHeight
495
+ metadataStatusHeight
496
+ outputHeight
497
+ outputStatusHeight
498
+ bodyMargin;
499
500
const newLayout: IDiffElementLayoutInfo = {
501
width: width,
502
editorHeight: editorHeight,
503
editorMargin: editorMargin,
504
metadataHeight: metadataHeight,
505
cellStatusHeight,
506
metadataStatusHeight: metadataStatusHeight,
507
outputTotalHeight: outputHeight,
508
outputStatusHeight: outputStatusHeight,
509
bodyMargin: bodyMargin,
510
rawOutputHeight: rawOutputHeight,
511
outputMetadataHeight: outputMetadataHeight,
512
totalHeight: totalHeight,
513
layoutState: CellLayoutState.Measured
514
};
515
516
let somethingChanged = false;
517
518
const changeEvent: CellDiffViewModelLayoutChangeEvent = {};
519
520
if (newLayout.width !== this._layoutInfo.width) {
521
changeEvent.width = true;
522
somethingChanged = true;
523
}
524
525
if (newLayout.editorHeight !== this._layoutInfo.editorHeight) {
526
changeEvent.editorHeight = true;
527
somethingChanged = true;
528
}
529
530
if (newLayout.editorMargin !== this._layoutInfo.editorMargin) {
531
changeEvent.editorMargin = true;
532
somethingChanged = true;
533
}
534
535
if (newLayout.metadataHeight !== this._layoutInfo.metadataHeight) {
536
changeEvent.metadataHeight = true;
537
somethingChanged = true;
538
}
539
540
if (newLayout.cellStatusHeight !== this._layoutInfo.cellStatusHeight) {
541
changeEvent.cellStatusHeight = true;
542
somethingChanged = true;
543
}
544
545
if (newLayout.metadataStatusHeight !== this._layoutInfo.metadataStatusHeight) {
546
changeEvent.metadataStatusHeight = true;
547
somethingChanged = true;
548
}
549
550
if (newLayout.outputTotalHeight !== this._layoutInfo.outputTotalHeight) {
551
changeEvent.outputTotalHeight = true;
552
somethingChanged = true;
553
}
554
555
if (newLayout.outputStatusHeight !== this._layoutInfo.outputStatusHeight) {
556
changeEvent.outputStatusHeight = true;
557
somethingChanged = true;
558
}
559
560
if (newLayout.bodyMargin !== this._layoutInfo.bodyMargin) {
561
changeEvent.bodyMargin = true;
562
somethingChanged = true;
563
}
564
565
if (newLayout.outputMetadataHeight !== this._layoutInfo.outputMetadataHeight) {
566
changeEvent.outputMetadataHeight = true;
567
somethingChanged = true;
568
}
569
570
if (newLayout.totalHeight !== this._layoutInfo.totalHeight) {
571
changeEvent.totalHeight = true;
572
somethingChanged = true;
573
}
574
575
if (somethingChanged) {
576
this._layoutInfo = newLayout;
577
this._fireLayoutChangeEvent(changeEvent);
578
}
579
}
580
581
getHeight(lineHeight: number) {
582
if (this._layoutInfo.layoutState === CellLayoutState.Uninitialized) {
583
const editorHeight = this.cellFoldingState === PropertyFoldingState.Collapsed ? 0 : this.computeInputEditorHeight(lineHeight);
584
return this._computeTotalHeight(editorHeight);
585
} else {
586
return this._layoutInfo.totalHeight;
587
}
588
}
589
590
private _computeTotalHeight(editorHeight: number) {
591
const totalHeight = editorHeight
592
+ this._layoutInfo.editorMargin
593
+ this._layoutInfo.metadataHeight
594
+ this._layoutInfo.cellStatusHeight
595
+ this._layoutInfo.metadataStatusHeight
596
+ this._layoutInfo.outputTotalHeight
597
+ this._layoutInfo.outputStatusHeight
598
+ this._layoutInfo.outputMetadataHeight
599
+ this._layoutInfo.bodyMargin;
600
601
return totalHeight;
602
}
603
604
public computeInputEditorHeight(lineHeight: number): number {
605
const lineCount = Math.max(this.original?.textModel.textBuffer.getLineCount() ?? 1, this.modified?.textModel.textBuffer.getLineCount() ?? 1);
606
return this.diffEditorHeightCalculator.computeHeightFromLines(lineCount);
607
}
608
609
private _getOutputTotalHeight(rawOutputHeight: number, metadataHeight: number) {
610
if (this.outputFoldingState === PropertyFoldingState.Collapsed) {
611
return 0;
612
}
613
614
if (this.renderOutput) {
615
if (this.isOutputEmpty()) {
616
// single line;
617
return 24;
618
}
619
return this.getRichOutputTotalHeight() + metadataHeight;
620
} else {
621
return rawOutputHeight;
622
}
623
}
624
625
private _fireLayoutChangeEvent(state: CellDiffViewModelLayoutChangeEvent) {
626
this._layoutInfoEmitter.fire(state);
627
this.editorEventDispatcher.emit([{ type: NotebookDiffViewEventType.CellLayoutChanged, source: this._layoutInfo }]);
628
}
629
630
abstract checkIfInputModified(): false | { reason: string | undefined };
631
abstract checkIfOutputsModified(): false | { reason: string | undefined };
632
abstract checkMetadataIfModified(): false | { reason: string | undefined };
633
abstract isOutputEmpty(): boolean;
634
abstract getRichOutputTotalHeight(): number;
635
abstract getCellByUri(cellUri: URI): IGenericCellViewModel;
636
abstract getOutputOffsetInCell(diffSide: DiffSide, index: number): number;
637
abstract getOutputOffsetInContainer(diffSide: DiffSide, index: number): number;
638
abstract updateOutputHeight(diffSide: DiffSide, index: number, height: number): void;
639
abstract getNestedCellViewModel(diffSide: DiffSide): DiffNestedCellViewModel;
640
641
getComputedCellContainerWidth(layoutInfo: NotebookLayoutInfo, diffEditor: boolean, fullWidth: boolean) {
642
if (fullWidth) {
643
return layoutInfo.width - 2 * DIFF_CELL_MARGIN + (diffEditor ? DiffEditorWidget.ENTIRE_DIFF_OVERVIEW_WIDTH : 0) - 2;
644
}
645
646
return (layoutInfo.width - 2 * DIFF_CELL_MARGIN + (diffEditor ? DiffEditorWidget.ENTIRE_DIFF_OVERVIEW_WIDTH : 0)) / 2 - 18 - 2;
647
}
648
649
getOutputEditorViewState(): editorCommon.ICodeEditorViewState | editorCommon.IDiffEditorViewState | null {
650
return this._outputEditorViewState;
651
}
652
653
saveOutputEditorViewState(viewState: editorCommon.ICodeEditorViewState | editorCommon.IDiffEditorViewState | null) {
654
this._outputEditorViewState = viewState;
655
}
656
657
getMetadataEditorViewState(): editorCommon.ICodeEditorViewState | editorCommon.IDiffEditorViewState | null {
658
return this._metadataEditorViewState;
659
}
660
661
saveMetadataEditorViewState(viewState: editorCommon.ICodeEditorViewState | editorCommon.IDiffEditorViewState | null) {
662
this._metadataEditorViewState = viewState;
663
}
664
665
getSourceEditorViewState(): editorCommon.ICodeEditorViewState | editorCommon.IDiffEditorViewState | null {
666
return this._sourceEditorViewState;
667
}
668
669
saveSpirceEditorViewState(viewState: editorCommon.ICodeEditorViewState | editorCommon.IDiffEditorViewState | null) {
670
this._sourceEditorViewState = viewState;
671
}
672
}
673
674
export class SideBySideDiffElementViewModel extends DiffElementCellViewModelBase {
675
get originalDocument() {
676
return this.otherDocumentTextModel;
677
}
678
679
get modifiedDocument() {
680
return this.mainDocumentTextModel;
681
}
682
683
declare readonly original: DiffNestedCellViewModel;
684
declare readonly modified: DiffNestedCellViewModel;
685
override readonly type: 'unchanged' | 'modified';
686
687
/**
688
* The height of the editor when the unchanged lines are collapsed.
689
*/
690
private editorHeightWithUnchangedLinesCollapsed?: number;
691
constructor(
692
mainDocumentTextModel: NotebookTextModel,
693
readonly otherDocumentTextModel: NotebookTextModel,
694
original: NotebookCellTextModel,
695
modified: NotebookCellTextModel,
696
type: 'unchanged' | 'modified',
697
editorEventDispatcher: NotebookDiffEditorEventDispatcher,
698
initData: {
699
metadataStatusHeight: number;
700
outputStatusHeight: number;
701
fontInfo: FontInfo | undefined;
702
},
703
notebookService: INotebookService,
704
configurationService: IConfigurationService,
705
index: number,
706
diffEditorHeightCalculator: IDiffEditorHeightCalculatorService
707
) {
708
super(
709
mainDocumentTextModel,
710
original,
711
modified,
712
type,
713
editorEventDispatcher,
714
initData,
715
notebookService,
716
index,
717
configurationService,
718
diffEditorHeightCalculator);
719
720
this.type = type;
721
722
this.cellFoldingState = this.modified.textModel.getValue() !== this.original.textModel.getValue() ? PropertyFoldingState.Expanded : PropertyFoldingState.Collapsed;
723
this.metadataFoldingState = PropertyFoldingState.Collapsed;
724
this.outputFoldingState = PropertyFoldingState.Collapsed;
725
726
if (this.checkMetadataIfModified()) {
727
this.metadataFoldingState = PropertyFoldingState.Expanded;
728
}
729
730
if (this.checkIfOutputsModified()) {
731
this.outputFoldingState = PropertyFoldingState.Expanded;
732
}
733
734
this._register(this.original.onDidChangeOutputLayout(() => {
735
this._layout({ recomputeOutput: true });
736
}));
737
738
this._register(this.modified.onDidChangeOutputLayout(() => {
739
this._layout({ recomputeOutput: true });
740
}));
741
742
this._register(this.modified.textModel.onDidChangeContent(() => {
743
if (mainDocumentTextModel.transientOptions.cellContentMetadata) {
744
const cellMetadataKeys = [...Object.keys(mainDocumentTextModel.transientOptions.cellContentMetadata)];
745
const modifiedMedataRaw = Object.assign({}, this.modified.metadata);
746
const originalCellMetadata = this.original.metadata;
747
for (const key of cellMetadataKeys) {
748
if (key in originalCellMetadata) {
749
modifiedMedataRaw[key] = originalCellMetadata[key];
750
}
751
}
752
753
this.modified.textModel.metadata = modifiedMedataRaw;
754
}
755
}));
756
}
757
758
override checkIfInputModified(): false | { reason: string | undefined } {
759
if (this.original.textModel.getTextBufferHash() === this.modified.textModel.getTextBufferHash()) {
760
return false;
761
}
762
return {
763
reason: 'Cell content has changed',
764
};
765
}
766
checkIfOutputsModified() {
767
if (this.mainDocumentTextModel.transientOptions.transientOutputs || this.ignoreOutputs) {
768
return false;
769
}
770
771
const ret = outputsEqual(this.original?.outputs ?? [], this.modified?.outputs ?? []);
772
773
if (ret === OutputComparison.Unchanged) {
774
return false;
775
}
776
777
return {
778
reason: ret === OutputComparison.Metadata ? 'Output metadata has changed' : undefined,
779
kind: ret
780
};
781
}
782
783
checkMetadataIfModified() {
784
if (this.ignoreMetadata) {
785
return false;
786
}
787
const modified = hash(getFormattedMetadataJSON(this.mainDocumentTextModel.transientOptions.transientCellMetadata, this.original?.metadata || {}, this.original?.language)) !== hash(getFormattedMetadataJSON(this.mainDocumentTextModel.transientOptions.transientCellMetadata, this.modified?.metadata ?? {}, this.modified?.language));
788
if (modified) {
789
return { reason: undefined };
790
} else {
791
return false;
792
}
793
}
794
795
updateOutputHeight(diffSide: DiffSide, index: number, height: number) {
796
if (diffSide === DiffSide.Original) {
797
this.original.updateOutputHeight(index, height);
798
} else {
799
this.modified.updateOutputHeight(index, height);
800
}
801
}
802
803
getOutputOffsetInContainer(diffSide: DiffSide, index: number) {
804
if (diffSide === DiffSide.Original) {
805
return this.original.getOutputOffset(index);
806
} else {
807
return this.modified.getOutputOffset(index);
808
}
809
}
810
811
getOutputOffsetInCell(diffSide: DiffSide, index: number) {
812
const offsetInOutputsContainer = this.getOutputOffsetInContainer(diffSide, index);
813
814
return this._layoutInfo.editorHeight
815
+ this._layoutInfo.editorMargin
816
+ this._layoutInfo.metadataHeight
817
+ this._layoutInfo.cellStatusHeight
818
+ this._layoutInfo.metadataStatusHeight
819
+ this._layoutInfo.outputStatusHeight
820
+ this._layoutInfo.bodyMargin / 2
821
+ offsetInOutputsContainer;
822
}
823
824
isOutputEmpty() {
825
if (this.mainDocumentTextModel.transientOptions.transientOutputs) {
826
return true;
827
}
828
829
if (this.checkIfOutputsModified()) {
830
return false;
831
}
832
833
// outputs are not changed
834
835
return (this.original?.outputs || []).length === 0;
836
}
837
838
getRichOutputTotalHeight() {
839
return Math.max(this.original.getOutputTotalHeight(), this.modified.getOutputTotalHeight());
840
}
841
842
getNestedCellViewModel(diffSide: DiffSide): DiffNestedCellViewModel {
843
return diffSide === DiffSide.Original ? this.original : this.modified;
844
}
845
846
getCellByUri(cellUri: URI): IGenericCellViewModel {
847
if (cellUri.toString() === this.original.uri.toString()) {
848
return this.original;
849
} else {
850
return this.modified;
851
}
852
}
853
854
public override computeInputEditorHeight(lineHeight: number): number {
855
if (this.type === 'modified' &&
856
typeof this.editorHeightWithUnchangedLinesCollapsed === 'number' &&
857
this.checkIfInputModified()) {
858
return this.editorHeightWithUnchangedLinesCollapsed;
859
}
860
861
return super.computeInputEditorHeight(lineHeight);
862
}
863
864
private async computeModifiedInputEditorHeight() {
865
if (this.checkIfInputModified()) {
866
this.editorHeightWithUnchangedLinesCollapsed = this._layoutInfo.editorHeight = await this.diffEditorHeightCalculator.diffAndComputeHeight(this.original.uri, this.modified.uri);
867
}
868
}
869
870
private async computeModifiedMetadataEditorHeight() {
871
if (this.checkMetadataIfModified()) {
872
const originalMetadataUri = CellUri.generateCellPropertyUri(this.originalDocument.uri, this.original.handle, Schemas.vscodeNotebookCellMetadata);
873
const modifiedMetadataUri = CellUri.generateCellPropertyUri(this.modifiedDocument.uri, this.modified.handle, Schemas.vscodeNotebookCellMetadata);
874
this._layoutInfo.metadataHeight = await this.diffEditorHeightCalculator.diffAndComputeHeight(originalMetadataUri, modifiedMetadataUri);
875
}
876
}
877
878
public async computeEditorHeights() {
879
if (this.type === 'unchanged') {
880
return;
881
}
882
883
await Promise.all([this.computeModifiedInputEditorHeight(), this.computeModifiedMetadataEditorHeight()]);
884
}
885
886
}
887
888
export class SingleSideDiffElementViewModel extends DiffElementCellViewModelBase {
889
get cellViewModel() {
890
return this.type === 'insert' ? this.modified! : this.original!;
891
}
892
893
get originalDocument() {
894
if (this.type === 'insert') {
895
return this.otherDocumentTextModel;
896
} else {
897
return this.mainDocumentTextModel;
898
}
899
}
900
901
get modifiedDocument() {
902
if (this.type === 'insert') {
903
return this.mainDocumentTextModel;
904
} else {
905
return this.otherDocumentTextModel;
906
}
907
}
908
909
override readonly type: 'insert' | 'delete';
910
911
constructor(
912
mainDocumentTextModel: NotebookTextModel,
913
readonly otherDocumentTextModel: NotebookTextModel,
914
original: NotebookCellTextModel | undefined,
915
modified: NotebookCellTextModel | undefined,
916
type: 'insert' | 'delete',
917
editorEventDispatcher: NotebookDiffEditorEventDispatcher,
918
initData: {
919
metadataStatusHeight: number;
920
outputStatusHeight: number;
921
fontInfo: FontInfo | undefined;
922
},
923
notebookService: INotebookService,
924
configurationService: IConfigurationService,
925
diffEditorHeightCalculator: IDiffEditorHeightCalculatorService,
926
index: number
927
) {
928
super(mainDocumentTextModel, original, modified, type, editorEventDispatcher, initData, notebookService, index, configurationService, diffEditorHeightCalculator);
929
this.type = type;
930
931
this._register(this.cellViewModel.onDidChangeOutputLayout(() => {
932
this._layout({ recomputeOutput: true });
933
}));
934
}
935
936
override checkIfInputModified(): false | { reason: string | undefined } {
937
return {
938
reason: 'Cell content has changed',
939
};
940
}
941
942
getNestedCellViewModel(diffSide: DiffSide): DiffNestedCellViewModel {
943
return this.type === 'insert' ? this.modified! : this.original!;
944
}
945
946
947
checkIfOutputsModified(): false | { reason: string | undefined } {
948
return false;
949
}
950
951
checkMetadataIfModified(): false | { reason: string | undefined } {
952
return false;
953
}
954
955
updateOutputHeight(diffSide: DiffSide, index: number, height: number) {
956
this.cellViewModel?.updateOutputHeight(index, height);
957
}
958
959
getOutputOffsetInContainer(diffSide: DiffSide, index: number) {
960
return this.cellViewModel.getOutputOffset(index);
961
}
962
963
getOutputOffsetInCell(diffSide: DiffSide, index: number) {
964
const offsetInOutputsContainer = this.cellViewModel.getOutputOffset(index);
965
966
return this._layoutInfo.editorHeight
967
+ this._layoutInfo.editorMargin
968
+ this._layoutInfo.metadataHeight
969
+ this._layoutInfo.cellStatusHeight
970
+ this._layoutInfo.metadataStatusHeight
971
+ this._layoutInfo.outputStatusHeight
972
+ this._layoutInfo.bodyMargin / 2
973
+ offsetInOutputsContainer;
974
}
975
976
isOutputEmpty() {
977
if (this.mainDocumentTextModel.transientOptions.transientOutputs) {
978
return true;
979
}
980
981
// outputs are not changed
982
983
return (this.original?.outputs || this.modified?.outputs || []).length === 0;
984
}
985
986
getRichOutputTotalHeight() {
987
return this.cellViewModel?.getOutputTotalHeight() ?? 0;
988
}
989
990
getCellByUri(cellUri: URI): IGenericCellViewModel {
991
return this.cellViewModel;
992
}
993
}
994
995
export const enum OutputComparison {
996
Unchanged = 0,
997
Metadata = 1,
998
Other = 2
999
}
1000
1001
export function outputEqual(a: ICellOutput, b: ICellOutput): OutputComparison {
1002
if (hash(a.metadata) === hash(b.metadata)) {
1003
return OutputComparison.Other;
1004
}
1005
1006
// metadata not equal
1007
for (let j = 0; j < a.outputs.length; j++) {
1008
const aOutputItem = a.outputs[j];
1009
const bOutputItem = b.outputs[j];
1010
1011
if (aOutputItem.mime !== bOutputItem.mime) {
1012
return OutputComparison.Other;
1013
}
1014
1015
if (aOutputItem.data.buffer.length !== bOutputItem.data.buffer.length) {
1016
return OutputComparison.Other;
1017
}
1018
1019
for (let k = 0; k < aOutputItem.data.buffer.length; k++) {
1020
if (aOutputItem.data.buffer[k] !== bOutputItem.data.buffer[k]) {
1021
return OutputComparison.Other;
1022
}
1023
}
1024
}
1025
1026
return OutputComparison.Metadata;
1027
}
1028
1029
function outputsEqual(original: ICellOutput[], modified: ICellOutput[]) {
1030
if (original.length !== modified.length) {
1031
return OutputComparison.Other;
1032
}
1033
1034
const len = original.length;
1035
for (let i = 0; i < len; i++) {
1036
const a = original[i];
1037
const b = modified[i];
1038
1039
if (hash(a.metadata) !== hash(b.metadata)) {
1040
return OutputComparison.Metadata;
1041
}
1042
1043
if (a.outputs.length !== b.outputs.length) {
1044
return OutputComparison.Other;
1045
}
1046
1047
for (let j = 0; j < a.outputs.length; j++) {
1048
const aOutputItem = a.outputs[j];
1049
const bOutputItem = b.outputs[j];
1050
1051
if (aOutputItem.mime !== bOutputItem.mime) {
1052
return OutputComparison.Other;
1053
}
1054
1055
if (aOutputItem.data.buffer.length !== bOutputItem.data.buffer.length) {
1056
return OutputComparison.Other;
1057
}
1058
1059
for (let k = 0; k < aOutputItem.data.buffer.length; k++) {
1060
if (aOutputItem.data.buffer[k] !== bOutputItem.data.buffer[k]) {
1061
return OutputComparison.Other;
1062
}
1063
}
1064
}
1065
}
1066
1067
return OutputComparison.Unchanged;
1068
}
1069
1070
export function getStreamOutputData(outputs: IOutputItemDto[]) {
1071
if (!outputs.length) {
1072
return null;
1073
}
1074
1075
const first = outputs[0];
1076
const mime = first.mime;
1077
const sameStream = !outputs.find(op => op.mime !== mime);
1078
1079
if (sameStream) {
1080
return outputs.map(opit => opit.data.toString()).join('');
1081
} else {
1082
return null;
1083
}
1084
}
1085
1086
export function getFormattedOutputJSON(outputs: IOutputDto[]) {
1087
if (outputs.length === 1) {
1088
const streamOutputData = getStreamOutputData(outputs[0].outputs);
1089
if (streamOutputData) {
1090
return streamOutputData;
1091
}
1092
}
1093
1094
return JSON.stringify(outputs.map(output => {
1095
return ({
1096
metadata: output.metadata,
1097
outputItems: output.outputs.map(opit => ({
1098
mimeType: opit.mime,
1099
data: opit.data.toString()
1100
}))
1101
});
1102
}), undefined, '\t');
1103
}
1104
1105