diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index d0dd612a13e..133bb0a0127 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -1506,6 +1506,13 @@ declare module 'vscode' { export type CellOutput = CellStreamOutput | CellErrorOutput | CellDisplayOutput; + export enum NotebookCellRunState { + Running = 1, + Idle = 2, + Success = 3, + Error = 4 + } + export interface NotebookCellMetadata { /** * Controls if the content of a cell is editable or not. @@ -1522,6 +1529,16 @@ declare module 'vscode' { * The order in which this cell was executed. */ executionOrder?: number; + + /** + * A status message to be shown in the cell's status bar + */ + statusMessage?: string; + + /** + * The cell's current run state + */ + runState?: NotebookCellRunState; } export interface NotebookCell { diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 349ccf2b540..d6e45fc1b0e 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1038,6 +1038,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I TimelineItem: extHostTypes.TimelineItem, CellKind: extHostTypes.CellKind, CellOutputKind: extHostTypes.CellOutputKind, + NotebookCellRunState: extHostTypes.NotebookCellRunState }; }; } diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index d3a0c8654e3..c1be69f6102 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -2713,6 +2713,13 @@ export enum CellOutputKind { Rich = 3 } +export enum NotebookCellRunState { + Running = 1, + Idle = 2, + Success = 3, + Error = 4 +} + //#endregion //#region Timeline diff --git a/src/vs/workbench/contrib/notebook/browser/constants.ts b/src/vs/workbench/contrib/notebook/browser/constants.ts index 227cf8c490e..1b909749d85 100644 --- a/src/vs/workbench/contrib/notebook/browser/constants.ts +++ b/src/vs/workbench/contrib/notebook/browser/constants.ts @@ -9,6 +9,7 @@ export const CELL_RUN_GUTTER = 32; // TODO should be dynamic based on execution export const EDITOR_TOOLBAR_HEIGHT = 20; export const BOTTOM_CELL_TOOLBAR_HEIGHT = 32; +export const CELL_STATUSBAR_HEIGHT = 28; // Top margin of editor export const EDITOR_TOP_MARGIN = 0; diff --git a/src/vs/workbench/contrib/notebook/browser/media/notebook.css b/src/vs/workbench/contrib/notebook/browser/media/notebook.css index 5d1c05cbe1c..4cff020947b 100644 --- a/src/vs/workbench/contrib/notebook/browser/media/notebook.css +++ b/src/vs/workbench/contrib/notebook/browser/media/notebook.css @@ -140,6 +140,42 @@ margin-right: 24px; } +.monaco-workbench .part.editor > .content .notebook-editor .cell-statusbar-container { + height: 28px; + display: flex; + position: relative; +} + +.monaco-workbench .part.editor > .content .notebook-editor .cell-statusbar-container .cell-status-message { + display: flex; + align-items: center; + font-family: Menlo, Monaco, Consolas, "Droid Sans Mono", "Courier New", monospace, "Droid Sans Fallback"; +} + +.monaco-workbench .part.editor > .content .notebook-editor .cell-statusbar-container .cell-run-status { + height: 100%; + display: flex; + align-items: center; + margin-left: 18px; + width: 18px; + margin-right: 2px; +} + +.monaco-workbench .part.editor > .content .notebook-editor .cell-statusbar-container .cell-run-status .codicon-check { + color: #73C991; +} + +.monaco-workbench .part.editor > .content .notebook-editor .cell-status-placeholder { + position: absolute; + left: 18px; + opacity: 0.7; + font-family: Menlo, Monaco, Consolas, "Droid Sans Mono", "Courier New", monospace, "Droid Sans Fallback"; + display: flex; + align-items: center; + bottom: 0px; + top: 0px; +} + .monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell .run-button-container { position: relative; } @@ -175,7 +211,7 @@ visibility: hidden; } -.monaco-workbench .part.editor > .content .notebook-editor .cell .cell-editor-container { +.monaco-workbench .part.editor > .content .notebook-editor .cell .cell-editor-part { position: relative; } diff --git a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index b99f6d0f277..8e7dd4a9c34 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts @@ -285,7 +285,10 @@ export interface MarkdownCellRenderTemplate extends BaseCellRenderTemplate { } export interface CodeCellRenderTemplate extends BaseCellRenderTemplate { - editorContainer: HTMLElement; + statusBarContainer: HTMLElement; + cellRunStatusContainer: HTMLElement; + cellStatusMessageContainer: HTMLElement; + cellStatusPlaceholderContainer: HTMLElement; runToolbar: ToolBar; runButtonContainer: HTMLElement; executionOrderLabel: HTMLElement; diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts index f18eba035e6..b834378d284 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts @@ -768,7 +768,7 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { } } - async _executeNotebookCell(cell: ICellViewModel, tokenSource: CancellationTokenSource): Promise { + private async _executeNotebookCell(cell: ICellViewModel, tokenSource: CancellationTokenSource): Promise { try { cell.currentTokenSource = tokenSource; const provider = this.notebookService.getContributedNotebookProviders(this.viewModel!.uri)[0]; @@ -903,7 +903,8 @@ registerThemingParticipant((theme, collector) => { const color = getExtraColor(theme, embeddedEditorBackground, { dark: 'rgba(0, 0, 0, .4)', extra_dark: 'rgba(200, 235, 255, .064)', light: '#f4f4f4', hc: null }); if (color) { collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .cell .monaco-editor-background, - .monaco-workbench .part.editor > .content .notebook-editor .cell .margin-view-overlays { background: ${color}; }`); + .monaco-workbench .part.editor > .content .notebook-editor .cell .margin-view-overlays, + .monaco-workbench .part.editor > .content .notebook-editor .cell .cell-statusbar-container { background: ${color}; }`); } const link = theme.getColor(textLinkForeground); if (link) { @@ -933,11 +934,15 @@ registerThemingParticipant((theme, collector) => { } const containerBackground = theme.getColor(notebookOutputContainerColor); - if (containerBackground) { collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .output { background-color: ${containerBackground}; }`); } + const editorBackgroundColor = theme.getColor(editorBackground); + if (editorBackgroundColor) { + collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .cell-statusbar-container { border-top: solid 1px ${editorBackgroundColor}; }`); + } + const focusedCellIndicatorColor = theme.getColor(focusedCellIndicator); if (focusedCellIndicatorColor) { collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .monaco-list-row.focused .notebook-cell-focus-indicator { border-color: ${focusedCellIndicatorColor}; }`); 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 a2a79f6793b..f957b609378 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts @@ -35,7 +35,7 @@ import { StatefullMarkdownCell } from 'vs/workbench/contrib/notebook/browser/vie import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; import { MarkdownCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel'; import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; -import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, NotebookCellRunState } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { renderCodicons } from 'vs/base/common/codicons'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { KeyCode } from 'vs/base/common/keyCodes'; @@ -397,10 +397,10 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende const runToolbar = this.createToolbar(runButtonContainer); disposables.add(runToolbar); - const executionOrderLabel = DOM.append(runButtonContainer, $('div.execution-count-label')); - const editorContainer = DOM.append(cellContainer, $('.cell-editor-container')); + const editorPart = DOM.append(cellContainer, $('.cell-editor-part')); + const editorContainer = DOM.append(editorPart, $('.cell-editor-container')); const editor = this.instantiationService.createInstance(CodeEditorWidget, editorContainer, { ...this.editorOptions, dimension: { @@ -409,22 +409,26 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende } }, {}); - const focusIndicator = DOM.append(container, DOM.$('.notebook-cell-focus-indicator')); - - const outputContainer = document.createElement('div'); - DOM.addClasses(outputContainer, 'output'); - container.appendChild(outputContainer); - - const progressBar = new ProgressBar(editorContainer); + const progressBar = new ProgressBar(editorPart); progressBar.hide(); disposables.add(progressBar); + const statusBarContainer = DOM.append(editorPart, $('.cell-statusbar-container')); + const cellRunStatusContainer = DOM.append(statusBarContainer, $('.cell-run-status')); + const cellStatusMessageContainer = DOM.append(statusBarContainer, $('.cell-status-message')); + const cellStatusPlaceholderContainer = DOM.append(statusBarContainer, $('.cell-status-placeholder')); + + const focusIndicator = DOM.append(container, DOM.$('.notebook-cell-focus-indicator')); + const outputContainer = DOM.append(container, $('.output')); const bottomCellContainer = DOM.append(container, $('.cell-bottom-toolbar-container')); return { container, cellContainer, - editorContainer, + statusBarContainer, + cellRunStatusContainer, + cellStatusMessageContainer, + cellStatusPlaceholderContainer, progressBar, focusIndicator, toolbar, @@ -456,6 +460,39 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende } } + private updateForMetadata(element: CodeCellViewModel, templateData: CodeCellRenderTemplate, cellEditableKey: IContextKey): void { + const metadata = element.getEvaluatedMetadata(this.notebookEditor.viewModel!.notebookDocument.metadata); + DOM.toggleClass(templateData.cellContainer, 'runnable', !!metadata.runnable); + this.renderExecutionOrder(element, templateData); + cellEditableKey.set(!!metadata.editable); + templateData.cellStatusMessageContainer.textContent = metadata?.statusMessage || ''; + + if (metadata.runState === NotebookCellRunState.Success) { + templateData.cellRunStatusContainer.innerHTML = renderCodicons('$(check)'); + } else if (metadata.runState === NotebookCellRunState.Error) { + templateData.cellRunStatusContainer.innerHTML = renderCodicons('$(error)'); + } else { + templateData.cellRunStatusContainer.innerHTML = ''; + } + + if (!metadata.statusMessage && (typeof metadata.runState === 'undefined' || metadata.runState === NotebookCellRunState.Idle)) { + templateData.cellStatusPlaceholderContainer.textContent = 'Ctrl + Enter to run'; + } else { + templateData.cellStatusPlaceholderContainer.textContent = ''; + } + } + + private renderExecutionOrder(element: CodeCellViewModel, templateData: CodeCellRenderTemplate): void { + const hasExecutionOrder = this.notebookEditor.viewModel!.notebookDocument.metadata?.hasExecutionOrder; + if (hasExecutionOrder) { + const executionOrdeerLabel = typeof element.metadata?.executionOrder === 'number' ? `[ ${element.metadata.executionOrder} ]` : + '[ ]'; + templateData.executionOrderLabel.innerText = executionOrdeerLabel; + } else { + templateData.executionOrderLabel.innerText = ''; + } + } + renderElement(element: CodeCellViewModel, index: number, templateData: CodeCellRenderTemplate, height: number | undefined): void { if (height === undefined) { return; @@ -483,28 +520,11 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende this.updateForRunState(element, templateData, runStateKey); elementDisposable.add(element.onDidChangeCellRunState(() => this.updateForRunState(element, templateData, runStateKey))); - const renderExecutionOrder = () => { - const hasExecutionOrder = this.notebookEditor.viewModel!.notebookDocument.metadata?.hasExecutionOrder; - if (hasExecutionOrder) { - const executionOrdeerLabel = typeof element.metadata?.executionOrder === 'number' ? `[ ${element.metadata.executionOrder} ]` : - '[ ]'; - templateData.executionOrderLabel.innerText = executionOrdeerLabel; - } else { - templateData.executionOrderLabel.innerText = ''; - } - }; - contextKeyService.createKey(NOTEBOOK_CELL_TYPE_CONTEXT_KEY, 'code'); contextKeyService.createKey(NOTEBOOK_VIEW_TYPE, element.viewType); const cellEditableKey = contextKeyService.createKey(NOTEBOOK_CELL_EDITABLE_CONTEXT_KEY, !!(element.metadata?.editable)); - const updateForMetadata = () => { - const metadata = element.getEvaluatedMetadata(this.notebookEditor.viewModel!.notebookDocument.metadata); - DOM.toggleClass(templateData.cellContainer, 'runnable', !!metadata.runnable); - renderExecutionOrder(); - cellEditableKey.set(!!metadata.editable); - }; - updateForMetadata(); - elementDisposable.add(element.onDidChangeMetadata(() => updateForMetadata())); + this.updateForMetadata(element, templateData, cellEditableKey); + elementDisposable.add(element.onDidChangeMetadata(() => this.updateForMetadata(element, templateData, cellEditableKey))); this.setupCellToolbarActions(contextKeyService, templateData, elementDisposable); diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts index 27125437587..4af9f361af5 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts @@ -68,6 +68,7 @@ export abstract class BaseCellViewModel extends Disposable implements ICellViewM this._onDidChangeCellEditState.fire(); } + // TODO - move any "run"/"status" concept to Code-specific places private _currentTokenSource: CancellationTokenSource | undefined; public set currentTokenSource(v: CancellationTokenSource | undefined) { this._currentTokenSource = v; @@ -338,7 +339,10 @@ export abstract class BaseCellViewModel extends Disposable implements ICellViewM return { editable, - runnable + runnable, + executionOrder: this.metadata?.executionOrder, + runState: this.metadata?.runState, + statusMessage: this.metadata?.statusMessage }; } diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts index 131c4cad98c..40fc9f07117 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts @@ -8,7 +8,7 @@ import * as UUID from 'vs/base/common/uuid'; import * as model from 'vs/editor/common/model'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { PrefixSumComputer } from 'vs/editor/common/viewModel/prefixSumComputer'; -import { EDITOR_BOTTOM_PADDING, EDITOR_TOOLBAR_HEIGHT, EDITOR_TOP_PADDING, CELL_MARGIN, CELL_RUN_GUTTER, EDITOR_TOP_MARGIN, BOTTOM_CELL_TOOLBAR_HEIGHT } from 'vs/workbench/contrib/notebook/browser/constants'; +import { EDITOR_BOTTOM_PADDING, EDITOR_TOOLBAR_HEIGHT, EDITOR_TOP_PADDING, CELL_MARGIN, CELL_RUN_GUTTER, EDITOR_TOP_MARGIN, BOTTOM_CELL_TOOLBAR_HEIGHT, CELL_STATUSBAR_HEIGHT } from 'vs/workbench/contrib/notebook/browser/constants'; import { CellEditState, ICellViewModel, CellFindMatch, CodeCellLayoutChangeEvent, CodeCellLayoutInfo, NotebookLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CellKind, NotebookCellOutputsSplice } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { BaseCellViewModel } from './baseCellViewModel'; @@ -108,7 +108,7 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod // recompute this._ensureOutputsTop(); const outputTotalHeight = this._outputsTop!.getTotalValue(); - const totalHeight = EDITOR_TOOLBAR_HEIGHT + this.editorHeight + EDITOR_TOP_MARGIN + outputTotalHeight + BOTTOM_CELL_TOOLBAR_HEIGHT; + const totalHeight = EDITOR_TOOLBAR_HEIGHT + this.editorHeight + EDITOR_TOP_MARGIN + outputTotalHeight + BOTTOM_CELL_TOOLBAR_HEIGHT + CELL_STATUSBAR_HEIGHT; const indicatorHeight = this.editorHeight + outputTotalHeight; const outputContainerOffset = EDITOR_TOOLBAR_HEIGHT + EDITOR_TOP_MARGIN + this.editorHeight; const bottomToolbarOffset = totalHeight - BOTTOM_CELL_TOOLBAR_HEIGHT; diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index 90e768b9520..1e4b71c1428 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -50,10 +50,19 @@ export interface NotebookDocumentMetadata { hasExecutionOrder: boolean; } +export enum NotebookCellRunState { + Running = 1, + Idle = 2, + Success = 3, + Error = 4 +} + export interface NotebookCellMetadata { editable?: boolean; runnable?: boolean; executionOrder?: number; + statusMessage?: string; + runState?: NotebookCellRunState; } export interface INotebookDisplayOrder {