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
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, 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
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
};
202
}
203
204
updateExecutionState(e: ICellExecutionStateChangedEvent) {
205
if (e.changed) {
206
this.executionErrorDiagnostic.set(undefined, undefined);
207
this._onDidStartExecution.fire(e);
208
} else {
209
this._onDidStopExecution.fire(e);
210
}
211
}
212
213
override updateOptions(e: NotebookOptionsChangeEvent) {
214
super.updateOptions(e);
215
if (e.cellStatusBarVisibility || e.insertToolbarPosition || e.cellToolbarLocation) {
216
this.layoutChange({});
217
}
218
}
219
220
pauseLayout() {
221
this._pauseableEmitter.pause();
222
}
223
224
resumeLayout() {
225
this._pauseableEmitter.resume();
226
}
227
228
layoutChange(state: CodeCellLayoutChangeEvent, source?: string) {
229
// recompute
230
this._ensureOutputsTop();
231
const notebookLayoutConfiguration = this.viewContext.notebookOptions.getLayoutConfiguration();
232
const bottomToolbarDimensions = this.viewContext.notebookOptions.computeBottomToolbarDimensions(this.viewType);
233
const outputShowMoreContainerHeight = state.outputShowMoreContainerHeight ? state.outputShowMoreContainerHeight : this._layoutInfo.outputShowMoreContainerHeight;
234
const outputTotalHeight = Math.max(this._outputMinHeight, this.isOutputCollapsed ? notebookLayoutConfiguration.collapsedIndicatorHeight : this._outputsTop!.getTotalSum());
235
const commentHeight = state.commentHeight ? this._commentHeight : this._layoutInfo.commentHeight;
236
237
const originalLayout = this.layoutInfo;
238
if (!this.isInputCollapsed) {
239
let newState: CellLayoutState;
240
let editorHeight: number;
241
let totalHeight: number;
242
let hasHorizontalScrolling = false;
243
const chatHeight = state.chatHeight ? this._chatHeight : this._layoutInfo.chatHeight;
244
if (!state.editorHeight && this._layoutInfo.layoutState === CellLayoutState.FromCache && !state.outputHeight) {
245
// No new editorHeight info - keep cached totalHeight and estimate editorHeight
246
const estimate = this.estimateEditorHeight(state.font?.lineHeight ?? this._layoutInfo.fontInfo?.lineHeight);
247
editorHeight = estimate.editorHeight;
248
hasHorizontalScrolling = estimate.hasHorizontalScrolling;
249
totalHeight = this._layoutInfo.totalHeight;
250
newState = CellLayoutState.FromCache;
251
} else if (state.editorHeight || this._layoutInfo.layoutState === CellLayoutState.Measured) {
252
// Editor has been measured
253
editorHeight = this._editorHeight;
254
totalHeight = this.computeTotalHeight(this._editorHeight, outputTotalHeight, outputShowMoreContainerHeight, chatHeight);
255
newState = CellLayoutState.Measured;
256
hasHorizontalScrolling = this._layoutInfo.estimatedHasHorizontalScrolling;
257
} else {
258
const estimate = this.estimateEditorHeight(state.font?.lineHeight ?? this._layoutInfo.fontInfo?.lineHeight);
259
editorHeight = estimate.editorHeight;
260
hasHorizontalScrolling = estimate.hasHorizontalScrolling;
261
totalHeight = this.computeTotalHeight(editorHeight, outputTotalHeight, outputShowMoreContainerHeight, chatHeight);
262
newState = CellLayoutState.Estimated;
263
}
264
265
const statusBarHeight = this.viewContext.notebookOptions.computeEditorStatusbarHeight(this.internalMetadata, this.uri);
266
const codeIndicatorHeight = editorHeight + statusBarHeight;
267
const outputIndicatorHeight = outputTotalHeight + outputShowMoreContainerHeight;
268
const outputContainerOffset = notebookLayoutConfiguration.editorToolbarHeight
269
+ notebookLayoutConfiguration.cellTopMargin // CELL_TOP_MARGIN
270
+ chatHeight
271
+ editorHeight
272
+ statusBarHeight;
273
const outputShowMoreContainerOffset = totalHeight
274
- bottomToolbarDimensions.bottomToolbarGap
275
- bottomToolbarDimensions.bottomToolbarHeight / 2
276
- outputShowMoreContainerHeight;
277
const bottomToolbarOffset = this.viewContext.notebookOptions.computeBottomToolbarOffset(totalHeight, this.viewType);
278
const editorWidth = state.outerWidth !== undefined
279
? this.viewContext.notebookOptions.computeCodeCellEditorWidth(state.outerWidth)
280
: this._layoutInfo?.editorWidth;
281
282
this._layoutInfo = {
283
fontInfo: state.font ?? this._layoutInfo.fontInfo ?? null,
284
chatHeight,
285
editorHeight,
286
editorWidth,
287
statusBarHeight,
288
outputContainerOffset,
289
outputTotalHeight,
290
outputShowMoreContainerHeight,
291
outputShowMoreContainerOffset,
292
commentOffset: outputContainerOffset + outputTotalHeight,
293
commentHeight,
294
totalHeight,
295
codeIndicatorHeight,
296
outputIndicatorHeight,
297
bottomToolbarOffset,
298
layoutState: newState,
299
estimatedHasHorizontalScrolling: hasHorizontalScrolling
300
};
301
} else {
302
const codeIndicatorHeight = notebookLayoutConfiguration.collapsedIndicatorHeight;
303
const outputIndicatorHeight = outputTotalHeight + outputShowMoreContainerHeight;
304
const chatHeight = state.chatHeight ? this._chatHeight : this._layoutInfo.chatHeight;
305
306
const outputContainerOffset = notebookLayoutConfiguration.cellTopMargin + notebookLayoutConfiguration.collapsedIndicatorHeight;
307
const totalHeight =
308
notebookLayoutConfiguration.cellTopMargin
309
+ notebookLayoutConfiguration.collapsedIndicatorHeight
310
+ notebookLayoutConfiguration.cellBottomMargin //CELL_BOTTOM_MARGIN
311
+ bottomToolbarDimensions.bottomToolbarGap //BOTTOM_CELL_TOOLBAR_GAP
312
+ chatHeight
313
+ commentHeight
314
+ outputTotalHeight + outputShowMoreContainerHeight;
315
const outputShowMoreContainerOffset = totalHeight
316
- bottomToolbarDimensions.bottomToolbarGap
317
- bottomToolbarDimensions.bottomToolbarHeight / 2
318
- outputShowMoreContainerHeight;
319
const bottomToolbarOffset = this.viewContext.notebookOptions.computeBottomToolbarOffset(totalHeight, this.viewType);
320
const editorWidth = state.outerWidth !== undefined
321
? this.viewContext.notebookOptions.computeCodeCellEditorWidth(state.outerWidth)
322
: this._layoutInfo?.editorWidth;
323
324
this._layoutInfo = {
325
fontInfo: state.font ?? this._layoutInfo.fontInfo ?? null,
326
editorHeight: this._layoutInfo.editorHeight,
327
editorWidth,
328
chatHeight: chatHeight,
329
statusBarHeight: 0,
330
outputContainerOffset,
331
outputTotalHeight,
332
outputShowMoreContainerHeight,
333
outputShowMoreContainerOffset,
334
commentOffset: outputContainerOffset + outputTotalHeight,
335
commentHeight,
336
totalHeight,
337
codeIndicatorHeight,
338
outputIndicatorHeight,
339
bottomToolbarOffset,
340
layoutState: this._layoutInfo.layoutState,
341
estimatedHasHorizontalScrolling: false
342
};
343
}
344
345
this._fireOnDidChangeLayout({
346
...state,
347
totalHeight: this.layoutInfo.totalHeight !== originalLayout.totalHeight,
348
source,
349
});
350
}
351
352
private _fireOnDidChangeLayout(state: CodeCellLayoutChangeEvent) {
353
this._pauseableEmitter.fire(state);
354
}
355
356
override restoreEditorViewState(editorViewStates: editorCommon.ICodeEditorViewState | null, totalHeight?: number) {
357
super.restoreEditorViewState(editorViewStates);
358
if (totalHeight !== undefined && this._layoutInfo.layoutState !== CellLayoutState.Measured) {
359
this._layoutInfo = {
360
...this._layoutInfo,
361
totalHeight: totalHeight,
362
layoutState: CellLayoutState.FromCache,
363
};
364
}
365
}
366
367
getDynamicHeight() {
368
this._onLayoutInfoRead.fire();
369
return this._layoutInfo.totalHeight;
370
}
371
372
getHeight(lineHeight: number) {
373
if (this._layoutInfo.layoutState === CellLayoutState.Uninitialized) {
374
const estimate = this.estimateEditorHeight(lineHeight);
375
return this.computeTotalHeight(estimate.editorHeight, 0, 0, 0);
376
} else {
377
return this._layoutInfo.totalHeight;
378
}
379
}
380
381
private estimateEditorHeight(lineHeight: number | undefined = 20): { editorHeight: number; hasHorizontalScrolling: boolean } {
382
let hasHorizontalScrolling = false;
383
const cellEditorOptions = this.viewContext.getBaseCellEditorOptions(this.language);
384
if (this.layoutInfo.fontInfo && cellEditorOptions.value.wordWrap === 'off') {
385
for (let i = 0; i < this.lineCount; i++) {
386
const max = this.textBuffer.getLineLastNonWhitespaceColumn(i + 1);
387
const estimatedWidth = max * (this.layoutInfo.fontInfo.typicalHalfwidthCharacterWidth + this.layoutInfo.fontInfo.letterSpacing);
388
if (estimatedWidth > this.layoutInfo.editorWidth) {
389
hasHorizontalScrolling = true;
390
break;
391
}
392
}
393
}
394
395
const verticalScrollbarHeight = hasHorizontalScrolling ? 12 : 0; // take zoom level into account
396
const editorPadding = this.viewContext.notebookOptions.computeEditorPadding(this.internalMetadata, this.uri);
397
const editorHeight = this.lineCount * lineHeight
398
+ editorPadding.top
399
+ editorPadding.bottom // EDITOR_BOTTOM_PADDING
400
+ verticalScrollbarHeight;
401
return {
402
editorHeight,
403
hasHorizontalScrolling
404
};
405
}
406
407
private computeTotalHeight(editorHeight: number, outputsTotalHeight: number, outputShowMoreContainerHeight: number, chatHeight: number): number {
408
const layoutConfiguration = this.viewContext.notebookOptions.getLayoutConfiguration();
409
const { bottomToolbarGap } = this.viewContext.notebookOptions.computeBottomToolbarDimensions(this.viewType);
410
return layoutConfiguration.editorToolbarHeight
411
+ layoutConfiguration.cellTopMargin
412
+ chatHeight
413
+ editorHeight
414
+ this.viewContext.notebookOptions.computeEditorStatusbarHeight(this.internalMetadata, this.uri)
415
+ this._commentHeight
416
+ outputsTotalHeight
417
+ outputShowMoreContainerHeight
418
+ bottomToolbarGap
419
+ layoutConfiguration.cellBottomMargin;
420
}
421
422
protected onDidChangeTextModelContent(): void {
423
if (this.getEditState() !== CellEditState.Editing) {
424
this.updateEditState(CellEditState.Editing, 'onDidChangeTextModelContent');
425
this._onDidChangeState.fire({ contentChanged: true });
426
}
427
}
428
429
onDeselect() {
430
this.updateEditState(CellEditState.Preview, 'onDeselect');
431
}
432
433
updateOutputShowMoreContainerHeight(height: number) {
434
this.layoutChange({ outputShowMoreContainerHeight: height }, 'CodeCellViewModel#updateOutputShowMoreContainerHeight');
435
}
436
437
updateOutputMinHeight(height: number) {
438
this.outputMinHeight = height;
439
}
440
441
unlockOutputHeight() {
442
this.outputMinHeight = 0;
443
this.layoutChange({ outputHeight: true });
444
}
445
446
updateOutputHeight(index: number, height: number, source?: string) {
447
if (index >= this._outputCollection.length) {
448
throw new Error('Output index out of range!');
449
}
450
451
this._ensureOutputsTop();
452
453
try {
454
if (index === 0 || height > 0) {
455
this._outputViewModels[index].setVisible(true);
456
} else if (height === 0) {
457
this._outputViewModels[index].setVisible(false);
458
}
459
} catch (e) {
460
const errorMessage = `Failed to update output height for cell ${this.handle}, output ${index}. `
461
+ `this.outputCollection.length: ${this._outputCollection.length}, this._outputViewModels.length: ${this._outputViewModels.length}`;
462
throw new Error(`${errorMessage}.\n Error: ${e.message}`);
463
}
464
465
if (this._outputViewModels[index].visible.get() && height < 28) {
466
height = 28;
467
}
468
469
this._outputCollection[index] = height;
470
if (this._outputsTop!.setValue(index, height)) {
471
this.layoutChange({ outputHeight: true }, source);
472
}
473
}
474
475
getOutputOffsetInContainer(index: number) {
476
this._ensureOutputsTop();
477
478
if (index >= this._outputCollection.length) {
479
throw new Error('Output index out of range!');
480
}
481
482
return this._outputsTop!.getPrefixSum(index - 1);
483
}
484
485
getOutputOffset(index: number): number {
486
return this.layoutInfo.outputContainerOffset + this.getOutputOffsetInContainer(index);
487
}
488
489
spliceOutputHeights(start: number, deleteCnt: number, heights: number[]) {
490
this._ensureOutputsTop();
491
492
this._outputsTop!.removeValues(start, deleteCnt);
493
if (heights.length) {
494
const values = new Uint32Array(heights.length);
495
for (let i = 0; i < heights.length; i++) {
496
values[i] = heights[i];
497
}
498
499
this._outputsTop!.insertValues(start, values);
500
}
501
502
this.layoutChange({ outputHeight: true }, 'CodeCellViewModel#spliceOutputs');
503
}
504
505
private _ensureOutputsTop(): void {
506
if (!this._outputsTop) {
507
const values = new Uint32Array(this._outputCollection.length);
508
for (let i = 0; i < this._outputCollection.length; i++) {
509
values[i] = this._outputCollection[i];
510
}
511
512
this._outputsTop = new PrefixSumComputer(values);
513
}
514
}
515
516
private readonly _hasFindResult = this._register(new Emitter<boolean>());
517
public readonly hasFindResult: Event<boolean> = this._hasFindResult.event;
518
519
startFind(value: string, options: INotebookFindOptions): CellFindMatch | null {
520
const matches = super.cellStartFind(value, options);
521
522
if (matches === null) {
523
return null;
524
}
525
526
return {
527
cell: this,
528
contentMatches: matches
529
};
530
}
531
532
override dispose() {
533
super.dispose();
534
535
this._outputCollection = [];
536
this._outputsTop = null;
537
dispose(this._outputViewModels);
538
}
539
}
540
541