diff --git a/src/vs/base/browser/ui/codicons/codicon/codicon-modifiers.css b/src/vs/base/browser/ui/codicons/codicon/codicon-modifiers.css index 4b5b5141489..9666216f6ae 100644 --- a/src/vs/base/browser/ui/codicons/codicon/codicon-modifiers.css +++ b/src/vs/base/browser/ui/codicons/codicon/codicon-modifiers.css @@ -13,7 +13,10 @@ } } -.codicon-sync.codicon-modifier-spin, .codicon-loading.codicon-modifier-spin, .codicon-gear.codicon-modifier-spin { +.codicon-sync.codicon-modifier-spin, +.codicon-loading.codicon-modifier-spin, +.codicon-gear.codicon-modifier-spin, +.codicon-notebook-state-executing.codicon-modifier-spin { /* Use steps to throttle FPS to reduce CPU usage */ animation: codicon-spin 1.5s steps(30) infinite; } diff --git a/src/vs/workbench/api/common/extHostNotebook.ts b/src/vs/workbench/api/common/extHostNotebook.ts index dc2c4fc32e4..e5704becf78 100644 --- a/src/vs/workbench/api/common/extHostNotebook.ts +++ b/src/vs/workbench/api/common/extHostNotebook.ts @@ -752,7 +752,7 @@ class NotebookCellExecutionTask extends Disposable { that.mixinMetadata({ runState: extHostTypes.NotebookCellExecutionState.Executing, - runStartTime: context?.startTime + runStartTime: context?.startTime ?? null }); }, diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/statusBar/contributedStatusBarItemController.ts b/src/vs/workbench/contrib/notebook/browser/contrib/statusBar/contributedStatusBarItemController.ts new file mode 100644 index 00000000000..22ee9a032f4 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/contrib/statusBar/contributedStatusBarItemController.ts @@ -0,0 +1,123 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { flatten } from 'vs/base/common/arrays'; +import { Throttler } from 'vs/base/common/async'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; +import { ICellVisibilityChangeEvent, NotebookVisibleCellObserver } from 'vs/workbench/contrib/notebook/browser/contrib/statusBar/notebookVisibleCellObserver'; +import { ICellViewModel, INotebookEditor, INotebookEditorContribution } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { registerNotebookContribution } from 'vs/workbench/contrib/notebook/browser/notebookEditorExtensions'; +import { NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; +import { INotebookCellStatusBarService } from 'vs/workbench/contrib/notebook/common/notebookCellStatusBarService'; +import { INotebookCellStatusBarItemList } from 'vs/workbench/contrib/notebook/common/notebookCommon'; + +export class ContributedStatusBarItemController extends Disposable implements INotebookEditorContribution { + static id: string = 'workbench.notebook.statusBar'; + + private readonly _visibleCells = new Map(); + + private readonly _observer: NotebookVisibleCellObserver; + + constructor( + private readonly _notebookEditor: INotebookEditor, + @INotebookCellStatusBarService private readonly _notebookCellStatusBarService: INotebookCellStatusBarService + ) { + super(); + this._observer = this._register(new NotebookVisibleCellObserver(this._notebookEditor)); + this._register(this._observer.onDidChangeVisibleCells(this._updateVisibleCells, this)); + + this._updateEverything(); + this._register(this._notebookCellStatusBarService.onDidChangeProviders(this._updateEverything, this)); + this._register(this._notebookCellStatusBarService.onDidChangeItems(this._updateEverything, this)); + } + + private _updateEverything(): void { + this._visibleCells.forEach(cell => cell.dispose()); + this._visibleCells.clear(); + this._updateVisibleCells({ added: this._observer.visibleCells, removed: [] }); + } + + private _updateVisibleCells(e: ICellVisibilityChangeEvent): void { + const vm = this._notebookEditor.viewModel; + if (!vm) { + return; + } + + for (let newCell of e.added) { + const helper = new CellStatusBarHelper(vm, newCell, this._notebookCellStatusBarService); + this._visibleCells.set(newCell.handle, helper); + } + + for (let oldCell of e.removed) { + this._visibleCells.get(oldCell.handle)?.dispose(); + this._visibleCells.delete(oldCell.handle); + } + } + + override dispose(): void { + super.dispose(); + + this._visibleCells.forEach(cell => cell.dispose()); + this._visibleCells.clear(); + } +} + +class CellStatusBarHelper extends Disposable { + private _currentItemIds: string[] = []; + private _currentItemLists: INotebookCellStatusBarItemList[] = []; + + private readonly _cancelTokenSource: CancellationTokenSource; + + private readonly _updateThrottler = new Throttler(); + + constructor( + private readonly _notebookViewModel: NotebookViewModel, + private readonly _cell: ICellViewModel, + private readonly _notebookCellStatusBarService: INotebookCellStatusBarService + ) { + super(); + + this._cancelTokenSource = new CancellationTokenSource(); + this._register(toDisposable(() => this._cancelTokenSource.dispose(true))); + + this._updateSoon(); + this._register(this._cell.model.onDidChangeContent(() => this._updateSoon())); + this._register(this._cell.model.onDidChangeLanguage(() => this._updateSoon())); + this._register(this._cell.model.onDidChangeMetadata(() => this._updateSoon())); + this._register(this._cell.model.onDidChangeOutputs(() => this._updateSoon())); + } + + private _updateSoon(): void { + this._updateThrottler.queue(() => this._update()); + } + + private async _update() { + const cellIndex = this._notebookViewModel.getCellIndex(this._cell); + const docUri = this._notebookViewModel.notebookDocument.uri; + const viewType = this._notebookViewModel.notebookDocument.viewType; + const itemLists = await this._notebookCellStatusBarService.getStatusBarItemsForCell(docUri, cellIndex, viewType, this._cancelTokenSource.token); + if (this._cancelTokenSource.token.isCancellationRequested) { + itemLists.forEach(itemList => itemList.dispose && itemList.dispose()); + return; + } + + const items = flatten(itemLists.map(itemList => itemList.items)); + const newIds = this._notebookViewModel.deltaCellStatusBarItems(this._currentItemIds, [{ handle: this._cell.handle, items }]); + + this._currentItemLists.forEach(itemList => itemList.dispose && itemList.dispose()); + this._currentItemLists = itemLists; + this._currentItemIds = newIds; + } + + override dispose() { + super.dispose(); + + this._notebookViewModel.deltaCellStatusBarItems(this._currentItemIds, [{ handle: this._cell.handle, items: [] }]); + this._currentItemLists.forEach(itemList => itemList.dispose && itemList.dispose()); + } +} + +registerNotebookContribution(ContributedStatusBarItemController.id, ContributedStatusBarItemController); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/statusBar/executionStatusBarItemController.ts b/src/vs/workbench/contrib/notebook/browser/contrib/statusBar/executionStatusBarItemController.ts new file mode 100644 index 00000000000..9eed1fef299 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/contrib/statusBar/executionStatusBarItemController.ts @@ -0,0 +1,228 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { RunOnceScheduler } from 'vs/base/common/async'; +import { Event } from 'vs/base/common/event'; +import { Disposable, dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { localize } from 'vs/nls'; +import { themeColorFromId, ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { ICellVisibilityChangeEvent, NotebookVisibleCellObserver } from 'vs/workbench/contrib/notebook/browser/contrib/statusBar/notebookVisibleCellObserver'; +import { ICellViewModel, INotebookEditor, INotebookEditorContribution } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { registerNotebookContribution } from 'vs/workbench/contrib/notebook/browser/notebookEditorExtensions'; +import { cellStatusIconSuccess, cellStatusIconError } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidget'; +import { successStateIcon, errorStateIcon, pendingStateIcon, executingStateIcon } from 'vs/workbench/contrib/notebook/browser/notebookIcons'; +import { NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; +import { CellStatusbarAlignment, INotebookCellStatusBarItem, NotebookCellExecutionState } from 'vs/workbench/contrib/notebook/common/notebookCommon'; + +export class NotebookStatusBarController extends Disposable implements INotebookEditorContribution { + static id: string = 'workbench.notebook.statusBar'; + + private readonly _visibleCells = new Map(); + + private readonly _observer: NotebookVisibleCellObserver; + + constructor( + private readonly _notebookEditor: INotebookEditor, + ) { + super(); + this._observer = this._register(new NotebookVisibleCellObserver(this._notebookEditor)); + this._register(this._observer.onDidChangeVisibleCells(this._updateVisibleCells, this)); + + this._updateEverything(); + } + + private _updateEverything(): void { + this._visibleCells.forEach(dispose); + this._visibleCells.clear(); + this._updateVisibleCells({ added: this._observer.visibleCells, removed: [] }); + } + + private _updateVisibleCells(e: ICellVisibilityChangeEvent): void { + const vm = this._notebookEditor.viewModel; + if (!vm) { + return; + } + + for (let newCell of e.added) { + const helpers = [ + new ExecutionStateCellStatusBarHelper(vm, newCell), + new TimerCellStatusBarHelper(vm, newCell) + ]; + this._visibleCells.set(newCell.handle, helpers); + } + + for (let oldCell of e.removed) { + this._visibleCells.get(oldCell.handle)?.forEach(dispose); + this._visibleCells.delete(oldCell.handle); + } + } + + override dispose(): void { + super.dispose(); + + this._visibleCells.forEach(dispose); + this._visibleCells.clear(); + } +} + +/** + * Shows the cell's execution state in the cell status bar. When the "executing" state is shown, it will be shown for a minimum brief time. + */ +class ExecutionStateCellStatusBarHelper extends Disposable { + private static readonly MIN_SPINNER_TIME = 500; + + private _currentItemIds: string[] = []; + + private _currentExecutingStateTimer: any; + + constructor( + private readonly _notebookViewModel: NotebookViewModel, + private readonly _cell: ICellViewModel, + ) { + super(); + + this._update(); + this._register( + Event.filter(this._cell.model.onDidChangeMetadata, e => !!e.runStateChanged) + (() => this._update())); + } + + private async _update() { + const items = this._getItemsForCell(this._cell); + if (Array.isArray(items)) { + this._currentItemIds = this._notebookViewModel.deltaCellStatusBarItems(this._currentItemIds, [{ handle: this._cell.handle, items }]); + } + } + + /** + * Returns undefined if there should be no change, and an empty array if all items should be removed. + */ + private _getItemsForCell(cell: ICellViewModel): INotebookCellStatusBarItem[] | undefined { + if (this._currentExecutingStateTimer) { + return; + } + + const item = this._getItemForState(cell.metadata?.runState, cell.metadata?.lastRunSuccess); + + // Show the execution spinner for a minimum time + if (cell.metadata?.runState === NotebookCellExecutionState.Executing) { + this._currentExecutingStateTimer = setTimeout(() => { + this._currentExecutingStateTimer = undefined; + if (cell.metadata?.runState !== NotebookCellExecutionState.Executing) { + this._update(); + } + }, ExecutionStateCellStatusBarHelper.MIN_SPINNER_TIME); + } + + return item ? [item] : []; + } + + private _getItemForState(runState: NotebookCellExecutionState | undefined, lastRunSuccess: boolean | undefined): INotebookCellStatusBarItem | undefined { + if (runState === NotebookCellExecutionState.Idle && lastRunSuccess) { + return { + icon: successStateIcon, + iconColor: themeColorFromId(cellStatusIconSuccess), + tooltip: localize('notebook.cell.status.success', "Success"), + alignment: CellStatusbarAlignment.Left, + priority: Number.MAX_SAFE_INTEGER + }; + } else if (runState === NotebookCellExecutionState.Idle && !lastRunSuccess) { + return { + icon: errorStateIcon, + iconColor: themeColorFromId(cellStatusIconError), + tooltip: localize('notebook.cell.status.failed', "Failed"), + alignment: CellStatusbarAlignment.Left, + priority: Number.MAX_SAFE_INTEGER + }; + } else if (runState === NotebookCellExecutionState.Pending) { + return { + icon: pendingStateIcon, + tooltip: localize('notebook.cell.status.pending', "Pending"), + alignment: CellStatusbarAlignment.Left, + priority: Number.MAX_SAFE_INTEGER + }; + } else if (runState === NotebookCellExecutionState.Executing) { + return { + icon: ThemeIcon.modify(executingStateIcon, 'spin'), + tooltip: localize('notebook.cell.status.executing', "Executing"), + alignment: CellStatusbarAlignment.Left, + priority: Number.MAX_SAFE_INTEGER + }; + } + + return; + } + + override dispose() { + super.dispose(); + + this._notebookViewModel.deltaCellStatusBarItems(this._currentItemIds, [{ handle: this._cell.handle, items: [] }]); + } +} + +class TimerCellStatusBarHelper extends Disposable { + private static UPDATE_INTERVAL = 100; + private _currentItemIds: string[] = []; + + private _scheduler: RunOnceScheduler; + + constructor( + private readonly _notebookViewModel: NotebookViewModel, + private readonly _cell: ICellViewModel, + ) { + super(); + + this._update(); + this._register( + Event.filter(this._cell.model.onDidChangeMetadata, e => !!e.runStateChanged) + (() => this._update())); + this._scheduler = this._register(new RunOnceScheduler(() => this._update(), TimerCellStatusBarHelper.UPDATE_INTERVAL)); + } + + private async _update() { + let item: INotebookCellStatusBarItem | undefined; + if (this._cell.metadata?.runState === NotebookCellExecutionState.Executing) { + const startTime = this._cell.metadata.runStartTime; + const adjustment = this._cell.metadata.runStartTimeAdjustment; + if (typeof startTime === 'number') { + item = this._getTimeItem(startTime, Date.now(), adjustment); + this._scheduler.schedule(); + } + } else if (this._cell.metadata?.runState === NotebookCellExecutionState.Idle) { + const startTime = this._cell.metadata.runStartTime; + const endTime = this._cell.metadata.runEndTime; + if (typeof startTime === 'number' && typeof endTime === 'number') { + item = this._getTimeItem(startTime, endTime); + } + } + + const items = item ? [item] : []; + this._currentItemIds = this._notebookViewModel.deltaCellStatusBarItems(this._currentItemIds, [{ handle: this._cell.handle, items }]); + } + + private _getTimeItem(startTime: number, endTime: number, adjustment: number = 0): INotebookCellStatusBarItem { + const duration = endTime - startTime + adjustment; + return { + text: this._formatDuration(duration), + alignment: CellStatusbarAlignment.Left, + priority: Number.MAX_SAFE_INTEGER - 1 + }; + } + + private _formatDuration(duration: number) { + const seconds = Math.floor(duration / 1000); + const tenths = String(duration - seconds * 1000).charAt(0); + + return `${seconds}.${tenths}s`; + } + + override dispose() { + super.dispose(); + + this._notebookViewModel.deltaCellStatusBarItems(this._currentItemIds, [{ handle: this._cell.handle, items: [] }]); + } +} + +registerNotebookContribution(NotebookStatusBarController.id, NotebookStatusBarController); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/statusBar/notebookVisibleCellObserver.ts b/src/vs/workbench/contrib/notebook/browser/contrib/statusBar/notebookVisibleCellObserver.ts new file mode 100644 index 00000000000..c0827f60e77 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/contrib/statusBar/notebookVisibleCellObserver.ts @@ -0,0 +1,83 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { diffSets } from 'vs/base/common/collections'; +import { Emitter } from 'vs/base/common/event'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { isDefined } from 'vs/base/common/types'; +import { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; +import { cellRangesToIndexes } from 'vs/workbench/contrib/notebook/common/notebookRange'; + +export interface ICellVisibilityChangeEvent { + added: CellViewModel[]; + removed: CellViewModel[]; +} + +export class NotebookVisibleCellObserver extends Disposable { + private readonly _onDidChangeVisibleCells = this._register(new Emitter()); + readonly onDidChangeVisibleCells = this._onDidChangeVisibleCells.event; + + private readonly _viewModelDisposables = this._register(new DisposableStore()); + + private _visibleCells: CellViewModel[] = []; + + get visibleCells(): CellViewModel[] { + return this._visibleCells; + } + + constructor(private readonly _notebookEditor: INotebookEditor) { + super(); + + this._register(this._notebookEditor.onDidChangeVisibleRanges(this._updateVisibleCells, this)); + this._register(this._notebookEditor.onDidChangeModel(this._onModelChange, this)); + this._updateVisibleCells(); + } + + private _onModelChange() { + this._viewModelDisposables.clear(); + const vm = this._notebookEditor.viewModel; + if (vm) { + this._viewModelDisposables.add(vm.onDidChangeViewCells(() => this.updateEverything())); + } + + this.updateEverything(); + } + + protected updateEverything(): void { + this._onDidChangeVisibleCells.fire({ added: [], removed: Array.from(this._visibleCells) }); + this._visibleCells = []; + this._updateVisibleCells(); + } + + private _updateVisibleCells(): void { + const vm = this._notebookEditor.viewModel; + if (!vm) { + return; + } + + const rangesWithEnd = this._notebookEditor.visibleRanges + .map(range => ({ start: range.start, end: range.end + 1 })); + const newVisibleCells = cellRangesToIndexes(rangesWithEnd) + .map(index => vm.cellAt(index)) + .filter(isDefined); + const newVisibleHandles = new Set(newVisibleCells.map(cell => cell.handle)); + const oldVisibleHandles = new Set(this._visibleCells.map(cell => cell.handle)); + const diff = diffSets(oldVisibleHandles, newVisibleHandles); + + const added = diff.added + .map(handle => vm.getCellByHandle(handle)) + .filter(isDefined); + const removed = diff.removed + .map(handle => vm.getCellByHandle(handle)) + .filter(isDefined); + + this._visibleCells = newVisibleCells; + this._onDidChangeVisibleCells.fire({ + added, + removed + }); + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/statusBar/statusBarProviders.ts b/src/vs/workbench/contrib/notebook/browser/contrib/statusBar/statusBarProviders.ts index 52babeda60a..172ea36e907 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/statusBar/statusBarProviders.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/statusBar/statusBarProviders.ts @@ -105,8 +105,14 @@ class BuiltinCellStatusBarProviders extends Disposable { @IInstantiationService instantiationService: IInstantiationService, @INotebookCellStatusBarService notebookCellStatusBarService: INotebookCellStatusBarService) { super(); - this._register(notebookCellStatusBarService.registerCellStatusBarItemProvider(instantiationService.createInstance(CellStatusBarPlaceholderProvider))); - this._register(notebookCellStatusBarService.registerCellStatusBarItemProvider(instantiationService.createInstance(CellStatusBarLanguagePickerProvider))); + + const builtinProviders = [ + CellStatusBarPlaceholderProvider, + CellStatusBarLanguagePickerProvider, + ]; + builtinProviders.forEach(p => { + this._register(notebookCellStatusBarService.registerCellStatusBarItemProvider(instantiationService.createInstance(p))); + }); } } diff --git a/src/vs/workbench/contrib/notebook/browser/media/notebook.css b/src/vs/workbench/contrib/notebook/browser/media/notebook.css index 62a7105fb43..95ce008fc51 100644 --- a/src/vs/workbench/contrib/notebook/browser/media/notebook.css +++ b/src/vs/workbench/contrib/notebook/browser/media/notebook.css @@ -425,10 +425,6 @@ z-index: 26; } -.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-status-right { - flex-direction: row-reverse; -} - .monaco-workbench .notebookOverlay .cell-statusbar-container .cell-status-right .cell-contributed-items { justify-content: flex-end; } @@ -946,6 +942,10 @@ margin-left: 4px; } +.cell-contributed-items.cell-contributed-items-right { + flex-direction: row-reverse; +} + .monaco-workbench .notebookOverlay .output .error::-webkit-scrollbar, .monaco-workbench .notebookOverlay .output-plaintext::-webkit-scrollbar { width: 10px; /* width of the entire scrollbar */ diff --git a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts index c6db38500ca..fdf366fd545 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -58,7 +58,8 @@ import 'vs/workbench/contrib/notebook/browser/contrib/format/formatting'; import 'vs/workbench/contrib/notebook/browser/contrib/marker/markerProvider'; import 'vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline'; import 'vs/workbench/contrib/notebook/browser/contrib/statusBar/statusBarProviders'; -import 'vs/workbench/contrib/notebook/browser/contrib/statusBar/cellStatusBar'; +import 'vs/workbench/contrib/notebook/browser/contrib/statusBar/contributedStatusBarItemController'; +import 'vs/workbench/contrib/notebook/browser/contrib/statusBar/executionStatusBarItemController'; import 'vs/workbench/contrib/notebook/browser/contrib/status/editorStatus'; import 'vs/workbench/contrib/notebook/browser/contrib/undoRedo/notebookUndoRedo'; import 'vs/workbench/contrib/notebook/browser/contrib/cellOperations/cellOperations'; diff --git a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index 68d8b66cfdd..6ded5c79ec9 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts @@ -19,7 +19,6 @@ import { Range } from 'vs/editor/common/core/range'; import { FindMatch, IReadonlyTextBuffer, ITextModel } from 'vs/editor/common/model'; import { ContextKeyExpr, RawContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { OutputRenderer } from 'vs/workbench/contrib/notebook/browser/view/output/outputRenderer'; -import { RunStateRenderer, TimerRenderer } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer'; import { CellViewModel, IModelDecorationsChangeAccessor, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; import { CellKind, NotebookCellMetadata, INotebookKernel, IOrderedMimeType, INotebookRendererInfo, ICellOutput, IOutputItemDto, INotebookCellStatusBarItem } from 'vs/workbench/contrib/notebook/common/notebookCommon'; @@ -730,7 +729,6 @@ export interface MarkdownCellRenderTemplate extends BaseCellRenderTemplate { } export interface CodeCellRenderTemplate extends BaseCellRenderTemplate { - cellRunState: RunStateRenderer; runToolbar: ToolBar; runButtonContainer: HTMLElement; executionOrderLabel: HTMLElement; @@ -739,7 +737,6 @@ export interface CodeCellRenderTemplate extends BaseCellRenderTemplate { focusSinkElement: HTMLElement; editor: ICodeEditor; progressBar: ProgressBar; - timer: TimerRenderer; focusIndicatorRight: HTMLElement; focusIndicatorBottom: HTMLElement; dragHandle: HTMLElement; diff --git a/src/vs/workbench/contrib/notebook/browser/notebookIcons.ts b/src/vs/workbench/contrib/notebook/browser/notebookIcons.ts index 22adce5fbc2..edeff4ff744 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookIcons.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookIcons.ts @@ -24,6 +24,8 @@ export const unfoldIcon = registerIcon('notebook-unfold', Codicon.unfold, locali export const successStateIcon = registerIcon('notebook-state-success', Codicon.check, localize('successStateIcon', 'Icon to indicate a success state in notebook editors.')); export const errorStateIcon = registerIcon('notebook-state-error', Codicon.error, localize('errorStateIcon', 'Icon to indicate an error state in notebook editors.')); +export const pendingStateIcon = registerIcon('notebook-state-pending', Codicon.clock, localize('pendingStateIcon', 'Icon to indicate a pending state in notebook editors.')); +export const executingStateIcon = registerIcon('notebook-state-executing', Codicon.sync, localize('executingStateIcon', 'Icon to indicate an executing state in notebook editors.')); export const collapsedIcon = registerIcon('notebook-collapsed', Codicon.chevronRight, localize('collapsedIcon', 'Icon to annotate a collapsed section in notebook editors.')); export const expandedIcon = registerIcon('notebook-expanded', Codicon.chevronDown, localize('expandedIcon', 'Icon to annotate an expanded section in notebook editors.')); 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 1404c7a45fb..39f8f28660d 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts @@ -343,8 +343,6 @@ export class MarkdownCellRenderer extends AbstractCellRenderer implements IListR const focusIndicatorBottom = DOM.append(container, $('.cell-focus-indicator.cell-focus-indicator-bottom')); const statusBar = disposables.add(this.instantiationService.createInstance(CellEditorStatusBar, editorPart)); - DOM.hide(statusBar.durationContainer); - DOM.hide(statusBar.cellRunStatusContainer); const titleMenu = disposables.add(this.cellMenus.getCellTitleMenu(contextKeyService)); @@ -717,8 +715,6 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende disposables.add(progressBar); const statusBar = disposables.add(this.instantiationService.createInstance(CellEditorStatusBar, editorPart)); - const timer = new TimerRenderer(statusBar.durationContainer); - const cellRunState = new RunStateRenderer(statusBar.cellRunStatusContainer); const outputContainer = DOM.append(container, $('.output')); const outputShowMoreContainer = DOM.append(container, $('.output-show-more-container')); @@ -742,7 +738,6 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende container, decorationContainer, cellContainer, - cellRunState, progressBar, statusBar, focusIndicatorLeft: focusIndicator, @@ -761,7 +756,6 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende disposables, elementDisposables: new DisposableStore(), bottomCellContainer, - timer, titleMenu, dragHandle, toJSON: () => { return {}; } @@ -833,26 +827,6 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende const metadata = element.metadata; this.updateExecutionOrder(metadata, templateData); - templateData.cellRunState.renderState(element.metadata?.runState, () => { - if (!this.notebookEditor.viewModel) { - return -1; - } - - return this.notebookEditor.viewModel.getCellIndex(element); - }, element.metadata?.lastRunSuccess); - - if (metadata.runState === NotebookCellExecutionState.Executing) { - if (metadata.runStartTime) { - templateData.elementDisposables.add(templateData.timer.start(metadata.runStartTime, metadata.runStartTimeAdjustment ?? 0)); - } else { - templateData.timer.clear(); - } - } else if (metadata.runState !== NotebookCellExecutionState.Pending && metadata.runStartTime && metadata.runEndTime) { - templateData.timer.show(metadata.runEndTime - metadata.runStartTime); - } else { - templateData.timer.clear(); - } - if (metadata.runState === NotebookCellExecutionState.Executing) { templateData.progressBar.infinite().show(500); } else { @@ -951,7 +925,6 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende this.updateForLayout(element, templateData); })); - templateData.cellRunState.clear(); this.updateForMetadata(element, templateData, cellEditorOptions); this.updateForHover(element, templateData); this.updateForFocus(element, templateData); diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellWidgets.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellWidgets.ts index a065a735404..6fe9bd5debe 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellWidgets.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellWidgets.ts @@ -5,6 +5,7 @@ import * as DOM from 'vs/base/browser/dom'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { renderIcon } from 'vs/base/browser/ui/iconLabel/iconLabels'; import { SimpleIconLabel } from 'vs/base/browser/ui/iconLabel/simpleIconLabel'; import { WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from 'vs/base/common/actions'; import { toErrorMessage } from 'vs/base/common/errorMessage'; @@ -13,12 +14,14 @@ import { stripIcons } from 'vs/base/common/iconLabels'; import { KeyCode } from 'vs/base/common/keyCodes'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { ElementSizeObserver } from 'vs/editor/browser/config/elementSizeObserver'; -import { IDimension } from 'vs/editor/common/editorCommon'; +import { IDimension, isThemeColor } from 'vs/editor/common/editorCommon'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IThemeService, ThemeColor } from 'vs/platform/theme/common/themeService'; import { INotebookCellActionContext } from 'vs/workbench/contrib/notebook/browser/contrib/coreActions'; +import { CodeCellLayoutInfo, MarkdownCellLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CellStatusbarAlignment, INotebookCellStatusBarItem } from 'vs/workbench/contrib/notebook/common/notebookCommon'; const $ = DOM.$; @@ -30,15 +33,12 @@ export interface IClickTarget { export const enum ClickTargetType { Container = 0, - CellStatus = 1, - ContributedTextItem = 2, - ContributedCommandItem = 3 + ContributedTextItem = 1, + ContributedCommandItem = 2 } export class CellEditorStatusBar extends Disposable { - readonly cellRunStatusContainer: HTMLElement; readonly statusBarContainer: HTMLElement; - readonly durationContainer: HTMLElement; private readonly leftContributedItemsContainer: HTMLElement; private readonly rightContributedItemsContainer: HTMLElement; @@ -53,20 +53,21 @@ export class CellEditorStatusBar extends Disposable { constructor( container: HTMLElement, - @IInstantiationService private readonly instantiationService: IInstantiationService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IThemeService private readonly _themeService: IThemeService, ) { super(); this.statusBarContainer = DOM.append(container, $('.cell-statusbar-container')); this.statusBarContainer.tabIndex = -1; const leftItemsContainer = DOM.append(this.statusBarContainer, $('.cell-status-left')); const rightItemsContainer = DOM.append(this.statusBarContainer, $('.cell-status-right')); - this.cellRunStatusContainer = DOM.append(leftItemsContainer, $('.cell-run-status')); - this.durationContainer = DOM.append(leftItemsContainer, $('.cell-run-duration')); this.leftContributedItemsContainer = DOM.append(leftItemsContainer, $('.cell-contributed-items.cell-contributed-items-left')); this.rightContributedItemsContainer = DOM.append(rightItemsContainer, $('.cell-contributed-items.cell-contributed-items-right')); this.itemsDisposable = this._register(new DisposableStore()); + this._register(this._themeService.onDidColorThemeChange(() => this.currentContext && this.update(this.currentContext))); + this._register(DOM.addDisposableListener(this.statusBarContainer, DOM.EventType.CLICK, e => { if (e.target === leftItemsContainer || e.target === rightItemsContainer || e.target === this.statusBarContainer) { // hit on empty space @@ -74,14 +75,6 @@ export class CellEditorStatusBar extends Disposable { type: ClickTargetType.Container, event: e }); - } else if (e.target && ( - this.cellRunStatusContainer.contains(e.target as Node) - || this.durationContainer.contains(e.target as Node) - )) { - this._onDidClick.fire({ - type: ClickTargetType.CellStatus, - event: e - }); } else { if ((e.target as HTMLElement).classList.contains('cell-status-item-has-command')) { this._onDidClick.fire({ @@ -99,13 +92,18 @@ export class CellEditorStatusBar extends Disposable { })); } - update(context: INotebookCellActionContext) { - this.currentContext = context; - this.itemsDisposable.clear(); - this.updateStatusBarItems(); - } + private layout(): void { + if (!this.currentContext) { + return; + } + + // TODO@roblou maybe more props should be in common layoutInfo? + const layoutInfo = this.currentContext.cell.layoutInfo as CodeCellLayoutInfo | MarkdownCellLayoutInfo; + const width = layoutInfo.editorWidth; + if (!width) { + return; + } - layout(width: number): void { this.width = width; this.statusBarContainer.style.width = `${width}px`; @@ -117,13 +115,17 @@ export class CellEditorStatusBar extends Disposable { return this.width / 2; } - private async updateStatusBarItems() { + update(context: INotebookCellActionContext) { + this.currentContext = context; + this.itemsDisposable.clear(); + if (!this.currentContext) { return; } this.itemsDisposable.add(this.currentContext.notebookEditor.onDidChangeActiveCell(() => this.updateActiveCell())); this.itemsDisposable.add(this.currentContext.cell.onDidChangeCellStatusBarItems(() => this.updateRenderedItems())); + this.itemsDisposable.add(this.currentContext.cell.onDidChangeLayout(e => this.layout())); this.updateActiveCell(); this.updateRenderedItems(); } @@ -144,9 +146,9 @@ export class CellEditorStatusBar extends Disposable { const maxItemWidth = this.getMaxItemWidth(); const leftItems = items.filter(item => item.alignment === CellStatusbarAlignment.Left) - .map(item => this.itemsDisposable.add(this.instantiationService.createInstance(CellStatusBarItem, this.currentContext!, item, maxItemWidth))); + .map(item => this.itemsDisposable.add(this._instantiationService.createInstance(CellStatusBarItem, this.currentContext!, item, maxItemWidth))); const rightItems = items.filter(item => item.alignment === CellStatusbarAlignment.Right).reverse() - .map(item => this.itemsDisposable.add(this.instantiationService.createInstance(CellStatusBarItem, this.currentContext!, item, maxItemWidth))); + .map(item => this.itemsDisposable.add(this._instantiationService.createInstance(CellStatusBarItem, this.currentContext!, item, maxItemWidth))); leftItems.forEach(itemView => this.leftContributedItemsContainer.appendChild(itemView.container)); rightItems.forEach(itemView => this.rightContributedItemsContainer.appendChild(itemView.container)); this.items = [...leftItems, ...rightItems]; @@ -165,12 +167,43 @@ class CellStatusBarItem extends Disposable { private readonly _context: INotebookCellActionContext, private readonly _itemModel: INotebookCellStatusBarItem, maxWidth: number | undefined, - @ITelemetryService private readonly telemetryService: ITelemetryService, - @ICommandService private readonly commandService: ICommandService, - @INotificationService private readonly notificationService: INotificationService + @ITelemetryService private readonly _telemetryService: ITelemetryService, + @ICommandService private readonly _commandService: ICommandService, + @INotificationService private readonly _notificationService: INotificationService, + @IThemeService private readonly _themeService: IThemeService, ) { super(); - new SimpleIconLabel(this.container).text = this._itemModel.text.replace(/\n/g, ' '); + + const resolveColor = (color: ThemeColor | string) => { + return isThemeColor(color) ? + this._themeService.getColorTheme().getColor(color.id)?.toString() : + color; + }; + + if (this._itemModel.icon) { + const iconContainer = renderIcon(this._itemModel.icon); + if (this._itemModel.iconColor) { + const colorResult = resolveColor(this._itemModel.iconColor); + iconContainer.style.color = colorResult || ''; + } + + this.container.appendChild(iconContainer); + } + + if (this._itemModel.text) { + const textContainer = $('span', undefined); + new SimpleIconLabel(textContainer).text = this._itemModel.text.replace(/\n/g, ' '); + this.container.appendChild(textContainer); + } + + if (this._itemModel.color) { + this.container.style.color = resolveColor(this._itemModel.color) || ''; + } + + if (this._itemModel.backgroundColor) { + const colorResult = resolveColor(this._itemModel.backgroundColor); + this.container.style.backgroundColor = colorResult || ''; + } if (this._itemModel.opacity) { this.container.style.opacity = this._itemModel.opacity; @@ -230,11 +263,11 @@ class CellStatusBarItem extends Disposable { args.unshift(this._context); - this.telemetryService.publicLog2('workbenchActionExecuted', { id, from: 'cell status bar' }); + this._telemetryService.publicLog2('workbenchActionExecuted', { id, from: 'cell status bar' }); try { - await this.commandService.executeCommand(id, ...args); + await this._commandService.executeCommand(id, ...args); } catch (error) { - this.notificationService.error(toErrorMessage(error)); + this._notificationService.error(toErrorMessage(error)); } } } 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 094eb0e0d3d..39d075c91ad 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/codeCell.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/codeCell.ts @@ -293,7 +293,6 @@ export class CodeCell extends Disposable { private layoutEditor(dimension: IDimension): void { this.templateData.editor?.layout(dimension); - this.templateData.statusBar.layout(dimension.width); } private onCellWidthChange(): void { diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/markdownCell.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/markdownCell.ts index 08ae528666b..fb7db714bf8 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/markdownCell.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/markdownCell.ts @@ -375,7 +375,6 @@ export class StatefulMarkdownCell extends Disposable { private layoutEditor(dimension: DOM.IDimension): void { this.editor?.layout(dimension); - this.templateData.statusBar.layout(dimension.width); } private onCellEditorWidthChange(): void { diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index ff1cc5a142f..eca6480e5ef 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -20,7 +20,7 @@ import { IEditorModel } from 'vs/platform/editor/common/editor'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { IEditorInput, IRevertOptions, ISaveOptions } from 'vs/workbench/common/editor'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; -import { ThemeColor } from 'vs/platform/theme/common/themeService'; +import { ThemeColor, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { IWorkingCopyBackupMeta } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; import { NotebookSelector } from 'vs/workbench/contrib/notebook/common/notebookSelector'; import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; @@ -815,7 +815,11 @@ export interface INotebookDiffResult { export interface INotebookCellStatusBarItem { readonly alignment: CellStatusbarAlignment; readonly priority?: number; - readonly text: string; + readonly text?: string; + readonly icon?: ThemeIcon; + readonly color?: string | ThemeColor; + readonly iconColor?: string | ThemeColor; + readonly backgroundColor?: string | ThemeColor; readonly tooltip?: string; readonly command?: string | Command; readonly accessibilityInformation?: IAccessibilityInformation;