diff --git a/src/vs/workbench/contrib/notebook/browser/media/notebook.css b/src/vs/workbench/contrib/notebook/browser/media/notebook.css index 7d344798029..dca6fbb5d88 100644 --- a/src/vs/workbench/contrib/notebook/browser/media/notebook.css +++ b/src/vs/workbench/contrib/notebook/browser/media/notebook.css @@ -415,7 +415,7 @@ .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .notebook-folding-indicator.mouseover .codicon.codicon-notebook-expanded { opacity: 0; - transition: opacity 0.s; + transition: opacity 0.1 s; } .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .markdown-cell-hover .notebook-folding-indicator.mouseover .codicon.codicon-notebook-expanded { @@ -851,6 +851,15 @@ padding: 4px 4px 4px 4px; } +.monaco-workbench .notebookOverlay > .cell-list-container .notebook-folded-hint { + position: absolute; + font-size: var(--notebook-cell-output-font-size); + font-family: var(--monaco-monospace-font); + font-style: italic; + opacity: 0.7; + user-select: none; +} + /** Theming */ .monaco-action-bar .action-item.verticalSeparator { diff --git a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index 40239ceea31..046df719d31 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts @@ -237,6 +237,7 @@ export interface MarkdownCellLayoutInfo { readonly bottomToolbarOffset: number; readonly totalHeight: number; readonly layoutState: CellLayoutState; + readonly foldHintHeight: number; } export interface MarkdownCellLayoutChangeEvent { diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index 3ee42848ddb..299b615ef86 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -867,6 +867,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD styleSheets.push(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row div.cell.markdown { padding-left: ${cellRunGutter}px; }`); styleSheets.push(`.monaco-workbench .notebookOverlay > .cell-list-container .notebook-folding-indicator { left: ${(markdownCellGutter - 20) / 2 + markdownCellLeftMargin}px; }`); + styleSheets.push(`.notebookOverlay > .cell-list-container .notebook-folded-hint { left: ${markdownCellGutter + markdownCellLeftMargin + 8}px; }`); styleSheets.push(`.notebookOverlay .monaco-list .monaco-list-row :not(.webview-backed-markdown-cell) .cell-focus-indicator-top { height: ${cellTopMargin}px; }`); styleSheets.push(`.notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-side { bottom: ${bottomToolbarGap}px; }`); styleSheets.push(`.notebookOverlay .monaco-list .monaco-list-row.code-cell-row .cell-focus-indicator-left { width: ${codeCellLeftMargin + cellRunGutter}px; }`); diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellFocusIndicator.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellFocusIndicator.ts index ee4e94a8ac4..93609a74e5d 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellFocusIndicator.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellFocusIndicator.ts @@ -8,6 +8,7 @@ import { FastDomNode } from 'vs/base/browser/fastDomNode'; import { CellViewModelStateChangeEvent, ICellViewModel, INotebookEditorDelegate } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CellPart } from 'vs/workbench/contrib/notebook/browser/view/cellParts/cellPart'; import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; +import { MarkupCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel'; import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; export class CellFocusIndicator extends CellPart { @@ -50,7 +51,7 @@ export class CellFocusIndicator extends CellPart { updateInternalLayoutNow(element: ICellViewModel): void { if (element.cellKind === CellKind.Markup) { // markdown cell - const indicatorPostion = this.notebookEditor.notebookOptions.computeIndicatorPosition(element.layoutInfo.totalHeight, this.notebookEditor.textModel?.viewType); + const indicatorPostion = this.notebookEditor.notebookOptions.computeIndicatorPosition(element.layoutInfo.totalHeight, (element as MarkupCellViewModel).layoutInfo.foldHintHeight, this.notebookEditor.textModel?.viewType); this.bottom.domNode.style.transform = `translateY(${indicatorPostion.bottomIndicatorTop}px)`; this.left.setHeight(indicatorPostion.verticalIndicatorHeight); this.right.setHeight(indicatorPostion.verticalIndicatorHeight); diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/markdownCell.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/markdownCell.ts index fe51639fb3f..fe5e7a493c0 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/markdownCell.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/markdownCell.ts @@ -27,6 +27,7 @@ import { ILanguageService } from 'vs/editor/common/services/language'; import { CellEditorOptions } from 'vs/workbench/contrib/notebook/browser/view/cellParts/cellEditorOptions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { CellPart } from 'vs/workbench/contrib/notebook/browser/view/cellParts/cellPart'; +import { localize } from 'vs/nls'; export class StatefulMarkdownCell extends Disposable { @@ -75,7 +76,7 @@ export class StatefulMarkdownCell extends Disposable { this.updateForHover(); this.updateForFocusModeChange(); this.foldingState = viewCell.foldingState; - this.setFoldingIndicator(); + this.layoutFoldingIndicator(); this.updateFoldingIconShowClass(); // the markdown preview's height might already be updated after the renderer calls `element.getHeight()` @@ -138,7 +139,7 @@ export class StatefulMarkdownCell extends Disposable { if (foldingState !== this.foldingState) { this.foldingState = foldingState; - this.setFoldingIndicator(); + this.layoutFoldingIndicator(); } } @@ -415,6 +416,7 @@ export class StatefulMarkdownCell extends Disposable { relayoutCell(): void { this.notebookEditor.layoutNotebookCell(this.viewCell, this.viewCell.layoutInfo.totalHeight); + this.layoutFoldingIndicator(); } updateEditorOptions(newValue: IEditorOptions): void { @@ -424,16 +426,33 @@ export class StatefulMarkdownCell extends Disposable { } } - setFoldingIndicator() { + private layoutFoldingIndicator() { switch (this.foldingState) { case CellFoldingState.None: this.templateData.foldingIndicator.innerText = ''; + DOM.hide(this.templateData.foldedContentHint); break; case CellFoldingState.Collapsed: - DOM.reset(this.templateData.foldingIndicator, renderIcon(collapsedIcon)); - break; + { + DOM.reset(this.templateData.foldingIndicator, renderIcon(collapsedIcon)); + + if (this.viewCell.isInputCollapsed) { + DOM.hide(this.templateData.foldedContentHint); + } else { + const idx = this.notebookEditor._getViewModel().getCellIndex(this.viewCell); + const length = this.notebookEditor._getViewModel().getFoldedLength(idx); + DOM.reset(this.templateData.foldedContentHint, this.getHiddenCellsLabel(length)); + DOM.show(this.templateData.foldedContentHint); + + const { bottomToolbarGap } = this.notebookEditor.notebookOptions.computeBottomToolbarDimensions(this.viewCell.viewType); + const foldHintTop = this.viewCell.layoutInfo.totalHeight - bottomToolbarGap - this.viewCell.layoutInfo.foldHintHeight; + this.templateData.foldedContentHint.style.top = `${foldHintTop}px`; + } + break; + } case CellFoldingState.Expanded: DOM.reset(this.templateData.foldingIndicator, renderIcon(expandedIcon)); + DOM.hide(this.templateData.foldedContentHint); break; default: @@ -441,6 +460,14 @@ export class StatefulMarkdownCell extends Disposable { } } + private getHiddenCellsLabel(num: number): string { + if (num === 1) { + return localize('hiddenCellsLabel', "1 cell hidden..."); + } else { + return localize('hiddenCellsLabelPlural', "{0} cells hidden...", num); + } + } + private bindEditorListeners(editor: CodeEditorWidget) { this.localDisposables.clear(); diff --git a/src/vs/workbench/contrib/notebook/browser/view/notebookRenderingCommon.ts b/src/vs/workbench/contrib/notebook/browser/view/notebookRenderingCommon.ts index 170b09b9d6b..6aebe8e307a 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/notebookRenderingCommon.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/notebookRenderingCommon.ts @@ -116,6 +116,7 @@ export interface BaseCellRenderTemplate { export interface MarkdownCellRenderTemplate extends BaseCellRenderTemplate { editorContainer: HTMLElement; foldingIndicator: HTMLElement; + foldedContentHint: HTMLElement; currentEditor?: ICodeEditor; } 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 f7a70492805..7e1f74472c6 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts @@ -156,6 +156,7 @@ export class MarkupCellRenderer extends AbstractCellRenderer implements IListRen const focusIndicatorLeft = new FastDomNode(DOM.append(container, DOM.$('.cell-focus-indicator.cell-focus-indicator-side.cell-focus-indicator-left'))); const foldingIndicator = DOM.append(focusIndicatorLeft.domNode, DOM.$('.notebook-folding-indicator')); const focusIndicatorRight = new FastDomNode(DOM.append(container, DOM.$('.cell-focus-indicator.cell-focus-indicator-side.cell-focus-indicator-right'))); + const foldedContentHint = DOM.append(container, $('.notebook-folded-hint')); const codeInnerContent = DOM.append(container, $('.cell.code')); const editorPart = DOM.append(codeInnerContent, $('.cell-editor-part')); @@ -196,6 +197,7 @@ export class MarkupCellRenderer extends AbstractCellRenderer implements IListRen betweenCellToolbar, titleToolbar, statusBar, + foldedContentHint, toJSON: () => { return {}; } }; diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel.ts index 50b9c59248a..ba7efcf01a7 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel.ts @@ -7,7 +7,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import * as UUID from 'vs/base/common/uuid'; import * as editorCommon from 'vs/editor/common/editorCommon'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { EditorFoldingStateDelegate } from 'vs/workbench/contrib/notebook/browser/contrib/fold/foldingModel'; +import { CellFoldingState, EditorFoldingStateDelegate } from 'vs/workbench/contrib/notebook/browser/contrib/fold/foldingModel'; import { CellEditState, CellFindMatch, CellLayoutState, ICellOutputViewModel, ICellViewModel, MarkdownCellLayoutChangeEvent, MarkdownCellLayoutInfo, NotebookCellStateChangedEvent, NotebookLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { BaseCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; @@ -40,25 +40,14 @@ export class MarkupCellViewModel extends BaseCellViewModel implements ICellViewM private _previewHeight = 0; set renderedMarkdownHeight(newHeight: number) { - if (this.getEditState() === CellEditState.Preview) { - this._previewHeight = newHeight; - const { bottomToolbarGap } = this.viewContext.notebookOptions.computeBottomToolbarDimensions(this.viewType); - - this._updateTotalHeight(this._previewHeight + bottomToolbarGap); - } + this._previewHeight = newHeight; + this._updateTotalHeight(this._computeTotalHeight()); } private _editorHeight = 0; set editorHeight(newHeight: number) { this._editorHeight = newHeight; - const layoutConfiguration = this.viewContext.notebookOptions.getLayoutConfiguration(); - const { bottomToolbarGap } = this.viewContext.notebookOptions.computeBottomToolbarDimensions(this.viewType); - - this._updateTotalHeight(this._editorHeight - + layoutConfiguration.markdownCellTopMargin // MARKDOWN_CELL_TOP_MARGIN - + layoutConfiguration.markdownCellBottomMargin // MARKDOWN_CELL_BOTTOM_MARGIN - + bottomToolbarGap // BOTTOM_CELL_TOOLBAR_GAP - + this.viewContext.notebookOptions.computeStatusBarHeight()); + this._updateTotalHeight(this._computeTotalHeight()); } get editorHeight() { @@ -125,32 +114,48 @@ export class MarkupCellViewModel extends BaseCellViewModel implements ICellViewM : 0, bottomToolbarOffset: bottomToolbarGap, totalHeight: 100, - layoutState: CellLayoutState.Uninitialized + layoutState: CellLayoutState.Uninitialized, + foldHintHeight: 0 }; this._register(this.onDidChangeState(e => { this.viewContext.eventDispatcher.emit([new NotebookCellStateChangedEvent(e, this)]); + + if (e.foldingStateChanged) { + this._updateTotalHeight(this._computeTotalHeight()); + } })); } + private _computeTotalHeight(): number { + const layoutConfiguration = this.viewContext.notebookOptions.getLayoutConfiguration(); + const { bottomToolbarGap } = this.viewContext.notebookOptions.computeBottomToolbarDimensions(this.viewType); + const foldHintHeight = this._computeFoldHintHeight(); + + if (this.getEditState() === CellEditState.Editing) { + return this._editorHeight + + layoutConfiguration.markdownCellTopMargin + + layoutConfiguration.markdownCellBottomMargin + + bottomToolbarGap + + this.viewContext.notebookOptions.computeStatusBarHeight() + + foldHintHeight; + } else { + // @rebornix + // On file open, the previewHeight + bottomToolbarGap for a cell out of viewport can be 0 + // When it's 0, the list view will never try to render it anymore even if we scroll the cell into view. + // Thus we make sure it's greater than 0 + return Math.max(1, this._previewHeight + bottomToolbarGap + foldHintHeight); + } + } + + private _computeFoldHintHeight(): number { + return this.foldingState === CellFoldingState.Collapsed ? + this.viewContext.notebookOptions.getLayoutConfiguration().markdownFoldHintHeight : 0; + } + updateOptions(e: NotebookOptionsChangeEvent) { if (e.cellStatusBarVisibility || e.insertToolbarPosition || e.cellToolbarLocation) { - const layoutConfiguration = this.viewContext.notebookOptions.getLayoutConfiguration(); - const { bottomToolbarGap } = this.viewContext.notebookOptions.computeBottomToolbarDimensions(this.viewType); - - if (this.getEditState() === CellEditState.Editing) { - this._updateTotalHeight(this._editorHeight - + layoutConfiguration.markdownCellTopMargin - + layoutConfiguration.markdownCellBottomMargin - + bottomToolbarGap - + this.viewContext.notebookOptions.computeStatusBarHeight()); - } else { - // @rebornix - // On file open, the previewHeight + bottomToolbarGap for a cell out of viewport can be 0 - // When it's 0, the list view will never try to render it anymore even if we scroll the cell into view. - // Thus we make sure it's greater than 0 - this._updateTotalHeight(Math.max(1, this._previewHeight + bottomToolbarGap)); - } + this._updateTotalHeight(this._computeTotalHeight()); } } @@ -166,7 +171,7 @@ export class MarkupCellViewModel extends BaseCellViewModel implements ICellViewM // throw new Error('Method not implemented.'); } - triggerfoldingStateChange() { + triggerFoldingStateChange() { this._onDidChangeState.fire({ foldingStateChanged: true }); } @@ -178,6 +183,7 @@ export class MarkupCellViewModel extends BaseCellViewModel implements ICellViewM layoutChange(state: MarkdownCellLayoutChangeEvent) { // recompute + const foldHintHeight = this._computeFoldHintHeight(); if (!this.isInputCollapsed) { const editorWidth = state.outerWidth !== undefined ? this.viewContext.notebookOptions.computeMarkdownCellEditorWidth(state.outerWidth) @@ -194,7 +200,8 @@ export class MarkupCellViewModel extends BaseCellViewModel implements ICellViewM editorHeight: this._editorHeight, bottomToolbarOffset: this.viewContext.notebookOptions.computeBottomToolbarOffset(totalHeight, this.viewType), totalHeight, - layoutState: CellLayoutState.Measured + layoutState: CellLayoutState.Measured, + foldHintHeight }; } else { const editorWidth = state.outerWidth !== undefined @@ -211,7 +218,8 @@ export class MarkupCellViewModel extends BaseCellViewModel implements ICellViewM previewHeight: this._previewHeight, bottomToolbarOffset: this.viewContext.notebookOptions.computeBottomToolbarOffset(totalHeight, this.viewType), totalHeight, - layoutState: CellLayoutState.Measured + layoutState: CellLayoutState.Measured, + foldHintHeight: 0 }; } @@ -229,7 +237,8 @@ export class MarkupCellViewModel extends BaseCellViewModel implements ICellViewM bottomToolbarOffset: this._layoutInfo.bottomToolbarOffset, totalHeight: totalHeight, editorHeight: this._editorHeight, - layoutState: CellLayoutState.FromCache + layoutState: CellLayoutState.FromCache, + foldHintHeight: this._layoutInfo.foldHintHeight }; this.layoutChange({}); } diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts index 1367017c438..4580705f462 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts @@ -455,6 +455,18 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD return this._foldingRanges.isCollapsed(range) ? CellFoldingState.Collapsed : CellFoldingState.Expanded; } + getFoldedLength(index: number): number { + if (!this._foldingRanges) { + return 0; + } + + const range = this._foldingRanges.findRange(index + 1); + const startIndex = this._foldingRanges.getStartLineNumber(range) - 1; + const endIndex = this._foldingRanges.getEndLineNumber(range) - 1; + + return endIndex - startIndex; + } + updateFoldingRanges(ranges: FoldingRegions) { this._foldingRanges = ranges; let updateHiddenAreas = false; @@ -496,7 +508,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD this._viewCells.forEach(cell => { if (cell.cellKind === CellKind.Markup) { - cell.triggerfoldingStateChange(); + cell.triggerFoldingStateChange(); } }); } diff --git a/src/vs/workbench/contrib/notebook/common/notebookOptions.ts b/src/vs/workbench/contrib/notebook/common/notebookOptions.ts index 6f8c5d3a8b6..aabc9d77856 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookOptions.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookOptions.ts @@ -40,6 +40,7 @@ export interface NotebookLayoutConfiguration { markdownCellTopMargin: number; markdownCellBottomMargin: number; markdownPreviewPadding: number; + markdownFoldHintHeight: number; // bottomToolbarGap: number; // bottomToolbarHeight: number; editorToolbarHeight: number; @@ -168,7 +169,8 @@ export class NotebookOptions extends Disposable { markupFontSize, editorOptionsCustomizations, focusIndicatorGap: 3, - interactiveWindowCollapseCodeCells + interactiveWindowCollapseCodeCells, + markdownFoldHintHeight: 22 }; this._register(this.configurationService.onDidChangeConfiguration(e => { @@ -519,12 +521,12 @@ export class NotebookOptions extends Disposable { }; } - computeIndicatorPosition(totalHeight: number, viewType?: string) { + computeIndicatorPosition(totalHeight: number, foldHintHeight: number, viewType?: string) { const { bottomToolbarGap } = this.computeBottomToolbarDimensions(viewType); return { - bottomIndicatorTop: totalHeight - bottomToolbarGap - this._layoutConfiguration.cellBottomMargin, - verticalIndicatorHeight: totalHeight - bottomToolbarGap + bottomIndicatorTop: totalHeight - bottomToolbarGap - this._layoutConfiguration.cellBottomMargin - foldHintHeight, + verticalIndicatorHeight: totalHeight - bottomToolbarGap - foldHintHeight }; } }