diff --git a/packages/notebook/src/browser/notebook-editor-widget.tsx b/packages/notebook/src/browser/notebook-editor-widget.tsx index be12463809da9..99b4380378a64 100644 --- a/packages/notebook/src/browser/notebook-editor-widget.tsx +++ b/packages/notebook/src/browser/notebook-editor-widget.tsx @@ -30,6 +30,7 @@ import { NotebookEditorWidgetService } from './service/notebook-editor-widget-se import { NotebookMainToolbarRenderer } from './view/notebook-main-toolbar'; import { Deferred } from '@theia/core/lib/common/promise-util'; import { MarkdownString } from '@theia/core/lib/common/markdown-rendering'; +import { NotebookContextManager } from './service/notebook-context-manager'; const PerfectScrollbar = require('react-perfect-scrollbar'); @@ -39,12 +40,16 @@ export function createNotebookEditorWidgetContainer(parent: interfaces.Container const child = parent.createChild(); child.bind(NotebookEditorProps).toConstantValue(props); + + child.bind(NotebookContextManager).toSelf().inSingletonScope(); + child.bind(NotebookMainToolbarRenderer).toSelf().inSingletonScope(); + child.bind(NotebookEditorWidget).toSelf(); return child; } -const NotebookEditorProps = Symbol('NotebookEditorProps'); +export const NotebookEditorProps = Symbol('NotebookEditorProps'); interface RenderMessage { rendererId: string; @@ -79,6 +84,9 @@ export class NotebookEditorWidget extends ReactWidget implements Navigatable, Sa @inject(NotebookMainToolbarRenderer) protected notebookMainToolbarRenderer: NotebookMainToolbarRenderer; + @inject(NotebookContextManager) + protected notebookContextManager: NotebookContextManager; + @inject(NotebookCodeCellRenderer) protected codeCellRenderer: NotebookCodeCellRenderer; @inject(NotebookMarkdownCellRenderer) @@ -159,6 +167,7 @@ export class NotebookEditorWidget extends ReactWidget implements Navigatable, Sa // Ensure that the model is loaded before adding the editor this.notebookEditorService.addNotebookEditor(this); this.update(); + this.notebookContextManager.init(this); return this._model; } @@ -186,7 +195,7 @@ export class NotebookEditorWidget extends ReactWidget implements Navigatable, Sa protected render(): ReactNode { if (this._model) { return
- {this.notebookMainToolbarRenderer.render(this._model)} + {this.notebookMainToolbarRenderer.render(this._model, this.node)} { bind(NotebookColorContribution).toSelf().inSingletonScope(); @@ -81,7 +80,6 @@ export default new ContainerModule(bind => { bind(NotebookCodeCellRenderer).toSelf().inSingletonScope(); bind(NotebookMarkdownCellRenderer).toSelf().inSingletonScope(); - bind(NotebookMainToolbarRenderer).toSelf().inSingletonScope(); bind(NotebookEditorWidgetContainerFactory).toFactory(ctx => (props: NotebookEditorProps) => createNotebookEditorWidgetContainer(ctx.container, props).get(NotebookEditorWidget) diff --git a/packages/notebook/src/browser/service/notebook-context-manager.ts b/packages/notebook/src/browser/service/notebook-context-manager.ts new file mode 100644 index 0000000000000..d5b33c3bcac9a --- /dev/null +++ b/packages/notebook/src/browser/service/notebook-context-manager.ts @@ -0,0 +1,63 @@ +// ***************************************************************************** +// 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 +// ***************************************************************************** + +import { inject, injectable } from '@theia/core/shared/inversify'; +import { ContextKeyChangeEvent, ContextKeyService } from '@theia/core/lib/browser/context-key-service'; +import { DisposableCollection, Emitter } from '@theia/core'; +import { NotebookKernelService } from './notebook-kernel-service'; +import { NOTEBOOK_KERNEL, NOTEBOOK_KERNEL_SELECTED, NOTEBOOK_VIEW_TYPE } from '../contributions/notebook-context-keys'; +import { NotebookEditorWidget } from '../notebook-editor-widget'; + +@injectable() +export class NotebookContextManager { + @inject(ContextKeyService) protected contextKeyService: ContextKeyService; + + @inject(NotebookKernelService) + protected readonly notebookKernelService: NotebookKernelService; + + protected readonly toDispose = new DisposableCollection(); + + protected readonly onDidChangeContextEmitter = new Emitter(); + readonly onDidChangeContext = this.onDidChangeContextEmitter.event; + + init(widget: NotebookEditorWidget): void { + const scopedStore = this.contextKeyService.createScoped(widget.node); + + this.toDispose.dispose(); + + scopedStore.setContext(NOTEBOOK_VIEW_TYPE, widget?.notebookType); + + const kernel = widget?.model ? this.notebookKernelService.getSelectedNotebookKernel(widget.model) : undefined; + scopedStore.setContext(NOTEBOOK_KERNEL_SELECTED, !!kernel); + scopedStore.setContext(NOTEBOOK_KERNEL, kernel?.id); + this.toDispose.push(this.notebookKernelService.onDidChangeSelectedKernel(e => { + if (e.notebook.toString() === widget?.getResourceUri()?.toString()) { + scopedStore.setContext(NOTEBOOK_KERNEL_SELECTED, !!e.newKernel); + scopedStore.setContext(NOTEBOOK_KERNEL, e.newKernel); + this.onDidChangeContextEmitter.fire(this.createContextKeyChangedEvent([NOTEBOOK_KERNEL_SELECTED, NOTEBOOK_KERNEL])); + } + })); + this.onDidChangeContextEmitter.fire(this.createContextKeyChangedEvent([NOTEBOOK_VIEW_TYPE, NOTEBOOK_KERNEL_SELECTED, NOTEBOOK_KERNEL])); + } + + createContextKeyChangedEvent(affectedKeys: string[]): ContextKeyChangeEvent { + return { affects: keys => affectedKeys.some(key => keys.has(key)) }; + } + + dispose(): void { + this.toDispose.dispose(); + } +} diff --git a/packages/notebook/src/browser/service/notebook-kernel-service.ts b/packages/notebook/src/browser/service/notebook-kernel-service.ts index 111365b8c973d..9f5e0b81f3a38 100644 --- a/packages/notebook/src/browser/service/notebook-kernel-service.ts +++ b/packages/notebook/src/browser/service/notebook-kernel-service.ts @@ -24,6 +24,7 @@ import { StorageService } from '@theia/core/lib/browser'; import { NotebookKernelSourceAction } from '../../common'; import { NotebookModel } from '../view-model/notebook-model'; import { NotebookService } from './notebook-service'; +import { NotebookEditorWidgetService } from './notebook-editor-widget-service'; export interface SelectedNotebookKernelChangeEvent { notebook: URI; @@ -157,6 +158,9 @@ export class NotebookKernelService { @inject(StorageService) protected storageService: StorageService; + @inject(NotebookEditorWidgetService) + protected notebookEditorService: NotebookEditorWidgetService; + protected readonly kernels = new Map(); protected notebookBindings: Record = {}; @@ -239,14 +243,18 @@ export class NotebookKernelService { const all = kernels.map(obj => obj.kernel); // bound kernel - const selectedId = this.notebookBindings[`${notebook.viewType}/${notebook.uri}`]; - const selected = selectedId ? this.kernels.get(selectedId)?.kernel : undefined; + const selected = this.getSelectedNotebookKernel(notebook); const suggestions = kernels.filter(item => item.instanceAffinity > 1).map(item => item.kernel); // TODO implement notebookAffinity const hidden = kernels.filter(item => item.instanceAffinity < 0).map(item => item.kernel); return { all, selected, suggestions, hidden }; } + getSelectedNotebookKernel(notebook: NotebookTextModelLike): NotebookKernel | undefined { + const selectedId = this.notebookBindings[`${notebook.viewType}/${notebook.uri}`]; + return selectedId ? this.kernels.get(selectedId)?.kernel : undefined; + } + selectKernelForNotebook(kernel: NotebookKernel | undefined, notebook: NotebookTextModelLike): void { const key = `${notebook.viewType}/${notebook.uri}`; const oldKernel = this.notebookBindings[key]; diff --git a/packages/notebook/src/browser/view/notebook-main-toolbar.tsx b/packages/notebook/src/browser/view/notebook-main-toolbar.tsx index 4755e402bde7e..12dff11891552 100644 --- a/packages/notebook/src/browser/view/notebook-main-toolbar.tsx +++ b/packages/notebook/src/browser/view/notebook-main-toolbar.tsx @@ -22,6 +22,7 @@ import { NotebookKernelService } from '../service/notebook-kernel-service'; import { inject, injectable } from '@theia/core/shared/inversify'; import { ContextKeyService } from '@theia/core/lib/browser/context-key-service'; import { NotebookCommand } from '../../common'; +import { NotebookContextManager } from '../service/notebook-context-manager'; export interface NotebookMainToolbarProps { notebookModel: NotebookModel @@ -29,6 +30,8 @@ export interface NotebookMainToolbarProps { notebookKernelService: NotebookKernelService; commandRegistry: CommandRegistry; contextKeyService: ContextKeyService; + editorNode: HTMLElement; + notebookContextManager: NotebookContextManager; } @injectable() @@ -37,14 +40,16 @@ export class NotebookMainToolbarRenderer { @inject(CommandRegistry) protected readonly commandRegistry: CommandRegistry; @inject(MenuModelRegistry) protected readonly menuRegistry: MenuModelRegistry; @inject(ContextKeyService) protected readonly contextKeyService: ContextKeyService; + @inject(NotebookContextManager) protected readonly notebookContextManager: NotebookContextManager; - render(notebookModel: NotebookModel): React.ReactNode { + render(notebookModel: NotebookModel, editorNode: HTMLElement): React.ReactNode { return ; + editorNode={editorNode} + notebookContextManager={this.notebookContextManager} />; } } @@ -70,12 +75,18 @@ export class NotebookMainToolbar extends React.Component(); - this.getMenuItems().filter(item => item.when).forEach(item => props.contextKeyService.parseKeys(item.when!)?.forEach(key => contextKeys.add(key))); + this.getAllContextKeys(this.getMenuItems(), contextKeys); + props.notebookContextManager.onDidChangeContext(e => { + if (e.affects(contextKeys)) { + this.forceUpdate(); + } + }); props.contextKeyService.onDidChange(e => { if (e.affects(contextKeys)) { this.forceUpdate(); } }); + } override componentWillUnmount(): void { @@ -103,7 +114,7 @@ export class NotebookMainToolbar extends React.Component 0 && } ; - } else if (!item.when || this.props.contextKeyService.match(item.when)) { + } else if (!item.when || this.props.contextKeyService.match(item.when, this.props.editorNode)) { const visibleCommand = Boolean(this.props.commandRegistry.getVisibleHandler(item.command ?? '', this.props.notebookModel)); if (!visibleCommand) { return undefined; @@ -125,6 +136,16 @@ export class NotebookMainToolbar extends React.Component): void { + menus.filter(item => item.when) + .forEach(item => this.props.contextKeyService.parseKeys(item.when!)?.forEach(key => keySet.add(key))); + + menus.filter(item => item.children && item.children.length > 0) + .forEach(item => this.getAllContextKeys(item.children!, keySet)); } } diff --git a/packages/plugin-ext/src/main/browser/main-context.ts b/packages/plugin-ext/src/main/browser/main-context.ts index 2755e258d5053..95cef28663854 100644 --- a/packages/plugin-ext/src/main/browser/main-context.ts +++ b/packages/plugin-ext/src/main/browser/main-context.ts @@ -59,10 +59,8 @@ import { UntitledResourceResolver } from '@theia/core/lib/common/resource'; import { ThemeService } from '@theia/core/lib/browser/theming'; import { TabsMainImpl } from './tabs/tabs-main'; import { NotebooksMainImpl } from './notebooks/notebooks-main'; -import { NotebookService } from '@theia/notebook/lib/browser'; import { LocalizationMainImpl } from './localization-main'; import { NotebookRenderersMainImpl } from './notebooks/notebook-renderers-main'; -import { HostedPluginSupport } from '../../hosted/browser/hosted-plugin'; import { NotebookEditorsMainImpl } from './notebooks/notebook-editors-main'; import { NotebookDocumentsMainImpl } from './notebooks/notebook-documents-main'; import { NotebookKernelsMainImpl } from './notebooks/notebook-kernels-main'; @@ -102,9 +100,7 @@ export function setUpPluginApi(rpc: RPCProtocol, container: interfaces.Container const documentsMain = new DocumentsMainImpl(editorsAndDocuments, modelService, rpc, editorManager, openerService, shell, untitledResourceResolver, languageService); rpc.set(PLUGIN_RPC_CONTEXT.DOCUMENTS_MAIN, documentsMain); - const notebookService = container.get(NotebookService); - const pluginSupport = container.get(HostedPluginSupport); - rpc.set(PLUGIN_RPC_CONTEXT.NOTEBOOKS_MAIN, new NotebooksMainImpl(rpc, notebookService, pluginSupport)); + rpc.set(PLUGIN_RPC_CONTEXT.NOTEBOOKS_MAIN, new NotebooksMainImpl(rpc, container, commandRegistryMain)); rpc.set(PLUGIN_RPC_CONTEXT.NOTEBOOK_RENDERERS_MAIN, new NotebookRenderersMainImpl(rpc, container)); const notebookEditorsMain = new NotebookEditorsMainImpl(rpc, container); rpc.set(PLUGIN_RPC_CONTEXT.NOTEBOOK_EDITORS_MAIN, notebookEditorsMain); diff --git a/packages/plugin-ext/src/main/browser/notebooks/notebooks-main.ts b/packages/plugin-ext/src/main/browser/notebooks/notebooks-main.ts index 9764b6242ccde..7263af2d788a6 100644 --- a/packages/plugin-ext/src/main/browser/notebooks/notebooks-main.ts +++ b/packages/plugin-ext/src/main/browser/notebooks/notebooks-main.ts @@ -19,11 +19,13 @@ import { BinaryBuffer } from '@theia/core/lib/common/buffer'; import { NotebookCellStatusBarItem, NotebookData, TransientOptions } from '@theia/notebook/lib/common'; import { NotebookService } from '@theia/notebook/lib/browser'; import { Disposable } from '@theia/plugin'; -import { MAIN_RPC_CONTEXT, NotebooksExt, NotebooksMain } from '../../../common'; +import { CommandRegistryMain, MAIN_RPC_CONTEXT, NotebooksExt, NotebooksMain } from '../../../common'; import { RPCProtocol } from '../../../common/rpc-protocol'; import { NotebookDto } from './notebook-dto'; import { UriComponents } from '@theia/core/lib/common/uri'; import { HostedPluginSupport } from '../../../hosted/browser/hosted-plugin'; +import { NotebookModel } from '@theia/notebook/lib/browser/view-model/notebook-model'; +import { interfaces } from '@theia/core/shared/inversify'; export interface NotebookCellStatusBarItemList { items: NotebookCellStatusBarItem[]; @@ -38,20 +40,33 @@ export interface NotebookCellStatusBarItemProvider { export class NotebooksMainImpl implements NotebooksMain { - private readonly disposables = new DisposableCollection(); + protected readonly disposables = new DisposableCollection(); - private readonly proxy: NotebooksExt; - private readonly notebookSerializer = new Map(); - private readonly notebookCellStatusBarRegistrations = new Map(); + protected notebookService: NotebookService; + + protected readonly proxy: NotebooksExt; + protected readonly notebookSerializer = new Map(); + protected readonly notebookCellStatusBarRegistrations = new Map(); constructor( rpc: RPCProtocol, - private notebookService: NotebookService, - plugins: HostedPluginSupport + container: interfaces.Container, + commands: CommandRegistryMain ) { + this.notebookService = container.get(NotebookService); + const plugins = container.get(HostedPluginSupport); + this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.NOTEBOOKS_EXT); - notebookService.onWillUseNotebookSerializer(async event => plugins.activateByNotebookSerializer(event)); - notebookService.markReady(); + this.notebookService.onWillUseNotebookSerializer(async event => plugins.activateByNotebookSerializer(event)); + this.notebookService.markReady(); + commands.registerArgumentProcessor({ + processArgument: arg => { + if (arg instanceof NotebookModel) { + return arg.uri; + } + return arg; + } + }); } dispose(): void { diff --git a/packages/workspace/src/browser/workspace-trust-service.ts b/packages/workspace/src/browser/workspace-trust-service.ts index 6572da09024ee..c7ed506edb7b9 100644 --- a/packages/workspace/src/browser/workspace-trust-service.ts +++ b/packages/workspace/src/browser/workspace-trust-service.ts @@ -26,6 +26,7 @@ import { } from './workspace-trust-preferences'; import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider'; import { WorkspaceService } from './workspace-service'; +import { ContextKeyService } from '@theia/core/lib/browser/context-key-service'; const STORAGE_TRUSTED = 'trusted'; @@ -49,6 +50,9 @@ export class WorkspaceTrustService { @inject(WindowService) protected readonly windowService: WindowService; + @inject(ContextKeyService) + protected readonly contextKeyService: ContextKeyService; + protected workspaceTrust = new Deferred(); @postConstruct() @@ -71,6 +75,7 @@ export class WorkspaceTrustService { const trust = givenTrust ?? await this.calculateWorkspaceTrust(); if (trust !== undefined) { await this.storeWorkspaceTrust(trust); + this.contextKeyService.setContext('isWorkspaceTrusted', trust); this.workspaceTrust.resolve(trust); } }