diff --git a/packages/notebook/package.json b/packages/notebook/package.json index c92f573b77a4f..f0a42ea6d15bb 100644 --- a/packages/notebook/package.json +++ b/packages/notebook/package.json @@ -5,7 +5,8 @@ "dependencies": { "@theia/core": "1.38.0", "@theia/filesystem": "1.38.0", - "@theia/monaco": "1.38.0" + "@theia/monaco": "1.38.0", + "uuid": "^8.3.2" }, "publishConfig": { "access": "public" diff --git a/packages/notebook/src/browser/contributions/notebook-actions-contribution.ts b/packages/notebook/src/browser/contributions/notebook-actions-contribution.ts index 561560b71178b..fc6216c7158ac 100644 --- a/packages/notebook/src/browser/contributions/notebook-actions-contribution.ts +++ b/packages/notebook/src/browser/contributions/notebook-actions-contribution.ts @@ -20,13 +20,19 @@ 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'; export namespace NotebookCommands { - export const Add_NEW_CELL_COMMAND = Command.toDefaultLocalizedCommand({ + export const ADD_NEW_CELL_COMMAND = Command.toDefaultLocalizedCommand({ id: 'notebook.add-new-cell', iconClass: codicon('add') }); + export const SELECT_KERNEL_COMMAND = Command.toDefaultLocalizedCommand({ + id: 'notebook.selectKernel', + category: 'Notebook', + iconClass: codicon('server-environment') + }); } @injectable() @@ -35,13 +41,22 @@ export class NotebookActionsContribution implements CommandContribution { @inject(NotebookService) protected notebookService: NotebookService; + @inject(NotebookKernelQuickPickService) + protected notebookKernelQuickPickService: NotebookKernelQuickPickService; + registerCommands(commands: CommandRegistry): void { - commands.registerCommand(NotebookCommands.Add_NEW_CELL_COMMAND, { + commands.registerCommand(NotebookCommands.ADD_NEW_CELL_COMMAND, { execute: (notebookModel: NotebookModel, cellKind: CellKind, index?: number) => { notebookModel.insertNewCell(index ?? notebookModel.cells.length, [this.notebookService.createEmptyCellModel(notebookModel, cellKind)]); } }); + + commands.registerCommand(NotebookCommands.SELECT_KERNEL_COMMAND, { + execute: (notebookModel: NotebookModel) => { + this.notebookKernelQuickPickService.showQuickPick(notebookModel); + } + }); } } diff --git a/packages/notebook/src/browser/contributions/notebook-cell-actions-contribution.ts b/packages/notebook/src/browser/contributions/notebook-cell-actions-contribution.ts index bc9e5b78ca523..c9031febe7866 100644 --- a/packages/notebook/src/browser/contributions/notebook-cell-actions-contribution.ts +++ b/packages/notebook/src/browser/contributions/notebook-cell-actions-contribution.ts @@ -21,6 +21,7 @@ import { NotebookModel } from '../view-model/notebook-model'; import { NotebookCellModel } from '../view-model/notebook-cell-model'; import { NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_TYPE, NotebookContextKeys } from './notebook-context-keys'; import { ContextKeyService } from '@theia/core/lib/browser/context-key-service'; +import { NotebookExecutionService } from '../service/notebook-execution-service'; export namespace NotebookCellCommands { export const EDIT_COMMAND = Command.toDefaultLocalizedCommand({ @@ -39,6 +40,10 @@ export namespace NotebookCellCommands { id: 'notebook.cell.split-cell', iconClass: codicon('split-vertical'), }); + export const EXECUTE_SINGLE_CELL_COMMAND = Command.toDefaultLocalizedCommand({ + id: 'notebook.cell.execute-cell', + iconClass: codicon('play'), + }); } @injectable() @@ -47,13 +52,8 @@ export class NotebookCellActionContribution implements MenuContribution, Command @inject(ContextKeyService) protected contextKeyService: ContextKeyService; - protected runDeleteAction(notebookModel: NotebookModel, cell: NotebookCellModel): void { - notebookModel.removeCell(notebookModel.cells.indexOf(cell), 1); - } - - protected requestCellEdit(notebookModel: NotebookModel, cell: NotebookCellModel): void { - cell.requestEdit(); - } + @inject(NotebookExecutionService) + protected notebookExecutionService: NotebookExecutionService; @postConstruct() protected init(): void { @@ -74,6 +74,12 @@ export class NotebookCellActionContribution implements MenuContribution, Command when: `${NOTEBOOK_CELL_TYPE} == 'markdown' && ${NOTEBOOK_CELL_MARKDOWN_EDIT_MODE}`, order: '10' }); + menus.registerMenuAction([menuId], { + commandId: NotebookCellCommands.EXECUTE_SINGLE_CELL_COMMAND.id, + icon: NotebookCellCommands.EXECUTE_SINGLE_CELL_COMMAND.iconClass, + when: `${NOTEBOOK_CELL_TYPE} == 'code'`, + order: '10' + }); menus.registerMenuAction([menuId], { commandId: NotebookCellCommands.SPLIT_CELL_COMMAND.id, icon: NotebookCellCommands.SPLIT_CELL_COMMAND.iconClass, @@ -86,7 +92,7 @@ export class NotebookCellActionContribution implements MenuContribution, Command }); const moreMenuPath = [menuId, 'more']; - menus.registerSubmenu(moreMenuPath, 'more', { icon: codicon('ellipsis'), role: CompoundMenuNodeRole.Submenu, order: '100' }); + menus.registerSubmenu(moreMenuPath, 'more', { icon: codicon('ellipsis'), role: CompoundMenuNodeRole.Submenu, order: '999' }); menus.registerMenuAction(moreMenuPath, { commandId: NotebookCellCommands.EDIT_COMMAND.id, label: 'test submenu item', @@ -94,10 +100,15 @@ export class NotebookCellActionContribution implements MenuContribution, Command } registerCommands(commands: CommandRegistry): void { - commands.registerCommand(NotebookCellCommands.EDIT_COMMAND, { execute: this.requestCellEdit }); + commands.registerCommand(NotebookCellCommands.EDIT_COMMAND, { execute: (_, cell: NotebookCellModel) => cell.requestEdit() }); commands.registerCommand(NotebookCellCommands.STOP_EDIT_COMMAND, { execute: (_, cell: NotebookCellModel) => cell.requestStopEdit() }); - commands.registerCommand(NotebookCellCommands.DELETE_COMMAND, { execute: this.runDeleteAction }); + commands.registerCommand(NotebookCellCommands.DELETE_COMMAND, { + execute: (notebookModel: NotebookModel, cell: NotebookCellModel) => notebookModel.removeCell(notebookModel.cells.indexOf(cell), 1) + }); commands.registerCommand(NotebookCellCommands.SPLIT_CELL_COMMAND); - } + commands.registerCommand(NotebookCellCommands.EXECUTE_SINGLE_CELL_COMMAND, { + execute: (notebookModel: NotebookModel, cell: NotebookCellModel) => this.notebookExecutionService.executeNotebookCells(notebookModel, [cell]) + }); + } } diff --git a/packages/notebook/src/browser/index.ts b/packages/notebook/src/browser/index.ts index a040f88a5c51a..cf1a85722f5a6 100644 --- a/packages/notebook/src/browser/index.ts +++ b/packages/notebook/src/browser/index.ts @@ -18,4 +18,5 @@ export * from './notebook-type-registry'; export * from './notebook-editor-widget'; export * from './service/notebook-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-frontend-module.ts b/packages/notebook/src/browser/notebook-frontend-module.ts index 511db65d7b001..4e71486aeab63 100644 --- a/packages/notebook/src/browser/notebook-frontend-module.ts +++ b/packages/notebook/src/browser/notebook-frontend-module.ts @@ -32,6 +32,10 @@ import { createNotebookEditorWidgetContainer, NotebookEditorContainerFactory, No import { NotebookCodeCellRenderer } from './view/notebook-code-cell-view'; import { NotebookMarkdownCellRenderer } from './view/notebook-markdown-cell-view'; import { NotebookActionsContribution } from './contributions/notebook-actions-contribution'; +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'; export default new ContainerModule(bind => { bindContributionProvider(bind, Symbol('notebooks')); @@ -45,6 +49,10 @@ export default new ContainerModule(bind => { bind(NotebookCellToolbarFactory).toSelf().inSingletonScope(); bind(NotebookService).toSelf().inSingletonScope(); + bind(NotebookExecutionService).toSelf().inSingletonScope(); + bind(NotebookExecutionStateService).toSelf().inSingletonScope(); + bind(NotebookKernelService).toSelf().inSingletonScope(); + bind(NotebookKernelQuickPickService).toSelf().inSingletonScope(); bind(NotebookCellResourceResolver).toSelf().inSingletonScope(); bind(ResourceResolver).toService(NotebookCellResourceResolver); diff --git a/packages/notebook/src/browser/service/notebook-execution-service.ts b/packages/notebook/src/browser/service/notebook-execution-service.ts new file mode 100644 index 0000000000000..500d9ff1b6b13 --- /dev/null +++ b/packages/notebook/src/browser/service/notebook-execution-service.ts @@ -0,0 +1,120 @@ +// ***************************************************************************** +// 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 } from '@theia/core/shared/inversify'; +import { CellExecution, NotebookExecutionStateService } from '../service/notebook-execution-state-service'; +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'; + +export interface CellExecutionParticipant { + onWillExecuteCell(executions: CellExecution[]): Promise; +} + +@injectable() +export class NotebookExecutionService { + + @inject(NotebookExecutionStateService) + protected notebookExecutionStateService: NotebookExecutionStateService; + + @inject(NotebookKernelService) + protected notebookKernelService: NotebookKernelService; + + private readonly cellExecutionParticipants = new Set(); + + async executeNotebookCells(notebook: NotebookModel, cells: Iterable): Promise { + const cellsArr = Array.from(cells) + .filter(c => c.cellKind === CellKind.Code); + if (!cellsArr.length) { + return; + } + + console.debug(`NotebookExecutionService#executeNotebookCells ${JSON.stringify(cellsArr.map(c => c.handle))}`); + + // create cell executions + const cellExecutions: [NotebookCellModel, CellExecution][] = []; + for (const cell of cellsArr) { + const cellExe = this.notebookExecutionStateService.getCellExecution(cell.uri); + if (!cellExe) { + cellExecutions.push([cell, this.notebookExecutionStateService.createCellExecution(notebook.uri, cell.handle)]); + } + } + + const kernel = this.notebookKernelService.getSelectedOrSuggestedKernel(notebook); + + if (!kernel) { + // clear all pending cell executions + cellExecutions.forEach(cellExe => cellExe[1].complete({})); + return; + } + + // filter cell executions based on selected kernel + const validCellExecutions: CellExecution[] = []; + for (const [cell, cellExecution] of cellExecutions) { + if (!kernel.supportedLanguages.includes(cell.language)) { + cellExecution.complete({}); + } else { + validCellExecutions.push(cellExecution); + } + } + + // request execution + if (validCellExecutions.length > 0) { + await this.runExecutionParticipants(validCellExecutions); + + this.notebookKernelService.selectKernelForNotebook(kernel, notebook); + await kernel.executeNotebookCellsRequest(notebook.uri, validCellExecutions.map(c => c.cellHandle)); + // the connecting state can change before the kernel resolves executeNotebookCellsRequest + const unconfirmed = validCellExecutions.filter(exe => exe.state === NotebookCellExecutionState.Unconfirmed); + if (unconfirmed.length) { + console.debug(`NotebookExecutionService#executeNotebookCells completing unconfirmed executions ${JSON.stringify(unconfirmed.map(exe => exe.cellHandle))}`); + unconfirmed.forEach(exe => exe.complete({})); + } + } + } + + registerExecutionParticipant(participant: CellExecutionParticipant): Disposable { + this.cellExecutionParticipants.add(participant); + return Disposable.create(() => this.cellExecutionParticipants.delete(participant)); + } + + private async runExecutionParticipants(executions: CellExecution[]): Promise { + for (const participant of this.cellExecutionParticipants) { + await participant.onWillExecuteCell(executions); + } + return; + } + + async cancelNotebookCellHandles(notebook: NotebookModel, cells: Iterable): Promise { + const cellsArr = Array.from(cells); + console.debug(`NotebookExecutionService#cancelNotebookCellHandles ${JSON.stringify(cellsArr)}`); + const kernel = this.notebookKernelService.getSelectedOrSuggestedKernel(notebook); + if (kernel) { + await kernel.cancelNotebookCellExecution(notebook.uri, cellsArr); + + } + } + + async cancelNotebookCells(notebook: NotebookModel, cells: Iterable): Promise { + this.cancelNotebookCellHandles(notebook, Array.from(cells, cell => cell.handle)); + } +} diff --git a/packages/notebook/src/browser/service/notebook-execution-state-service.ts b/packages/notebook/src/browser/service/notebook-execution-state-service.ts new file mode 100644 index 0000000000000..1fa0aff515f4a --- /dev/null +++ b/packages/notebook/src/browser/service/notebook-execution-state-service.ts @@ -0,0 +1,325 @@ +// ***************************************************************************** +// 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 { 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, + CellUri, CellPartialInternalMetadataEditByHandle, NotebookCellExecutionState, CellEditOperation, NotebookCellInternalMetadata +} from '../../common'; +import { NotebookModel } from '../view-model/notebook-model'; +import { v4 } from 'uuid'; + +export type CellExecuteUpdate = CellExecuteOutputEdit | CellExecuteOutputItemEdit | CellExecutionStateUpdate; + +export interface CellExecutionComplete { + runEndTime?: number; + lastRunSuccess?: boolean; +} + +export interface CellExecutionStateUpdate { + editType: CellExecutionUpdateType.ExecutionState; + executionOrder?: number; + runStartTime?: number; + didPause?: boolean; + isPaused?: boolean; +} + +export interface ICellExecutionStateUpdate { + editType: CellExecutionUpdateType.ExecutionState; + executionOrder?: number; + runStartTime?: number; + didPause?: boolean; + isPaused?: boolean; +} + +export interface ICellExecutionStateUpdate { + editType: CellExecutionUpdateType.ExecutionState; + executionOrder?: number; + runStartTime?: number; + didPause?: boolean; + isPaused?: boolean; +} + +export interface ICellExecutionComplete { + runEndTime?: number; + lastRunSuccess?: boolean; +} +export enum NotebookExecutionType { + cell, + notebook +} + +export interface NotebookFailStateChangedEvent { + visible: boolean; + notebook: URI; +} + +export interface FailedCellInfo { + cellHandle: number; + disposable: Disposable; + visible: boolean; +} + +@injectable() +export class NotebookExecutionStateService implements Disposable { + + @inject(NotebookService) + protected notebookService: NotebookService; + + protected readonly executions = new Map(); + + private readonly onDidChangeExecutionEmitter = new Emitter(); + onDidChangeExecution = this.onDidChangeExecutionEmitter.event; + + private readonly onDidChangeLastRunFailStateEmitter = new Emitter(); + onDidChangeLastRunFailState = this.onDidChangeLastRunFailStateEmitter.event; + + createCellExecution(notebookUri: URI, cellHandle: number): CellExecution { + const notebook = this.notebookService.getNotebookEditorModel(notebookUri); + + if (!notebook) { + throw new Error(`Notebook not found: ${notebookUri.toString()}`); + } + + let execution = this.executions.get(`${notebookUri}/${cellHandle}`); + + if (!execution) { + execution = this.createNotebookCellExecution(notebook, cellHandle); + this.executions.set(`${notebookUri}/${cellHandle}`, execution); + execution.initialize(); + this.onDidChangeExecutionEmitter.fire(new CellExecutionStateChangedEvent(notebookUri, cellHandle, execution)); + } + + return execution; + + } + + private createNotebookCellExecution(notebook: NotebookModel, cellHandle: number): CellExecution { + const notebookUri = notebook.uri; + const execution = new CellExecution(cellHandle, notebook); + execution.onDidUpdate(() => this.onDidChangeExecutionEmitter.fire(new CellExecutionStateChangedEvent(notebookUri, cellHandle, execution))); + execution.onDidComplete(lastRunSuccess => this.onCellExecutionDidComplete(notebookUri, cellHandle, execution, lastRunSuccess)); + + return execution; + } + + private onCellExecutionDidComplete(notebookUri: URI, cellHandle: number, exe: CellExecution, lastRunSuccess?: boolean): void { + const notebookExecutions = this.executions.get(`${notebookUri}/${cellHandle}`); + if (!notebookExecutions) { + console.debug(`NotebookExecutionStateService#_onCellExecutionDidComplete - unknown notebook ${notebookUri.toString()}`); + return; + } + + exe.dispose(); + this.executions.delete(`${notebookUri}/${cellHandle}`); + + this.onDidChangeExecutionEmitter.fire(new CellExecutionStateChangedEvent(notebookUri, cellHandle)); + } + + getCellExecution(cellUri: URI): CellExecution | undefined { + const parsed = CellUri.parse(cellUri); + if (!parsed) { + throw new Error(`Not a cell URI: ${cellUri}`); + } + + return this.executions.get(`${parsed.notebook.toString()}/${parsed.handle}`); + } + + dispose(): void { + this.onDidChangeExecutionEmitter.dispose(); + this.onDidChangeLastRunFailStateEmitter.dispose(); + + this.executions.forEach(cellExecution => cellExecution.dispose()); + } + +} + +export class CellExecution implements Disposable { + private readonly onDidUpdateEmitter = new Emitter(); + readonly onDidUpdate = this.onDidUpdateEmitter.event; + + private readonly onDidCompleteEmitter = new Emitter(); + readonly onDidComplete = this.onDidCompleteEmitter.event; + + private _state: NotebookCellExecutionState = NotebookCellExecutionState.Unconfirmed; + get state(): NotebookCellExecutionState { + return this._state; + } + + get notebookURI(): URI { + return this.notebook.uri; + } + + private _didPause = false; + get didPause(): boolean { + return this._didPause; + } + + private _isPaused = false; + get isPaused(): boolean { + return this._isPaused; + } + + constructor( + readonly cellHandle: number, + private readonly notebook: NotebookModel, + ) { + console.debug(`CellExecution#ctor ${this.getCellLog()}`); + } + + initialize(): void { + const startExecuteEdit: CellPartialInternalMetadataEditByHandle = { + editType: CellEditType.PartialInternalMetadata, + handle: this.cellHandle, + internalMetadata: { + executionId: v4(), + runStartTime: undefined, + runEndTime: undefined, + lastRunSuccess: undefined, + executionOrder: undefined, + renderDuration: undefined, + } + }; + this.applyExecutionEdits([startExecuteEdit]); + } + + private getCellLog(): string { + return `${this.notebookURI.toString()}, ${this.cellHandle}`; + } + + confirm(): void { + console.debug(`CellExecution#confirm ${this.getCellLog()}`); + this._state = NotebookCellExecutionState.Pending; + this.onDidUpdateEmitter.fire(); + } + + update(updates: CellExecuteUpdate[]): void { + if (updates.some(u => u.editType === CellExecutionUpdateType.ExecutionState)) { + this._state = NotebookCellExecutionState.Executing; + } + + if (!this._didPause && updates.some(u => u.editType === CellExecutionUpdateType.ExecutionState && u.didPause)) { + this._didPause = true; + } + + const lastIsPausedUpdate = [...updates].reverse().find(u => u.editType === CellExecutionUpdateType.ExecutionState && typeof u.isPaused === 'boolean'); + if (lastIsPausedUpdate) { + this._isPaused = (lastIsPausedUpdate as ICellExecutionStateUpdate).isPaused!; + } + + const cellModel = this.notebook.cells.find(c => c.handle === this.cellHandle); + if (!cellModel) { + console.debug(`CellExecution#update, updating cell not in notebook: ${this.notebook.uri.toString()}, ${this.cellHandle}`); + } else { + const edits = updates.map(update => updateToEdit(update, this.cellHandle)); + this.applyExecutionEdits(edits); + } + + if (updates.some(u => u.editType === CellExecutionUpdateType.ExecutionState)) { + this.onDidUpdateEmitter.fire(); + } + + } + + complete(completionData: CellExecutionComplete): void { + const cellModel = this.notebook.cells.find(c => c.handle === this.cellHandle); + if (!cellModel) { + console.debug(`CellExecution#complete, completing cell not in notebook: ${this.notebook.uri.toString()}, ${this.cellHandle}`); + } else { + const edit: CellEditOperation = { + editType: CellEditType.PartialInternalMetadata, + handle: this.cellHandle, + internalMetadata: { + lastRunSuccess: completionData.lastRunSuccess, + // eslint-disable-next-line no-null/no-null + runStartTime: this._didPause ? null : cellModel.internalMetadata.runStartTime, + // eslint-disable-next-line no-null/no-null + runEndTime: this._didPause ? null : completionData.runEndTime, + } + }; + this.applyExecutionEdits([edit]); + } + + this.onDidCompleteEmitter.fire(completionData.lastRunSuccess); + + } + + dispose(): void { + this.onDidUpdateEmitter.dispose(); + this.onDidCompleteEmitter.dispose(); + } + + private applyExecutionEdits(edits: CellEditOperation[]): void { + this.notebook.applyEdits(edits); + } +} + +class CellExecutionStateChangedEvent { + readonly type = NotebookExecutionType.cell; + constructor( + readonly notebook: URI, + readonly cellHandle: number, + readonly changed?: CellExecution + ) { } + + affectsCell(cell: URI): boolean { + const parsedUri = CellUri.parse(cell); + return !!parsedUri && this.notebook.toString() === parsedUri.notebook.toString() && this.cellHandle === parsedUri.handle; + } + + affectsNotebook(notebook: URI): boolean { + return this.notebook.toString() === notebook.toString(); + } +} + +function updateToEdit(update: CellExecuteUpdate, cellHandle: number): CellEditOperation { + if (update.editType === CellExecutionUpdateType.Output) { + // return { + // editType: CellEditType.Output, + // handle: update.cellHandle, + // append: update.append, + // outputs: update.outputs, + // }; + } else if (update.editType === CellExecutionUpdateType.OutputItems) { + // return { + // editType: CellEditType.OutputItems, + // items: update.items, + // append: update.append, + // }; + } else if (update.editType === CellExecutionUpdateType.ExecutionState) { + const newInternalMetadata: Partial = {}; + if (typeof update.executionOrder !== 'undefined') { + newInternalMetadata.executionOrder = update.executionOrder; + } + if (typeof update.runStartTime !== 'undefined') { + newInternalMetadata.runStartTime = update.runStartTime; + } + return { + editType: CellEditType.PartialInternalMetadata, + handle: cellHandle, + internalMetadata: newInternalMetadata + }; + } + + throw new Error('Unknown cell update type'); +} diff --git a/packages/notebook/src/browser/service/notebook-execution-state-servicel.ts b/packages/notebook/src/browser/service/notebook-execution-state-servicel.ts deleted file mode 100644 index 8995568666ffd..0000000000000 --- a/packages/notebook/src/browser/service/notebook-execution-state-servicel.ts +++ /dev/null @@ -1,33 +0,0 @@ -// ***************************************************************************** -// 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 -// ***************************************************************************** - -import { URI } from '@theia/core'; - -export interface INotebookCellExecution { - readonly notebook: URI; - readonly cellHandle: number; - readonly state: NotebookCellExecutionState; - readonly didPause: boolean; - readonly isPaused: boolean; - - confirm(): void; - update(updates: CellExecuteUpdate[]): void; - complete(complete: CellExecutionComplete): void; -} - -export class NotebookExecutionStateService { - createCellExecution(notebook: URI, cellHandle: number): NotebookCellExecution; -} 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 new file mode 100644 index 0000000000000..9edc0b96e638f --- /dev/null +++ b/packages/notebook/src/browser/service/notebook-kernel-quick-pick-service.ts @@ -0,0 +1,43 @@ + +// ***************************************************************************** +// 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 { QuickPickService } from '@theia/core'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { NotebookKernelService, NotebookKernel } from './notebook-kernel-service'; +import { NotebookModel } from '../view-model/notebook-model'; + +@injectable() +export class NotebookKernelQuickPickService { + + @inject(QuickPickService) + protected quickPickService: QuickPickService; + + @inject(NotebookKernelService) + protected notebookKernelService: NotebookKernelService; + + async showQuickPick(notebook: NotebookModel): Promise { + return (await this.quickPickService.show(this.getKernels(notebook).map(kernel => ({ id: kernel.id, label: kernel.label }))))?.id; + } + + protected getKernels(notebook: NotebookModel): NotebookKernel[] { + return this.notebookKernelService.getMatchingKernel(notebook).all; + } +} diff --git a/packages/notebook/src/browser/service/notebook-kernel-service.ts b/packages/notebook/src/browser/service/notebook-kernel-service.ts index d501fea266f1e..0d054e39d3b5b 100644 --- a/packages/notebook/src/browser/service/notebook-kernel-service.ts +++ b/packages/notebook/src/browser/service/notebook-kernel-service.ts @@ -19,7 +19,10 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable, Event, URI } from '@theia/core'; +import { inject, injectable } from '@theia/core/shared/inversify'; import { Emitter } from '@theia/core/shared/vscode-languageserver-protocol'; +import { NotebookModel } from '../view-model/notebook-model'; +import { NotebookService } from './notebook-service'; export interface SelectedNotebooksChangeEvent { notebook: URI; @@ -76,7 +79,30 @@ export interface INotebookProxyKernelChangeEvent extends NotebookKernelChangeEve export interface NotebookTextModelLike { uri: URI; viewType: string } -export class NotebookKernelSerivce { +class KernelInfo { + + private static logicClock = 0; + + readonly kernel: NotebookKernel; + public score: number; + readonly time: number; + + constructor(kernel: NotebookKernel) { + this.kernel = kernel; + this.score = -1; + this.time = KernelInfo.logicClock++; + } +} + +@injectable() +export class NotebookKernelService { + + @inject(NotebookService) + protected notebookService: NotebookService; + + private readonly kernels = new Map(); + + private readonly notebookBindings = new Map(); private readonly onDidAddKernelEmitter = new Emitter(); readonly onDidAddKernel: Event = this.onDidAddKernelEmitter.event; @@ -91,11 +117,75 @@ export class NotebookKernelSerivce { readonly onDidChangeNotebookAffinity: Event = this.onDidChangeNotebookAffinityEmitter.event; registerKernel(kernel: NotebookKernel): Disposable { - throw new Error('Method not implemented.'); + if (this.kernels.has(kernel.id)) { + throw new Error(`NOTEBOOK CONTROLLER with id '${kernel.id}' already exists`); + } + + this.kernels.set(kernel.id, new KernelInfo(kernel)); + this.onDidAddKernelEmitter.fire(kernel); + + return Disposable.create(() => { + if (this.kernels.delete(kernel.id)) { + this.onDidRemoveKernelEmitter.fire(kernel); + } + }); } getMatchingKernel(notebook: NotebookTextModelLike): NotebookKernelMatchResult { - throw new Error('Method not implemented.'); + const kernels: { kernel: NotebookKernel; instanceAffinity: number; score: number }[] = []; + for (const info of this.kernels.values()) { + const score = NotebookKernelService.score(info.kernel, notebook); + if (score) { + kernels.push({ + score, + kernel: info.kernel, + instanceAffinity: 1 /* vscode.NotebookControllerPriority.Default */, + }); + } + } + + kernels + .sort((a, b) => b.instanceAffinity - a.instanceAffinity || a.score - b.score || a.kernel.label.localeCompare(b.kernel.label)); + const all = kernels.map(obj => obj.kernel); + + // 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 hidden = kernels.filter(item => item.instanceAffinity < 0).map(item => item.kernel); + return { all, selected, suggestions, hidden }; + + } + + selectKernelForNotebook(kernel: NotebookKernel | undefined, notebook: NotebookTextModelLike): void { + const key = `${notebook.viewType}/${notebook.uri}`; + const oldKernel = this.notebookBindings.get(key); + if (oldKernel !== kernel?.id) { + if (kernel) { + this.notebookBindings.set(key, kernel.id); + } else { + this.notebookBindings.delete(key); + } + } + } + + getSelectedOrSuggestedKernel(notebook: NotebookModel): NotebookKernel | undefined { + const info = this.getMatchingKernel(notebook); + if (info.selected) { + return info.selected; + } + + return info.all.length === 1 ? info.all[0] : undefined; + } + + private static score(kernel: NotebookKernel, notebook: NotebookTextModelLike): number { + if (kernel.viewType === '*') { + return 5; + } else if (kernel.viewType === notebook.viewType) { + return 10; + } else { + return 0; + } } } diff --git a/packages/notebook/src/browser/view-model/notebook-cell-model.ts b/packages/notebook/src/browser/view-model/notebook-cell-model.ts index eb1588a19eef7..539d107666554 100644 --- a/packages/notebook/src/browser/view-model/notebook-cell-model.ts +++ b/packages/notebook/src/browser/view-model/notebook-cell-model.ts @@ -13,6 +13,10 @@ // // 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, Emitter, Event, URI } from '@theia/core'; import { inject, injectable, interfaces } from '@theia/core/shared/inversify'; @@ -80,7 +84,22 @@ export class NotebookCellModel implements Disposable { readonly metadata: NotebookCellMetadata; - readonly internalMetadata: NotebookCellInternalMetadata; + private _internalMetadata: NotebookCellInternalMetadata; + + get internalMetadata(): NotebookCellInternalMetadata { + return this._internalMetadata; + } + + set internalMetadata(newInternalMetadata: NotebookCellInternalMetadata) { + const lastRunSuccessChanged = this._internalMetadata.lastRunSuccess !== newInternalMetadata.lastRunSuccess; + newInternalMetadata = { + ...newInternalMetadata, + ...{ runStartTimeAdjustment: computeRunStartTimeAdjustment(this._internalMetadata, newInternalMetadata) } + }; + this._internalMetadata = newInternalMetadata; + this.ChangeInternalMetadataEmitter.fire({ lastRunSuccessChanged }); + + } textModel: MonacoEditorModel; @@ -116,7 +135,7 @@ export class NotebookCellModel implements Disposable { ) { this.outputs = props.outputs.map(op => new NotebookCellOutputModel(op)); this.metadata = props.metadata ?? {}; - this.internalMetadata = props.internalMetadata ?? {}; + this._internalMetadata = props.internalMetadata ?? {}; } refChanged(node: HTMLLIElement): void { @@ -156,3 +175,12 @@ export class NotebookCellModel implements Disposable { }; } } + +function computeRunStartTimeAdjustment(oldMetadata: NotebookCellInternalMetadata, newMetadata: NotebookCellInternalMetadata): number | undefined { + if (oldMetadata.runStartTime !== newMetadata.runStartTime && typeof newMetadata.runStartTime === 'number') { + const offset = Date.now() - newMetadata.runStartTime; + return offset < 0 ? Math.abs(offset) : 0; + } else { + return newMetadata.runStartTimeAdjustment; + } +} diff --git a/packages/notebook/src/browser/view-model/notebook-model.ts b/packages/notebook/src/browser/view-model/notebook-model.ts index 7e89dc0120d45..762dfb547e3e5 100644 --- a/packages/notebook/src/browser/view-model/notebook-model.ts +++ b/packages/notebook/src/browser/view-model/notebook-model.ts @@ -17,7 +17,10 @@ import { Disposable, URI } from '@theia/core'; import { Emitter } from '@theia/core/shared/vscode-languageserver-protocol'; import { Saveable, SaveOptions } from '@theia/core/lib/browser'; -import { CellUri, NotebookCellsChangeType, NotebookCellTextModelSplice, NotebookData, NotebookModelWillAddRemoveEvent } from '../../common'; +import { + CellEditOperation, CellEditType, CellUri, NotebookCellInternalMetadata, + NotebookCellsChangeType, NotebookCellTextModelSplice, NotebookData, NotebookModelWillAddRemoveEvent, 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'; @@ -166,4 +169,65 @@ export class NotebookModel implements Saveable, Disposable { this.cells.splice(index, count); this.didAddRemoveCellEmitter.fire({ rawEvent: { kind: NotebookCellsChangeType.ModelChange, changes } }); } + + applyEdits(rawEdits: CellEditOperation[]): void { + const editsWithDetails = rawEdits.map((edit, index) => { + let cellIndex: number = -1; + if ('index' in edit) { + cellIndex = edit.index; + } else if ('handle' in edit) { + cellIndex = this.getCellIndexByHandle(edit.handle); + } + + return { + edit, + cellIndex, + end: edit.editType === CellEditType.Replace ? edit.index + edit.count : cellIndex, + originalIndex: index + }; + }).filter(edit => !!edit); + + for (const edit of editsWithDetails) { + switch (edit.edit.editType) { + case CellEditType.Replace: + break; + // case CellEditType.Output: { + // break; + // } + // case CellEditType.OutputItems: + // break; + // case CellEditType.Metadata: + // break; + // case CellEditType.PartialMetadata: + // break; + case CellEditType.PartialInternalMetadata: + this.changeCellInternalMetadataPartial(this.cells[edit.cellIndex], edit.edit.internalMetadata); + break; + // case CellEditType.CellLanguage: + // break; + // case CellEditType.DocumentMetadata: + // break; + // case CellEditType.Move: + // break; + + } + } + } + + private changeCellInternalMetadataPartial(cell: NotebookCellModel, internalMetadata: NullablePartialNotebookCellInternalMetadata): void { + const newInternalMetadata: NotebookCellInternalMetadata = { + ...cell.internalMetadata + }; + let k: keyof NotebookCellInternalMetadata; + // eslint-disable-next-line guard-for-in + for (k in internalMetadata) { + newInternalMetadata[k] = (internalMetadata[k] ?? undefined) as never; + } + + cell.internalMetadata = newInternalMetadata; + } + + private getCellIndexByHandle(handle: number): number { + return this.cells.findIndex(c => c.handle === handle); + } } diff --git a/packages/notebook/src/browser/view/notebook-cell-list-view.tsx b/packages/notebook/src/browser/view/notebook-cell-list-view.tsx index 5801abd7b1565..dfb48d951c7ec 100644 --- a/packages/notebook/src/browser/view/notebook-cell-list-view.tsx +++ b/packages/notebook/src/browser/view/notebook-cell-list-view.tsx @@ -67,7 +67,7 @@ export class NotebookCellListView extends React.Component; }; +export enum NotebookCellExecutionState { + Unconfirmed = 1, + Pending = 2, + Executing = 3 +} + +export enum CellExecutionUpdateType { + Output = 1, + OutputItems = 2, + ExecutionState = 3, +} + +export interface CellExecuteOutputEdit { + editType: CellExecutionUpdateType.Output; + cellHandle: number; + append?: boolean; + outputs: CellOutput[]; +} + +export interface CellExecuteOutputItemEdit { + editType: CellExecutionUpdateType.OutputItems; + append?: boolean; + items: CellOutputItem[]; +} + +export interface CellExecutionStateUpdateDto { + editType: CellExecutionUpdateType.ExecutionState; + executionOrder?: number; + runStartTime?: number; + didPause?: boolean; + isPaused?: boolean; +} + +export const enum CellEditType { + Replace = 1, + Output = 2, + Metadata = 3, + CellLanguage = 4, + DocumentMetadata = 5, + Move = 6, + OutputItems = 7, + PartialMetadata = 8, + PartialInternalMetadata = 9, +} + +export type ImmediateCellEditOperation = CellPartialInternalMetadataEditByHandle; // add more later on +export type CellEditOperation = ImmediateCellEditOperation | CellReplaceEdit; // add more later on + +export type NullablePartialNotebookCellInternalMetadata = { + [Key in keyof Partial]: NotebookCellInternalMetadata[Key] | null +}; +export interface CellPartialInternalMetadataEditByHandle { + editType: CellEditType.PartialInternalMetadata; + handle: number; + internalMetadata: NullablePartialNotebookCellInternalMetadata; +} + /** * Whether the provided mime type is a text stream like `stdout`, `stderr`. */ diff --git a/packages/plugin-ext/src/common/plugin-api-rpc.ts b/packages/plugin-ext/src/common/plugin-api-rpc.ts index ae02d8fbd919a..832064b690421 100644 --- a/packages/plugin-ext/src/common/plugin-api-rpc.ts +++ b/packages/plugin-ext/src/common/plugin-api-rpc.ts @@ -117,7 +117,7 @@ import { isString, isObject, PickOptions, QuickInputButtonHandle } from '@theia/ import { Severity } from '@theia/core/lib/common/severity'; import { DebugConfiguration, DebugSessionOptions } from '@theia/debug/lib/common/debug-configuration'; import * as notebookCommon from '@theia/notebook/lib/common'; -import { CellRange } from '@theia/notebook/lib/common'; +import { CellExecutionUpdateType, CellRange, NotebookCellExecutionState } from '@theia/notebook/lib/common'; import { LanguagePackBundle } from './language-pack-service'; export interface PreferenceData { @@ -2380,18 +2380,6 @@ export interface CellExecutionStateUpdateDto { isPaused?: boolean; } -export enum CellExecutionUpdateType { - Output = 1, - OutputItems = 2, - ExecutionState = 3, -} - -export enum NotebookCellExecutionState { - Unconfirmed = 1, - Pending = 2, - Executing = 3 -} - export interface CellExecutionCompleteDto { runEndTime?: number; lastRunSuccess?: boolean; 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 469afde3a452d..81fec1395578e 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 @@ -23,11 +23,10 @@ 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 { NotebookKernelChangeEvent, NotebookKernelSerivce, NotebookService } from '@theia/notebook/lib/browser'; +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 { NotebookCellExecution } from '@theia/plugin'; abstract class NotebookKernel { private readonly onDidChangeEmitter = new Emitter(); @@ -110,11 +109,12 @@ export class NotebookKernelsMainImpl implements NotebookKernelsMain { private readonly kernels = new Map(); - private notebookKernelService: NotebookKernelSerivce; + private notebookKernelService: NotebookKernelService; private notebookService: NotebookService; private languageService: LanguageService; + private notebookExecutionStateService: NotebookExecutionStateService; - private readonly executions = new Map(); + private readonly executions = new Map(); constructor( rpc: RPCProtocol, @@ -122,7 +122,8 @@ export class NotebookKernelsMainImpl implements NotebookKernelsMain { ) { this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.NOTEBOOK_KERNELS_EXT); - this.notebookKernelService = container.get(NotebookKernelSerivce); + this.notebookKernelService = container.get(NotebookKernelService); + this.notebookExecutionStateService = container.get(NotebookExecutionStateService); this.notebookService = container.get(NotebookService); this.languageService = container.get(LanguageService); } @@ -207,16 +208,14 @@ export class NotebookKernelsMainImpl implements NotebookKernelsMain { throw new Error('Method not implemented.'); } $removeKernelDetectionTask(handle: number): void { - throw new Error('Method not implemented.'); } $addKernelSourceActionProvider(handle: number, eventHandle: number, notebookType: string): Promise { - throw new Error('Method not implemented.'); + return Promise.resolve(); } $removeKernelSourceActionProvider(handle: number, eventHandle: number): void { throw new Error('Method not implemented.'); } $emitNotebookKernelSourceActionsChangeEvent(eventHandle: number): void { - throw new Error('Method not implemented.'); } dispose(): void { diff --git a/packages/plugin-ext/src/plugin/notebook/notebook-kernels.ts b/packages/plugin-ext/src/plugin/notebook/notebook-kernels.ts index c98d88735eeec..e8ee3fba5f7e8 100644 --- a/packages/plugin-ext/src/plugin/notebook/notebook-kernels.ts +++ b/packages/plugin-ext/src/plugin/notebook/notebook-kernels.ts @@ -20,10 +20,7 @@ import { CancellationToken } from '@theia/plugin'; import { - CellExecuteUpdateDto, - CellExecutionUpdateType, - NotebookCellExecutionState, NotebookKernelDto, NotebookKernelsExt, NotebookKernelsMain, NotebookKernelSourceActionDto, NotebookOutputDto, - PLUGIN_RPC_CONTEXT + CellExecuteUpdateDto, NotebookKernelDto, NotebookKernelsExt, NotebookKernelsMain, NotebookKernelSourceActionDto, NotebookOutputDto, PLUGIN_RPC_CONTEXT } from '../../common'; import { RPCProtocol } from '../../common/rpc-protocol'; import { UriComponents } from '../../common/uri-components'; @@ -34,6 +31,7 @@ import { Cell } from './notebook-document'; import { NotebooksExtImpl } from './notebooks'; import { NotebookCellOutput, NotebookCellOutputItem } from '../type-converters'; import { timeout, Deferred } from '@theia/core/lib/common/promise-util'; +import { CellExecutionUpdateType, NotebookCellExecutionState } from '@theia/notebook/lib/common'; interface KernelData { extensionId: string; @@ -51,6 +49,15 @@ export class NotebookKernelsExtImpl implements NotebookKernelsExt { private readonly proxy: NotebookKernelsMain; + private kernelDetectionTasks = new Map(); + private currentkernelDetectionTaskHandle = 0; + + private kernelSourceActionProviders = new Map(); + private currentSourceActionProviderHandle = 0; + + private readonly onDidChangeCellExecutionStateEmitter = new Emitter(); + readonly onDidChangeNotebookCellExecutionState = this.onDidChangeCellExecutionStateEmitter.event; + constructor( rpc: RPCProtocol, private notebooks: NotebooksExtImpl @@ -233,6 +240,45 @@ export class NotebookKernelsExtImpl implements NotebookKernelsExt { return execution.asApiObject(); } + createNotebookControllerDetectionTask(viewType: string): theia.NotebookControllerDetectionTask { + const handle = this.currentkernelDetectionTaskHandle++; + const that = this; + + this.proxy.$addKernelDetectionTask(handle, viewType); + + const detectionTask: theia.NotebookControllerDetectionTask = { + dispose: () => { + this.kernelDetectionTasks.delete(handle); + that.proxy.$removeKernelDetectionTask(handle); + } + }; + + this.kernelDetectionTasks.set(handle, detectionTask); + return detectionTask; + } + + registerKernelSourceActionProvider(viewType: string, provider: theia.NotebookKernelSourceActionProvider): Disposable { + const handle = this.currentSourceActionProviderHandle++; + const eventHandle = typeof provider.onDidChangeNotebookKernelSourceActions === 'function' ? handle : undefined; + const that = this; + + this.kernelSourceActionProviders.set(handle, provider); + this.proxy.$addKernelSourceActionProvider(handle, handle, viewType); + + let subscription: theia.Disposable | undefined; + if (eventHandle !== undefined) { + subscription = provider.onDidChangeNotebookKernelSourceActions!(_ => this.proxy.$emitNotebookKernelSourceActionsChangeEvent(eventHandle)); + } + + return { + dispose: () => { + this.kernelSourceActionProviders.delete(handle); + that.proxy.$removeKernelSourceActionProvider(handle, handle); + subscription?.dispose(); + } + }; + } + $acceptNotebookAssociation(handle: number, uri: UriComponents, value: boolean): void { const obj = this.kernelData.get(handle); if (obj) { @@ -286,7 +332,7 @@ export class NotebookKernelsExtImpl implements NotebookKernelsExt { } // cancel or interrupt depends on the controller. When an interrupt handler is used we - // don't trigger the cancelation token of executions. + // don't trigger the cancelation token of executions.N const document = this.notebooks.getNotebookDocument(URI.fromComponents(uri)); if (obj.controller.interruptHandler) { await obj.controller.interruptHandler.call(obj.controller, document.apiNotebook); @@ -313,7 +359,7 @@ export class NotebookKernelsExtImpl implements NotebookKernelsExt { } $cellExecutionChanged(uri: UriComponents, cellHandle: number, state: NotebookCellExecutionState | undefined): void { - throw new Error('Method not implemented.'); // Proposed Api + throw new Error('Method not implemented.'); // Proposed Api though seems needed by jupyter } $provideKernelSourceActions(handle: number, token: CancellationToken): Promise { throw new Error('Method not implemented.'); diff --git a/packages/plugin-ext/src/plugin/plugin-context.ts b/packages/plugin-ext/src/plugin/plugin-context.ts index 29af347d1f315..d0ba5ba46e72d 100644 --- a/packages/plugin-ext/src/plugin/plugin-context.ts +++ b/packages/plugin-ext/src/plugin/plugin-context.ts @@ -165,8 +165,8 @@ import { InlayHintKind, InlayHintLabelPart, TelemetryTrustedValue, - NotebookCell, NotebookCellKind, + NotebookCellExecutionState, NotebookCellStatusBarAlignment, NotebookEditorRevealType, NotebookControllerAffinity, @@ -178,6 +178,7 @@ import { NotebookRange, NotebookCellStatusBarItem, NotebookEdit, + NotebookKernelSourceAction, TestRunProfileKind, TestTag, TestRunRequest, @@ -244,6 +245,7 @@ import { LocalizationExtImpl } from './localization-ext'; import { NotebooksExtImpl } from './notebook/notebooks'; import { TelemetryExtImpl } from './telemetry-ext'; import { NotebookRenderersExtImpl } from './notebook/notebook-renderers'; +import { NotebookKernelsExtImpl } from './notebook/notebook-kernels'; export function createAPIFactory( rpc: RPCProtocol, @@ -269,6 +271,7 @@ export function createAPIFactory( 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 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 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)); @@ -1153,33 +1156,7 @@ export function createAPIFactory( notebook: theia.NotebookDocument, controller: theia.NotebookController) => void | Thenable ) { - return { - id, - notebookType, - label, - handler, - createNotebookCellExecution: (cell: NotebookCell) => ({ - cell, - token: CancellationToken.None, - executionOrder: undefined, - start: () => undefined, - end: () => undefined, - clearOutput: () => ({} as Thenable), - replaceOutput: () => ({} as Thenable), - appendOutput: () => ({} as Thenable), - replaceOutputItems: () => ({} as Thenable), - appendOutputItems: () => ({} as Thenable) - }), - executeHandler( - cells: theia.NotebookCell[], - notebook: theia.NotebookDocument, - controller: theia.NotebookController - ): (void | Thenable) { }, - onDidChangeSelectedNotebooks: () => Disposable.create(() => { }), - updateNotebookAffinity: (notebook: theia.NotebookDocument, affinity: theia.NotebookControllerAffinity) => undefined, - dispose: () => undefined, - }; - + return notebookKernels.createNotebookController(plugin.model.id, id, notebookType, label, handler); }, createRendererMessaging(rendererId) { return notebookRenderers.createRendererMessaging(rendererId); @@ -1189,6 +1166,14 @@ export function createAPIFactory( provider ) { return notebooksExt.registerNotebookCellStatusBarItemProvider(notebookType, provider); + }, + onDidChangeNotebookCellExecutionState: notebookKernels.onDidChangeNotebookCellExecutionState, + + createNotebookControllerDetectionTask(notebookType: string) { + return notebookKernels.createNotebookControllerDetectionTask(notebookType); + }, + registerKernelSourceActionProvider(notebookType: string, provider: theia.NotebookKernelSourceActionProvider) { + return notebookKernels.registerKernelSourceActionProvider(notebookType, provider); } }; @@ -1342,6 +1327,7 @@ export function createAPIFactory( InlayHintLabelPart, TelemetryTrustedValue, NotebookCellData, + NotebookCellExecutionState, NotebookCellKind, NotebookCellOutput, NotebookCellOutputItem, @@ -1353,6 +1339,7 @@ export function createAPIFactory( NotebookDocument, NotebookRange, NotebookEdit, + NotebookKernelSourceAction, TestRunProfileKind, TestTag, TestRunRequest, diff --git a/packages/plugin-ext/src/plugin/types-impl.ts b/packages/plugin-ext/src/plugin/types-impl.ts index 4983160bbc780..fbda527b0fa29 100644 --- a/packages/plugin-ext/src/plugin/types-impl.ts +++ b/packages/plugin-ext/src/plugin/types-impl.ts @@ -1130,6 +1130,31 @@ export enum NotebookEditorRevealType { InCenterIfOutsideViewport = 2, AtTop = 3 } + +export enum NotebookCellExecutionState { + /** + * The cell is idle. + */ + Idle = 1, + /** + * Execution for the cell is pending. + */ + Pending = 2, + /** + * The cell is currently executing. + */ + Executing = 3, +} + +export class NotebookKernelSourceAction { + description?: string; + detail?: string; + command?: theia.Command; + constructor( + public label: string + ) { } +} + @es5ClassCompat export class NotebookCellData implements theia.NotebookCellData { languageId: string; diff --git a/packages/plugin/src/theia.d.ts b/packages/plugin/src/theia.d.ts index f7461bcb45b44..559fafe9b6590 100644 --- a/packages/plugin/src/theia.d.ts +++ b/packages/plugin/src/theia.d.ts @@ -24,6 +24,8 @@ import './theia-extra'; import './theia-proposed'; import './theia.proposed.externalUriOpener'; import './vscode.proposed.editSessionIdentityProvider'; +import './theia.proposed.notebookCellExecutionState'; +import './theia.proposed.notebookKernelSource'; /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable max-len */ @@ -15716,7 +15718,6 @@ export module '@theia/plugin' { */ export function registerNotebookCellStatusBarItemProvider(notebookType: string, provider: NotebookCellStatusBarItemProvider): Disposable; } -} /** * Namespace for testing functionality. Tests are published by registering @@ -16339,3 +16340,4 @@ interface Thenable { then(onfulfilled?: (value: T) => TResult | Thenable, onrejected?: (reason: any) => void): Thenable; } +} diff --git a/packages/plugin/src/theia.proposed.notebookCellExecutionState.d.ts b/packages/plugin/src/theia.proposed.notebookCellExecutionState.d.ts new file mode 100644 index 0000000000000..92ea46487f92e --- /dev/null +++ b/packages/plugin/src/theia.proposed.notebookCellExecutionState.d.ts @@ -0,0 +1,68 @@ +// ***************************************************************************** +// Copyright (C) 2018 Red Hat, Inc. 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/124970 + + /** + * The execution state of a notebook cell. + */ + export enum NotebookCellExecutionState { + /** + * The cell is idle. + */ + Idle = 1, + /** + * Execution for the cell is pending. + */ + Pending = 2, + /** + * The cell is currently executing. + */ + Executing = 3, + } + + /** + * An event describing a cell execution state change. + */ + export interface NotebookCellExecutionStateChangeEvent { + /** + * The {@link NotebookCell cell} for which the execution state has changed. + */ + readonly cell: NotebookCell; + + /** + * The new execution state of the cell. + */ + readonly state: NotebookCellExecutionState; + } + + export namespace notebooks { + + /** + * An {@link Event} which fires when the execution state of a cell has changed. + */ + // todo@API this is an event that is fired for a property that cells don't have and that makes me wonder + // how a correct consumer works, e.g the consumer could have been late and missed an event? + export const onDidChangeNotebookCellExecutionState: Event; + } +} diff --git a/packages/plugin/src/theia.proposed.notebookKernelSource.d.ts b/packages/plugin/src/theia.proposed.notebookKernelSource.d.ts new file mode 100644 index 0000000000000..86ca3061e742c --- /dev/null +++ b/packages/plugin/src/theia.proposed.notebookKernelSource.d.ts @@ -0,0 +1,62 @@ +// ***************************************************************************** +// Copyright (C) 2018 Red Hat, Inc. 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' { + export interface NotebookControllerDetectionTask { + /** + * Dispose and remove the detection task. + */ + dispose(): void; + } + + export class NotebookKernelSourceAction { + readonly label: string; + readonly description?: string; + readonly detail?: string; + readonly command: string | Command; + readonly documentation?: Uri; + + constructor(label: string); + } + + export interface NotebookKernelSourceActionProvider { + /** + * An optional event to signal that the kernel source actions have changed. + */ + onDidChangeNotebookKernelSourceActions?: Event; + /** + * Provide kernel source actions + */ + provideNotebookKernelSourceActions(token: CancellationToken): ProviderResult; + } + + export namespace notebooks { + /** + * Create notebook controller detection task + */ + export function createNotebookControllerDetectionTask(notebookType: string): NotebookControllerDetectionTask; + + /** + * Register a notebook kernel source action provider + */ + export function registerKernelSourceActionProvider(notebookType: string, provider: NotebookKernelSourceActionProvider): Disposable; + } +}