diff --git a/packages/core/src/common/array-utils.ts b/packages/core/src/common/array-utils.ts index c8781cef5ad3c..0d50e35058db0 100644 --- a/packages/core/src/common/array-utils.ts +++ b/packages/core/src/common/array-utils.ts @@ -106,4 +106,24 @@ export namespace ArrayUtils { export function coalesce(array: ReadonlyArray): T[] { return array.filter(e => !!e); } + + /** + * groups array elements through a comparator function + * @param data array of elements to group + * @param compare comparator function: return of 0 means should group, anything above means not group + * @returns array of arrays with grouped elements + */ + export function groupBy(data: ReadonlyArray, compare: (a: T, b: T) => number): T[][] { + const result: T[][] = []; + let currentGroup: T[] | undefined = undefined; + for (const element of data.slice(0).sort(compare)) { + if (!currentGroup || compare(currentGroup[0], element) !== 0) { + currentGroup = [element]; + result.push(currentGroup); + } else { + currentGroup.push(element); + } + } + return result; + } } diff --git a/packages/core/src/common/event.ts b/packages/core/src/common/event.ts index 80b57dba57a76..d97e79977a256 100644 --- a/packages/core/src/common/event.ts +++ b/packages/core/src/common/event.ts @@ -16,7 +16,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { Disposable, DisposableGroup } from './disposable'; +import { Disposable, DisposableGroup, DisposableCollection } from './disposable'; import { MaybePromise } from './types'; /** @@ -67,6 +67,16 @@ export namespace Event { set maxListeners(maxListeners: number) { } }); } + + /** + * Given a collection of events, returns a single event which emits whenever any of the provided events emit. + */ + export function any(...events: Event[]): Event; + export function any(...events: Event[]): Event; + export function any(...events: Event[]): Event { + return (listener, thisArgs = undefined, disposables?: Disposable[]) => + new DisposableCollection(...events.map(event => event(e => listener.call(thisArgs, e), undefined, disposables))); + } } type Callback = (...args: any[]) => any; @@ -276,7 +286,7 @@ export class Emitter { */ fire(event: T): any { if (this._callbacks) { - this._callbacks.invoke(event); + return this._callbacks.invoke(event); } } diff --git a/packages/core/src/common/uri.ts b/packages/core/src/common/uri.ts index fe24bc2e54339..2076519c7d6f6 100644 --- a/packages/core/src/common/uri.ts +++ b/packages/core/src/common/uri.ts @@ -27,6 +27,10 @@ export class URI { return new URI(Uri.file(path)); } + public static isUri(uri: unknown): boolean { + return Uri.isUri(uri); + } + private readonly codeUri: Uri; private _path: Path | undefined; diff --git a/packages/notebook/src/browser/contributions/notebook-actions-contribution.ts b/packages/notebook/src/browser/contributions/notebook-actions-contribution.ts index fc6216c7158ac..999596eabf751 100644 --- a/packages/notebook/src/browser/contributions/notebook-actions-contribution.ts +++ b/packages/notebook/src/browser/contributions/notebook-actions-contribution.ts @@ -20,7 +20,7 @@ import { codicon } from '@theia/core/lib/browser'; import { NotebookModel } from '../view-model/notebook-model'; import { NotebookService } from '../service/notebook-service'; import { CellKind } from '../../common'; -import { NotebookKernelQuickPickService } from '../service/notebook-kernel-quick-pick-service'; +import { KernelPickerMRUStrategy, NotebookKernelQuickPickService } from '../service/notebook-kernel-quick-pick-service'; export namespace NotebookCommands { export const ADD_NEW_CELL_COMMAND = Command.toDefaultLocalizedCommand({ @@ -42,7 +42,7 @@ export class NotebookActionsContribution implements CommandContribution { protected notebookService: NotebookService; @inject(NotebookKernelQuickPickService) - protected notebookKernelQuickPickService: NotebookKernelQuickPickService; + protected notebookKernelQuickPickService: KernelPickerMRUStrategy; registerCommands(commands: CommandRegistry): void { commands.registerCommand(NotebookCommands.ADD_NEW_CELL_COMMAND, { @@ -53,9 +53,7 @@ export class NotebookActionsContribution implements CommandContribution { }); commands.registerCommand(NotebookCommands.SELECT_KERNEL_COMMAND, { - execute: (notebookModel: NotebookModel) => { - this.notebookKernelQuickPickService.showQuickPick(notebookModel); - } + execute: (notebookModel: NotebookModel) => this.notebookKernelQuickPickService.showQuickPick(notebookModel) }); } diff --git a/packages/notebook/src/browser/index.ts b/packages/notebook/src/browser/index.ts index cf1a85722f5a6..cfe130289cf0c 100644 --- a/packages/notebook/src/browser/index.ts +++ b/packages/notebook/src/browser/index.ts @@ -17,6 +17,7 @@ export * from './notebook-type-registry'; export * from './notebook-editor-widget'; export * from './service/notebook-service'; +export * from './service/notebook-editor-service'; export * from './service/notebook-kernel-service'; export * from './service/notebook-execution-state-service'; export * from './service/notebook-model-resolver-service'; diff --git a/packages/notebook/src/browser/notebook-cell-resource-resolver.ts b/packages/notebook/src/browser/notebook-cell-resource-resolver.ts index 132aa428dd2f3..cb74368552f61 100644 --- a/packages/notebook/src/browser/notebook-cell-resource-resolver.ts +++ b/packages/notebook/src/browser/notebook-cell-resource-resolver.ts @@ -40,7 +40,7 @@ export class NotebookCellResource implements Resource { } dispose(): void { - throw new Error('Method not implemented.'); + this.didChangeContentsEmitter.dispose(); } } diff --git a/packages/notebook/src/browser/notebook-editor-widget.tsx b/packages/notebook/src/browser/notebook-editor-widget.tsx index 991f4a9203500..2db387708292f 100644 --- a/packages/notebook/src/browser/notebook-editor-widget.tsx +++ b/packages/notebook/src/browser/notebook-editor-widget.tsx @@ -16,7 +16,7 @@ import * as React from '@theia/core/shared/react'; import { CommandRegistry, URI } from '@theia/core'; -import { ReactWidget, Navigatable, SaveableSource, Saveable } from '@theia/core/lib/browser'; +import { ReactWidget, Navigatable, SaveableSource, Saveable, Message } from '@theia/core/lib/browser'; import { ReactNode } from '@theia/core/shared/react'; import { CellKind } from '../common'; import { Cellrenderer as CellRenderer, NotebookCellListView } from './view/notebook-cell-list-view'; @@ -25,6 +25,8 @@ import { NotebookMarkdownCellRenderer } from './view/notebook-markdown-cell-view import { NotebookModel } from './view-model/notebook-model'; import { NotebookCellToolbarFactory } from './view/notebook-cell-toolbar-factory'; import { inject, injectable, interfaces } from '@theia/core/shared/inversify'; +import { Emitter } from '@theia/core/shared/vscode-languageserver-protocol'; +import { NotebookEditorWidgetService } from './service/notebook-editor-service'; export const NotebookEditorContainerFactory = Symbol('NotebookModelFactory'); @@ -44,7 +46,7 @@ export interface NotebookEditorProps { readonly notebookType: string, notebookData: NotebookModel } - +export const NOTEBOOK_EDITOR_ID_PREFIX = 'notebook:'; @injectable() export class NotebookEditorWidget extends ReactWidget implements Navigatable, SaveableSource { static readonly ID = 'notebook'; @@ -57,19 +59,29 @@ export class NotebookEditorWidget extends ReactWidget implements Navigatable, Sa @inject(CommandRegistry) protected commandRegistry: CommandRegistry; + @inject(NotebookEditorWidgetService) + protected notebookEditorService: NotebookEditorWidgetService; + + private readonly onDidChangeModelEmitter = new Emitter(); + readonly onDidChangeModel = this.onDidChangeModelEmitter.event; + private readonly renderers = new Map(); get notebookType(): string { return this.props.notebookType; } + get model(): NotebookModel { + return this.props.notebookData; + } + constructor( @inject(NotebookCodeCellRenderer) codeCellRenderer: NotebookCodeCellRenderer, @inject(NotebookMarkdownCellRenderer) markdownCellRenderer: NotebookMarkdownCellRenderer, @inject(NotebookEditorProps) private readonly props: NotebookEditorProps) { super(); this.saveable = this.props.notebookData; - this.id = 'notebook:' + this.props.uri.toString(); + this.id = NOTEBOOK_EDITOR_ID_PREFIX + this.props.uri.toString(); this.title.closable = true; this.update(); @@ -93,4 +105,14 @@ export class NotebookEditorWidget extends ReactWidget implements Navigatable, Sa commandRegistry={this.commandRegistry}/> ; } + + protected override onAfterAttach(msg: Message): void { + super.onAfterAttach(msg); + this.notebookEditorService.addNotebookEditor(this); + } + + protected override onAfterDetach(msg: Message): void { + super.onAfterDetach(msg); + this.notebookEditorService.removeNotebookEditor(this); + } } diff --git a/packages/notebook/src/browser/notebook-frontend-module.ts b/packages/notebook/src/browser/notebook-frontend-module.ts index 4e71486aeab63..f5536bd22ecac 100644 --- a/packages/notebook/src/browser/notebook-frontend-module.ts +++ b/packages/notebook/src/browser/notebook-frontend-module.ts @@ -35,7 +35,9 @@ import { NotebookActionsContribution } from './contributions/notebook-actions-co import { NotebookExecutionService } from './service/notebook-execution-service'; import { NotebookExecutionStateService } from './service/notebook-execution-state-service'; import { NotebookKernelService } from './service/notebook-kernel-service'; -import { NotebookKernelQuickPickService } from './service/notebook-kernel-quick-pick-service'; +import { KernelPickerMRUStrategy, NotebookKernelQuickPickService } from './service/notebook-kernel-quick-pick-service'; +import { NotebookKernelHistoryService } from './service/notebookKernelHistoryService'; +import { NotebookEditorWidgetService } from './service/notebook-editor-service'; export default new ContainerModule(bind => { bindContributionProvider(bind, Symbol('notebooks')); @@ -49,10 +51,12 @@ export default new ContainerModule(bind => { bind(NotebookCellToolbarFactory).toSelf().inSingletonScope(); bind(NotebookService).toSelf().inSingletonScope(); + bind(NotebookEditorWidgetService).toSelf().inSingletonScope(); bind(NotebookExecutionService).toSelf().inSingletonScope(); bind(NotebookExecutionStateService).toSelf().inSingletonScope(); bind(NotebookKernelService).toSelf().inSingletonScope(); - bind(NotebookKernelQuickPickService).toSelf().inSingletonScope(); + bind(NotebookKernelHistoryService).toSelf().inSingletonScope(); + bind(NotebookKernelQuickPickService).to(KernelPickerMRUStrategy).inSingletonScope(); bind(NotebookCellResourceResolver).toSelf().inSingletonScope(); bind(ResourceResolver).toService(NotebookCellResourceResolver); diff --git a/packages/notebook/src/browser/service/notebook-cell-context-manager.ts b/packages/notebook/src/browser/service/notebook-cell-context-manager.ts index 8fc571d082b7f..f83b4a3a96cf6 100644 --- a/packages/notebook/src/browser/service/notebook-cell-context-manager.ts +++ b/packages/notebook/src/browser/service/notebook-cell-context-manager.ts @@ -16,10 +16,9 @@ import { inject, injectable } from '@theia/core/shared/inversify'; import { ContextKeyService, ScopedValueStore } from '@theia/core/lib/browser/context-key-service'; -import { Disposable } from '@theia/core/shared/vscode-languageserver-protocol'; import { NotebookCellModel } from '../view-model/notebook-cell-model'; import { NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_TYPE } from '../contributions/notebook-context-keys'; -import { DisposableCollection } from '@theia/core'; +import { Disposable, DisposableCollection } from '@theia/core'; import { CellKind } from '../../common'; @injectable() diff --git a/packages/notebook/src/browser/service/notebook-editor-service.ts b/packages/notebook/src/browser/service/notebook-editor-service.ts new file mode 100644 index 0000000000000..f3012f4110809 --- /dev/null +++ b/packages/notebook/src/browser/service/notebook-editor-service.ts @@ -0,0 +1,85 @@ +// ***************************************************************************** +// Copyright (C) 2023 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 { Disposable, DisposableCollection, Emitter } from '@theia/core'; +import { inject, injectable, postConstruct } from '@theia/core/shared/inversify'; +import { ApplicationShell } from '@theia/core/lib/browser'; +import { NotebookEditorWidget, NOTEBOOK_EDITOR_ID_PREFIX } from '../notebook-editor-widget'; + +@injectable() +export class NotebookEditorWidgetService implements Disposable { + + @inject(ApplicationShell) + protected applicationShell: ApplicationShell; + + private readonly notebookEditors = new Map(); + + private readonly onNotebookEditorAddEmitter = new Emitter(); + private readonly onNotebookEditorsRemoveEmitter = new Emitter(); + readonly onDidAddNotebookEditor = this.onNotebookEditorAddEmitter.event; + readonly onDidRemoveNotebookEditor = this.onNotebookEditorsRemoveEmitter.event; + + private readonly onFocusedEditorChangedEmitter = new Emitter(); + readonly onFocusedEditorChanged = this.onFocusedEditorChangedEmitter.event; + + private readonly listeners = new DisposableCollection(); + + currentfocusedEditor?: NotebookEditorWidget = undefined; + + @postConstruct() + protected init(): void { + this.listeners.push(this.applicationShell.onDidChangeActiveWidget(event => { + if (event.newValue?.id.startsWith(NOTEBOOK_EDITOR_ID_PREFIX) && event.newValue !== this.currentfocusedEditor) { + this.currentfocusedEditor = event.newValue as NotebookEditorWidget; + this.onFocusedEditorChangedEmitter.fire(this.currentfocusedEditor); + } + })); + } + + dispose(): void { + this.onNotebookEditorAddEmitter.dispose(); + this.onNotebookEditorsRemoveEmitter.dispose(); + this.listeners.dispose(); + } + + // --- editor management + + addNotebookEditor(editor: NotebookEditorWidget): void { + this.notebookEditors.set(editor.id, editor); + this.onNotebookEditorAddEmitter.fire(editor); + } + + removeNotebookEditor(editor: NotebookEditorWidget): void { + if (this.notebookEditors.has(editor.id)) { + this.notebookEditors.delete(editor.id); + this.onNotebookEditorsRemoveEmitter.fire(editor); + } + } + + getNotebookEditor(editorId: string): NotebookEditorWidget | undefined { + return this.notebookEditors.get(editorId); + } + + listNotebookEditors(): readonly NotebookEditorWidget[] { + return [...this.notebookEditors].map(e => e[1]); + } + +} diff --git a/packages/notebook/src/browser/service/notebook-execution-service.ts b/packages/notebook/src/browser/service/notebook-execution-service.ts index 500d9ff1b6b13..94539079c91e1 100644 --- a/packages/notebook/src/browser/service/notebook-execution-service.ts +++ b/packages/notebook/src/browser/service/notebook-execution-service.ts @@ -23,8 +23,11 @@ import { CellExecution, NotebookExecutionStateService } from '../service/noteboo import { CellKind, NotebookCellExecutionState } from '../../common'; import { NotebookCellModel } from '../view-model/notebook-cell-model'; import { NotebookModel } from '../view-model/notebook-model'; -import { NotebookKernelService } from './notebook-kernel-service'; -import { Disposable } from '@theia/core'; +import { NotebookKernelService, NotebookKernel } from './notebook-kernel-service'; +import { CommandService, Disposable } from '@theia/core'; +import { NotebookKernelQuickPickService, NotebookKernelQuickPickServiceImpl } from './notebook-kernel-quick-pick-service'; +import { NotebookKernelHistoryService } from './notebookKernelHistoryService'; +import { NotebookCommands } from '../contributions/notebook-actions-contribution'; export interface CellExecutionParticipant { onWillExecuteCell(executions: CellExecution[]): Promise; @@ -39,6 +42,15 @@ export class NotebookExecutionService { @inject(NotebookKernelService) protected notebookKernelService: NotebookKernelService; + @inject(NotebookKernelHistoryService) + protected notebookKernelHistoryService: NotebookKernelHistoryService; + + @inject(CommandService) + protected commandService: CommandService; + + @inject(NotebookKernelQuickPickService) + protected notebookKernelQuickPickService: NotebookKernelQuickPickServiceImpl; + private readonly cellExecutionParticipants = new Set(); async executeNotebookCells(notebook: NotebookModel, cells: Iterable): Promise { @@ -59,7 +71,7 @@ export class NotebookExecutionService { } } - const kernel = this.notebookKernelService.getSelectedOrSuggestedKernel(notebook); + const kernel = await this.resolveKernel(notebook); if (!kernel) { // clear all pending cell executions @@ -117,4 +129,16 @@ export class NotebookExecutionService { async cancelNotebookCells(notebook: NotebookModel, cells: Iterable): Promise { this.cancelNotebookCellHandles(notebook, Array.from(cells, cell => cell.handle)); } + + async resolveKernel(notebook: NotebookModel): Promise { + const alreadySelected = this.notebookKernelHistoryService.getKernels(notebook); + + if (alreadySelected.selected) { + return alreadySelected.selected; + } + + await this.commandService.executeCommand(NotebookCommands.SELECT_KERNEL_COMMAND.id, notebook); + const { selected } = this.notebookKernelHistoryService.getKernels(notebook); + return selected; + } } diff --git a/packages/notebook/src/browser/service/notebook-execution-state-service.ts b/packages/notebook/src/browser/service/notebook-execution-state-service.ts index 1fa0aff515f4a..d144b10336a44 100644 --- a/packages/notebook/src/browser/service/notebook-execution-state-service.ts +++ b/packages/notebook/src/browser/service/notebook-execution-state-service.ts @@ -18,9 +18,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { URI } from '@theia/core'; +import { Disposable, Emitter, URI } from '@theia/core'; import { inject, injectable } from '@theia/core/shared/inversify'; -import { Disposable, Emitter } from '@theia/core/shared/vscode-languageserver-protocol'; import { NotebookService } from './notebook-service'; import { CellEditType, CellExecuteOutputEdit, CellExecuteOutputItemEdit, CellExecutionUpdateType, diff --git a/packages/notebook/src/browser/service/notebook-kernel-quick-pick-service.ts b/packages/notebook/src/browser/service/notebook-kernel-quick-pick-service.ts index 9edc0b96e638f..7b036f95dc10a 100644 --- a/packages/notebook/src/browser/service/notebook-kernel-quick-pick-service.ts +++ b/packages/notebook/src/browser/service/notebook-kernel-quick-pick-service.ts @@ -19,25 +19,622 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { QuickPickService } from '@theia/core'; +import { ArrayUtils, Command, CommandService, DisposableCollection, Event, nls, QuickInputButton, QuickInputService, QuickPickInput, QuickPickItem, URI, } from '@theia/core'; import { inject, injectable } from '@theia/core/shared/inversify'; -import { NotebookKernelService, NotebookKernel } from './notebook-kernel-service'; +import { NotebookKernelService, NotebookKernel, NotebookKernelMatchResult, SourceCommand } from './notebook-kernel-service'; import { NotebookModel } from '../view-model/notebook-model'; +import { NotebookEditorWidget } from '../notebook-editor-widget'; +import { codicon, OpenerService } from '@theia/core/lib/browser'; +import { NotebookKernelHistoryService } from './notebookKernelHistoryService'; +import debounce = require('@theia/core/shared/lodash.debounce'); -@injectable() -export class NotebookKernelQuickPickService { +export const JUPYTER_EXTENSION_ID = 'ms-toolsai.jupyter'; + +export const NotebookKernelQuickPickService = Symbol('NotebookKernelQuickPickService'); + +type KernelPick = QuickPickItem & { kernel: NotebookKernel }; +function isKernelPick(item: QuickPickInput): item is KernelPick { + return 'kernel' in item; +} +type GroupedKernelsPick = QuickPickItem & { kernels: NotebookKernel[]; source: string }; +function isGroupedKernelsPick(item: QuickPickInput): item is GroupedKernelsPick { + return 'kernels' in item; +} +type SourcePick = QuickPickItem & { action: SourceCommand }; +function isSourcePick(item: QuickPickInput): item is SourcePick { + return 'action' in item; +} +type InstallExtensionPick = QuickPickItem & { extensionIds: string[] }; + +type KernelSourceQuickPickItem = QuickPickItem & { command: Command; documentation?: string }; +function isKernelSourceQuickPickItem(item: QuickPickItem): item is KernelSourceQuickPickItem { + return 'command' in item; +} + +function supportAutoRun(item: QuickPickInput): item is QuickPickItem { + return 'autoRun' in item && !!item.autoRun; +} + +type KernelQuickPickItem = (QuickPickItem & { autoRun?: boolean }) | InstallExtensionPick | KernelPick | GroupedKernelsPick | SourcePick | KernelSourceQuickPickItem; - @inject(QuickPickService) - protected quickPickService: QuickPickService; +const KERNELPICKERUPDATEDEBOUNCE = 200; + +export type KernelQuickPickContext = + { id: string; extension: string } | + { notebookEditorId: string } | + { id: string; extension: string; notebookEditorId: string } | + { ui?: boolean; notebookEditor?: NotebookEditorWidget }; + +function toKernelQuickPick(kernel: NotebookKernel, selected: NotebookKernel | undefined): KernelPick { + const res: KernelPick = { + kernel, + label: kernel.label, + description: kernel.description, + detail: kernel.detail + }; + if (kernel.id === selected?.id) { + if (!res.description) { + res.description = nls.localizeByDefault('Currently Selected'); + } else { + res.description = nls.localizeByDefault('{0} - Currently Selected', res.description); + } + } + return res; +} + +@injectable() +export abstract class NotebookKernelQuickPickServiceImpl { @inject(NotebookKernelService) - protected notebookKernelService: NotebookKernelService; + protected readonly notebookKernelService: NotebookKernelService; + @inject(QuickInputService) + protected readonly quickInputService: QuickInputService; + @inject(CommandService) + protected readonly commandService: CommandService; + + async showQuickPick(editor: NotebookModel, wantedId?: string, skipAutoRun?: boolean): Promise { + const notebook = editor; + const matchResult = this.getMatchingResult(notebook); + const { selected, all } = matchResult; + + let newKernel: NotebookKernel | undefined; + if (wantedId) { + for (const candidate of all) { + if (candidate.id === wantedId) { + newKernel = candidate; + break; + } + } + if (!newKernel) { + console.warn(`wanted kernel DOES NOT EXIST, wanted: ${wantedId}, all: ${all.map(k => k.id)}`); + return false; + } + } + + if (newKernel) { + this.selectKernel(notebook, newKernel); + return true; + } + + const quickPick = this.quickInputService.createQuickPick(); + const quickPickItems = this.getKernelPickerQuickPickItems(matchResult); + + if (quickPickItems.length === 1 && supportAutoRun(quickPickItems[0]) && !skipAutoRun) { + return this.handleQuickPick(editor, quickPickItems[0], quickPickItems as KernelQuickPickItem[]); + } + + quickPick.items = quickPickItems; + quickPick.canSelectMany = false; + quickPick.placeholder = selected + ? nls.localizeByDefault("Change kernel for '{0}'", 'current') // TODO get label for curent notebook from a label provider + : nls.localizeByDefault("Select kernel for '{0}'", 'current'); + + quickPick.busy = this.notebookKernelService.getKernelDetectionTasks(notebook).length > 0; + + const kernelDetectionTaskListener = this.notebookKernelService.onDidChangeKernelDetectionTasks(() => { + quickPick.busy = this.notebookKernelService.getKernelDetectionTasks(notebook).length > 0; + }); + + const kernelChangeEventListener = debounce( + Event.any( + this.notebookKernelService.onDidChangeSourceActions, + this.notebookKernelService.onDidAddKernel, + this.notebookKernelService.onDidRemoveKernel, + this.notebookKernelService.onDidChangeNotebookAffinity + ), + KERNELPICKERUPDATEDEBOUNCE + )(async () => { + // reset quick pick progress + quickPick.busy = false; + + const currentActiveItems = quickPick.activeItems; + const newMatchResult = this.getMatchingResult(notebook); + const newQuickPickItems = this.getKernelPickerQuickPickItems(newMatchResult); + quickPick.keepScrollPosition = true; - async showQuickPick(notebook: NotebookModel): Promise { - return (await this.quickPickService.show(this.getKernels(notebook).map(kernel => ({ id: kernel.id, label: kernel.label }))))?.id; + // recalcuate active items + const activeItems: KernelQuickPickItem[] = []; + for (const item of currentActiveItems) { + if (isKernelPick(item)) { + const kernelId = item.kernel.id; + const sameItem = newQuickPickItems.find(pi => isKernelPick(pi) && pi.kernel.id === kernelId) as KernelPick | undefined; + if (sameItem) { + activeItems.push(sameItem); + } + } else if (isSourcePick(item)) { + const sameItem = newQuickPickItems.find(pi => isSourcePick(pi) && pi.action.command.id === item.action.command.id) as SourcePick | undefined; + if (sameItem) { + activeItems.push(sameItem); + } + } + } + + quickPick.items = newQuickPickItems; + quickPick.activeItems = activeItems; + }, this); + + const pick = await new Promise<{ selected: KernelQuickPickItem | undefined; items: KernelQuickPickItem[] }>((resolve, reject) => { + quickPick.onDidAccept(() => { + const item = quickPick.selectedItems[0]; + if (item) { + resolve({ selected: item, items: quickPick.items as KernelQuickPickItem[] }); + } else { + resolve({ selected: undefined, items: quickPick.items as KernelQuickPickItem[] }); + } + + quickPick.hide(); + }); + + quickPick.onDidHide(() => { + kernelDetectionTaskListener.dispose(); + kernelChangeEventListener?.dispose(); + quickPick.dispose(); + resolve({ selected: undefined, items: quickPick.items as KernelQuickPickItem[] }); + }); + quickPick.show(); + }); + + if (pick.selected) { + return this.handleQuickPick(editor, pick.selected, pick.items); + } + + return false; + } + + protected getMatchingResult(notebook: NotebookModel): NotebookKernelMatchResult { + return this.notebookKernelService.getMatchingKernel(notebook); + } + + protected abstract getKernelPickerQuickPickItems(matchResult: NotebookKernelMatchResult): QuickPickInput[]; + + protected async handleQuickPick(editor: NotebookModel, pick: KernelQuickPickItem, quickPickItems: KernelQuickPickItem[]): Promise { + if (isKernelPick(pick)) { + const newKernel = pick.kernel; + this.selectKernel(editor, newKernel); + return true; + } + + // actions + // if (isSearchMarketplacePick(pick)) { + // await this.showKernelExtension( + // this.paneCompositePartService, + // this.extensionWorkbenchService, + // this.extensionService, + // editor.textModel.viewType, + // [] + // ); + // // suggestedExtension must be defined for this option to be shown, but still check to make TS happy + // } else if (isInstallExtensionPick(pick)) { + // await this.showKernelExtension( + // this.paneCompositePartService, + // this.extensionWorkbenchService, + // this.extensionService, + // editor.textModel.viewType, + // pick.extensionIds, + // ); + // } else + if (isSourcePick(pick)) { + // selected explicilty, it should trigger the execution? + pick.action.run(); + } + + return true; } - protected getKernels(notebook: NotebookModel): NotebookKernel[] { - return this.notebookKernelService.getMatchingKernel(notebook).all; + protected selectKernel(notebook: NotebookModel, kernel: NotebookKernel): void { + this.notebookKernelService.selectKernelForNotebook(kernel, notebook); + } + + // protected async showKernelExtension( + // paneCompositePartService: PaneCompositePartService, + // extensionWorkbenchService: ExtensionsWorkbenchService, + // extensionService: ExtensionService, + // viewType: string, + // extIds: string[], + // ) { + // // If extension id is provided attempt to install the extension as the user has requested the suggested ones be installed + // const extensionsToInstall: IExtension[] = []; + + // for (const extId of extIds) { + // const extension = (await extensionWorkbenchService.getExtensions([{ id: extId }], CancellationToken.None))[0]; + // const canInstall = await extensionWorkbenchService.canInstall(extension); + // if (canInstall) { + // extensionsToInstall.push(extension); + // } + // } + + // if (extensionsToInstall.length) { + // await Promise.all(extensionsToInstall.map(async extension => { + // await extensionWorkbenchService.install( + // extension, + // { + // installPreReleaseVersion: false, + // context: { skipWalkthrough: true } + // }, + // ProgressLocation.Notification + // ); + // })); + + // await extensionService.activateByEvent(`onNotebook:${viewType}`); + // return; + // } + + // const viewlet = await paneCompositePartService.openPaneComposite(EXTENSIONVIEWLETID, ViewContainerLocation.Sidebar, true); + // const view = viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer | undefined; + // const pascalCased = viewType.split(/[^a-z0-9]/ig).map(uppercaseFirstLetter).join(''); + // view?.search(`@tag:notebookKernel${pascalCased}`); + // } + + // private async showInstallKernelExtensionRecommendation( + // notebookModel: NotebookModel, + // quickPick: QuickPick, + // extensionWorkbenchService: ExtensionsWorkbenchService, + // token: CancellationToken + // ): Promise { + // quickPick.busy = true; + + // const newQuickPickItems = await this.getKernelRecommendationsQuickPickItems(notebookModel, extensionWorkbenchService); + // quickPick.busy = false; + + // if (token.isCancellationRequested) { + // return; + // } + + // if (newQuickPickItems && quickPick.items.length === 0) { + // quickPick.items = newQuickPickItems; + // } + // } + + // protected async getKernelRecommendationsQuickPickItems( + // notebookModel: NotebookModel, + // extensionWorkbenchService: ExtensionsWorkbenchService, + // ): Promise[] | undefined> { + // const quickPickItems: QuickPickInput[] = []; + + // const language = this.getSuggestedLanguage(notebookModel); + // const suggestedExtension: NotebookExtensionRecommendation | undefined = language ? this.getSuggestedKernelFromLanguage(notebookModel.viewType, language) : undefined; + // if (suggestedExtension) { + // await extensionWorkbenchService.queryLocal(); + // const extensions = extensionWorkbenchService.installed.filter(e => suggestedExtension.extensionIds.includes(e.identifier.id)); + + // if (extensions.length === suggestedExtension.extensionIds.length) { + // // it's installed but might be detecting kernels + // return undefined; + // } + + // // We have a suggested kernel, show an option to install it + // quickPickItems.push({ + // id: 'installSuggested', + // description: suggestedExtension.displayName ?? suggestedExtension.extensionIds.join(', '), + // label: `$(${Codicon.lightbulb.id}) ` + nls.localizeByDefault('Install suggested extensions'), + // extensionIds: suggestedExtension.extensionIds + // } as InstallExtensionPick); + // } + // // there is no kernel, show the install from marketplace + // quickPickItems.push({ + // id: 'install', + // label: nls.localizeByDefault('Browse marketplace for kernel extensions'), + // } as SearchMarketplacePick); + + // return quickPickItems; + // } + + /** + * Examine the most common language in the notebook + * @param notebookModel The notebook text model + * @returns What the suggested language is for the notebook. Used for kernal installing + */ + // private getSuggestedLanguage(notebookModel: NotebookModel): string | undefined { + // const metaData = notebookModel.data.metadata; + // let suggestedKernelLanguage: string | undefined = (metaData.custom as any)?.metadata?.languageinfo?.name; + // // TODO how do we suggest multi language notebooks? + // if (!suggestedKernelLanguage) { + // const cellLanguages = notebookModel.cells.map(cell => cell.language).filter(language => language !== 'markdown'); + // // Check if cell languages is all the same + // if (cellLanguages.length > 1) { + // const firstLanguage = cellLanguages[0]; + // if (cellLanguages.every(language => language === firstLanguage)) { + // suggestedKernelLanguage = firstLanguage; + // } + // } + // } + // return suggestedKernelLanguage; + // } + + /** + * Given a language and notebook view type suggest a kernel for installation + * @param language The language to find a suggested kernel extension for + * @returns A recommednation object for the recommended extension, else undefined + */ + // private getSuggestedKernelFromLanguage(viewType: string, language: string): NotebookExtensionRecommendation | undefined { + // const recommendation = KERNELRECOMMENDATIONS.get(viewType)?.get(language); + // return recommendation; + // } + +} +@injectable() +export class KernelPickerMRUStrategy extends NotebookKernelQuickPickServiceImpl { + + @inject(OpenerService) + protected openerService: OpenerService; + + @inject(NotebookKernelHistoryService) + protected notebookKernelHistoryService: NotebookKernelHistoryService; + + protected getKernelPickerQuickPickItems(matchResult: NotebookKernelMatchResult): QuickPickInput[] { + const quickPickItems: QuickPickInput[] = []; + + if (matchResult.selected) { + const kernelItem = toKernelQuickPick(matchResult.selected, matchResult.selected); + quickPickItems.push(kernelItem); + } + + // TODO use suggested here wehen kernel affinity is implemented. For now though show all kernels + matchResult.all.filter(kernel => kernel.id !== matchResult.selected?.id).map(kernel => toKernelQuickPick(kernel, matchResult.selected)) + .forEach(kernel => { + quickPickItems.push(kernel); + }); + + const shouldAutoRun = quickPickItems.length === 0; + + if (quickPickItems.length > 0) { + quickPickItems.push({ + type: 'separator' + }); + } + + // select another kernel quick pick + quickPickItems.push({ + id: 'selectAnother', + label: nls.localizeByDefault('Select Another Kernel...'), + autoRun: shouldAutoRun + }); + + return quickPickItems; + } + + protected override selectKernel(notebook: NotebookModel, kernel: NotebookKernel): void { + const currentInfo = this.notebookKernelService.getMatchingKernel(notebook); + if (currentInfo.selected) { + // there is already a selected kernel + this.notebookKernelHistoryService.addMostRecentKernel(currentInfo.selected); + } + super.selectKernel(notebook, kernel); + this.notebookKernelHistoryService.addMostRecentKernel(kernel); + } + + protected override getMatchingResult(notebook: NotebookModel): NotebookKernelMatchResult { + const { selected, all } = this.notebookKernelHistoryService.getKernels(notebook); + const matchingResult = this.notebookKernelService.getMatchingKernel(notebook); + return { + selected: selected, + all: matchingResult.all, + suggestions: all, + hidden: [] + }; + } + + protected override async handleQuickPick(editor: NotebookModel, pick: KernelQuickPickItem, items: KernelQuickPickItem[]): Promise { + if (pick.id === 'selectAnother') { + return this.displaySelectAnotherQuickPick(editor, items.length === 1 && items[0] === pick); + } + + return super.handleQuickPick(editor, pick, items); + } + + private async displaySelectAnotherQuickPick(editor: NotebookModel, kernelListEmpty: boolean): Promise { + const notebook: NotebookModel = editor; + const disposables = new DisposableCollection(); + const quickPick = this.quickInputService.createQuickPick(); + const quickPickItem = await new Promise(resolve => { + // select from kernel sources + quickPick.title = kernelListEmpty ? nls.localizeByDefault('Select Kernel') : nls.localizeByDefault('Select Another Kernel'); + quickPick.placeholder = nls.localizeByDefault('Type to choose a kernel source'); + quickPick.busy = true; + // quickPick.buttons = [this.quickInputService.backButton]; + quickPick.show(); + + disposables.push(quickPick.onDidTriggerButton(button => { + if (button === this.quickInputService.backButton) { + resolve(button); + } + })); + quickPick.onDidTriggerItemButton(async e => { + + if (isKernelSourceQuickPickItem(e.item) && e.item.documentation !== undefined) { + const uri: URI | undefined = URI.isUri(e.item.documentation) ? new URI(e.item.documentation) : await this.commandService.executeCommand(e.item.documentation); + if (uri) { + (await this.openerService.getOpener(uri, { openExternal: true })).open(uri, { openExternal: true }); + } + } + }); + disposables.push(quickPick.onDidAccept(async () => { + resolve(quickPick.selectedItems[0]); + })); + disposables.push(quickPick.onDidHide(() => { + resolve(undefined); + })); + + this.calculdateKernelSources(editor).then(quickPickItems => { + quickPick.items = quickPickItems; + if (quickPick.items.length > 0) { + quickPick.busy = false; + } + }); + + debounce( + Event.any( + this.notebookKernelService.onDidChangeSourceActions, + this.notebookKernelService.onDidAddKernel, + this.notebookKernelService.onDidRemoveKernel + ), + KERNELPICKERUPDATEDEBOUNCE, + )(async () => { + quickPick.busy = true; + const quickPickItems = await this.calculdateKernelSources(editor); + quickPick.items = quickPickItems; + quickPick.busy = false; + }); + }); + + quickPick.hide(); + disposables.dispose(); + + if (quickPickItem === this.quickInputService.backButton) { + return this.showQuickPick(editor, undefined, true); + } + + if (quickPickItem) { + const selectedKernelPickItem = quickPickItem as KernelQuickPickItem; + if (isKernelSourceQuickPickItem(selectedKernelPickItem)) { + try { + const selectedKernelId = await this.executeCommand(notebook, selectedKernelPickItem.command); + if (selectedKernelId) { + const { all } = this.getMatchingResult(notebook); + const notebookKernel = all.find(kernel => kernel.id === `ms-toolsai.jupyter/${selectedKernelId}`); + if (notebookKernel) { + this.selectKernel(notebook, notebookKernel); + return true; + } + return true; + } else { + return this.displaySelectAnotherQuickPick(editor, false); + } + } catch (ex) { + return false; + } + } else if (isKernelPick(selectedKernelPickItem)) { + this.selectKernel(notebook, selectedKernelPickItem.kernel); + return true; + } else if (isGroupedKernelsPick(selectedKernelPickItem)) { + await this.selectOneKernel(notebook, selectedKernelPickItem.source, selectedKernelPickItem.kernels); + return true; + } else if (isSourcePick(selectedKernelPickItem)) { + // selected explicilty, it should trigger the execution? + try { + await selectedKernelPickItem.action.run(); + return true; + } catch (ex) { + return false; + } + } + // } else if (isSearchMarketplacePick(selectedKernelPickItem)) { + // await this.showKernelExtension( + // this.paneCompositePartService, + // this.extensionWorkbenchService, + // this.extensionService, + // editor.textModel.viewType, + // [] + // ); + // return true; + // } else if (isInstallExtensionPick(selectedKernelPickItem)) { + // await this.showKernelExtension( + // this.paneCompositePartService, + // this.extensionWorkbenchService, + // this.extensionService, + // editor.textModel.viewType, + // selectedKernelPickItem.extensionIds, + // ); + // return true; + // } + } + + return false; + } + + private async calculdateKernelSources(editor: NotebookModel): Promise[]> { + const notebook: NotebookModel = editor; + + const actions = await this.notebookKernelService.getKernelSourceActionsFromProviders(notebook); + const matchResult = this.getMatchingResult(notebook); + + const others = matchResult.all.filter(item => item.extension !== JUPYTER_EXTENSION_ID); + const quickPickItems: QuickPickInput[] = []; + + // group controllers by extension + for (const group of ArrayUtils.groupBy(others, (a, b) => a.extension === b.extension ? 0 : 1)) { + const source = group[0].extension; + if (group.length > 1) { + quickPickItems.push({ + label: source, + kernels: group + }); + } else { + quickPickItems.push({ + label: group[0].label, + kernel: group[0] + }); + } + } + + const validActions = actions.filter(action => action.command); + + quickPickItems.push(...validActions.map(action => { + const buttons = action.documentation ? [{ + iconClass: codicon('info'), + tooltip: nls.localizeByDefault('Learn More'), + }] : []; + return { + id: typeof action.command! === 'string' ? action.command! : action.command!.id, + label: action.label, + description: action.description, + command: action.command, + documentation: action.documentation, + buttons + }; + })); + + return quickPickItems; + } + + private async selectOneKernel(notebook: NotebookModel, source: string, kernels: NotebookKernel[]): Promise { + const quickPickItems: QuickPickInput[] = kernels.map(kernel => toKernelQuickPick(kernel, undefined)); + const quickPick = this.quickInputService.createQuickPick(); + quickPick.items = quickPickItems; + quickPick.canSelectMany = false; + + quickPick.title = nls.localizeByDefault('Select Kernel from {0}', source); + + quickPick.onDidAccept(async () => { + if (quickPick.selectedItems && quickPick.selectedItems.length > 0 && isKernelPick(quickPick.selectedItems[0])) { + this.selectKernel(notebook, quickPick.selectedItems[0].kernel); + } + + quickPick.hide(); + quickPick.dispose(); + }); + + quickPick.onDidHide(() => { + quickPick.dispose(); + }); + + quickPick.show(); + } + + private async executeCommand(notebook: NotebookModel, command: string | Command): Promise { + const id = typeof command === 'string' ? command : command.id; + + return this.commandService.executeCommand(id, { uri: notebook.uri }); + } } diff --git a/packages/notebook/src/browser/service/notebook-kernel-service.ts b/packages/notebook/src/browser/service/notebook-kernel-service.ts index 0d054e39d3b5b..b59979b9232df 100644 --- a/packages/notebook/src/browser/service/notebook-kernel-service.ts +++ b/packages/notebook/src/browser/service/notebook-kernel-service.ts @@ -18,9 +18,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable, Event, URI } from '@theia/core'; +import { Command, CommandRegistry, Disposable, Emitter, Event, URI } from '@theia/core'; import { inject, injectable } from '@theia/core/shared/inversify'; -import { Emitter } from '@theia/core/shared/vscode-languageserver-protocol'; +import { NotebookKernelSourceAction } from '../../common'; import { NotebookModel } from '../view-model/notebook-model'; import { NotebookService } from './notebook-service'; @@ -77,6 +77,10 @@ export interface INotebookProxyKernelChangeEvent extends NotebookKernelChangeEve connectionState?: true; } +export interface NotebookKernelDetectionTask { + readonly notebookType: string; +} + export interface NotebookTextModelLike { uri: URI; viewType: string } class KernelInfo { @@ -94,8 +98,60 @@ class KernelInfo { } } +export interface NotebookSourceActionChangeEvent { + notebook?: URI; + viewType: string; +} + +export interface KernelSourceActionProvider { + readonly viewType: string; + onDidChangeSourceActions?: Event; + provideKernelSourceActions(): Promise; +} + +export class SourceCommand implements Disposable { + execution: Promise | undefined; + private readonly onDidChangeStateEmitter = new Emitter(); + readonly onDidChangeState = this.onDidChangeStateEmitter.event; + + constructor( + readonly commandRegistry: CommandRegistry, + readonly command: Command, + readonly model: NotebookTextModelLike, + readonly isPrimary: boolean + ) { } + + async run(): Promise { + if (this.execution) { + return this.execution; + } + + this.execution = this.runCommand(); + this.onDidChangeStateEmitter.fire(); + await this.execution; + this.execution = undefined; + this.onDidChangeStateEmitter.fire(); + } + + private async runCommand(): Promise { + try { + await this.commandRegistry.executeCommand(this.command.id, { + uri: this.model.uri, + }); + + } catch (error) { + console.warn(`Kernel source command failed: ${error}`); + } + } + + dispose(): void { + this.onDidChangeStateEmitter.dispose(); + } + +} + @injectable() -export class NotebookKernelService { +export class NotebookKernelService implements Disposable { @inject(NotebookService) protected notebookService: NotebookService; @@ -104,6 +160,14 @@ export class NotebookKernelService { private readonly notebookBindings = new Map(); + private readonly kernelDetectionTasks = new Map(); + private readonly onDidChangeKernelDetectionTasksEmitter = new Emitter(); + readonly onDidChangeKernelDetectionTasks = this.onDidChangeKernelDetectionTasksEmitter.event; + + private readonly onDidChangeSourceActionsEmitter = new Emitter(); + private readonly kernelSourceActionProviders = new Map(); + readonly onDidChangeSourceActions: Event = this.onDidChangeSourceActionsEmitter.event; + private readonly onDidAddKernelEmitter = new Emitter(); readonly onDidAddKernel: Event = this.onDidAddKernelEmitter.event; @@ -151,7 +215,7 @@ export class NotebookKernelService { // bound kernel const selectedId = this.notebookBindings.get(`${notebook.viewType}/${notebook.uri}`); const selected = selectedId ? this.kernels.get(selectedId)?.kernel : undefined; - const suggestions = kernels.filter(item => item.instanceAffinity > 1).map(item => item.kernel); + 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 }; @@ -188,4 +252,62 @@ export class NotebookKernelService { } } + registerNotebookKernelDetectionTask(task: NotebookKernelDetectionTask): Disposable { + const notebookType = task.notebookType; + const all = this.kernelDetectionTasks.get(notebookType) ?? []; + all.push(task); + this.kernelDetectionTasks.set(notebookType, all); + this.onDidChangeKernelDetectionTasksEmitter.fire(notebookType); + return Disposable.create(() => { + const allTasks = this.kernelDetectionTasks.get(notebookType) ?? []; + const idx = allTasks.indexOf(task); + if (idx >= 0) { + allTasks.splice(idx, 1); + this.kernelDetectionTasks.set(notebookType, allTasks); + this.onDidChangeKernelDetectionTasksEmitter.fire(notebookType); + } + }); + } + + getKernelDetectionTasks(notebook: NotebookTextModelLike): NotebookKernelDetectionTask[] { + return this.kernelDetectionTasks.get(notebook.viewType) ?? []; + } + + registerKernelSourceActionProvider(viewType: string, provider: KernelSourceActionProvider): Disposable { + const providers = this.kernelSourceActionProviders.get(viewType) ?? []; + providers.push(provider); + this.kernelSourceActionProviders.set(viewType, providers); + this.onDidChangeSourceActionsEmitter.fire({ viewType: viewType }); + + const eventEmitterDisposable = provider.onDidChangeSourceActions?.(() => { + this.onDidChangeSourceActionsEmitter.fire({ viewType: viewType }); + }); + + return Disposable.create(() => { + const sourceProviders = this.kernelSourceActionProviders.get(viewType) ?? []; + const idx = sourceProviders.indexOf(provider); + if (idx >= 0) { + sourceProviders.splice(idx, 1); + this.kernelSourceActionProviders.set(viewType, sourceProviders); + } + + eventEmitterDisposable?.dispose(); + }); + } + + getKernelSourceActionsFromProviders(notebook: NotebookTextModelLike): Promise { + const viewType = notebook.viewType; + const providers = this.kernelSourceActionProviders.get(viewType) ?? []; + const promises = providers.map(provider => provider.provideKernelSourceActions()); + return Promise.all(promises).then(actions => actions.reduce((a, b) => a.concat(b), [])); + } + + dispose(): void { + this.onDidChangeKernelDetectionTasksEmitter.dispose(); + this.onDidChangeSourceActionsEmitter.dispose(); + this.onDidAddKernelEmitter.dispose(); + this.onDidRemoveKernelEmitter.dispose(); + this.onDidChangeSelectedNotebooksEmitter.dispose(); + this.onDidChangeNotebookAffinityEmitter.dispose(); + } } diff --git a/packages/notebook/src/browser/service/notebook-model-resolver-service.ts b/packages/notebook/src/browser/service/notebook-model-resolver-service.ts index 0653bb0de0f6b..997699fe327a3 100644 --- a/packages/notebook/src/browser/service/notebook-model-resolver-service.ts +++ b/packages/notebook/src/browser/service/notebook-model-resolver-service.ts @@ -14,9 +14,8 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { URI } from '@theia/core'; +import { Emitter, URI } from '@theia/core'; import { inject, injectable } from '@theia/core/shared/inversify'; -import { Emitter } from '@theia/core/shared/vscode-languageserver-protocol'; import { UriComponents } from '@theia/core/lib/common/uri'; import { FileService } from '@theia/filesystem/lib/browser/file-service'; import { NotebookData } from '../../common'; @@ -47,7 +46,7 @@ export class NotebookModelResolverService { const notebookData = await this.resolveExistingNotebookData(arg as URI, viewType!); - const notebookModel = this.notebookService.createNotebookModel(notebookData, viewType, arg as URI); + const notebookModel = await this.notebookService.createNotebookModel(notebookData, viewType, arg as URI); notebookModel.onDirtyChanged(() => this.onDidChangeDirtyEmitter.fire(notebookModel)); notebookModel.onDidSaveNotebook(() => this.onDidSaveNotebookEmitter.fire(notebookModel.uri.toComponents())); diff --git a/packages/notebook/src/browser/service/notebook-service.ts b/packages/notebook/src/browser/service/notebook-service.ts index 5262bd387decf..be449e16ada3b 100644 --- a/packages/notebook/src/browser/service/notebook-service.ts +++ b/packages/notebook/src/browser/service/notebook-service.ts @@ -96,7 +96,7 @@ export class NotebookService implements Disposable { }); } - createNotebookModel(data: NotebookData, viewType: string, uri: URI): NotebookModel { + async createNotebookModel(data: NotebookData, viewType: string, uri: URI): Promise { const serializer = this.notebookProviders.get(viewType)?.serializer; if (!serializer) { throw new Error('no notebook serializer for ' + viewType); @@ -140,4 +140,8 @@ export class NotebookService implements Disposable { async willOpenNotebook(type: string): Promise { return this.willOpenNotebookTypeEmitter.sequence(async listener => listener(type)); } + + listNotebookDocuments(): NotebookModel[] { + return [...this.notebookModels.values()]; + } } diff --git a/packages/notebook/src/browser/service/notebookKernelHistoryService.ts b/packages/notebook/src/browser/service/notebookKernelHistoryService.ts new file mode 100644 index 0000000000000..e44140cee2c48 --- /dev/null +++ b/packages/notebook/src/browser/service/notebookKernelHistoryService.ts @@ -0,0 +1,109 @@ +// ***************************************************************************** +// Copyright (C) 2023 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 { inject, injectable, postConstruct } from '@theia/core/shared/inversify'; +import { StorageService } from '@theia/core/lib/browser'; +import { NotebookKernel, NotebookTextModelLike, NotebookKernelService } from './notebook-kernel-service'; +import { Disposable } from '@theia/core'; + +interface KernelsList { + [viewType: string]: string[]; +} + +interface MostRecentKernelsResult { + selected?: NotebookKernel, + all: NotebookKernel[] +} + +const MAX_KERNELS_IN_HISTORY = 5; + +@injectable() +export class NotebookKernelHistoryService implements Disposable { + + @inject(StorageService) + protected storageService: StorageService; + + @inject(NotebookKernelService) + protected notebookKernelService: NotebookKernelService; + + declare serviceBrand: undefined; + + private static STORAGE_KEY = 'notebook.kernelHistory'; + private mostRecentKernelsMap: KernelsList = {}; + + @postConstruct() + protected init(): void { + this.loadState(); + } + + getKernels(notebook: NotebookTextModelLike): MostRecentKernelsResult { + const allAvailableKernels = this.notebookKernelService.getMatchingKernel(notebook); + const allKernels = allAvailableKernels.all; + const selectedKernel = allAvailableKernels.selected; + // We will suggest the only kernel + const suggested = allAvailableKernels.all.length === 1 ? allAvailableKernels.all[0] : undefined; + const mostRecentKernelIds = this.mostRecentKernelsMap[notebook.viewType] ? this.mostRecentKernelsMap[notebook.viewType].map(kernel => kernel[1]) : []; + const all = mostRecentKernelIds.map(kernelId => allKernels.find(kernel => kernel.id === kernelId)).filter(kernel => !!kernel) as NotebookKernel[]; + + return { + selected: selectedKernel ?? suggested, + all + }; + } + + addMostRecentKernel(kernel: NotebookKernel): void { + const viewType = kernel.viewType; + const recentKeynels = this.mostRecentKernelsMap[viewType] ?? [kernel.id]; + + if (recentKeynels.length > MAX_KERNELS_IN_HISTORY) { + recentKeynels.splice(MAX_KERNELS_IN_HISTORY); + } + + this.mostRecentKernelsMap[viewType] = recentKeynels; + this.saveState(); + } + + private saveState(): void { + let notEmpty = false; + for (const [_, kernels] of Object.entries(this.mostRecentKernelsMap)) { + notEmpty = notEmpty || Object.entries(kernels).length > 0; + } + + this.storageService.setData(NotebookKernelHistoryService.STORAGE_KEY, notEmpty ? JSON.stringify(this.mostRecentKernelsMap) : undefined); + } + + private async loadState(): Promise { + const kernelMap = await this.storageService.getData(NotebookKernelHistoryService.STORAGE_KEY); + if (kernelMap) { + this.mostRecentKernelsMap = kernelMap as KernelsList; + } else { + this.mostRecentKernelsMap = {}; + } + } + + clear(): void { + this.mostRecentKernelsMap = {}; + this.saveState(); + } + + dispose(): void { + + } +} diff --git a/packages/notebook/src/browser/view-model/notebook-model.ts b/packages/notebook/src/browser/view-model/notebook-model.ts index 762dfb547e3e5..4f03350561dd9 100644 --- a/packages/notebook/src/browser/view-model/notebook-model.ts +++ b/packages/notebook/src/browser/view-model/notebook-model.ts @@ -14,18 +14,18 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { Disposable, URI } from '@theia/core'; -import { Emitter } from '@theia/core/shared/vscode-languageserver-protocol'; +import { Disposable, Emitter, URI } from '@theia/core'; import { Saveable, SaveOptions } from '@theia/core/lib/browser'; import { CellEditOperation, CellEditType, CellUri, NotebookCellInternalMetadata, - NotebookCellsChangeType, NotebookCellTextModelSplice, NotebookData, NotebookModelWillAddRemoveEvent, NullablePartialNotebookCellInternalMetadata + NotebookCellsChangeType, NotebookCellTextModelSplice, NotebookData, NotebookModelWillAddRemoveEvent, NotebookTextModelChangedEvent, NullablePartialNotebookCellInternalMetadata } from '../../common'; import { NotebookSerializer } from '../service/notebook-service'; import { FileService } from '@theia/filesystem/lib/browser/file-service'; import { NotebookCellModel, NotebookCellModelFactory, NotebookCellModelProps } from './notebook-cell-model'; import { MonacoTextModelService } from '@theia/monaco/lib/browser/monaco-text-model-service'; import { inject, injectable, interfaces } from '@theia/core/shared/inversify'; +import { NotebookKernel } from '../service/notebook-kernel-service'; export const NotebookModelFactory = Symbol('NotebookModelFactory'); @@ -58,6 +58,9 @@ export class NotebookModel implements Saveable, Disposable { private readonly didAddRemoveCellEmitter = new Emitter(); readonly onDidAddOrRemoveCell = this.didAddRemoveCellEmitter.event; + private readonly onDidChangeContentEmitter = new Emitter(); + readonly onDidChangeContent = this.onDidChangeContentEmitter.event; + @inject(FileService) private readonly fileService: FileService; @@ -65,6 +68,8 @@ export class NotebookModel implements Saveable, Disposable { currentLastHandle: number = 0; + kernel?: NotebookKernel; + dirty: boolean; selectedCell?: NotebookCellModel; private dirtyCells: NotebookCellModel[] = []; diff --git a/packages/notebook/src/browser/view/notebook-cell-editor.tsx b/packages/notebook/src/browser/view/notebook-cell-editor.tsx index cc81eb1922c78..f9682a8ba71af 100644 --- a/packages/notebook/src/browser/view/notebook-cell-editor.tsx +++ b/packages/notebook/src/browser/view/notebook-cell-editor.tsx @@ -33,15 +33,15 @@ export function CellEditor({ textModelService, monacoServices, notebookModel, ce React.useEffect(() => { (async () => { const editorNode = document.getElementById(uri.toString())!; - const cellDocument = await textModelService.createModelReference(uri); + const cellDocument = textModelService.get(uri.toString()) ?? (await textModelService.createModelReference(cell.uri)).object; const editor = new MonacoEditor(uri, - cellDocument.object, + cellDocument, editorNode, monacoServices, Object.assign( { minHeight: -1, maxHeight: -1, - model: (await cellDocument.object.load()).textEditorModel, + model: (await cellDocument.load()).textEditorModel, }, MonacoEditorProvider.inlineOptions)); editor.setLanguage(cell.language); editor.getControl().onDidContentSizeChange(() => { diff --git a/packages/notebook/src/common/notebook-common.ts b/packages/notebook/src/common/notebook-common.ts index b94b4496a229f..2154e0e908218 100644 --- a/packages/notebook/src/common/notebook-common.ts +++ b/packages/notebook/src/common/notebook-common.ts @@ -14,11 +14,11 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { Command, Event, URI } from '@theia/core'; +import { CancellationToken, Command, Event, URI } from '@theia/core'; import { MarkdownString } from '@theia/core/lib/common/markdown-rendering/markdown-string'; import { BinaryBuffer } from '@theia/core/lib/common/buffer'; -import { CancellationToken } from '@theia/core/shared/vscode-languageserver-protocol'; import { UriComponents } from '@theia/core/lib/common/uri'; +import { CellRange } from './notebook-range'; export enum CellKind { Markup = 1, @@ -164,6 +164,31 @@ export enum NotebookCellsChangeType { Unknown = 100 } +export enum SelectionStateType { + Handle = 0, + Index = 1 +} +export interface SelectionHandleState { + kind: SelectionStateType.Handle; + primary: number | null; + selections: number[]; +} + +export interface SelectionIndexState { + kind: SelectionStateType.Index; + focus: CellRange; + selections: CellRange[]; +} + +export type SelectionState = SelectionHandleState | SelectionIndexState; + +export interface NotebookTextModelChangedEvent { + readonly rawEvents: NotebookRawContentEvent[]; + readonly versionId: number; + readonly synchronous: boolean | undefined; + readonly endSelectionState: SelectionState | undefined; +}; + export interface NotebookCellsInitializeEvent { readonly kind: NotebookCellsChangeType.Initialize; readonly changes: NotebookCellTextModelSplice[]; @@ -301,6 +326,14 @@ export interface CellPartialInternalMetadataEditByHandle { internalMetadata: NullablePartialNotebookCellInternalMetadata; } +export interface NotebookKernelSourceAction { + readonly label: string; + readonly description?: string; + readonly detail?: string; + readonly command?: string | Command; + readonly documentation?: UriComponents | string; +} + /** * Whether the provided mime type is a text stream like `stdout`, `stderr`. */ diff --git a/packages/plugin-ext/src/common/collections.ts b/packages/plugin-ext/src/common/collections.ts index 96a90b74dda62..59c4bd84032e9 100644 --- a/packages/plugin-ext/src/common/collections.ts +++ b/packages/plugin-ext/src/common/collections.ts @@ -35,3 +35,20 @@ export function diffSets(before: Set, after: Set): { removed: T[]; adde } return { removed, added }; } + +export function diffMaps(before: Map, after: Map): { removed: V[]; added: V[] } { + const removed: V[] = []; + const added: V[] = []; + for (const [index, value] of before) { + if (!after.has(index)) { + removed.push(value); + } + } + for (const [index, value] of after) { + if (!before.has(index)) { + added.push(value); + } + } + return { removed, added }; +} + diff --git a/packages/plugin-ext/src/common/plugin-api-rpc.ts b/packages/plugin-ext/src/common/plugin-api-rpc.ts index 832064b690421..e1df9e6a5e60d 100644 --- a/packages/plugin-ext/src/common/plugin-api-rpc.ts +++ b/packages/plugin-ext/src/common/plugin-api-rpc.ts @@ -29,6 +29,7 @@ import { FileOperationOptions, TextDocumentChangeReason, IndentAction, + NotebookRendererScript, } from '../plugin/types-impl'; import { UriComponents } from './uri-components'; import { @@ -2103,6 +2104,7 @@ export const PLUGIN_RPC_CONTEXT = { NOTEBOOKS_MAIN: createProxyIdentifier('NotebooksMain'), NOTEBOOK_DOCUMENTS_MAIN: createProxyIdentifier('NotebookDocumentsMain'), NOTEBOOK_EDITORS_MAIN: createProxyIdentifier('NotebookEditorsMain'), + NOTEBOOK_DOCUMENTS_AND_EDITORS_MAIN: createProxyIdentifier('NotebooksAndEditorsMain'), NOTEBOOK_RENDERERS_MAIN: createProxyIdentifier('NotebookRenderersMain'), NOTEBOOK_KERNELS_MAIN: createProxyIdentifier('NotebookKernelsMain'), STATUS_BAR_MESSAGE_REGISTRY_MAIN: >createProxyIdentifier('StatusBarMessageRegistryMain'), @@ -2355,6 +2357,7 @@ export interface NotebookKernelDto { supportsInterrupt?: boolean; supportsExecutionOrder?: boolean; preloads?: { uri: UriComponents; provides: readonly string[] }[]; + rendererScripts?: NotebookRendererScript[]; } export type CellExecuteUpdateDto = CellExecuteOutputEditDto | CellExecuteOutputItemEditDto | CellExecutionStateUpdateDto; @@ -2393,6 +2396,14 @@ export interface NotebookKernelSourceActionDto { readonly documentation?: UriComponents | string; } +export interface NotebookEditorAddData { + id: string; + documentUri: UriComponents; + selections: CellRange[]; + visibleRanges: CellRange[]; + viewColumn?: number; +} + export interface NotebooksExt extends NotebookDocumentsAndEditorsExt { $provideNotebookCellStatusBarItems(handle: number, uri: UriComponents, index: number, token: CancellationToken): Promise; $releaseNotebookCellStatusBarItems(id: number): void; @@ -2455,7 +2466,10 @@ export interface NotebookDocumentsExt { } export interface NotebookDocumentsAndEditorsExt { - $acceptDocumentAndEditorsDelta(delta: NotebookDocumentsAndEditorsDelta): void; + $acceptDocumentsAndEditorsDelta(delta: NotebookDocumentsAndEditorsDelta): Promise; +} + +export interface NotebookDocumentsAndEditorsMain extends Disposable { } export type NotebookEditorViewColumnInfo = Record; @@ -2465,7 +2479,7 @@ export interface NotebookEditorsExt { $acceptEditorViewColumns(data: NotebookEditorViewColumnInfo): void; } -export interface NotebookEditorsMain { +export interface NotebookEditorsMain extends Disposable { $tryShowNotebookDocument(uriComponents: UriComponents, viewType: string, options: NotebookDocumentShowOptions): Promise; $tryRevealRange(id: string, range: CellRange, revealType: NotebookEditorRevealType): Promise; $trySetSelections(id: string, range: CellRange[]): void; diff --git a/packages/plugin-ext/src/main/browser/main-context.ts b/packages/plugin-ext/src/main/browser/main-context.ts index 372911f568590..46418fb981849 100644 --- a/packages/plugin-ext/src/main/browser/main-context.ts +++ b/packages/plugin-ext/src/main/browser/main-context.ts @@ -66,6 +66,8 @@ 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'; +import { NotebooksAndEditorsMain } from './notebooks/notebook-documents-and-editors-main'; export function setUpPluginApi(rpc: RPCProtocol, container: interfaces.Container): void { const authenticationMain = new AuthenticationMainImpl(rpc, container); @@ -104,8 +106,12 @@ export function setUpPluginApi(rpc: RPCProtocol, container: interfaces.Container const pluginSupport = container.get(HostedPluginSupport); rpc.set(PLUGIN_RPC_CONTEXT.NOTEBOOKS_MAIN, new NotebooksMainImpl(rpc, notebookService, pluginSupport)); rpc.set(PLUGIN_RPC_CONTEXT.NOTEBOOK_RENDERERS_MAIN, new NotebookRenderersMainImpl(rpc)); - rpc.set(PLUGIN_RPC_CONTEXT.NOTEBOOK_EDITORS_MAIN, new NotebookEditorsMainImpl(rpc)); - rpc.set(PLUGIN_RPC_CONTEXT.NOTEBOOK_DOCUMENTS_MAIN, new NotebookDocumentsMainImpl(rpc, container)); + const notebookEditorsMain = new NotebookEditorsMainImpl(rpc); + rpc.set(PLUGIN_RPC_CONTEXT.NOTEBOOK_EDITORS_MAIN, notebookEditorsMain); + const notebookDocumentsMain = new NotebookDocumentsMainImpl(rpc, container); + rpc.set(PLUGIN_RPC_CONTEXT.NOTEBOOK_DOCUMENTS_MAIN, notebookDocumentsMain); + rpc.set(PLUGIN_RPC_CONTEXT.NOTEBOOK_DOCUMENTS_AND_EDITORS_MAIN, new NotebooksAndEditorsMain(rpc, container, notebookDocumentsMain, notebookEditorsMain)); + rpc.set(PLUGIN_RPC_CONTEXT.NOTEBOOK_KERNELS_MAIN, new NotebookKernelsMainImpl(rpc, container)); const bulkEditService = container.get(MonacoBulkEditService); const monacoEditorService = container.get(MonacoEditorService); diff --git a/packages/plugin-ext/src/main/browser/notebooks/notebook-documents-and-editors-main.ts b/packages/plugin-ext/src/main/browser/notebooks/notebook-documents-and-editors-main.ts new file mode 100644 index 0000000000000..84202d8c2ef21 --- /dev/null +++ b/packages/plugin-ext/src/main/browser/notebooks/notebook-documents-and-editors-main.ts @@ -0,0 +1,243 @@ +// ***************************************************************************** +// Copyright (C) 2023 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 { Disposable, DisposableCollection } from '@theia/core'; +import { interfaces } from '@theia/core/shared/inversify'; +import { UriComponents } from '@theia/core/lib/common/uri'; +import { NotebookEditorWidget, NotebookService, NotebookEditorWidgetService } from '@theia/notebook/lib/browser'; +import { NotebookModel } from '@theia/notebook/lib/browser/view-model/notebook-model'; +import { MAIN_RPC_CONTEXT, NotebookDocumentsAndEditorsDelta, NotebookDocumentsAndEditorsMain, NotebookModelAddedData, NotebooksExt } from '../../../common'; +import { RPCProtocol } from '../../../common/rpc-protocol'; +import { NotebookDto } from './notebookDto'; +import { WidgetManager } from '@theia/core/lib/browser'; +import { NotebookEditorsMainImpl } from './notebook-editors-main'; +import { NotebookDocumentsMainImpl } from './notebook-documents-main'; +import { diffMaps, diffSets } from '../../../common/collections'; + +interface NotebookAndEditorDelta { + removedDocuments: UriComponents[]; + addedDocuments: NotebookModel[]; + removedEditors: string[]; + addedEditors: NotebookEditorWidget[]; + newActiveEditor?: string | null; + visibleEditors?: string[]; +} + +class NotebookAndEditorState { + static delta(before: NotebookAndEditorState | undefined, after: NotebookAndEditorState): NotebookAndEditorDelta { + if (!before) { + return { + addedDocuments: [...after.documents], + removedDocuments: [], + addedEditors: [...after.textEditors.values()], + removedEditors: [], + visibleEditors: [...after.visibleEditors].map(editor => editor[0]) + }; + } + const documentDelta = diffSets(before.documents, after.documents); + const editorDelta = diffMaps(before.textEditors, after.textEditors); + + const newActiveEditor = before.activeEditor !== after.activeEditor ? after.activeEditor : undefined; + const visibleEditorDelta = diffMaps(before.visibleEditors, after.visibleEditors); + + return { + addedDocuments: documentDelta.added, + removedDocuments: documentDelta.removed.map(e => e.uri.toComponents()), + addedEditors: editorDelta.added, + removedEditors: editorDelta.removed.map(removed => removed.id), + newActiveEditor: newActiveEditor, + visibleEditors: visibleEditorDelta.added.length === 0 && visibleEditorDelta.removed.length === 0 + ? undefined + : [...after.visibleEditors].map(editor => editor[0]) + }; + } + + constructor( + readonly documents: Set, + readonly textEditors: Map, + readonly activeEditor: string | null | undefined, + readonly visibleEditors: Map + ) { + // + } +} + +export class NotebooksAndEditorsMain implements NotebookDocumentsAndEditorsMain { + + protected readonly proxy: NotebooksExt; + protected readonly disposables = new DisposableCollection(); + + protected readonly editorListeners = new Map(); + + protected currentState?: NotebookAndEditorState; + + protected readonly notebookService: NotebookService; + protected readonly notebookeditorService: NotebookEditorWidgetService; + protected readonly WidgetManager: WidgetManager; + + constructor( + rpc: RPCProtocol, + container: interfaces.Container, + protected readonly notebookDocumentsmain: NotebookDocumentsMainImpl, + protected readonly notebookEditorsMain: NotebookEditorsMainImpl + ) { + this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.NOTEBOOKS_EXT); + + this.notebookService = container.get(NotebookService); + this.notebookeditorService = container.get(NotebookEditorWidgetService); + this.WidgetManager = container.get(WidgetManager); + + this.notebookService.onWillAddNotebookDocument(async () => this.updateState(), this, this.disposables); + this.notebookService.onDidRemoveNotebookDocument(async () => this.updateState(), this, this.disposables); + // this.WidgetManager.onActiveEditorChanged(() => this.updateState(), this, this.disposables); + this.notebookeditorService.onDidAddNotebookEditor(this.handleEditorAdd, this, this.disposables); + this.notebookeditorService.onDidRemoveNotebookEditor(this.handleEditorRemove, this, this.disposables); + this.notebookeditorService.onFocusedEditorChanged(async editor => this.updateState(editor), this, this.disposables); + + this.updateState(); + } + + dispose(): void { + this.notebookDocumentsmain.dispose(); + this.notebookEditorsMain.dispose(); + this.disposables.dispose(); + this.editorListeners.forEach(listeners => listeners.forEach(listener => listener.dispose())); + } + + private handleEditorAdd(editor: NotebookEditorWidget): void { + this.editorListeners.set(editor.id, [ + editor.onDidChangeModel(() => this.updateState()), + ]); + this.updateState(); + } + + private handleEditorRemove(editor: NotebookEditorWidget): void { + const listeners = this.editorListeners.get(editor.id); + listeners?.forEach(listener => listener.dispose()); + this.editorListeners.delete(editor.id); + this.updateState(); + } + + private async updateState(focusedEditor?: NotebookEditorWidget): Promise { + + const editors = new Map(); + const visibleEditorsMap = new Map(); + + for (const editor of this.notebookeditorService.listNotebookEditors()) { + if (editor.model) { + editors.set(editor.id, editor); + } + } + + const activeNotebookEditor = this.notebookeditorService.currentfocusedEditor; + let activeEditor: string | null = null; + if (activeNotebookEditor) { + activeEditor = activeNotebookEditor.id; + } else if (focusedEditor?.model) { + activeEditor = focusedEditor.id; + } + if (activeEditor && !editors.has(activeEditor)) { + activeEditor = null; + } + + const notebookEditors = this.WidgetManager.getWidgets(NotebookEditorWidget.ID) as NotebookEditorWidget[]; + for (const notebookEditor of notebookEditors) { + if (notebookEditor?.model && editors.has(notebookEditor.id) && notebookEditor.isVisible) { + visibleEditorsMap.set(notebookEditor.id, notebookEditor); + } + } + + const newState = new NotebookAndEditorState( + new Set(this.notebookService.listNotebookDocuments()), + editors, + activeEditor, visibleEditorsMap); + await this.onDelta(NotebookAndEditorState.delta(this.currentState, newState)); + this.currentState = newState; + } + + private async onDelta(delta: NotebookAndEditorDelta): Promise { + if (NotebooksAndEditorsMain._isDeltaEmpty(delta)) { + return; + } + + const dto: NotebookDocumentsAndEditorsDelta = { + removedDocuments: delta.removedDocuments, + removedEditors: delta.removedEditors, + newActiveEditor: delta.newActiveEditor, + visibleEditors: delta.visibleEditors, + addedDocuments: delta.addedDocuments.map(NotebooksAndEditorsMain.asModelAddData), + // addedEditors: delta.addedEditors.map(this.asEditorAddData, this), + }; + + // send to extension FIRST + await this.proxy.$acceptDocumentsAndEditorsDelta(dto); + + // handle internally + this.notebookEditorsMain.handleEditorsRemoved(delta.removedEditors); + this.notebookDocumentsmain.handleNotebooksRemoved(delta.removedDocuments); + this.notebookDocumentsmain.handleNotebooksAdded(delta.addedDocuments); + this.notebookEditorsMain.handleEditorsAdded(delta.addedEditors); + } + + private static _isDeltaEmpty(delta: NotebookAndEditorDelta): boolean { + if (delta.addedDocuments !== undefined && delta.addedDocuments.length > 0) { + return false; + } + if (delta.removedDocuments !== undefined && delta.removedDocuments.length > 0) { + return false; + } + if (delta.addedEditors !== undefined && delta.addedEditors.length > 0) { + return false; + } + if (delta.removedEditors !== undefined && delta.removedEditors.length > 0) { + return false; + } + if (delta.visibleEditors !== undefined && delta.visibleEditors.length > 0) { + return false; + } + if (delta.newActiveEditor !== undefined) { + return false; + } + return true; + } + + private static asModelAddData(e: NotebookModel): NotebookModelAddedData { + return { + viewType: e.viewType, + uri: e.uri.toComponents(), + metadata: e.data.metadata, + versionId: 1, // TODO implement versionID support + cells: e.cells.map(NotebookDto.toNotebookCellDto) + }; + } + + // private asEditorAddData(add: NotebookEditorWidget): NotebookEditorAddData { + + // const pane = this.editorManager.visibleEditorPanes.find(pane => getNotebookEditorFromEditorPane(pane) === add); + + // return { + // id: add.id, + // documentUri: add.textModel.uri, + // selections: add.getSelections(), + // visibleRanges: add.visibleRanges, + // viewColumn: pane && editorGroupToColumn(this._editorGroupService, pane.group) + // }; + // } +} diff --git a/packages/plugin-ext/src/main/browser/notebooks/notebook-documents-main.ts b/packages/plugin-ext/src/main/browser/notebooks/notebook-documents-main.ts index 052865db94fb9..c744d47c75d39 100644 --- a/packages/plugin-ext/src/main/browser/notebooks/notebook-documents-main.ts +++ b/packages/plugin-ext/src/main/browser/notebooks/notebook-documents-main.ts @@ -17,17 +17,19 @@ import { DisposableCollection } from '@theia/core'; import { URI, UriComponents } from '@theia/core/lib/common/uri'; import { interfaces } from '@theia/core/shared/inversify'; -import { ResourceMap } from '@theia/monaco-editor-core/esm/vs/base/common/map'; import { NotebookModelResolverService } from '@theia/notebook/lib/browser'; -import { MAIN_RPC_CONTEXT, NotebookDataDto, NotebookDocumentsExt, NotebookDocumentsMain } from '../../../common'; +import { NotebookModel } from '@theia/notebook/lib/browser/view-model/notebook-model'; +import { NotebookCellsChangeType } from '@theia/notebook/lib/common'; +import { MAIN_RPC_CONTEXT, NotebookCellsChangedEventDto, NotebookDataDto, NotebookDocumentsExt, NotebookDocumentsMain } from '../../../common'; import { RPCProtocol } from '../../../common/rpc-protocol'; +import { NotebookDto } from './notebookDto'; export class NotebookDocumentsMainImpl implements NotebookDocumentsMain { private readonly disposables = new DisposableCollection(); private readonly proxy: NotebookDocumentsExt; - private readonly documentEventListenersMapping = new ResourceMap(); + private readonly documentEventListenersMapping = new Map(); private readonly notebookModelResolverService: NotebookModelResolverService; @@ -50,83 +52,82 @@ export class NotebookDocumentsMainImpl implements NotebookDocumentsMain { this.documentEventListenersMapping.forEach(value => value.dispose()); } - // handleNotebooksAdded(notebooks: readonly NotebookTextModel[]): void { - - // for (const textModel of notebooks) { - // const disposableStore = new DisposableCollection(); - // disposableStore.push(textModel.onDidChangeContent(event => { - - // const eventDto: NotebookCellsChangedEventDto = { - // versionId: event.versionId, - // rawEvents: [] - // }; - - // for (const e of event.rawEvents) { - - // switch (e.kind) { - // case NotebookCellsChangeType.ModelChange: - // eventDto.rawEvents.push({ - // kind: e.kind, - // changes: e.changes.map(diff => [diff[0], diff[1], diff[2].map(cell => - // NotebookDto.toNotebookCellDto(cell))] as [number, number, NotebookCellDto[]]) - // }); - // break; - // case NotebookCellsChangeType.Move: - // eventDto.rawEvents.push({ - // kind: e.kind, - // index: e.index, - // length: e.length, - // newIdx: e.newIdx, - // }); - // break; - // case NotebookCellsChangeType.Output: - // eventDto.rawEvents.push({ - // kind: e.kind, - // index: e.index, - // outputs: e.outputs.map(NotebookDto.toNotebookOutputDto) - // }); - // break; - // case NotebookCellsChangeType.OutputItem: - // eventDto.rawEvents.push({ - // kind: e.kind, - // index: e.index, - // outputId: e.outputId, - // outputItems: e.outputItems.map(NotebookDto.toNotebookOutputItemDto), - // append: e.append - // }); - // break; - // case NotebookCellsChangeType.ChangeCellLanguage: - // case NotebookCellsChangeType.ChangeCellContent: - // case NotebookCellsChangeType.ChangeCellMetadata: - // case NotebookCellsChangeType.ChangeCellInternalMetadata: - // eventDto.rawEvents.push(e); - // break; - // } - // } - - // const hasDocumentMetadataChangeEvent = event.rawEvents.find(e => e.kind === NotebookCellsChangeType.ChangeDocumentMetadata); - - // // using the model resolver service to know if the model is dirty or not. - // // assuming this is the first listener it can mean that at first the model - // // is marked as dirty and that another event is fired - // this.proxy.$acceptModelChanged( - // textModel.uri, - // eventDto, - // this.notebookEditorModelResolverService.isDirty(textModel.uri), - // hasDocumentMetadataChangeEvent ? textModel.metadata : undefined - // ); - // })); - - // this.documentEventListenersMapping.set(textModel.uri, disposableStore); - // } - // } - - // handleNotebooksRemoved(uris: URI[]): void { - // for (const uri of uris) { - // this.documentEventListenersMapping.get(uri)?.dispose(); - // this.documentEventListenersMapping.delete(uri); - // } - // } + handleNotebooksAdded(notebooks: readonly NotebookModel[]): void { + + for (const textModel of notebooks) { + const disposableStore = new DisposableCollection(); + disposableStore.push(textModel.onDidChangeContent(event => { + + const eventDto: NotebookCellsChangedEventDto = { + versionId: 1, // TODO implement version ID support + rawEvents: [] + }; + + for (const e of event.rawEvents) { + + switch (e.kind) { + case NotebookCellsChangeType.ModelChange: + eventDto.rawEvents.push({ + kind: e.kind, + changes: []// e.changes.map(diff => [diff[0], diff[1], diff[2]] as [number, number, NotebookCellDataDto[]]) + }); + break; + case NotebookCellsChangeType.Move: + eventDto.rawEvents.push({ + kind: e.kind, + index: e.index, + length: e.length, + newIdx: e.newIdx, + }); + break; + case NotebookCellsChangeType.Output: + eventDto.rawEvents.push({ + kind: e.kind, + index: e.index, + outputs: e.outputs.map(NotebookDto.toNotebookOutputDto) + }); + break; + case NotebookCellsChangeType.OutputItem: + eventDto.rawEvents.push({ + kind: e.kind, + index: e.index, + outputId: e.outputId, + outputItems: e.outputItems.map(NotebookDto.toNotebookOutputItemDto), + append: e.append + }); + break; + case NotebookCellsChangeType.ChangeCellLanguage: + case NotebookCellsChangeType.ChangeCellContent: + case NotebookCellsChangeType.ChangeCellMetadata: + case NotebookCellsChangeType.ChangeCellInternalMetadata: + eventDto.rawEvents.push(e); + break; + } + } + + const hasDocumentMetadataChangeEvent = event.rawEvents.find(e => e.kind === NotebookCellsChangeType.ChangeDocumentMetadata); + + // using the model resolver service to know if the model is dirty or not. + // assuming this is the first listener it can mean that at first the model + // is marked as dirty and that another event is fired + this.proxy.$acceptModelChanged( + textModel.uri.toComponents(), + eventDto, + textModel.isDirty(), + hasDocumentMetadataChangeEvent ? textModel.data.metadata : undefined + ); + })); + + this.documentEventListenersMapping.set(textModel.uri.toString(), disposableStore); + } + } + + handleNotebooksRemoved(uris: UriComponents[]): void { + for (const uri of uris) { + this.documentEventListenersMapping.get(uri.toString())?.dispose(); + this.documentEventListenersMapping.delete(uri.toString()); + } + } async $tryCreateNotebook(options: { viewType: string; content?: NotebookDataDto }): Promise { const ref = await this.notebookModelResolverService.resolve({ untitledResource: undefined }, options.viewType); diff --git a/packages/plugin-ext/src/main/browser/notebooks/notebook-editors-main.ts b/packages/plugin-ext/src/main/browser/notebooks/notebook-editors-main.ts index 159abc3f0d671..231365ff8bfcd 100644 --- a/packages/plugin-ext/src/main/browser/notebooks/notebook-editors-main.ts +++ b/packages/plugin-ext/src/main/browser/notebooks/notebook-editors-main.ts @@ -20,6 +20,7 @@ import { UriComponents } from '@theia/core/lib/common/uri'; import { CellRange } from '@theia/notebook/lib/common'; +import { NotebookEditorWidget } from '@theia/notebook/lib/browser'; import { MAIN_RPC_CONTEXT, NotebookDocumentShowOptions, NotebookEditorRevealType, NotebookEditorsExt, NotebookEditorsMain } from '../../../common'; import { RPCProtocol } from '../../../common/rpc-protocol'; @@ -27,6 +28,8 @@ export class NotebookEditorsMainImpl implements NotebookEditorsMain { protected readonly proxy: NotebookEditorsExt; + private readonly mainThreadEditors = new Map(); + constructor( rpc: RPCProtocol, ) { @@ -43,4 +46,19 @@ export class NotebookEditorsMainImpl implements NotebookEditorsMain { throw new Error('Method not implemented.'); } + handleEditorsAdded(editors: readonly NotebookEditorWidget[]): void { + for (const editor of editors) { + this.mainThreadEditors.set(editor.id, editor); + } + } + + handleEditorsRemoved(editorIds: readonly string[]): void { + for (const id of editorIds) { + this.mainThreadEditors.get(id)?.dispose(); + this.mainThreadEditors.delete(id); + } + } + + dispose(): void { + } } diff --git a/packages/plugin-ext/src/main/browser/notebooks/notebook-kernels-main.ts b/packages/plugin-ext/src/main/browser/notebooks/notebook-kernels-main.ts index 81fec1395578e..80635c7075e1f 100644 --- a/packages/plugin-ext/src/main/browser/notebooks/notebook-kernels-main.ts +++ b/packages/plugin-ext/src/main/browser/notebooks/notebook-kernels-main.ts @@ -18,15 +18,15 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Emitter, Event, URI } from '@theia/core'; +import { CancellationToken, Disposable, Emitter, Event, URI } from '@theia/core'; import { UriComponents } from '@theia/core/lib/common/uri'; import { LanguageService } from '@theia/core/lib/browser/language-service'; import { CellExecutionCompleteDto, CellExecutionStateUpdateDto, MAIN_RPC_CONTEXT, NotebookKernelDto, NotebookKernelsExt, NotebookKernelsMain } from '../../../common'; import { RPCProtocol } from '../../../common/rpc-protocol'; import { CellExecution, NotebookExecutionStateService, NotebookKernelChangeEvent, NotebookKernelService, NotebookService } from '@theia/notebook/lib/browser'; -import { Disposable } from '@theia/core/shared/vscode-languageserver-protocol'; import { combinedDisposable } from '@theia/monaco-editor-core/esm/vs/base/common/lifecycle'; import { interfaces } from '@theia/core/shared/inversify'; +import { NotebookKernelSourceAction } from '@theia/notebook/lib/common'; abstract class NotebookKernel { private readonly onDidChangeEmitter = new Emitter(); @@ -103,12 +103,27 @@ abstract class NotebookKernel { abstract cancelNotebookCellExecution(uri: URI, cellHandles: number[]): Promise; } +class KernelDetectionTask { + constructor(readonly notebookType: string) { } +} + +export interface KernelSourceActionProvider { + readonly viewType: string; + onDidChangeSourceActions?: Event; + provideKernelSourceActions(): Promise; +} + export class NotebookKernelsMainImpl implements NotebookKernelsMain { private readonly proxy: NotebookKernelsExt; private readonly kernels = new Map(); + private readonly kernelDetectionTasks = new Map(); + + private readonly kernelSourceActionProviders = new Map(); + private readonly kernelSourceActionProvidersEventRegistrations = new Map(); + private notebookKernelService: NotebookKernelService; private notebookService: NotebookService; private languageService: LanguageService; @@ -204,16 +219,53 @@ export class NotebookKernelsMainImpl implements NotebookKernelsMain { $completeNotebookExecution(handle: number): void { throw new Error('Method not implemented.'); } - $addKernelDetectionTask(handle: number, notebookType: string): Promise { - throw new Error('Method not implemented.'); + async $addKernelDetectionTask(handle: number, notebookType: string): Promise { + const kernelDetectionTask = new KernelDetectionTask(notebookType); + const registration = this.notebookKernelService.registerNotebookKernelDetectionTask(kernelDetectionTask); + this.kernelDetectionTasks.set(handle, [kernelDetectionTask, registration]); } $removeKernelDetectionTask(handle: number): void { + const tuple = this.kernelDetectionTasks.get(handle); + if (tuple) { + tuple[1].dispose(); + this.kernelDetectionTasks.delete(handle); + } } - $addKernelSourceActionProvider(handle: number, eventHandle: number, notebookType: string): Promise { - return Promise.resolve(); + async $addKernelSourceActionProvider(handle: number, eventHandle: number, notebookType: string): Promise { + const kernelSourceActionProvider: KernelSourceActionProvider = { + viewType: notebookType, + provideKernelSourceActions: async () => { + const actions = await this.proxy.$provideKernelSourceActions(handle, CancellationToken.None); + + return actions.map(action => ({ + label: action.label, + command: action.command, + description: action.description, + detail: action.detail, + documentation: action.documentation, + })); + } + }; + + if (typeof eventHandle === 'number') { + const emitter = new Emitter(); + this.kernelSourceActionProvidersEventRegistrations.set(eventHandle, emitter); + kernelSourceActionProvider.onDidChangeSourceActions = emitter.event; + } + + const registration = this.notebookKernelService.registerKernelSourceActionProvider(notebookType, kernelSourceActionProvider); + this.kernelSourceActionProviders.set(handle, [kernelSourceActionProvider, registration]); } + $removeKernelSourceActionProvider(handle: number, eventHandle: number): void { - throw new Error('Method not implemented.'); + const tuple = this.kernelSourceActionProviders.get(handle); + if (tuple) { + tuple[1].dispose(); + this.kernelSourceActionProviders.delete(handle); + } + if (typeof eventHandle === 'number') { + this.kernelSourceActionProvidersEventRegistrations.delete(eventHandle); + } } $emitNotebookKernelSourceActionsChangeEvent(eventHandle: number): void { } diff --git a/packages/plugin-ext/src/main/browser/notebooks/notebook-renderers-main.ts b/packages/plugin-ext/src/main/browser/notebooks/notebook-renderers-main.ts index a72aaaaffbf0c..b9193eb2a0b6b 100644 --- a/packages/plugin-ext/src/main/browser/notebooks/notebook-renderers-main.ts +++ b/packages/plugin-ext/src/main/browser/notebooks/notebook-renderers-main.ts @@ -33,7 +33,7 @@ export class NotebookRenderersMainImpl implements NotebookRenderersMain { } $postMessage(editorId: string | undefined, rendererId: string, message: unknown): Promise { - throw new Error('Method not implemented.'); + return Promise.resolve(true); } dispose(): void { diff --git a/packages/plugin-ext/src/plugin/editors-and-documents.ts b/packages/plugin-ext/src/plugin/editors-and-documents.ts index cf299753269d2..4d3255e9f5507 100644 --- a/packages/plugin-ext/src/plugin/editors-and-documents.ts +++ b/packages/plugin-ext/src/plugin/editors-and-documents.ts @@ -43,7 +43,7 @@ export class EditorsAndDocumentsExtImpl implements EditorsAndDocumentsExt { constructor(private readonly rpc: RPCProtocol) { } - $acceptEditorsAndDocumentsDelta(delta: EditorsAndDocumentsDelta): void { + async $acceptEditorsAndDocumentsDelta(delta: EditorsAndDocumentsDelta): Promise { const removedDocuments = new Array(); const addedDocuments = new Array(); const removedEditors = new Array(); diff --git a/packages/plugin-ext/src/plugin/notebook/notebook-document.ts b/packages/plugin-ext/src/plugin/notebook/notebook-document.ts index 66cc82efc2a6d..086f4663c7dd8 100644 --- a/packages/plugin-ext/src/plugin/notebook/notebook-document.ts +++ b/packages/plugin-ext/src/plugin/notebook/notebook-document.ts @@ -22,9 +22,9 @@ import * as theia from '@theia/plugin'; import * as rpc from '../../common'; import { EditorsAndDocumentsExtImpl } from '../editors-and-documents'; import * as notebookCommon from '@theia/notebook/lib/common'; -import { URI } from '@theia/core'; +import { Disposable, URI } from '@theia/core'; import * as typeConverters from '../type-converters'; -import { ModelAddedData, NotebookCellDto, NotebookCellsChangedEventDto, NotebookOutputDto } from '../../common'; +import { ModelAddedData, NotebookCellDto, NotebookCellsChangedEventDto, NotebookModelAddedData, NotebookOutputDto } from '../../common'; import { NotebookRange } from '../types-impl'; import { UriComponents } from '../../common/uri-components'; @@ -160,13 +160,13 @@ export class Cell { } -export class NotebookDocument { +export class NotebookDocument implements Disposable { - private readonly cells: Cell[] = []; + private readonly cells: Cell[]; private readonly notebookType: string; - private notebook: theia.NotebookDocument | undefined; + private notebook?: theia.NotebookDocument; private metadata: Record; private versionId: number = 0; private isDirty: boolean = false; @@ -175,10 +175,13 @@ export class NotebookDocument { constructor( private readonly proxy: rpc.NotebookDocumentsMain, private readonly editorsAndDocuments: EditorsAndDocumentsExtImpl, - // private readonly textDocuments: DocumentsExt, - public readonly uri: theia.Uri + public readonly uri: theia.Uri, + notebookData: NotebookModelAddedData ) { - + this.notebookType = notebookData.viewType; + this.metadata = notebookData.metadata ?? {}; + this.versionId = notebookData.versionId; + this.cells = notebookData.cells.map(cell => new Cell(this, editorsAndDocuments, cell)); } get apiNotebook(): theia.NotebookDocument { @@ -280,17 +283,17 @@ export class NotebookDocument { this.setCellOutputs(rawEvent.index, rawEvent.outputs); relaxedCellChanges.push({ cell: this.cells[rawEvent.index].apiCell, outputs: this.cells[rawEvent.index].apiCell.outputs }); - // } else if (rawEvent.kind === notebookCommon.NotebookCellsChangeType.OutputItem) { - // this._setCellOutputItems(rawEvent.index, rawEvent.outputId, rawEvent.append, rawEvent.outputItems); - // relaxedCellChanges.push({ cell: this.cells[rawEvent.index].apiCell, outputs: this.cells[rawEvent.index].apiCell.outputs }); - // } else if (rawEvent.kind === notebookCommon.NotebookCellsChangeType.ChangeCellLanguage) { - // this.changeCellLanguage(rawEvent.index, rawEvent.language); - // relaxedCellChanges.push({ cell: this.cells[rawEvent.index].apiCell, document: this.cells[rawEvent.index].apiCell.document }); + // } else if (rawEvent.kind === notebookCommon.NotebookCellsChangeType.OutputItem) { + // this._setCellOutputItems(rawEvent.index, rawEvent.outputId, rawEvent.append, rawEvent.outputItems); + // relaxedCellChanges.push({ cell: this.cells[rawEvent.index].apiCell, outputs: this.cells[rawEvent.index].apiCell.outputs }); + // } else if (rawEvent.kind === notebookCommon.NotebookCellsChangeType.ChangeCellLanguage) { + // this.changeCellLanguage(rawEvent.index, rawEvent.language); + // relaxedCellChanges.push({ cell: this.cells[rawEvent.index].apiCell, document: this.cells[rawEvent.index].apiCell.document }); } else if (rawEvent.kind === notebookCommon.NotebookCellsChangeType.ChangeCellContent) { relaxedCellChanges.push({ cell: this.cells[rawEvent.index].apiCell, document: this.cells[rawEvent.index].apiCell.document }); - // } else if (rawEvent.kind === notebookCommon.NotebookCellsChangeType.ChangeCellMime) { - // this._changeCellMime(rawEvent.index, rawEvent.mime); + // } else if (rawEvent.kind === notebookCommon.NotebookCellsChangeType.ChangeCellMime) { + // this._changeCellMime(rawEvent.index, rawEvent.mime); } else if (rawEvent.kind === notebookCommon.NotebookCellsChangeType.ChangeCellMetadata) { this.changeCellMetadata(rawEvent.index, rawEvent.metadata); relaxedCellChanges.push({ cell: this.cells[rawEvent.index].apiCell, metadata: this.cells[rawEvent.index].apiCell.metadata }); @@ -430,4 +433,7 @@ export class NotebookDocument { return this.cells.indexOf(cell); } + dispose(): void { + this.disposed = true; + } } diff --git a/packages/plugin-ext/src/plugin/notebook/notebook-editor.ts b/packages/plugin-ext/src/plugin/notebook/notebook-editor.ts index 671afda95849e..5bc65a14ff60c 100644 --- a/packages/plugin-ext/src/plugin/notebook/notebook-editor.ts +++ b/packages/plugin-ext/src/plugin/notebook/notebook-editor.ts @@ -21,14 +21,16 @@ import * as theia from '@theia/plugin'; import { NotebookDocument } from './notebook-document'; -export class NotebookEditorExtImpl { +export class NotebookEditor { - public static readonly apiEditorsToExtHost = new WeakMap(); + public static readonly apiEditorsToExtHost = new WeakMap(); private selections: theia.NotebookRange[] = []; private visibleRanges: theia.NotebookRange[] = []; private viewColumn?: theia.ViewColumn; + private internalVisible: boolean = false; + private editor?: theia.NotebookEditor; constructor( @@ -82,11 +84,19 @@ export class NotebookEditorExtImpl { }, }; - NotebookEditorExtImpl.apiEditorsToExtHost.set(this.editor, this); + NotebookEditor.apiEditorsToExtHost.set(this.editor, this); } return this.editor; } + get visible(): boolean { + return this.internalVisible; + } + + acceptVisibility(value: boolean): void { + this.internalVisible = value; + } + acceptVisibleRanges(value: theia.NotebookRange[]): void { this.visibleRanges = value; } diff --git a/packages/plugin-ext/src/plugin/notebook/notebook-kernels.ts b/packages/plugin-ext/src/plugin/notebook/notebook-kernels.ts index e8ee3fba5f7e8..6d18380dff851 100644 --- a/packages/plugin-ext/src/plugin/notebook/notebook-kernels.ts +++ b/packages/plugin-ext/src/plugin/notebook/notebook-kernels.ts @@ -25,13 +25,14 @@ import { import { RPCProtocol } from '../../common/rpc-protocol'; import { UriComponents } from '../../common/uri-components'; import * as theia from '@theia/plugin'; -import { CancellationTokenSource, DisposableCollection, Emitter, URI } from '@theia/core'; -import { Disposable } from '@theia/core/shared/vscode-languageserver-protocol'; +import { CancellationTokenSource, Disposable, DisposableCollection, Emitter, URI } from '@theia/core'; import { Cell } from './notebook-document'; import { NotebooksExtImpl } from './notebooks'; -import { NotebookCellOutput, NotebookCellOutputItem } from '../type-converters'; +import { NotebookCellOutput, NotebookCellOutputItem, NotebookKernelSourceAction } from '../type-converters'; import { timeout, Deferred } from '@theia/core/lib/common/promise-util'; import { CellExecutionUpdateType, NotebookCellExecutionState } from '@theia/notebook/lib/common'; +import { CommandRegistryImpl } from '../command-registry'; +import { NotebookRendererScript } from '../types-impl'; interface KernelData { extensionId: string; @@ -60,7 +61,8 @@ export class NotebookKernelsExtImpl implements NotebookKernelsExt { constructor( rpc: RPCProtocol, - private notebooks: NotebooksExtImpl + private readonly notebooks: NotebooksExtImpl, + private readonly commands: CommandRegistryImpl ) { this.proxy = rpc.getProxy(PLUGIN_RPC_CONTEXT.NOTEBOOK_KERNELS_MAIN); } @@ -68,7 +70,7 @@ export class NotebookKernelsExtImpl implements NotebookKernelsExt { private currentHandle = 0; createNotebookController(extensionId: string, id: string, viewType: string, label: string, handler?: (cells: theia.NotebookCell[], - notebook: theia.NotebookDocument, controller: theia.NotebookController) => void | Thenable): theia.NotebookController { + notebook: theia.NotebookDocument, controller: theia.NotebookController) => void | Thenable, rendererScripts?: NotebookRendererScript[]): theia.NotebookController { for (const kernelData of this.kernelData.values()) { if (kernelData.controller.id === id && extensionId === kernelData.extensionId) { @@ -94,6 +96,7 @@ export class NotebookKernelsExtImpl implements NotebookKernelsExt { notebookType: viewType, extensionId: extensionId, label: label || extensionId, + rendererScripts }; // @@ -129,6 +132,7 @@ export class NotebookKernelsExtImpl implements NotebookKernelsExt { get id(): string { return id; }, get notebookType(): string { return data.notebookType; }, onDidChangeSelectedNotebooks: onDidChangeSelection.event, + onDidReceiveMessage: onDidReceiveMessage.event, get label(): string { return data.label; }, @@ -164,6 +168,13 @@ export class NotebookKernelsExtImpl implements NotebookKernelsExt { data.supportsExecutionOrder = value; update(); }, + get rendererScripts(): NotebookRendererScript[] { + return data.rendererScripts ?? []; + }, + set rendererScripts(value) { + data.rendererScripts = value; + update(); + }, get executeHandler(): (cells: theia.NotebookCell[], notebook: theia.NotebookDocument, controller: theia.NotebookController) => void | Thenable { return executeHandler; }, @@ -203,7 +214,12 @@ export class NotebookKernelsExtImpl implements NotebookKernelsExt { updateNotebookAffinity(notebook, priority): void { that.proxy.$updateNotebookPriority(handle, notebook.uri, priority); }, - + async postMessage(message: unknown, editor?: theia.NotebookEditor): Promise { + return Promise.resolve(true); // TODO needs implementation + }, + asWebviewUri(localResource: theia.Uri): theia.Uri { + throw new Error('Method not implemented.'); + } }; this.kernelData.set(handle, { @@ -359,10 +375,18 @@ export class NotebookKernelsExtImpl implements NotebookKernelsExt { } $cellExecutionChanged(uri: UriComponents, cellHandle: number, state: NotebookCellExecutionState | undefined): void { - throw new Error('Method not implemented.'); // Proposed Api though seems needed by jupyter + // Proposed Api though seems needed by jupyter for telemetry } - $provideKernelSourceActions(handle: number, token: CancellationToken): Promise { - throw new Error('Method not implemented.'); + + async $provideKernelSourceActions(handle: number, token: CancellationToken): Promise { + const provider = this.kernelSourceActionProviders.get(handle); + if (provider) { + const disposables = new DisposableCollection(); + const ret = await provider.provideNotebookKernelSourceActions(token); + return (ret ?? []).map(item => NotebookKernelSourceAction.from(item, this.commands.converter, disposables)); + } + return []; + } } diff --git a/packages/plugin-ext/src/plugin/notebook/notebook-renderers.ts b/packages/plugin-ext/src/plugin/notebook/notebook-renderers.ts index abcdd4fc362a5..cbb66bb0e69f0 100644 --- a/packages/plugin-ext/src/plugin/notebook/notebook-renderers.ts +++ b/packages/plugin-ext/src/plugin/notebook/notebook-renderers.ts @@ -22,7 +22,7 @@ import { NotebookRenderersExt, NotebookRenderersMain, PLUGIN_RPC_CONTEXT } from import { RPCProtocol } from '../../common/rpc-protocol'; import { NotebooksExtImpl } from './notebooks'; import * as theia from '@theia/plugin'; -import { NotebookEditorExtImpl } from './notebook-editor'; +import { NotebookEditor } from './notebook-editor'; import { Emitter } from '@theia/core'; export class NotebookRenderersExtImpl implements NotebookRenderersExt { @@ -44,7 +44,7 @@ export class NotebookRenderersExtImpl implements NotebookRenderersExt { onDidReceiveMessage: (listener, thisArg, disposables) => this.getOrCreateEmitterFor(rendererId).event(listener, thisArg, disposables), postMessage: (message, editorOrAlias) => { - const extHostEditor = editorOrAlias && NotebookEditorExtImpl.apiEditorsToExtHost.get(editorOrAlias); + const extHostEditor = editorOrAlias && NotebookEditor.apiEditorsToExtHost.get(editorOrAlias); return this.proxy.$postMessage(extHostEditor?.id, rendererId, message); }, }; diff --git a/packages/plugin-ext/src/plugin/notebook/notebooks.ts b/packages/plugin-ext/src/plugin/notebook/notebooks.ts index 54f485a8cfd0d..ccd7a8b0f8d55 100644 --- a/packages/plugin-ext/src/plugin/notebook/notebooks.ts +++ b/packages/plugin-ext/src/plugin/notebook/notebooks.ts @@ -19,8 +19,13 @@ *--------------------------------------------------------------------------------------------*/ import { CancellationToken, Disposable, DisposableCollection, Emitter, Event, URI } from '@theia/core'; +import { URI as TheiaURI } from '../types-impl'; import * as theia from '@theia/plugin'; -import { NotebookCellStatusBarListDto, NotebookDataDto, NotebookDocumentsAndEditorsDelta, NotebooksExt, NotebooksMain, Plugin, PLUGIN_RPC_CONTEXT } from '../../common'; +import { + CommandRegistryExt, ModelAddedData, NotebookCellStatusBarListDto, NotebookDataDto, + NotebookDocumentsAndEditorsDelta, NotebookDocumentsMain, NotebookEditorAddData, 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'; @@ -28,8 +33,9 @@ import { CommandsConverter } from '../command-registry'; // import { EditorsAndDocumentsExtImpl } from '../editors-and-documents'; import * as typeConverters from '../type-converters'; import { BinaryBuffer } from '@theia/core/lib/common/buffer'; -import { NotebookDocument } from './notebook-document'; -import { NotebookEditorExtImpl } from './notebook-editor'; +import { Cell, NotebookDocument } from './notebook-document'; +import { NotebookEditor } from './notebook-editor'; +import { EditorsAndDocumentsExtImpl } from '../editors-and-documents'; export class NotebooksExtImpl implements NotebooksExt { @@ -47,23 +53,45 @@ export class NotebooksExtImpl implements NotebooksExt { private DidChangeVisibleNotebookEditorsEmitter = new Emitter(); onDidChangeVisibleNotebookEditors = this.DidChangeVisibleNotebookEditorsEmitter.event; - private readonly documents = new Map(); - private readonly editors = new Map(); + private activeNotebookEditor: NotebookEditor | undefined; + get activeApiNotebookEditor(): theia.NotebookEditor | undefined { + return this.activeNotebookEditor?.apiEditor; + } + + private visibleNotebookEditors: NotebookEditor[] = []; + get visibleApiNotebookEditors(): theia.NotebookEditor[] { + return this.visibleNotebookEditors.map(editor => editor.apiEditor); + } + + private readonly documents = new Map(); + private readonly editors = new Map(); private statusBarRegistry = new Cache('NotebookCellStatusBarCache'); private notebookProxy: NotebooksMain; + private notebookDocumentsProxy: NotebookDocumentsMain; constructor( rpc: RPCProtocol, - // private editorsAndDocuments: EditorsAndDocumentsExtImpl + commands: CommandRegistryExt, + private textDocumentsAndEditors: EditorsAndDocumentsExtImpl, ) { this.notebookProxy = rpc.getProxy(PLUGIN_RPC_CONTEXT.NOTEBOOKS_MAIN); + this.notebookDocumentsProxy = rpc.getProxy(PLUGIN_RPC_CONTEXT.NOTEBOOK_DOCUMENTS_MAIN); + + commands.registerArgumentProcessor({ + processArgument: (arg: { uri: URI }) => { + if (arg && arg.uri && this.documents.has(arg.uri.toString())) { + return this.documents.get(arg.uri.toString())?.apiNotebook; + } + return arg; + } + }); } async $provideNotebookCellStatusBarItems(handle: number, uri: UriComponents, index: number, token: CancellationToken): Promise { const provider = this.notebookStatusBarItemProviders.get(handle); const revivedUri = URI.fromComponents(uri); - const document = this.documents.get(revivedUri); + const document = this.documents.get(revivedUri.toString()); if (!document || !provider) { return; } @@ -153,7 +181,7 @@ export class NotebooksExtImpl implements NotebooksExt { }); } - getEditorById(editorId: string): NotebookEditorExtImpl { + getEditorById(editorId: string): NotebookEditor { const editor = this.editors.get(editorId); if (!editor) { throw new Error(`unknown text editor: ${editorId}. known editors: ${[...this.editors.keys()]} `); @@ -161,128 +189,143 @@ export class NotebooksExtImpl implements NotebooksExt { return editor; } - $acceptDocumentAndEditorsDelta(delta: NotebookDocumentsAndEditorsDelta): void { - - // if (delta.removedDocuments) { - // for (const uri of delta.removedDocuments) { - // const revivedUri = URI.revive(uri); - // const document = this.documents.get(revivedUri); - - // if (document) { - // document.dispose(); - // this.documents.delete(revivedUri); - // this.editorsAndDocuments.$acceptEditorsAndDocumentsDelta({ removedDocuments: document.apiNotebook.getCells().map(cell => cell.document.uri) }); - // this.DidCloseNotebookDocumentEmitter.fire(document.apiNotebook); - // } - - // for (const editor of this._editors.values()) { - // if (editor.notebookData.uri.toString() === revivedUri.toString()) { - // this._editors.delete(editor.id); - // } - // } - // } - // } - - // if (delta.addedDocuments) { - - // const addedCellDocuments: IModelAddedData[] = []; - - // for (const modelData of delta.value.addedDocuments) { - // const uri = URI.revive(modelData.uri); - - // if (this.documents.has(uri)) { - // throw new Error(`adding EXISTING notebook ${uri} `); - // } - - // const document = new ExtHostNotebookDocument( - // this._notebookDocumentsProxy, - // this._textDocumentsAndEditors, - // this.documents, - // uri, - // modelData - // ); - - // // add cell document as theia.TextDocument - // addedCellDocuments.push(...modelData.cells.map(cell => ExtHostCell.asModelAddData(document.apiNotebook, cell))); - - // this.documents.get(uri)?.dispose(); - // this.documents.set(uri, document); - // this._textDocumentsAndEditors.$acceptDocumentsAndEditorsDelta({ addedDocuments: addedCellDocuments }); - - // this.DidOpenNotebookDocumentEmitter.fire(document.apiNotebook); - // } - // } - - // if (delta.addedEditors) { - // for (const editorModelData of delta.value.addedEditors) { - // if (this._editors.has(editorModelData.id)) { - // return; - // } - - // const revivedUri = URI.revive(editorModelData.documentUri); - // const document = this.documents.get(revivedUri); - - // if (document) { - // this._createExtHostEditor(document, editorModelData.id, editorModelData); - // } - // } - // } - - // const removedEditors: ExtHostNotebookEditor[] = []; - - // if (delta.removedEditors) { - // for (const editorid of delta.removedEditors) { - // const editor = this._editors.get(editorid); - - // if (editor) { - // this._editors.delete(editorid); - - // if (this._activeNotebookEditor?.id === editor.id) { - // this._activeNotebookEditor = undefined; - // } - - // removedEditors.push(editor); - // } - // } - // } - - // if (delta.visibleEditors) { - // this._visibleNotebookEditors = delta.visibleEditors.map(id => this._editors.get(id)!).filter(editor => !!editor) as ExtHostNotebookEditor[]; - // const visibleEditorsSet = new Set(); - // this._visibleNotebookEditors.forEach(editor => visibleEditorsSet.add(editor.id)); - - // for (const editor of this._editors.values()) { - // const newValue = visibleEditorsSet.has(editor.id); - // editor._acceptVisibility(newValue); - // } - - // this._visibleNotebookEditors = [...this._editors.values()].map(e => e).filter(e => e.visible); - // this.DidChangeVisibleNotebookEditorsEmitter.fire(this.visibleNotebookEditors); - // } - - // if (delta.value.newActiveEditor === null) { - // // clear active notebook as current active editor is non-notebook editor - // this._activeNotebookEditor = undefined; - // } else if (delta.value.newActiveEditor) { - // const activeEditor = this._editors.get(delta.value.newActiveEditor); - // if (!activeEditor) { - // console.error(`FAILED to find active notebook editor ${delta.value.newActiveEditor}`); - // } - // this._activeNotebookEditor = this._editors.get(delta.value.newActiveEditor); - // } - // if (delta.value.newActiveEditor !== undefined) { - // this.DidChangeActiveNotebookEditorEmitter.fire(this._activeNotebookEditor?.apiEditor); - // } + async $acceptDocumentsAndEditorsDelta(delta: NotebookDocumentsAndEditorsDelta): Promise { + if (delta.removedDocuments) { + for (const uri of delta.removedDocuments) { + const revivedUri = URI.fromComponents(uri); + const document = this.documents.get(revivedUri.toString()); + + if (document) { + document.dispose(); + this.documents.delete(revivedUri.toString()); + // this.editorsAndDocuments.$acceptEditorsAndDocumentsDelta({ removedDocuments: document.apiNotebook.getCells().map(cell => cell.document.uri) }); + this.DidCloseNotebookDocumentEmitter.fire(document.apiNotebook); + } + + for (const editor of this.editors.values()) { + if (editor.notebookData.uri.toString() === revivedUri.toString()) { + this.editors.delete(editor.id); + } + } + } + } + + if (delta.addedDocuments) { + + const addedCellDocuments: ModelAddedData[] = []; + + for (const modelData of delta.addedDocuments) { + const uri = TheiaURI.from(modelData.uri); + + if (this.documents.has(uri.toString())) { + throw new Error(`adding EXISTING notebook ${uri} `); + } + + const document = new NotebookDocument( + this.notebookDocumentsProxy, + this.textDocumentsAndEditors, + uri, + modelData + ); + + // add cell document as theia.TextDocument + addedCellDocuments.push(...modelData.cells.map(cell => Cell.asModelAddData(document.apiNotebook, cell))); + + this.documents.get(uri.toString())?.dispose(); + this.documents.set(uri.toString(), document); + this.textDocumentsAndEditors.$acceptEditorsAndDocumentsDelta({ addedDocuments: addedCellDocuments }); + + this.DidOpenNotebookDocumentEmitter.fire(document.apiNotebook); + } + } + + if (delta.addedEditors) { + for (const editorModelData of delta.addedEditors) { + if (this.editors.has(editorModelData.id)) { + return; + } + + const revivedUri = URI.fromComponents(editorModelData.documentUri); + const document = this.documents.get(revivedUri.toString()); + + if (document) { + this.createExtHostEditor(document, editorModelData.id, editorModelData); + } + } + } + + const removedEditors: NotebookEditor[] = []; + + if (delta.removedEditors) { + for (const editorid of delta.removedEditors) { + const editor = this.editors.get(editorid); + + if (editor) { + this.editors.delete(editorid); + + if (this.activeNotebookEditor?.id === editor.id) { + this.activeNotebookEditor = undefined; + } + + removedEditors.push(editor); + } + } + } + + if (delta.visibleEditors) { + this.visibleNotebookEditors = delta.visibleEditors.map(id => this.editors.get(id)!).filter(editor => !!editor) as NotebookEditor[]; + const visibleEditorsSet = new Set(); + this.visibleNotebookEditors.forEach(editor => visibleEditorsSet.add(editor.id)); + + for (const editor of this.editors.values()) { + const newValue = visibleEditorsSet.has(editor.id); + editor.acceptVisibility(newValue); + } + + this.visibleNotebookEditors = [...this.editors.values()].map(e => e).filter(e => e.visible); + this.DidChangeVisibleNotebookEditorsEmitter.fire(this.visibleApiNotebookEditors); + } + + if (delta.newActiveEditor === null) { + // clear active notebook as current active editor is non-notebook editor + this.activeNotebookEditor = undefined; + } else if (delta.newActiveEditor) { + const activeEditor = this.editors.get(delta.newActiveEditor); + if (!activeEditor) { + console.error(`FAILED to find active notebook editor ${delta.newActiveEditor}`); + } + this.activeNotebookEditor = this.editors.get(delta.newActiveEditor); + } + if (delta.newActiveEditor !== undefined) { + this.DidChangeActiveNotebookEditorEmitter.fire(this.activeNotebookEditor?.apiEditor); + } } getNotebookDocument(uri: URI, relaxed: true): NotebookDocument | undefined; getNotebookDocument(uri: URI): NotebookDocument; getNotebookDocument(uri: URI, relaxed?: true): NotebookDocument | undefined { - const result = this.documents.get(uri); + const result = this.documents.get(uri.toString()); if (!result && !relaxed) { throw new Error(`NO notebook document for '${uri}'`); } return result; } + private createExtHostEditor(document: NotebookDocument, editorId: string, data: NotebookEditorAddData): void { + + if (this.editors.has(editorId)) { + throw new Error(`editor with id ALREADY EXSIST: ${editorId}`); + } + + const editor = new NotebookEditor( + editorId, + document, + data.visibleRanges.map(typeConverters.NotebookRange.to), + data.selections.map(typeConverters.NotebookRange.to), + typeof data.viewColumn === 'number' ? typeConverters.ViewColumn.to(data.viewColumn) : undefined + ); + + this.editors.set(editorId, editor); + } + } diff --git a/packages/plugin-ext/src/plugin/plugin-context.ts b/packages/plugin-ext/src/plugin/plugin-context.ts index 7c1940c4b41f7..42dd747e1974f 100644 --- a/packages/plugin-ext/src/plugin/plugin-context.ts +++ b/packages/plugin-ext/src/plugin/plugin-context.ts @@ -179,6 +179,7 @@ import { NotebookCellStatusBarItem, NotebookEdit, NotebookKernelSourceAction, + NotebookRendererScript, TestRunProfileKind, TestTag, TestRunRequest, @@ -270,9 +271,9 @@ export function createAPIFactory( const notificationExt = rpc.set(MAIN_RPC_CONTEXT.NOTIFICATION_EXT, new NotificationExtImpl(rpc)); const editors = rpc.set(MAIN_RPC_CONTEXT.TEXT_EDITORS_EXT, new TextEditorsExtImpl(rpc, editorsAndDocumentsExt)); const documents = rpc.set(MAIN_RPC_CONTEXT.DOCUMENTS_EXT, new DocumentsExtImpl(rpc, editorsAndDocumentsExt)); - const notebooksExt = rpc.set(MAIN_RPC_CONTEXT.NOTEBOOKS_EXT, new NotebooksExtImpl(rpc)); + const notebooksExt = rpc.set(MAIN_RPC_CONTEXT.NOTEBOOKS_EXT, new NotebooksExtImpl(rpc, commandRegistry, editorsAndDocumentsExt)); const notebookRenderers = rpc.set(MAIN_RPC_CONTEXT.NOTEBOOK_RENDERERS_EXT, new NotebookRenderersExtImpl(rpc, notebooksExt)); - const notebookKernels = rpc.set(MAIN_RPC_CONTEXT.NOTEBOOK_KERNELS_EXT, new NotebookKernelsExtImpl(rpc, notebooksExt)); + const notebookKernels = rpc.set(MAIN_RPC_CONTEXT.NOTEBOOK_KERNELS_EXT, new NotebookKernelsExtImpl(rpc, notebooksExt, commandRegistry)); const statusBarMessageRegistryExt = new StatusBarMessageRegistryExt(rpc); const terminalExt = rpc.set(MAIN_RPC_CONTEXT.TERMINAL_EXT, new TerminalServiceExtImpl(rpc)); const outputChannelRegistryExt = rpc.set(MAIN_RPC_CONTEXT.OUTPUT_CHANNEL_REGISTRY_EXT, new OutputChannelRegistryExtImpl(rpc)); @@ -440,9 +441,9 @@ export function createAPIFactory( return Disposable.NULL; }, get activeNotebookEditor(): theia.NotebookEditor | undefined { - return undefined; + return notebooksExt.activeApiNotebookEditor; }, onDidChangeActiveNotebookEditor(listener, thisArg?, disposables?) { - return Disposable.NULL; + return notebooksExt.onDidChangeActiveNotebookEditor(listener, thisArg, disposables); }, onDidChangeNotebookEditorSelection(listener, thisArg?, disposables?) { return Disposable.NULL; @@ -1155,9 +1156,10 @@ export function createAPIFactory( label, handler?: (cells: theia.NotebookCell[], notebook: theia.NotebookDocument, - controller: theia.NotebookController) => void | Thenable + controller: theia.NotebookController) => void | Thenable, + rendererScripts?: NotebookRendererScript[] ) { - return notebookKernels.createNotebookController(plugin.model.id, id, notebookType, label, handler); + return notebookKernels.createNotebookController(plugin.model.id, id, notebookType, label, handler, rendererScripts); }, createRendererMessaging(rendererId) { return notebookRenderers.createRendererMessaging(rendererId); @@ -1341,6 +1343,7 @@ export function createAPIFactory( NotebookRange, NotebookEdit, NotebookKernelSourceAction, + NotebookRendererScript, TestRunProfileKind, TestTag, TestRunRequest, @@ -1393,6 +1396,8 @@ export interface ExtensionPlugin extends theia.Plugin { } export class Plugin implements theia.Plugin { + #pluginManager: PluginManager; + id: string; pluginPath: string; pluginUri: theia.Uri; @@ -1400,7 +1405,9 @@ export class Plugin implements theia.Plugin { packageJSON: any; pluginType: theia.PluginType; - constructor(protected readonly pluginManager: PluginManager, plugin: InternalPlugin) { + constructor(pluginManager: PluginManager, plugin: InternalPlugin) { + this.#pluginManager = pluginManager; + this.id = plugin.model.id; this.pluginPath = plugin.pluginFolder; this.packageJSON = plugin.rawModel; @@ -1415,26 +1422,29 @@ export class Plugin implements theia.Plugin { } get isActive(): boolean { - return this.pluginManager.isActive(this.id); + return this.#pluginManager.isActive(this.id); } get exports(): T { - return this.pluginManager.getPluginExport(this.id); + return this.#pluginManager.getPluginExport(this.id); } activate(): PromiseLike { - return this.pluginManager.activatePlugin(this.id).then(() => this.exports); + return this.#pluginManager.activatePlugin(this.id).then(() => this.exports); } } export class PluginExt extends Plugin implements ExtensionPlugin { + #pluginManager: PluginManager; + extensionPath: string; extensionUri: theia.Uri; extensionKind: ExtensionKind; isFromDifferentExtensionHost: boolean; - constructor(protected override readonly pluginManager: PluginManager, plugin: InternalPlugin, isFromDifferentExtensionHost = false) { + constructor(pluginManager: PluginManager, plugin: InternalPlugin, isFromDifferentExtensionHost = false) { super(pluginManager, plugin); + this.#pluginManager = pluginManager; this.extensionPath = this.pluginPath; this.extensionUri = this.pluginUri; @@ -1443,14 +1453,14 @@ export class PluginExt extends Plugin implements ExtensionPlugin { } override get isActive(): boolean { - return this.pluginManager.isActive(this.id); + return this.#pluginManager.isActive(this.id); } override get exports(): T { - return this.pluginManager.getPluginExport(this.id); + return this.#pluginManager.getPluginExport(this.id); } override activate(): PromiseLike { - return this.pluginManager.activatePlugin(this.id).then(() => this.exports); + return this.#pluginManager.activatePlugin(this.id).then(() => this.exports); } } diff --git a/packages/plugin-ext/src/plugin/type-converters.ts b/packages/plugin-ext/src/plugin/type-converters.ts index aecac5ac0c7a4..9df2bb1687ba2 100644 --- a/packages/plugin-ext/src/plugin/type-converters.ts +++ b/packages/plugin-ext/src/plugin/type-converters.ts @@ -1565,3 +1565,17 @@ export namespace NotebookRange { return new types.NotebookRange(range.start, range.end); } } + +export namespace NotebookKernelSourceAction { + export function from(item: theia.NotebookKernelSourceAction, commandsConverter: CommandsConverter, disposables: DisposableCollection): rpc.NotebookKernelSourceActionDto { + const command = typeof item.command === 'string' ? { title: '', command: item.command } : item.command; + + return { + command: commandsConverter.toSafeCommand(command, disposables), + label: item.label, + description: item.description, + detail: item.detail, + documentation: item.documentation + }; + } +} diff --git a/packages/plugin-ext/src/plugin/types-impl.ts b/packages/plugin-ext/src/plugin/types-impl.ts index 2fab56aeb6525..46780d021893b 100644 --- a/packages/plugin-ext/src/plugin/types-impl.ts +++ b/packages/plugin-ext/src/plugin/types-impl.ts @@ -1370,6 +1370,18 @@ export class NotebookEdit implements theia.NotebookEdit { } } +export class NotebookRendererScript implements theia.NotebookRendererScript { + provides: readonly string[]; + + constructor( + public uri: theia.Uri, + provides?: string | readonly string[] + ) { + this.provides = Array.isArray(provides) ? provides : [provides]; + }; + +} + @es5ClassCompat export class ParameterInformation { label: string | [number, number]; diff --git a/packages/plugin/src/theia.d.ts b/packages/plugin/src/theia.d.ts index f5cdac7e84f51..c088c11474ed1 100644 --- a/packages/plugin/src/theia.d.ts +++ b/packages/plugin/src/theia.d.ts @@ -30,6 +30,7 @@ import './theia.proposed.extensionsAny'; import './theia.proposed.externalUriOpener'; import './theia.proposed.notebookCellExecutionState'; import './theia.proposed.notebookKernelSource'; +import './theia.proposed.notebookMessaging'; import './theia.proposed.findTextInFiles'; import './theia.proposed.fsChunks'; import './theia.proposed.profileContentHandlers'; diff --git a/packages/plugin/src/theia.proposed.notebookCellExecutionState.d.ts b/packages/plugin/src/theia.proposed.notebookCellExecutionState.d.ts index 92ea46487f92e..ab8279581a8a8 100644 --- a/packages/plugin/src/theia.proposed.notebookCellExecutionState.d.ts +++ b/packages/plugin/src/theia.proposed.notebookCellExecutionState.d.ts @@ -1,5 +1,5 @@ // ***************************************************************************** -// Copyright (C) 2018 Red Hat, Inc. and others. +// Copyright (C) 2023 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 diff --git a/packages/plugin/src/theia.proposed.notebookKernelSource.d.ts b/packages/plugin/src/theia.proposed.notebookKernelSource.d.ts index 86ca3061e742c..5ef2fcd67e35f 100644 --- a/packages/plugin/src/theia.proposed.notebookKernelSource.d.ts +++ b/packages/plugin/src/theia.proposed.notebookKernelSource.d.ts @@ -1,5 +1,5 @@ // ***************************************************************************** -// Copyright (C) 2018 Red Hat, Inc. and others. +// Copyright (C) 2023 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 diff --git a/packages/plugin/src/theia.proposed.notebookMessaging.d.ts b/packages/plugin/src/theia.proposed.notebookMessaging.d.ts new file mode 100644 index 0000000000000..c2f1ee0330acb --- /dev/null +++ b/packages/plugin/src/theia.proposed.notebookMessaging.d.ts @@ -0,0 +1,84 @@ +// ***************************************************************************** +// Copyright (C) 2023 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. + *--------------------------------------------------------------------------------------------*/ + +declare module '@theia/plugin' { + + // https://github.com/microsoft/vscode/issues/123601 + + /** + * Represents a script that is loaded into the notebook renderer before rendering output. This allows + * to provide and share functionality for notebook markup and notebook output renderers. + */ + export class NotebookRendererScript { + + /** + * APIs that the preload provides to the renderer. These are matched + * against the `dependencies` and `optionalDependencies` arrays in the + * notebook renderer contribution point. + */ + provides: readonly string[]; + + /** + * URI of the JavaScript module to preload. + * + * This module must export an `activate` function that takes a context object that contains the notebook API. + */ + uri: Uri; + + /** + * @param uri URI of the JavaScript module to preload + * @param provides Value for the `provides` property + */ + constructor(uri: Uri, provides?: string | readonly string[]); + } + + export interface NotebookController { + + // todo@API allow add, not remove + readonly rendererScripts: NotebookRendererScript[]; + + /** + * An event that fires when a {@link NotebookController.rendererScripts renderer script} has send a message to + * the controller. + */ + readonly onDidReceiveMessage: Event<{ readonly editor: NotebookEditor; readonly message: unknown }>; + + /** + * Send a message to the renderer of notebook editors. + * + * Note that only editors showing documents that are bound to this controller + * are receiving the message. + * + * @param message The message to send. + * @param editor A specific editor to send the message to. When `undefined` all applicable editors are receiving the message. + * @returns A promise that resolves to a boolean indicating if the message has been send or not. + */ + postMessage(message: unknown, editor?: NotebookEditor): Thenable; + + asWebviewUri(localResource: Uri): Uri; + } + + export namespace notebooks { + + export function createNotebookController(id: string, viewType: string, label: string, handler?: (cells: NotebookCell[], notebook: NotebookDocument, + controller: NotebookController) => void | Thenable, rendererScripts?: NotebookRendererScript[]): NotebookController; + } +}