Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts
5221 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, Event, PauseableEmitter } from '../../../../../base/common/event.js';
7
import { dispose } from '../../../../../base/common/lifecycle.js';
8
import { observableValue } from '../../../../../base/common/observable.js';
9
import * as UUID from '../../../../../base/common/uuid.js';
10
import { ICodeEditorService } from '../../../../../editor/browser/services/codeEditorService.js';
11
import * as editorCommon from '../../../../../editor/common/editorCommon.js';
12
import { PrefixSumComputer } from '../../../../../editor/common/model/prefixSumComputer.js';
13
import { ITextModelService } from '../../../../../editor/common/services/resolverService.js';
14
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
15
import { IUndoRedoService } from '../../../../../platform/undoRedo/common/undoRedo.js';
16
import { CellEditState, CellFindMatch, CellLayoutState, CodeCellLayoutChangeEvent, CodeCellLayoutInfo, ICellOutputViewModel, ICellViewModel } from '../notebookBrowser.js';
17
import { NotebookOptionsChangeEvent } from '../notebookOptions.js';
18
import { NotebookLayoutInfo } from '../notebookViewEvents.js';
19
import { CellOutputViewModel } from './cellOutputViewModel.js';
20
import { ViewContext } from './viewContext.js';
21
import { NotebookCellTextModel } from '../../common/model/notebookCellTextModel.js';
22
import { CellKind, INotebookFindOptions, NotebookCellOutputsSplice } from '../../common/notebookCommon.js';
23
import { ICellExecutionError, ICellExecutionStateChangedEvent } from '../../common/notebookExecutionStateService.js';
24
import { INotebookService } from '../../common/notebookService.js';
25
import { BaseCellViewModel } from './baseCellViewModel.js';
26
import { IInlineChatSessionService } from '../../../inlineChat/browser/inlineChatSessionService.js';
27
28
export const outputDisplayLimit = 500;
29
30
export class CodeCellViewModel extends BaseCellViewModel implements ICellViewModel {
31
readonly cellKind = CellKind.Code;
32
33
protected readonly _onLayoutInfoRead = this._register(new Emitter<void>());
34
readonly onLayoutInfoRead = this._onLayoutInfoRead.event;
35
36
protected readonly _onDidStartExecution = this._register(new Emitter<ICellExecutionStateChangedEvent>());
37
readonly onDidStartExecution = this._onDidStartExecution.event;
38
protected readonly _onDidStopExecution = this._register(new Emitter<ICellExecutionStateChangedEvent>());
39
readonly onDidStopExecution = this._onDidStopExecution.event;
40
41
protected readonly _onDidChangeOutputs = this._register(new Emitter<NotebookCellOutputsSplice>());
42
readonly onDidChangeOutputs = this._onDidChangeOutputs.event;
43
44
private readonly _onDidRemoveOutputs = this._register(new Emitter<readonly ICellOutputViewModel[]>());
45
readonly onDidRemoveOutputs = this._onDidRemoveOutputs.event;
46
47
private _outputCollection: number[] = [];
48
49
private _outputsTop: PrefixSumComputer | null = null;
50
51
protected _pauseableEmitter = this._register(new PauseableEmitter<CodeCellLayoutChangeEvent>());
52
53
readonly onDidChangeLayout = this._pauseableEmitter.event;
54
55
private _editorHeight = 0;
56
set editorHeight(height: number) {
57
if (this._editorHeight === height) {
58
return;
59
}
60
61
this._editorHeight = height;
62
this.layoutChange({ editorHeight: true }, 'CodeCellViewModel#editorHeight');
63
}
64
65
get editorHeight() {
66
throw new Error('editorHeight is write-only');
67
}
68
69
private _chatHeight = 0;
70
set chatHeight(height: number) {
71
if (this._chatHeight === height) {
72
return;
73
}
74
75
this._chatHeight = height;
76
this.layoutChange({ chatHeight: true }, 'CodeCellViewModel#chatHeight');
77
}
78
79
get chatHeight() {
80
return this._chatHeight;
81
}
82
83
private _hoveringOutput: boolean = false;
84
public get outputIsHovered(): boolean {
85
return this._hoveringOutput;
86
}
87
88
public set outputIsHovered(v: boolean) {
89
this._hoveringOutput = v;
90
this._onDidChangeState.fire({ outputIsHoveredChanged: true });
91
}
92
93
private _focusOnOutput: boolean = false;
94
public get outputIsFocused(): boolean {
95
return this._focusOnOutput;
96
}
97
98
public set outputIsFocused(v: boolean) {
99
this._focusOnOutput = v;
100
this._onDidChangeState.fire({ outputIsFocusedChanged: true });
101
}
102
103
private _focusInputInOutput: boolean = false;
104
public get inputInOutputIsFocused(): boolean {
105
return this._focusInputInOutput;
106
}
107
108
public set inputInOutputIsFocused(v: boolean) {
109
this._focusInputInOutput = v;
110
}
111
112
private _outputMinHeight: number = 0;
113
114
private get outputMinHeight() {
115
return this._outputMinHeight;
116
}
117
118
/**
119
* The minimum height of the output region. It's only set to non-zero temporarily when replacing an output with a new one.
120
* It's reset to 0 when the new output is rendered, or in one second.
121
*/
122
private set outputMinHeight(newMin: number) {
123
this._outputMinHeight = newMin;
124
}
125
126
private _layoutInfo: CodeCellLayoutInfo;
127
128
get layoutInfo() {
129
return this._layoutInfo;
130
}
131
132
private _outputViewModels: ICellOutputViewModel[];
133
134
get outputsViewModels() {
135
return this._outputViewModels;
136
}
137
138
readonly executionErrorDiagnostic = observableValue<ICellExecutionError | undefined>('excecutionError', undefined);
139
140
constructor(
141
viewType: string,
142
model: NotebookCellTextModel,
143
initialNotebookLayoutInfo: NotebookLayoutInfo | null,
144
readonly viewContext: ViewContext,
145
@IConfigurationService configurationService: IConfigurationService,
146
@INotebookService private readonly _notebookService: INotebookService,
147
@ITextModelService modelService: ITextModelService,
148
@IUndoRedoService undoRedoService: IUndoRedoService,
149
@ICodeEditorService codeEditorService: ICodeEditorService,
150
@IInlineChatSessionService inlineChatSessionService: IInlineChatSessionService
151
) {
152
super(viewType, model, UUID.generateUuid(), viewContext, configurationService, modelService, undoRedoService, codeEditorService, inlineChatSessionService);
153
this._outputViewModels = this.model.outputs.map(output => new CellOutputViewModel(this, output, this._notebookService));
154
155
this._register(this.model.onDidChangeOutputs((splice) => {
156
const removedOutputs: ICellOutputViewModel[] = [];
157
let outputLayoutChange = false;
158
for (let i = splice.start; i < splice.start + splice.deleteCount; i++) {
159
if (this._outputCollection[i] !== undefined && this._outputCollection[i] !== 0) {
160
outputLayoutChange = true;
161
}
162
}
163
164
this._outputCollection.splice(splice.start, splice.deleteCount, ...splice.newOutputs.map(() => 0));
165
removedOutputs.push(...this._outputViewModels.splice(splice.start, splice.deleteCount, ...splice.newOutputs.map(output => new CellOutputViewModel(this, output, this._notebookService))));
166
167
this._outputsTop = null;
168
this._onDidChangeOutputs.fire(splice);
169
this._onDidRemoveOutputs.fire(removedOutputs);
170
if (outputLayoutChange) {
171
this.layoutChange({ outputHeight: true }, 'CodeCellViewModel#model.onDidChangeOutputs');
172
}
173
if (!this._outputCollection.length) {
174
this.executionErrorDiagnostic.set(undefined, undefined);
175
}
176
dispose(removedOutputs);
177
}));
178
179
this._outputCollection = new Array(this.model.outputs.length);
180
const layoutConfiguration = this.viewContext.notebookOptions.getLayoutConfiguration();
181
this._layoutInfo = {
182
fontInfo: initialNotebookLayoutInfo?.fontInfo || null,
183
editorHeight: 0,
184
editorWidth: initialNotebookLayoutInfo
185
? this.viewContext.notebookOptions.computeCodeCellEditorWidth(initialNotebookLayoutInfo.width)
186
: 0,
187
chatHeight: 0,
188
statusBarHeight: 0,
189
commentOffset: 0,
190
commentHeight: 0,
191
outputContainerOffset: 0,
192
outputTotalHeight: 0,
193
outputShowMoreContainerHeight: 0,
194
outputShowMoreContainerOffset: 0,
195
totalHeight: this.computeTotalHeight(17, 0, 0, 0),
196
codeIndicatorHeight: 0,
197
outputIndicatorHeight: 0,
198
bottomToolbarOffset: 0,
199
layoutState: CellLayoutState.Uninitialized,
200
estimatedHasHorizontalScrolling: false,
201
outlineWidth: 1,
202
topMargin: layoutConfiguration.cellTopMargin,
203
bottomMargin: layoutConfiguration.cellBottomMargin,
204
};
205
}
206
207
updateExecutionState(e: ICellExecutionStateChangedEvent) {
208
if (e.changed) {
209
this.executionErrorDiagnostic.set(undefined, undefined);
210
this._onDidStartExecution.fire(e);
211
} else {
212
this._onDidStopExecution.fire(e);
213
}
214
}
215
216
override updateOptions(e: NotebookOptionsChangeEvent) {
217
super.updateOptions(e);
218
if (e.cellStatusBarVisibility || e.insertToolbarPosition || e.cellToolbarLocation) {
219
this.layoutChange({});
220
}
221
}
222
223
pauseLayout() {
224
this._pauseableEmitter.pause();
225
}
226
227
resumeLayout() {
228
this._pauseableEmitter.resume();
229
}
230
231
layoutChange(state: CodeCellLayoutChangeEvent, source?: string) {
232
// recompute
233
this._ensureOutputsTop();
234
const notebookLayoutConfiguration = this.viewContext.notebookOptions.getLayoutConfiguration();
235
const bottomToolbarDimensions = this.viewContext.notebookOptions.computeBottomToolbarDimensions(this.viewType);
236
const outputShowMoreContainerHeight = state.outputShowMoreContainerHeight ? state.outputShowMoreContainerHeight : this._layoutInfo.outputShowMoreContainerHeight;
237
const outputTotalHeight = Math.max(this._outputMinHeight, this.isOutputCollapsed ? notebookLayoutConfiguration.collapsedIndicatorHeight : this._outputsTop!.getTotalSum());
238
const commentHeight = state.commentHeight ? this._commentHeight : this._layoutInfo.commentHeight;
239
240
const originalLayout = this.layoutInfo;
241
if (!this.isInputCollapsed) {
242
let newState: CellLayoutState;
243
let editorHeight: number;
244
let totalHeight: number;
245
let hasHorizontalScrolling = false;
246
const chatHeight = state.chatHeight ? this._chatHeight : this._layoutInfo.chatHeight;
247
if (!state.editorHeight && this._layoutInfo.layoutState === CellLayoutState.FromCache && !state.outputHeight) {
248
// No new editorHeight info - keep cached totalHeight and estimate editorHeight
249
const estimate = this.estimateEditorHeight(state.font?.lineHeight ?? this._layoutInfo.fontInfo?.lineHeight);
250
editorHeight = estimate.editorHeight;
251
hasHorizontalScrolling = estimate.hasHorizontalScrolling;
252
totalHeight = this._layoutInfo.totalHeight;
253
newState = CellLayoutState.FromCache;
254
} else if (state.editorHeight || this._layoutInfo.layoutState === CellLayoutState.Measured) {
255
// Editor has been measured
256
editorHeight = this._editorHeight;
257
totalHeight = this.computeTotalHeight(this._editorHeight, outputTotalHeight, outputShowMoreContainerHeight, chatHeight);
258
newState = CellLayoutState.Measured;
259
hasHorizontalScrolling = this._layoutInfo.estimatedHasHorizontalScrolling;
260
} else {
261
const estimate = this.estimateEditorHeight(state.font?.lineHeight ?? this._layoutInfo.fontInfo?.lineHeight);
262
editorHeight = estimate.editorHeight;
263
hasHorizontalScrolling = estimate.hasHorizontalScrolling;
264
totalHeight = this.computeTotalHeight(editorHeight, outputTotalHeight, outputShowMoreContainerHeight, chatHeight);
265
newState = CellLayoutState.Estimated;
266
}
267
268
const statusBarHeight = this.viewContext.notebookOptions.computeEditorStatusbarHeight(this.internalMetadata, this.uri);
269
const codeIndicatorHeight = editorHeight + statusBarHeight;
270
const outputIndicatorHeight = outputTotalHeight + outputShowMoreContainerHeight;
271
const outputContainerOffset = notebookLayoutConfiguration.editorToolbarHeight
272
+ notebookLayoutConfiguration.cellTopMargin // CELL_TOP_MARGIN
273
+ chatHeight
274
+ editorHeight
275
+ statusBarHeight;
276
const outputShowMoreContainerOffset = totalHeight
277
- bottomToolbarDimensions.bottomToolbarGap
278
- bottomToolbarDimensions.bottomToolbarHeight / 2
279
- outputShowMoreContainerHeight;
280
const bottomToolbarOffset = this.viewContext.notebookOptions.computeBottomToolbarOffset(totalHeight, this.viewType);
281
const editorWidth = state.outerWidth !== undefined
282
? this.viewContext.notebookOptions.computeCodeCellEditorWidth(state.outerWidth)
283
: this._layoutInfo?.editorWidth;
284
285
this._layoutInfo = {
286
fontInfo: state.font ?? this._layoutInfo.fontInfo ?? null,
287
chatHeight,
288
editorHeight,
289
editorWidth,
290
statusBarHeight,
291
outputContainerOffset,
292
outputTotalHeight,
293
outputShowMoreContainerHeight,
294
outputShowMoreContainerOffset,
295
commentOffset: outputContainerOffset + outputTotalHeight,
296
commentHeight,
297
totalHeight,
298
codeIndicatorHeight,
299
outputIndicatorHeight,
300
bottomToolbarOffset,
301
layoutState: newState,
302
estimatedHasHorizontalScrolling: hasHorizontalScrolling,
303
topMargin: notebookLayoutConfiguration.cellTopMargin,
304
bottomMargin: notebookLayoutConfiguration.cellBottomMargin,
305
outlineWidth: 1
306
};
307
} else {
308
const codeIndicatorHeight = notebookLayoutConfiguration.collapsedIndicatorHeight;
309
const outputIndicatorHeight = outputTotalHeight + outputShowMoreContainerHeight;
310
const chatHeight = state.chatHeight ? this._chatHeight : this._layoutInfo.chatHeight;
311
312
const outputContainerOffset = notebookLayoutConfiguration.cellTopMargin + notebookLayoutConfiguration.collapsedIndicatorHeight;
313
const totalHeight =
314
notebookLayoutConfiguration.cellTopMargin
315
+ notebookLayoutConfiguration.collapsedIndicatorHeight
316
+ notebookLayoutConfiguration.cellBottomMargin //CELL_BOTTOM_MARGIN
317
+ bottomToolbarDimensions.bottomToolbarGap //BOTTOM_CELL_TOOLBAR_GAP
318
+ chatHeight
319
+ commentHeight
320
+ outputTotalHeight + outputShowMoreContainerHeight;
321
const outputShowMoreContainerOffset = totalHeight
322
- bottomToolbarDimensions.bottomToolbarGap
323
- bottomToolbarDimensions.bottomToolbarHeight / 2
324
- outputShowMoreContainerHeight;
325
const bottomToolbarOffset = this.viewContext.notebookOptions.computeBottomToolbarOffset(totalHeight, this.viewType);
326
const editorWidth = state.outerWidth !== undefined
327
? this.viewContext.notebookOptions.computeCodeCellEditorWidth(state.outerWidth)
328
: this._layoutInfo?.editorWidth;
329
330
this._layoutInfo = {
331
fontInfo: state.font ?? this._layoutInfo.fontInfo ?? null,
332
editorHeight: this._layoutInfo.editorHeight,
333
editorWidth,
334
chatHeight: chatHeight,
335
statusBarHeight: 0,
336
outputContainerOffset,
337
outputTotalHeight,
338
outputShowMoreContainerHeight,
339
outputShowMoreContainerOffset,
340
commentOffset: outputContainerOffset + outputTotalHeight,
341
commentHeight,
342
totalHeight,
343
codeIndicatorHeight,
344
outputIndicatorHeight,
345
bottomToolbarOffset,
346
layoutState: this._layoutInfo.layoutState,
347
estimatedHasHorizontalScrolling: false,
348
outlineWidth: 1,
349
topMargin: notebookLayoutConfiguration.cellTopMargin,
350
bottomMargin: notebookLayoutConfiguration.cellBottomMargin,
351
};
352
}
353
354
this._fireOnDidChangeLayout({
355
...state,
356
totalHeight: this.layoutInfo.totalHeight !== originalLayout.totalHeight,
357
source,
358
});
359
}
360
361
private _fireOnDidChangeLayout(state: CodeCellLayoutChangeEvent) {
362
this._pauseableEmitter.fire(state);
363
}
364
365
override restoreEditorViewState(editorViewStates: editorCommon.ICodeEditorViewState | null, totalHeight?: number) {
366
super.restoreEditorViewState(editorViewStates);
367
if (totalHeight !== undefined && this._layoutInfo.layoutState !== CellLayoutState.Measured) {
368
this._layoutInfo = {
369
...this._layoutInfo,
370
totalHeight: totalHeight,
371
layoutState: CellLayoutState.FromCache,
372
};
373
}
374
}
375
376
getDynamicHeight() {
377
this._onLayoutInfoRead.fire();
378
return this._layoutInfo.totalHeight;
379
}
380
381
getHeight(lineHeight: number) {
382
if (this._layoutInfo.layoutState === CellLayoutState.Uninitialized) {
383
const estimate = this.estimateEditorHeight(lineHeight);
384
return this.computeTotalHeight(estimate.editorHeight, 0, 0, 0);
385
} else {
386
return this._layoutInfo.totalHeight;
387
}
388
}
389
390
private estimateEditorHeight(lineHeight: number | undefined = 20): { editorHeight: number; hasHorizontalScrolling: boolean } {
391
let hasHorizontalScrolling = false;
392
const cellEditorOptions = this.viewContext.getBaseCellEditorOptions(this.language);
393
if (this.layoutInfo.fontInfo && cellEditorOptions.value.wordWrap === 'off') {
394
for (let i = 0; i < this.lineCount; i++) {
395
const max = this.textBuffer.getLineLastNonWhitespaceColumn(i + 1);
396
const estimatedWidth = max * (this.layoutInfo.fontInfo.typicalHalfwidthCharacterWidth + this.layoutInfo.fontInfo.letterSpacing);
397
if (estimatedWidth > this.layoutInfo.editorWidth) {
398
hasHorizontalScrolling = true;
399
break;
400
}
401
}
402
}
403
404
const verticalScrollbarHeight = hasHorizontalScrolling ? 12 : 0; // take zoom level into account
405
const editorPadding = this.viewContext.notebookOptions.computeEditorPadding(this.internalMetadata, this.uri);
406
const editorHeight = this.lineCount * lineHeight
407
+ editorPadding.top
408
+ editorPadding.bottom // EDITOR_BOTTOM_PADDING
409
+ verticalScrollbarHeight;
410
return {
411
editorHeight,
412
hasHorizontalScrolling
413
};
414
}
415
416
private computeTotalHeight(editorHeight: number, outputsTotalHeight: number, outputShowMoreContainerHeight: number, chatHeight: number): number {
417
const layoutConfiguration = this.viewContext.notebookOptions.getLayoutConfiguration();
418
const { bottomToolbarGap } = this.viewContext.notebookOptions.computeBottomToolbarDimensions(this.viewType);
419
return layoutConfiguration.editorToolbarHeight
420
+ layoutConfiguration.cellTopMargin
421
+ chatHeight
422
+ editorHeight
423
+ this.viewContext.notebookOptions.computeEditorStatusbarHeight(this.internalMetadata, this.uri)
424
+ this._commentHeight
425
+ outputsTotalHeight
426
+ outputShowMoreContainerHeight
427
+ bottomToolbarGap
428
+ layoutConfiguration.cellBottomMargin;
429
}
430
431
protected onDidChangeTextModelContent(): void {
432
if (this.getEditState() !== CellEditState.Editing) {
433
this.updateEditState(CellEditState.Editing, 'onDidChangeTextModelContent');
434
this._onDidChangeState.fire({ contentChanged: true });
435
}
436
}
437
438
onDeselect() {
439
this.updateEditState(CellEditState.Preview, 'onDeselect');
440
}
441
442
updateOutputShowMoreContainerHeight(height: number) {
443
this.layoutChange({ outputShowMoreContainerHeight: height }, 'CodeCellViewModel#updateOutputShowMoreContainerHeight');
444
}
445
446
updateOutputMinHeight(height: number) {
447
this.outputMinHeight = height;
448
}
449
450
unlockOutputHeight() {
451
this.outputMinHeight = 0;
452
this.layoutChange({ outputHeight: true });
453
}
454
455
updateOutputHeight(index: number, height: number, source?: string) {
456
if (index >= this._outputCollection.length) {
457
throw new Error('Output index out of range!');
458
}
459
460
this._ensureOutputsTop();
461
462
try {
463
if (index === 0 || height > 0) {
464
this._outputViewModels[index].setVisible(true);
465
} else if (height === 0) {
466
this._outputViewModels[index].setVisible(false);
467
}
468
} catch (e) {
469
const errorMessage = `Failed to update output height for cell ${this.handle}, output ${index}. `
470
+ `this.outputCollection.length: ${this._outputCollection.length}, this._outputViewModels.length: ${this._outputViewModels.length}`;
471
throw new Error(`${errorMessage}.\n Error: ${e.message}`);
472
}
473
474
if (this._outputViewModels[index].visible.get() && height < 28) {
475
height = 28;
476
}
477
478
this._outputCollection[index] = height;
479
if (this._outputsTop!.setValue(index, height)) {
480
this.layoutChange({ outputHeight: true }, source);
481
}
482
}
483
484
getOutputOffsetInContainer(index: number) {
485
this._ensureOutputsTop();
486
487
if (index >= this._outputCollection.length) {
488
throw new Error('Output index out of range!');
489
}
490
491
return this._outputsTop!.getPrefixSum(index - 1);
492
}
493
494
getOutputOffset(index: number): number {
495
return this.layoutInfo.outputContainerOffset + this.getOutputOffsetInContainer(index);
496
}
497
498
spliceOutputHeights(start: number, deleteCnt: number, heights: number[]) {
499
this._ensureOutputsTop();
500
501
this._outputsTop!.removeValues(start, deleteCnt);
502
if (heights.length) {
503
const values = new Uint32Array(heights.length);
504
for (let i = 0; i < heights.length; i++) {
505
values[i] = heights[i];
506
}
507
508
this._outputsTop!.insertValues(start, values);
509
}
510
511
this.layoutChange({ outputHeight: true }, 'CodeCellViewModel#spliceOutputs');
512
}
513
514
private _ensureOutputsTop(): void {
515
if (!this._outputsTop) {
516
const values = new Uint32Array(this._outputCollection.length);
517
for (let i = 0; i < this._outputCollection.length; i++) {
518
values[i] = this._outputCollection[i];
519
}
520
521
this._outputsTop = new PrefixSumComputer(values);
522
}
523
}
524
525
private readonly _hasFindResult = this._register(new Emitter<boolean>());
526
public readonly hasFindResult: Event<boolean> = this._hasFindResult.event;
527
528
startFind(value: string, options: INotebookFindOptions): CellFindMatch | null {
529
const matches = super.cellStartFind(value, options);
530
531
if (matches === null) {
532
return null;
533
}
534
535
return {
536
cell: this,
537
contentMatches: matches
538
};
539
}
540
541
override dispose() {
542
super.dispose();
543
544
this._outputCollection = [];
545
this._outputsTop = null;
546
dispose(this._outputViewModels);
547
}
548
}
549
550