diff --git a/src/vs/workbench/api/browser/mainThreadNotebook.ts b/src/vs/workbench/api/browser/mainThreadNotebook.ts index 6f2566049d7..9affac7817e 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebook.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebook.ts @@ -4,17 +4,17 @@ *--------------------------------------------------------------------------------------------*/ import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; -import { MainContext, MainThreadNotebookShape, NotebookExtensionDescription, IExtHostContext, ExtHostNotebookShape, ExtHostContext, NotebookCellOutputsSplice } from '../common/extHost.protocol'; +import { MainContext, MainThreadNotebookShape, NotebookExtensionDescription, IExtHostContext, ExtHostNotebookShape, ExtHostContext } from '../common/extHost.protocol'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { URI, UriComponents } from 'vs/base/common/uri'; import { INotebookService, IMainNotebookController } from 'vs/workbench/contrib/notebook/browser/notebookService'; import { Emitter, Event } from 'vs/base/common/event'; -import { ICell, IOutput, INotebook, INotebookMimeTypeSelector, NOTEBOOK_DISPLAY_ORDER, NotebookCellsSplice } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { ICell, IOutput, INotebook, INotebookMimeTypeSelector, NOTEBOOK_DISPLAY_ORDER, NotebookCellsSplice, NotebookCellOutputsSplice } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; export class MainThreadCell implements ICell { - private _onDidChangeOutputs = new Emitter(); - onDidChangeOutputs: Event = this._onDidChangeOutputs.event; + private _onDidChangeOutputs = new Emitter(); + onDidChangeOutputs: Event = this._onDidChangeOutputs.event; private _onDidChangeDirtyState = new Emitter(); onDidChangeDirtyState: Event = this._onDidChangeDirtyState.event; @@ -25,11 +25,6 @@ export class MainThreadCell implements ICell { return this._outputs; } - set outputs(newOutputs: IOutput[]) { - this._outputs = newOutputs; - this._onDidChangeOutputs.fire(); - } - private _isDirty: boolean = false; get isDirty() { @@ -65,7 +60,7 @@ export class MainThreadCell implements ICell { this.outputs.splice(splice[0], splice[1], ...splice[2]); }); - this._onDidChangeOutputs.fire(); + this._onDidChangeOutputs.fire(splices); } save() { diff --git a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index 07763cb1144..113c2760348 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts @@ -9,6 +9,7 @@ import { BareFontInfo } from 'vs/editor/common/config/fontInfo'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/renderers/cellViewModel'; import { OutputRenderer } from 'vs/workbench/contrib/notebook/browser/output/outputRenderer'; +import { IOutput } from 'vs/workbench/contrib/notebook/common/notebookCommon'; export interface INotebookEditor { viewType: string | undefined; @@ -19,7 +20,7 @@ export interface INotebookEditor { focusNotebookCell(cell: CellViewModel, focusEditor: boolean): void; getActiveCell(): CellViewModel | undefined; layoutNotebookCell(cell: CellViewModel, height: number): void; - createInset(cell: CellViewModel, index: number, shadowContent: string, offset: number): void; + createInset(cell: CellViewModel, output: IOutput, shadowContent: string, offset: number): void; triggerScroll(event: IMouseWheelEvent): void; getFontInfo(): BareFontInfo | undefined; getListDimension(): DOM.Dimension | null; diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts index 3551a3e43ec..5f20d3f9fc2 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts @@ -29,7 +29,7 @@ import { OutputRenderer } from 'vs/workbench/contrib/notebook/browser/output/out import { BackLayerWebView } from 'vs/workbench/contrib/notebook/browser/renderers/backLayerWebView'; import { CodeCellRenderer, MarkdownCellRenderer, NotebookCellListDelegate } from 'vs/workbench/contrib/notebook/browser/renderers/cellRenderer'; import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/renderers/cellViewModel'; -import { CELL_MARGIN, INotebook, NotebookCellsSplice } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CELL_MARGIN, INotebook, NotebookCellsSplice, IOutput } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { IWebviewService } from 'vs/workbench/contrib/webview/browser/webview'; import { getExtraColor } from 'vs/workbench/contrib/welcome/walkThrough/common/walkThroughUtils'; import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; @@ -322,24 +322,22 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { const updateScrollPosition = () => { let scrollTop = this.list?.scrollTop || 0; this.webview!.element.style.top = `${scrollTop}px`; - let updateItems: { top: number, id: string }[] = []; + let updateItems: { cell: CellViewModel, output: IOutput, cellTop: number }[] = []; - // const date = new Date(); - this.webview?.mapping.forEach((item) => { - let index = this.model!.getNotebook().cells.indexOf(item.cell.cell); - let top = this.list?.getAbsoluteTop(index) || 0; - let newTop = this.webview!.shouldRenderInset(item.cell.id, top); + if (this.webview?.insetMapping) { + this.webview?.insetMapping.forEach((value, key) => { + let cell = value.cell; + let index = this.model!.getNotebook().cells.indexOf(cell.cell); + let cellTop = this.list?.getAbsoluteTop(index) || 0; + if (this.webview!.shouldUpdateInset(cell, key, cellTop)) { + updateItems.push({ + cell: cell, + output: key, + cellTop: cellTop + }); + } + }); - if (newTop !== undefined) { - updateItems.push({ - top: newTop, - id: item.cell.id - }); - } - }); - - if (updateItems.length > 0) { - // console.log('----- did scroll ---- ', date.getMinutes() + ':' + date.getSeconds() + ':' + date.getMilliseconds()); this.webview?.updateViewScrollTop(-scrollTop, updateItems); } }; @@ -348,7 +346,9 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { // console.log('----- will scroll ---- ', date.getMinutes() + ':' + date.getSeconds() + ':' + date.getMilliseconds()); this.webview?.updateViewScrollTop(-e.scrollTop, []); })); - this.localStore.add(this.list!.onDidScroll(() => updateScrollPosition())); + this.localStore.add(this.list!.onDidScroll(() => { + updateScrollPosition(); + })); this.localStore.add(this.list!.onDidChangeContentHeight(() => updateScrollPosition())); this.localStore.add(this.list!.onFocusChange((e) => { if (e.elements.length > 0) { @@ -531,29 +531,24 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { this.list?.triggerScrollFromMouseWheelEvent(event); } - createInset(cell: CellViewModel, outputIndex: number, shadowContent: string, offset: number) { + createInset(cell: CellViewModel, output: IOutput, shadowContent: string, offset: number) { if (!this.webview) { return; } let preloads = this.notebook!.renderers; - if (!this.webview!.mapping.has(cell.id)) { + if (!this.webview!.insetMapping.has(output)) { let index = this.model!.getNotebook().cells.indexOf(cell.cell); - let top = this.list?.getAbsoluteTop(index) || 0; - this.webview!.createInset(cell, offset, shadowContent, top + offset, preloads); - this.webview!.outputMapping.set(cell.id + `-${outputIndex}`, true); - } else if (!this.webview!.outputMapping.has(cell.id + `-${outputIndex}`)) { - let index = this.model!.getNotebook().cells.indexOf(cell.cell); - let top = this.list?.getAbsoluteTop(index) || 0; - this.webview!.outputMapping.set(cell.id + `-${outputIndex}`, true); - this.webview!.createInset(cell, offset, shadowContent, top + offset, preloads); + let cellTop = this.list?.getAbsoluteTop(index) || 0; + + this.webview!.createInset(cell, output, cellTop, offset, shadowContent, preloads); } else { let index = this.model!.getNotebook().cells.indexOf(cell.cell); - let top = this.list?.getAbsoluteTop(index) || 0; + let cellTop = this.list?.getAbsoluteTop(index) || 0; let scrollTop = this.list?.scrollTop || 0; - this.webview!.updateViewScrollTop(-scrollTop, [{ id: cell.id, top: top + offset }]); + this.webview!.updateViewScrollTop(-scrollTop, [{ cell: cell, output: output, cellTop: cellTop }]); } } diff --git a/src/vs/workbench/contrib/notebook/browser/renderers/backLayerWebView.ts b/src/vs/workbench/contrib/notebook/browser/renderers/backLayerWebView.ts index 4888fb5ff39..8a2654b1088 100644 --- a/src/vs/workbench/contrib/notebook/browser/renderers/backLayerWebView.ts +++ b/src/vs/workbench/contrib/notebook/browser/renderers/backLayerWebView.ts @@ -12,7 +12,7 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment' import { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { INotebookService } from 'vs/workbench/contrib/notebook/browser/notebookService'; import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/renderers/cellViewModel'; -import { CELL_MARGIN } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CELL_MARGIN, IOutput } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { IWebviewService, WebviewElement } from 'vs/workbench/contrib/webview/browser/webview'; import { WebviewResourceScheme } from 'vs/workbench/contrib/webview/common/resourceLoader'; @@ -37,6 +37,7 @@ export interface ICreationRequestMessage { type: 'html'; content: string; id: string; + outputId: string; top: number; } @@ -71,8 +72,8 @@ let version = 0; export class BackLayerWebView extends Disposable { element: HTMLElement; webview: WebviewElement; - mapping: Map = new Map(); - outputMapping: Map = new Map(); + insetMapping: Map = new Map(); + reversedInsetMapping: Map = new Map(); preloadsCache: Map = new Map(); constructor(public webviewService: IWebviewService, public notebookService: INotebookService, public notebookEditor: INotebookEditor, public environmentSerice: IEnvironmentService) { @@ -166,29 +167,33 @@ export class BackLayerWebView extends Disposable { case 'html': { let cellOutputContainer = document.getElementById(id); + let outputId = event.data.outputId; if (!cellOutputContainer) { let newElement = document.createElement('div'); - newElement.style.position = 'absolute'; - newElement.style.top = event.data.top + 'px'; + newElement.id = id; document.getElementById('container').appendChild(newElement); cellOutputContainer = newElement; } let outputNode = document.createElement('div'); + outputNode.style.position = 'absolute'; + outputNode.style.top = event.data.top + 'px'; + + outputNode.id = outputId; let content = event.data.content; outputNode.innerHTML = content; cellOutputContainer.appendChild(outputNode); // eval domEval(outputNode); - resizeObserve(cellOutputContainer, id); + resizeObserve(outputNode, outputId); vscode.postMessage({ type: 'dimension', - id: id, + id: outputId, data: { - height: cellOutputContainer.clientHeight + height: outputNode.clientHeight } }); } @@ -247,15 +252,23 @@ export class BackLayerWebView extends Disposable { this._register(this.webview.onMessage((data: IMessage) => { if (data.type === 'dimension') { - let cell = this.mapping.get(data.id)?.cell; + let output = this.reversedInsetMapping.get(data.id); + + if (!output) { + return; + } + + let cell = this.insetMapping.get(output)!.cell; let height = data.data.height; let outputHeight = height === 0 ? 0 : height + 16; + if (cell) { - const lineNum = cell.lineCount; - const lineHeight = this.notebookEditor.getFontInfo()?.lineHeight ?? 18; - const totalHeight = lineNum * lineHeight; - cell.dynamicHeight = totalHeight + 32 /* code cell padding */ + outputHeight; - this.notebookEditor.layoutNotebookCell(cell, totalHeight + 32 /* code cell padding */ + outputHeight); + let editorHeight = cell.editorHeight; + let outputIndex = cell.outputs.indexOf(output); + cell.updateOutputHeight(outputIndex, outputHeight); + let totalOutputHeight = cell.getOutputTotalHeight(); + cell.dynamicHeight = editorHeight + 32 /* code cell padding */ + totalOutputHeight; + this.notebookEditor.layoutNotebookCell(cell, cell.dynamicHeight); } } else if (data.type === 'scroll-ack') { // const date = new Date(); @@ -276,45 +289,64 @@ export class BackLayerWebView extends Disposable { return webview; } - shouldRenderInset(id: string, widgetTop: number) { - let item = this.mapping.get(id); + shouldUpdateInset(cell: CellViewModel, output: IOutput, cellTop: number) { + let outputCache = this.insetMapping.get(output)!; + let outputIndex = cell.outputs.indexOf(output); - if (item && widgetTop + item.offset !== item.top) { - return widgetTop + item.offset; + let outputOffsetInOutputContainer = cell.getOutputOffset(outputIndex); + let outputOffset = cellTop + cell.editorHeight + 16 /* editor padding */ + 8 + outputOffsetInOutputContainer; + + if (outputOffset === outputCache.cacheOffset) { + return false; } - return undefined; + return true; } - updateViewScrollTop(top: number, items: { top: number, id: string }[]) { - items.forEach(item => { - if (this.mapping.has(item.id)) { - this.mapping.get(item.id)!.top = item.top; - } + updateViewScrollTop(top: number, items: { cell: CellViewModel, output: IOutput, cellTop: number }[]) { + let widgets: IContentWidgetTopRequest[] = items.map(item => { + let outputCache = this.insetMapping.get(item.output)!; + let id = outputCache.outputId; + let outputIndex = item.cell.outputs.indexOf(item.output); + + let outputOffsetInOutputContainer = item.cell.getOutputOffset(outputIndex); + let outputOffset = item.cellTop + item.cell.editorHeight + 16 /* editor padding */ + 8 + outputOffsetInOutputContainer; + outputCache.cacheOffset = outputOffset; + + // console.log('trigger output offset change', outputOffset); + + return { + id: id, + top: outputOffset + }; }); let message: IViewScrollTopRequestMessage = { top, type: 'view-scroll', version: version++, - widgets: items + widgets: widgets }; this.webview.sendMessage(message); } - createInset(cell: CellViewModel, offset: number, shadowContent: string, initialTop: number, preloads: Set) { + createInset(cell: CellViewModel, output: IOutput, cellTop: number, offset: number, shadowContent: string, preloads: Set) { this.updateRendererPreloads(preloads); + let initialTop = cellTop + offset; + let outputId = UUID.generateUuid(); let message: ICreationRequestMessage = { type: 'html', content: shadowContent, id: cell.id, + outputId: outputId, top: initialTop }; this.webview.sendMessage(message); - this.mapping.set(cell.id, { cell: cell, offset, top: initialTop }); + this.insetMapping.set(output, { outputId: outputId, cell: cell, cacheOffset: initialTop }); + this.reversedInsetMapping.set(outputId, output); } clearInsets() { @@ -322,8 +354,8 @@ export class BackLayerWebView extends Disposable { type: 'clear' }); - this.mapping = new Map(); - this.outputMapping = new Map(); + this.insetMapping = new Map(); + this.reversedInsetMapping = new Map(); } updateRendererPreloads(preloads: Set) { diff --git a/src/vs/workbench/contrib/notebook/browser/renderers/cellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/renderers/cellViewModel.ts index 043f2df71fa..b8f708b97da 100644 --- a/src/vs/workbench/contrib/notebook/browser/renderers/cellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/renderers/cellViewModel.ts @@ -8,7 +8,7 @@ import { ITextModel } from 'vs/editor/common/model'; import { Emitter } from 'vs/base/common/event'; import * as UUID from 'vs/base/common/uuid'; import { MarkdownRenderer } from 'vs/workbench/contrib/notebook/browser/renderers/mdRenderer'; -import { ICell } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { ICell, NotebookCellOutputsSplice, IOutput } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { PrefixSumComputer } from 'vs/editor/common/viewModel/prefixSumComputer'; @@ -22,7 +22,7 @@ export class CellViewModel extends Disposable { readonly onDidDispose = this._onDidDispose.event; protected readonly _onDidChangeEditingState = new Emitter(); readonly onDidChangeEditingState = this._onDidChangeEditingState.event; - protected readonly _onDidChangeOutputs = new Emitter(); + protected readonly _onDidChangeOutputs = new Emitter(); readonly onDidChangeOutputs = this._onDidChangeOutputs.event; private _outputCollection: number[] = []; protected _outputsTop: PrefixSumComputer | null = null; @@ -53,6 +53,16 @@ export class CellViewModel extends Disposable { return this._dynamicHeight; } + + private _editorHeight = 0; + set editorHeight(height: number) { + this._editorHeight = height; + } + + get editorHeight(): number { + return this._editorHeight; + } + private _textModel?: ITextModel; readonly id: string = UUID.generateUuid(); @@ -66,12 +76,14 @@ export class CellViewModel extends Disposable { ) { super(); if (this.cell.onDidChangeOutputs) { - this._register(this.cell.onDidChangeOutputs(() => { + this._register(this.cell.onDidChangeOutputs((splices) => { this._outputCollection = new Array(this.cell.outputs.length); this._outputsTop = null; - this._onDidChangeOutputs.fire(); + this._onDidChangeOutputs.fire(splices); })); } + + this._outputCollection = new Array(this.cell.outputs.length); } hasDynamicHeight() { if (this._dynamicHeight !== null) { @@ -171,7 +183,41 @@ export class CellViewModel extends Disposable { this._ensureOutputsTop(); - return this._outputsTop!.getAccumulatedValue(index); + return this._outputsTop!.getAccumulatedValue(index - 1); + } + + getOutputHeight(output: IOutput): number | undefined { + let index = this.cell.outputs.indexOf(output); + + if (index < 0) { + return undefined; + } + + if (index < this._outputCollection.length) { + return this._outputCollection[index]; + } + + return undefined; + } + + getOutputTotalHeight(): number { + this._ensureOutputsTop(); + + return this._outputsTop!.getTotalValue(); + } + + spliceOutputHeights(start: number, deleteCnt: number, heights: number[]) { + this._ensureOutputsTop(); + + this._outputsTop!.removeValues(start, deleteCnt); + if (heights.length) { + const values = new Uint32Array(heights.length); + for (let i = 0; i < heights.length; i++) { + values[i] = heights[i]; + } + + this._outputsTop!.insertValues(start, values); + } } protected _ensureOutputsTop(): void { diff --git a/src/vs/workbench/contrib/notebook/browser/renderers/codeCell.ts b/src/vs/workbench/contrib/notebook/browser/renderers/codeCell.ts index 55be8567075..85b157b154f 100644 --- a/src/vs/workbench/contrib/notebook/browser/renderers/codeCell.ts +++ b/src/vs/workbench/contrib/notebook/browser/renderers/codeCell.ts @@ -3,23 +3,25 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/renderers/cellViewModel'; import { getResizesObserver } from 'vs/workbench/contrib/notebook/browser/renderers/sizeObserver'; -import { CELL_MARGIN } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CELL_MARGIN, IOutput } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { CellRenderTemplate, INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { raceCancellation } from 'vs/base/common/async'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; export class CodeCell extends Disposable { + private outputResizeListeners = new Map(); + private outputElements = new Map(); constructor( - notebookEditor: INotebookEditor, - viewCell: CellViewModel, - templateData: CellRenderTemplate, + private notebookEditor: INotebookEditor, + private viewCell: CellViewModel, + private templateData: CellRenderTemplate, ) { super(); - let width; + let width: number; const listDimension = notebookEditor.getListDimension(); if (listDimension) { width = listDimension.width - CELL_MARGIN * 2; @@ -36,20 +38,35 @@ export class CodeCell extends Disposable { height: totalHeight } ); + viewCell.editorHeight = totalHeight; + const cts = new CancellationTokenSource(); this._register({ dispose() { cts.dispose(true); } }); - raceCancellation(viewCell.resolveTextModel(), cts.token).then(model => model && templateData.editor?.setModel(model)); + raceCancellation(viewCell.resolveTextModel(), cts.token).then(model => { + if (model) { + templateData.editor?.setModel(model); - let realContentHeight = templateData.editor?.getContentHeight(); - - if (realContentHeight !== undefined && realContentHeight !== totalHeight) { - templateData.editor?.layout( - { - width: width, - height: realContentHeight + let realContentHeight = templateData.editor?.getContentHeight(); + let width: number; + const listDimension = notebookEditor.getListDimension(); + if (listDimension) { + width = listDimension.width - CELL_MARGIN * 2; + } else { + width = templateData.container.clientWidth - 24 /** for scrollbar and margin right */; } - ); - } + + if (realContentHeight !== undefined && realContentHeight !== totalHeight) { + templateData.editor?.layout( + { + width: width, + height: realContentHeight + } + ); + + viewCell.editorHeight = realContentHeight; + } + } + }); let cellWidthResizeObserver = getResizesObserver(templateData.cellContainer, { width: width, @@ -63,6 +80,8 @@ export class CodeCell extends Disposable { height: realContentHeight } ); + + viewCell.editorHeight = realContentHeight; }); cellWidthResizeObserver.startObserving(); @@ -80,8 +99,10 @@ export class CodeCell extends Disposable { } ); + viewCell.editorHeight = e.contentHeight; + if (viewCell.outputs.length) { - let outputHeight = templateData.outputContainer!.clientHeight; + let outputHeight = viewCell.getOutputTotalHeight(); notebookEditor.layoutNotebookCell(viewCell, e.contentHeight + 32 + outputHeight); } else { notebookEditor.layoutNotebookCell(viewCell, e.contentHeight + 16); @@ -90,93 +111,150 @@ export class CodeCell extends Disposable { } })); - this._register(viewCell.onDidChangeOutputs(() => { - if (viewCell.outputs.length > 0) { - let hasDynamicHeight = true; - for (let i = 0; i < viewCell.outputs.length; i++) { - let outputItemDiv = document.createElement('div'); - let result = notebookEditor.getOutputRenderer().render(viewCell.outputs[i], outputItemDiv); - templateData.outputContainer?.appendChild(outputItemDiv); - if (result) { - hasDynamicHeight = hasDynamicHeight || result?.hasDynamicHeight; - if (result.shadowContent) { - hasDynamicHeight = false; - notebookEditor.createInset(viewCell, i, result.shadowContent, totalHeight + 8); - } - } - } - - if (hasDynamicHeight) { - let clientHeight = templateData.outputContainer!.clientHeight; - let listDimension = notebookEditor.getListDimension(); - let dimension = listDimension ? { - width: listDimension.width - CELL_MARGIN * 2, - height: clientHeight - } : undefined; - const elementSizeObserver = getResizesObserver(templateData.outputContainer!, dimension, () => { - if (templateData.outputContainer && document.body.contains(templateData.outputContainer!)) { - let height = elementSizeObserver.getHeight(); - if (clientHeight !== height) { - viewCell.dynamicHeight = totalHeight + 32 + height; - notebookEditor.layoutNotebookCell(viewCell, totalHeight + 32 + height); - } - - elementSizeObserver.dispose(); - } - }); - elementSizeObserver.startObserving(); - viewCell.dynamicHeight = totalHeight + 32 + clientHeight; - notebookEditor.layoutNotebookCell(viewCell, totalHeight + 32 + clientHeight); - - this._register(elementSizeObserver); - } + this._register(viewCell.onDidChangeOutputs((splices) => { + if (this.viewCell.outputs.length) { + this.templateData.outputContainer!.style.display = 'block'; + } else { + this.templateData.outputContainer!.style.display = 'none'; } + + let reversedSplices = splices.reverse(); + + reversedSplices.forEach(splice => { + viewCell.spliceOutputHeights(splice[0], splice[1], splice[2].map(_ => 0)); + }); + + let removedKeys: IOutput[] = []; + + this.outputElements.forEach((value, key) => { + if (viewCell.outputs.indexOf(key) < 0) { + // already removed + removedKeys.push(key); + // remove element from DOM + this.templateData?.outputContainer?.removeChild(value); + } + }); + + removedKeys.forEach(key => { + // remove element cache + this.outputElements.delete(key); + // remove elment resize listener if there is one + this.outputResizeListeners.delete(key); + }); + + let prevElement: HTMLElement | undefined = undefined; + + this.viewCell.outputs.reverse().forEach(output => { + if (this.outputElements.has(output)) { + // already exist + prevElement = this.outputElements.get(output); + return; + } + + // newly added element + let currIndex = this.viewCell.outputs.indexOf(output); + this.renderOutput(output, currIndex, prevElement); + prevElement = this.outputElements.get(output); + }); + + let editorHeight = templateData.editor!.getContentHeight(); + let totalOutputHeight = viewCell.getOutputTotalHeight(); + notebookEditor.layoutNotebookCell(viewCell, editorHeight + 32 + totalOutputHeight); })); if (viewCell.outputs.length > 0) { + this.templateData.outputContainer!.style.display = 'block'; // there are outputs, we need to calcualte their sizes and trigger relayout + // @todo, if there is no resizable output, we should not check their height individually, which hurts the performance + for (let index = 0; index < this.viewCell.outputs.length; index++) { + const currOutput = this.viewCell.outputs[index]; - let hasDynamicHeight = true; - for (let i = 0; i < viewCell.outputs.length; i++) { - let outputItemDiv = document.createElement('div'); - let result = notebookEditor.getOutputRenderer().render(viewCell.outputs[i], outputItemDiv); - templateData.outputContainer?.appendChild(outputItemDiv); - if (result) { - hasDynamicHeight = hasDynamicHeight || result?.hasDynamicHeight; - if (result.shadowContent) { - hasDynamicHeight = false; - notebookEditor.createInset(viewCell, i, result.shadowContent, totalHeight + 8); - } - } + // always add to the end + this.renderOutput(currOutput, index, undefined); } - if (hasDynamicHeight) { - let clientHeight = templateData.outputContainer!.clientHeight; - let listDimension = notebookEditor.getListDimension(); - let dimension = listDimension ? { - width: listDimension.width - CELL_MARGIN * 2, - height: clientHeight - } : undefined; - const elementSizeObserver = getResizesObserver(templateData.outputContainer!, dimension, () => { - if (templateData.outputContainer && document.body.contains(templateData.outputContainer!)) { - let height = elementSizeObserver.getHeight(); - if (clientHeight !== height) { - viewCell.dynamicHeight = totalHeight + 32 + height; - notebookEditor.layoutNotebookCell(viewCell, totalHeight + 32 + height); - } - - elementSizeObserver.dispose(); - } - }); - elementSizeObserver.startObserving(); - this._register(elementSizeObserver); - } - - const outputHeight = templateData.outputContainer?.clientHeight || 0; - notebookEditor.layoutNotebookCell(viewCell, totalHeight + 32 + outputHeight); + let totalOutputHeight = viewCell.getOutputTotalHeight(); + this.notebookEditor.layoutNotebookCell(viewCell, totalHeight + 32 + totalOutputHeight); } else { // noop + this.templateData.outputContainer!.style.display = 'none'; } } + + renderOutput(currOutput: IOutput, index: number, beforeElement?: HTMLElement) { + let outputItemDiv = document.createElement('div'); + let result = this.notebookEditor.getOutputRenderer().render(currOutput, outputItemDiv); + + if (!result) { + this.viewCell.updateOutputHeight(index, 0); + return; + } + + this.outputElements.set(currOutput, outputItemDiv); + + if (beforeElement) { + this.templateData.outputContainer?.insertBefore(outputItemDiv, beforeElement); + } else { + this.templateData.outputContainer?.appendChild(outputItemDiv); + } + + if (result.shadowContent) { + let editorHeight = this.viewCell.editorHeight; + this.notebookEditor.createInset(this.viewCell, currOutput, result.shadowContent, editorHeight + 8 + this.viewCell.getOutputOffset(index)); + } + + let hasDynamicHeight = result.hasDynamicHeight; + + if (hasDynamicHeight) { + let clientHeight = outputItemDiv.clientHeight; + let listDimension = this.notebookEditor.getListDimension(); + let dimension = listDimension ? { + width: listDimension.width - CELL_MARGIN * 2, + height: clientHeight + } : undefined; + const elementSizeObserver = getResizesObserver(outputItemDiv, dimension, () => { + if (this.templateData.outputContainer && document.body.contains(this.templateData.outputContainer!)) { + let height = elementSizeObserver.getHeight(); + + if (clientHeight === height) { + console.log(this.viewCell.outputs); + return; + } + + const currIndex = this.viewCell.outputs.indexOf(currOutput); + if (currIndex < 0) { + return; + } + + this.viewCell.updateOutputHeight(currIndex, height); + const editorHeight = this.viewCell.editorHeight; + const totalOutputHeight = this.viewCell.getOutputTotalHeight(); + this.viewCell.dynamicHeight = editorHeight + 32 + totalOutputHeight; + this.notebookEditor.layoutNotebookCell(this.viewCell, editorHeight + 32 + totalOutputHeight); + } + }); + elementSizeObserver.startObserving(); + this.outputResizeListeners.set(currOutput, elementSizeObserver); + this.viewCell.updateOutputHeight(index, clientHeight); + } else { + if (result.shadowContent) { + // webview + // noop + // let cachedHeight = this.viewCell.getOutputHeight(currOutput); + } else { + // static output + let clientHeight = outputItemDiv.clientHeight; + this.viewCell.updateOutputHeight(index, clientHeight); + } + } + } + + dispose() { + this.outputResizeListeners.forEach((value) => { + value.dispose(); + }); + + super.dispose(); + } } diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index e44f65feedb..0221991fab0 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -100,7 +100,7 @@ export interface ICell { language: string; cell_type: 'markdown' | 'code'; outputs: IOutput[]; - onDidChangeOutputs?: Event; + onDidChangeOutputs?: Event; isDirty: boolean; } @@ -158,3 +158,9 @@ export type NotebookCellsSplice = [ number /* delete count */, ICell[] ]; + +export type NotebookCellOutputsSplice = [ + number /* start */, + number /* delete count */, + IOutput[] +];