Path: blob/main/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { Emitter, Event, PauseableEmitter } from '../../../../../base/common/event.js';6import { dispose } from '../../../../../base/common/lifecycle.js';7import { observableValue } from '../../../../../base/common/observable.js';8import * as UUID from '../../../../../base/common/uuid.js';9import { ICodeEditorService } from '../../../../../editor/browser/services/codeEditorService.js';10import * as editorCommon from '../../../../../editor/common/editorCommon.js';11import { PrefixSumComputer } from '../../../../../editor/common/model/prefixSumComputer.js';12import { ITextModelService } from '../../../../../editor/common/services/resolverService.js';13import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';14import { IUndoRedoService } from '../../../../../platform/undoRedo/common/undoRedo.js';15import { CellEditState, CellFindMatch, CellLayoutState, CodeCellLayoutChangeEvent, CodeCellLayoutInfo, ICellOutputViewModel, ICellViewModel } from '../notebookBrowser.js';16import { NotebookOptionsChangeEvent } from '../notebookOptions.js';17import { NotebookLayoutInfo } from '../notebookViewEvents.js';18import { CellOutputViewModel } from './cellOutputViewModel.js';19import { ViewContext } from './viewContext.js';20import { NotebookCellTextModel } from '../../common/model/notebookCellTextModel.js';21import { CellKind, INotebookFindOptions, NotebookCellOutputsSplice } from '../../common/notebookCommon.js';22import { ICellExecutionError, ICellExecutionStateChangedEvent } from '../../common/notebookExecutionStateService.js';23import { INotebookService } from '../../common/notebookService.js';24import { BaseCellViewModel } from './baseCellViewModel.js';25import { IInlineChatSessionService } from '../../../inlineChat/browser/inlineChatSessionService.js';2627export const outputDisplayLimit = 500;2829export class CodeCellViewModel extends BaseCellViewModel implements ICellViewModel {30readonly cellKind = CellKind.Code;3132protected readonly _onLayoutInfoRead = this._register(new Emitter<void>());33readonly onLayoutInfoRead = this._onLayoutInfoRead.event;3435protected readonly _onDidStartExecution = this._register(new Emitter<ICellExecutionStateChangedEvent>());36readonly onDidStartExecution = this._onDidStartExecution.event;37protected readonly _onDidStopExecution = this._register(new Emitter<ICellExecutionStateChangedEvent>());38readonly onDidStopExecution = this._onDidStopExecution.event;3940protected readonly _onDidChangeOutputs = this._register(new Emitter<NotebookCellOutputsSplice>());41readonly onDidChangeOutputs = this._onDidChangeOutputs.event;4243private readonly _onDidRemoveOutputs = this._register(new Emitter<readonly ICellOutputViewModel[]>());44readonly onDidRemoveOutputs = this._onDidRemoveOutputs.event;4546private _outputCollection: number[] = [];4748private _outputsTop: PrefixSumComputer | null = null;4950protected _pauseableEmitter = this._register(new PauseableEmitter<CodeCellLayoutChangeEvent>());5152readonly onDidChangeLayout = this._pauseableEmitter.event;5354private _editorHeight = 0;55set editorHeight(height: number) {56if (this._editorHeight === height) {57return;58}5960this._editorHeight = height;61this.layoutChange({ editorHeight: true }, 'CodeCellViewModel#editorHeight');62}6364get editorHeight() {65throw new Error('editorHeight is write-only');66}6768private _chatHeight = 0;69set chatHeight(height: number) {70if (this._chatHeight === height) {71return;72}7374this._chatHeight = height;75this.layoutChange({ chatHeight: true }, 'CodeCellViewModel#chatHeight');76}7778get chatHeight() {79return this._chatHeight;80}8182private _hoveringOutput: boolean = false;83public get outputIsHovered(): boolean {84return this._hoveringOutput;85}8687public set outputIsHovered(v: boolean) {88this._hoveringOutput = v;89this._onDidChangeState.fire({ outputIsHoveredChanged: true });90}9192private _focusOnOutput: boolean = false;93public get outputIsFocused(): boolean {94return this._focusOnOutput;95}9697public set outputIsFocused(v: boolean) {98this._focusOnOutput = v;99this._onDidChangeState.fire({ outputIsFocusedChanged: true });100}101102private _focusInputInOutput: boolean = false;103public get inputInOutputIsFocused(): boolean {104return this._focusInputInOutput;105}106107public set inputInOutputIsFocused(v: boolean) {108this._focusInputInOutput = v;109}110111private _outputMinHeight: number = 0;112113private get outputMinHeight() {114return this._outputMinHeight;115}116117/**118* The minimum height of the output region. It's only set to non-zero temporarily when replacing an output with a new one.119* It's reset to 0 when the new output is rendered, or in one second.120*/121private set outputMinHeight(newMin: number) {122this._outputMinHeight = newMin;123}124125private _layoutInfo: CodeCellLayoutInfo;126127get layoutInfo() {128return this._layoutInfo;129}130131private _outputViewModels: ICellOutputViewModel[];132133get outputsViewModels() {134return this._outputViewModels;135}136137readonly executionErrorDiagnostic = observableValue<ICellExecutionError | undefined>('excecutionError', undefined);138139constructor(140viewType: string,141model: NotebookCellTextModel,142initialNotebookLayoutInfo: NotebookLayoutInfo | null,143readonly viewContext: ViewContext,144@IConfigurationService configurationService: IConfigurationService,145@INotebookService private readonly _notebookService: INotebookService,146@ITextModelService modelService: ITextModelService,147@IUndoRedoService undoRedoService: IUndoRedoService,148@ICodeEditorService codeEditorService: ICodeEditorService,149@IInlineChatSessionService inlineChatSessionService: IInlineChatSessionService150) {151super(viewType, model, UUID.generateUuid(), viewContext, configurationService, modelService, undoRedoService, codeEditorService, inlineChatSessionService);152this._outputViewModels = this.model.outputs.map(output => new CellOutputViewModel(this, output, this._notebookService));153154this._register(this.model.onDidChangeOutputs((splice) => {155const removedOutputs: ICellOutputViewModel[] = [];156let outputLayoutChange = false;157for (let i = splice.start; i < splice.start + splice.deleteCount; i++) {158if (this._outputCollection[i] !== undefined && this._outputCollection[i] !== 0) {159outputLayoutChange = true;160}161}162163this._outputCollection.splice(splice.start, splice.deleteCount, ...splice.newOutputs.map(() => 0));164removedOutputs.push(...this._outputViewModels.splice(splice.start, splice.deleteCount, ...splice.newOutputs.map(output => new CellOutputViewModel(this, output, this._notebookService))));165166this._outputsTop = null;167this._onDidChangeOutputs.fire(splice);168this._onDidRemoveOutputs.fire(removedOutputs);169if (outputLayoutChange) {170this.layoutChange({ outputHeight: true }, 'CodeCellViewModel#model.onDidChangeOutputs');171}172if (!this._outputCollection.length) {173this.executionErrorDiagnostic.set(undefined, undefined);174}175dispose(removedOutputs);176}));177178this._outputCollection = new Array(this.model.outputs.length);179180this._layoutInfo = {181fontInfo: initialNotebookLayoutInfo?.fontInfo || null,182editorHeight: 0,183editorWidth: initialNotebookLayoutInfo184? this.viewContext.notebookOptions.computeCodeCellEditorWidth(initialNotebookLayoutInfo.width)185: 0,186chatHeight: 0,187statusBarHeight: 0,188commentOffset: 0,189commentHeight: 0,190outputContainerOffset: 0,191outputTotalHeight: 0,192outputShowMoreContainerHeight: 0,193outputShowMoreContainerOffset: 0,194totalHeight: this.computeTotalHeight(17, 0, 0, 0),195codeIndicatorHeight: 0,196outputIndicatorHeight: 0,197bottomToolbarOffset: 0,198layoutState: CellLayoutState.Uninitialized,199estimatedHasHorizontalScrolling: false200};201}202203updateExecutionState(e: ICellExecutionStateChangedEvent) {204if (e.changed) {205this.executionErrorDiagnostic.set(undefined, undefined);206this._onDidStartExecution.fire(e);207} else {208this._onDidStopExecution.fire(e);209}210}211212override updateOptions(e: NotebookOptionsChangeEvent) {213super.updateOptions(e);214if (e.cellStatusBarVisibility || e.insertToolbarPosition || e.cellToolbarLocation) {215this.layoutChange({});216}217}218219pauseLayout() {220this._pauseableEmitter.pause();221}222223resumeLayout() {224this._pauseableEmitter.resume();225}226227layoutChange(state: CodeCellLayoutChangeEvent, source?: string) {228// recompute229this._ensureOutputsTop();230const notebookLayoutConfiguration = this.viewContext.notebookOptions.getLayoutConfiguration();231const bottomToolbarDimensions = this.viewContext.notebookOptions.computeBottomToolbarDimensions(this.viewType);232const outputShowMoreContainerHeight = state.outputShowMoreContainerHeight ? state.outputShowMoreContainerHeight : this._layoutInfo.outputShowMoreContainerHeight;233const outputTotalHeight = Math.max(this._outputMinHeight, this.isOutputCollapsed ? notebookLayoutConfiguration.collapsedIndicatorHeight : this._outputsTop!.getTotalSum());234const commentHeight = state.commentHeight ? this._commentHeight : this._layoutInfo.commentHeight;235236const originalLayout = this.layoutInfo;237if (!this.isInputCollapsed) {238let newState: CellLayoutState;239let editorHeight: number;240let totalHeight: number;241let hasHorizontalScrolling = false;242const chatHeight = state.chatHeight ? this._chatHeight : this._layoutInfo.chatHeight;243if (!state.editorHeight && this._layoutInfo.layoutState === CellLayoutState.FromCache && !state.outputHeight) {244// No new editorHeight info - keep cached totalHeight and estimate editorHeight245const estimate = this.estimateEditorHeight(state.font?.lineHeight ?? this._layoutInfo.fontInfo?.lineHeight);246editorHeight = estimate.editorHeight;247hasHorizontalScrolling = estimate.hasHorizontalScrolling;248totalHeight = this._layoutInfo.totalHeight;249newState = CellLayoutState.FromCache;250} else if (state.editorHeight || this._layoutInfo.layoutState === CellLayoutState.Measured) {251// Editor has been measured252editorHeight = this._editorHeight;253totalHeight = this.computeTotalHeight(this._editorHeight, outputTotalHeight, outputShowMoreContainerHeight, chatHeight);254newState = CellLayoutState.Measured;255hasHorizontalScrolling = this._layoutInfo.estimatedHasHorizontalScrolling;256} else {257const estimate = this.estimateEditorHeight(state.font?.lineHeight ?? this._layoutInfo.fontInfo?.lineHeight);258editorHeight = estimate.editorHeight;259hasHorizontalScrolling = estimate.hasHorizontalScrolling;260totalHeight = this.computeTotalHeight(editorHeight, outputTotalHeight, outputShowMoreContainerHeight, chatHeight);261newState = CellLayoutState.Estimated;262}263264const statusBarHeight = this.viewContext.notebookOptions.computeEditorStatusbarHeight(this.internalMetadata, this.uri);265const codeIndicatorHeight = editorHeight + statusBarHeight;266const outputIndicatorHeight = outputTotalHeight + outputShowMoreContainerHeight;267const outputContainerOffset = notebookLayoutConfiguration.editorToolbarHeight268+ notebookLayoutConfiguration.cellTopMargin // CELL_TOP_MARGIN269+ chatHeight270+ editorHeight271+ statusBarHeight;272const outputShowMoreContainerOffset = totalHeight273- bottomToolbarDimensions.bottomToolbarGap274- bottomToolbarDimensions.bottomToolbarHeight / 2275- outputShowMoreContainerHeight;276const bottomToolbarOffset = this.viewContext.notebookOptions.computeBottomToolbarOffset(totalHeight, this.viewType);277const editorWidth = state.outerWidth !== undefined278? this.viewContext.notebookOptions.computeCodeCellEditorWidth(state.outerWidth)279: this._layoutInfo?.editorWidth;280281this._layoutInfo = {282fontInfo: state.font ?? this._layoutInfo.fontInfo ?? null,283chatHeight,284editorHeight,285editorWidth,286statusBarHeight,287outputContainerOffset,288outputTotalHeight,289outputShowMoreContainerHeight,290outputShowMoreContainerOffset,291commentOffset: outputContainerOffset + outputTotalHeight,292commentHeight,293totalHeight,294codeIndicatorHeight,295outputIndicatorHeight,296bottomToolbarOffset,297layoutState: newState,298estimatedHasHorizontalScrolling: hasHorizontalScrolling299};300} else {301const codeIndicatorHeight = notebookLayoutConfiguration.collapsedIndicatorHeight;302const outputIndicatorHeight = outputTotalHeight + outputShowMoreContainerHeight;303const chatHeight = state.chatHeight ? this._chatHeight : this._layoutInfo.chatHeight;304305const outputContainerOffset = notebookLayoutConfiguration.cellTopMargin + notebookLayoutConfiguration.collapsedIndicatorHeight;306const totalHeight =307notebookLayoutConfiguration.cellTopMargin308+ notebookLayoutConfiguration.collapsedIndicatorHeight309+ notebookLayoutConfiguration.cellBottomMargin //CELL_BOTTOM_MARGIN310+ bottomToolbarDimensions.bottomToolbarGap //BOTTOM_CELL_TOOLBAR_GAP311+ chatHeight312+ commentHeight313+ outputTotalHeight + outputShowMoreContainerHeight;314const outputShowMoreContainerOffset = totalHeight315- bottomToolbarDimensions.bottomToolbarGap316- bottomToolbarDimensions.bottomToolbarHeight / 2317- outputShowMoreContainerHeight;318const bottomToolbarOffset = this.viewContext.notebookOptions.computeBottomToolbarOffset(totalHeight, this.viewType);319const editorWidth = state.outerWidth !== undefined320? this.viewContext.notebookOptions.computeCodeCellEditorWidth(state.outerWidth)321: this._layoutInfo?.editorWidth;322323this._layoutInfo = {324fontInfo: state.font ?? this._layoutInfo.fontInfo ?? null,325editorHeight: this._layoutInfo.editorHeight,326editorWidth,327chatHeight: chatHeight,328statusBarHeight: 0,329outputContainerOffset,330outputTotalHeight,331outputShowMoreContainerHeight,332outputShowMoreContainerOffset,333commentOffset: outputContainerOffset + outputTotalHeight,334commentHeight,335totalHeight,336codeIndicatorHeight,337outputIndicatorHeight,338bottomToolbarOffset,339layoutState: this._layoutInfo.layoutState,340estimatedHasHorizontalScrolling: false341};342}343344this._fireOnDidChangeLayout({345...state,346totalHeight: this.layoutInfo.totalHeight !== originalLayout.totalHeight,347source,348});349}350351private _fireOnDidChangeLayout(state: CodeCellLayoutChangeEvent) {352this._pauseableEmitter.fire(state);353}354355override restoreEditorViewState(editorViewStates: editorCommon.ICodeEditorViewState | null, totalHeight?: number) {356super.restoreEditorViewState(editorViewStates);357if (totalHeight !== undefined && this._layoutInfo.layoutState !== CellLayoutState.Measured) {358this._layoutInfo = {359...this._layoutInfo,360totalHeight: totalHeight,361layoutState: CellLayoutState.FromCache,362};363}364}365366getDynamicHeight() {367this._onLayoutInfoRead.fire();368return this._layoutInfo.totalHeight;369}370371getHeight(lineHeight: number) {372if (this._layoutInfo.layoutState === CellLayoutState.Uninitialized) {373const estimate = this.estimateEditorHeight(lineHeight);374return this.computeTotalHeight(estimate.editorHeight, 0, 0, 0);375} else {376return this._layoutInfo.totalHeight;377}378}379380private estimateEditorHeight(lineHeight: number | undefined = 20): { editorHeight: number; hasHorizontalScrolling: boolean } {381let hasHorizontalScrolling = false;382const cellEditorOptions = this.viewContext.getBaseCellEditorOptions(this.language);383if (this.layoutInfo.fontInfo && cellEditorOptions.value.wordWrap === 'off') {384for (let i = 0; i < this.lineCount; i++) {385const max = this.textBuffer.getLineLastNonWhitespaceColumn(i + 1);386const estimatedWidth = max * (this.layoutInfo.fontInfo.typicalHalfwidthCharacterWidth + this.layoutInfo.fontInfo.letterSpacing);387if (estimatedWidth > this.layoutInfo.editorWidth) {388hasHorizontalScrolling = true;389break;390}391}392}393394const verticalScrollbarHeight = hasHorizontalScrolling ? 12 : 0; // take zoom level into account395const editorPadding = this.viewContext.notebookOptions.computeEditorPadding(this.internalMetadata, this.uri);396const editorHeight = this.lineCount * lineHeight397+ editorPadding.top398+ editorPadding.bottom // EDITOR_BOTTOM_PADDING399+ verticalScrollbarHeight;400return {401editorHeight,402hasHorizontalScrolling403};404}405406private computeTotalHeight(editorHeight: number, outputsTotalHeight: number, outputShowMoreContainerHeight: number, chatHeight: number): number {407const layoutConfiguration = this.viewContext.notebookOptions.getLayoutConfiguration();408const { bottomToolbarGap } = this.viewContext.notebookOptions.computeBottomToolbarDimensions(this.viewType);409return layoutConfiguration.editorToolbarHeight410+ layoutConfiguration.cellTopMargin411+ chatHeight412+ editorHeight413+ this.viewContext.notebookOptions.computeEditorStatusbarHeight(this.internalMetadata, this.uri)414+ this._commentHeight415+ outputsTotalHeight416+ outputShowMoreContainerHeight417+ bottomToolbarGap418+ layoutConfiguration.cellBottomMargin;419}420421protected onDidChangeTextModelContent(): void {422if (this.getEditState() !== CellEditState.Editing) {423this.updateEditState(CellEditState.Editing, 'onDidChangeTextModelContent');424this._onDidChangeState.fire({ contentChanged: true });425}426}427428onDeselect() {429this.updateEditState(CellEditState.Preview, 'onDeselect');430}431432updateOutputShowMoreContainerHeight(height: number) {433this.layoutChange({ outputShowMoreContainerHeight: height }, 'CodeCellViewModel#updateOutputShowMoreContainerHeight');434}435436updateOutputMinHeight(height: number) {437this.outputMinHeight = height;438}439440unlockOutputHeight() {441this.outputMinHeight = 0;442this.layoutChange({ outputHeight: true });443}444445updateOutputHeight(index: number, height: number, source?: string) {446if (index >= this._outputCollection.length) {447throw new Error('Output index out of range!');448}449450this._ensureOutputsTop();451452try {453if (index === 0 || height > 0) {454this._outputViewModels[index].setVisible(true);455} else if (height === 0) {456this._outputViewModels[index].setVisible(false);457}458} catch (e) {459const errorMessage = `Failed to update output height for cell ${this.handle}, output ${index}. `460+ `this.outputCollection.length: ${this._outputCollection.length}, this._outputViewModels.length: ${this._outputViewModels.length}`;461throw new Error(`${errorMessage}.\n Error: ${e.message}`);462}463464if (this._outputViewModels[index].visible.get() && height < 28) {465height = 28;466}467468this._outputCollection[index] = height;469if (this._outputsTop!.setValue(index, height)) {470this.layoutChange({ outputHeight: true }, source);471}472}473474getOutputOffsetInContainer(index: number) {475this._ensureOutputsTop();476477if (index >= this._outputCollection.length) {478throw new Error('Output index out of range!');479}480481return this._outputsTop!.getPrefixSum(index - 1);482}483484getOutputOffset(index: number): number {485return this.layoutInfo.outputContainerOffset + this.getOutputOffsetInContainer(index);486}487488spliceOutputHeights(start: number, deleteCnt: number, heights: number[]) {489this._ensureOutputsTop();490491this._outputsTop!.removeValues(start, deleteCnt);492if (heights.length) {493const values = new Uint32Array(heights.length);494for (let i = 0; i < heights.length; i++) {495values[i] = heights[i];496}497498this._outputsTop!.insertValues(start, values);499}500501this.layoutChange({ outputHeight: true }, 'CodeCellViewModel#spliceOutputs');502}503504private _ensureOutputsTop(): void {505if (!this._outputsTop) {506const values = new Uint32Array(this._outputCollection.length);507for (let i = 0; i < this._outputCollection.length; i++) {508values[i] = this._outputCollection[i];509}510511this._outputsTop = new PrefixSumComputer(values);512}513}514515private readonly _hasFindResult = this._register(new Emitter<boolean>());516public readonly hasFindResult: Event<boolean> = this._hasFindResult.event;517518startFind(value: string, options: INotebookFindOptions): CellFindMatch | null {519const matches = super.cellStartFind(value, options);520521if (matches === null) {522return null;523}524525return {526cell: this,527contentMatches: matches528};529}530531override dispose() {532super.dispose();533534this._outputCollection = [];535this._outputsTop = null;536dispose(this._outputViewModels);537}538}539540541