Path: blob/main/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts
5221 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);179const layoutConfiguration = this.viewContext.notebookOptions.getLayoutConfiguration();180this._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: false,200outlineWidth: 1,201topMargin: layoutConfiguration.cellTopMargin,202bottomMargin: layoutConfiguration.cellBottomMargin,203};204}205206updateExecutionState(e: ICellExecutionStateChangedEvent) {207if (e.changed) {208this.executionErrorDiagnostic.set(undefined, undefined);209this._onDidStartExecution.fire(e);210} else {211this._onDidStopExecution.fire(e);212}213}214215override updateOptions(e: NotebookOptionsChangeEvent) {216super.updateOptions(e);217if (e.cellStatusBarVisibility || e.insertToolbarPosition || e.cellToolbarLocation) {218this.layoutChange({});219}220}221222pauseLayout() {223this._pauseableEmitter.pause();224}225226resumeLayout() {227this._pauseableEmitter.resume();228}229230layoutChange(state: CodeCellLayoutChangeEvent, source?: string) {231// recompute232this._ensureOutputsTop();233const notebookLayoutConfiguration = this.viewContext.notebookOptions.getLayoutConfiguration();234const bottomToolbarDimensions = this.viewContext.notebookOptions.computeBottomToolbarDimensions(this.viewType);235const outputShowMoreContainerHeight = state.outputShowMoreContainerHeight ? state.outputShowMoreContainerHeight : this._layoutInfo.outputShowMoreContainerHeight;236const outputTotalHeight = Math.max(this._outputMinHeight, this.isOutputCollapsed ? notebookLayoutConfiguration.collapsedIndicatorHeight : this._outputsTop!.getTotalSum());237const commentHeight = state.commentHeight ? this._commentHeight : this._layoutInfo.commentHeight;238239const originalLayout = this.layoutInfo;240if (!this.isInputCollapsed) {241let newState: CellLayoutState;242let editorHeight: number;243let totalHeight: number;244let hasHorizontalScrolling = false;245const chatHeight = state.chatHeight ? this._chatHeight : this._layoutInfo.chatHeight;246if (!state.editorHeight && this._layoutInfo.layoutState === CellLayoutState.FromCache && !state.outputHeight) {247// No new editorHeight info - keep cached totalHeight and estimate editorHeight248const estimate = this.estimateEditorHeight(state.font?.lineHeight ?? this._layoutInfo.fontInfo?.lineHeight);249editorHeight = estimate.editorHeight;250hasHorizontalScrolling = estimate.hasHorizontalScrolling;251totalHeight = this._layoutInfo.totalHeight;252newState = CellLayoutState.FromCache;253} else if (state.editorHeight || this._layoutInfo.layoutState === CellLayoutState.Measured) {254// Editor has been measured255editorHeight = this._editorHeight;256totalHeight = this.computeTotalHeight(this._editorHeight, outputTotalHeight, outputShowMoreContainerHeight, chatHeight);257newState = CellLayoutState.Measured;258hasHorizontalScrolling = this._layoutInfo.estimatedHasHorizontalScrolling;259} else {260const estimate = this.estimateEditorHeight(state.font?.lineHeight ?? this._layoutInfo.fontInfo?.lineHeight);261editorHeight = estimate.editorHeight;262hasHorizontalScrolling = estimate.hasHorizontalScrolling;263totalHeight = this.computeTotalHeight(editorHeight, outputTotalHeight, outputShowMoreContainerHeight, chatHeight);264newState = CellLayoutState.Estimated;265}266267const statusBarHeight = this.viewContext.notebookOptions.computeEditorStatusbarHeight(this.internalMetadata, this.uri);268const codeIndicatorHeight = editorHeight + statusBarHeight;269const outputIndicatorHeight = outputTotalHeight + outputShowMoreContainerHeight;270const outputContainerOffset = notebookLayoutConfiguration.editorToolbarHeight271+ notebookLayoutConfiguration.cellTopMargin // CELL_TOP_MARGIN272+ chatHeight273+ editorHeight274+ statusBarHeight;275const outputShowMoreContainerOffset = totalHeight276- bottomToolbarDimensions.bottomToolbarGap277- bottomToolbarDimensions.bottomToolbarHeight / 2278- outputShowMoreContainerHeight;279const bottomToolbarOffset = this.viewContext.notebookOptions.computeBottomToolbarOffset(totalHeight, this.viewType);280const editorWidth = state.outerWidth !== undefined281? this.viewContext.notebookOptions.computeCodeCellEditorWidth(state.outerWidth)282: this._layoutInfo?.editorWidth;283284this._layoutInfo = {285fontInfo: state.font ?? this._layoutInfo.fontInfo ?? null,286chatHeight,287editorHeight,288editorWidth,289statusBarHeight,290outputContainerOffset,291outputTotalHeight,292outputShowMoreContainerHeight,293outputShowMoreContainerOffset,294commentOffset: outputContainerOffset + outputTotalHeight,295commentHeight,296totalHeight,297codeIndicatorHeight,298outputIndicatorHeight,299bottomToolbarOffset,300layoutState: newState,301estimatedHasHorizontalScrolling: hasHorizontalScrolling,302topMargin: notebookLayoutConfiguration.cellTopMargin,303bottomMargin: notebookLayoutConfiguration.cellBottomMargin,304outlineWidth: 1305};306} else {307const codeIndicatorHeight = notebookLayoutConfiguration.collapsedIndicatorHeight;308const outputIndicatorHeight = outputTotalHeight + outputShowMoreContainerHeight;309const chatHeight = state.chatHeight ? this._chatHeight : this._layoutInfo.chatHeight;310311const outputContainerOffset = notebookLayoutConfiguration.cellTopMargin + notebookLayoutConfiguration.collapsedIndicatorHeight;312const totalHeight =313notebookLayoutConfiguration.cellTopMargin314+ notebookLayoutConfiguration.collapsedIndicatorHeight315+ notebookLayoutConfiguration.cellBottomMargin //CELL_BOTTOM_MARGIN316+ bottomToolbarDimensions.bottomToolbarGap //BOTTOM_CELL_TOOLBAR_GAP317+ chatHeight318+ commentHeight319+ outputTotalHeight + outputShowMoreContainerHeight;320const outputShowMoreContainerOffset = totalHeight321- bottomToolbarDimensions.bottomToolbarGap322- bottomToolbarDimensions.bottomToolbarHeight / 2323- outputShowMoreContainerHeight;324const bottomToolbarOffset = this.viewContext.notebookOptions.computeBottomToolbarOffset(totalHeight, this.viewType);325const editorWidth = state.outerWidth !== undefined326? this.viewContext.notebookOptions.computeCodeCellEditorWidth(state.outerWidth)327: this._layoutInfo?.editorWidth;328329this._layoutInfo = {330fontInfo: state.font ?? this._layoutInfo.fontInfo ?? null,331editorHeight: this._layoutInfo.editorHeight,332editorWidth,333chatHeight: chatHeight,334statusBarHeight: 0,335outputContainerOffset,336outputTotalHeight,337outputShowMoreContainerHeight,338outputShowMoreContainerOffset,339commentOffset: outputContainerOffset + outputTotalHeight,340commentHeight,341totalHeight,342codeIndicatorHeight,343outputIndicatorHeight,344bottomToolbarOffset,345layoutState: this._layoutInfo.layoutState,346estimatedHasHorizontalScrolling: false,347outlineWidth: 1,348topMargin: notebookLayoutConfiguration.cellTopMargin,349bottomMargin: notebookLayoutConfiguration.cellBottomMargin,350};351}352353this._fireOnDidChangeLayout({354...state,355totalHeight: this.layoutInfo.totalHeight !== originalLayout.totalHeight,356source,357});358}359360private _fireOnDidChangeLayout(state: CodeCellLayoutChangeEvent) {361this._pauseableEmitter.fire(state);362}363364override restoreEditorViewState(editorViewStates: editorCommon.ICodeEditorViewState | null, totalHeight?: number) {365super.restoreEditorViewState(editorViewStates);366if (totalHeight !== undefined && this._layoutInfo.layoutState !== CellLayoutState.Measured) {367this._layoutInfo = {368...this._layoutInfo,369totalHeight: totalHeight,370layoutState: CellLayoutState.FromCache,371};372}373}374375getDynamicHeight() {376this._onLayoutInfoRead.fire();377return this._layoutInfo.totalHeight;378}379380getHeight(lineHeight: number) {381if (this._layoutInfo.layoutState === CellLayoutState.Uninitialized) {382const estimate = this.estimateEditorHeight(lineHeight);383return this.computeTotalHeight(estimate.editorHeight, 0, 0, 0);384} else {385return this._layoutInfo.totalHeight;386}387}388389private estimateEditorHeight(lineHeight: number | undefined = 20): { editorHeight: number; hasHorizontalScrolling: boolean } {390let hasHorizontalScrolling = false;391const cellEditorOptions = this.viewContext.getBaseCellEditorOptions(this.language);392if (this.layoutInfo.fontInfo && cellEditorOptions.value.wordWrap === 'off') {393for (let i = 0; i < this.lineCount; i++) {394const max = this.textBuffer.getLineLastNonWhitespaceColumn(i + 1);395const estimatedWidth = max * (this.layoutInfo.fontInfo.typicalHalfwidthCharacterWidth + this.layoutInfo.fontInfo.letterSpacing);396if (estimatedWidth > this.layoutInfo.editorWidth) {397hasHorizontalScrolling = true;398break;399}400}401}402403const verticalScrollbarHeight = hasHorizontalScrolling ? 12 : 0; // take zoom level into account404const editorPadding = this.viewContext.notebookOptions.computeEditorPadding(this.internalMetadata, this.uri);405const editorHeight = this.lineCount * lineHeight406+ editorPadding.top407+ editorPadding.bottom // EDITOR_BOTTOM_PADDING408+ verticalScrollbarHeight;409return {410editorHeight,411hasHorizontalScrolling412};413}414415private computeTotalHeight(editorHeight: number, outputsTotalHeight: number, outputShowMoreContainerHeight: number, chatHeight: number): number {416const layoutConfiguration = this.viewContext.notebookOptions.getLayoutConfiguration();417const { bottomToolbarGap } = this.viewContext.notebookOptions.computeBottomToolbarDimensions(this.viewType);418return layoutConfiguration.editorToolbarHeight419+ layoutConfiguration.cellTopMargin420+ chatHeight421+ editorHeight422+ this.viewContext.notebookOptions.computeEditorStatusbarHeight(this.internalMetadata, this.uri)423+ this._commentHeight424+ outputsTotalHeight425+ outputShowMoreContainerHeight426+ bottomToolbarGap427+ layoutConfiguration.cellBottomMargin;428}429430protected onDidChangeTextModelContent(): void {431if (this.getEditState() !== CellEditState.Editing) {432this.updateEditState(CellEditState.Editing, 'onDidChangeTextModelContent');433this._onDidChangeState.fire({ contentChanged: true });434}435}436437onDeselect() {438this.updateEditState(CellEditState.Preview, 'onDeselect');439}440441updateOutputShowMoreContainerHeight(height: number) {442this.layoutChange({ outputShowMoreContainerHeight: height }, 'CodeCellViewModel#updateOutputShowMoreContainerHeight');443}444445updateOutputMinHeight(height: number) {446this.outputMinHeight = height;447}448449unlockOutputHeight() {450this.outputMinHeight = 0;451this.layoutChange({ outputHeight: true });452}453454updateOutputHeight(index: number, height: number, source?: string) {455if (index >= this._outputCollection.length) {456throw new Error('Output index out of range!');457}458459this._ensureOutputsTop();460461try {462if (index === 0 || height > 0) {463this._outputViewModels[index].setVisible(true);464} else if (height === 0) {465this._outputViewModels[index].setVisible(false);466}467} catch (e) {468const errorMessage = `Failed to update output height for cell ${this.handle}, output ${index}. `469+ `this.outputCollection.length: ${this._outputCollection.length}, this._outputViewModels.length: ${this._outputViewModels.length}`;470throw new Error(`${errorMessage}.\n Error: ${e.message}`);471}472473if (this._outputViewModels[index].visible.get() && height < 28) {474height = 28;475}476477this._outputCollection[index] = height;478if (this._outputsTop!.setValue(index, height)) {479this.layoutChange({ outputHeight: true }, source);480}481}482483getOutputOffsetInContainer(index: number) {484this._ensureOutputsTop();485486if (index >= this._outputCollection.length) {487throw new Error('Output index out of range!');488}489490return this._outputsTop!.getPrefixSum(index - 1);491}492493getOutputOffset(index: number): number {494return this.layoutInfo.outputContainerOffset + this.getOutputOffsetInContainer(index);495}496497spliceOutputHeights(start: number, deleteCnt: number, heights: number[]) {498this._ensureOutputsTop();499500this._outputsTop!.removeValues(start, deleteCnt);501if (heights.length) {502const values = new Uint32Array(heights.length);503for (let i = 0; i < heights.length; i++) {504values[i] = heights[i];505}506507this._outputsTop!.insertValues(start, values);508}509510this.layoutChange({ outputHeight: true }, 'CodeCellViewModel#spliceOutputs');511}512513private _ensureOutputsTop(): void {514if (!this._outputsTop) {515const values = new Uint32Array(this._outputCollection.length);516for (let i = 0; i < this._outputCollection.length; i++) {517values[i] = this._outputCollection[i];518}519520this._outputsTop = new PrefixSumComputer(values);521}522}523524private readonly _hasFindResult = this._register(new Emitter<boolean>());525public readonly hasFindResult: Event<boolean> = this._hasFindResult.event;526527startFind(value: string, options: INotebookFindOptions): CellFindMatch | null {528const matches = super.cellStartFind(value, options);529530if (matches === null) {531return null;532}533534return {535cell: this,536contentMatches: matches537};538}539540override dispose() {541super.dispose();542543this._outputCollection = [];544this._outputsTop = null;545dispose(this._outputViewModels);546}547}548549550