diff --git a/src/vs/workbench/contrib/notebook/browser/media/notebook.css b/src/vs/workbench/contrib/notebook/browser/media/notebook.css index e631715e60bd1401c6ca84d3444d909bcd4931cf..ded66762727e26e5eea36311a72d8c913a8c8dda 100644 --- a/src/vs/workbench/contrib/notebook/browser/media/notebook.css +++ b/src/vs/workbench/contrib/notebook/browser/media/notebook.css @@ -211,6 +211,15 @@ max-width: 100%; } +.monaco-workbench .notebookOverlay .output-show-more-container { + position: absolute; +} + +.monaco-workbench .notebookOverlay .output-show-more-container p { + padding: 8px 8px 0 8px; + margin: 0px; +} + .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .menu { position: absolute; left: 0; diff --git a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index 2b41fd8c6fe1abf8f365ab1ca76de8b899400a7b..ae272ddd48800c99e04c6a2aabfc04fbc9ac818b 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts @@ -91,6 +91,8 @@ export interface CodeCellLayoutInfo { readonly totalHeight: number; readonly outputContainerOffset: number; readonly outputTotalHeight: number; + readonly outputShowMoreContainerHeight: number; + readonly outputShowMoreContainerOffset: number; readonly indicatorHeight: number; readonly bottomToolbarOffset: number; readonly layoutState: CodeCellLayoutState; @@ -99,6 +101,7 @@ export interface CodeCellLayoutInfo { export interface CodeCellLayoutChangeEvent { editorHeight?: boolean; outputHeight?: boolean; + outputShowMoreContainerHeight?: number; totalHeight?: boolean; outerWidth?: number; font?: BareFontInfo; @@ -558,6 +561,7 @@ export interface CodeCellRenderTemplate extends BaseCellRenderTemplate { runButtonContainer: HTMLElement; executionOrderLabel: HTMLElement; outputContainer: HTMLElement; + outputShowMoreContainer: HTMLElement; focusSinkElement: HTMLElement; editor: ICodeEditor; progressBar: ProgressBar; diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index fd7a18d951af013d2a84cb3ccee400a7d78e5656..f74505c4a2ba6df366d994685143ac2080c42e79 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -1955,12 +1955,17 @@ registerThemingParticipant((theme, collector) => { const link = theme.getColor(textLinkForeground); if (link) { collector.addRule(`.notebookOverlay .output a, - .notebookOverlay .cell.markdown a { color: ${link};} `); + .notebookOverlay .cell.markdown a, + .notebookOverlay .output-show-more-container a + { color: ${link};} `); + } const activeLink = theme.getColor(textLinkActiveForeground); if (activeLink) { collector.addRule(`.notebookOverlay .output a:hover, - .notebookOverlay .cell .output a:active { color: ${activeLink}; }`); + .notebookOverlay .cell .output a:active, + .notebookOverlay .output-show-more-container a:active + { color: ${activeLink}; }`); } const shortcut = theme.getColor(textPreformatForeground); if (shortcut) { @@ -1984,6 +1989,7 @@ registerThemingParticipant((theme, collector) => { if (containerBackground) { collector.addRule(`.notebookOverlay .output { background-color: ${containerBackground}; }`); collector.addRule(`.notebookOverlay .output-element { background-color: ${containerBackground}; }`); + collector.addRule(`.notebookOverlay .output-show-more-container { background-color: ${containerBackground}; }`); } const editorBackgroundColor = theme.getColor(editorBackground); @@ -2150,6 +2156,9 @@ registerThemingParticipant((theme, collector) => { collector.addRule(`.notebookOverlay .output { margin: 0px ${CELL_MARGIN}px 0px ${CODE_CELL_LEFT_MARGIN + CELL_RUN_GUTTER}px; }`); collector.addRule(`.notebookOverlay .output { width: calc(100% - ${CODE_CELL_LEFT_MARGIN + CELL_RUN_GUTTER + (CELL_MARGIN * 2)}px); }`); + collector.addRule(`.notebookOverlay .output-show-more-container { margin: 0px ${CELL_MARGIN}px 0px ${CODE_CELL_LEFT_MARGIN + CELL_RUN_GUTTER}px; }`); + collector.addRule(`.notebookOverlay .output-show-more-container { width: calc(100% - ${CODE_CELL_LEFT_MARGIN + CELL_RUN_GUTTER + (CELL_MARGIN * 2)}px); }`); + collector.addRule(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row div.cell.markdown { padding-left: ${CELL_RUN_GUTTER}px; }`); collector.addRule(`.notebookOverlay .cell .run-button-container { width: 20px; margin: 0px ${Math.floor(CELL_RUN_GUTTER - 20) / 2}px; }`); collector.addRule(`.notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-top { height: ${CELL_TOP_MARGIN}px; }`); diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts index 25156780e5944f83419f4c208a84beadb45beed2..1cf9a98fecaadbbc99558895f7aa9924c991fb04 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts @@ -727,6 +727,7 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende const cellRunState = new RunStateRenderer(statusBar.cellRunStatusContainer, runToolbar, this.instantiationService); const outputContainer = DOM.append(container, $('.output')); + const outputShowMoreContainer = DOM.append(container, $('.output-show-more-container')); const focusIndicatorRight = DOM.append(container, DOM.$('.cell-focus-indicator.cell-focus-indicator-side.cell-focus-indicator-right')); @@ -761,6 +762,7 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende runButtonContainer, executionOrderLabel, outputContainer, + outputShowMoreContainer, editor, disposables, elementDisposables: new DisposableStore(), @@ -843,6 +845,7 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende templateData.focusIndicatorRight.style.height = `${element.layoutInfo.indicatorHeight}px`; templateData.focusIndicatorBottom.style.top = `${element.layoutInfo.totalHeight - BOTTOM_CELL_TOOLBAR_GAP - CELL_BOTTOM_MARGIN}px`; templateData.outputContainer.style.top = `${element.layoutInfo.outputContainerOffset}px`; + templateData.outputShowMoreContainer.style.top = `${element.layoutInfo.outputShowMoreContainerOffset}px`; templateData.dragHandle.style.height = `${element.layoutInfo.totalHeight - BOTTOM_CELL_TOOLBAR_GAP}px`; } diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/codeCell.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/codeCell.ts index 98638963f255d19e3fbe3e39bf6a3ea1c6f93b9f..58466c85d25124fe8db971f67363cd4ba1accd8f 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/codeCell.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/codeCell.ts @@ -5,14 +5,19 @@ import * as DOM from 'vs/base/browser/dom'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { renderMarkdown } from 'vs/base/browser/markdownRenderer'; import { raceCancellation } from 'vs/base/common/async'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { IMarkdownString } from 'vs/base/common/htmlContent'; import { KeyCode } from 'vs/base/common/keyCodes'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { IDimension } from 'vs/editor/common/editorCommon'; +import { format } from 'vs/base/common/jsonFormatter'; +import { applyEdits } from 'vs/base/common/jsonEdit'; import { IModeService } from 'vs/editor/common/services/modeService'; import * as nls from 'vs/nls'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; import { EDITOR_BOTTOM_PADDING, EDITOR_TOP_PADDING } from 'vs/workbench/contrib/notebook/browser/constants'; import { CellFocusMode, CodeCellRenderTemplate, INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; @@ -20,7 +25,9 @@ import { getResizesObserver } from 'vs/workbench/contrib/notebook/browser/view/r import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; import { BUILTIN_RENDERER_ID, CellOutputKind, CellUri, IInsetRenderOutput, IProcessedOutput, IRenderOutput, ITransformedDisplayOutputDto, outputHasDynamicHeight, RenderOutputType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; +import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +const OUTPUT_COUNT_LIMIT = 500; interface IMimeTypeRenderer extends IQuickPickItem { index: number; } @@ -236,6 +243,8 @@ export class CodeCell extends Disposable { private templateData: CodeCellRenderTemplate, @INotebookService private notebookService: INotebookService, @IQuickInputService private readonly quickInputService: IQuickInputService, + @IOpenerService readonly openerService: IOpenerService, + @ITextFileService readonly textFileService: ITextFileService, @IModeService private readonly _modeService: IModeService ) { super(); @@ -378,8 +387,9 @@ export class CodeCell extends Disposable { }); let prevElement: HTMLElement | undefined = undefined; + const outputsToRender = this.viewCell.outputs.slice(0, Math.min(OUTPUT_COUNT_LIMIT, this.viewCell.outputs.length)); - [...this.viewCell.outputs].reverse().forEach(output => { + outputsToRender.reverse().forEach(output => { if (this.outputEntries.has(output)) { // already exist prevElement = this.outputEntries.get(output)!.domNode; @@ -392,6 +402,13 @@ export class CodeCell extends Disposable { prevElement = this.outputEntries.get(output)?.domNode; }); + if (this.viewCell.outputs.length > OUTPUT_COUNT_LIMIT) { + this.templateData.outputShowMoreContainer.style.display = 'block'; + this.viewCell.updateOutputShowMoreContainerHeight(46); + } else { + this.templateData.outputShowMoreContainer.style.display = 'none'; + } + const editorHeight = templateData.editor!.getContentHeight(); viewCell.editorHeight = editorHeight; @@ -476,7 +493,8 @@ export class CodeCell extends Disposable { this.templateData.outputContainer!.style.display = 'block'; // there are outputs, we need to calcualte their sizes and trigger relayout // @TODO@rebornix, 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 outputsToRender = this.viewCell.outputs.slice(0, Math.min(OUTPUT_COUNT_LIMIT, this.viewCell.outputs.length)); + for (let index = 0; index < outputsToRender.length; index++) { const currOutput = this.viewCell.outputs[index]; // always add to the end @@ -484,6 +502,11 @@ export class CodeCell extends Disposable { } viewCell.editorHeight = editorHeight; + if (this.viewCell.outputs.length > OUTPUT_COUNT_LIMIT) { + this.templateData.outputShowMoreContainer.style.display = 'block'; + this.viewCell.updateOutputShowMoreContainerHeight(46); + } + if (layoutCache) { this.relayoutCellDebounced(); } else { @@ -496,10 +519,74 @@ export class CodeCell extends Disposable { this.templateData.outputContainer!.style.display = 'none'; } + this.templateData.outputShowMoreContainer.innerText = ''; + this.templateData.outputShowMoreContainer.appendChild(this.generateShowMoreElement()); + // this.templateData.outputShowMoreContainer.style.top = `${this.viewCell.layoutInfo.outputShowMoreContainerOffset}px`; + + if (this.viewCell.outputs.length < OUTPUT_COUNT_LIMIT) { + this.templateData.outputShowMoreContainer.style.display = 'none'; + this.viewCell.updateOutputShowMoreContainerHeight(0); + } + // Need to do this after the intial renderOutput updateForCollapseState(); } + generateShowMoreElement(): any { + const md: IMarkdownString = { + value: `There are more than ${OUTPUT_COUNT_LIMIT} outputs, [show more ...](command:workbench.action.openLargeOutput)`, + isTrusted: true, + supportThemeIcons: true + }; + + const element = renderMarkdown(md, { + actionHandler: { + callback: (content) => { + if (content === 'command:workbench.action.openLargeOutput') { + const content = JSON.stringify(this.viewCell.outputs.map(output => { + switch (output.outputKind) { + case CellOutputKind.Text: + return { + outputKind: 'text', + text: output.text + }; + case CellOutputKind.Error: + return { + outputKind: 'error', + ename: output.ename, + evalue: output.evalue, + traceback: output.traceback + }; + case CellOutputKind.Rich: + return { + data: output.data, + metadata: output.metadata + }; + } + })); + const edits = format(content, undefined, {}); + const metadataSource = applyEdits(content, edits); + + return this.textFileService.untitled.resolve({ + associatedResource: undefined, + mode: 'json', + initialValue: metadataSource + }).then(model => { + const resource = model.resource; + this.openerService.open(resource); + }); + } + + return; + }, + disposeables: new DisposableStore() + } + }); + + element.classList.add('output-show-more'); + return element; + } + private viewUpdate(): void { if (this.viewCell.metadata?.inputCollapsed && this.viewCell.metadata.outputCollapsed) { this.viewUpdateAllCollapsed(); diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts index 9e57b51a6e0ed7eb309fa36b73f27f79dedce4a9..eee92f542e57f4b295ca74c1441bf8e6be061e6f 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts @@ -89,6 +89,8 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod editorWidth: initialNotebookLayoutInfo ? this.computeEditorWidth(initialNotebookLayoutInfo!.width) : 0, outputContainerOffset: 0, outputTotalHeight: 0, + outputShowMoreContainerHeight: 0, + outputShowMoreContainerOffset: 0, totalHeight: 0, indicatorHeight: 0, bottomToolbarOffset: 0, @@ -103,6 +105,7 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod layoutChange(state: CodeCellLayoutChangeEvent) { // recompute this._ensureOutputsTop(); + const outputShowMoreContainerHeight = state.outputShowMoreContainerHeight ? state.outputShowMoreContainerHeight : this._layoutInfo.outputShowMoreContainerHeight; let outputTotalHeight = this.metadata?.outputCollapsed ? COLLAPSED_INDICATOR_HEIGHT : this._outputsTop!.getTotalValue(); if (!this.metadata?.inputCollapsed) { @@ -117,17 +120,18 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod } else if (state.editorHeight || this._layoutInfo.layoutState === CodeCellLayoutState.Measured) { // Editor has been measured editorHeight = this._editorHeight; - totalHeight = this.computeTotalHeight(this._editorHeight, outputTotalHeight); + totalHeight = this.computeTotalHeight(this._editorHeight, outputTotalHeight, outputShowMoreContainerHeight); newState = CodeCellLayoutState.Measured; } else { editorHeight = this.estimateEditorHeight(state.font?.lineHeight); - totalHeight = this.computeTotalHeight(editorHeight, outputTotalHeight); + totalHeight = this.computeTotalHeight(editorHeight, outputTotalHeight, outputShowMoreContainerHeight); newState = CodeCellLayoutState.Estimated; } const statusbarHeight = this.getEditorStatusbarHeight(); - const indicatorHeight = editorHeight + statusbarHeight + outputTotalHeight; + const indicatorHeight = editorHeight + statusbarHeight + outputTotalHeight + outputShowMoreContainerHeight; const outputContainerOffset = EDITOR_TOOLBAR_HEIGHT + CELL_TOP_MARGIN + editorHeight + statusbarHeight; + const outputShowMoreContainerOffset = totalHeight - BOTTOM_CELL_TOOLBAR_GAP - BOTTOM_CELL_TOOLBAR_HEIGHT / 2 - outputShowMoreContainerHeight; const bottomToolbarOffset = totalHeight - BOTTOM_CELL_TOOLBAR_GAP - BOTTOM_CELL_TOOLBAR_HEIGHT / 2; const editorWidth = state.outerWidth !== undefined ? this.computeEditorWidth(state.outerWidth) : this._layoutInfo?.editorWidth; @@ -137,6 +141,8 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod editorWidth, outputContainerOffset, outputTotalHeight, + outputShowMoreContainerHeight, + outputShowMoreContainerOffset, totalHeight, indicatorHeight, bottomToolbarOffset, @@ -144,9 +150,10 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod }; } else { outputTotalHeight = this.metadata?.inputCollapsed && this.metadata.outputCollapsed ? 0 : outputTotalHeight; - const indicatorHeight = COLLAPSED_INDICATOR_HEIGHT + outputTotalHeight; + const indicatorHeight = COLLAPSED_INDICATOR_HEIGHT + outputTotalHeight + outputShowMoreContainerHeight; const outputContainerOffset = CELL_TOP_MARGIN + COLLAPSED_INDICATOR_HEIGHT; - const totalHeight = CELL_TOP_MARGIN + COLLAPSED_INDICATOR_HEIGHT + CELL_BOTTOM_MARGIN + BOTTOM_CELL_TOOLBAR_GAP + outputTotalHeight; + const totalHeight = CELL_TOP_MARGIN + COLLAPSED_INDICATOR_HEIGHT + CELL_BOTTOM_MARGIN + BOTTOM_CELL_TOOLBAR_GAP + outputTotalHeight + outputShowMoreContainerHeight; + const outputShowMoreContainerOffset = totalHeight - BOTTOM_CELL_TOOLBAR_GAP - BOTTOM_CELL_TOOLBAR_HEIGHT / 2 - outputShowMoreContainerHeight; const bottomToolbarOffset = totalHeight - BOTTOM_CELL_TOOLBAR_GAP - BOTTOM_CELL_TOOLBAR_HEIGHT / 2; const editorWidth = state.outerWidth !== undefined ? this.computeEditorWidth(state.outerWidth) : this._layoutInfo?.editorWidth; @@ -156,6 +163,8 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod editorWidth, outputContainerOffset, outputTotalHeight, + outputShowMoreContainerHeight, + outputShowMoreContainerOffset, totalHeight, indicatorHeight, bottomToolbarOffset, @@ -183,6 +192,8 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod editorWidth: this._layoutInfo.editorWidth, outputContainerOffset: this._layoutInfo.outputContainerOffset, outputTotalHeight: this._layoutInfo.outputTotalHeight, + outputShowMoreContainerHeight: this._layoutInfo.outputShowMoreContainerHeight, + outputShowMoreContainerOffset: this._layoutInfo.outputShowMoreContainerOffset, totalHeight: totalHeight, indicatorHeight: this._layoutInfo.indicatorHeight, bottomToolbarOffset: this._layoutInfo.bottomToolbarOffset, @@ -203,7 +214,7 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod getHeight(lineHeight: number) { if (this._layoutInfo.layoutState === CodeCellLayoutState.Uninitialized) { const editorHeight = this.estimateEditorHeight(lineHeight); - return this.computeTotalHeight(editorHeight, 0); + return this.computeTotalHeight(editorHeight, 0, 0); } else { return this._layoutInfo.totalHeight; } @@ -213,8 +224,8 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod return this.lineCount * lineHeight + EDITOR_TOP_PADDING + EDITOR_BOTTOM_PADDING; } - private computeTotalHeight(editorHeight: number, outputsTotalHeight: number): number { - return EDITOR_TOOLBAR_HEIGHT + CELL_TOP_MARGIN + editorHeight + this.getEditorStatusbarHeight() + outputsTotalHeight + BOTTOM_CELL_TOOLBAR_GAP + CELL_BOTTOM_MARGIN; + private computeTotalHeight(editorHeight: number, outputsTotalHeight: number, outputShowMoreContainerHeight: number): number { + return EDITOR_TOOLBAR_HEIGHT + CELL_TOP_MARGIN + editorHeight + this.getEditorStatusbarHeight() + outputsTotalHeight + outputShowMoreContainerHeight + BOTTOM_CELL_TOOLBAR_GAP + CELL_BOTTOM_MARGIN; } /** @@ -240,6 +251,10 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod this.editState = CellEditState.Preview; } + updateOutputShowMoreContainerHeight(height: number) { + this.layoutChange({ outputShowMoreContainerHeight: height }); + } + updateOutputHeight(index: number, height: number) { if (index >= this._outputCollection.length) { throw new Error('Output index out of range!');