diff --git a/packages/notebook/src/browser/notebook-frontend-module.ts b/packages/notebook/src/browser/notebook-frontend-module.ts index b5158315668b2..5671f2dc3355f 100644 --- a/packages/notebook/src/browser/notebook-frontend-module.ts +++ b/packages/notebook/src/browser/notebook-frontend-module.ts @@ -51,6 +51,7 @@ import { NotebookOptionsService } from './service/notebook-options'; import { NotebookUndoRedoHandler } from './contributions/notebook-undo-redo-handler'; import { NotebookStatusBarContribution } from './contributions/notebook-status-bar-contribution'; import { NotebookCellEditorService } from './service/notebook-cell-editor-service'; +import { NotebookCellStatusBarService } from './service/notebook-cell-status-bar-service'; export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(NotebookColorContribution).toSelf().inSingletonScope(); @@ -74,6 +75,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(NotebookKernelQuickPickService).toSelf().inSingletonScope(); bind(NotebookClipboardService).toSelf().inSingletonScope(); bind(NotebookCellEditorService).toSelf().inSingletonScope(); + bind(NotebookCellStatusBarService).toSelf().inSingletonScope(); bind(NotebookCellResourceResolver).toSelf().inSingletonScope(); bind(ResourceResolver).toService(NotebookCellResourceResolver); diff --git a/packages/notebook/src/browser/service/notebook-cell-status-bar-service.ts b/packages/notebook/src/browser/service/notebook-cell-status-bar-service.ts new file mode 100644 index 0000000000000..92fedf6014325 --- /dev/null +++ b/packages/notebook/src/browser/service/notebook-cell-status-bar-service.ts @@ -0,0 +1,94 @@ +// ***************************************************************************** +// Copyright (C) 2024 TypeFox and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken, Command, Disposable, Emitter, Event, URI } from '@theia/core'; +import { CellStatusbarAlignment } from '../../common'; +import { ThemeColor } from '@theia/core/lib/common/theme'; +import { AccessibilityInformation } from '@theia/core/lib/common/accessibility'; +import { injectable } from '@theia/core/shared/inversify'; +import { MarkdownString } from '@theia/core/lib/common/markdown-rendering'; + +export interface NotebookCellStatusBarItem { + readonly alignment: CellStatusbarAlignment; + readonly priority?: number; + readonly text: string; + readonly color?: string | ThemeColor; + readonly backgroundColor?: string | ThemeColor; + readonly tooltip?: string | MarkdownString; + readonly command?: string | (Command & { arguments?: unknown[] }); + readonly accessibilityInformation?: AccessibilityInformation; + readonly opacity?: string; + readonly onlyShowWhenActive?: boolean; +} +export interface NotebookCellStatusBarItemList { + items: NotebookCellStatusBarItem[]; + dispose?(): void; +} + +export interface NotebookCellStatusBarItemProvider { + viewType: string; + onDidChangeStatusBarItems?: Event; + provideCellStatusBarItems(uri: URI, index: number, token: CancellationToken): Promise; +} + +@injectable() +export class NotebookCellStatusBarService implements Disposable { + + protected readonly onDidChangeProvidersEmitter = new Emitter(); + readonly onDidChangeProviders: Event = this.onDidChangeProvidersEmitter.event; + + protected readonly onDidChangeItemsEmitter = new Emitter(); + readonly onDidChangeItems: Event = this.onDidChangeItemsEmitter.event; + + protected readonly providers: NotebookCellStatusBarItemProvider[] = []; + + registerCellStatusBarItemProvider(provider: NotebookCellStatusBarItemProvider): Disposable { + this.providers.push(provider); + let changeListener: Disposable | undefined; + if (provider.onDidChangeStatusBarItems) { + changeListener = provider.onDidChangeStatusBarItems(() => this.onDidChangeItemsEmitter.fire()); + } + + this.onDidChangeProvidersEmitter.fire(); + + return Disposable.create(() => { + changeListener?.dispose(); + const idx = this.providers.findIndex(p => p === provider); + this.providers.splice(idx, 1); + }); + } + + async getStatusBarItemsForCell(notebookUri: URI, cellIndex: number, viewType: string, token: CancellationToken): Promise { + const providers = this.providers.filter(p => p.viewType === viewType || p.viewType === '*'); + return Promise.all(providers.map(async p => { + try { + return await p.provideCellStatusBarItems(notebookUri, cellIndex, token) ?? { items: [] }; + } catch (e) { + console.error(e); + return { items: [] }; + } + })); + } + + dispose(): void { + this.onDidChangeItemsEmitter.dispose(); + this.onDidChangeProvidersEmitter.dispose(); + } +} diff --git a/packages/notebook/src/browser/style/index.css b/packages/notebook/src/browser/style/index.css index e557a7983e7d8..0ad64eb41136f 100644 --- a/packages/notebook/src/browser/style/index.css +++ b/packages/notebook/src/browser/style/index.css @@ -508,3 +508,22 @@ mark.theia-find-match.theia-find-match-selected { color: var(--theia-editor-findMatchForeground); background-color: var(--theia-editor-findMatchBackground); } + +.cell-status-bar-item { + align-items: center; + display: flex; + height: 16px; + margin: 0 3px; + overflow: hidden; + padding: 0 3px; + text-overflow: clip; + white-space: pre; +} + +.cell-status-item-has-command { + cursor: pointer; +} + +.cell-status-item-has-command:hover { + background-color: var(--theia-toolbar-hoverBackground); +} diff --git a/packages/notebook/src/browser/view/notebook-code-cell-view.tsx b/packages/notebook/src/browser/view/notebook-code-cell-view.tsx index 537776e03adaf..17c4902a1f507 100644 --- a/packages/notebook/src/browser/view/notebook-code-cell-view.tsx +++ b/packages/notebook/src/browser/view/notebook-code-cell-view.tsx @@ -27,7 +27,7 @@ import { NotebookCellActionContribution, NotebookCellCommands } from '../contrib import { CellExecution, NotebookExecutionStateService } from '../service/notebook-execution-state-service'; import { codicon } from '@theia/core/lib/browser'; import { NotebookCellExecutionState } from '../../common'; -import { CommandRegistry, DisposableCollection, nls } from '@theia/core'; +import { CancellationToken, CommandRegistry, DisposableCollection, nls } from '@theia/core'; import { NotebookContextManager } from '../service/notebook-context-manager'; import { NotebookViewportService } from './notebook-viewport-service'; import { EditorPreferences } from '@theia/editor/lib/browser'; @@ -36,6 +36,8 @@ import { MarkdownRenderer } from '@theia/core/lib/browser/markdown-rendering/mar import { MarkdownString } from '@theia/monaco-editor-core/esm/vs/base/common/htmlContent'; import { NotebookCellEditorService } from '../service/notebook-cell-editor-service'; import { CellOutputWebview } from '../renderers/cell-output-webview'; +import { NotebookCellStatusBarItem, NotebookCellStatusBarItemList, NotebookCellStatusBarService } from '../service/notebook-cell-status-bar-service'; +import { LabelParser } from '@theia/core/lib/browser/label-parser'; @injectable() export class NotebookCodeCellRenderer implements CellRenderer { @@ -75,6 +77,12 @@ export class NotebookCodeCellRenderer implements CellRenderer { @inject(CellOutputWebview) protected readonly outputWebview: CellOutputWebview; + @inject(NotebookCellStatusBarService) + protected readonly notebookCellStatusBarService: NotebookCellStatusBarService; + + @inject(LabelParser) + protected readonly labelParser: LabelParser; + render(notebookModel: NotebookModel, cell: NotebookCellModel, handle: number): React.ReactNode { return
observeCellHeight(ref, cell)}>
@@ -87,6 +95,8 @@ export class NotebookCodeCellRenderer implements CellRenderer { cell.requestFocusEditor()} />
; @@ -182,7 +192,9 @@ export interface NotebookCodeCellStatusProps { notebook: NotebookModel; cell: NotebookCellModel; commandRegistry: CommandRegistry; + cellStatusBarService: NotebookCellStatusBarService; executionStateService?: NotebookExecutionStateService; + labelParser: LabelParser; onClick: () => void; } @@ -195,6 +207,8 @@ export class NotebookCodeCellStatus extends React.Component { this.forceUpdate(); })); + + this.updateStatusBarItems(); + this.props.cellStatusBarService.onDidChangeItems(() => this.updateStatusBarItems()); + this.props.notebook.onContentChanged(() => this.updateStatusBarItems()); + } + + async updateStatusBarItems(): Promise { + this.statusBarItems = await this.props.cellStatusBarService.getStatusBarItemsForCell( + this.props.notebook.uri, + this.props.notebook.cells.indexOf(this.props.cell), + this.props.notebook.viewType, + CancellationToken.None); + this.forceUpdate(); } override componentWillUnmount(): void { @@ -235,6 +262,7 @@ export class NotebookCodeCellStatus extends React.Component this.props.onClick()}>
{this.props.executionStateService && this.renderExecutionState()} + {this.statusBarItems?.length && this.renderStatusBarItems()}
{ @@ -244,7 +272,7 @@ export class NotebookCodeCellStatus extends React.Component; } - private renderExecutionState(): React.ReactNode { + protected renderExecutionState(): React.ReactNode { const state = this.state.currentExecution?.state; const { lastRunSuccess } = this.props.cell.internalMetadata; @@ -270,7 +298,7 @@ export class NotebookCodeCellStatus extends React.Component; } - private getExecutionTime(): number { + protected getExecutionTime(): number { const { runStartTime, runEndTime } = this.props.cell.internalMetadata; const { executionTime } = this.state; if (runStartTime !== undefined && runEndTime !== undefined) { @@ -279,9 +307,42 @@ export class NotebookCodeCellStatus extends React.Component + { + this.statusBarItems.flatMap((itemList, listIndex) => + itemList.items.map((item, index) => this.renderStatusBarItem(item, `${listIndex}-${index}`) + ) + ) + } + ; + } + + protected renderStatusBarItem(item: NotebookCellStatusBarItem, key: string): React.ReactNode { + const content = this.props.labelParser.parse(item.text).map(part => { + if (typeof part === 'string') { + return part; + } else { + return ; + } + }); + return
{ + if (item.command) { + if (typeof item.command === 'string') { + this.props.commandRegistry.executeCommand(item.command); + } else { + this.props.commandRegistry.executeCommand(item.command.id, ...(item.command.arguments ?? [])); + } + } + }}> + {content} +
; + } + } interface NotebookCellOutputProps { diff --git a/packages/notebook/src/browser/view/notebook-markdown-cell-view.tsx b/packages/notebook/src/browser/view/notebook-markdown-cell-view.tsx index e24ff32f838b7..c0397fe0649e2 100644 --- a/packages/notebook/src/browser/view/notebook-markdown-cell-view.tsx +++ b/packages/notebook/src/browser/view/notebook-markdown-cell-view.tsx @@ -30,6 +30,8 @@ import { NotebookCodeCellStatus } from './notebook-code-cell-view'; import { NotebookEditorFindMatch, NotebookEditorFindMatchOptions } from './notebook-find-widget'; import * as mark from 'advanced-mark.js'; import { NotebookCellEditorService } from '../service/notebook-cell-editor-service'; +import { NotebookCellStatusBarService } from '../service/notebook-cell-status-bar-service'; +import { LabelParser } from '@theia/core/lib/browser/label-parser'; @injectable() export class NotebookMarkdownCellRenderer implements CellRenderer { @@ -51,6 +53,12 @@ export class NotebookMarkdownCellRenderer implements CellRenderer { @inject(NotebookCellEditorService) protected readonly notebookCellEditorService: NotebookCellEditorService; + @inject(NotebookCellStatusBarService) + protected readonly notebookCellStatusBarService: NotebookCellStatusBarService; + + @inject(LabelParser) + protected readonly labelParser: LabelParser; + render(notebookModel: NotebookModel, cell: NotebookCellModel): React.ReactNode { return ; + notebookCellEditorService={this.notebookCellEditorService} + notebookCellStatusBarService={this.notebookCellStatusBarService} + labelParser={this.labelParser} + />; } renderSidebar(notebookModel: NotebookModel, cell: NotebookCellModel): React.ReactNode { @@ -86,11 +97,15 @@ interface MarkdownCellProps { notebookModel: NotebookModel; notebookContextManager: NotebookContextManager; notebookOptionsService: NotebookOptionsService; - notebookCellEditorService: NotebookCellEditorService + notebookCellEditorService: NotebookCellEditorService; + notebookCellStatusBarService: NotebookCellStatusBarService; + labelParser: LabelParser; } function MarkdownCell({ - markdownRenderer, monacoServices, cell, notebookModel, notebookContextManager, notebookOptionsService, commandRegistry, notebookCellEditorService + markdownRenderer, monacoServices, cell, notebookModel, notebookContextManager, + notebookOptionsService, commandRegistry, notebookCellEditorService, notebookCellStatusBarService, + labelParser }: MarkdownCellProps): React.JSX.Element { const [editMode, setEditMode] = React.useState(cell.editing); let empty = false; @@ -147,6 +162,8 @@ function MarkdownCell({ fontInfo={notebookOptionsService.editorFontInfo} /> cell.requestFocusEditor()} />
) : (
; - provideCellStatusBarItems(uri: UriComponents, index: number, token: CancellationToken): Promise; -} +import { + NotebookCellStatusBarItemProvider, + NotebookCellStatusBarItemList, + NotebookCellStatusBarService +} from '@theia/notebook/lib/browser/service/notebook-cell-status-bar-service'; export class NotebooksMainImpl implements NotebooksMain { protected readonly disposables = new DisposableCollection(); protected notebookService: NotebookService; + protected cellStatusBarService: NotebookCellStatusBarService; protected readonly proxy: NotebooksExt; protected readonly notebookSerializer = new Map(); @@ -55,6 +49,7 @@ export class NotebooksMainImpl implements NotebooksMain { commands: CommandRegistryMain ) { this.notebookService = container.get(NotebookService); + this.cellStatusBarService = container.get(NotebookCellStatusBarService); const plugins = container.get(HostedPluginSupport); this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.NOTEBOOKS_EXT); @@ -111,8 +106,8 @@ export class NotebooksMainImpl implements NotebooksMain { async $registerNotebookCellStatusBarItemProvider(handle: number, eventHandle: number | undefined, viewType: string): Promise { const that = this; const provider: NotebookCellStatusBarItemProvider = { - async provideCellStatusBarItems(uri: UriComponents, index: number, token: CancellationToken): Promise { - const result = await that.proxy.$provideNotebookCellStatusBarItems(handle, uri, index, token); + async provideCellStatusBarItems(notebookUri: URI, index: number, token: CancellationToken): Promise { + const result = await that.proxy.$provideNotebookCellStatusBarItems(handle, notebookUri.toComponents(), index, token); return { items: result?.items ?? [], dispose(): void { @@ -131,8 +126,8 @@ export class NotebooksMainImpl implements NotebooksMain { provider.onDidChangeStatusBarItems = emitter.event; } - // const disposable = this._cellStatusBarService.registerCellStatusBarItemProvider(provider); - // this.notebookCellStatusBarRegistrations.set(handle, disposable); + const disposable = this.cellStatusBarService.registerCellStatusBarItemProvider(provider); + this.notebookCellStatusBarRegistrations.set(handle, disposable); } async $unregisterNotebookCellStatusBarItemProvider(handle: number, eventHandle: number | undefined): Promise { diff --git a/packages/plugin-ext/src/plugin/notebook/notebooks.ts b/packages/plugin-ext/src/plugin/notebook/notebooks.ts index d31d9554ca2ac..6101c2fc97c8a 100644 --- a/packages/plugin-ext/src/plugin/notebook/notebooks.ts +++ b/packages/plugin-ext/src/plugin/notebook/notebooks.ts @@ -22,14 +22,14 @@ import { CancellationToken, Disposable, DisposableCollection, Emitter, Event, UR import { URI as TheiaURI } from '../types-impl'; import * as theia from '@theia/plugin'; import { - CommandRegistryExt, NotebookCellStatusBarListDto, NotebookDataDto, + NotebookCellStatusBarListDto, NotebookDataDto, NotebookDocumentsAndEditorsDelta, NotebookDocumentShowOptions, NotebookDocumentsMain, NotebookEditorAddData, NotebookEditorsMain, NotebooksExt, NotebooksMain, Plugin, PLUGIN_RPC_CONTEXT } from '../../common'; import { Cache } from '../../common/cache'; import { RPCProtocol } from '../../common/rpc-protocol'; import { UriComponents } from '../../common/uri-components'; -import { CommandsConverter } from '../command-registry'; +import { CommandRegistryImpl, CommandsConverter } from '../command-registry'; import * as typeConverters from '../type-converters'; import { BinaryBuffer } from '@theia/core/lib/common/buffer'; import { Cell, NotebookDocument } from './notebook-document'; @@ -74,10 +74,11 @@ export class NotebooksExtImpl implements NotebooksExt { constructor( rpc: RPCProtocol, - commands: CommandRegistryExt, + commands: CommandRegistryImpl, private textDocumentsAndEditors: EditorsAndDocumentsExtImpl, private textDocuments: DocumentsExtImpl, ) { + this.commandsConverter = commands.converter; this.notebookProxy = rpc.getProxy(PLUGIN_RPC_CONTEXT.NOTEBOOKS_MAIN); this.notebookDocumentsProxy = rpc.getProxy(PLUGIN_RPC_CONTEXT.NOTEBOOK_DOCUMENTS_MAIN); this.notebookEditors = rpc.getProxy(PLUGIN_RPC_CONTEXT.NOTEBOOK_EDITORS_MAIN);