diff --git a/examples/browser/package.json b/examples/browser/package.json index fe1a8d23a2fe1..00bad2b4b44b9 100644 --- a/examples/browser/package.json +++ b/examples/browser/package.json @@ -34,6 +34,7 @@ "@theia/mini-browser": "1.40.0", "@theia/monaco": "1.40.0", "@theia/navigator": "1.40.0", + "@theia/notebook": "1.40.0", "@theia/outline-view": "1.40.0", "@theia/output": "1.40.0", "@theia/plugin-dev": "1.40.0", diff --git a/examples/browser/tsconfig.json b/examples/browser/tsconfig.json index d4bcfc14426b9..885bcb416bb9c 100644 --- a/examples/browser/tsconfig.json +++ b/examples/browser/tsconfig.json @@ -65,6 +65,9 @@ { "path": "../../packages/navigator" }, + { + "path": "../../packages/notebook" + }, { "path": "../../packages/outline-view" }, diff --git a/package.json b/package.json index 1b0aa68d29fbc..3ab0e5d3f8e33 100644 --- a/package.json +++ b/package.json @@ -113,7 +113,6 @@ "vscode.git-base", "vscode.github", "vscode.github-authentication", - "vscode.ipynb", "vscode.microsoft-authentication", "ms-vscode.references-view" ] diff --git a/packages/core/src/browser/saveable.ts b/packages/core/src/browser/saveable.ts index 02f8c07af11be..a0d0810b447be 100644 --- a/packages/core/src/browser/saveable.ts +++ b/packages/core/src/browser/saveable.ts @@ -16,13 +16,13 @@ import { Widget } from '@phosphor/widgets'; import { Message } from '@phosphor/messaging'; -import { Event } from '../common/event'; +import { Emitter, Event } from '../common/event'; import { MaybePromise } from '../common/types'; import { Key } from './keyboard/keys'; import { AbstractDialog } from './dialogs'; import { waitForClosed } from './widgets'; import { nls } from '../common/nls'; -import { isObject } from '../common'; +import { Disposable, isObject } from '../common'; export interface Saveable { readonly dirty: boolean; @@ -50,6 +50,45 @@ export interface SaveableSource { readonly saveable: Saveable; } +export class SaveableDelegate implements Saveable { + dirty = false; + protected readonly onDirtyChangedEmitter = new Emitter(); + + get onDirtyChanged(): Event { + return this.onDirtyChangedEmitter.event; + } + autoSave: 'off' | 'afterDelay' | 'onFocusChange' | 'onWindowChange' = 'off'; + + async save(options?: SaveOptions): Promise { + await this.delegate?.save(options); + } + + revert?(options?: Saveable.RevertOptions): Promise; + createSnapshot?(): Saveable.Snapshot; + applySnapshot?(snapshot: object): void; + + protected delegate?: Saveable; + protected toDispose?: Disposable; + + set(delegate: Saveable): void { + this.toDispose?.dispose(); + this.delegate = delegate; + this.toDispose = this.delegate.onDirtyChanged(() => { + this.dirty = delegate.dirty; + this.onDirtyChangedEmitter.fire(); + }); + this.autoSave = delegate.autoSave; + if (this.dirty !== delegate.dirty) { + this.dirty = delegate.dirty; + this.onDirtyChangedEmitter.fire(); + } + this.revert = delegate.revert?.bind(delegate); + this.createSnapshot = delegate.createSnapshot?.bind(delegate); + this.applySnapshot = delegate.applySnapshot?.bind(delegate); + } + +} + export namespace Saveable { export interface RevertOptions { /** 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/types.ts b/packages/core/src/common/types.ts index e5c7bb26462c5..ab819250e9b7e 100644 --- a/packages/core/src/common/types.ts +++ b/packages/core/src/common/types.ts @@ -56,6 +56,23 @@ export function isFunction unknown>(value: unk return typeof value === 'function'; } +/** + * @returns whether the provided parameter is an empty JavaScript Object or not. + */ +export function isEmptyObject(obj: unknown): obj is object { + if (!isObject(obj)) { + return false; + } + + for (const key in obj) { + if (Object.prototype.hasOwnProperty.call(obj, key)) { + return false; + } + } + + return true; +} + export function isObject(value: unknown): value is UnknownObject { // eslint-disable-next-line no-null/no-null return typeof value === 'object' && value !== null; 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/editor/src/browser/editor-frontend-module.ts b/packages/editor/src/browser/editor-frontend-module.ts index fea78996564e9..48c0524489116 100644 --- a/packages/editor/src/browser/editor-frontend-module.ts +++ b/packages/editor/src/browser/editor-frontend-module.ts @@ -38,6 +38,7 @@ import { QuickAccessContribution } from '@theia/core/lib/browser/quick-input/qui import { QuickEditorService } from './quick-editor-service'; import { EditorLanguageStatusService } from './language-status/editor-language-status-service'; import { EditorLineNumberContribution } from './editor-linenumber-contribution'; +import { UndoRedoService } from './undo-redo-service'; export default new ContainerModule(bind => { bindEditorPreferences(bind); @@ -86,4 +87,6 @@ export default new ContainerModule(bind => { bind(ActiveEditorAccess).toSelf().inSingletonScope(); bind(EditorAccess).to(CurrentEditorAccess).inSingletonScope().whenTargetNamed(EditorAccess.CURRENT); bind(EditorAccess).to(ActiveEditorAccess).inSingletonScope().whenTargetNamed(EditorAccess.ACTIVE); + + bind(UndoRedoService).toSelf().inSingletonScope(); }); diff --git a/packages/plugin-ext/src/main/browser/custom-editors/undo-redo-service.ts b/packages/editor/src/browser/undo-redo-service.ts similarity index 95% rename from packages/plugin-ext/src/main/browser/custom-editors/undo-redo-service.ts rename to packages/editor/src/browser/undo-redo-service.ts index 36b628e23953b..a66b02990e0d2 100644 --- a/packages/plugin-ext/src/main/browser/custom-editors/undo-redo-service.ts +++ b/packages/editor/src/browser/undo-redo-service.ts @@ -94,16 +94,16 @@ export class ResourceEditStack { this.past.push(element); } - getClosestPastElement(): StackElement | null { + getClosestPastElement(): StackElement | undefined { if (this.past.length === 0) { - return null; + return undefined; } return this.past[this.past.length - 1]; } - getClosestFutureElement(): StackElement | null { + getClosestFutureElement(): StackElement | undefined { if (this.future.length === 0) { - return null; + return undefined; } return this.future[this.future.length - 1]; } diff --git a/packages/monaco/src/browser/monaco-code-editor.ts b/packages/monaco/src/browser/monaco-code-editor.ts new file mode 100644 index 0000000000000..62a57e8a56fd8 --- /dev/null +++ b/packages/monaco/src/browser/monaco-code-editor.ts @@ -0,0 +1,210 @@ +// ***************************************************************************** +// 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 { EditorServiceOverrides, MonacoEditor, MonacoEditorServices } from './monaco-editor'; + +import { CodeEditorWidget } from '@theia/monaco-editor-core/esm/vs/editor/browser/widget/codeEditorWidget'; +import { IInstantiationService } from '@theia/monaco-editor-core/esm/vs/platform/instantiation/common/instantiation'; +import { StandaloneServices } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices'; +import { ServiceCollection } from '@theia/monaco-editor-core/esm/vs/platform/instantiation/common/serviceCollection'; +import { Disposable, DisposableCollection, Emitter, TextDocumentContentChangeDelta, URI } from '@theia/core'; +import { MonacoEditorModel } from './monaco-editor-model'; +import { Dimension, EditorMouseEvent, MouseTarget, Position, TextDocumentChangeEvent } from '@theia/editor/lib/browser'; +import * as monaco from '@theia/monaco-editor-core'; +import { ElementExt } from '@theia/core/shared/@phosphor/domutils'; + +export class MonacoCodeEditor extends MonacoEditorServices implements Disposable { + + protected editor: CodeEditorWidget; + protected readonly toDispose = new DisposableCollection(); + + protected readonly onCursorPositionChangedEmitter = new Emitter(); + protected readonly onSelectionChangedEmitter = new Emitter(); + protected readonly onFocusChangedEmitter = new Emitter(); + protected readonly onDocumentContentChangedEmitter = new Emitter(); + readonly onDocumentContentChanged = this.onDocumentContentChangedEmitter.event; + protected readonly onMouseDownEmitter = new Emitter(); + protected readonly onLanguageChangedEmitter = new Emitter(); + readonly onLanguageChanged = this.onLanguageChangedEmitter.event; + protected readonly onScrollChangedEmitter = new Emitter(); + readonly onEncodingChanged = this.document.onDidChangeEncoding; + protected readonly onResizeEmitter = new Emitter(); + readonly onDidResize = this.onResizeEmitter.event; + + constructor( + readonly uri: URI, + readonly document: MonacoEditorModel, + readonly node: HTMLElement, + services: MonacoEditorServices, + options?: MonacoEditor.IOptions, + override?: EditorServiceOverrides + ) { + super(services); + this.toDispose.pushAll([ + this.onCursorPositionChangedEmitter, + this.onSelectionChangedEmitter, + this.onFocusChangedEmitter, + this.onDocumentContentChangedEmitter, + this.onMouseDownEmitter, + this.onLanguageChangedEmitter, + this.onScrollChangedEmitter + ]); + this.toDispose.push(this.create(options, override)); + this.addHandlers(this.editor); + this.editor.setModel(document.textEditorModel); + } + + getControl(): CodeEditorWidget { + return this.editor; + } + + protected create(options?: MonacoEditor.IOptions, override?: EditorServiceOverrides): Disposable { + const combinedOptions = { + ...options, + lightbulb: { enabled: true }, + fixedOverflowWidgets: true, + automaticLayout: true, + scrollbar: { + useShadows: false, + verticalHasArrows: false, + horizontalHasArrows: false, + verticalScrollbarSize: 10, + horizontalScrollbarSize: 10, + ...options?.scrollbar, + } + }; + const instantiator = this.getInstantiatorWithOverrides(override); + return this.editor = instantiator.createInstance(CodeEditorWidget, this.node, { + ...combinedOptions, + dimension: { + width: 0, + height: 0 + }, + }, { + + }); + } + + protected addHandlers(codeEditor: CodeEditorWidget): void { + this.toDispose.push(codeEditor.onDidChangeModelLanguage(e => + this.fireLanguageChanged(e.newLanguage) + )); + this.toDispose.push(codeEditor.onDidChangeConfiguration(() => this.refresh())); + this.toDispose.push(codeEditor.onDidChangeModel(() => this.refresh())); + this.toDispose.push(codeEditor.onDidChangeModelContent(e => { + this.refresh(); + this.onDocumentContentChangedEmitter.fire({ document: this.document, contentChanges: e.changes.map(this.mapModelContentChange.bind(this)) }); + })); + this.toDispose.push(codeEditor.onMouseDown(e => { + const { element, position, range } = e.target; + this.onMouseDownEmitter.fire({ + target: { + ...(e.target as unknown as MouseTarget), + element: element || undefined, + mouseColumn: this.m2p.asPosition(undefined, e.target.mouseColumn).character, + range: range && this.m2p.asRange(range) || undefined, + position: position && this.m2p.asPosition(position.lineNumber, position.column) || undefined, + detail: undefined + }, + event: e.event.browserEvent + }); + })); + this.toDispose.push(codeEditor.onDidScrollChange(e => { + this.onScrollChangedEmitter.fire(undefined); + })); + } + + setLanguage(languageId: string): void { + monaco.editor.setModelLanguage(this.document.textEditorModel, languageId); + } + + protected fireLanguageChanged(languageId: string): void { + this.onLanguageChangedEmitter.fire(languageId); + } + + protected getInstantiatorWithOverrides(override?: EditorServiceOverrides): IInstantiationService { + const instantiator = StandaloneServices.initialize({}); + if (override) { + const overrideServices = new ServiceCollection(...override); + return instantiator.createChild(overrideServices); + } + return instantiator; + } + + protected mapModelContentChange(change: monaco.editor.IModelContentChange): TextDocumentContentChangeDelta { + return { + range: this.m2p.asRange(change.range), + rangeLength: change.rangeLength, + text: change.text + }; + } + + refresh(): void { + this.autoresize(); + } + + resizeToFit(): void { + this.autoresize(); + // eslint-disable-next-line no-null/no-null + this.onResizeEmitter.fire(null); + } + + setSize(dimension: Dimension): void { + this.resize(dimension); + this.onResizeEmitter.fire(dimension); + } + + protected autoresize(): void { + this.resize(); + } + + protected resize(dimension?: Dimension): void { + if (this.node) { + const layoutSize = this.computeLayoutSize(this.node, dimension); + this.editor.layout(layoutSize); + } + } + + protected computeLayoutSize(hostNode: HTMLElement, dimension: monaco.editor.IDimension | undefined): monaco.editor.IDimension { + if (dimension && dimension.width >= 0 && dimension.height >= 0) { + return dimension; + } + const boxSizing = ElementExt.boxSizing(hostNode); + + const width = (!dimension || dimension.width < 0) ? + this.getWidth(hostNode, boxSizing) : + dimension.width; + + const height = (!dimension || dimension.height < 0) ? + this.getHeight(hostNode, boxSizing) : + dimension.height; + + return { width, height }; + } + + protected getWidth(hostNode: HTMLElement, boxSizing: ElementExt.IBoxSizing): number { + return hostNode.offsetWidth - boxSizing.horizontalSum; + } + + protected getHeight(hostNode: HTMLElement, boxSizing: ElementExt.IBoxSizing): number { + return this.editor.getContentHeight(); + } + + dispose(): void { + this.toDispose.dispose(); + } + +} diff --git a/packages/monaco/src/browser/monaco-editor-provider.ts b/packages/monaco/src/browser/monaco-editor-provider.ts index 20bf5ff613a71..e939ee5f2cc6c 100644 --- a/packages/monaco/src/browser/monaco-editor-provider.ts +++ b/packages/monaco/src/browser/monaco-editor-provider.ts @@ -235,7 +235,7 @@ export class MonacoEditorProvider { protected get preferencePrefixes(): string[] { return ['editor.']; } - protected async createMonacoEditor(uri: URI, override: EditorServiceOverrides, toDispose: DisposableCollection): Promise { + async createMonacoEditor(uri: URI, override: EditorServiceOverrides, toDispose: DisposableCollection): Promise { const model = await this.getModel(uri, toDispose); const options = this.createMonacoEditorOptions(model); const factory = this.factories.getContributions().find(({ scheme }) => uri.scheme === scheme); @@ -403,10 +403,10 @@ export class MonacoEditorProvider { const overrides = override ? Array.from(override) : []; overrides.push([IContextMenuService, { showContextMenu: () => {/** no op! */ } }]); const document = new MonacoEditorModel({ - uri, - readContents: async () => '', - dispose: () => { } - }, this.m2p, this.p2m); + uri, + readContents: async () => '', + dispose: () => { } + }, this.m2p, this.p2m); toDispose.push(document); const model = (await document.load()).textEditorModel; return new MonacoEditor( diff --git a/packages/monaco/src/browser/monaco-languages.ts b/packages/monaco/src/browser/monaco-languages.ts index d533475d804e0..6bfb920e8bf74 100644 --- a/packages/monaco/src/browser/monaco-languages.ts +++ b/packages/monaco/src/browser/monaco-languages.ts @@ -85,6 +85,10 @@ export class MonacoLanguages implements LanguageService { return this.getLanguage(languageId)?.extensions.values().next().value; } + getLanguageIdByLanguageName(languageName: string): string | undefined { + return monaco.languages.getLanguages().find(language => language.aliases?.includes(languageName))?.id; + } + protected mergeLanguages(registered: monaco.languages.ILanguageExtensionPoint[]): Map> { const languages = new Map>(); for (const { id, aliases, extensions, filenames } of registered) { diff --git a/packages/notebook/.eslintrc.js b/packages/notebook/.eslintrc.js new file mode 100644 index 0000000000000..13089943582b6 --- /dev/null +++ b/packages/notebook/.eslintrc.js @@ -0,0 +1,10 @@ +/** @type {import('eslint').Linter.Config} */ +module.exports = { + extends: [ + '../../configs/build.eslintrc.json' + ], + parserOptions: { + tsconfigRootDir: __dirname, + project: 'tsconfig.json' + } +}; diff --git a/packages/notebook/README.md b/packages/notebook/README.md new file mode 100644 index 0000000000000..7dd65bc96ec64 --- /dev/null +++ b/packages/notebook/README.md @@ -0,0 +1,30 @@ +
+ +
+ +theia-ext-logo + +

ECLIPSE THEIA - NOTEBOOK EXTENSION

+ +
+ +
+ +## Description + +The `@theia/notebook` extension contributes functionality for integrated notebooks + +## Additional Information + +- [API documentation for `@theia/notebook`](https://eclipse-theia.github.io/theia/docs/next/modules/notebook.html) +- [Theia - GitHub](https://github.com/eclipse-theia/theia) +- [Theia - Website](https://theia-ide.org/) + +## License + +- [Eclipse Public License 2.0](http://www.eclipse.org/legal/epl-2.0/) +- [δΈ€ (Secondary) GNU General Public License, version 2 with the GNU Classpath Exception](https://projects.eclipse.org/license/secondary-gpl-2.0-cp) + +## Trademark +"Theia" is a trademark of the Eclipse Foundation +https://www.eclipse.org/theia diff --git a/packages/notebook/package.json b/packages/notebook/package.json new file mode 100644 index 0000000000000..34cc9629e9bbd --- /dev/null +++ b/packages/notebook/package.json @@ -0,0 +1,51 @@ +{ + "name": "@theia/notebook", + "version": "1.40.0", + "description": "Theia - Notebook Extension", + "dependencies": { + "@theia/core": "1.40.0", + "@theia/editor": "1.40.0", + "@theia/filesystem": "1.40.0", + "@theia/monaco": "1.40.0", + "uuid": "^8.3.2" + }, + "publishConfig": { + "access": "public" + }, + "theiaExtensions": [ + { + "frontend": "lib/browser/notebook-frontend-module" + } + ], + "keywords": [ + "theia-extension" + ], + "license": "EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0", + "repository": { + "type": "git", + "url": "https://github.com/eclipse-theia/theia.git" + }, + "bugs": { + "url": "https://github.com/eclipse-theia/theia/issues" + }, + "homepage": "https://github.com/eclipse-theia/theia", + "files": [ + "lib", + "src" + ], + "scripts": { + "build": "theiaext build", + "clean": "theiaext clean", + "compile": "theiaext compile", + "docs": "theiaext docs", + "lint": "theiaext lint", + "watch": "theiaext watch" + }, + "devDependencies": { + "@theia/ext-scripts": "1.40.0", + "@types/vscode-notebook-renderer": "^1.72.0" + }, + "nyc": { + "extends": "../../configs/nyc.json" + } +} diff --git a/packages/notebook/src/browser/contributions/notebook-actions-contribution.ts b/packages/notebook/src/browser/contributions/notebook-actions-contribution.ts new file mode 100644 index 0000000000000..abd9516f3b346 --- /dev/null +++ b/packages/notebook/src/browser/contributions/notebook-actions-contribution.ts @@ -0,0 +1,171 @@ +// ***************************************************************************** +// 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 { Command, CommandContribution, CommandRegistry, CompoundMenuNodeRole, MenuContribution, MenuModelRegistry, nls } from '@theia/core'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { ApplicationShell, codicon, CommonCommands } from '@theia/core/lib/browser'; +import { NotebookModel } from '../view-model/notebook-model'; +import { NotebookService } from '../service/notebook-service'; +import { CellEditType, CellKind } from '../../common'; +import { KernelPickerMRUStrategy, NotebookKernelQuickPickService } from '../service/notebook-kernel-quick-pick-service'; +import { NotebookExecutionService } from '../service/notebook-execution-service'; +import { NotebookEditorWidget } from '../notebook-editor-widget'; + +export namespace NotebookCommands { + export const ADD_NEW_CELL_COMMAND = Command.toDefaultLocalizedCommand({ + id: 'notebook.add-new-cell', + iconClass: codicon('add') + }); + + export const ADD_NEW_MARKDOWN_CELL_COMMAND = Command.toDefaultLocalizedCommand({ + id: 'notebook.add-new-markdown-cell', + iconClass: codicon('add') + }); + + export const ADD_NEW_CODE_CELL_COMMAND = Command.toDefaultLocalizedCommand({ + id: 'notebook.add-new-code-cell', + iconClass: codicon('add') + }); + + export const SELECT_KERNEL_COMMAND = Command.toDefaultLocalizedCommand({ + id: 'notebook.selectKernel', + category: 'Notebook', + iconClass: codicon('server-environment') + }); + + export const EXECUTE_NOTEBOOK_COMMAND = Command.toDefaultLocalizedCommand({ + id: 'notebook.execute', + category: 'Notebook', + iconClass: codicon('run-all') + }); + + export const CLEAR_ALL_OUTPUTS_COMMAND = Command.toDefaultLocalizedCommand({ + id: 'notebook.clear-all-outputs', + category: 'Notebook', + iconClass: codicon('clear-all') + }); +} + +@injectable() +export class NotebookActionsContribution implements CommandContribution, MenuContribution { + + @inject(NotebookService) + protected notebookService: NotebookService; + + @inject(NotebookKernelQuickPickService) + protected notebookKernelQuickPickService: KernelPickerMRUStrategy; + + @inject(NotebookExecutionService) + protected notebookExecutionService: NotebookExecutionService; + + @inject(ApplicationShell) + protected shell: ApplicationShell; + + registerCommands(commands: CommandRegistry): void { + commands.registerCommand(NotebookCommands.ADD_NEW_CELL_COMMAND, { + execute: (notebookModel: NotebookModel, cellKind: CellKind, index?: number) => { + const insertIndex = index ?? (notebookModel.selectedCell ? notebookModel.cells.indexOf(notebookModel.selectedCell) : 0); + let firstCodeCell; + if (cellKind === CellKind.Code) { + firstCodeCell = notebookModel.cells.find(cell => cell.cellKind === CellKind.Code); + } + + notebookModel.applyEdits([{ + editType: CellEditType.Replace, + index: insertIndex, + count: 0, + cells: [{ + cellKind, + language: firstCodeCell?.language ?? 'markdown', + source: '', + outputs: [], + metadata: {}, + }] + }], true); + } + }); + + commands.registerCommand(NotebookCommands.ADD_NEW_MARKDOWN_CELL_COMMAND, { + execute: (notebookModel: NotebookModel) => commands.executeCommand(NotebookCommands.ADD_NEW_CELL_COMMAND.id, notebookModel, CellKind.Markup) + }); + + commands.registerCommand(NotebookCommands.ADD_NEW_CODE_CELL_COMMAND, { + execute: (notebookModel: NotebookModel) => commands.executeCommand(NotebookCommands.ADD_NEW_CELL_COMMAND.id, notebookModel, CellKind.Code) + }); + + commands.registerCommand(NotebookCommands.SELECT_KERNEL_COMMAND, { + execute: (notebookModel: NotebookModel) => this.notebookKernelQuickPickService.showQuickPick(notebookModel) + }); + + commands.registerCommand(NotebookCommands.EXECUTE_NOTEBOOK_COMMAND, { + execute: (notebookModel: NotebookModel) => this.notebookExecutionService.executeNotebookCells(notebookModel, notebookModel.cells) + }); + + commands.registerCommand(NotebookCommands.CLEAR_ALL_OUTPUTS_COMMAND, { + execute: (notebookModel: NotebookModel) => + notebookModel.cells.forEach(cell => cell.spliceNotebookCellOutputs({ start: 0, deleteCount: cell.outputs.length, newOutputs: [] })) + }); + + commands.registerHandler(CommonCommands.UNDO.id, { + isEnabled: () => this.shell.activeWidget instanceof NotebookEditorWidget, + execute: () => (this.shell.activeWidget as NotebookEditorWidget).undo() + }); + commands.registerHandler(CommonCommands.REDO.id, { + isEnabled: () => this.shell.activeWidget instanceof NotebookEditorWidget, + execute: () => (this.shell.activeWidget as NotebookEditorWidget).redo() + }); + } + + registerMenus(menus: MenuModelRegistry): void { + // independent submenu for plugins to add commands + menus.registerIndependentSubmenu(NotebookMenus.NOTEBOOK_MAIN_TOOLBAR, 'Notebook Main Toolbar'); + // Add Notebook Cell items + menus.registerSubmenu(NotebookMenus.NOTEBOOK_MAIN_TOOLBAR_CELL_ADD_GROUP, 'Add Notebook Cell', { role: CompoundMenuNodeRole.Group }); + menus.registerMenuAction(NotebookMenus.NOTEBOOK_MAIN_TOOLBAR_CELL_ADD_GROUP, { + commandId: NotebookCommands.ADD_NEW_CODE_CELL_COMMAND.id, + label: nls.localizeByDefault('Code'), + icon: codicon('add'), + }); + menus.registerMenuAction(NotebookMenus.NOTEBOOK_MAIN_TOOLBAR_CELL_ADD_GROUP, { + commandId: NotebookCommands.ADD_NEW_MARKDOWN_CELL_COMMAND.id, + label: nls.localizeByDefault('Markdown'), + icon: codicon('add'), + }); + + // Execution related items + menus.registerSubmenu(NotebookMenus.NOTEBOOK_MAIN_TOOLBAR_EXECUTION_GROUP, 'Cell Execution', { role: CompoundMenuNodeRole.Group }); + menus.registerMenuAction(NotebookMenus.NOTEBOOK_MAIN_TOOLBAR_EXECUTION_GROUP, { + commandId: NotebookCommands.EXECUTE_NOTEBOOK_COMMAND.id, + label: nls.localizeByDefault('Run All'), + icon: codicon('run-all'), + order: '10' + }); + menus.registerMenuAction(NotebookMenus.NOTEBOOK_MAIN_TOOLBAR_EXECUTION_GROUP, { + commandId: NotebookCommands.CLEAR_ALL_OUTPUTS_COMMAND.id, + label: nls.localizeByDefault('Clear All Outputs'), + icon: codicon('clear-all'), + order: '30' + }); + // other items + } + +} + +export namespace NotebookMenus { + export const NOTEBOOK_MAIN_TOOLBAR = 'notebook/toolbar'; + export const NOTEBOOK_MAIN_TOOLBAR_CELL_ADD_GROUP = [NOTEBOOK_MAIN_TOOLBAR, 'cell-add-group']; + export const NOTEBOOK_MAIN_TOOLBAR_EXECUTION_GROUP = [NOTEBOOK_MAIN_TOOLBAR, 'cell-execution-group']; +} diff --git a/packages/notebook/src/browser/contributions/notebook-cell-actions-contribution.ts b/packages/notebook/src/browser/contributions/notebook-cell-actions-contribution.ts new file mode 100644 index 0000000000000..d06d0d94c0dfc --- /dev/null +++ b/packages/notebook/src/browser/contributions/notebook-cell-actions-contribution.ts @@ -0,0 +1,204 @@ +// ***************************************************************************** +// 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 { Command, CommandContribution, CommandRegistry, CompoundMenuNodeRole, MenuContribution, MenuModelRegistry, nls } from '@theia/core'; +import { codicon } from '@theia/core/lib/browser'; +import { inject, injectable, postConstruct } from '@theia/core/shared/inversify'; +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, NOTEBOOK_CELL_EXECUTING } from './notebook-context-keys'; +import { ContextKeyService } from '@theia/core/lib/browser/context-key-service'; +import { NotebookExecutionService } from '../service/notebook-execution-service'; +import { NotebookCellOutputModel } from '../view-model/notebook-cell-output-model'; +import { CellEditType } from '../../common'; + +export namespace NotebookCellCommands { + export const EDIT_COMMAND = Command.toDefaultLocalizedCommand({ + id: 'notebook.cell.edit', + iconClass: codicon('edit') + }); + export const STOP_EDIT_COMMAND = Command.toDefaultLocalizedCommand({ + id: 'notebook.cell.stop-edit', + iconClass: codicon('check') + }); + export const DELETE_COMMAND = Command.toDefaultLocalizedCommand({ + id: 'notebook.cell.delete', + iconClass: codicon('trash') + }); + export const SPLIT_CELL_COMMAND = Command.toDefaultLocalizedCommand({ + 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'), + }); + export const STOP_CELL_EXECUTION_COMMAND = Command.toDefaultLocalizedCommand({ + id: 'notebook.cell.stop-cell-execution', + iconClass: codicon('stop'), + }); + + export const CLEAR_OUTPUTS_COMMAND = Command.toDefaultLocalizedCommand({ + id: 'notebook.cell.clear-outputs', + label: 'Clear Cell Outputs', + }); + export const CHANGE_OUTPUT_PRESENTATION_COMMAND = Command.toDefaultLocalizedCommand({ + id: 'notebook.cell.change-presentation', + label: 'Change Presentation', + }); +} + +@injectable() +export class NotebookCellActionContribution implements MenuContribution, CommandContribution { + + @inject(ContextKeyService) + protected contextKeyService: ContextKeyService; + + @inject(NotebookExecutionService) + protected notebookExecutionService: NotebookExecutionService; + + @postConstruct() + protected init(): void { + NotebookContextKeys.initNotebookContextKeys(this.contextKeyService); + } + + registerMenus(menus: MenuModelRegistry): void { + menus.registerMenuAction(NotebookCellActionContribution.ACTION_MENU, { + commandId: NotebookCellCommands.EDIT_COMMAND.id, + icon: NotebookCellCommands.EDIT_COMMAND.iconClass, + when: `${NOTEBOOK_CELL_TYPE} == 'markdown' && !${NOTEBOOK_CELL_MARKDOWN_EDIT_MODE}`, + label: nls.localizeByDefault('Edit Cell'), + order: '10' + }); + menus.registerMenuAction(NotebookCellActionContribution.ACTION_MENU, { + commandId: NotebookCellCommands.STOP_EDIT_COMMAND.id, + icon: NotebookCellCommands.STOP_EDIT_COMMAND.iconClass, + when: `${NOTEBOOK_CELL_TYPE} == 'markdown' && ${NOTEBOOK_CELL_MARKDOWN_EDIT_MODE}`, + label: nls.localizeByDefault('Stop Editing Cell'), + order: '10' + }); + menus.registerMenuAction(NotebookCellActionContribution.ACTION_MENU, { + commandId: NotebookCellCommands.EXECUTE_SINGLE_CELL_COMMAND.id, + icon: NotebookCellCommands.EXECUTE_SINGLE_CELL_COMMAND.iconClass, + when: `${NOTEBOOK_CELL_TYPE} == 'code'`, + label: nls.localizeByDefault('Execute Cell'), + order: '10' + }); + + menus.registerMenuAction(NotebookCellActionContribution.ACTION_MENU, { + commandId: NotebookCellCommands.SPLIT_CELL_COMMAND.id, + icon: NotebookCellCommands.SPLIT_CELL_COMMAND.iconClass, + label: nls.localizeByDefault('Split Cell'), + order: '20' + }); + menus.registerMenuAction(NotebookCellActionContribution.ACTION_MENU, { + commandId: NotebookCellCommands.DELETE_COMMAND.id, + icon: NotebookCellCommands.DELETE_COMMAND.iconClass, + label: nls.localizeByDefault('Delete Cell'), + order: '999' + }); + + menus.registerSubmenu( + NotebookCellActionContribution.ADDITIONAL_ACTION_MENU, + nls.localizeByDefault('More'), + { + icon: codicon('ellipsis'), + role: CompoundMenuNodeRole.Submenu, + order: '30' + } + ); + + menus.registerIndependentSubmenu(NotebookCellActionContribution.CONTRIBUTED_CELL_ACTION_MENU, '', { role: CompoundMenuNodeRole.Flat }); + // since contributions are adding to an independent submenu we have to manually add it to the more submenu + menus.getMenu(NotebookCellActionContribution.ADDITIONAL_ACTION_MENU).addNode(menus.getMenuNode(NotebookCellActionContribution.CONTRIBUTED_CELL_ACTION_MENU)); + + // code cell sidebar menu + menus.registerMenuAction(NotebookCellActionContribution.CODE_CELL_SIDEBAR_MENU, { + commandId: NotebookCellCommands.EXECUTE_SINGLE_CELL_COMMAND.id, + icon: NotebookCellCommands.EXECUTE_SINGLE_CELL_COMMAND.iconClass, + label: nls.localizeByDefault('Execute Cell'), + when: `!${NOTEBOOK_CELL_EXECUTING}` + }); + menus.registerMenuAction(NotebookCellActionContribution.CODE_CELL_SIDEBAR_MENU, { + commandId: NotebookCellCommands.STOP_CELL_EXECUTION_COMMAND.id, + icon: NotebookCellCommands.STOP_CELL_EXECUTION_COMMAND.iconClass, + label: nls.localizeByDefault('Stop Cell Execution'), + when: NOTEBOOK_CELL_EXECUTING + }); + + // Notebook Cell extra execution options + menus.registerIndependentSubmenu(NotebookCellActionContribution.CONTRIBUTED_CELL_EXECUTION_MENU, + nls.localizeByDefault('More...'), + { role: CompoundMenuNodeRole.Flat, icon: codicon('chevron-down') }); + menus.getMenu(NotebookCellActionContribution.CODE_CELL_SIDEBAR_MENU).addNode(menus.getMenuNode(NotebookCellActionContribution.CONTRIBUTED_CELL_EXECUTION_MENU)); + + // code cell output sidebar menu + menus.registerSubmenu( + NotebookCellActionContribution.ADDITIONAL_OUTPUT_SIDEBAR_MENU, + nls.localizeByDefault('More'), + { + icon: codicon('ellipsis'), + role: CompoundMenuNodeRole.Submenu + }); + menus.registerMenuAction(NotebookCellActionContribution.ADDITIONAL_OUTPUT_SIDEBAR_MENU, { + commandId: NotebookCellCommands.CLEAR_OUTPUTS_COMMAND.id, + label: nls.localizeByDefault('Clear Cell Outputs'), + }); + menus.registerMenuAction(NotebookCellActionContribution.ADDITIONAL_OUTPUT_SIDEBAR_MENU, { + commandId: NotebookCellCommands.CHANGE_OUTPUT_PRESENTATION_COMMAND.id, + label: nls.localizeByDefault('Change Presentation'), + }); + + } + + registerCommands(commands: CommandRegistry): void { + 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: (notebookModel: NotebookModel, cell: NotebookCellModel) => notebookModel.applyEdits([{ + editType: CellEditType.Replace, + index: notebookModel.cells.indexOf(cell), + count: 1, + cells: [] + }], true) + }); + commands.registerCommand(NotebookCellCommands.SPLIT_CELL_COMMAND); + + commands.registerCommand(NotebookCellCommands.EXECUTE_SINGLE_CELL_COMMAND, { + execute: (notebookModel: NotebookModel, cell: NotebookCellModel) => this.notebookExecutionService.executeNotebookCells(notebookModel, [cell]) + }); + commands.registerCommand(NotebookCellCommands.STOP_CELL_EXECUTION_COMMAND, { + execute: (notebookModel: NotebookModel, cell: NotebookCellModel) => this.notebookExecutionService.cancelNotebookCells(notebookModel, [cell]) + }); + commands.registerCommand(NotebookCellCommands.CLEAR_OUTPUTS_COMMAND, { + execute: (notebookModel: NotebookModel, cell: NotebookCellModel) => cell.spliceNotebookCellOutputs({ start: 0, deleteCount: cell.outputs.length, newOutputs: [] }) + }); + commands.registerCommand(NotebookCellCommands.CHANGE_OUTPUT_PRESENTATION_COMMAND, { + execute: (_, __, output: NotebookCellOutputModel) => output.requestOutputPresentationUpdate() + }); + } +} + +export namespace NotebookCellActionContribution { + export const ACTION_MENU = ['notebook-cell-actions-menu']; + export const ADDITIONAL_ACTION_MENU = [...ACTION_MENU, 'more']; + export const CONTRIBUTED_CELL_ACTION_MENU = 'notebook/cell/title'; + export const CONTRIBUTED_CELL_EXECUTION_MENU = 'notebook/cell/execute'; + export const CODE_CELL_SIDEBAR_MENU = ['code-cell-sidebar-menu']; + export const OUTPUT_SIDEBAR_MENU = ['code-cell-output-sidebar-menu']; + export const ADDITIONAL_OUTPUT_SIDEBAR_MENU = [...OUTPUT_SIDEBAR_MENU, 'more']; + +} diff --git a/packages/notebook/src/browser/contributions/notebook-color-contribution.ts b/packages/notebook/src/browser/contributions/notebook-color-contribution.ts new file mode 100644 index 0000000000000..0e7ca598fa36b --- /dev/null +++ b/packages/notebook/src/browser/contributions/notebook-color-contribution.ts @@ -0,0 +1,268 @@ +// ***************************************************************************** +// 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 { ColorContribution } from '@theia/core/lib/browser/color-application-contribution'; +import { ColorRegistry } from '@theia/core/lib/browser/color-registry'; +import { Color } from '@theia/core/lib/common/color'; +import { injectable } from '@theia/core/shared/inversify'; + +@injectable() +export class NotebookColorContribution implements ColorContribution { + registerColors(colors: ColorRegistry): void { + colors.register( + { + id: 'notebook.cellBorderColor', + defaults: { + dark: Color.transparent('list.inactiveSelectionBackground', 1), + light: Color.transparent('list.inactiveSelectionBackground', 1), + hcDark: 'panel.border', + hcLight: 'panel.border' + }, + description: 'The border color for notebook cells.' + }, + { + id: 'notebook.focusedEditorBorder', + defaults: { + dark: 'focusBorder', + light: 'focusBorder', + hcDark: 'focusBorder', + hcLight: 'focusBorder' + }, + description: 'The color of the notebook cell editor border.' + }, + { + id: 'notebookStatusSuccessIcon.foreground', + defaults: { + dark: 'debugIcon.startForeground', + light: 'debugIcon.startForeground', + hcDark: 'debugIcon.startForeground', + hcLight: 'debugIcon.startForeground' + }, + description: 'The error icon color of notebook cells in the cell status bar.' + }, + { + id: 'notebookEditorOverviewRuler.runningCellForeground', + defaults: { + dark: 'debugIcon.startForeground', + light: 'debugIcon.startForeground', + hcDark: 'debugIcon.startForeground', + hcLight: 'debugIcon.startForeground' + }, + description: 'The color of the running cell decoration in the notebook editor overview ruler.' + }, + { + id: 'notebookStatusErrorIcon.foreground', + defaults: { + dark: 'errorForeground', + light: 'errorForeground', + hcDark: 'errorForeground', + hcLight: 'errorForeground' + }, + description: 'The error icon color of notebook cells in the cell status bar.' + }, + { + id: 'notebookStatusRunningIcon.foreground', + defaults: { + dark: 'foreground', + light: 'foreground', + hcDark: 'foreground', + hcLight: 'foreground' + }, + description: 'The running icon color of notebook cells in the cell status bar.' + }, + { + id: 'notebook.outputContainerBorderColor', + defaults: { + dark: undefined, + light: undefined, + hcDark: undefined, + hcLight: undefined + }, + description: 'The border color of the notebook output container.' + }, + { + id: 'notebook.outputContainerBackgroundColor', + defaults: { + dark: undefined, + light: undefined, + hcDark: undefined, + hcLight: undefined + }, + description: 'The color of the notebook output container background.' + }, + { + id: 'notebook.cellToolbarSeparator', + defaults: { + dark: Color.rgba(128, 128, 128, 0.35), + light: Color.rgba(128, 128, 128, 0.35), + hcDark: 'contrastBorder', + hcLight: 'contrastBorder' + }, + description: 'The color of the separator in the cell bottom toolbar' + }, + { + id: 'notebook.focusedCellBackground', + defaults: { + dark: undefined, + light: undefined, + hcDark: undefined, + hcLight: undefined + }, + description: 'The background color of a cell when the cell is focused.' + }, + { + id: 'notebook.selectedCellBackground', + defaults: { + dark: 'list.inactiveSelectionBackground', + light: 'list.inactiveSelectionBackground', + hcDark: undefined, + hcLight: undefined + }, + description: 'The background color of a cell when the cell is selected.' + }, + { + id: 'notebook.cellHoverBackground', + defaults: { + dark: Color.transparent('notebook.focusedCellBackground', 0.5), + light: Color.transparent('notebook.focusedCellBackground', 0.7), + hcDark: undefined, + hcLight: undefined + }, + description: 'The background color of a cell when the cell is hovered.' + }, + { + id: 'notebook.selectedCellBorder', + defaults: { + dark: 'notebook.cellBorderColor', + light: 'notebook.cellBorderColor', + hcDark: 'contrastBorder', + hcLight: 'contrastBorder' + }, + description: "The color of the cell's top and bottom border when the cell is selected but not focused." + }, + { + id: 'notebook.inactiveSelectedCellBorder', + defaults: { + dark: undefined, + light: undefined, + hcDark: 'focusBorder', + hcLight: 'focusBorder' + }, + description: "The color of the cell's borders when multiple cells are selected." + }, + { + id: 'notebook.focusedCellBorder', + defaults: { + dark: 'focusBorder', + light: 'focusBorder', + hcDark: 'focusBorder', + hcLight: 'focusBorder' + }, + description: "The color of the cell's focus indicator borders when the cell is focused." + }, + { + id: 'notebook.inactiveFocusedCellBorder', + defaults: { + dark: 'notebook.cellBorderColor', + light: 'notebook.cellBorderColor', + hcDark: 'notebook.cellBorderColor', + hcLight: 'notebook.cellBorderColor' + }, + description: "The color of the cell's top and bottom border when a cell is focused while the primary focus is outside of the editor." + }, + { + id: 'notebook.cellStatusBarItemHoverBackground', + defaults: { + dark: Color.rgba(0, 0, 0, 0.08), + light: Color.rgba(255, 255, 255, 0.15), + hcDark: Color.rgba(0, 0, 0, 0.08), + hcLight: Color.rgba(255, 255, 255, 0.15) + }, + description: 'The background color of notebook cell status bar items.' + }, + { + id: 'notebook.cellInsertionIndicator', + defaults: { + dark: 'focusBorder', + light: 'focusBorder', + hcDark: 'focusBorder', + hcLight: undefined + }, + description: 'Notebook background color.' + }, + { + id: 'notebookScrollbarSlider.background', + defaults: { + dark: 'scrollbarSlider.background', + light: 'scrollbarSlider.background', + hcDark: 'scrollbarSlider.background', + hcLight: 'scrollbarSlider.background' + }, + description: 'Notebook scrollbar slider background color.' + }, + { + id: 'notebookScrollbarSlider.hoverBackground', + defaults: { + dark: 'scrollbarSlider.hoverBackground', + light: 'scrollbarSlider.hoverBackground', + hcDark: 'scrollbarSlider.hoverBackground', + hcLight: 'scrollbarSlider.hoverBackground' + }, + description: 'Notebook scrollbar slider background color when hovering.' + }, + { + id: 'notebookScrollbarSlider.activeBackground', + defaults: { + dark: 'scrollbarSlider.activeBackground', + light: 'scrollbarSlider.activeBackground', + hcDark: 'scrollbarSlider.activeBackground', + hcLight: 'scrollbarSlider.activeBackground' + }, + description: 'Notebook scrollbar slider background color when clicked on.' + }, + { + id: 'notebook.symbolHighlightBackground', + defaults: { + dark: Color.rgba(255, 255, 255, 0.04), + light: Color.rgba(253, 255, 0, 0.2), + hcDark: undefined, + hcLight: undefined + }, + description: 'Background color of highlighted cell' + }, + { + id: 'notebook.cellEditorBackground', + defaults: { + dark: 'sideBar.background', + light: 'sideBar.background', + hcDark: undefined, + hcLight: undefined + }, + description: 'Cell editor background color.' + }, + { + id: 'notebook.editorBackground', + defaults: { + dark: 'editorPane.background', + light: 'editorPane.background', + hcDark: undefined, + hcLight: undefined + }, + description: 'Notebook background color.' + } + ); + } +} diff --git a/packages/notebook/src/browser/contributions/notebook-context-keys.ts b/packages/notebook/src/browser/contributions/notebook-context-keys.ts new file mode 100644 index 0000000000000..aff494546161f --- /dev/null +++ b/packages/notebook/src/browser/contributions/notebook-context-keys.ts @@ -0,0 +1,99 @@ +// ***************************************************************************** +// 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 { ContextKeyService } from '@theia/core/lib/browser/context-key-service'; + +export type NotebookCellExecutionStateContext = 'idle' | 'pending' | 'executing' | 'succeeded' | 'failed'; + +export const HAS_OPENED_NOTEBOOK = 'userHasOpenedNotebook'; +export const KEYBINDING_CONTEXT_NOTEBOOK_FIND_WIDGET_FOCUSED = 'notebookFindWidgetFocused'; +export const NOTEBOOK_EDITOR_FOCUSED = 'notebookEditorFocused'; +export const NOTEBOOK_CELL_LIST_FOCUSED = 'notebookCellListFocused'; +export const NOTEBOOK_OUTPUT_FOCUSED = 'notebookOutputFocused'; +export const NOTEBOOK_EDITOR_EDITABLE = 'notebookEditable'; +export const NOTEBOOK_HAS_RUNNING_CELL = 'notebookHasRunningCell'; +export const NOTEBOOK_USE_CONSOLIDATED_OUTPUT_BUTTON = 'notebookUseConsolidatedOutputButton'; +export const NOTEBOOK_BREAKPOINT_MARGIN_ACTIVE = 'notebookBreakpointMargin'; +export const NOTEBOOK_CELL_TOOLBAR_LOCATION = 'notebookCellToolbarLocation'; +export const NOTEBOOK_CURSOR_NAVIGATION_MODE = 'notebookCursorNavigationMode'; +export const NOTEBOOK_LAST_CELL_FAILED = 'notebookLastCellFailed'; +export const NOTEBOOK_VIEW_TYPE = 'notebookType'; +export const NOTEBOOK_CELL_TYPE = 'notebookCellType'; +export const NOTEBOOK_CELL_EDITABLE = 'notebookCellEditable'; +export const NOTEBOOK_CELL_FOCUSED = 'notebookCellFocused'; +export const NOTEBOOK_CELL_EDITOR_FOCUSED = 'notebookCellEditorFocused'; +export const NOTEBOOK_CELL_MARKDOWN_EDIT_MODE = 'notebookCellMarkdownEditMode'; +export const NOTEBOOK_CELL_LINE_NUMBERS = 'notebookCellLineNumbers'; +export const NOTEBOOK_CELL_EXECUTION_STATE = 'notebookCellExecutionState'; +export const NOTEBOOK_CELL_EXECUTING = 'notebookCellExecuting'; +export const NOTEBOOK_CELL_HAS_OUTPUTS = 'notebookCellHasOutputs'; +export const NOTEBOOK_CELL_INPUT_COLLAPSED = 'notebookCellInputIsCollapsed'; +export const NOTEBOOK_CELL_OUTPUT_COLLAPSED = 'notebookCellOutputIsCollapsed'; +export const NOTEBOOK_CELL_RESOURCE = 'notebookCellResource'; +export const NOTEBOOK_KERNEL = 'notebookKernel'; +export const NOTEBOOK_KERNEL_COUNT = 'notebookKernelCount'; +export const NOTEBOOK_KERNEL_SOURCE_COUNT = 'notebookKernelSourceCount'; +export const NOTEBOOK_KERNEL_SELECTED = 'notebookKernelSelected'; +export const NOTEBOOK_INTERRUPTIBLE_KERNEL = 'notebookInterruptibleKernel'; +export const NOTEBOOK_MISSING_KERNEL_EXTENSION = 'notebookMissingKernelExtension'; +export const NOTEBOOK_HAS_OUTPUTS = 'notebookHasOutputs'; + +export namespace NotebookContextKeys { + export function initNotebookContextKeys(service: ContextKeyService): void { + service.createKey(HAS_OPENED_NOTEBOOK, false); + service.createKey(KEYBINDING_CONTEXT_NOTEBOOK_FIND_WIDGET_FOCUSED, false); + + // // Is Notebook + // export const NOTEBOOK_IS_ACTIVE_EDITOR = ContextKeyExpr.equals('activeEditor', NOTEBOOK_EDITOR_ID); + // export const INTERACTIVE_WINDOW_IS_ACTIVE_EDITOR = ContextKeyExpr.equals('activeEditor', INTERACTIVE_WINDOW_EDITOR_ID); + + // Editor keys + service.createKey(NOTEBOOK_EDITOR_FOCUSED, false); + service.createKey(NOTEBOOK_CELL_LIST_FOCUSED, false); + service.createKey(NOTEBOOK_OUTPUT_FOCUSED, false); + service.createKey(NOTEBOOK_EDITOR_EDITABLE, true); + service.createKey(NOTEBOOK_HAS_RUNNING_CELL, false); + service.createKey(NOTEBOOK_USE_CONSOLIDATED_OUTPUT_BUTTON, false); + service.createKey(NOTEBOOK_BREAKPOINT_MARGIN_ACTIVE, false); + service.createKey(NOTEBOOK_CELL_TOOLBAR_LOCATION, 'left'); + service.createKey(NOTEBOOK_CURSOR_NAVIGATION_MODE, false); + service.createKey(NOTEBOOK_LAST_CELL_FAILED, false); + + // Cell keys + service.createKey(NOTEBOOK_VIEW_TYPE, undefined); + service.createKey(NOTEBOOK_CELL_TYPE, undefined); + service.createKey(NOTEBOOK_CELL_EDITABLE, false); + service.createKey(NOTEBOOK_CELL_FOCUSED, false); + service.createKey(NOTEBOOK_CELL_EDITOR_FOCUSED, false); + service.createKey(NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, false); + service.createKey(NOTEBOOK_CELL_LINE_NUMBERS, 'inherit'); + service.createKey(NOTEBOOK_CELL_EXECUTION_STATE, undefined); + service.createKey(NOTEBOOK_CELL_EXECUTING, false); + service.createKey(NOTEBOOK_CELL_HAS_OUTPUTS, false); + service.createKey(NOTEBOOK_CELL_INPUT_COLLAPSED, false); + service.createKey(NOTEBOOK_CELL_OUTPUT_COLLAPSED, false); + service.createKey(NOTEBOOK_CELL_RESOURCE, ''); + + // Kernels + service.createKey(NOTEBOOK_KERNEL, undefined); + service.createKey(NOTEBOOK_KERNEL_COUNT, 0); + service.createKey(NOTEBOOK_KERNEL_SOURCE_COUNT, 0); + service.createKey(NOTEBOOK_KERNEL_SELECTED, false); + service.createKey(NOTEBOOK_INTERRUPTIBLE_KERNEL, false); + service.createKey(NOTEBOOK_MISSING_KERNEL_EXTENSION, false); + service.createKey(NOTEBOOK_HAS_OUTPUTS, false); + } +} diff --git a/packages/notebook/src/browser/index.ts b/packages/notebook/src/browser/index.ts new file mode 100644 index 0000000000000..74299913c414f --- /dev/null +++ b/packages/notebook/src/browser/index.ts @@ -0,0 +1,26 @@ +// ***************************************************************************** +// 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 +// ***************************************************************************** + +export * from './notebook-type-registry'; +export * from './notebook-renderer-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'; +export * from './service/notebook-renderer-messaging-service'; +export * from './renderers/cell-output-webview'; diff --git a/packages/notebook/src/browser/notebook-cell-resource-resolver.ts b/packages/notebook/src/browser/notebook-cell-resource-resolver.ts new file mode 100644 index 0000000000000..c09d85f3a30b2 --- /dev/null +++ b/packages/notebook/src/browser/notebook-cell-resource-resolver.ts @@ -0,0 +1,79 @@ +// ***************************************************************************** +// 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 { Emitter, Resource, ResourceReadOptions, ResourceResolver, ResourceVersion, URI } from '@theia/core'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { CellUri } from '../common'; +import { NotebookService } from './service/notebook-service'; +import { NotebookCellModel } from './view-model/notebook-cell-model'; + +export class NotebookCellResource implements Resource { + + protected readonly didChangeContentsEmitter = new Emitter(); + readonly onDidChangeContents = this.didChangeContentsEmitter.event; + + version?: ResourceVersion | undefined; + encoding?: string | undefined; + isReadonly?: boolean | undefined; + + private cell: NotebookCellModel; + + constructor(public uri: URI, cell: NotebookCellModel) { + this.cell = cell; + } + + readContents(options?: ResourceReadOptions | undefined): Promise { + return Promise.resolve(this.cell.source); + } + + dispose(): void { + this.didChangeContentsEmitter.dispose(); + } + +} + +@injectable() +export class NotebookCellResourceResolver implements ResourceResolver { + + @inject(NotebookService) + protected readonly notebookService: NotebookService; + + async resolve(uri: URI): Promise { + if (uri.scheme !== CellUri.scheme) { + throw new Error(`Cannot resolve cell uri with scheme '${uri.scheme}'`); + } + + const parsedUri = CellUri.parse(uri); + if (!parsedUri) { + throw new Error(`Cannot parse uri '${uri.toString()}'`); + } + + const notebookModel = this.notebookService.getNotebookEditorModel(parsedUri.notebook); + + if (!notebookModel) { + throw new Error(`No notebook found for uri '${parsedUri.notebook}'`); + } + + const notebookCellModel = notebookModel.cells.find(cell => cell.handle === parsedUri.handle); + + if (!notebookCellModel) { + throw new Error(`No cell found with handle '${parsedUri.handle}' in '${parsedUri.notebook}'`); + } + + return new NotebookCellResource(uri, notebookCellModel); + } + +} diff --git a/packages/notebook/src/browser/notebook-editor-widget-factory.ts b/packages/notebook/src/browser/notebook-editor-widget-factory.ts new file mode 100644 index 0000000000000..d17832b56fd8a --- /dev/null +++ b/packages/notebook/src/browser/notebook-editor-widget-factory.ts @@ -0,0 +1,70 @@ +// ***************************************************************************** +// 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'; +import { WidgetFactory, NavigatableWidgetOptions, LabelProvider } from '@theia/core/lib/browser'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { NotebookEditorWidget, NotebookEditorContainerFactory, NotebookEditorProps } from './notebook-editor-widget'; +import { NotebookService } from './service/notebook-service'; +import { NotebookModelResolverService } from './service/notebook-model-resolver-service'; + +export interface NotebookEditorWidgetOptions extends NavigatableWidgetOptions { + notebookType: string; +} + +@injectable() +export class NotebookEditorWidgetFactory implements WidgetFactory { + readonly id: string = NotebookEditorWidget.ID; + + @inject(NotebookService) + protected readonly notebookService: NotebookService; + + @inject(NotebookModelResolverService) + protected readonly notebookModelResolver: NotebookModelResolverService; + + @inject(LabelProvider) + protected readonly labelProvider: LabelProvider; + + @inject(NotebookEditorContainerFactory) + protected readonly createNotebookEditorWidget: (props: NotebookEditorProps) => NotebookEditorWidget; + + async createWidget(options?: NotebookEditorWidgetOptions): Promise { + if (!options) { + throw new Error('no options found for widget. Need at least uri and notebookType'); + } + const uri = new URI(options.uri); + + await this.notebookService.willOpenNotebook(options.notebookType); + + const editor = await this.createEditor(uri, options.notebookType); + + const icon = this.labelProvider.getIcon(uri); + editor.title.label = this.labelProvider.getName(uri); + editor.title.iconClass = icon + ' file-icon'; + + return editor; + } + + private async createEditor(uri: URI, notebookType: string): Promise { + + return this.createNotebookEditorWidget({ + uri, + notebookType, + notebookData: this.notebookModelResolver.resolve(uri, notebookType), + }); + } + +} diff --git a/packages/notebook/src/browser/notebook-editor-widget.tsx b/packages/notebook/src/browser/notebook-editor-widget.tsx new file mode 100644 index 0000000000000..7d600ce5afa08 --- /dev/null +++ b/packages/notebook/src/browser/notebook-editor-widget.tsx @@ -0,0 +1,157 @@ +// ***************************************************************************** +// 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 * as React from '@theia/core/shared/react'; +import { CommandRegistry, MenuModelRegistry, URI } from '@theia/core'; +import { ReactWidget, Navigatable, SaveableSource, Message, SaveableDelegate } 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'; +import { NotebookCodeCellRenderer } from './view/notebook-code-cell-view'; +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'; +import { NotebookMainToolbarRenderer } from './view/notebook-main-toolbar'; + +export const NotebookEditorContainerFactory = Symbol('NotebookModelFactory'); + +export function createNotebookEditorWidgetContainer(parent: interfaces.Container, props: NotebookEditorProps): interfaces.Container { + const child = parent.createChild(); + + child.bind(NotebookEditorProps).toConstantValue(props); + child.bind(NotebookEditorWidget).toSelf(); + + return child; +} + +const NotebookEditorProps = Symbol('NotebookEditorProps'); + +export interface NotebookEditorProps { + uri: URI, + readonly notebookType: string, + notebookData: Promise +} +export const NOTEBOOK_EDITOR_ID_PREFIX = 'notebook:'; + +@injectable() +export class NotebookEditorWidget extends ReactWidget implements Navigatable, SaveableSource { + static readonly ID = 'notebook'; + + readonly saveable = new SaveableDelegate(); + + @inject(NotebookCellToolbarFactory) + protected readonly cellToolbarFactory: NotebookCellToolbarFactory; + + @inject(CommandRegistry) + protected commandRegistry: CommandRegistry; + + @inject(MenuModelRegistry) + protected menuRegistry: MenuModelRegistry; + + @inject(NotebookEditorWidgetService) + protected notebookEditorService: NotebookEditorWidgetService; + + @inject(NotebookMainToolbarRenderer) + protected notebookMainToolbarRenderer: NotebookMainToolbarRenderer; + + protected readonly onDidChangeModelEmitter = new Emitter(); + readonly onDidChangeModel = this.onDidChangeModelEmitter.event; + + protected readonly renderers = new Map(); + protected _model?: NotebookModel; + + get notebookType(): string { + return this.props.notebookType; + } + + get model(): NotebookModel | undefined { + return this._model; + } + + constructor( + @inject(NotebookCodeCellRenderer) codeCellRenderer: NotebookCodeCellRenderer, + @inject(NotebookMarkdownCellRenderer) markdownCellRenderer: NotebookMarkdownCellRenderer, + @inject(NotebookEditorProps) private readonly props: NotebookEditorProps) { + super(); + this.id = NOTEBOOK_EDITOR_ID_PREFIX + this.props.uri.toString(); + this.node.tabIndex = -1; + + this.title.closable = true; + this.update(); + + this.toDispose.push(this.onDidChangeModelEmitter); + + this.renderers.set(CellKind.Markup, markdownCellRenderer); + this.renderers.set(CellKind.Code, codeCellRenderer); + this.waitForData(); + } + + protected async waitForData(): Promise { + this._model = await this.props.notebookData; + this.saveable.set(this._model); + this.toDispose.push(this._model); + // Ensure that the model is loaded before adding the editor + this.notebookEditorService.addNotebookEditor(this); + this.update(); + } + + protected override onActivateRequest(msg: Message): void { + super.onActivateRequest(msg); + this.node.focus(); + } + + getResourceUri(): URI | undefined { + return this.props.uri; + } + + createMoveToUri(resourceUri: URI): URI | undefined { + return this.props.uri; + } + + undo(): void { + this.model?.undo(); + } + + redo(): void { + this.model?.redo(); + } + + protected render(): ReactNode { + if (this._model) { + return
+ {this.notebookMainToolbarRenderer.render(this._model)} + +
; + } else { + return
; + } + } + + protected override onAfterAttach(msg: Message): void { + super.onAfterAttach(msg); + } + + 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 new file mode 100644 index 0000000000000..7746ad0f10ec8 --- /dev/null +++ b/packages/notebook/src/browser/notebook-frontend-module.ts @@ -0,0 +1,95 @@ +// ***************************************************************************** +// 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 '../../src/browser/style/index.css'; + +import { ContainerModule } from '@theia/core/shared/inversify'; +import { OpenHandler, WidgetFactory } from '@theia/core/lib/browser'; +import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution'; +import { NotebookOpenHandler } from './notebook-open-handler'; +import { CommandContribution, MenuContribution, ResourceResolver, } from '@theia/core'; +import { NotebookTypeRegistry } from './notebook-type-registry'; +import { NotebookRendererRegistry } from './notebook-renderer-registry'; +import { NotebookService } from './service/notebook-service'; +import { NotebookEditorWidgetFactory } from './notebook-editor-widget-factory'; +import { NotebookCellResourceResolver } from './notebook-cell-resource-resolver'; +import { NotebookModelResolverService } from './service/notebook-model-resolver-service'; +import { NotebookCellActionContribution } from './contributions/notebook-cell-actions-contribution'; +import { NotebookCellToolbarFactory } from './view/notebook-cell-toolbar-factory'; +import { createNotebookModelContainer, NotebookModel, NotebookModelFactory, NotebookModelProps } from './view-model/notebook-model'; +import { createNotebookCellModelContainer, NotebookCellModel, NotebookCellModelFactory, NotebookCellModelProps } from './view-model/notebook-cell-model'; +import { createNotebookEditorWidgetContainer, NotebookEditorContainerFactory, NotebookEditorProps, NotebookEditorWidget } from './notebook-editor-widget'; +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 { KernelPickerMRUStrategy, NotebookKernelQuickPickService } from './service/notebook-kernel-quick-pick-service'; +import { NotebookKernelHistoryService } from './service/notebook-kernel-history-service'; +import { NotebookEditorWidgetService } from './service/notebook-editor-service'; +import { NotebookRendererMessagingService } from './service/notebook-renderer-messaging-service'; +import { NotebookColorContribution } from './contributions/notebook-color-contribution'; +import { NotebookCellContextManager } from './service/notebook-cell-context-manager'; +import { NotebookMainToolbarRenderer } from './view/notebook-main-toolbar'; + +export default new ContainerModule(bind => { + bind(NotebookColorContribution).toSelf().inSingletonScope(); + bind(ColorContribution).toService(NotebookColorContribution); + + bind(NotebookOpenHandler).toSelf().inSingletonScope(); + bind(OpenHandler).toService(NotebookOpenHandler); + + bind(NotebookTypeRegistry).toSelf().inSingletonScope(); + bind(NotebookRendererRegistry).toSelf().inSingletonScope(); + + bind(WidgetFactory).to(NotebookEditorWidgetFactory).inSingletonScope(); + 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(NotebookRendererMessagingService).toSelf().inSingletonScope(); + bind(NotebookKernelHistoryService).toSelf().inSingletonScope(); + bind(NotebookKernelQuickPickService).to(KernelPickerMRUStrategy).inSingletonScope(); + + bind(NotebookCellResourceResolver).toSelf().inSingletonScope(); + bind(ResourceResolver).toService(NotebookCellResourceResolver); + bind(NotebookModelResolverService).toSelf().inSingletonScope(); + + bind(NotebookCellActionContribution).toSelf().inSingletonScope(); + bind(MenuContribution).toService(NotebookCellActionContribution); + bind(CommandContribution).toService(NotebookCellActionContribution); + + bind(NotebookActionsContribution).toSelf().inSingletonScope(); + bind(CommandContribution).toService(NotebookActionsContribution); + bind(MenuContribution).toService(NotebookActionsContribution); + + bind(NotebookCodeCellRenderer).toSelf().inSingletonScope(); + bind(NotebookMarkdownCellRenderer).toSelf().inSingletonScope(); + bind(NotebookMainToolbarRenderer).toSelf().inSingletonScope(); + + bind(NotebookEditorContainerFactory).toFactory(ctx => (props: NotebookEditorProps) => + createNotebookEditorWidgetContainer(ctx.container, props).get(NotebookEditorWidget) + ); + bind(NotebookModelFactory).toFactory(ctx => (props: NotebookModelProps) => + createNotebookModelContainer(ctx.container, props).get(NotebookModel) + ); + bind(NotebookCellModelFactory).toFactory(ctx => (props: NotebookCellModelProps) => + createNotebookCellModelContainer(ctx.container, props, NotebookCellContextManager).get(NotebookCellModel) + ); +}); diff --git a/packages/notebook/src/browser/notebook-open-handler.ts b/packages/notebook/src/browser/notebook-open-handler.ts new file mode 100644 index 0000000000000..7edb01ca97db5 --- /dev/null +++ b/packages/notebook/src/browser/notebook-open-handler.ts @@ -0,0 +1,87 @@ +// ***************************************************************************** +// 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, MaybePromise } from '@theia/core'; +import { NavigatableWidgetOpenHandler, WidgetOpenerOptions } from '@theia/core/lib/browser'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { NotebookFileSelector, NotebookTypeDescriptor } from '../common/notebook-protocol'; +import { NotebookTypeRegistry } from './notebook-type-registry'; +import { NotebookEditorWidget } from './notebook-editor-widget'; +import { match } from '@theia/core/lib/common/glob'; +import { NotebookEditorWidgetOptions } from './notebook-editor-widget-factory'; + +@injectable() +export class NotebookOpenHandler extends NavigatableWidgetOpenHandler { + + id: string = 'notebook'; + + constructor(@inject(NotebookTypeRegistry) private notebookTypeRegistry: NotebookTypeRegistry) { + super(); + } + + canHandle(uri: URI, options?: WidgetOpenerOptions | undefined): MaybePromise { + const priorities = this.notebookTypeRegistry.notebookTypes + .filter(notebook => notebook.selector && this.matches(notebook.selector, uri)) + .map(notebook => this.calculatePriority(notebook)); + return Math.max(...priorities); + } + + protected findHighestPriorityType(uri: URI): NotebookTypeDescriptor | undefined { + const matchingTypes = this.notebookTypeRegistry.notebookTypes + .filter(notebookType => notebookType.selector && this.matches(notebookType.selector, uri)) + .map(notebookType => ({ descriptor: notebookType, priority: this.calculatePriority(notebookType) })); + + if (matchingTypes.length === 0) { + return undefined; + } + let type = matchingTypes[0]; + for (let i = 1; i < matchingTypes.length; i++) { + const notebookType = matchingTypes[i]; + if (notebookType.priority > type.priority) { + type = notebookType; + } + } + return type.descriptor; + } + + protected calculatePriority(notebookType: NotebookTypeDescriptor | undefined): number { + if (!notebookType) { + return -1; + } + return notebookType.priority === 'option' ? 100 : 200; + } + + protected override createWidgetOptions(uri: URI, options?: WidgetOpenerOptions | undefined): NotebookEditorWidgetOptions { + const widgetOptions = super.createWidgetOptions(uri, options); + const notebookType = this.findHighestPriorityType(uri); + if (!notebookType) { + throw new Error('No notebook types registered for uri: ' + uri.toString()); + } + return { + notebookType: notebookType.type, + ...widgetOptions + }; + } + + matches(selectors: readonly NotebookFileSelector[], resource: URI): boolean { + return selectors.some(selector => this.selectorMatches(selector, resource)); + } + + selectorMatches(selector: NotebookFileSelector, resource: URI): boolean { + return !!selector.filenamePattern + && match(selector.filenamePattern, resource.path.name + resource.path.ext); + } +} diff --git a/packages/notebook/src/browser/notebook-renderer-registry.ts b/packages/notebook/src/browser/notebook-renderer-registry.ts new file mode 100644 index 0000000000000..782830968cee7 --- /dev/null +++ b/packages/notebook/src/browser/notebook-renderer-registry.ts @@ -0,0 +1,62 @@ +// ***************************************************************************** +// 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, Path } from '@theia/core'; +import { injectable } from '@theia/core/shared/inversify'; +import { NotebookRendererDescriptor } from '../common/notebook-protocol'; + +export interface NotebookRendererInfo { + readonly id: string; + readonly displayName: string; + readonly mimeTypes: string[]; + readonly entrypoint: { readonly extends?: string; readonly uri: string }; + readonly requiresMessaging: boolean; +} + +@injectable() +export class NotebookRendererRegistry { + + readonly notebookRenderers: NotebookRendererInfo[] = []; + + registerNotebookRenderer(type: NotebookRendererDescriptor, basePath: string): Disposable { + let entrypoint; + if (typeof type.entrypoint === 'string') { + entrypoint = { + uri: new Path(basePath).join(type.entrypoint).toString() + }; + } else { + entrypoint = { + uri: new Path(basePath).join(type.entrypoint.path).toString(), + extends: type.entrypoint.extends + }; + } + + this.notebookRenderers.push({ + ...type, + mimeTypes: type.mimeTypes || [], + requiresMessaging: type.requiresMessaging === 'always' || type.requiresMessaging === 'optional', + entrypoint + }); + return Disposable.create(() => { + this.notebookRenderers.splice(this.notebookRenderers.findIndex(renderer => renderer.id === type.id), 1); + }); + } +} + diff --git a/packages/notebook/src/browser/notebook-type-registry.ts b/packages/notebook/src/browser/notebook-type-registry.ts new file mode 100644 index 0000000000000..973652e2304b6 --- /dev/null +++ b/packages/notebook/src/browser/notebook-type-registry.ts @@ -0,0 +1,30 @@ +// ***************************************************************************** +// 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 { Disposable } from '@theia/core'; +import { injectable } from '@theia/core/shared/inversify'; +import { NotebookTypeDescriptor } from '../common/notebook-protocol'; + +@injectable() +export class NotebookTypeRegistry { + readonly notebookTypes: NotebookTypeDescriptor[] = []; + + registerNotebookType(type: NotebookTypeDescriptor): Disposable { + this.notebookTypes.push(type); + return Disposable.create(() => { + this.notebookTypes.splice(this.notebookTypes.indexOf(type), 1); + }); + } +} diff --git a/packages/notebook/src/browser/renderers/cell-output-webview.ts b/packages/notebook/src/browser/renderers/cell-output-webview.ts new file mode 100644 index 0000000000000..3e84347f854fb --- /dev/null +++ b/packages/notebook/src/browser/renderers/cell-output-webview.ts @@ -0,0 +1,32 @@ +// ***************************************************************************** +// 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 { Disposable } from '@theia/core'; +import { NotebookCellModel } from '../view-model/notebook-cell-model'; + +export const CellOutputWebviewFactory = Symbol('outputWebviewFactory'); + +export type CellOutputWebviewFactory = (cell: NotebookCellModel) => Promise; + +export interface CellOutputWebview extends Disposable { + + readonly id: string; + + render(): React.JSX.Element; + + attachWebview(): void; + isAttached(): boolean +} diff --git a/packages/notebook/src/browser/service/notebook-cell-context-manager.ts b/packages/notebook/src/browser/service/notebook-cell-context-manager.ts new file mode 100644 index 0000000000000..a0d4386f0128a --- /dev/null +++ b/packages/notebook/src/browser/service/notebook-cell-context-manager.ts @@ -0,0 +1,70 @@ +// ***************************************************************************** +// 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 { inject, injectable } from '@theia/core/shared/inversify'; +import { ContextKeyService, ScopedValueStore } from '@theia/core/lib/browser/context-key-service'; +import { NotebookCellModel } from '../view-model/notebook-cell-model'; +import { NOTEBOOK_CELL_EXECUTING, NOTEBOOK_CELL_EXECUTION_STATE, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_TYPE } from '../contributions/notebook-context-keys'; +import { Disposable, DisposableCollection, Emitter } from '@theia/core'; +import { CellKind } from '../../common'; +import { NotebookExecutionStateService } from '../service/notebook-execution-state-service'; + +@injectable() +export class NotebookCellContextManager implements Disposable { + @inject(ContextKeyService) protected contextKeyService: ContextKeyService; + + @inject(NotebookExecutionStateService) + protected readonly executionStateService: NotebookExecutionStateService; + + protected readonly toDispose = new DisposableCollection(); + + protected currentStore: ScopedValueStore; + protected currentContext: HTMLLIElement; + + protected readonly onDidChangeContextEmitter = new Emitter(); + readonly onDidChangeContext = this.onDidChangeContextEmitter.event; + + updateCellContext(cell: NotebookCellModel, newHtmlContext: HTMLLIElement): void { + if (newHtmlContext !== this.currentContext) { + this.toDispose.dispose(); + this.currentStore?.dispose(); + + this.currentContext = newHtmlContext; + this.currentStore = this.contextKeyService.createScoped(newHtmlContext); + + this.currentStore.setContext(NOTEBOOK_CELL_TYPE, cell.cellKind === CellKind.Code ? 'code' : 'markdown'); + + this.toDispose.push(cell.onDidRequestCellEditChange(cellEdit => { + this.currentStore?.setContext(NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, cellEdit); + this.onDidChangeContextEmitter.fire(); + })); + this.toDispose.push(this.executionStateService.onDidChangeExecution(e => { + if (e.affectsCell(cell.uri)) { + this.currentStore?.setContext(NOTEBOOK_CELL_EXECUTING, !!e.changed); + this.currentStore?.setContext(NOTEBOOK_CELL_EXECUTION_STATE, e.changed?.state ?? 'idle'); + this.onDidChangeContextEmitter.fire(); + } + })); + this.onDidChangeContextEmitter.fire(); + } + } + + dispose(): void { + this.toDispose.dispose(); + this.currentStore?.dispose(); + this.onDidChangeContextEmitter.dispose(); + } +} 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..2cce03f68e1ae --- /dev/null +++ b/packages/notebook/src/browser/service/notebook-editor-service.ts @@ -0,0 +1,86 @@ +// ***************************************************************************** +// 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 toDispose = new DisposableCollection(); + + currentFocusedEditor?: NotebookEditorWidget = undefined; + + @postConstruct() + protected init(): void { + this.toDispose.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.onFocusedEditorChangedEmitter.dispose(); + this.toDispose.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 new file mode 100644 index 0000000000000..d0e3525194f8a --- /dev/null +++ b/packages/notebook/src/browser/service/notebook-execution-service.ts @@ -0,0 +1,139 @@ +// ***************************************************************************** +// 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, NotebookKernel } from './notebook-kernel-service'; +import { CommandService, Disposable } from '@theia/core'; +import { NotebookKernelQuickPickService, NotebookKernelQuickPickServiceImpl } from './notebook-kernel-quick-pick-service'; +import { NotebookKernelHistoryService } from './notebook-kernel-history-service'; +import { NotebookCommands } from '../contributions/notebook-actions-contribution'; + +export interface CellExecutionParticipant { + onWillExecuteCell(executions: CellExecution[]): Promise; +} + +@injectable() +export class NotebookExecutionService { + + @inject(NotebookExecutionStateService) + protected notebookExecutionStateService: NotebookExecutionStateService; + + @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 { + const cellsArr = Array.from(cells) + .filter(c => c.cellKind === CellKind.Code); + if (!cellsArr.length) { + return; + } + + // 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 = await this.resolveKernel(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) { + 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); + 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)); + } + + 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 new file mode 100644 index 0000000000000..63227fd5b6545 --- /dev/null +++ b/packages/notebook/src/browser/service/notebook-execution-state-service.ts @@ -0,0 +1,322 @@ +// ***************************************************************************** +// 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, Emitter, URI } from '@theia/core'; +import { inject, injectable } from '@theia/core/shared/inversify'; +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) { + 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 { + 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, false); + } +} + +export 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.isEqual(parsedUri.notebook) && this.cellHandle === parsedUri.handle; + } + + affectsNotebook(notebook: URI): boolean { + return this.notebook.toString() === notebook.toString(); + } +} + +export 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-kernel-history-service.ts b/packages/notebook/src/browser/service/notebook-kernel-history-service.ts new file mode 100644 index 0000000000000..a28cba3bbccc4 --- /dev/null +++ b/packages/notebook/src/browser/service/notebook-kernel-history-service.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 recentKernels = this.mostRecentKernelsMap[viewType] ?? [kernel.id]; + + if (recentKernels.length > MAX_KERNELS_IN_HISTORY) { + recentKernels.splice(MAX_KERNELS_IN_HISTORY); + } + + this.mostRecentKernelsMap[viewType] = recentKernels; + 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 ? 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/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..14777243002ea --- /dev/null +++ b/packages/notebook/src/browser/service/notebook-kernel-quick-pick-service.ts @@ -0,0 +1,494 @@ + +// ***************************************************************************** +// 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 { 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, 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 './notebook-kernel-history-service'; +import debounce = require('@theia/core/shared/lodash.debounce'); + +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; + +const KERNEL_PICKER_UPDATE_DEBOUNCE = 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 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 current 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 + ), + KERNEL_PICKER_UPDATE_DEBOUNCE + )(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; + + // recalculate 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; + } + + if (isSourcePick(pick)) { + // selected explicitly, it should trigger the execution? + pick.action.run(); + } + + return true; + } + + protected selectKernel(notebook: NotebookModel, kernel: NotebookKernel): void { + this.notebookKernelService.selectKernelForNotebook(kernel, notebook); + } +} +@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 when 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.calculateKernelSources(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 + ), + KERNEL_PICKER_UPDATE_DEBOUNCE, + )(async () => { + quickPick.busy = true; + const quickPickItems = await this.calculateKernelSources(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 calculateKernelSources(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 new file mode 100644 index 0000000000000..756dc86237848 --- /dev/null +++ b/packages/notebook/src/browser/service/notebook-kernel-service.ts @@ -0,0 +1,357 @@ +// ***************************************************************************** +// 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 { Command, CommandRegistry, Disposable, Emitter, Event, URI } from '@theia/core'; +import { inject, injectable, postConstruct } from '@theia/core/shared/inversify'; +import { StorageService } from '@theia/core/lib/browser'; +import { NotebookKernelSourceAction } from '../../common'; +import { NotebookModel } from '../view-model/notebook-model'; +import { NotebookService } from './notebook-service'; + +export interface SelectedNotebookKernelChangeEvent { + notebook: URI; + oldKernel: string | undefined; + newKernel: string | undefined; +} + +export interface NotebookKernelMatchResult { + readonly selected: NotebookKernel | undefined; + readonly suggestions: NotebookKernel[]; + readonly all: NotebookKernel[]; + readonly hidden: NotebookKernel[]; +} + +export interface NotebookKernelChangeEvent { + label?: true; + description?: true; + detail?: true; + supportedLanguages?: true; + hasExecutionOrder?: true; + hasInterruptHandler?: true; +} + +export interface NotebookKernel { + readonly id: string; + readonly viewType: string; + readonly onDidChange: Event>; + readonly extension: string; + + readonly localResourceRoot: URI; + readonly preloadUris: URI[]; + readonly preloadProvides: string[]; + + label: string; + description?: string; + detail?: string; + supportedLanguages: string[]; + implementsInterrupt?: boolean; + implementsExecutionOrder?: boolean; + + executeNotebookCellsRequest(uri: URI, cellHandles: number[]): Promise; + cancelNotebookCellExecution(uri: URI, cellHandles: number[]): Promise; +} + +export const enum ProxyKernelState { + Disconnected = 1, + Connected = 2, + Initializing = 3 +} + +export interface INotebookProxyKernelChangeEvent extends NotebookKernelChangeEvent { + connectionState?: true; +} + +export interface NotebookKernelDetectionTask { + readonly notebookType: string; +} + +export interface NotebookTextModelLike { uri: URI; viewType: string } + +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++; + } +} + +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; + protected 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(); + } + +} + +const NOTEBOOK_KERNEL_BINDING_STORAGE_KEY = 'notebook.kernel.bindings'; +@injectable() +export class NotebookKernelService implements Disposable { + + @inject(NotebookService) + protected notebookService: NotebookService; + + @inject(StorageService) + protected storageService: StorageService; + + private readonly kernels = new Map(); + + private notebookBindings: { [key: string]: string } = {}; + + 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; + + private readonly onDidRemoveKernelEmitter = new Emitter(); + readonly onDidRemoveKernel: Event = this.onDidRemoveKernelEmitter.event; + + private readonly onDidChangeSelectedNotebookKernelBindingEmitter = new Emitter(); + readonly onDidChangeSelectedKernel: Event = this.onDidChangeSelectedNotebookKernelBindingEmitter.event; + + private readonly onDidChangeNotebookAffinityEmitter = new Emitter(); + readonly onDidChangeNotebookAffinity: Event = this.onDidChangeNotebookAffinityEmitter.event; + + @postConstruct() + init(): void { + this.storageService.getData(NOTEBOOK_KERNEL_BINDING_STORAGE_KEY).then((value: { [key: string]: string } | undefined) => { + if (value) { + this.notebookBindings = value; + } + }); + } + + registerKernel(kernel: NotebookKernel): Disposable { + 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); + + // auto associate the new kernel to existing notebooks it was + // associated to in the past. + for (const notebook of this.notebookService.getNotebookModels()) { + this.tryAutoBindNotebook(notebook, kernel); + } + + return Disposable.create(() => { + if (this.kernels.delete(kernel.id)) { + this.onDidRemoveKernelEmitter.fire(kernel); + } + }); + } + + getMatchingKernel(notebook: NotebookTextModelLike): NotebookKernelMatchResult { + 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[`${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); // TODO implement notebookAffinity + 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[key]; + if (oldKernel !== kernel?.id) { + if (kernel) { + this.notebookBindings[key] = kernel.id; + } else { + delete this.notebookBindings[key]; + } + this.storageService.setData(NOTEBOOK_KERNEL_BINDING_STORAGE_KEY, this.notebookBindings); + this.onDidChangeSelectedNotebookKernelBindingEmitter.fire({ notebook: notebook.uri, oldKernel, newKernel: kernel?.id }); + } + } + + 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; + } + + getKernel(id: string): NotebookKernel | undefined { + return this.kernels.get(id)?.kernel; + } + + private static score(kernel: NotebookKernel, notebook: NotebookTextModelLike): number { + if (kernel.viewType === '*') { + return 5; + } else if (kernel.viewType === notebook.viewType) { + return 10; + } else { + return 0; + } + } + + private tryAutoBindNotebook(notebook: NotebookModel, onlyThisKernel?: NotebookKernel): void { + + const id = this.notebookBindings[`${notebook.viewType}/${notebook.uri}`]; + if (!id) { + // no kernel associated + return; + } + const existingKernel = this.kernels.get(id); + if (!existingKernel || !NotebookKernelService.score(existingKernel.kernel, notebook)) { + // associated kernel not known, not matching + return; + } + if (!onlyThisKernel || existingKernel.kernel === onlyThisKernel) { + this.onDidChangeSelectedNotebookKernelBindingEmitter.fire({ notebook: notebook.uri, oldKernel: undefined, newKernel: existingKernel.kernel.id }); + } + } + + 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 taskIndex = allTasks.indexOf(task); + if (taskIndex >= 0) { + allTasks.splice(taskIndex, 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 providerIndex = sourceProviders.indexOf(provider); + if (providerIndex >= 0) { + sourceProviders.splice(providerIndex, 1); + this.kernelSourceActionProviders.set(viewType, sourceProviders); + } + + eventEmitterDisposable?.dispose(); + }); + } + + async getKernelSourceActionsFromProviders(notebook: NotebookTextModelLike): Promise { + const viewType = notebook.viewType; + const providers = this.kernelSourceActionProviders.get(viewType) ?? []; + const promises = providers.map(provider => provider.provideKernelSourceActions()); + const allActions = await Promise.all(promises); + return allActions.flat(); + } + + dispose(): void { + this.onDidChangeKernelDetectionTasksEmitter.dispose(); + this.onDidChangeSourceActionsEmitter.dispose(); + this.onDidAddKernelEmitter.dispose(); + this.onDidRemoveKernelEmitter.dispose(); + this.onDidChangeSelectedNotebookKernelBindingEmitter.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 new file mode 100644 index 0000000000000..07386ab739c0d --- /dev/null +++ b/packages/notebook/src/browser/service/notebook-model-resolver-service.ts @@ -0,0 +1,141 @@ +// ***************************************************************************** +// 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 { Emitter, URI } from '@theia/core'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { UriComponents } from '@theia/core/lib/common/uri'; +import { FileService } from '@theia/filesystem/lib/browser/file-service'; +import { CellKind, NotebookData } from '../../common'; +import { NotebookModel } from '../view-model/notebook-model'; +import { NotebookService } from './notebook-service'; +import { NotebookTypeRegistry } from '../notebook-type-registry'; +import { NotebookFileSelector } from '../../common/notebook-protocol'; + +export interface UntitledResource { + untitledResource: URI | undefined +} +@injectable() +export class NotebookModelResolverService { + + @inject(FileService) + protected fileService: FileService; + + @inject(NotebookService) + protected notebookService: NotebookService; + + @inject(NotebookTypeRegistry) + protected notebookTypeRegistry: NotebookTypeRegistry; + + protected onDidChangeDirtyEmitter = new Emitter(); + readonly onDidChangeDirty = this.onDidChangeDirtyEmitter.event; + protected onDidSaveNotebookEmitter = new Emitter(); + readonly onDidSaveNotebook = this.onDidSaveNotebookEmitter.event; + + async resolve(resource: URI, viewType?: string): Promise; + async resolve(resource: UntitledResource, viewType: string): Promise; + async resolve(arg: URI | UntitledResource, viewType: string): Promise { + let resource: URI; + // let hasAssociatedFilePath = false; + if (arg instanceof URI) { + resource = arg; + } else { + arg = arg as UntitledResource; + if (!arg.untitledResource) { + const notebookTypeInfo = this.notebookTypeRegistry.notebookTypes.find(info => info.type === viewType); + if (!notebookTypeInfo) { + throw new Error('UNKNOWN view type: ' + viewType); + } + + const suffix = this.getPossibleFileEndings(notebookTypeInfo.selector ?? []) ?? ''; + for (let counter = 1; ; counter++) { + const candidate = new URI() + .withScheme('untitled') + .withPath(`Untitled-notebook-${counter}${suffix}`) + .withQuery(viewType); + if (!this.notebookService.getNotebookEditorModel(candidate)) { + resource = candidate; + break; + } + } + } else if (arg.untitledResource.scheme === 'untitled') { + resource = arg.untitledResource; + } else { + resource = arg.untitledResource.withScheme('untitled'); + // hasAssociatedFilePath = true; + } + } + + const notebookData = await this.resolveExistingNotebookData(resource, viewType!); + + const notebookModel = await this.notebookService.createNotebookModel(notebookData, viewType, resource); + + notebookModel.onDirtyChanged(() => this.onDidChangeDirtyEmitter.fire(notebookModel)); + notebookModel.onDidSaveNotebook(() => this.onDidSaveNotebookEmitter.fire(notebookModel.uri.toComponents())); + + return notebookModel; + } + + protected async resolveExistingNotebookData(resource: URI, viewType: string): Promise { + if (resource.scheme === 'untitled') { + + return { + cells: [ + { + cellKind: CellKind.Markup, + language: 'markdown', + outputs: [], + source: '' + } + ], + metadata: {} + }; + } else { + const file = await this.fileService.readFile(resource); + + const dataProvider = await this.notebookService.getNotebookDataProvider(viewType); + const notebook = await dataProvider.serializer.dataToNotebook(file.value); + + return notebook; + } + } + + protected getPossibleFileEndings(selectors: readonly NotebookFileSelector[]): string | undefined { + for (const selector of selectors) { + const ending = this.possibleFileEnding(selector); + if (ending) { + return ending; + } + } + return undefined; + } + + protected possibleFileEnding(selector: NotebookFileSelector): string | undefined { + + const pattern = /^.*(\.[a-zA-Z0-9_-]+)$/; + + const candidate: string | undefined = typeof selector === 'string' ? selector : selector.filenamePattern; + + if (candidate) { + const match = pattern.exec(candidate); + if (match) { + return match[1]; + } + } + + return undefined; + } + +} diff --git a/packages/notebook/src/browser/service/notebook-renderer-messaging-service.ts b/packages/notebook/src/browser/service/notebook-renderer-messaging-service.ts new file mode 100644 index 0000000000000..923dafbb1db93 --- /dev/null +++ b/packages/notebook/src/browser/service/notebook-renderer-messaging-service.ts @@ -0,0 +1,111 @@ +// ***************************************************************************** +// 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 { Emitter } from '@theia/core'; +import { injectable } from '@theia/core/shared/inversify'; +import { Disposable } from '@theia/core/shared/vscode-languageserver-protocol'; + +interface MessageToSend { + editorId: string; + rendererId: string; + message: unknown +}; + +export interface ScopedRendererMessaging extends Disposable { + /** + * Method called when a message is received. Should return a boolean + * indicating whether a renderer received it. + */ + receiveMessageHandler?: (rendererId: string, message: unknown) => Promise; + + /** + * Sends a message to an extension from a renderer. + */ + postMessage(rendererId: string, message: unknown): void; +} + +@injectable() +export class NotebookRendererMessagingService implements Disposable { + + private readonly postMessageEmitter = new Emitter(); + readonly onShouldPostMessage = this.postMessageEmitter.event; + + private readonly activations = new Map(); + private readonly scopedMessaging = new Map(); + + receiveMessage(editorId: string | undefined, rendererId: string, message: unknown): Promise { + if (editorId === undefined) { + const sends = [...this.scopedMessaging.values()].map(e => e.receiveMessageHandler?.(rendererId, message)); + return Promise.all(sends).then(values => values.some(value => !!value)); + } + + return this.scopedMessaging.get(editorId)?.receiveMessageHandler?.(rendererId, message) ?? Promise.resolve(false); + } + + prepare(rendererId: string): void { + if (this.activations.has(rendererId)) { + return; + } + + const queue: MessageToSend[] = []; + this.activations.set(rendererId, queue); + + // activate renderer + // this.extensionService.activateByEvent(`onRenderer:${rendererId}`).then(() => { + // for (const message of queue) { + // this.postMessageEmitter.fire(message); + // } + // this.activations.set(rendererId, undefined); + // }); + } + + public getScoped(editorId: string): ScopedRendererMessaging { + const existing = this.scopedMessaging.get(editorId); + if (existing) { + return existing; + } + + const messaging: ScopedRendererMessaging = { + postMessage: (rendererId, message) => this.postMessage(editorId, rendererId, message), + dispose: () => this.scopedMessaging.delete(editorId), + }; + + this.scopedMessaging.set(editorId, messaging); + return messaging; + } + + private postMessage(editorId: string, rendererId: string, message: unknown): void { + if (!this.activations.has(rendererId)) { + this.prepare(rendererId); + } + + const activation = this.activations.get(rendererId); + const toSend = { rendererId, editorId, message }; + if (activation === undefined) { + this.postMessageEmitter.fire(toSend); + } else { + activation.push(toSend); + } + } + + dispose(): void { + this.postMessageEmitter.dispose(); + } +} diff --git a/packages/notebook/src/browser/service/notebook-service.ts b/packages/notebook/src/browser/service/notebook-service.ts new file mode 100644 index 0000000000000..2bf7bc0c6c0da --- /dev/null +++ b/packages/notebook/src/browser/service/notebook-service.ts @@ -0,0 +1,178 @@ +// ***************************************************************************** +// 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 { Disposable, DisposableCollection, Emitter, URI } from '@theia/core'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { BinaryBuffer } from '@theia/core/lib/common/buffer'; +import { NotebookData, NotebookExtensionDescription, TransientOptions } from '../../common'; +import { NotebookModel, NotebookModelFactory, NotebookModelProps } from '../view-model/notebook-model'; +import { FileService } from '@theia/filesystem/lib/browser/file-service'; +import { MonacoTextModelService } from '@theia/monaco/lib/browser/monaco-text-model-service'; +import { NotebookCellModel, NotebookCellModelFactory, NotebookCellModelProps } from '../view-model/notebook-cell-model'; +import { Deferred } from '@theia/core/lib/common/promise-util'; + +export const NotebookProvider = Symbol('notebook provider'); + +export interface SimpleNotebookProviderInfo { + readonly notebookType: string, + readonly serializer: NotebookSerializer, + readonly extensionData: NotebookExtensionDescription +} + +export interface NotebookSerializer { + options: TransientOptions; + dataToNotebook(data: BinaryBuffer): Promise; + notebookToData(data: NotebookData): Promise; +} + +@injectable() +export class NotebookService implements Disposable { + + @inject(FileService) + protected fileService: FileService; + + @inject(MonacoTextModelService) + protected modelService: MonacoTextModelService; + + @inject(NotebookModelFactory) + protected notebookModelFactory: (props: NotebookModelProps) => NotebookModel; + + @inject(NotebookCellModelFactory) + protected notebookCellModelFactory: (props: NotebookCellModelProps) => NotebookCellModel; + + protected notebookSerializerEmitter = new Emitter(); + readonly onNotebookSerializer = this.notebookSerializerEmitter.event; + + protected readonly disposables = new DisposableCollection(); + + protected readonly notebookProviders = new Map(); + protected readonly notebookModels = new Map(); + + protected readonly didAddViewTypeEmitter = new Emitter(); + readonly onDidAddViewType = this.didAddViewTypeEmitter.event; + + protected readonly didRemoveViewTypeEmitter = new Emitter(); + readonly onDidRemoveViewType = this.didRemoveViewTypeEmitter.event; + + protected readonly willOpenNotebookTypeEmitter = new Emitter(); + readonly onWillOpenNotebook = this.willOpenNotebookTypeEmitter.event; + + protected readonly willAddNotebookDocumentEmitter = new Emitter(); + readonly onWillAddNotebookDocument = this.willAddNotebookDocumentEmitter.event; + protected readonly didAddNotebookDocumentEmitter = new Emitter(); + readonly onDidAddNotebookDocument = this.didAddNotebookDocumentEmitter.event; + protected readonly willRemoveNotebookDocumentEmitter = new Emitter(); + readonly onWillRemoveNotebookDocument = this.willRemoveNotebookDocumentEmitter.event; + protected readonly didRemoveNotebookDocumentEmitter = new Emitter(); + readonly onDidRemoveNotebookDocument = this.didRemoveNotebookDocumentEmitter.event; + + dispose(): void { + this.disposables.dispose(); + } + + protected readonly ready = new Deferred(); + + /** + * Marks the notebook service as ready. From this point on, the service will start dispatching the `onNotebookSerializer` event. + */ + markReady(): void { + this.ready.resolve(); + } + + registerNotebookSerializer(notebookType: string, extensionData: NotebookExtensionDescription, serializer: NotebookSerializer): Disposable { + if (this.notebookProviders.has(notebookType)) { + throw new Error(`notebook provider for viewtype '${notebookType}' already exists`); + } + + this.notebookProviders.set(notebookType, { notebookType: notebookType, serializer, extensionData }); + this.didAddViewTypeEmitter.fire(notebookType); + + return Disposable.create(() => { + this.notebookProviders.delete(notebookType); + this.didRemoveViewTypeEmitter.fire(notebookType); + }); + } + + 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); + } + + this.willAddNotebookDocumentEmitter.fire(uri); + const model = this.notebookModelFactory({ data, uri, viewType, serializer }); + this.notebookModels.set(uri.toString(), model); + // Resolve cell text models right after creating the notebook model + // This ensures that all text models are available in the plugin host + await Promise.all(model.cells.map(e => e.resolveTextModel())); + this.didAddNotebookDocumentEmitter.fire(model); + return model; + } + + async getNotebookDataProvider(viewType: string): Promise { + await this.ready.promise; + await this.notebookSerializerEmitter.sequence(async listener => listener(`onNotebookSerializer:${viewType}`)); + + const result = await this.waitForNotebookProvider(viewType); + if (!result) { + throw new Error(`No provider registered for view type: '${viewType}'`); + } + return result; + } + + /** + * When the application starts up, notebook providers from plugins are not registered yet. + * It takes a few seconds for the plugin host to start so that notebook data providers can be registered. + * This methods waits until the notebook provider is registered. + */ + protected async waitForNotebookProvider(type: string): Promise { + if (this.notebookProviders.has(type)) { + return this.notebookProviders.get(type); + } + const deferred = new Deferred(); + // 20 seconds of timeout + const timeoutDuration = 20_000; + const disposable = this.onDidAddViewType(viewType => { + if (viewType === type) { + clearTimeout(timeout); + disposable.dispose(); + deferred.resolve(this.notebookProviders.get(type)); + } + }); + const timeout = setTimeout(() => { + clearTimeout(timeout); + disposable.dispose(); + deferred.reject(); + }, timeoutDuration); + return deferred.promise; + } + + getNotebookEditorModel(uri: URI): NotebookModel | undefined { + return this.notebookModels.get(uri.toString()); + } + + getNotebookModels(): Iterable { + return this.notebookModels.values(); + } + + 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/style/index.css b/packages/notebook/src/browser/style/index.css new file mode 100644 index 0000000000000..2c9b1844a0641 --- /dev/null +++ b/packages/notebook/src/browser/style/index.css @@ -0,0 +1,236 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +:root { + --theia-notebook-markdown-size: 17px; +} + +.theia-notebook-cell-list { + overflow-y: auto; + list-style: none; + padding-left: 0px; + background-color: var(--theia-notebook-editorBackground); +} + +.theia-notebook-cell { + cursor: grab; + display: flex; + margin: 10px 0px; +} + +.theia-notebook-cell:hover .theia-notebook-cell-marker { + visibility: visible; +} + +.theia-notebook-cell-marker { + background-color: var(--theia-notebook-inactiveFocusedCellBorder); + width: 3px; + margin: 0px 8px 0px 4px; + border-radius: 4px; + visibility: hidden; +} + +.theia-notebook-cell-marker-selected { + visibility: visible; + background-color: var(--theia-notebook-focusedCellBorder); +} + +.theia-notebook-cell-marker:hover { + width: 5px; + margin: 0px 6px 0px 4px; +} + +.theia-notebook-cell-content { + flex: 1; + width: calc(100% - 15px); +} + +.theia-notebook-markdown-content { + padding: 8px 16px 8px 36px; + font-size: var(--theia-notebook-markdown-size); +} + +.theia-notebook-markdown-content > *:first-child { + margin-top: 0; + padding-top: 0; +} + +.theia-notebook-markdown-content > *:only-child, +.theia-notebook-markdown-content > *:last-child { + margin-bottom: 0; + padding-bottom: 0; +} + +.theia-notebook-empty-markdown { + opacity: 0.6; +} + +.theia-notebook-cell-editor { + padding: 10px 10px 0 10px; +} + +.theia-notebook-cell-editor-container { + width: calc(100% - 46px); + flex: 1; + outline: 1px solid var(--theia-notebook-cellBorderColor); + margin: 0px 10px; +} + +.theia-notebook-cell.focused .theia-notebook-cell-editor-container { + outline-color: var(--theia-notebook-focusedEditorBorder); +} + +.notebook-cell-status { + display: flex; + flex-direction: row; + font-size: 12px; + height: 16px; +} + +.notebook-cell-status-left { + display: flex; + flex-direction: row; + flex-grow: 1; +} + +.notebook-cell-status-right { + margin: 0 5px; +} + +.notebook-cell-status-item { + margin: 0 3px; + padding: 0 3px; + display: flex; + align-items: center; +} + +.theia-notebook-cell-toolbar { + border: 1px solid var(--theia-notebook-cellToolbarSeparator); + display: flex; + position: absolute; + margin: -20px 0 0 66px; + padding: 2px; + background-color: var(--theia-editor-background); +} + +.theia-notebook-cell-sidebar { + display: flex; + flex-direction: column; + padding: 2px; + background-color: var(--theia-editor-background); +} + +.theia-notebook-cell-toolbar-item { + height: 18px; + width: 18px; +} + +.theia-notebook-cell-toolbar-item:hover { + background-color: var(--theia-toolbar-hoverBackground); +} + +.theia-notebook-cell-toolbar-item:active { + background-color: var(--theia-toolbar-active); +} + +.theia-notebook-cell-divider { + height: 20px; + width: 100%; +} + +.theia-notebook-cell-with-sidebar { + display: flex; + flex-direction: row; +} + +.theia-notebook-cell-sidebar { + display: flex; + flex-direction: column; +} + +.theia-notebook-main-toolbar { + position: sticky; + top: 0; + background: var(--theia-editor-background); + display: flex; + flex-direction: row; + z-index: 1; + /*needed to be on rendered on top of monaco editors*/ +} + +.theia-notebook-main-toolbar-item { + height: 22px; + display: flex; + align-items: center; + margin: 0 4px; + padding: 2px; + text-align: center; + color: var(--theia-foreground) !important; + cursor: pointer; +} + +.theia-notebook-main-toolbar-item:hover { + background-color: var(--theia-toolbar-hoverBackground); +} + +.theia-notebook-main-toolbar-item-text { + padding: 0 4px; +} + +.theia-notebook-toolbar-separator { + width: 1px; + background-color: var(--theia-notebook-cellToolbarSeparator); + margin: 0 4px; +} + +.theia-notebook-add-cell-buttons { + justify-content: center; + display: flex; +} + +.theia-notebook-add-cell-button { + border: 1px solid var(--theia-notebook-cellToolbarSeparator); + background-color: var(--theia-editor-background); + color: var(--theia-foreground); + vertical-align: middle; + text-align: center; + height: 24px; + margin: 0 8px; +} + +.theia-notebook-add-cell-button:hover { + background-color: var(--theia-toolbar-hoverBackground); +} + +.theia-notebook-add-cell-button:active { + background-color: var(--theia-toolbar-active); +} + +.theia-notebook-add-cell-button-icon { + vertical-align: middle; +} + +.theia-notebook-cell-output-webview { + padding: 5px 0px; + margin: 0px 10px; + width: 100%; +} + +.theia-notebook-cell-drop-indicator { + height: 2px; + background-color: var(--theia-notebook-focusedCellBorder); + width: 100%; +} diff --git a/packages/notebook/src/browser/view-model/notebook-cell-model.ts b/packages/notebook/src/browser/view-model/notebook-cell-model.ts new file mode 100644 index 0000000000000..44551c8f19d40 --- /dev/null +++ b/packages/notebook/src/browser/view-model/notebook-cell-model.ts @@ -0,0 +1,271 @@ +// ***************************************************************************** +// 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, Event, URI } from '@theia/core'; +import { inject, injectable, interfaces, postConstruct } from '@theia/core/shared/inversify'; +import { MonacoEditorModel } from '@theia/monaco/lib/browser/monaco-editor-model'; +import { MonacoTextModelService } from '@theia/monaco/lib/browser/monaco-text-model-service'; +import { + CellInternalMetadataChangedEvent, CellKind, NotebookCellCollapseState, NotebookCellInternalMetadata, + NotebookCellMetadata, NotebookCellOutputsSplice, CellOutput, CellData, NotebookCell +} from '../../common'; +import { NotebookCellOutputModel } from './notebook-cell-output-model'; + +export const NotebookCellModelFactory = Symbol('NotebookModelFactory'); + +export function createNotebookCellModelContainer(parent: interfaces.Container, props: NotebookCellModelProps, + notebookCellContextManager: new (...args: never[]) => unknown): interfaces.Container { + const child = parent.createChild(); + + child.bind(NotebookCellModelProps).toConstantValue(props); + // We need the constructor as property here to avoid circular dependencies for the context manager + child.bind(NotebookCellContextManager).to(notebookCellContextManager).inSingletonScope(); + child.bind(NotebookCellModel).toSelf(); + + return child; +} + +const NotebookCellContextManager = Symbol('NotebookCellContextManager'); +interface NotebookCellContextManager { + updateCellContext(cell: NotebookCellModel, context: HTMLElement): void; + dispose(): void; + onDidChangeContext: Event; +} + +const NotebookCellModelProps = Symbol('NotebookModelProps'); +export interface NotebookCellModelProps { + readonly uri: URI, + readonly handle: number, + source: string, + language: string, + readonly cellKind: CellKind, + outputs: CellOutput[], + metadata?: NotebookCellMetadata | undefined, + internalMetadata?: NotebookCellInternalMetadata | undefined, + readonly collapseState?: NotebookCellCollapseState | undefined, + +} + +@injectable() +export class NotebookCellModel implements NotebookCell, Disposable { + + protected readonly onDidChangeOutputsEmitter = new Emitter(); + readonly onDidChangeOutputs: Event = this.onDidChangeOutputsEmitter.event; + + protected readonly onDidChangeOutputItemsEmitter = new Emitter(); + readonly onDidChangeOutputItems: Event = this.onDidChangeOutputItemsEmitter.event; + + protected readonly onDidChangeContentEmitter = new Emitter<'content' | 'language' | 'mime'>(); + readonly onDidChangeContent: Event<'content' | 'language' | 'mime'> = this.onDidChangeContentEmitter.event; + + protected readonly onDidChangeMetadataEmitter = new Emitter(); + readonly onDidChangeMetadata: Event = this.onDidChangeMetadataEmitter.event; + + protected readonly onDidChangeInternalMetadataEmitter = new Emitter(); + readonly onDidChangeInternalMetadata: Event = this.onDidChangeInternalMetadataEmitter.event; + + protected readonly onDidChangeLanguageEmitter = new Emitter(); + readonly onDidChangeLanguage: Event = this.onDidChangeLanguageEmitter.event; + + protected readonly onDidRequestCellEditChangeEmitter = new Emitter(); + readonly onDidRequestCellEditChange = this.onDidRequestCellEditChangeEmitter.event; + + @inject(NotebookCellContextManager) + readonly notebookCellContextManager: NotebookCellContextManager; + + @inject(NotebookCellModelProps) + protected readonly props: NotebookCellModelProps; + @inject(MonacoTextModelService) + protected readonly textModelService: MonacoTextModelService; + + get outputs(): NotebookCellOutputModel[] { + return this._outputs; + } + + protected _outputs: NotebookCellOutputModel[]; + + get metadata(): NotebookCellMetadata { + return this._metadata; + } + + protected _metadata: NotebookCellMetadata; + + protected toDispose = new DisposableCollection(); + + protected _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.onDidChangeInternalMetadataEmitter.fire({ lastRunSuccessChanged }); + + } + + textModel: MonacoEditorModel; + + protected htmlContext: HTMLLIElement; + + get context(): HTMLLIElement { + return this.htmlContext; + } + + get textBuffer(): string { + return this.textModel ? this.textModel.getText() : this.source; + } + + get source(): string { + return this.props.source; + } + set source(source: string) { + this.props.source = source; + } + get language(): string { + return this.props.language; + } + + set language(newLanguage: string) { + if (this.language === newLanguage) { + return; + } + + this.props.language = newLanguage; + if (this.textModel) { + this.textModel.setLanguageId(newLanguage); + } + + this.language = newLanguage; + this.onDidChangeLanguageEmitter.fire(newLanguage); + this.onDidChangeContentEmitter.fire('language'); + } + + get uri(): URI { + return this.props.uri; + } + get handle(): number { + return this.props.handle; + } + get cellKind(): CellKind { + return this.props.cellKind; + } + + @postConstruct() + protected init(): void { + this._outputs = this.props.outputs.map(op => new NotebookCellOutputModel(op)); + this._metadata = this.props.metadata ?? {}; + this._internalMetadata = this.props.internalMetadata ?? {}; + } + + refChanged(node: HTMLLIElement): void { + if (node) { + this.htmlContext = node; + this.notebookCellContextManager.updateCellContext(this, node); + } + } + + dispose(): void { + this.onDidChangeOutputsEmitter.dispose(); + this.onDidChangeOutputItemsEmitter.dispose(); + this.onDidChangeContentEmitter.dispose(); + this.onDidChangeMetadataEmitter.dispose(); + this.onDidChangeInternalMetadataEmitter.dispose(); + this.onDidChangeLanguageEmitter.dispose(); + this.notebookCellContextManager.dispose(); + this.textModel.dispose(); + this.toDispose.dispose(); + } + + requestEdit(): void { + this.onDidRequestCellEditChangeEmitter.fire(true); + } + + requestStopEdit(): void { + this.onDidRequestCellEditChangeEmitter.fire(false); + } + + spliceNotebookCellOutputs(splice: NotebookCellOutputsSplice): void { + if (splice.deleteCount > 0 && splice.newOutputs.length > 0) { + const commonLen = Math.min(splice.deleteCount, splice.newOutputs.length); + // update + for (let i = 0; i < commonLen; i++) { + const currentOutput = this.outputs[splice.start + i]; + const newOutput = splice.newOutputs[i]; + + this.replaceOutputItems(currentOutput.outputId, newOutput); + } + + this.outputs.splice(splice.start + commonLen, splice.deleteCount - commonLen, ...splice.newOutputs.slice(commonLen).map(op => new NotebookCellOutputModel(op))); + this.onDidChangeOutputsEmitter.fire({ start: splice.start + commonLen, deleteCount: splice.deleteCount - commonLen, newOutputs: splice.newOutputs.slice(commonLen) }); + } else { + this.outputs.splice(splice.start, splice.deleteCount, ...splice.newOutputs.map(op => new NotebookCellOutputModel(op))); + this.onDidChangeOutputsEmitter.fire(splice); + } + } + + replaceOutputItems(outputId: string, newOutputItem: CellOutput): boolean { + const output = this.outputs.find(out => out.outputId === outputId); + + if (!output) { + return false; + } + + output.replaceData(newOutputItem); + this.onDidChangeOutputItemsEmitter.fire(); + return true; + } + + getData(): CellData { + return { + cellKind: this.cellKind, + language: this.language, + outputs: this.outputs.map(output => output.getData()), + source: this.textBuffer, + collapseState: this.props.collapseState, + internalMetadata: this.internalMetadata, + metadata: this.metadata + }; + } + + async resolveTextModel(): Promise { + if (this.textModel) { + return this.textModel; + } + + const ref = await this.textModelService.createModelReference(this.uri); + this.textModel = ref.object; + return ref.object; + } +} + +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-cell-output-model.ts b/packages/notebook/src/browser/view-model/notebook-cell-output-model.ts new file mode 100644 index 0000000000000..0e9205c4fcaf2 --- /dev/null +++ b/packages/notebook/src/browser/view-model/notebook-cell-output-model.ts @@ -0,0 +1,69 @@ +// ***************************************************************************** +// 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 { Disposable, Emitter } from '@theia/core'; +import { CellOutput, CellOutputItem } from '../../common'; + +export class NotebookCellOutputModel implements Disposable { + + private didChangeDataEmitter = new Emitter(); + readonly onDidChangeData = this.didChangeDataEmitter.event; + + private requestOutputPresentationChangeEmitter = new Emitter(); + readonly onRequestOutputPresentationChange = this.requestOutputPresentationChangeEmitter.event; + + get outputId(): string { + return this.rawOutput.outputId; + } + + get outputs(): CellOutputItem[] { + return this.rawOutput.outputs || []; + } + + get metadata(): Record | undefined { + return this.rawOutput.metadata; + } + + constructor(private rawOutput: CellOutput) { } + + replaceData(rawData: CellOutput): void { + this.rawOutput = rawData; + this.didChangeDataEmitter.fire(); + } + + appendData(items: CellOutputItem[]): void { + this.rawOutput.outputs.push(...items); + this.didChangeDataEmitter.fire(); + } + + dispose(): void { + this.didChangeDataEmitter.dispose(); + this.requestOutputPresentationChangeEmitter.dispose(); + } + + requestOutputPresentationUpdate(): void { + this.requestOutputPresentationChangeEmitter.fire(); + } + + getData(): CellOutput { + return { + outputs: this.outputs, + metadata: this.metadata, + outputId: this.outputId + }; + } + +} diff --git a/packages/notebook/src/browser/view-model/notebook-model.ts b/packages/notebook/src/browser/view-model/notebook-model.ts new file mode 100644 index 0000000000000..624ecedad58b5 --- /dev/null +++ b/packages/notebook/src/browser/view-model/notebook-model.ts @@ -0,0 +1,372 @@ +// ***************************************************************************** +// Copyright (C) 20023 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 { Disposable, Emitter, URI } from '@theia/core'; +import { Saveable, SaveOptions } from '@theia/core/lib/browser'; +import { + CellData, + CellEditOperation, CellEditType, CellUri, NotebookCellInternalMetadata, + NotebookCellsChangeType, NotebookCellTextModelSplice, NotebookData, + NotebookDocumentMetadata, 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'; +import { UndoRedoService } from '@theia/editor/lib/browser/undo-redo-service'; + +export const NotebookModelFactory = Symbol('NotebookModelFactory'); + +export function createNotebookModelContainer(parent: interfaces.Container, props: NotebookModelProps): interfaces.Container { + const child = parent.createChild(); + + child.bind(NotebookModelProps).toConstantValue(props); + child.bind(NotebookModel).toSelf(); + + return child; +} + +const NotebookModelProps = Symbol('NotebookModelProps'); +export interface NotebookModelProps { + data: NotebookData, + uri: URI, + viewType: string, + serializer: NotebookSerializer, +} + +@injectable() +export class NotebookModel implements Saveable, Disposable { + + private readonly onDirtyChangedEmitter = new Emitter(); + readonly onDirtyChanged = this.onDirtyChangedEmitter.event; + + private readonly onDidSaveNotebookEmitter = new Emitter(); + readonly onDidSaveNotebook = this.onDidSaveNotebookEmitter.event; + + private readonly onDidAddOrRemoveCellEmitter = new Emitter(); + readonly onDidAddOrRemoveCell = this.onDidAddOrRemoveCellEmitter.event; + + private readonly onDidChangeContentEmitter = new Emitter(); + readonly onDidChangeContent = this.onDidChangeContentEmitter.event; + + @inject(FileService) + private readonly fileService: FileService; + + @inject(UndoRedoService) + private readonly undoRedoService: UndoRedoService; + + readonly autoSave: 'off' | 'afterDelay' | 'onFocusChange' | 'onWindowChange'; + + nextHandle: number = 0; + + kernel?: NotebookKernel; + + dirty: boolean; + selectedCell?: NotebookCellModel; + private dirtyCells: NotebookCellModel[] = []; + + cells: NotebookCellModel[]; + + get uri(): URI { + return this.props.uri; + } + + get viewType(): string { + return this.props.viewType; + } + + metadata: NotebookDocumentMetadata = {}; + + constructor(@inject(NotebookModelProps) private props: NotebookModelProps, + @inject(MonacoTextModelService) modelService: MonacoTextModelService, + @inject(NotebookCellModelFactory) private cellModelFactory: (props: NotebookCellModelProps) => NotebookCellModel) { + this.dirty = false; + + this.cells = props.data.cells.map((cell, index) => cellModelFactory({ + uri: CellUri.generate(props.uri, index), + handle: index, + source: cell.source, + language: cell.language, + cellKind: cell.cellKind, + outputs: cell.outputs, + metadata: cell.metadata, + internalMetadata: cell.internalMetadata, + collapseState: cell.collapseState + })); + + this.addCellOutputListeners(this.cells); + + this.metadata = this.metadata; + + modelService.onDidCreate(editorModel => { + const modelUri = new URI(editorModel.uri); + if (modelUri.scheme === CellUri.scheme) { + const cellUri = CellUri.parse(modelUri); + if (cellUri && cellUri.notebook.isEqual(this.uri)) { + const cell = this.cells.find(c => c.handle === cellUri.handle); + if (cell) { + cell.textModel = editorModel; + } + } + } + }); + this.nextHandle = this.cells.length; + } + + dispose(): void { + this.onDirtyChangedEmitter.dispose(); + this.onDidSaveNotebookEmitter.dispose(); + this.onDidAddOrRemoveCellEmitter.dispose(); + this.onDidChangeContentEmitter.dispose(); + this.cells.forEach(cell => cell.dispose()); + } + + async save(options: SaveOptions): Promise { + this.dirtyCells = []; + this.dirty = false; + this.onDirtyChangedEmitter.fire(); + + const serializedNotebook = await this.props.serializer.notebookToData({ + cells: this.cells.map(cell => cell.getData()), + metadata: this.metadata + }); + this.fileService.writeFile(this.uri, serializedNotebook); + + this.onDidSaveNotebookEmitter.fire(); + } + + createSnapshot(): Saveable.Snapshot { + const model = this; + return { + read(): string { + return JSON.stringify({ + cells: model.cells.map(cell => cell.getData()), + metadata: model.metadata + }); + } + }; + } + + async revert(options?: Saveable.RevertOptions): Promise { + this.dirty = false; + this.onDirtyChangedEmitter.fire(); + } + + isDirty(): boolean { + return this.dirty; + } + + cellDirtyChanged(cell: NotebookCellModel, dirtyState: boolean): void { + if (dirtyState) { + this.dirtyCells.push(cell); + } else { + this.dirtyCells.splice(this.dirtyCells.indexOf(cell), 1); + } + + const oldDirtyState = this.dirty; + this.dirty = this.dirtyCells.length > 0; + if (this.dirty !== oldDirtyState) { + this.onDirtyChangedEmitter.fire(); + } + } + + undo(): void { + // TODO we probably need to check if a monaco editor is focused and if so, not undo + this.undoRedoService.undo(this.uri); + } + + redo(): void { + // TODO see undo + this.undoRedoService.redo(this.uri); + } + + setSelectedCell(cell: NotebookCellModel): void { + this.selectedCell = cell; + } + + private addCellOutputListeners(cells: NotebookCellModel[]): void { + for (const cell of cells) { + cell.onDidChangeOutputs(() => { + this.dirty = true; + this.onDirtyChangedEmitter.fire(); + }); + } + } + + applyEdits(rawEdits: CellEditOperation[], computeUndoRedo: boolean): 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, cellIndex } of editsWithDetails) { + const cell = this.cells[cellIndex]; + if (cell) { + this.cellDirtyChanged(cell, true); + } + switch (edit.editType) { + case CellEditType.Replace: + this.replaceCells(edit.index, edit.count, edit.cells, computeUndoRedo); + break; + case CellEditType.Output: { + if (edit.append) { + cell.spliceNotebookCellOutputs({ deleteCount: 0, newOutputs: edit.outputs, start: cell.outputs.length }); + } else { + // could definitely be more efficient. See vscode __spliceNotebookCellOutputs2 + // For now, just replace the whole existing output with the new output + cell.spliceNotebookCellOutputs({ start: 0, deleteCount: cell.outputs.length, newOutputs: edit.outputs }); + } + + break; + } + case CellEditType.OutputItems: + break; + case CellEditType.Metadata: + this.updateNotebookMetadata(edit.metadata, computeUndoRedo); + break; + case CellEditType.PartialInternalMetadata: + this.changeCellInternalMetadataPartial(this.cells[cellIndex], edit.internalMetadata); + break; + case CellEditType.CellLanguage: + this.changeCellLanguage(this.cells[cellIndex], edit.language, computeUndoRedo); + break; + case CellEditType.DocumentMetadata: + break; + case CellEditType.Move: + this.moveCellToIndex(cellIndex, edit.length, edit.newIdx, computeUndoRedo); + break; + + } + } + } + + private replaceCells(start: number, deleteCount: number, newCells: CellData[], computeUndoRedo: boolean): void { + const cells = newCells.map(cell => { + const handle = this.nextHandle++; + return this.cellModelFactory({ + uri: CellUri.generate(this.uri, handle), + handle: handle, + source: cell.source, + language: cell.language, + cellKind: cell.cellKind, + outputs: cell.outputs, + metadata: cell.metadata, + internalMetadata: cell.internalMetadata, + collapseState: cell.collapseState + }); + }); + this.addCellOutputListeners(cells); + + const changes: NotebookCellTextModelSplice[] = [[start, deleteCount, cells]]; + + const deletedCells = this.cells.splice(start, deleteCount, ...cells); + + for (const cell of deletedCells) { + cell.dispose(); + } + + if (computeUndoRedo) { + this.undoRedoService.pushElement(this.uri, + async () => this.replaceCells(start, newCells.length, deletedCells.map(cell => cell.getData()), false), + async () => this.replaceCells(start, deleteCount, newCells, false)); + } + + this.onDidAddOrRemoveCellEmitter.fire({ rawEvent: { kind: NotebookCellsChangeType.ModelChange, changes } }); + this.onDidChangeContentEmitter.fire({ rawEvents: [{ kind: NotebookCellsChangeType.ModelChange, changes }] }); + } + + 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; + this.onDidChangeContentEmitter.fire({ + rawEvents: [ + { kind: NotebookCellsChangeType.ChangeCellInternalMetadata, index: this.cells.indexOf(cell), internalMetadata: newInternalMetadata } + ] + }); + } + + private updateNotebookMetadata(metadata: NotebookDocumentMetadata, computeUndoRedo: boolean): void { + const oldMetadata = this.metadata; + if (computeUndoRedo) { + this.undoRedoService.pushElement(this.uri, + async () => this.updateNotebookMetadata(oldMetadata, false), + async () => this.updateNotebookMetadata(metadata, false) + ); + } + + this.metadata = metadata; + this.onDidChangeContentEmitter.fire({ + rawEvents: [{ kind: NotebookCellsChangeType.ChangeDocumentMetadata, metadata: this.metadata }], + synchronous: true, + }); + } + + private changeCellLanguage(cell: NotebookCellModel, languageId: string, computeUndoRedo: boolean): void { + if (cell.language === languageId) { + return; + } + + cell.language = languageId; + + this.onDidChangeContentEmitter.fire({ + rawEvents: [{ kind: NotebookCellsChangeType.ChangeCellLanguage, index: this.cells.indexOf(cell), language: languageId }], + synchronous: true, + }); + } + + private moveCellToIndex(fromIndex: number, length: number, toIndex: number, computeUndoRedo: boolean): boolean { + if (computeUndoRedo) { + this.undoRedoService.pushElement(this.uri, + async () => { this.moveCellToIndex(toIndex, length, fromIndex, false); }, + async () => { this.moveCellToIndex(fromIndex, length, toIndex, false); } + ); + } + + const cells = this.cells.splice(fromIndex, length); + this.cells.splice(toIndex, 0, ...cells); + this.onDidChangeContentEmitter.fire({ + rawEvents: [{ kind: NotebookCellsChangeType.Move, index: fromIndex, length, newIdx: toIndex, cells }], + }); + + return true; + } + + private getCellIndexByHandle(handle: number): number { + return this.cells.findIndex(c => c.handle === handle); + } +} diff --git a/packages/notebook/src/browser/view/notebook-cell-editor.tsx b/packages/notebook/src/browser/view/notebook-cell-editor.tsx new file mode 100644 index 0000000000000..da6afb8e97ee4 --- /dev/null +++ b/packages/notebook/src/browser/view/notebook-cell-editor.tsx @@ -0,0 +1,97 @@ +// ***************************************************************************** +// 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 * as React from '@theia/core/shared/react'; +import { NotebookModel } from '../view-model/notebook-model'; +import { NotebookCellModel } from '../view-model/notebook-cell-model'; +import { MonacoCodeEditor } from '@theia/monaco/lib/browser/monaco-code-editor'; +import { MonacoEditorServices } from '@theia/monaco/lib/browser/monaco-editor'; +import { MonacoEditorProvider } from '@theia/monaco/lib/browser/monaco-editor-provider'; +import { DisposableCollection } from '@theia/core'; + +interface CellEditorProps { + notebookModel: NotebookModel, + cell: NotebookCellModel, + monacoServices: MonacoEditorServices +} + +const DEFAULT_EDITOR_OPTIONS = { + ...MonacoEditorProvider.inlineOptions, + minHeight: -1, + maxHeight: -1, + scrollbar: { + ...MonacoEditorProvider.inlineOptions.scrollbar, + alwaysConsumeMouseWheel: false + } +}; + +export class CellEditor extends React.Component { + + protected editor?: MonacoCodeEditor; + protected toDispose = new DisposableCollection(); + protected container?: HTMLDivElement; + + override componentDidMount(): void { + this.disposeEditor(); + this.initEditor(); + } + + override componentWillUnmount(): void { + this.disposeEditor(); + } + + protected disposeEditor(): void { + this.toDispose.dispose(); + this.toDispose = new DisposableCollection(); + } + + protected async initEditor(): Promise { + const { cell, notebookModel, monacoServices } = this.props; + if (this.container) { + const editorNode = this.container; + const editorModel = await cell.resolveTextModel(); + const uri = cell.uri; + this.editor = new MonacoCodeEditor(uri, + editorModel, + editorNode, + monacoServices, + DEFAULT_EDITOR_OPTIONS); + this.toDispose.push(this.editor); + this.editor.setLanguage(cell.language); + this.toDispose.push(this.editor.getControl().onDidContentSizeChange(() => { + editorNode.style.height = this.editor!.getControl().getContentHeight() + 7 + 'px'; + this.editor!.setSize({ width: -1, height: this.editor!.getControl().getContentHeight() }); + })); + this.toDispose.push(this.editor.onDocumentContentChanged(e => { + notebookModel.cellDirtyChanged(cell, true); + cell.source = e.document.getText(); + })); + } + } + + protected assignRef = (component: HTMLDivElement) => { + this.container = component; + }; + + protected handleResize = () => { + this.editor?.refresh(); + }; + + override render(): React.ReactNode { + return
; + } + +} diff --git a/packages/notebook/src/browser/view/notebook-cell-list-view.tsx b/packages/notebook/src/browser/view/notebook-cell-list-view.tsx new file mode 100644 index 0000000000000..ada36f51fc247 --- /dev/null +++ b/packages/notebook/src/browser/view/notebook-cell-list-view.tsx @@ -0,0 +1,171 @@ +// ***************************************************************************** +// 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 * as React from '@theia/core/shared/react'; +import { CellEditType, CellKind } from '../../common'; +import { NotebookCellModel } from '../view-model/notebook-cell-model'; +import { NotebookModel } from '../view-model/notebook-model'; +import { NotebookCellToolbarFactory } from './notebook-cell-toolbar-factory'; +import { codicon } from '@theia/core/lib/browser'; +import { CommandRegistry, DisposableCollection, nls } from '@theia/core'; +import { NotebookCommands } from '../contributions/notebook-actions-contribution'; +import { NotebookCellActionContribution } from '../contributions/notebook-cell-actions-contribution'; + +export interface CellRenderer { + render(notebookData: NotebookModel, cell: NotebookCellModel, index: number): React.ReactNode +} + +interface CellListProps { + renderers: Map; + notebookModel: NotebookModel; + toolbarRenderer: NotebookCellToolbarFactory; + commandRegistry: CommandRegistry +} + +interface NotebookCellListState { + selectedCell?: NotebookCellModel; + dragOverIndicator: { cell: NotebookCellModel, position: 'top' | 'bottom' } | undefined; +} + +export class NotebookCellListView extends React.Component { + + protected toDispose = new DisposableCollection(); + + constructor(props: CellListProps) { + super(props); + this.state = { selectedCell: undefined, dragOverIndicator: undefined }; + this.toDispose.push(props.notebookModel.onDidAddOrRemoveCell(e => { + this.setState({ selectedCell: undefined }); + })); + } + + override componentWillUnmount(): void { + this.toDispose.dispose(); + } + + override render(): React.ReactNode { + return
    + {this.props.notebookModel.cells + .map((cell, index) => + + this.onAddNewCell(kind, index)} + onDrop={e => this.onDrop(e, index)} + onDragOver={e => this.onDragOver(e, cell, 'top')} /> + {this.shouldRenderDragOverIndicator(cell, 'top') && } +
  • { + this.setState({ selectedCell: cell }); + this.props.notebookModel.setSelectedCell(cell); + }} + onDragStart={e => this.onDragStart(e, index)} + onDragOver={e => this.onDragOver(e, cell)} + onDrop={e => this.onDrop(e, index)} + draggable={true} + ref={(node: HTMLLIElement) => cell.refChanged(node)}> +
    +
    + {this.renderCellContent(cell, index)} +
    + {this.state.selectedCell === cell && + this.props.toolbarRenderer.renderCellToolbar(NotebookCellActionContribution.ACTION_MENU, this.props.notebookModel, cell)} +
  • + {this.shouldRenderDragOverIndicator(cell, 'bottom') && } +
    + ) + } + this.onAddNewCell(kind, this.props.notebookModel.cells.length)} + onDrop={e => this.onDrop(e, this.props.notebookModel.cells.length - 1)} + onDragOver={e => this.onDragOver(e, this.props.notebookModel.cells[this.props.notebookModel.cells.length - 1], 'bottom')} /> +
; + } + + renderCellContent(cell: NotebookCellModel, index: number): React.ReactNode { + const renderer = this.props.renderers.get(cell.cellKind); + if (!renderer) { + throw new Error(`No renderer found for cell type ${cell.cellKind}`); + } + return renderer.render(this.props.notebookModel, cell, index); + } + + protected onDragStart(event: React.DragEvent, index: number): void { + event.stopPropagation(); + event.dataTransfer.setData('text/notebook-cell-index', index.toString()); + } + + protected onDragOver(event: React.DragEvent, cell: NotebookCellModel, position?: 'top' | 'bottom'): void { + event.preventDefault(); + event.stopPropagation(); + // show indicator + this.setState({ ...this.state, dragOverIndicator: { cell, position: position ?? event.nativeEvent.offsetY < event.currentTarget.clientHeight / 2 ? 'top' : 'bottom' } }); + } + + protected onDrop(event: React.DragEvent, dropElementIndex: number): void { + const index = parseInt(event.dataTransfer.getData('text/notebook-cell-index')); + const isTargetBelow = index < dropElementIndex; + let newIdx = this.state.dragOverIndicator?.position === 'top' ? dropElementIndex : dropElementIndex + 1; + newIdx = isTargetBelow ? newIdx - 1 : newIdx; + if (index !== undefined && index !== dropElementIndex) { + this.props.notebookModel.applyEdits([{ + editType: CellEditType.Move, + length: 1, + index, + newIdx + }], true); + } + this.setState({ ...this.state, dragOverIndicator: undefined }); + } + + protected onAddNewCell(kind: CellKind, index: number): void { + this.props.commandRegistry.executeCommand(NotebookCommands.ADD_NEW_CELL_COMMAND.id, + this.props.notebookModel, + kind, + index + ); + } + + protected shouldRenderDragOverIndicator(cell: NotebookCellModel, position: 'top' | 'bottom'): boolean { + return this.state.dragOverIndicator !== undefined && + this.state.dragOverIndicator.cell === cell && + this.state.dragOverIndicator.position === position; + } + +} + +export interface NotebookCellDividerProps { + onAddNewCell: (type: CellKind) => void; + onDrop: (event: React.DragEvent) => void; + onDragOver: (event: React.DragEvent) => void; +} + +export function NotebookCellDivider({ onAddNewCell, onDrop, onDragOver }: NotebookCellDividerProps): React.JSX.Element { + const [hover, setHover] = React.useState(false); + + return
  • setHover(true)} onMouseLeave={() => setHover(false)} onDrop={onDrop} onDragOver={onDragOver}> + {hover &&
    + + +
    } +
  • ; +} + +function CellDropIndicator(): React.JSX.Element { + return
    ; +} diff --git a/packages/notebook/src/browser/view/notebook-cell-toolbar-factory.tsx b/packages/notebook/src/browser/view/notebook-cell-toolbar-factory.tsx new file mode 100644 index 0000000000000..31fb3a15d2e5c --- /dev/null +++ b/packages/notebook/src/browser/view/notebook-cell-toolbar-factory.tsx @@ -0,0 +1,91 @@ +// ***************************************************************************** +// 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 * as React from '@theia/core/shared/react'; +import { CommandRegistry, CompoundMenuNodeRole, MenuModelRegistry, MenuNode } from '@theia/core'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { ContextKeyService } from '@theia/core/lib/browser/context-key-service'; +import { NotebookCellSidebar, NotebookCellToolbar } from './notebook-cell-toolbar'; +import { ContextMenuRenderer } from '@theia/core/lib/browser'; +import { NotebookModel } from '../view-model/notebook-model'; +import { NotebookCellModel } from '../view-model/notebook-cell-model'; +import { NotebookCellOutputModel } from '../view-model/notebook-cell-output-model'; + +export interface NotebookCellToolbarItem { + id: string; + icon?: string; + label?: string; + onClick: (e: React.MouseEvent) => void; +} + +@injectable() +export class NotebookCellToolbarFactory { + + @inject(MenuModelRegistry) + protected menuRegistry: MenuModelRegistry; + + @inject(ContextKeyService) + protected contextKeyService: ContextKeyService; + + @inject(ContextMenuRenderer) + protected readonly contextMenuRenderer: ContextMenuRenderer; + + @inject(CommandRegistry) + protected readonly commandRegistry: CommandRegistry; + + renderCellToolbar(menuPath: string[], notebookModel: NotebookModel, cell: NotebookCellModel): React.ReactNode { + return this.getMenuItems(menuPath, notebookModel, cell)} + onContextKeysChanged={cell.notebookCellContextManager.onDidChangeContext} />; + } + + renderSidebar(menuPath: string[], notebookModel: NotebookModel, cell: NotebookCellModel, output?: NotebookCellOutputModel): React.ReactNode { + return this.getMenuItems(menuPath, notebookModel, cell, output)} + onContextKeysChanged={cell.notebookCellContextManager.onDidChangeContext} />; + } + + private getMenuItems(menuItemPath: string[], notebookModel: NotebookModel, cell: NotebookCellModel, output?: NotebookCellOutputModel): NotebookCellToolbarItem[] { + const inlineItems: NotebookCellToolbarItem[] = []; + + for (const menuNode of this.menuRegistry.getMenu(menuItemPath).children) { + if (!menuNode.when || this.contextKeyService.match(menuNode.when, cell.context ?? undefined)) { + if (menuNode.role === CompoundMenuNodeRole.Flat) { + inlineItems.push(...menuNode.children?.map(child => this.createToolbarItem(child, notebookModel, cell, output)) ?? []); + } else { + inlineItems.push(this.createToolbarItem(menuNode, notebookModel, cell, output)); + } + } + } + return inlineItems; + } + + private createToolbarItem(menuNode: MenuNode, notebookModel: NotebookModel, cell: NotebookCellModel, output?: NotebookCellOutputModel): NotebookCellToolbarItem { + const menuPath = menuNode.role === CompoundMenuNodeRole.Submenu ? this.menuRegistry.getPath(menuNode) : undefined; + return { + id: menuNode.id, + icon: menuNode.icon, + label: menuNode.label, + onClick: menuPath ? + e => this.contextMenuRenderer.render( + { + anchor: e.nativeEvent, + menuPath, + includeAnchorArg: false, + args: [notebookModel, cell, output] + }) : + () => this.commandRegistry.executeCommand(menuNode.command!, notebookModel, cell, output) + }; + } +} diff --git a/packages/notebook/src/browser/view/notebook-cell-toolbar.tsx b/packages/notebook/src/browser/view/notebook-cell-toolbar.tsx new file mode 100644 index 0000000000000..d1e366eb43b1b --- /dev/null +++ b/packages/notebook/src/browser/view/notebook-cell-toolbar.tsx @@ -0,0 +1,70 @@ +// ***************************************************************************** +// 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 * as React from '@theia/core/shared/react'; +import { ACTION_ITEM } from '@theia/core/lib/browser'; +import { NotebookCellToolbarItem } from './notebook-cell-toolbar-factory'; +import { DisposableCollection, Event } from '@theia/core'; + +export interface NotebookCellToolbarProps { + getMenuItems: () => NotebookCellToolbarItem[]; + onContextKeysChanged: Event; +} + +interface NotebookCellToolbarState { + inlineItems: NotebookCellToolbarItem[]; +} + +abstract class NotebookCellActionItems extends React.Component { + + protected toDispose = new DisposableCollection(); + + constructor(props: NotebookCellToolbarProps) { + super(props); + this.toDispose.push(props.onContextKeysChanged(e => { + this.setState({ inlineItems: this.props.getMenuItems() }); + })); + this.state = { inlineItems: this.props.getMenuItems() }; + } + + override componentWillUnmount(): void { + this.toDispose.dispose(); + } + + protected renderItem(item: NotebookCellToolbarItem): React.ReactNode { + return
    ; + } + +} + +export class NotebookCellToolbar extends NotebookCellActionItems { + + override render(): React.ReactNode { + return
    + {this.state.inlineItems.map(item => this.renderItem(item))} +
    ; + } + +} + +export class NotebookCellSidebar extends NotebookCellActionItems { + + override render(): React.ReactNode { + return
    + {this.state.inlineItems.map(item => this.renderItem(item))} +
    ; + } +} + diff --git a/packages/notebook/src/browser/view/notebook-code-cell-view.tsx b/packages/notebook/src/browser/view/notebook-code-cell-view.tsx new file mode 100644 index 0000000000000..4ef29d2c375e8 --- /dev/null +++ b/packages/notebook/src/browser/view/notebook-code-cell-view.tsx @@ -0,0 +1,190 @@ +// ***************************************************************************** +// 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 { inject, injectable } from '@theia/core/shared/inversify'; +import * as React from '@theia/core/shared/react'; +import { MonacoEditorServices } from '@theia/monaco/lib/browser/monaco-editor'; +import { CellOutputWebviewFactory, CellOutputWebview } from '../renderers/cell-output-webview'; +import { NotebookRendererRegistry } from '../notebook-renderer-registry'; +import { NotebookCellModel } from '../view-model/notebook-cell-model'; +import { NotebookModel } from '../view-model/notebook-model'; +import { CellEditor } from './notebook-cell-editor'; +import { CellRenderer } from './notebook-cell-list-view'; +import { NotebookCellToolbarFactory } from './notebook-cell-toolbar-factory'; +import { NotebookCellActionContribution } from '../contributions/notebook-cell-actions-contribution'; +import { CellExecution, NotebookExecutionStateService } from '../service/notebook-execution-state-service'; +import { codicon } from '@theia/core/lib/browser'; +import { NotebookCellExecutionState } from '../../common'; +import { DisposableCollection } from '@theia/core'; + +@injectable() +export class NotebookCodeCellRenderer implements CellRenderer { + @inject(MonacoEditorServices) + protected readonly monacoServices: MonacoEditorServices; + + @inject(NotebookRendererRegistry) + protected readonly notebookRendererRegistry: NotebookRendererRegistry; + + @inject(CellOutputWebviewFactory) + protected readonly cellOutputWebviewFactory: CellOutputWebviewFactory; + + @inject(NotebookCellToolbarFactory) + protected readonly notebookCellToolbarFactory: NotebookCellToolbarFactory; + + @inject(NotebookExecutionStateService) + protected readonly executionStateService: NotebookExecutionStateService; + + render(notebookModel: NotebookModel, cell: NotebookCellModel, handle: number): React.ReactNode { + return
    +
    +
    + {this.notebookCellToolbarFactory.renderSidebar(NotebookCellActionContribution.CODE_CELL_SIDEBAR_MENU, notebookModel, cell)} + {/* cell-execution-order needs an own component. Could be a little more complicated +

    {`[${cell.exec ?? ' '}]`}

    */} +
    +
    + + +
    +
    +
    + + this.notebookCellToolbarFactory.renderSidebar(NotebookCellActionContribution.OUTPUT_SIDEBAR_MENU, notebookModel, cell, cell.outputs[0])} /> +
    +
    ; + } +} + +export interface NotebookCodeCellStatusProps { + cell: NotebookCellModel; + executionStateService: NotebookExecutionStateService +} + +export class NotebookCodeCellStatus extends React.Component { + + protected toDispose = new DisposableCollection(); + + constructor(props: NotebookCodeCellStatusProps) { + super(props); + + this.state = {}; + + this.toDispose.push(props.executionStateService.onDidChangeExecution(event => { + if (event.affectsCell(this.props.cell.uri)) { + this.setState({ currentExecution: event.changed }); + } + })); + } + + override componentWillUnmount(): void { + this.toDispose.dispose(); + } + + override render(): React.ReactNode { + return
    +
    + {this.renderExecutionState()} +
    +
    + {this.props.cell.language} +
    +
    ; + } + + private renderExecutionState(): React.ReactNode { + const state = this.state.currentExecution?.state; + const { lastRunSuccess } = this.props.cell.internalMetadata; + + let iconClasses: string | undefined = undefined; + let color: string | undefined = undefined; + if (!state && lastRunSuccess) { + iconClasses = codicon('check'); + color = 'green'; + } else if (!state && lastRunSuccess === false) { + iconClasses = codicon('error'); + color = 'red'; + } else if (state === NotebookCellExecutionState.Pending || state === NotebookCellExecutionState.Unconfirmed) { + iconClasses = codicon('clock'); + } else if (state === NotebookCellExecutionState.Executing) { + iconClasses = `${codicon('sync')} theia-animation-spin`; + } + return <> + {iconClasses && + <> + +
    {this.getExecutionTime()}
    + } + ; + } + + private getExecutionTime(): string { + const { runStartTime, runEndTime } = this.props.cell.internalMetadata; + if (runStartTime && runEndTime) { + return `${((runEndTime - runStartTime) / 1000).toLocaleString(undefined, { maximumFractionDigits: 1, minimumFractionDigits: 1 })}s`; + } + return '0.0s'; + } +} + +interface NotebookCellOutputProps { + cell: NotebookCellModel; + outputWebviewFactory: CellOutputWebviewFactory; + renderSidebar: () => React.ReactNode; +} + +export class NotebookCodeCellOutputs extends React.Component { + + protected outputsWebview: CellOutputWebview | undefined; + + constructor(props: NotebookCellOutputProps) { + super(props); + } + + override async componentDidMount(): Promise { + const { cell, outputWebviewFactory } = this.props; + cell.onDidChangeOutputs(async () => { + if (!this.outputsWebview && cell.outputs.length > 0) { + this.outputsWebview = await outputWebviewFactory(cell); + } else if (this.outputsWebview && cell.outputs.length === 0) { + this.outputsWebview.dispose(); + this.outputsWebview = undefined; + } + this.forceUpdate(); + }); + if (cell.outputs.length > 0) { + this.outputsWebview = await outputWebviewFactory(cell); + this.forceUpdate(); + } + } + + override componentDidUpdate(): void { + if (!this.outputsWebview?.isAttached()) { + this.outputsWebview?.attachWebview(); + } + } + + override render(): React.ReactNode { + return this.outputsWebview ? + <> + {this.props.renderSidebar()} + {this.outputsWebview.render()} + : + <>; + + } + +} diff --git a/packages/notebook/src/browser/view/notebook-main-toolbar.tsx b/packages/notebook/src/browser/view/notebook-main-toolbar.tsx new file mode 100644 index 0000000000000..bc12319b10ca5 --- /dev/null +++ b/packages/notebook/src/browser/view/notebook-main-toolbar.tsx @@ -0,0 +1,115 @@ +// ***************************************************************************** +// 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 { CommandRegistry, CompoundMenuNodeRole, DisposableCollection, MenuModelRegistry, MenuNode, nls } from '@theia/core'; +import * as React from '@theia/core/shared/react'; +import { codicon } from '@theia/core/lib/browser'; +import { NotebookCommands, NotebookMenus } from '../contributions/notebook-actions-contribution'; +import { NotebookModel } from '../view-model/notebook-model'; +import { NotebookKernelService } from '../service/notebook-kernel-service'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { ContextKeyService } from '@theia/core/lib/browser/context-key-service'; + +export interface NotebookMainToolbarProps { + notebookModel: NotebookModel + menuRegistry: MenuModelRegistry; + notebookKernelService: NotebookKernelService; + commandRegistry: CommandRegistry; + contextKeyService: ContextKeyService; +} + +@injectable() +export class NotebookMainToolbarRenderer { + @inject(NotebookKernelService) protected readonly notebookKernelService: NotebookKernelService; + @inject(CommandRegistry) protected readonly commandRegistry: CommandRegistry; + @inject(MenuModelRegistry) protected readonly menuRegistry: MenuModelRegistry; + @inject(ContextKeyService) protected readonly contextKeyService: ContextKeyService; + + render(notebookModel: NotebookModel): React.ReactNode { + return ; + } +} + +export class NotebookMainToolbar extends React.Component { + + protected toDispose = new DisposableCollection(); + + constructor(props: NotebookMainToolbarProps) { + super(props); + + this.state = { selectedKernelLabel: props.notebookKernelService.getSelectedOrSuggestedKernel(props.notebookModel)?.label }; + this.toDispose.push(props.notebookKernelService.onDidChangeSelectedKernel(event => { + if (props.notebookModel.uri.isEqual(event.notebook)) { + this.setState({ selectedKernelLabel: props.notebookKernelService.getKernel(event.newKernel ?? '')?.label }); + } + })); + // in case the selected kernel is added after the notebook is loaded + this.toDispose.push(props.notebookKernelService.onDidAddKernel(() => { + if (!this.state.selectedKernelLabel) { + this.setState({ selectedKernelLabel: props.notebookKernelService.getSelectedOrSuggestedKernel(props.notebookModel)?.label }); + } + })); + } + + override componentWillUnmount(): void { + this.toDispose.dispose(); + } + + override render(): React.ReactNode { + return
    + {this.getMenuItems().map(item => this.renderMenuItem(item))} +
    +
    this.props.commandRegistry.executeCommand(NotebookCommands.SELECT_KERNEL_COMMAND.id, this.props.notebookModel)}> + + + {this.state.selectedKernelLabel ?? nls.localizeByDefault('Select Kernel')} + +
    +
    ; + } + + protected renderMenuItem(item: MenuNode): React.ReactNode { + if (item.role === CompoundMenuNodeRole.Group) { + const itemNodes = item.children?.map(child => this.renderMenuItem(child)).filter(child => !!child); + return + {itemNodes} + {itemNodes && itemNodes.length > 0 && } + ; + } else if (!item.when || this.props.contextKeyService.match(item.when)) { + return
    { + if (item.command) { + this.props.commandRegistry.executeCommand(item.command, this.props.notebookModel); + } + }}> + + {item.label} +
    ; + } + return undefined; + } + + private getMenuItems(): readonly MenuNode[] { + const menuPath = NotebookMenus.NOTEBOOK_MAIN_TOOLBAR; + const pluginCommands = this.props.menuRegistry.getMenuNode(menuPath).children; + return this.props.menuRegistry.getMenu([menuPath]).children.concat(pluginCommands); + } +} diff --git a/packages/notebook/src/browser/view/notebook-markdown-cell-view.tsx b/packages/notebook/src/browser/view/notebook-markdown-cell-view.tsx new file mode 100644 index 0000000000000..3b48767a987ae --- /dev/null +++ b/packages/notebook/src/browser/view/notebook-markdown-cell-view.tsx @@ -0,0 +1,73 @@ +// ***************************************************************************** +// 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 * as React from '@theia/core/shared/react'; +import { MarkdownRenderer } from '@theia/core/lib/browser/markdown-rendering/markdown-renderer'; +import { MarkdownStringImpl } from '@theia/core/lib/common/markdown-rendering/markdown-string'; +import { NotebookModel } from '../view-model/notebook-model'; +import { CellRenderer } from './notebook-cell-list-view'; +import { NotebookCellModel } from '../view-model/notebook-cell-model'; +import { CellEditor } from './notebook-cell-editor'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { MonacoEditorServices } from '@theia/monaco/lib/browser/monaco-editor'; +import { nls } from '@theia/core'; + +@injectable() +export class NotebookMarkdownCellRenderer implements CellRenderer { + + @inject(MarkdownRenderer) + private readonly markdownRenderer: MarkdownRenderer; + @inject(MonacoEditorServices) + protected readonly monacoServices: MonacoEditorServices; + + render(notebookModel: NotebookModel, cell: NotebookCellModel): React.ReactNode { + return ; + } + +} + +interface MarkdownCellProps { + markdownRenderer: MarkdownRenderer, + monacoServices: MonacoEditorServices + + cell: NotebookCellModel, + notebookModel: NotebookModel +} + +function MarkdownCell({ markdownRenderer, monacoServices, cell, notebookModel }: MarkdownCellProps): React.JSX.Element { + const [editMode, setEditMode] = React.useState(false); + + React.useEffect(() => { + const listener = cell.onDidRequestCellEditChange(cellEdit => setEditMode(cellEdit)); + return () => listener.dispose(); + }, [editMode]); + + let markdownContent = React.useMemo(() => markdownRenderer.render(new MarkdownStringImpl(cell.source)).element.innerHTML, [cell, editMode]); + if (markdownContent.length === 0) { + markdownContent = `${nls.localizeByDefault('Empty markdown cell, double-click or press enter to edit.')}`; + } + + return editMode ? + : +
    cell.requestEdit()} + // This sets the non React HTML node from the markdown renderers output as a child node to this react component + // This is currently sadly the best way we have to combine React (Virtual Nodes) and normal dom nodes + // the HTML is allready sanitized by the markdown renderer, so we don't need to sanitize it again + dangerouslySetInnerHTML={{ __html: markdownContent }} // eslint-disable-line react/no-danger + />; +} diff --git a/packages/notebook/src/common/index.ts b/packages/notebook/src/common/index.ts new file mode 100644 index 0000000000000..548224ec19729 --- /dev/null +++ b/packages/notebook/src/common/index.ts @@ -0,0 +1,18 @@ +// ***************************************************************************** +// 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 +// ***************************************************************************** + +export * from './notebook-common'; +export * from './notebook-range'; diff --git a/packages/notebook/src/common/notebook-common.ts b/packages/notebook/src/common/notebook-common.ts new file mode 100644 index 0000000000000..86b59ff7dd5b3 --- /dev/null +++ b/packages/notebook/src/common/notebook-common.ts @@ -0,0 +1,462 @@ +// ***************************************************************************** +// 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 { 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 { UriComponents } from '@theia/core/lib/common/uri'; +import { CellRange } from './notebook-range'; + +export enum CellKind { + Markup = 1, + Code = 2 +} + +export interface NotebookCellMetadata { + /** + * custom metadata + */ + [key: string]: unknown; +} + +export interface NotebookCellInternalMetadata { + executionId?: string; + executionOrder?: number; + lastRunSuccess?: boolean; + runStartTime?: number; + runStartTimeAdjustment?: number; + runEndTime?: number; + renderDuration?: { [key: string]: number }; +} + +export type NotebookDocumentMetadata = Record; + +export interface NotebookCellStatusBarItem { + readonly alignment: CellStatusbarAlignment; + readonly priority?: number; + readonly text: string; + // readonly color?: string | ThemeColor; + // readonly backgroundColor?: string | ThemeColor; + readonly tooltip?: string | MarkdownString; + readonly command?: string | Command; + // readonly accessibilityInformation?: IAccessibilityInformation; + readonly opacity?: string; + readonly onlyShowWhenActive?: boolean; +} + +export const enum CellStatusbarAlignment { + Left = 1, + Right = 2 +} + +export type TransientCellMetadata = { readonly [K in keyof NotebookCellMetadata]?: boolean }; +export type CellContentMetadata = { readonly [K in keyof NotebookCellMetadata]?: boolean }; +export type TransientDocumentMetadata = { readonly [K in keyof NotebookDocumentMetadata]?: boolean }; + +export interface TransientOptions { + readonly transientOutputs: boolean; + readonly transientCellMetadata: TransientCellMetadata; + readonly transientDocumentMetadata: TransientDocumentMetadata; +} + +export interface NotebookExtensionDescription { + readonly id: string; + readonly location: string | undefined; +} + +export interface CellOutputItem { + readonly mime: string; + readonly data: BinaryBuffer; +} + +export interface CellOutput { + outputId: string; + outputs: CellOutputItem[]; + metadata?: Record; +} + +export interface NotebookCellCollapseState { + inputCollapsed?: boolean; + outputCollapsed?: boolean; +} + +export interface NotebookCell { + readonly uri: URI; + handle: number; + language: string; + cellKind: CellKind; + outputs: CellOutput[]; + metadata: NotebookCellMetadata; + internalMetadata: NotebookCellInternalMetadata; + textBuffer: string; + onDidChangeOutputs?: Event; + onDidChangeOutputItems?: Event; + onDidChangeLanguage: Event; + onDidChangeMetadata: Event; + onDidChangeInternalMetadata: Event; + +} + +export interface CellData { + source: string; + language: string; + cellKind: CellKind; + outputs: CellOutput[]; + metadata?: NotebookCellMetadata; + internalMetadata?: NotebookCellInternalMetadata; + collapseState?: NotebookCellCollapseState; +} + +export interface CellReplaceEdit { + editType: CellEditType.Replace; + index: number; + count: number; + cells: CellData[]; +} + +export interface NotebookDocumentMetadataEdit { + editType: CellEditType.DocumentMetadata; + metadata: NotebookDocumentMetadata; +} + +export interface NotebookData { + readonly cells: CellData[]; + readonly metadata: NotebookDocumentMetadata; +} + +export interface NotebookContributionData { + extension?: string; + providerDisplayName: string; + displayName: string; + filenamePattern: (string)[]; + exclusive: boolean; +} + +export interface NotebookCellStatusBarItemList { + items: NotebookCellStatusBarItem[]; + dispose?(): void; +} + +export interface NotebookCellStatusBarItemProvider { + viewType: string; + onDidChangeStatusBarItems?: Event; + provideCellStatusBarItems(uri: UriComponents, index: number, token: CancellationToken): Promise; +} + +export interface NotebookCellOutputsSplice { + start: number /* start */; + deleteCount: number /* delete count */; + newOutputs: CellOutput[]; +}; + +export interface CellInternalMetadataChangedEvent { + readonly lastRunSuccessChanged?: boolean; +} + +export type NotebookCellTextModelSplice = [ + start: number, + deleteCount: number, + newItems: T[] +]; + +export enum NotebookCellsChangeType { + ModelChange = 1, + Move = 2, + ChangeCellLanguage = 5, + Initialize = 6, + ChangeCellMetadata = 7, + Output = 8, + OutputItem = 9, + ChangeCellContent = 10, + ChangeDocumentMetadata = 11, + ChangeCellInternalMetadata = 12, + // ChangeCellMime = 13, + 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; + readonly endSelectionState?: SelectionState; +}; + +export interface NotebookCellsInitializeEvent { + readonly kind: NotebookCellsChangeType.Initialize; + readonly changes: NotebookCellTextModelSplice[]; +} + +export interface NotebookCellsChangeLanguageEvent { + readonly kind: NotebookCellsChangeType.ChangeCellLanguage; + readonly index: number; + readonly language: string; +} + +export interface NotebookCellsModelChangedEvent { + readonly kind: NotebookCellsChangeType.ModelChange; + readonly changes: NotebookCellTextModelSplice[]; +} + +export interface NotebookCellsModelMoveEvent { + readonly kind: NotebookCellsChangeType.Move; + readonly index: number; + readonly length: number; + readonly newIdx: number; + readonly cells: T[]; +} + +export interface NotebookOutputChangedEvent { + readonly kind: NotebookCellsChangeType.Output; + readonly index: number; + readonly outputs: CellOutput[]; + readonly append: boolean; +} + +export interface NotebookOutputItemChangedEvent { + readonly kind: NotebookCellsChangeType.OutputItem; + readonly index: number; + readonly outputId: string; + readonly outputItems: CellOutputItem[]; + readonly append: boolean; +} +export interface NotebookCellsChangeMetadataEvent { + readonly kind: NotebookCellsChangeType.ChangeCellMetadata; + readonly index: number; + readonly metadata: NotebookCellMetadata; +} + +export interface NotebookCellsChangeInternalMetadataEvent { + readonly kind: NotebookCellsChangeType.ChangeCellInternalMetadata; + readonly index: number; + readonly internalMetadata: NotebookCellInternalMetadata; +} + +export interface NotebookDocumentChangeMetadataEvent { + readonly kind: NotebookCellsChangeType.ChangeDocumentMetadata; + readonly metadata: NotebookDocumentMetadata; +} + +export interface NotebookDocumentUnknownChangeEvent { + readonly kind: NotebookCellsChangeType.Unknown; +} + +export interface NotebookCellContentChangeEvent { + readonly kind: NotebookCellsChangeType.ChangeCellContent; + readonly index: number; +} + +export type NotebookRawContentEvent = (NotebookCellsInitializeEvent | NotebookDocumentChangeMetadataEvent | NotebookCellContentChangeEvent | + NotebookCellsModelChangedEvent | NotebookCellsModelMoveEvent | NotebookOutputChangedEvent | NotebookOutputItemChangedEvent | + NotebookCellsChangeLanguageEvent | NotebookCellsChangeMetadataEvent | + NotebookCellsChangeInternalMetadataEvent | NotebookDocumentUnknownChangeEvent); // & { transient: boolean }; + +export interface NotebookModelChangedEvent { + readonly rawEvents: NotebookRawContentEvent[]; + readonly versionId: number; + // readonly synchronous: boolean | undefined; + // readonly endSelectionState: ISelectionState | undefined; +}; + +export interface NotebookModelWillAddRemoveEvent { + readonly rawEvent: NotebookCellsModelChangedEvent; +}; + +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 interface CellOutputEdit { + editType: CellEditType.Output; + index: number; + outputs: CellOutput[]; + append?: boolean; +} + +export interface CellOutputEditByHandle { + editType: CellEditType.Output; + handle: number; + outputs: CellOutput[]; + append?: boolean; +} + +export interface CellOutputItemEdit { + editType: CellEditType.OutputItems; + items: CellOutputItem[]; + append?: boolean; +} + +export interface CellMetadataEdit { + editType: CellEditType.Metadata; + index: number; + metadata: NotebookCellMetadata; +} + +export interface CellLanguageEdit { + editType: CellEditType.CellLanguage; + index: number; + language: string; +} + +export interface DocumentMetadataEdit { + editType: CellEditType.DocumentMetadata; + metadata: NotebookDocumentMetadata; +} + +export interface CellMoveEdit { + editType: CellEditType.Move; + index: number; + length: number; + newIdx: number; +} + +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 = CellOutputEditByHandle | CellOutputItemEdit | CellPartialInternalMetadataEditByHandle; // add more later on +export type CellEditOperation = ImmediateCellEditOperation | CellReplaceEdit | CellOutputEdit | + CellMetadataEdit | CellLanguageEdit | DocumentMetadataEdit | CellMoveEdit; // add more later on + +export type NullablePartialNotebookCellInternalMetadata = { + [Key in keyof Partial]: NotebookCellInternalMetadata[Key] | null +}; +export interface CellPartialInternalMetadataEditByHandle { + editType: CellEditType.PartialInternalMetadata; + handle: number; + 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`. + */ +export function isTextStreamMime(mimeType: string): boolean { + return ['application/vnd.code.notebook.stdout', 'application/vnd.code.notebook.stderr'].includes(mimeType); +} + +export namespace CellUri { + + export const scheme = 'vscode-notebook-cell'; + + const _lengths = ['W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f']; + const _padRegexp = new RegExp(`^[${_lengths.join('')}]+`); + const _radix = 7; + + export function generate(notebook: URI, handle: number): URI { + + const s = handle.toString(_radix); + const p = s.length < _lengths.length ? _lengths[s.length - 1] : 'z'; + + const fragment = `${p}${s}s${Buffer.from(BinaryBuffer.fromString(notebook.scheme).buffer).toString('base64')} `; + return notebook.withScheme(scheme).withFragment(fragment); + } + + export function parse(cell: URI): { notebook: URI; handle: number } | undefined { + if (cell.scheme !== scheme) { + return undefined; + } + + const idx = cell.fragment.indexOf('s'); + if (idx < 0) { + return undefined; + } + + const handle = parseInt(cell.fragment.substring(0, idx).replace(_padRegexp, ''), _radix); + const parsedScheme = Buffer.from(cell.fragment.substring(idx + 1), 'base64').toString(); + + if (isNaN(handle)) { + return undefined; + } + return { + handle, + notebook: cell.withScheme(parsedScheme).withoutFragment() + }; + } + + export function generateCellPropertyUri(notebook: URI, handle: number, cellScheme: string): URI { + return CellUri.generate(notebook, handle).withScheme(cellScheme); + } + + export function parseCellPropertyUri(uri: URI, propertyScheme: string): { notebook: URI; handle: number } | undefined { + if (uri.scheme !== propertyScheme) { + return undefined; + } + + return CellUri.parse(uri.withScheme(scheme)); + } +} diff --git a/packages/notebook/src/common/notebook-protocol.ts b/packages/notebook/src/common/notebook-protocol.ts new file mode 100644 index 0000000000000..4b306a395796d --- /dev/null +++ b/packages/notebook/src/common/notebook-protocol.ts @@ -0,0 +1,35 @@ +// ***************************************************************************** +// 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 +// ***************************************************************************** + +export interface NotebookTypeDescriptor { + readonly type: string; + readonly displayName: string; + readonly priority?: string | undefined; + readonly selector?: readonly NotebookFileSelector[]; +} + +export interface NotebookFileSelector { + readonly filenamePattern?: string; + readonly excludeFileNamePattern?: string; +} + +export interface NotebookRendererDescriptor { + readonly id: string; + readonly displayName: string; + readonly mimeTypes: string[]; + readonly entrypoint: string | { readonly extends: string; readonly path: string }; + readonly requiresMessaging?: 'always' | 'optional' | 'never' +} diff --git a/packages/notebook/src/common/notebook-range.ts b/packages/notebook/src/common/notebook-range.ts new file mode 100644 index 0000000000000..4a9f5b3d75407 --- /dev/null +++ b/packages/notebook/src/common/notebook-range.ts @@ -0,0 +1,30 @@ +// ***************************************************************************** +// 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 +// ***************************************************************************** + +/** + * [start, end] + */ +export interface CellRange { + /** + * zero based index + */ + start: number; + + /** + * zero based index + */ + end: number; +} diff --git a/packages/notebook/tsconfig.json b/packages/notebook/tsconfig.json new file mode 100644 index 0000000000000..8f53c0fe2dd53 --- /dev/null +++ b/packages/notebook/tsconfig.json @@ -0,0 +1,25 @@ +{ + "extends": "../../configs/base.tsconfig", + "compilerOptions": { + "composite": true, + "rootDir": "src", + "outDir": "lib" + }, + "include": [ + "src" + ], + "references": [ + { + "path": "../core" + }, + { + "path": "../editor" + }, + { + "path": "../filesystem" + }, + { + "path": "../monaco" + } + ] +} diff --git a/packages/plugin-ext/package.json b/packages/plugin-ext/package.json index c34ce9a3abdc2..80a9ee31fa524 100644 --- a/packages/plugin-ext/package.json +++ b/packages/plugin-ext/package.json @@ -19,6 +19,7 @@ "@theia/monaco": "1.40.0", "@theia/monaco-editor-core": "1.72.3", "@theia/navigator": "1.40.0", + "@theia/notebook": "1.40.0", "@theia/output": "1.40.0", "@theia/plugin": "1.40.0", "@theia/preferences": "1.40.0", @@ -33,6 +34,7 @@ "@types/mime": "^2.0.1", "@vscode/debugprotocol": "^1.51.0", "@vscode/proxy-agent": "^0.13.2", + "async-mutex": "^0.4.0", "decompress": "^4.2.1", "escape-html": "^1.0.3", "filenamify": "^4.1.0", 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 2857dbb4d106f..078f03a98181f 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 { @@ -117,6 +118,8 @@ import { Disposable } from '@theia/core/lib/common/disposable'; import { isString, isObject, PickOptions, QuickInputButtonHandle } from '@theia/core/lib/common'; 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 { CellExecutionUpdateType, CellRange, NotebookCellExecutionState } from '@theia/notebook/lib/common'; import { LanguagePackBundle } from './language-pack-service'; export interface PreferenceData { @@ -1272,6 +1275,7 @@ export interface ModelAddedData { uri: UriComponents; versionId: number; lines: string[]; + languageId?: string; EOL: string; modeId: string; isDirty: boolean; @@ -1472,8 +1476,37 @@ export interface WorkspaceEditMetadataDto { isRefactoring?: boolean; } +export type CellEditOperationDto = + { + editType: notebookCommon.CellEditType.Metadata; + index: number; + metadata: Record; + } | + { + editType: notebookCommon.CellEditType.DocumentMetadata; + metadata: Record; + } | + { + editType: notebookCommon.CellEditType.Replace; + index: number; + count: number; + cells: NotebookCellDataDto[]; + }; + +export interface NotebookWorkspaceEditMetadataDto { + needsConfirmation: boolean; + label: string; + description?: string; +} + +export interface WorkspaceNotebookCellEditDto { + metadata?: NotebookWorkspaceEditMetadataDto; + resource: UriComponents; + cellEdit: CellEditOperationDto; +} + export interface WorkspaceEditDto { - edits: Array; + edits: Array; } export interface CommandProperties { @@ -2107,6 +2140,12 @@ export const PLUGIN_RPC_CONTEXT = { MESSAGE_REGISTRY_MAIN: >createProxyIdentifier('MessageRegistryMain'), TEXT_EDITORS_MAIN: createProxyIdentifier('TextEditorsMain'), DOCUMENTS_MAIN: createProxyIdentifier('DocumentsMain'), + 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'), ENV_MAIN: createProxyIdentifier('EnvMain'), NOTIFICATION_MAIN: createProxyIdentifier('NotificationMain'), @@ -2148,6 +2187,11 @@ export const MAIN_RPC_CONTEXT = { TEXT_EDITORS_EXT: createProxyIdentifier('TextEditorsExt'), EDITORS_AND_DOCUMENTS_EXT: createProxyIdentifier('EditorsAndDocumentsExt'), DOCUMENTS_EXT: createProxyIdentifier('DocumentsExt'), + NOTEBOOKS_EXT: createProxyIdentifier('NotebooksExt'), + NOTEBOOK_DOCUMENTS_EXT: createProxyIdentifier('NotebookDocumentsExt'), + NOTEBOOK_EDITORS_EXT: createProxyIdentifier('NotebookEditorsExt'), + NOTEBOOK_RENDERERS_EXT: createProxyIdentifier('NotebooksExt'), + NOTEBOOK_KERNELS_EXT: createProxyIdentifier('NotebookKernelsExt'), TERMINAL_EXT: createProxyIdentifier('TerminalServiceExt'), OUTPUT_CHANNEL_REGISTRY_EXT: createProxyIdentifier('OutputChannelRegistryExt'), TREE_VIEWS_EXT: createProxyIdentifier('TreeViewsExt'), @@ -2208,6 +2252,286 @@ export interface AuthenticationMain { options: theia.AuthenticationGetSessionOptions): Promise; } +export interface NotebookOutputItemDto { + readonly mime: string; + readonly valueBytes: BinaryBuffer; +} + +export interface NotebookOutputDto { + outputId: string; + items: NotebookOutputItemDto[]; + metadata?: Record; +} + +export interface NotebookCellDataDto { + source: string; + language: string; + cellKind: notebookCommon.CellKind; + outputs: NotebookOutputDto[]; + metadata?: notebookCommon.NotebookCellMetadata; + internalMetadata?: notebookCommon.NotebookCellInternalMetadata; +} + +export interface NotebookDataDto { + readonly cells: NotebookCellDataDto[]; + readonly metadata: notebookCommon.NotebookDocumentMetadata; +} + +export interface NotebookCellDto { + handle: number; + uri: UriComponents; + eol: string; + source: string[]; + language: string; + mime?: string; + cellKind: notebookCommon.CellKind; + outputs: NotebookOutputDto[]; + metadata?: notebookCommon.NotebookCellMetadata; + internalMetadata?: notebookCommon.NotebookCellInternalMetadata; +} + +export interface NotebookModelAddedData { + uri: UriComponents; + versionId: number; + cells: NotebookCellDto[]; + viewType: string; + metadata?: notebookCommon.NotebookDocumentMetadata; +} + +export interface NotebookEditorAddData { + id: string; + documentUri: UriComponents; + selections: CellRange[]; + visibleRanges: CellRange[]; + viewColumn?: number; +} + +export interface NotebookDocumentsAndEditorsDelta { + removedDocuments?: UriComponents[]; + addedDocuments?: NotebookModelAddedData[]; + removedEditors?: string[]; + addedEditors?: NotebookEditorAddData[]; + newActiveEditor?: string | null; + visibleEditors?: string[]; +} + +export type NotebookCellStatusBarEntryDto = notebookCommon.NotebookCellStatusBarItem; + +export interface NotebookCellStatusBarListDto { + items: NotebookCellStatusBarEntryDto[]; + cacheId: number; +} + +export type NotebookRawContentEventDto = + // notebookCommon.NotebookCellsInitializeEvent + | { + + readonly kind: notebookCommon.NotebookCellsChangeType.ModelChange; + readonly changes: notebookCommon.NotebookCellTextModelSplice[]; + } + | { + readonly kind: notebookCommon.NotebookCellsChangeType.Move; + readonly index: number; + readonly length: number; + readonly newIdx: number; + } + | { + readonly kind: notebookCommon.NotebookCellsChangeType.Output; + readonly index: number; + readonly outputs: NotebookOutputDto[]; + } + | { + readonly kind: notebookCommon.NotebookCellsChangeType.OutputItem; + readonly index: number; + readonly outputId: string; + readonly outputItems: NotebookOutputItemDto[]; + readonly append: boolean; + } + | notebookCommon.NotebookCellsChangeLanguageEvent + | notebookCommon.NotebookCellsChangeMetadataEvent + | notebookCommon.NotebookCellsChangeInternalMetadataEvent + | notebookCommon.NotebookCellContentChangeEvent + ; + +export interface NotebookCellsChangedEventDto { + readonly rawEvents: NotebookRawContentEventDto[]; + readonly versionId: number; +}; + +export interface NotebookSelectionChangeEvent { + selections: CellRange[]; +} + +export interface NotebookVisibleRangesEvent { + ranges: CellRange[]; +} + +export interface NotebookEditorPropertiesChangeData { + visibleRanges?: NotebookVisibleRangesEvent; + selections?: NotebookSelectionChangeEvent; +} + +export enum NotebookEditorRevealType { + Default = 0, + InCenter = 1, + InCenterIfOutsideViewport = 2, + AtTop = 3 +} + +export interface NotebookDocumentShowOptions { + position?: EditorGroupColumn; + preserveFocus?: boolean; + pinned?: boolean; + selections?: CellRange[]; +} + +export interface NotebookKernelDto { + id: string; + notebookType: string; + extensionId: string; + // extensionLocation: UriComponents; + label: string; + detail?: string; + description?: string; + supportedLanguages?: string[]; + supportsInterrupt?: boolean; + supportsExecutionOrder?: boolean; + preloads?: { uri: UriComponents; provides: readonly string[] }[]; + rendererScripts?: NotebookRendererScript[]; +} + +export type CellExecuteUpdateDto = CellExecuteOutputEditDto | CellExecuteOutputItemEditDto | CellExecutionStateUpdateDto; + +export interface CellExecuteOutputEditDto { + editType: CellExecutionUpdateType.Output; + cellHandle: number; + append?: boolean; + outputs: NotebookOutputDto[]; +} + +export interface CellExecuteOutputItemEditDto { + editType: CellExecutionUpdateType.OutputItems; + append?: boolean; + items: NotebookOutputItemDto[]; +} + +export interface CellExecutionStateUpdateDto { + editType: CellExecutionUpdateType.ExecutionState; + executionOrder?: number; + runStartTime?: number; + didPause?: boolean; + isPaused?: boolean; +} + +export interface CellExecutionCompleteDto { + runEndTime?: number; + lastRunSuccess?: boolean; +} + +export interface NotebookKernelSourceActionDto { + readonly label: string; + readonly description?: string; + readonly detail?: string; + readonly command?: string | Command; + 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; + + $dataToNotebook(handle: number, data: BinaryBuffer, token: CancellationToken): Promise; + $notebookToData(handle: number, data: NotebookDataDto, token: CancellationToken): Promise; +} + +export interface NotebooksMain extends Disposable { + $registerNotebookSerializer(handle: number, extension: notebookCommon.NotebookExtensionDescription, viewType: string, options: notebookCommon.TransientOptions): void; + $unregisterNotebookSerializer(handle: number): void; + + $registerNotebookCellStatusBarItemProvider(handle: number, eventHandle: number | undefined, viewType: string): Promise; + $unregisterNotebookCellStatusBarItemProvider(handle: number, eventHandle: number | undefined): Promise; + $emitCellStatusBarEvent(eventHandle: number): void; +} + +export interface NotebookKernelsExt { + $acceptNotebookAssociation(handle: number, uri: UriComponents, value: boolean): void; + $executeCells(handle: number, uri: UriComponents, handles: number[]): Promise; + $cancelCells(handle: number, uri: UriComponents, handles: number[]): Promise; + $acceptKernelMessageFromRenderer(handle: number, editorId: string, message: any): void; + $cellExecutionChanged(uri: UriComponents, cellHandle: number, state: NotebookCellExecutionState | undefined): void; + $provideKernelSourceActions(handle: number, token: CancellationToken): Promise; +} + +export interface NotebookKernelsMain extends Disposable { + $postMessage(handle: number, editorId: string | undefined, message: any): Promise; + $addKernel(handle: number, data: NotebookKernelDto): Promise; + $updateKernel(handle: number, data: Partial): void; + $removeKernel(handle: number): void; + $updateNotebookPriority(handle: number, uri: UriComponents, value: number | undefined): void; + + $createExecution(handle: number, controllerId: string, uri: UriComponents, cellHandle: number): void; + $updateExecution(handle: number, data: CellExecuteUpdateDto[]): void; + $completeExecution(handle: number, data: CellExecutionCompleteDto): void; + + $createNotebookExecution(handle: number, controllerId: string, uri: UriComponents): void; + $beginNotebookExecution(handle: number): void; + $completeNotebookExecution(handle: number): void; + + $addKernelDetectionTask(handle: number, notebookType: string): Promise; + $removeKernelDetectionTask(handle: number): void; + + $addKernelSourceActionProvider(handle: number, eventHandle: number, notebookType: string): Promise; + $removeKernelSourceActionProvider(handle: number, eventHandle: number): void; + $emitNotebookKernelSourceActionsChangeEvent(eventHandle: number): void; +} + +export interface NotebookDocumentsMain extends Disposable { + $tryCreateNotebook(options: { viewType: string; content?: NotebookDataDto }): Promise; + $tryOpenNotebook(uriComponents: UriComponents): Promise; + $trySaveNotebook(uri: UriComponents): Promise; +} + +export interface NotebookDocumentsExt { + $acceptModelChanged(uriComponents: UriComponents, event: NotebookCellsChangedEventDto, isDirty: boolean, newMetadata?: notebookCommon.NotebookDocumentMetadata): void; + $acceptDirtyStateChanged(uriComponents: UriComponents, isDirty: boolean): void; + $acceptModelSaved(uriComponents: UriComponents): void; +} + +export interface NotebookDocumentsAndEditorsExt { + $acceptDocumentsAndEditorsDelta(delta: NotebookDocumentsAndEditorsDelta): Promise; +} + +export interface NotebookDocumentsAndEditorsMain extends Disposable { +} + +export type NotebookEditorViewColumnInfo = Record; + +export interface NotebookEditorsExt { + $acceptEditorPropertiesChanged(id: string, data: NotebookEditorPropertiesChangeData): void; + $acceptEditorViewColumns(data: NotebookEditorViewColumnInfo): void; +} + +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; +} +export interface NotebookRenderersExt { + $postRendererMessage(editorId: string, rendererId: string, message: unknown): void; +} + +export interface NotebookRenderersMain extends Disposable { + $postMessage(editorId: string | undefined, rendererId: string, message: unknown): Promise; +} + export interface RawColorInfo { color: [number, number, number, number]; range: Range; diff --git a/packages/plugin-ext/src/common/plugin-protocol.ts b/packages/plugin-ext/src/common/plugin-protocol.ts index ca1900bc8a6f7..a662207022d1e 100644 --- a/packages/plugin-ext/src/common/plugin-protocol.ts +++ b/packages/plugin-ext/src/common/plugin-protocol.ts @@ -99,6 +99,7 @@ export interface PluginPackageContribution { localizations?: PluginPackageLocalization[]; terminal?: PluginPackageTerminal; notebooks?: PluginPackageNotebook[]; + notebookRenderer?: PluginNotebookRendererContribution[]; } export interface PluginPackageNotebook { @@ -108,6 +109,14 @@ export interface PluginPackageNotebook { priority?: string; } +export interface PluginNotebookRendererContribution { + readonly id: string; + readonly displayName: string; + readonly mimeTypes: string[]; + readonly entrypoint: string | { readonly extends: string; readonly path: string }; + readonly requiresMessaging?: 'always' | 'optional' | 'never' +} + export interface PluginPackageAuthenticationProvider { id: string; label: string; @@ -585,6 +594,7 @@ export interface PluginContribution { localizations?: Localization[]; terminalProfiles?: TerminalProfile[]; notebooks?: NotebookContribution[]; + notebookRenderer?: NotebookRendererContribution[]; } export interface NotebookContribution { @@ -594,6 +604,14 @@ export interface NotebookContribution { priority?: string; } +export interface NotebookRendererContribution { + readonly id: string; + readonly displayName: string; + readonly mimeTypes: string[]; + readonly entrypoint: string | { readonly extends: string; readonly path: string }; + readonly requiresMessaging?: 'always' | 'optional' | 'never' +} + export interface AuthenticationProviderInformation { id: string; label: string; diff --git a/packages/plugin-ext/src/hosted/browser/hosted-plugin.ts b/packages/plugin-ext/src/hosted/browser/hosted-plugin.ts index df424168e1531..2e6b929c1f75a 100644 --- a/packages/plugin-ext/src/hosted/browser/hosted-plugin.ts +++ b/packages/plugin-ext/src/hosted/browser/hosted-plugin.ts @@ -69,6 +69,7 @@ import { LanguageService } from '@theia/monaco-editor-core/esm/vs/editor/common/ import { Measurement, Stopwatch } from '@theia/core/lib/common'; import { Uint8ArrayReadBuffer, Uint8ArrayWriteBuffer } from '@theia/core/lib/common/message-rpc/uint8-array-message-buffer'; import { BasicChannel } from '@theia/core/lib/common/message-rpc/channel'; +import { NotebookTypeRegistry, NotebookService } from '@theia/notebook/lib/browser'; export type PluginHost = 'frontend' | string; export type DebugActivationEvent = 'onDebugResolve' | 'onDebugInitialConfigurations' | 'onDebugAdapterProtocolTracker' | 'onDebugDynamicConfigurations'; @@ -114,6 +115,9 @@ export class HostedPluginSupport { @inject(WorkspaceService) protected readonly workspaceService: WorkspaceService; + @inject(NotebookService) + protected readonly notebookService: NotebookService; + @inject(CommandRegistry) protected readonly commands: CommandRegistry; @@ -132,6 +136,9 @@ export class HostedPluginSupport { @inject(FrontendApplicationStateService) protected readonly appState: FrontendApplicationStateService; + @inject(NotebookTypeRegistry) + protected readonly notebookTypeRegistry: NotebookTypeRegistry; + @inject(PluginViewRegistry) protected readonly viewRegistry: PluginViewRegistry; @@ -216,6 +223,7 @@ export class HostedPluginSupport { this.taskResolverRegistry.onWillProvideTaskResolver(event => this.ensureTaskActivation(event)); this.fileService.onWillActivateFileSystemProvider(event => this.ensureFileSystemActivation(event)); this.customEditorRegistry.onWillOpenCustomEditor(event => this.activateByCustomEditor(event)); + this.notebookService.onWillOpenNotebook(async event => this.activateByNotebook(event)); this.widgets.onDidCreateWidget(({ factoryId, widget }) => { if ((factoryId === WebviewWidget.FACTORY_ID || factoryId === CustomEditorWidget.FACTORY_ID) && widget instanceof WebviewWidget) { @@ -623,6 +631,10 @@ export class HostedPluginSupport { await this.activateByEvent(`onCustomEditor:${viewType}`); } + async activateByNotebook(viewType: string): Promise { + await this.activateByEvent(`onNotebook:${viewType}`); + } + activateByFileSystem(event: FileSystemProviderActivationEvent): Promise { return this.activateByEvent(`onFileSystem:${event.scheme}`); } diff --git a/packages/plugin-ext/src/hosted/node/scanners/scanner-theia.ts b/packages/plugin-ext/src/hosted/node/scanners/scanner-theia.ts index d1b2c24f29115..0624526dff6fb 100644 --- a/packages/plugin-ext/src/hosted/node/scanners/scanner-theia.ts +++ b/packages/plugin-ext/src/hosted/node/scanners/scanner-theia.ts @@ -313,6 +313,12 @@ export class TheiaPluginScanner implements PluginScanner { console.error(`Could not read '${rawPlugin.name}' contribution 'notebooks'.`, rawPlugin.contributes.authentication, err); } + try { + contributions.notebookRenderer = rawPlugin.contributes.notebookRenderer; + } catch (err) { + console.error(`Could not read '${rawPlugin.name}' contribution 'notebooks'.`, rawPlugin.contributes.authentication, err); + } + try { contributions.snippets = this.readSnippets(rawPlugin); } catch (err) { diff --git a/packages/plugin-ext/src/main/browser/custom-editors/custom-editor-widget.ts b/packages/plugin-ext/src/main/browser/custom-editors/custom-editor-widget.ts index 5ca290804bf0a..cee4ee0fc9416 100644 --- a/packages/plugin-ext/src/main/browser/custom-editors/custom-editor-widget.ts +++ b/packages/plugin-ext/src/main/browser/custom-editors/custom-editor-widget.ts @@ -21,7 +21,7 @@ import { ApplicationShell, NavigatableWidget, Saveable, SaveableSource, SaveOpti import { SaveResourceService } from '@theia/core/lib/browser/save-resource-service'; import { Reference } from '@theia/core/lib/common/reference'; import { WebviewWidget } from '../webview/webview'; -import { UndoRedoService } from './undo-redo-service'; +import { UndoRedoService } from '@theia/editor/lib/browser/undo-redo-service'; import { CustomEditorModel } from './custom-editors-main'; @injectable() diff --git a/packages/plugin-ext/src/main/browser/custom-editors/custom-editors-main.ts b/packages/plugin-ext/src/main/browser/custom-editors/custom-editors-main.ts index 16dfe775d7bd2..ba9a5850654ed 100644 --- a/packages/plugin-ext/src/main/browser/custom-editors/custom-editors-main.ts +++ b/packages/plugin-ext/src/main/browser/custom-editors/custom-editors-main.ts @@ -36,7 +36,7 @@ import { MonacoEditorModel } from '@theia/monaco/lib/browser/monaco-editor-model import { EditorModelService } from '../text-editor-model-service'; import { CustomEditorService } from './custom-editor-service'; import { FileService } from '@theia/filesystem/lib/browser/file-service'; -import { UndoRedoService } from './undo-redo-service'; +import { UndoRedoService } from '@theia/editor/lib/browser/undo-redo-service'; import { WebviewsMainImpl } from '../webviews-main'; import { WidgetManager } from '@theia/core/lib/browser/widget-manager'; import { ApplicationShell, DefaultUriLabelProviderContribution, Saveable, SaveOptions, WidgetOpenerOptions } from '@theia/core/lib/browser'; diff --git a/packages/plugin-ext/src/main/browser/editors-and-documents-main.ts b/packages/plugin-ext/src/main/browser/editors-and-documents-main.ts index c4875fca5ea23..125cfad683fa6 100644 --- a/packages/plugin-ext/src/main/browser/editors-and-documents-main.ts +++ b/packages/plugin-ext/src/main/browser/editors-and-documents-main.ts @@ -139,6 +139,7 @@ export class EditorsAndDocumentsMain implements Disposable { uri: model.textEditorModel.uri, versionId: model.textEditorModel.getVersionId(), lines: model.textEditorModel.getLinesContent(), + languageId: model.getLanguageId(), EOL: model.textEditorModel.getEOL(), modeId: model.languageId, isDirty: model.dirty diff --git a/packages/plugin-ext/src/main/browser/languages-main.ts b/packages/plugin-ext/src/main/browser/languages-main.ts index 6f348a7cf2570..0b9e9120fd9ce 100644 --- a/packages/plugin-ext/src/main/browser/languages-main.ts +++ b/packages/plugin-ext/src/main/browser/languages-main.ts @@ -32,6 +32,7 @@ import { LanguagesExt, WorkspaceEditDto, WorkspaceTextEditDto, + WorkspaceFileEditDto, PluginInfo, LanguageStatus as LanguageStatusDTO, InlayHintDto, @@ -1418,13 +1419,15 @@ export function toMonacoWorkspaceEdit(data: WorkspaceEditDto | undefined): monac metadata: edit.metadata }; } else { + const fileEdit = edit as WorkspaceFileEditDto; return { - newResource: monaco.Uri.revive(edit.newResource), - oldResource: monaco.Uri.revive(edit.oldResource), - options: edit.options, - metadata: edit.metadata + newResource: monaco.Uri.revive(fileEdit.newResource), + oldResource: monaco.Uri.revive(fileEdit.oldResource), + options: fileEdit.options, + metadata: fileEdit.metadata }; } + // TODO implement WorkspaceNotebookCellEditDto }) }; } diff --git a/packages/plugin-ext/src/main/browser/main-context.ts b/packages/plugin-ext/src/main/browser/main-context.ts index 7b05139454b25..6ea25d5c0e2d6 100644 --- a/packages/plugin-ext/src/main/browser/main-context.ts +++ b/packages/plugin-ext/src/main/browser/main-context.ts @@ -59,7 +59,15 @@ import { MonacoLanguages } from '@theia/monaco/lib/browser/monaco-languages'; import { UntitledResourceResolver } from '@theia/core/lib/common/resource'; import { ThemeService } from '@theia/core/lib/browser/theming'; import { TabsMainImpl } from './tabs/tabs-main'; +import { NotebooksMainImpl } from './notebooks/notebooks-main'; +import { NotebookService } from '@theia/notebook/lib/browser'; import { LocalizationMainImpl } from './localization-main'; +import { NotebookRenderersMainImpl } from './notebooks/notebook-renderers-main'; +import { HostedPluginSupport } from '../../hosted/browser/hosted-plugin'; +import { NotebookEditorsMainImpl } from './notebooks/notebook-editors-main'; +import { NotebookDocumentsMainImpl } from './notebooks/notebook-documents-main'; +import { NotebookKernelsMainImpl } from './notebooks/notebook-kernels-main'; +import { NotebooksAndEditorsMain } from './notebooks/notebook-documents-and-editors-main'; export function setUpPluginApi(rpc: RPCProtocol, container: interfaces.Container): void { const authenticationMain = new AuthenticationMainImpl(rpc, container); @@ -94,6 +102,17 @@ export function setUpPluginApi(rpc: RPCProtocol, container: interfaces.Container const documentsMain = new DocumentsMainImpl(editorsAndDocuments, modelService, rpc, editorManager, openerService, shell, untitledResourceResolver, languageService); rpc.set(PLUGIN_RPC_CONTEXT.DOCUMENTS_MAIN, documentsMain); + const notebookService = container.get(NotebookService); + const pluginSupport = container.get(HostedPluginSupport); + rpc.set(PLUGIN_RPC_CONTEXT.NOTEBOOKS_MAIN, new NotebooksMainImpl(rpc, notebookService, pluginSupport)); + rpc.set(PLUGIN_RPC_CONTEXT.NOTEBOOK_RENDERERS_MAIN, new NotebookRenderersMainImpl(rpc, container)); + const notebookEditorsMain = new NotebookEditorsMainImpl(rpc, container); + 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); const editorsMain = new TextEditorsMainImpl(editorsAndDocuments, documentsMain, rpc, bulkEditService, 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..3e20545e201aa --- /dev/null +++ b/packages/plugin-ext/src/main/browser/notebooks/notebook-documents-and-editors-main.ts @@ -0,0 +1,238 @@ +// ***************************************************************************** +// 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 './notebook-dto'; +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'; +import { Mutex } from 'async-mutex'; + +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 updateMutex = new Mutex(); + + 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.onDidAddNotebookDocument(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(async editor => this.handleEditorAdd(editor), this, this.disposables); + this.notebookEditorService.onDidRemoveNotebookEditor(async editor => this.handleEditorRemove(editor), this, this.disposables); + this.notebookEditorService.onFocusedEditorChanged(async editor => this.updateState(editor), this, this.disposables); + } + + dispose(): void { + this.notebookDocumentsMain.dispose(); + this.notebookEditorsMain.dispose(); + this.disposables.dispose(); + this.editorListeners.forEach(listeners => listeners.forEach(listener => listener.dispose())); + } + + private async handleEditorAdd(editor: NotebookEditorWidget): Promise { + const listeners = this.editorListeners.get(editor.id); + const disposable = editor.onDidChangeModel(() => this.updateState()); + if (listeners) { + listeners.push(disposable); + } else { + this.editorListeners.set(editor.id, [disposable]); + } + await 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 { + await this.updateMutex.runExclusive(async () => this.doUpdateState(focusedEditor)); + } + + private async doUpdateState(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.metadata, + versionId: 1, // TODO implement versionID support + cells: e.cells.map(NotebookDto.toNotebookCellDto) + }; + } +} 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 new file mode 100644 index 0000000000000..16012ddc0798a --- /dev/null +++ b/packages/plugin-ext/src/main/browser/notebooks/notebook-documents-main.ts @@ -0,0 +1,166 @@ +// ***************************************************************************** +// 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 { DisposableCollection } from '@theia/core'; +import { URI, UriComponents } from '@theia/core/lib/common/uri'; +import { interfaces } from '@theia/core/shared/inversify'; +import { NotebookModelResolverService } from '@theia/notebook/lib/browser'; +import { NotebookModel } from '@theia/notebook/lib/browser/view-model/notebook-model'; +import { NotebookCellsChangeType } from '@theia/notebook/lib/common'; +import { MAIN_RPC_CONTEXT, NotebookCellDto, NotebookCellsChangedEventDto, NotebookDataDto, NotebookDocumentsExt, NotebookDocumentsMain } from '../../../common'; +import { RPCProtocol } from '../../../common/rpc-protocol'; +import { NotebookDto } from './notebook-dto'; + +export class NotebookDocumentsMainImpl implements NotebookDocumentsMain { + + private readonly disposables = new DisposableCollection(); + + private readonly proxy: NotebookDocumentsExt; + private readonly documentEventListenersMapping = new Map(); + + private readonly notebookModelResolverService: NotebookModelResolverService; + + constructor( + rpc: RPCProtocol, + container: interfaces.Container + ) { + this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.NOTEBOOK_DOCUMENTS_EXT); + this.notebookModelResolverService = container.get(NotebookModelResolverService); + + // forward dirty and save events + this.disposables.push(this.notebookModelResolverService.onDidChangeDirty(model => this.proxy.$acceptDirtyStateChanged(model.uri.toComponents(), model.isDirty()))); + this.disposables.push(this.notebookModelResolverService.onDidSaveNotebook(e => this.proxy.$acceptModelSaved(e))); + + } + + dispose(): void { + this.disposables.dispose(); + // this.modelReferenceCollection.dispose(); + this.documentEventListenersMapping.forEach(value => value.dispose()); + } + + 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].map(NotebookDto.toNotebookCellDto)] 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.toComponents(), + eventDto, + textModel.isDirty(), + hasDocumentMetadataChangeEvent ? textModel.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); + + // untitled notebooks are disposed when they get saved. we should not hold a reference + // to such a disposed notebook and therefore dispose the reference as well + // ref.onWillDispose(() => { + // ref.dispose(); + // }); + + // untitled notebooks are dirty by default + this.proxy.$acceptDirtyStateChanged(ref.uri.toComponents(), true); + + // apply content changes... slightly HACKY -> this triggers a change event + // if (options.content) { + // const data = NotebookDto.fromNotebookDataDto(options.content); + // ref.notebook.reset(data.cells, data.metadata, ref.object.notebook.transientOptions); + // } + return ref.uri.toComponents(); + } + + async $tryOpenNotebook(uriComponents: UriComponents): Promise { + const uri = URI.fromComponents(uriComponents); + return uri.toComponents(); + } + + async $trySaveNotebook(uriComponents: UriComponents): Promise { + const uri = URI.fromComponents(uriComponents); + + const ref = await this.notebookModelResolverService.resolve(uri); + await ref.save({}); + ref.dispose(); + return true; + } +} diff --git a/packages/plugin-ext/src/main/browser/notebooks/notebook-dto.ts b/packages/plugin-ext/src/main/browser/notebooks/notebook-dto.ts new file mode 100644 index 0000000000000..3b3f0c0014e61 --- /dev/null +++ b/packages/plugin-ext/src/main/browser/notebooks/notebook-dto.ts @@ -0,0 +1,141 @@ +// ***************************************************************************** +// 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 { OS } from '@theia/core'; +import * as notebookCommon from '@theia/notebook/lib/common'; +import { NotebookCellModel } from '@theia/notebook/lib/browser/view-model/notebook-cell-model'; +import * as rpc from '../../../common'; + +export namespace NotebookDto { + + export function toNotebookOutputItemDto(item: notebookCommon.CellOutputItem): rpc.NotebookOutputItemDto { + return { + mime: item.mime, + valueBytes: item.data + }; + } + + export function toNotebookOutputDto(output: notebookCommon.CellOutput): rpc.NotebookOutputDto { + return { + outputId: output.outputId, + metadata: output.metadata, + items: output.outputs.map(toNotebookOutputItemDto) + }; + } + + export function toNotebookCellDataDto(cell: notebookCommon.CellData): rpc.NotebookCellDataDto { + return { + cellKind: cell.cellKind, + language: cell.language, + source: cell.source, + internalMetadata: cell.internalMetadata, + metadata: cell.metadata, + outputs: cell.outputs.map(toNotebookOutputDto) + }; + } + + export function toNotebookDataDto(data: notebookCommon.NotebookData): rpc.NotebookDataDto { + return { + metadata: data.metadata, + cells: data.cells.map(toNotebookCellDataDto) + }; + } + + export function fromNotebookOutputItemDto(item: rpc.NotebookOutputItemDto): notebookCommon.CellOutputItem { + return { + mime: item.mime, + data: item.valueBytes + }; + } + + export function fromNotebookOutputDto(output: rpc.NotebookOutputDto): notebookCommon.CellOutput { + return { + outputId: output.outputId, + metadata: output.metadata, + outputs: output.items.map(fromNotebookOutputItemDto) + }; + } + + export function fromNotebookCellDataDto(cell: rpc.NotebookCellDataDto): notebookCommon.CellData { + return { + cellKind: cell.cellKind, + language: cell.language, + source: cell.source, + outputs: cell.outputs.map(fromNotebookOutputDto), + metadata: cell.metadata, + internalMetadata: cell.internalMetadata + }; + } + + export function fromNotebookDataDto(data: rpc.NotebookDataDto): notebookCommon.NotebookData { + return { + metadata: data.metadata, + cells: data.cells.map(fromNotebookCellDataDto) + }; + } + + export function toNotebookCellDto(cell: NotebookCellModel): rpc.NotebookCellDto { + const eol = OS.backend.isWindows ? '\r\n' : '\n'; + return { + handle: cell.handle, + uri: cell.uri.toComponents(), + source: cell.textBuffer.split(eol), + eol, + language: cell.language, + cellKind: cell.cellKind, + outputs: cell.outputs.map(toNotebookOutputDto), + metadata: cell.metadata, + internalMetadata: cell.internalMetadata, + }; + } + + // export function fromCellExecuteUpdateDto(data: extHostProtocol.ICellExecuteUpdateDto): ICellExecuteUpdate { + // if (data.editType === CellExecutionUpdateType.Output) { + // return { + // editType: data.editType, + // cellHandle: data.cellHandle, + // append: data.append, + // outputs: data.outputs.map(fromNotebookOutputDto) + // }; + // } else if (data.editType === CellExecutionUpdateType.OutputItems) { + // return { + // editType: data.editType, + // append: data.append, + // outputId: data.outputId, + // items: data.items.map(fromNotebookOutputItemDto) + // }; + // } else { + // return data; + // } + // } + + // export function fromCellExecuteCompleteDto(data: extHostProtocol.ICellExecutionCompleteDto): ICellExecutionComplete { + // return data; + // } + + // export function fromCellEditOperationDto(edit: extHostProtocol.ICellEditOperationDto): notebookCommon.ICellEditOperation { + // if (edit.editType === notebookCommon.CellEditType.Replace) { + // return { + // editType: edit.editType, + // index: edit.index, + // count: edit.count, + // cells: edit.cells.map(fromNotebookCellDataDto) + // }; + // } else { + // return edit; + // } + // } +} 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 new file mode 100644 index 0000000000000..3a9a50208a6b9 --- /dev/null +++ b/packages/plugin-ext/src/main/browser/notebooks/notebook-editors-main.ts @@ -0,0 +1,70 @@ +// ***************************************************************************** +// 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 { UriComponents, URI } 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'; +import { interfaces } from '@theia/core/shared/inversify'; +import { open, OpenerService } from '@theia/core/lib/browser'; + +export class NotebookEditorsMainImpl implements NotebookEditorsMain { + + protected readonly proxy: NotebookEditorsExt; + protected readonly openerService: OpenerService; + + protected readonly mainThreadEditors = new Map(); + + constructor( + rpc: RPCProtocol, + container: interfaces.Container + ) { + this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.NOTEBOOK_EDITORS_EXT); + this.openerService = container.get(OpenerService); + } + + async $tryShowNotebookDocument(uriComponents: UriComponents, viewType: string, options: NotebookDocumentShowOptions): Promise { + const editor = await open(this.openerService, URI.fromComponents(uriComponents), {}); + return (editor as NotebookEditorWidget).id; + } + $tryRevealRange(id: string, range: CellRange, revealType: NotebookEditorRevealType): Promise { + throw new Error('Method not implemented.'); + } + $trySetSelections(id: string, range: CellRange[]): void { + 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 new file mode 100644 index 0000000000000..9f12589bb0237 --- /dev/null +++ b/packages/plugin-ext/src/main/browser/notebooks/notebook-kernels-main.ts @@ -0,0 +1,291 @@ +// ***************************************************************************** +// 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 { 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 { CellExecuteUpdateDto, CellExecutionCompleteDto, 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 { 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'; +import { NotebookDto } from '../../../plugin/type-converters'; + +abstract class NotebookKernel { + private readonly onDidChangeEmitter = new Emitter(); + private readonly preloads: { uri: URI; provides: readonly string[] }[]; + readonly onDidChange: Event = this.onDidChangeEmitter.event; + + readonly id: string; + readonly viewType: string; + readonly extension: string; + + implementsInterrupt: boolean; + label: string; + description?: string; + detail?: string; + supportedLanguages: string[]; + implementsExecutionOrder: boolean; + localResourceRoot: URI; + + public get preloadUris(): URI[] { + return this.preloads.map(p => p.uri); + } + + public get preloadProvides(): string[] { + return this.preloads.map(p => p.provides).flat(); + } + + constructor(data: NotebookKernelDto, private languageService: LanguageService) { + this.id = data.id; + this.viewType = data.notebookType; + this.extension = data.extensionId; + + this.implementsInterrupt = data.supportsInterrupt ?? false; + this.label = data.label; + this.description = data.description; + this.detail = data.detail; + this.supportedLanguages = (data.supportedLanguages && data.supportedLanguages.length > 0) ? data.supportedLanguages : languageService.languages.map(lang => lang.id); + this.implementsExecutionOrder = data.supportsExecutionOrder ?? false; + this.preloads = data.preloads?.map(u => ({ uri: URI.fromComponents(u.uri), provides: u.provides })) ?? []; + } + + update(data: Partial): void { + + const event: NotebookKernelChangeEvent = Object.create(null); + if (data.label !== undefined) { + this.label = data.label; + event.label = true; + } + if (data.description !== undefined) { + this.description = data.description; + event.description = true; + } + if (data.detail !== undefined) { + this.detail = data.detail; + event.detail = true; + } + if (data.supportedLanguages !== undefined) { + this.supportedLanguages = (data.supportedLanguages && data.supportedLanguages.length > 0) ? + data.supportedLanguages : + this.languageService.languages.map(lang => lang.id); + event.supportedLanguages = true; + } + if (data.supportsExecutionOrder !== undefined) { + this.implementsExecutionOrder = data.supportsExecutionOrder; + event.hasExecutionOrder = true; + } + if (data.supportsInterrupt !== undefined) { + this.implementsInterrupt = data.supportsInterrupt; + event.hasInterruptHandler = true; + } + this.onDidChangeEmitter.fire(event); + } + + abstract executeNotebookCellsRequest(uri: URI, cellHandles: number[]): Promise; + 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; + private notebookExecutionStateService: NotebookExecutionStateService; + + private readonly executions = new Map(); + + constructor( + rpc: RPCProtocol, + container: interfaces.Container, + ) { + this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.NOTEBOOK_KERNELS_EXT); + + this.notebookKernelService = container.get(NotebookKernelService); + this.notebookExecutionStateService = container.get(NotebookExecutionStateService); + this.notebookService = container.get(NotebookService); + this.languageService = container.get(LanguageService); + } + + $postMessage(handle: number, editorId: string | undefined, message: unknown): Promise { + throw new Error('Method not implemented.'); + } + + async $addKernel(handle: number, data: NotebookKernelDto): Promise { + const that = this; + const kernel = new class extends NotebookKernel { + async executeNotebookCellsRequest(uri: URI, handles: number[]): Promise { + await that.proxy.$executeCells(handle, uri.toComponents(), handles); + } + async cancelNotebookCellExecution(uri: URI, handles: number[]): Promise { + await that.proxy.$cancelCells(handle, uri.toComponents(), handles); + } + }(data, this.languageService); + + const listener = this.notebookKernelService.onDidChangeSelectedKernel(e => { + if (e.oldKernel === kernel.id) { + this.proxy.$acceptNotebookAssociation(handle, e.notebook.toComponents(), false); + } else if (e.newKernel === kernel.id) { + this.proxy.$acceptNotebookAssociation(handle, e.notebook.toComponents(), true); + } + }); + + const registration = this.notebookKernelService.registerKernel(kernel); + this.kernels.set(handle, [kernel, combinedDisposable(listener, registration)]); + + } + + $updateKernel(handle: number, data: Partial): void { + const tuple = this.kernels.get(handle); + if (tuple) { + tuple[0].update(data); + } + } + + $removeKernel(handle: number): void { + const tuple = this.kernels.get(handle); + if (tuple) { + tuple[1].dispose(); + this.kernels.delete(handle); + } + } + + $updateNotebookPriority(handle: number, uri: UriComponents, value: number | undefined): void { + throw new Error('Method not implemented.'); + } + + $createExecution(handle: number, controllerId: string, uriComponents: UriComponents, cellHandle: number): void { + const uri = URI.fromComponents(uriComponents); + const notebook = this.notebookService.getNotebookEditorModel(uri); + if (!notebook) { + throw new Error(`Notebook not found: ${uri.toString()}`); + } + + const kernel = this.notebookKernelService.getMatchingKernel(notebook); + if (!kernel.selected || kernel.selected.id !== controllerId) { + throw new Error(`Kernel is not selected: ${kernel.selected?.id} !== ${controllerId}`); + } + const execution = this.notebookExecutionStateService.createCellExecution(uri, cellHandle); + execution.confirm(); + this.executions.set(handle, execution); + } + + $updateExecution(handle: number, updates: CellExecuteUpdateDto[]): void { + const execution = this.executions.get(handle); + execution?.update(updates.map(NotebookDto.fromCellExecuteUpdateDto)); + + } + $completeExecution(handle: number, data: CellExecutionCompleteDto): void { + try { + const execution = this.executions.get(handle); + execution?.complete(NotebookDto.fromCellExecuteCompleteDto(data)); + } finally { + this.executions.delete(handle); + } + + } + + // TODO implement notebook execution (special api for executing full notebook instead of just cells) + $createNotebookExecution(handle: number, controllerId: string, uri: UriComponents): void { + throw new Error('Method not implemented.'); + } + $beginNotebookExecution(handle: number): void { + throw new Error('Method not implemented.'); + } + $completeNotebookExecution(handle: number): void { + 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); + } + } + 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 { + 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 { + } + + dispose(): void { + this.kernels.forEach(kernel => kernel[1].dispose()); + } + +} 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 new file mode 100644 index 0000000000000..867f6fa14faa3 --- /dev/null +++ b/packages/plugin-ext/src/main/browser/notebooks/notebook-renderers-main.ts @@ -0,0 +1,47 @@ +// ***************************************************************************** +// 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 { DisposableCollection } from '@theia/core'; +import { interfaces } from '@theia/core/shared/inversify'; +import { NotebookRendererMessagingService } from '@theia/notebook/lib/browser'; +import { MAIN_RPC_CONTEXT, NotebookRenderersExt, NotebookRenderersMain } from '../../../common'; +import { RPCProtocol } from '../../../common/rpc-protocol'; + +export class NotebookRenderersMainImpl implements NotebookRenderersMain { + private readonly proxy: NotebookRenderersExt; + private readonly rendererMessagingService: NotebookRendererMessagingService; + + private readonly disposables = new DisposableCollection(); + + constructor( + rpc: RPCProtocol, + container: interfaces.Container + ) { + this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.NOTEBOOK_RENDERERS_EXT); + this.rendererMessagingService = container.get(NotebookRendererMessagingService); + this.rendererMessagingService.onShouldPostMessage(e => { + this.proxy.$postRendererMessage(e.editorId, e.rendererId, e.message); + }); + } + + $postMessage(editorId: string | undefined, rendererId: string, message: unknown): Promise { + return this.rendererMessagingService.receiveMessage(editorId, rendererId, message); + } + + dispose(): void { + this.disposables.dispose(); + } +} diff --git a/packages/plugin-ext/src/main/browser/notebooks/notebooks-main.ts b/packages/plugin-ext/src/main/browser/notebooks/notebooks-main.ts new file mode 100644 index 0000000000000..f31816ea8656e --- /dev/null +++ b/packages/plugin-ext/src/main/browser/notebooks/notebooks-main.ts @@ -0,0 +1,124 @@ +// ***************************************************************************** +// 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 { CancellationToken, DisposableCollection, Emitter } from '@theia/core'; +import { BinaryBuffer } from '@theia/core/lib/common/buffer'; +import { NotebookCellStatusBarItemList, NotebookCellStatusBarItemProvider, NotebookData, NotebookExtensionDescription, TransientOptions } from '@theia/notebook/lib/common'; +import { NotebookService } from '@theia/notebook/lib/browser'; +import { Disposable } from '@theia/plugin'; +import { MAIN_RPC_CONTEXT, NotebooksExt, NotebooksMain } from '../../../common'; +import { RPCProtocol } from '../../../common/rpc-protocol'; +import { NotebookDto } from './notebook-dto'; +import { UriComponents } from '@theia/core/lib/common/uri'; +import { HostedPluginSupport } from '../../../hosted/browser/hosted-plugin'; + +export class NotebooksMainImpl implements NotebooksMain { + + private readonly disposables = new DisposableCollection(); + + private readonly proxy: NotebooksExt; + private readonly notebookSerializer = new Map(); + private readonly notebookCellStatusBarRegistrations = new Map(); + + constructor( + rpc: RPCProtocol, + private notebookService: NotebookService, + plugins: HostedPluginSupport + ) { + this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.NOTEBOOKS_EXT); + notebookService.onNotebookSerializer(async event => plugins.activateByEvent(event)); + notebookService.markReady(); + } + + dispose(): void { + this.disposables.dispose(); + for (const disposable of this.notebookSerializer.values()) { + disposable.dispose(); + } + } + + $registerNotebookSerializer(handle: number, extension: NotebookExtensionDescription, viewType: string, options: TransientOptions): void { + const disposables = new DisposableCollection(); + + disposables.push(this.notebookService.registerNotebookSerializer(viewType, extension, { + options, + dataToNotebook: async (data: BinaryBuffer): Promise => { + const dto = await this.proxy.$dataToNotebook(handle, data, CancellationToken.None); + return NotebookDto.fromNotebookDataDto(dto); + }, + notebookToData: (data: NotebookData): Promise => + this.proxy.$notebookToData(handle, NotebookDto.toNotebookDataDto(data), CancellationToken.None) + + })); + + this.notebookSerializer.set(handle, disposables); + } + + $unregisterNotebookSerializer(handle: number): void { + this.notebookSerializer.get(handle)?.dispose(); + this.notebookSerializer.delete(handle); + } + + $emitCellStatusBarEvent(eventHandle: number): void { + const emitter = this.notebookCellStatusBarRegistrations.get(eventHandle); + if (emitter instanceof Emitter) { + emitter.fire(undefined); + } + } + + async $registerNotebookCellStatusBarItemProvider(handle: number, eventHandle: number | undefined, viewType: string): Promise { + const that = this; + const provider: NotebookCellStatusBarItemProvider = { + async provideCellStatusBarItems(uri: UriComponents, index: number, token: CancellationToken): Promise { + const result = await that.proxy.$provideNotebookCellStatusBarItems(handle, uri, index, token); + return { + items: result?.items ?? [], + dispose(): void { + if (result) { + that.proxy.$releaseNotebookCellStatusBarItems(result.cacheId); + } + } + }; + }, + viewType + }; + + if (typeof eventHandle === 'number') { + const emitter = new Emitter(); + this.notebookCellStatusBarRegistrations.set(eventHandle, emitter); + provider.onDidChangeStatusBarItems = emitter.event; + } + + // const disposable = this._cellStatusBarService.registerCellStatusBarItemProvider(provider); + // this.notebookCellStatusBarRegistrations.set(handle, disposable); + } + + async $unregisterNotebookCellStatusBarItemProvider(handle: number, eventHandle: number | undefined): Promise { + // eslint-disable-next-line @typescript-eslint/no-shadow + const unregisterThing = (statusBarHandle: number) => { + const entry = this.notebookCellStatusBarRegistrations.get(statusBarHandle); + if (entry) { + this.notebookCellStatusBarRegistrations.get(statusBarHandle)?.dispose(); + this.notebookCellStatusBarRegistrations.delete(statusBarHandle); + } + }; + unregisterThing(handle); + if (typeof eventHandle === 'number') { + unregisterThing(eventHandle); + } + } +} + diff --git a/packages/plugin-ext/src/main/browser/notebooks/renderers/cell-output-webview.tsx b/packages/plugin-ext/src/main/browser/notebooks/renderers/cell-output-webview.tsx new file mode 100644 index 0000000000000..2568749ea440f --- /dev/null +++ b/packages/plugin-ext/src/main/browser/notebooks/renderers/cell-output-webview.tsx @@ -0,0 +1,198 @@ +// ***************************************************************************** +// 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 * as React from '@theia/core/shared/react'; +import { inject, injectable, interfaces, postConstruct } from '@theia/core/shared/inversify'; +import { NotebookRendererMessagingService, CellOutputWebview, NotebookRendererRegistry, NotebookEditorWidgetService } from '@theia/notebook/lib/browser'; +import { v4 } from 'uuid'; +import { NotebookCellModel } from '@theia/notebook/lib/browser/view-model/notebook-cell-model'; +import { WebviewWidget } from '../../webview/webview'; +import { Message, WidgetManager } from '@theia/core/lib/browser'; +import { outputWebviewPreload, PreloadContext } from './output-webview-internal'; +import { WorkspaceTrustService } from '@theia/workspace/lib/browser'; +import { ChangePreferredMimetypeMessage, FromWebviewMessage, OutputChangedMessage } from './webview-communication'; +import { CellUri, NotebookCellOutputsSplice } from '@theia/notebook/lib/common'; +import { Disposable, DisposableCollection, nls, QuickPickService } from '@theia/core'; +import { NotebookCellOutputModel } from '@theia/notebook/lib/browser/view-model/notebook-cell-output-model'; + +const CellModel = Symbol('CellModel'); + +export function createCellOutputWebviewContainer(ctx: interfaces.Container, cell: NotebookCellModel): interfaces.Container { + const child = ctx.createChild(); + child.bind(CellModel).toConstantValue(cell); + child.bind(CellOutputWebviewImpl).toSelf().inSingletonScope(); + return child; +} + +@injectable() +export class CellOutputWebviewImpl implements CellOutputWebview, Disposable { + + @inject(NotebookRendererMessagingService) + protected readonly messagingService: NotebookRendererMessagingService; + + @inject(CellModel) + protected readonly cell: NotebookCellModel; + + @inject(WidgetManager) + protected readonly widgetManager: WidgetManager; + + @inject(WorkspaceTrustService) + protected readonly workspaceTrustService: WorkspaceTrustService; + + @inject(NotebookRendererRegistry) + protected readonly notebookRendererRegistry: NotebookRendererRegistry; + + @inject(NotebookEditorWidgetService) + protected readonly notebookEditorWidgetService: NotebookEditorWidgetService; + + @inject(QuickPickService) + protected readonly quickPickService: QuickPickService; + + readonly id = v4(); + + protected readonly elementRef = React.createRef(); + protected outputPresentationListeners: DisposableCollection = new DisposableCollection(); + + protected webviewWidget: WebviewWidget; + + @postConstruct() + protected async init(): Promise { + this.cell.onDidChangeOutputs(outputChange => this.updateOutput(outputChange)); + + this.webviewWidget = await this.widgetManager.getOrCreateWidget(WebviewWidget.FACTORY_ID, { id: this.id }); + this.webviewWidget.setContentOptions({ allowScripts: true }); + this.webviewWidget.setHTML(await this.createWebviewContent()); + + this.webviewWidget.onMessage((message: FromWebviewMessage) => { + this.handleWebviewMessage(message); + }); + } + + render(): React.JSX.Element { + return
    ; + } + + attachWebview(): void { + if (this.elementRef.current) { + this.webviewWidget.processMessage(new Message('before-attach')); + this.elementRef.current.appendChild(this.webviewWidget.node); + this.webviewWidget.processMessage(new Message('after-attach')); + this.webviewWidget.setIframeHeight(0); + } + } + + isAttached(): boolean { + return this.elementRef.current?.contains(this.webviewWidget.node) ?? false; + } + + updateOutput(update: NotebookCellOutputsSplice): void { + if (this.cell.outputs.length > 0) { + if (this.webviewWidget.isHidden) { + this.webviewWidget.show(); + } + + this.outputPresentationListeners.dispose(); + this.outputPresentationListeners = new DisposableCollection(); + for (const output of this.cell.outputs) { + this.outputPresentationListeners.push(output.onRequestOutputPresentationChange(() => this.requestOutputPresentationUpdate(output))); + } + + const updateOutputMessage: OutputChangedMessage = { + type: 'outputChanged', + newOutputs: update.newOutputs.map(output => ({ + id: output.outputId, + items: output.outputs.map(item => ({ mime: item.mime, data: item.data.buffer })), + metadata: output.metadata + })), + deletedOutputIds: this.cell.outputs.slice(update.start, update.start + update.deleteCount).map(output => output.outputId) + }; + + this.webviewWidget.sendMessage(updateOutputMessage); + } + } + + private async requestOutputPresentationUpdate(output: NotebookCellOutputModel): Promise { + const selectedMime = await this.quickPickService.show( + output.outputs.map(item => ({label: item.mime})), + {description: nls.localizeByDefault('Select mimetype to render for current output' )}); + if (selectedMime) { + this.webviewWidget.sendMessage({ + type: 'changePreferredMimetype', + outputId: output.outputId, + mimeType: selectedMime.label + } as ChangePreferredMimetypeMessage); + } + } + + private handleWebviewMessage(message: FromWebviewMessage): void { + switch (message.type) { + case 'initialized': + this.updateOutput({newOutputs: this.cell.outputs, start: 0, deleteCount: 0}); + break; + case 'customRendererMessage': + this.messagingService.getScoped('').postMessage(message.rendererId, message.message); + break; + case 'didRenderOutput': + this.webviewWidget.setIframeHeight(message.contentHeight + 5); + break; + case 'did-scroll-wheel': + this.notebookEditorWidgetService.getNotebookEditor(`notebook:${CellUri.parse(this.cell.uri)?.notebook}`)?.node.scrollBy(message.deltaX, message.deltaY); + break; + } + } + + private async createWebviewContent(): Promise { + const isWorkspaceTrusted = await this.workspaceTrustService.getWorkspaceTrust(); + const preloads = this.preloadsScriptString(isWorkspaceTrusted); + const content = ` + + + + + + + + + `; + return content; + } + + private preloadsScriptString(isWorkspaceTrusted: boolean): string { + const ctx: PreloadContext = { + isWorkspaceTrusted, + rendererData: this.notebookRendererRegistry.notebookRenderers, + renderOptions: { // TODO these should be changeable in the settings + lineLimit: 30, + outputScrolling: false, + outputWordWrap: false, + } + }; + // TS will try compiling `import()` in webviewPreloads, so use a helper function instead + // of using `import(...)` directly + return ` + const __import = (x) => import(x); + (${outputWebviewPreload})(JSON.parse(decodeURIComponent("${encodeURIComponent(JSON.stringify(ctx))}")))`; + } + + dispose(): void { + this.outputPresentationListeners.dispose(); + this.webviewWidget.dispose(); + } +} diff --git a/packages/plugin-ext/src/main/browser/notebooks/renderers/output-webview-internal.ts b/packages/plugin-ext/src/main/browser/notebooks/renderers/output-webview-internal.ts new file mode 100644 index 0000000000000..17e18d2a48f17 --- /dev/null +++ b/packages/plugin-ext/src/main/browser/notebooks/renderers/output-webview-internal.ts @@ -0,0 +1,476 @@ +// ***************************************************************************** +// 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. + *--------------------------------------------------------------------------------------------*/ + +// only type imports are allowed here since this runs in an iframe. All other code is not accessible +import type * as webviewCommunication from './webview-communication'; +import type * as rendererApi from 'vscode-notebook-renderer'; +import type { Disposable, Event } from '@theia/core'; + +declare const acquireVsCodeApi: () => ({ + getState(): { [key: string]: unknown }; + setState(data: { [key: string]: unknown }): void; + postMessage: (msg: unknown) => void; +}); + +declare function __import(path: string): Promise; + +interface Listener { fn: (evt: T) => void; thisArg: unknown }; + +interface EmitterLike { + fire(data: T): void; + event: Event; +} + +interface RendererContext extends rendererApi.RendererContext { + readonly onDidChangeSettings: Event; + readonly settings: RenderOptions; +} + +interface NotebookRendererEntrypoint { + readonly path: string; + readonly extends?: string +}; + +export interface RenderOptions { + readonly lineLimit: number; + readonly outputScrolling: boolean; + readonly outputWordWrap: boolean; +} + +export interface PreloadContext { + readonly isWorkspaceTrusted: boolean; + readonly rendererData: readonly webviewCommunication.RendererMetadata[]; + readonly renderOptions: RenderOptions; +} + +export async function outputWebviewPreload(ctx: PreloadContext): Promise { + const theia = acquireVsCodeApi(); + const renderFallbackErrorName = 'vscode.fallbackToNextRenderer'; + + function createEmitter(listenerChange: (listeners: Set>) => void = () => undefined): EmitterLike { + const listeners = new Set>(); + return { + fire(data: T): void { + for (const listener of [...listeners]) { + listener.fn.call(listener.thisArg, data); + } + }, + event(fn, thisArg, disposables): Disposable { + const listenerObj = { fn, thisArg }; + const disposable: Disposable = { + dispose: () => { + listeners.delete(listenerObj); + listenerChange(listeners); + }, + }; + + listeners.add(listenerObj); + listenerChange(listeners); + + if (disposables) { + if ('push' in disposables) { + disposables.push(disposable); + } else { + disposables.add(disposable); + } + } + return disposable; + } + }; + }; + + const settingChange: EmitterLike = createEmitter(); + + class Output { + readonly outputId: string; + renderedItem?: rendererApi.OutputItem; + allItems: rendererApi.OutputItem[]; + + renderer: Renderer; + + element: HTMLElement; + + constructor(output: webviewCommunication.Output, items: rendererApi.OutputItem[]) { + this.element = document.createElement('div'); + // padding for scrollbars + this.element.style.paddingBottom = '10px'; + this.element.style.paddingRight = '10px'; + this.element.id = output.id; + document.body.appendChild(this.element); + + this.allItems = items; + } + + findItemToRender(preferredMimetype?: string): rendererApi.OutputItem { + if (preferredMimetype) { + const itemToRender = this.allItems.find(item => item.mime === preferredMimetype); + if (itemToRender) { + return itemToRender; + } + } + return this.renderedItem ?? this.allItems[0]; + } + + clear(): void { + this.renderer?.disposeOutputItem?.(this.renderedItem?.id); + this.element.innerHTML = ''; + } + } + + const outputs = new Map(); + + class Renderer { + + entrypoint: NotebookRendererEntrypoint; + + private rendererApi?: rendererApi.RendererApi; + + private onMessageEvent: EmitterLike = createEmitter(); + + constructor( + public readonly data: webviewCommunication.RendererMetadata + ) { } + + public receiveMessage(message: unknown): void { + this.onMessageEvent.fire(message); + } + + public disposeOutputItem(id?: string): void { + this.rendererApi?.disposeOutputItem?.(id); + } + + async getOrLoad(): Promise { + if (this.rendererApi) { + return this.rendererApi; + } + + const rendererModule = await __import(this.data.entrypoint.uri) as { activate: rendererApi.ActivationFunction }; + this.rendererApi = await rendererModule.activate(this.createRendererContext()); + return this.rendererApi; + } + + protected createRendererContext(): RendererContext { + const context: RendererContext = { + setState: newState => theia.setState({ ...theia.getState(), [this.data.id]: newState }), + getState: () => { + const state = theia.getState(); + return typeof state === 'object' && state ? state[this.data.id] as T : undefined; + }, + getRenderer: async (id: string) => { + const renderer = renderers.getRenderer(id); + if (!renderer) { + return undefined; + } + if (renderer.rendererApi) { + return renderer.rendererApi; + } + return renderer.getOrLoad(); + }, + workspace: { + get isTrusted(): boolean { return true; } // TODO use Workspace trust service + }, + settings: { + get lineLimit(): number { return ctx.renderOptions.lineLimit; }, + get outputScrolling(): boolean { return ctx.renderOptions.outputScrolling; }, + get outputWordWrap(): boolean { return ctx.renderOptions.outputWordWrap; }, + }, + get onDidChangeSettings(): Event { return settingChange.event; }, + }; + + if (this.data.requiresMessaging) { + context.onDidReceiveMessage = this.onMessageEvent.event; + context.postMessage = message => theia.postMessage({ type: 'customRendererMessage', rendererId: this.data.id, message }); + } + + return Object.freeze(context); + } + } + + const renderers = new class { + private readonly renderers = new Map(); + + constructor() { + for (const renderer of ctx.rendererData) { + this.addRenderer(renderer); + } + } + + public getRenderer(id: string): Renderer | undefined { + return this.renderers.get(id); + } + + private rendererEqual(a: webviewCommunication.RendererMetadata, b: webviewCommunication.RendererMetadata): boolean { + if (a.id !== b.id || a.entrypoint.uri !== b.entrypoint.uri || a.entrypoint.extends !== b.entrypoint.extends || a.requiresMessaging !== b.requiresMessaging) { + return false; + } + + if (a.mimeTypes.length !== b.mimeTypes.length) { + return false; + } + + for (let i = 0; i < a.mimeTypes.length; i++) { + if (a.mimeTypes[i] !== b.mimeTypes[i]) { + return false; + } + } + + return true; + } + + public updateRendererData(rendererData: readonly webviewCommunication.RendererMetadata[]): void { + const oldKeys = new Set(this.renderers.keys()); + const newKeys = new Set(rendererData.map(d => d.id)); + + for (const renderer of rendererData) { + const existing = this.renderers.get(renderer.id); + if (existing && this.rendererEqual(existing.data, renderer)) { + continue; + } + + this.addRenderer(renderer); + } + + for (const key of oldKeys) { + if (!newKeys.has(key)) { + this.renderers.delete(key); + } + } + } + + private addRenderer(renderer: webviewCommunication.RendererMetadata): void { + this.renderers.set(renderer.id, new Renderer(renderer)); + } + + public clearAll(): void { + for (const renderer of this.renderers.values()) { + renderer.disposeOutputItem(); + } + } + + public clearOutput(rendererId: string, outputId: string): void { + // outputRunner.cancelOutput(outputId); + this.renderers.get(rendererId)?.disposeOutputItem(outputId); + } + + public async render(output: Output, preferredMimeType: string | undefined, preferredRendererId: string | undefined, signal: AbortSignal): Promise { + const item = output.findItemToRender(preferredMimeType); + const primaryRenderer = this.findRenderer(preferredRendererId, item); + if (!primaryRenderer) { + this.showRenderError(item, output.element, 'No renderer found for output type.'); + return; + } + + // Try primary renderer first + if (!(await this.doRender(item, output.element, primaryRenderer, signal)).continue) { + output.renderer = primaryRenderer; + this.onRenderCompleted(); + return; + } + + // Primary renderer failed in an expected way. Fallback to render the next mime types + for (const additionalItem of output.allItems) { + if (additionalItem.mime === item.mime) { + continue; + } + + if (signal.aborted) { + return; + } + + if (additionalItem) { + const renderer = this.findRenderer(undefined, additionalItem); + if (renderer) { + if (!(await this.doRender(additionalItem, output.element, renderer, signal)).continue) { + output.renderer = renderer; + this.onRenderCompleted(); + return; // We rendered successfully + } + } + } + } + + // All renderers have failed and there is nothing left to fallback to + this.showRenderError(item, output.element, 'No fallback renderers found or all fallback renderers failed.'); + } + + private onRenderCompleted(): void { + // we need to check for all images are loaded. Otherwise we can't determine the correct height of the output + const images = Array.from(document.images); + if (images.length > 0) { + Promise.all(images.filter(img => !img.complete).map(img => new Promise(resolve => { img.onload = img.onerror = resolve; }))).then(() => + theia.postMessage({ type: 'didRenderOutput', contentHeight: document.body.clientHeight })); + } else { + theia.postMessage({ type: 'didRenderOutput', contentHeight: document.body.clientHeight }); + } + + } + + private async doRender(item: rendererApi.OutputItem, element: HTMLElement, renderer: Renderer, signal: AbortSignal): Promise<{ continue: boolean }> { + try { + (await renderer.getOrLoad())?.renderOutputItem(item, element, signal); + return { continue: false }; // We rendered successfully + } catch (e) { + if (signal.aborted) { + return { continue: false }; + } + + if (e instanceof Error && e.name === renderFallbackErrorName) { + return { continue: true }; + } else { + throw e; // Bail and let callers handle unknown errors + } + } + } + + private findRenderer(preferredRendererId: string | undefined, info: rendererApi.OutputItem): Renderer | undefined { + let foundRenderer: Renderer | undefined; + + if (typeof preferredRendererId === 'string') { + foundRenderer = Array.from(this.renderers.values()) + .find(renderer => renderer.data.id === preferredRendererId); + } else { + const rendererList = Array.from(this.renderers.values()) + .filter(renderer => renderer.data.mimeTypes.includes(info.mime) && !renderer.data.entrypoint.extends); + + if (rendererList.length) { + // De-prioritize built-in renderers + // rendererList.sort((a, b) => +a.data.isBuiltin - +b.data.isBuiltin); + + // Use first renderer we find in sorted list + foundRenderer = rendererList[0]; + } + } + return foundRenderer; + } + + private showRenderError(info: rendererApi.OutputItem, element: HTMLElement, errorMessage: string): void { + const errorContainer = document.createElement('div'); + + const error = document.createElement('div'); + error.className = 'no-renderer-error'; + error.innerText = errorMessage; + + const cellText = document.createElement('div'); + cellText.innerText = info.text(); + + errorContainer.appendChild(error); + errorContainer.appendChild(cellText); + + element.innerText = ''; + element.appendChild(errorContainer); + } + }(); + + function clearOutput(outputId: string): void { + outputs.get(outputId)?.clear(); + outputs.delete(outputId); + } + + function outputsChanged(changedEvent: webviewCommunication.OutputChangedMessage): void { + for (const outputId of changedEvent.deletedOutputIds ?? []) { + clearOutput(outputId); + } + + for (const outputData of changedEvent.newOutputs ?? []) { + const apiItems: rendererApi.OutputItem[] = outputData.items.map((item, index) => ({ + id: `${outputData.id}-${index}`, + mime: item.mime, + metadata: outputData.metadata, + data(): Uint8Array { + return item.data; + }, + text(): string { + return new TextDecoder().decode(this.data()); + }, + json(): unknown { + return JSON.parse(this.text()); + }, + blob(): Blob { + return new Blob([this.data()], { type: this.mime }); + }, + + })); + + const output = new Output(outputData, apiItems); + outputs.set(outputData.id, output); + + renderers.render(output, undefined, undefined, new AbortController().signal); + } + } + + function scrollParent(event: WheelEvent): boolean { + for (let node = event.target as Node | null; node; node = node.parentNode) { + if (!(node instanceof Element)) { + continue; + } + + // scroll up + if (event.deltaY < 0 && node.scrollTop > 0) { + // there is still some content to scroll + return false; + } + + // scroll down + if (event.deltaY > 0 && node.scrollTop + node.clientHeight < node.scrollHeight) { + // per https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight + // scrollTop is not rounded but scrollHeight and clientHeight are + // so we need to check if the difference is less than some threshold + if (node.scrollHeight - node.scrollTop - node.clientHeight > 2) { + return false; + } + } + } + + return true; + } + + const handleWheel = (event: WheelEvent & { wheelDeltaX?: number; wheelDeltaY?: number; wheelDelta?: number }) => { + if (scrollParent(event)) { + theia.postMessage({ + type: 'did-scroll-wheel', + deltaY: event.deltaY, + deltaX: event.deltaX, + }); + } + }; + + window.addEventListener('message', async rawEvent => { + const event = rawEvent as ({ data: webviewCommunication.ToWebviewMessage }); + + switch (event.data.type) { + case 'updateRenderers': + renderers.updateRendererData(event.data.rendererData); + break; + case 'outputChanged': + outputsChanged(event.data); + break; + case 'customRendererMessage': + renderers.getRenderer(event.data.rendererId)?.receiveMessage(event.data.message); + break; + case 'changePreferredMimetype': + clearOutput(event.data.outputId); + renderers.render(outputs.get(event.data.outputId)!, event.data.mimeType, undefined, new AbortController().signal); + break; + } + }); + window.addEventListener('wheel', handleWheel); + + theia.postMessage({ type: 'initialized' }); +} diff --git a/packages/plugin-ext/src/main/browser/notebooks/renderers/webview-communication.ts b/packages/plugin-ext/src/main/browser/notebooks/renderers/webview-communication.ts new file mode 100644 index 0000000000000..4fee027832c22 --- /dev/null +++ b/packages/plugin-ext/src/main/browser/notebooks/renderers/webview-communication.ts @@ -0,0 +1,79 @@ +// ***************************************************************************** +// 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. + *--------------------------------------------------------------------------------------------*/ + +export interface RendererMetadata { + readonly id: string; + readonly entrypoint: { readonly uri: string, readonly extends?: string; }; + readonly mimeTypes: readonly string[]; + readonly requiresMessaging: boolean; +} + +export interface CustomRendererMessage { + readonly type: 'customRendererMessage'; + readonly rendererId: string; + readonly message: unknown; +} + +export interface UpdateRenderersMessage { + readonly type: 'updateRenderers'; + readonly rendererData: readonly RendererMetadata[]; +} + +export interface OutputChangedMessage { + readonly type: 'outputChanged'; + readonly newOutputs?: Output[]; + readonly deletedOutputIds?: string[]; +} + +export interface ChangePreferredMimetypeMessage { + readonly type: 'changePreferredMimetype'; + readonly outputId: string; + readonly mimeType: string; +} + +export type ToWebviewMessage = UpdateRenderersMessage | OutputChangedMessage | ChangePreferredMimetypeMessage | CustomRendererMessage; + +export interface WebviewInitialized { + readonly type: 'initialized'; +} + +export interface OnDidRenderOutput { + readonly type: 'didRenderOutput'; + contentHeight: number; +} + +export interface WheelMessage { + readonly type: 'did-scroll-wheel'; + readonly deltaY: number; + readonly deltaX: number; +} + +export type FromWebviewMessage = WebviewInitialized | OnDidRenderOutput | WheelMessage | CustomRendererMessage; + +export interface Output { + id: string + metadata?: Record; + items: OutputItem[]; +} + +export interface OutputItem { + readonly mime: string; + readonly data: Uint8Array; +} diff --git a/packages/plugin-ext/src/main/browser/plugin-contribution-handler.ts b/packages/plugin-ext/src/main/browser/plugin-contribution-handler.ts index 50075206eb8dd..b96302b16ff7c 100644 --- a/packages/plugin-ext/src/main/browser/plugin-contribution-handler.ts +++ b/packages/plugin-ext/src/main/browser/plugin-contribution-handler.ts @@ -21,7 +21,10 @@ import { TextmateRegistry, getEncodedLanguageId, MonacoTextmateService, GrammarD import { MenusContributionPointHandler } from './menus/menus-contribution-handler'; import { PluginViewRegistry } from './view/plugin-view-registry'; import { PluginCustomEditorRegistry } from './custom-editors/plugin-custom-editor-registry'; -import { PluginContribution, IndentationRules, FoldingRules, ScopeMap, DeployedPlugin, GrammarsContribution, EnterAction, OnEnterRule, RegExpOptions } from '../../common'; +import { + PluginContribution, IndentationRules, FoldingRules, ScopeMap, DeployedPlugin, + GrammarsContribution, EnterAction, OnEnterRule, RegExpOptions, getPluginId +} from '../../common'; import { DefaultUriLabelProviderContribution, LabelProviderContribution, @@ -35,6 +38,7 @@ import { CommandRegistry, Command, CommandHandler } from '@theia/core/lib/common import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable'; import { Emitter } from '@theia/core/lib/common/event'; import { TaskDefinitionRegistry, ProblemMatcherRegistry, ProblemPatternRegistry } from '@theia/task/lib/browser'; +import { NotebookRendererRegistry, NotebookTypeRegistry } from '@theia/notebook/lib/browser'; import { PluginDebugService } from './debug/plugin-debug-service'; import { DebugSchemaUpdater } from '@theia/debug/lib/browser/debug-schema-updater'; import { MonacoThemingService } from '@theia/monaco/lib/browser/monaco-theming-service'; @@ -120,6 +124,12 @@ export class PluginContributionHandler { @inject(ContributedTerminalProfileStore) protected readonly contributedProfileStore: TerminalProfileStore; + @inject(NotebookTypeRegistry) + protected readonly notebookTypeRegistry: NotebookTypeRegistry; + + @inject(NotebookRendererRegistry) + protected readonly notebookRendererRegistry: NotebookRendererRegistry; + @inject(ContributionProvider) @named(LabelProviderContribution) protected readonly contributionProvider: ContributionProvider; @@ -395,6 +405,22 @@ export class PluginContributionHandler { } } + if (contributions.notebooks) { + for (const notebook of contributions.notebooks) { + pushContribution(`notebook.${notebook.type}`, + () => this.notebookTypeRegistry.registerNotebookType(notebook) + ); + } + } + + if (contributions.notebookRenderer) { + for (const renderer of contributions.notebookRenderer) { + pushContribution(`notebookRenderer.${renderer.id}`, + () => this.notebookRendererRegistry.registerNotebookRenderer(renderer, `/hostedPlugin/${getPluginId(plugin.metadata.model)}`) + ); + } + } + return toDispose; } diff --git a/packages/plugin-ext/src/main/browser/plugin-ext-frontend-module.ts b/packages/plugin-ext/src/main/browser/plugin-ext-frontend-module.ts index f737307a54961..b4268e6280e33 100644 --- a/packages/plugin-ext/src/main/browser/plugin-ext-frontend-module.ts +++ b/packages/plugin-ext/src/main/browser/plugin-ext-frontend-module.ts @@ -72,7 +72,6 @@ import { PluginCustomEditorRegistry } from './custom-editors/plugin-custom-edito import { CustomEditorWidgetFactory } from '../browser/custom-editors/custom-editor-widget-factory'; import { CustomEditorWidget } from './custom-editors/custom-editor-widget'; import { CustomEditorService } from './custom-editors/custom-editor-service'; -import { UndoRedoService } from './custom-editors/undo-redo-service'; import { WebviewFrontendSecurityWarnings } from './webview/webview-frontend-security-warnings'; import { PluginAuthenticationServiceImpl } from './plugin-authentication-service'; import { AuthenticationService } from '@theia/core/lib/browser/authentication-service'; @@ -85,6 +84,9 @@ import { DnDFileContentStore } from './view/dnd-file-content-store'; import { WebviewContextKeys } from './webview/webview-context-keys'; import { LanguagePackService, languagePackServicePath } from '../../common/language-pack-service'; import { TabBarToolbarContribution } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; +import { CellOutputWebviewFactory } from '@theia/notebook/lib/browser'; +import { CellOutputWebviewImpl, createCellOutputWebviewContainer } from './notebooks/renderers/cell-output-webview'; +import { NotebookCellModel } from '@theia/notebook/lib/browser/view-model/notebook-cell-model'; export default new ContainerModule((bind, unbind, isBound, rebind) => { @@ -193,8 +195,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(CustomEditorWidgetFactory).toDynamicValue(ctx => new CustomEditorWidgetFactory(ctx.container)).inSingletonScope(); bind(WidgetFactory).toService(CustomEditorWidgetFactory); - bind(UndoRedoService).toSelf().inSingletonScope(); - bind(PluginViewWidget).toSelf(); bind(WidgetFactory).toDynamicValue(({ container }) => ({ id: PLUGIN_VIEW_FACTORY_ID, @@ -257,4 +257,8 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { const provider = ctx.container.get(WebSocketConnectionProvider); return provider.createProxy(languagePackServicePath); }).inSingletonScope(); + + bind(CellOutputWebviewFactory).toFactory(ctx => async (cell: NotebookCellModel) => + createCellOutputWebviewContainer(ctx.container, cell).getAsync(CellOutputWebviewImpl) + ); }); diff --git a/packages/plugin-ext/src/main/browser/webview/webview.ts b/packages/plugin-ext/src/main/browser/webview/webview.ts index 6a095a26a70b3..ac6f7310adee1 100644 --- a/packages/plugin-ext/src/main/browser/webview/webview.ts +++ b/packages/plugin-ext/src/main/browser/webview/webview.ts @@ -592,6 +592,12 @@ export class WebviewWidget extends BaseWidget implements StatefulWidget, Extract this._state = state; } + setIframeHeight(height: number): void { + if (this.element) { + this.element.style.height = `${height}px`; + } + } + protected async doSend(channel: string, data?: any): Promise { if (!this.element) { return; 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 new file mode 100644 index 0000000000000..a49954a6fe125 --- /dev/null +++ b/packages/plugin-ext/src/plugin/notebook/notebook-document.ts @@ -0,0 +1,438 @@ +// ***************************************************************************** +// 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 * 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 { Disposable, URI } from '@theia/core'; +import * as typeConverters from '../type-converters'; +import { ModelAddedData, NotebookCellDto, NotebookCellsChangedEventDto, NotebookModelAddedData, NotebookOutputDto } from '../../common'; +import { NotebookRange } from '../types-impl'; +import { UriComponents } from '../../common/uri-components'; +import { DocumentsExtImpl } from '../documents'; + +class RawContentChangeEvent { + + constructor( + readonly start: number, + readonly deletedCount: number, + readonly deletedItems: theia.NotebookCell[], + readonly items: Cell[] + ) { } + + asApiEvent(): theia.NotebookDocumentContentChange { + return { + range: new NotebookRange(this.start, this.start + this.deletedCount), + addedCells: this.items.map(cell => cell.apiCell), + removedCells: this.deletedItems, + }; + } +} + +export class Cell { + + static asModelAddData(notebook: theia.NotebookDocument, cell: NotebookCellDto): ModelAddedData & { notebook: theia.NotebookDocument } { + return { + EOL: cell.eol, + lines: cell.source, + languageId: cell.language, + uri: cell.uri, + isDirty: false, + versionId: 1, + notebook, + modeId: '' + }; + } + + private cell: theia.NotebookCell | undefined; + + readonly handle: number; + readonly uri: URI; + readonly cellKind: notebookCommon.CellKind; + + private outputs: theia.NotebookCellOutput[]; + private metadata: Readonly; + private previousResult: Readonly; + + internalMetadata: notebookCommon.NotebookCellInternalMetadata; + + get language(): string { + return this.apiCell.document.languageId; + } + + constructor( + public readonly notebookDocument: NotebookDocument, + private readonly editorsAndDocuments: EditorsAndDocumentsExtImpl, + private readonly cellData: rpc.NotebookCellDto, + ) { + this.handle = cellData.handle; + this.uri = URI.fromComponents(cellData.uri); + this.cellKind = cellData.cellKind; + this.outputs = cellData.outputs.map(typeConverters.NotebookCellOutputConverter.to); + this.internalMetadata = cellData.internalMetadata ?? {}; + this.metadata = Object.freeze(cellData.metadata ?? {}); + this.previousResult = Object.freeze(typeConverters.NotebookCellExecutionSummary.to(cellData.internalMetadata ?? {})); + } + + get apiCell(): theia.NotebookCell { + if (!this.cell) { + const that = this; + const data = this.editorsAndDocuments.getDocument(this.uri.toString()); + if (!data) { + throw new Error(`MISSING extHostDocument for notebook cell: ${this.uri}`); + } + const apiCell: theia.NotebookCell = { + get index(): number { return that.notebookDocument.getCellIndex(that); }, + notebook: that.notebookDocument.apiNotebook, + kind: typeConverters.NotebookCellKind.to(this.cellData.cellKind), + document: data.document, + get outputs(): theia.NotebookCellOutput[] { return that.outputs.slice(0); }, + get metadata(): notebookCommon.NotebookCellMetadata { return that.metadata; }, + get executionSummary(): theia.NotebookCellExecutionSummary | undefined { return that.previousResult; } + }; + this.cell = Object.freeze(apiCell); + } + return this.cell; + } + + setOutputs(newOutputs: NotebookOutputDto[]): void { + this.outputs = newOutputs.map(typeConverters.NotebookCellOutputConverter.to); + } + + // setOutputItems(outputId: string, append: boolean, newOutputItems: NotebookOutputItemDto[]): void { + // const newItems = newOutputItems.map(typeConverters.NotebookCellOutputItem.to); + // const output = this.outputs.find(op => op.id === outputId); + // if (output) { + // if (!append) { + // output.items.length = 0; + // } + // output.items.push(...newItems); + + // // if (output.items.length > 1 && output.items.every(item => notebookCommon.isTextStreamMime(item.mime))) { + // // // Look for the mimes in the items, and keep track of their order. + // // // Merge the streams into one output item, per mime type. + // // const mimeOutputs = new Map(); + // // const mimeTypes: string[] = []; + // // output.items.forEach(item => { + // // let items: Uint8Array[]; + // // if (mimeOutputs.has(item.mime)) { + // // items = mimeOutputs.get(item.mime)!; + // // } else { + // // items = []; + // // mimeOutputs.set(item.mime, items); + // // mimeTypes.push(item.mime); + // // } + // // items.push(item.data); + // // }); + // // output.items.length = 0; + // // mimeTypes.forEach(mime => { + // // const compressed = notebookCommon.compressOutputItemStreams(mimeOutputs.get(mime)!); + // // output.items.push({ + // // mime, + // // data: compressed.buffer + // // }); + // // }); + // // } + // } + // } + + setMetadata(newMetadata: notebookCommon.NotebookCellMetadata): void { + this.metadata = Object.freeze(newMetadata); + } + + setInternalMetadata(newInternalMetadata: notebookCommon.NotebookCellInternalMetadata): void { + this.internalMetadata = newInternalMetadata; + this.previousResult = Object.freeze(typeConverters.NotebookCellExecutionSummary.to(newInternalMetadata)); + } + +} + +export class NotebookDocument implements Disposable { + + private readonly cells: Cell[]; + + private readonly notebookType: string; + + private notebook?: theia.NotebookDocument; + private metadata: Record; + private versionId: number = 0; + private isDirty: boolean = false; + private disposed: boolean = false; + + constructor( + private readonly proxy: rpc.NotebookDocumentsMain, + private readonly editorsAndDocuments: EditorsAndDocumentsExtImpl, + private readonly textDocuments: DocumentsExtImpl, + 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 { + if (!this.notebook) { + const that = this; + const apiObject: theia.NotebookDocument = { + get uri(): theia.Uri { return that.uri; }, + get version(): number { return that.versionId; }, + get notebookType(): string { return that.notebookType; }, + get isDirty(): boolean { return that.isDirty; }, + get isUntitled(): boolean { return that.uri.scheme === 'untitled'; }, + get isClosed(): boolean { return that.disposed; }, + get metadata(): Record { return that.metadata; }, + get cellCount(): number { return that.cells.length; }, + cellAt(index): theia.NotebookCell { + index = that.validateIndex(index); + return that.cells[index].apiCell; + }, + getCells(range): theia.NotebookCell[] { + const cells = range ? that.getCells(range) : that.cells; + return cells.map(cell => cell.apiCell); + }, + save(): Promise { + return that.save(); + } + }; + this.notebook = Object.freeze(apiObject); + } + return this.notebook; + } + + private validateIndex(index: number): number { + index = index | 0; + if (index < 0) { + return 0; + } else if (index >= this.cells.length) { + return this.cells.length - 1; + } else { + return index; + } + } + + private validateRange(range: theia.NotebookRange): theia.NotebookRange { + let start = range.start | 0; + let end = range.end | 0; + if (start < 0) { + start = 0; + } + if (end > this.cells.length) { + end = this.cells.length; + } + return range.with({ start, end }); + } + + private getCells(range: theia.NotebookRange): Cell[] { + range = this.validateRange(range); + const result: Cell[] = []; + for (let i = range.start; i < range.end; i++) { + result.push(this.cells[i]); + } + return result; + } + + private async save(): Promise { + if (this.disposed) { + return Promise.reject(new Error('Notebook has been closed')); + } + return this.proxy.$trySaveNotebook(this.uri); + } + + acceptDirty(isDirty: boolean): void { + this.isDirty = isDirty; + } + + acceptModelChanged(event: NotebookCellsChangedEventDto, isDirty: boolean, newMetadata: notebookCommon.NotebookDocumentMetadata | undefined): theia.NotebookDocumentChangeEvent { + this.versionId = event.versionId; + this.isDirty = isDirty; + // this.acceptDocumentPropertiesChanged({ metadata: newMetadata }); + + const result = { + notebook: this.apiNotebook, + metadata: newMetadata, + cellChanges: [], + contentChanges: [], + }; + + type RelaxedCellChange = Partial & { cell: theia.NotebookCell }; + const relaxedCellChanges: RelaxedCellChange[] = []; + + // -- apply change and populate content changes + + for (const rawEvent of event.rawEvents) { + if (rawEvent.kind === notebookCommon.NotebookCellsChangeType.ModelChange) { + this.spliceNotebookCells(rawEvent.changes, false, result.contentChanges); + } else if (rawEvent.kind === notebookCommon.NotebookCellsChangeType.Move) { + this.moveCells(rawEvent.index, rawEvent.length, rawEvent.newIdx, result.contentChanges); + } else if (rawEvent.kind === notebookCommon.NotebookCellsChangeType.Output) { + 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.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.ChangeCellMetadata) { + this.changeCellMetadata(rawEvent.index, rawEvent.metadata); + relaxedCellChanges.push({ cell: this.cells[rawEvent.index].apiCell, metadata: this.cells[rawEvent.index].apiCell.metadata }); + + } else if (rawEvent.kind === notebookCommon.NotebookCellsChangeType.ChangeCellInternalMetadata) { + this.changeCellInternalMetadata(rawEvent.index, rawEvent.internalMetadata); + relaxedCellChanges.push({ cell: this.cells[rawEvent.index].apiCell, executionSummary: this.cells[rawEvent.index].apiCell.executionSummary }); + } + } + + // -- compact cellChanges + + const map = new Map(); + for (let i = 0; i < relaxedCellChanges.length; i++) { + const relaxedCellChange = relaxedCellChanges[i]; + const existing = map.get(relaxedCellChange.cell); + if (existing === undefined) { + const newLen = result.cellChanges.push({ + document: undefined, + executionSummary: undefined, + metadata: undefined, + outputs: undefined, + ...relaxedCellChange, + }); + map.set(relaxedCellChange.cell, newLen - 1); + } else { + result.cellChanges[existing] = { + ...result.cellChanges[existing], + ...relaxedCellChange + }; + } + } + + // Freeze event properties so handlers cannot accidentally modify them + Object.freeze(result); + Object.freeze(result.cellChanges); + Object.freeze(result.contentChanges); + + return result; + } + + private spliceNotebookCells(splices: notebookCommon.NotebookCellTextModelSplice[], initialization: boolean, + bucket: theia.NotebookDocumentContentChange[] | undefined): void { + if (this.disposed) { + return; + } + + const contentChangeEvents: RawContentChangeEvent[] = []; + const addedCellDocuments: ModelAddedData[] = []; + const removedCellDocuments: UriComponents[] = []; + + splices.reverse().forEach(splice => { + const cellDtos = splice[2]; + const newCells = cellDtos.map((cell: NotebookCellDto) => { + + const extCell = new Cell(this, this.editorsAndDocuments, cell); + if (!initialization) { + addedCellDocuments.push(Cell.asModelAddData(this.apiNotebook, cell)); + } + return extCell; + }); + + const changeEvent = new RawContentChangeEvent(splice[0], splice[1], [], newCells); + const deletedItems = this.cells.splice(splice[0], splice[1], ...newCells); + for (const cell of deletedItems) { + removedCellDocuments.push(cell.uri.toComponents()); + changeEvent.deletedItems.push(cell.apiCell); + } + contentChangeEvents.push(changeEvent); + }); + + if (bucket) { + for (const changeEvent of contentChangeEvents) { + bucket.push(changeEvent.asApiEvent()); + } + } + } + + private moveCells(index: number, length: number, newIdx: number, bucket: theia.NotebookDocumentContentChange[]): void { + const cells = this.cells.splice(index, length); + this.cells.splice(newIdx, 0, ...cells); + const changes = [ + new RawContentChangeEvent(index, length, cells.map(c => c.apiCell), []), + new RawContentChangeEvent(newIdx, 0, [], cells) + ]; + for (const change of changes) { + bucket.push(change.asApiEvent()); + } + } + + private setCellOutputs(index: number, outputs: NotebookOutputDto[]): void { + const cell = this.cells[index]; + cell.setOutputs(outputs); + } + + // private _setCellOutputItems(index: number, outputId: string, append: boolean, outputItems: NotebookOutputItemDto[]): void { + // const cell = this.cells[index]; + // cell.setOutputItems(outputId, append, outputItems); + // } + + private changeCellLanguage(index: number, newLanguageId: string): void { + const cell = this.cells[index]; + if (cell.apiCell.document.languageId !== newLanguageId) { + this.textDocuments.$acceptModelModeChanged(cell.uri.toComponents(), cell.language, newLanguageId); + } + } + + private changeCellMetadata(index: number, newMetadata: notebookCommon.NotebookCellMetadata): void { + const cell = this.cells[index]; + cell.setMetadata(newMetadata); + } + + private changeCellInternalMetadata(index: number, newInternalMetadata: notebookCommon.NotebookCellInternalMetadata): void { + const cell = this.cells[index]; + cell.setInternalMetadata(newInternalMetadata); + } + + getCellFromApiCell(apiCell: theia.NotebookCell): Cell | undefined { + return this.cells.find(cell => cell.apiCell === apiCell); + } + + getCell(cellHandle: number): Cell | undefined { + return this.cells.find(cell => cell.handle === cellHandle); + } + + getCellFromIndex(index: number): Cell | undefined { + return this.cells[index]; + } + + getCellIndex(cell: Cell): number { + return this.cells.indexOf(cell); + } + + dispose(): void { + this.disposed = true; + } +} diff --git a/packages/plugin-ext/src/plugin/notebook/notebook-documents.ts b/packages/plugin-ext/src/plugin/notebook/notebook-documents.ts new file mode 100644 index 0000000000000..e638657320df6 --- /dev/null +++ b/packages/plugin-ext/src/plugin/notebook/notebook-documents.ts @@ -0,0 +1,58 @@ +// ***************************************************************************** +// 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 * as theia from '@theia/plugin'; +import { Emitter } from '@theia/core'; +import { UriComponents } from '../../common/uri-components'; +import { NotebookCellsChangedEventDto, NotebookDocumentsExt } from '../../common'; +import { NotebooksExtImpl } from './notebooks'; +import { URI } from '../types-impl'; + +export type NotebookDocumentMetadata = Record; + +export class NotebookDocumentsExtImpl implements NotebookDocumentsExt { + + private readonly didSaveNotebookDocumentEmitter = new Emitter(); + readonly onDidSaveNotebookDocument = this.didSaveNotebookDocumentEmitter.event; + + private readonly didChangeNotebookDocumentEmitter = new Emitter(); + readonly onDidChangeNotebookDocument = this.didChangeNotebookDocumentEmitter.event; + + constructor( + private readonly notebooksAndEditors: NotebooksExtImpl, + ) { } + + $acceptModelChanged(uri: UriComponents, event: NotebookCellsChangedEventDto, + isDirty: boolean, newMetadata?: NotebookDocumentMetadata): void { + const document = this.notebooksAndEditors.getNotebookDocument(URI.from(uri)); + const e = document.acceptModelChanged(event, isDirty, newMetadata); + this.didChangeNotebookDocumentEmitter.fire(e); + } + + $acceptDirtyStateChanged(uri: UriComponents, isDirty: boolean): void { + const document = this.notebooksAndEditors.getNotebookDocument(URI.from(uri)); + document.acceptDirty(isDirty); + } + + $acceptModelSaved(uri: UriComponents): void { + const document = this.notebooksAndEditors.getNotebookDocument(URI.from(uri)); + this.didSaveNotebookDocumentEmitter.fire(document.apiNotebook); + } +} diff --git a/packages/plugin-ext/src/plugin/notebook/notebook-editor.ts b/packages/plugin-ext/src/plugin/notebook/notebook-editor.ts new file mode 100644 index 0000000000000..5bc65a14ff60c --- /dev/null +++ b/packages/plugin-ext/src/plugin/notebook/notebook-editor.ts @@ -0,0 +1,116 @@ +// ***************************************************************************** +// 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 * as theia from '@theia/plugin'; +import { NotebookDocument } from './notebook-document'; + +export class NotebookEditor { + + 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( + readonly id: string, + // private readonly _proxy: MainThreadNotebookEditorsShape, + readonly notebookData: NotebookDocument, + visibleRanges: theia.NotebookRange[], + selections: theia.NotebookRange[], + viewColumn: theia.ViewColumn | undefined + ) { + this.selections = selections; + this.visibleRanges = visibleRanges; + this.viewColumn = viewColumn; + } + + get apiEditor(): theia.NotebookEditor { + if (!this.editor) { + const that = this; + this.editor = { + get notebook(): theia.NotebookDocument { + return that.notebookData.apiNotebook; + }, + get selection(): theia.NotebookRange { + return that.selections[0]; + }, + set selection(selection: theia.NotebookRange) { + this.selections = [selection]; + }, + get selections(): theia.NotebookRange[] { + return that.selections; + }, + set selections(value: theia.NotebookRange[]) { + // if (!Array.isArray(value) || !value.every(extHostTypes.NotebookRange.isNotebookRange)) { + // throw illegalArgument('selections'); + // } + that.selections = value; + that.trySetSelections(value); + }, + get visibleRanges(): theia.NotebookRange[] { + return that.visibleRanges; + }, + revealRange(range, revealType): void { + // that._proxy.$tryRevealRange( + // that.id, + // extHostConverter.NotebookRange.from(range), + // revealType ?? extHostTypes.NotebookEditorRevealType.Default + // ); + }, + get viewColumn(): theia.ViewColumn | undefined { + return that.viewColumn; + }, + }; + + 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; + } + + acceptSelections(selections: theia.NotebookRange[]): void { + this.selections = selections; + } + + private trySetSelections(value: theia.NotebookRange[]): void { + // NB Unimplemented: implement "selections" + // this._proxy.$trySetSelections(this.id, value.map(extHostConverter.NotebookRange.from)); + } + + acceptViewColumn(value: theia.ViewColumn | undefined): void { + this.viewColumn = value; + } +} diff --git a/packages/plugin-ext/src/plugin/notebook/notebook-editors.ts b/packages/plugin-ext/src/plugin/notebook/notebook-editors.ts new file mode 100644 index 0000000000000..1d1ec3e4ae8a5 --- /dev/null +++ b/packages/plugin-ext/src/plugin/notebook/notebook-editors.ts @@ -0,0 +1,71 @@ +// ***************************************************************************** +// 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 { Emitter } from '@theia/core'; +import { NotebookEditorPropertiesChangeData, NotebookEditorsExt, NotebookEditorViewColumnInfo } from '../../common'; +import * as typeConverters from '../type-converters'; +import * as theia from '@theia/plugin'; +import { NotebooksExtImpl } from './notebooks'; + +export class NotebookEditorsExtImpl implements NotebookEditorsExt { + + private readonly DidChangeNotebookEditorSelectionEmitter = new Emitter(); + private readonly DidChangeNotebookEditorVisibleRangesEmitter = new Emitter(); + + readonly onDidChangeNotebookEditorSelection = this.DidChangeNotebookEditorSelectionEmitter.event; + readonly onDidChangeNotebookEditorVisibleRanges = this.DidChangeNotebookEditorVisibleRangesEmitter.event; + + constructor( + private readonly notebooksAndEditors: NotebooksExtImpl, + ) { } + + $acceptEditorPropertiesChanged(id: string, data: NotebookEditorPropertiesChangeData): void { + const editor = this.notebooksAndEditors.getEditorById(id); + // ONE: make all state updates + if (data.visibleRanges) { + editor.acceptVisibleRanges(data.visibleRanges.ranges.map(typeConverters.NotebookRange.to)); + } + if (data.selections) { + editor.acceptSelections(data.selections.selections.map(typeConverters.NotebookRange.to)); + } + + // TWO: send all events after states have been updated + if (data.visibleRanges) { + this.DidChangeNotebookEditorVisibleRangesEmitter.fire({ + notebookEditor: editor.apiEditor, + visibleRanges: editor.apiEditor.visibleRanges + }); + } + if (data.selections) { + this.DidChangeNotebookEditorSelectionEmitter.fire(Object.freeze({ + notebookEditor: editor.apiEditor, + selections: editor.apiEditor.selections + })); + } + } + + $acceptEditorViewColumns(data: NotebookEditorViewColumnInfo): void { + // eslint-disable-next-line guard-for-in + for (const id in data) { + const editor = this.notebooksAndEditors.getEditorById(id); + editor.acceptViewColumn(typeConverters.ViewColumn.to(data[id])); + } + } +} diff --git a/packages/plugin-ext/src/plugin/notebook/notebook-kernels.ts b/packages/plugin-ext/src/plugin/notebook/notebook-kernels.ts new file mode 100644 index 0000000000000..4f0e2d78ab8ef --- /dev/null +++ b/packages/plugin-ext/src/plugin/notebook/notebook-kernels.ts @@ -0,0 +1,616 @@ +// ***************************************************************************** +// 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 { CancellationToken } from '@theia/plugin'; +import { + CellExecuteUpdateDto, NotebookKernelDto, NotebookKernelsExt, NotebookKernelsMain, NotebookKernelSourceActionDto, NotebookOutputDto, PLUGIN_RPC_CONTEXT +} from '../../common'; +import { RPCProtocol } from '../../common/rpc-protocol'; +import { UriComponents } from '../../common/uri-components'; +import * as theia from '@theia/plugin'; +import { CancellationTokenSource, Disposable, DisposableCollection, Emitter } from '@theia/core'; +import { Cell } from './notebook-document'; +import { NotebooksExtImpl } from './notebooks'; +import { NotebookCellOutputConverter, 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 { NotebookCellOutput, NotebookRendererScript, URI } from '../types-impl'; + +interface KernelData { + extensionId: string; + controller: theia.NotebookController; + onDidChangeSelection: Emitter<{ selected: boolean; notebook: theia.NotebookDocument }>; + onDidReceiveMessage: Emitter<{ editor: theia.NotebookEditor; message: unknown }>; + associatedNotebooks: Map; +} + +export class NotebookKernelsExtImpl implements NotebookKernelsExt { + + private readonly activeExecutions = new Map(); + + private readonly kernelData = new Map(); + + 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 readonly notebooks: NotebooksExtImpl, + private readonly commands: CommandRegistryImpl + ) { + this.proxy = rpc.getProxy(PLUGIN_RPC_CONTEXT.NOTEBOOK_KERNELS_MAIN); + } + + private currentHandle = 0; + + createNotebookController(extensionId: string, id: string, viewType: string, label: string, handler?: (cells: theia.NotebookCell[], + 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) { + throw new Error(`notebook controller with id '${id}' ALREADY exist`); + } + } + + const handle = this.currentHandle++; + const that = this; + + console.debug(`NotebookController[${handle}], CREATED by ${extensionId}, ${id}`); + + const defaultExecuteHandler = () => console.warn(`NO execute handler from notebook controller '${data.id}' of extension: '${extensionId}'`); + + let isDisposed = false; + const commandDisposables = new DisposableCollection(); + + const onDidChangeSelection = new Emitter<{ selected: boolean; notebook: theia.NotebookDocument }>(); + const onDidReceiveMessage = new Emitter<{ editor: theia.NotebookEditor; message: unknown }>(); + + const data: NotebookKernelDto = { + id: createKernelId(extensionId, id), + notebookType: viewType, + extensionId: extensionId, + label: label || extensionId, + }; + + // + let executeHandler = handler ?? defaultExecuteHandler; + let interruptHandler: ((this: theia.NotebookController, notebook: theia.NotebookDocument) => void | Thenable) | undefined; + + this.proxy.$addKernel(handle, data).catch(err => { + // this can happen when a kernel with that ID is already registered + console.log(err); + isDisposed = true; + }); + + // update: all setters write directly into the dto object + // and trigger an update. the actual update will only happen + // once per event loop execution + let tokenPool = 0; + const update = () => { + if (isDisposed) { + return; + } + const myToken = ++tokenPool; + Promise.resolve().then(() => { + if (myToken === tokenPool) { + this.proxy.$updateKernel(handle, data); + } + }); + }; + + // notebook documents that are associated to this controller + const associatedNotebooks = new Map(); + + const controller: theia.NotebookController = { + get id(): string { return id; }, + get notebookType(): string { return data.notebookType; }, + onDidChangeSelectedNotebooks: onDidChangeSelection.event, + onDidReceiveMessage: onDidReceiveMessage.event, + get label(): string { + return data.label; + }, + set label(value) { + data.label = value ?? extensionId; + update(); + }, + get detail(): string { + return data.detail ?? ''; + }, + set detail(value) { + data.detail = value; + update(); + }, + get description(): string { + return data.description ?? ''; + }, + set description(value) { + data.description = value; + update(); + }, + get supportedLanguages(): string[] | undefined { + return data.supportedLanguages; + }, + set supportedLanguages(value) { + data.supportedLanguages = value; + update(); + }, + get supportsExecutionOrder(): boolean { + return data.supportsExecutionOrder ?? false; + }, + set supportsExecutionOrder(value) { + 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; + }, + set executeHandler(value) { + executeHandler = value ?? defaultExecuteHandler; + }, + get interruptHandler(): ((this: theia.NotebookController, notebook: theia.NotebookDocument) => void | Thenable) | undefined { + return interruptHandler; + }, + set interruptHandler(value) { + interruptHandler = value; + data.supportsInterrupt = Boolean(value); + update(); + }, + createNotebookCellExecution(cell): theia.NotebookCellExecution { + if (isDisposed) { + throw new Error('notebook controller is DISPOSED'); + } + if (!associatedNotebooks.has(cell.notebook.uri.toString())) { + console.debug(`NotebookController[${handle}] NOT associated to notebook, associated to THESE notebooks:`, + Array.from(associatedNotebooks.keys()).map(u => u.toString())); + throw new Error(`notebook controller is NOT associated to notebook: ${cell.notebook.uri.toString()}`); + } + return that.createNotebookCellExecution(cell, createKernelId(extensionId, this.id)); + }, + dispose: () => { + if (!isDisposed) { + console.debug(`NotebookController[${handle}], DISPOSED`); + isDisposed = true; + this.kernelData.delete(handle); + commandDisposables.dispose(); + onDidChangeSelection.dispose(); + onDidReceiveMessage.dispose(); + this.proxy.$removeKernel(handle); + } + }, + 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, { + extensionId: extensionId, + controller, + onDidReceiveMessage, + onDidChangeSelection, + associatedNotebooks + }); + return controller; + } + + createNotebookCellExecution(cell: theia.NotebookCell, controllerId: string): theia.NotebookCellExecution { + if (cell.index < 0) { + throw new Error('CANNOT execute cell that has been REMOVED from notebook'); + } + const notebook = this.notebooks.getNotebookDocument(URI.from(cell.notebook.uri)); + const cellObj = notebook.getCellFromApiCell(cell); + if (!cellObj) { + throw new Error('invalid cell'); + } + if (this.activeExecutions.has(cellObj.uri.toString())) { + throw new Error(`duplicate execution for ${cellObj.uri}`); + } + const execution = new NotebookCellExecutionTask(controllerId, cellObj, this.proxy); + this.activeExecutions.set(cellObj.uri.toString(), execution); + const listener = execution.onDidChangeState(() => { + if (execution.state === NotebookCellExecutionTaskState.Resolved) { + execution.dispose(); + listener.dispose(); + this.activeExecutions.delete(cellObj.uri.toString()); + } + }); + 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) { + // update data structure + const notebook = this.notebooks.getNotebookDocument(URI.from(uri))!; + if (value) { + obj.associatedNotebooks.set(notebook.uri.toString(), true); + } else { + obj.associatedNotebooks.delete(notebook.uri.toString()); + } + console.debug(`NotebookController[${handle}] ASSOCIATE notebook`, notebook.uri.toString(), value); + // send event + obj.onDidChangeSelection.fire({ + selected: value, + notebook: notebook.apiNotebook + }); + } + + } + + async $executeCells(handle: number, uri: UriComponents, handles: number[]): Promise { + const obj = this.kernelData.get(handle); + if (!obj) { + // extension can dispose kernels in the meantime + return Promise.resolve(); + } + const document = this.notebooks.getNotebookDocument(URI.from(uri)); + const cells: theia.NotebookCell[] = []; + for (const cellHandle of handles) { + const cell = document.getCell(cellHandle); + if (cell) { + cells.push(cell.apiCell); + } + } + + try { + console.debug(`NotebookController[${handle}] EXECUTE cells`, document.uri.toString(), cells.length); + await obj.controller.executeHandler.call(obj.controller, cells, document.apiNotebook, obj.controller); + } catch (err) { + console.error(`NotebookController[${handle}] execute cells FAILED`, err); + console.error(err); + } + + } + + async $cancelCells(handle: number, uri: UriComponents, handles: number[]): Promise { + const obj = this.kernelData.get(handle); + if (!obj) { + // extension can dispose kernels in the meantime + return Promise.resolve(); + } + + // cancel or interrupt depends on the controller. When an interrupt handler is used we + // don't trigger the cancelation token of executions.N + const document = this.notebooks.getNotebookDocument(URI.from(uri)); + if (obj.controller.interruptHandler) { + await obj.controller.interruptHandler.call(obj.controller, document.apiNotebook); + + } else { + for (const cellHandle of handles) { + const cell = document.getCell(cellHandle); + if (cell) { + this.activeExecutions.get(cell.uri.toString())?.cancel(); + } + } + } + } + + $acceptKernelMessageFromRenderer(handle: number, editorId: string, message: unknown): void { + const obj = this.kernelData.get(handle); + if (!obj) { + // extension can dispose kernels in the meantime + return; + } + + const editor = this.notebooks.getEditorById(editorId); + obj.onDidReceiveMessage.fire(Object.freeze({ editor: editor.apiEditor, message })); + } + + $cellExecutionChanged(uri: UriComponents, cellHandle: number, state: NotebookCellExecutionState | undefined): void { + // Proposed Api though seems needed by jupyter for telemetry + } + + 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 []; + + } + +} + +enum NotebookCellExecutionTaskState { + Init, + Started, + Resolved +} + +class NotebookCellExecutionTask implements Disposable { + private static HANDLE = 0; + private _handle = NotebookCellExecutionTask.HANDLE++; + + private _onDidChangeState = new Emitter(); + readonly onDidChangeState = this._onDidChangeState.event; + + private _state = NotebookCellExecutionTaskState.Init; + get state(): NotebookCellExecutionTaskState { return this._state; } + + private readonly tokenSource = new CancellationTokenSource(); + + private readonly collector: TimeoutBasedCollector; + + private executionOrder: number | undefined; + + constructor( + controllerId: string, + private readonly cell: Cell, + private readonly proxy: NotebookKernelsMain + ) { + this.collector = new TimeoutBasedCollector(10, updates => this.update(updates)); + + this.executionOrder = cell.internalMetadata.executionOrder; + this.proxy.$createExecution(this._handle, controllerId, this.cell.notebookDocument.uri, this.cell.handle); + } + + cancel(): void { + this.tokenSource.cancel(); + } + + private async updateSoon(update: CellExecuteUpdateDto): Promise { + await this.collector.addItem(update); + } + + private async update(update: CellExecuteUpdateDto | CellExecuteUpdateDto[]): Promise { + const updates = Array.isArray(update) ? update : [update]; + return this.proxy.$updateExecution(this._handle, updates); + } + + private verifyStateForOutput(): void { + if (this._state === NotebookCellExecutionTaskState.Init) { + throw new Error('Must call start before modifying cell output'); + } + + if (this._state === NotebookCellExecutionTaskState.Resolved) { + throw new Error('Cannot modify cell output after calling resolve'); + } + } + + private cellIndexToHandle(cellOrCellIndex: theia.NotebookCell | undefined): number { + let cell: Cell | undefined = this.cell; + if (cellOrCellIndex) { + cell = this.cell.notebookDocument.getCellFromApiCell(cellOrCellIndex); + } + if (!cell) { + throw new Error('INVALID cell'); + } + return cell.handle; + } + + private validateAndConvertOutputs(items: NotebookCellOutput[]): NotebookOutputDto[] { + return items.map(output => { + const newOutput = NotebookCellOutputConverter.ensureUniqueMimeTypes(output.items, true); + if (newOutput === output.items) { + return NotebookCellOutputConverter.from(output); + } + return NotebookCellOutputConverter.from({ + items: newOutput, + outputId: output.outputId, + metadata: output.metadata + }); + }); + } + + private async updateOutputs(outputs: NotebookCellOutput | NotebookCellOutput[], cell: theia.NotebookCell | undefined, append: boolean): Promise { + const handle = this.cellIndexToHandle(cell); + const outputDtos = this.validateAndConvertOutputs(Array.isArray(outputs) ? outputs : [outputs]); + return this.updateSoon( + { + editType: CellExecutionUpdateType.Output, + cellHandle: handle, + append, + outputs: outputDtos + }); + } + + private async updateOutputItems(items: theia.NotebookCellOutputItem | theia.NotebookCellOutputItem[], output: theia.NotebookCellOutput, append: boolean): Promise { + items = NotebookCellOutputConverter.ensureUniqueMimeTypes(Array.isArray(items) ? items : [items], true); + return this.updateSoon({ + editType: CellExecutionUpdateType.OutputItems, + items: items.map(NotebookCellOutputItem.from), + append + }); + } + + asApiObject(): theia.NotebookCellExecution { + const that = this; + const result: theia.NotebookCellExecution = { + get token(): CancellationToken { return that.tokenSource.token; }, + get cell(): theia.NotebookCell { return that.cell.apiCell; }, + get executionOrder(): number | undefined { return that.executionOrder; }, + set executionOrder(v: number | undefined) { + that.executionOrder = v; + that.update([{ + editType: CellExecutionUpdateType.ExecutionState, + executionOrder: that.executionOrder + }]); + }, + + start(startTime?: number): void { + if (that._state === NotebookCellExecutionTaskState.Resolved || that._state === NotebookCellExecutionTaskState.Started) { + throw new Error('Cannot call start again'); + } + + that._state = NotebookCellExecutionTaskState.Started; + that._onDidChangeState.fire(); + + that.update({ + editType: CellExecutionUpdateType.ExecutionState, + runStartTime: startTime + }); + }, + + end(success: boolean | undefined, endTime?: number): void { + if (that._state === NotebookCellExecutionTaskState.Resolved) { + throw new Error('Cannot call resolve twice'); + } + + that._state = NotebookCellExecutionTaskState.Resolved; + that._onDidChangeState.fire(); + + // The last update needs to be ordered correctly and applied immediately, + // so we use updateSoon and immediately flush. + that.collector.flush(); + + that.proxy.$completeExecution(that._handle, { + runEndTime: endTime, + lastRunSuccess: success + }); + }, + + clearOutput(cell?: theia.NotebookCell): Thenable { + that.verifyStateForOutput(); + return that.updateOutputs([], cell, false); + }, + + appendOutput(outputs: NotebookCellOutput | NotebookCellOutput[], cell?: theia.NotebookCell): Promise { + that.verifyStateForOutput(); + return that.updateOutputs(outputs, cell, true); + }, + + replaceOutput(outputs: NotebookCellOutput | NotebookCellOutput[], cell?: theia.NotebookCell): Promise { + that.verifyStateForOutput(); + return that.updateOutputs(outputs, cell, false); + }, + + appendOutputItems(items: theia.NotebookCellOutputItem | theia.NotebookCellOutputItem[], output: theia.NotebookCellOutput): Promise { + that.verifyStateForOutput(); + return that.updateOutputItems(items, output, true); + }, + + replaceOutputItems(items: theia.NotebookCellOutputItem | theia.NotebookCellOutputItem[], output: theia.NotebookCellOutput): Promise { + that.verifyStateForOutput(); + return that.updateOutputItems(items, output, false); + } + }; + return Object.freeze(result); + } + + dispose(): void { + + } +} + +class TimeoutBasedCollector { + private batch: T[] = []; + private startedTimer = Date.now(); + private currentDeferred: Deferred | undefined; + + constructor( + private readonly delay: number, + private readonly callback: (items: T[]) => Promise) { } + + addItem(item: T): Promise { + this.batch.push(item); + if (!this.currentDeferred) { + this.currentDeferred = new Deferred(); + this.startedTimer = Date.now(); + timeout(this.delay).then(() => this.flush()); + } + + // This can be called by the extension repeatedly for a long time before the timeout is able to run. + // Force a flush after the delay. + if (Date.now() - this.startedTimer > this.delay) { + return this.flush(); + } + + return this.currentDeferred.promise; + } + + flush(): Promise { + if (this.batch.length === 0 || !this.currentDeferred) { + return Promise.resolve(); + } + + const deferred = this.currentDeferred; + this.currentDeferred = undefined; + const batch = this.batch; + this.batch = []; + return this.callback(batch) + .finally(() => deferred.resolve()); + } +} + +export function createKernelId(extensionIdentifier: string, id: string): string { + return `${extensionIdentifier}/${id}`; +} diff --git a/packages/plugin-ext/src/plugin/notebook/notebook-renderers.ts b/packages/plugin-ext/src/plugin/notebook/notebook-renderers.ts new file mode 100644 index 0000000000000..cbb66bb0e69f0 --- /dev/null +++ b/packages/plugin-ext/src/plugin/notebook/notebook-renderers.ts @@ -0,0 +1,72 @@ +// ***************************************************************************** +// 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 { NotebookRenderersExt, NotebookRenderersMain, PLUGIN_RPC_CONTEXT } from '../../common'; +import { RPCProtocol } from '../../common/rpc-protocol'; +import { NotebooksExtImpl } from './notebooks'; +import * as theia from '@theia/plugin'; +import { NotebookEditor } from './notebook-editor'; +import { Emitter } from '@theia/core'; + +export class NotebookRenderersExtImpl implements NotebookRenderersExt { + private readonly rendererMessageEmitters = new Map>(); + private readonly proxy: NotebookRenderersMain; + + constructor(rpc: RPCProtocol, private readonly notebooksExt: NotebooksExtImpl) { + this.proxy = rpc.getProxy(PLUGIN_RPC_CONTEXT.NOTEBOOK_RENDERERS_MAIN); + } + + public $postRendererMessage(editorId: string, rendererId: string, message: unknown): void { + const editor = this.notebooksExt.getEditorById(editorId); + this.rendererMessageEmitters.get(rendererId)?.fire({ editor: editor.apiEditor, message }); + } + + public createRendererMessaging(rendererId: string): theia.NotebookRendererMessaging { + + const messaging: theia.NotebookRendererMessaging = { + onDidReceiveMessage: (listener, thisArg, disposables) => this.getOrCreateEmitterFor(rendererId).event(listener, thisArg, disposables), + postMessage: (message, editorOrAlias) => { + + const extHostEditor = editorOrAlias && NotebookEditor.apiEditorsToExtHost.get(editorOrAlias); + return this.proxy.$postMessage(extHostEditor?.id, rendererId, message); + }, + }; + + return messaging; + } + + private getOrCreateEmitterFor(rendererId: string): Emitter<{ editor: theia.NotebookEditor, message: unknown }> { + let emitter = this.rendererMessageEmitters.get(rendererId); + if (emitter) { + return emitter; + } + + emitter = new Emitter({ + onLastListenerRemove: () => { + emitter?.dispose(); + this.rendererMessageEmitters.delete(rendererId); + } + }); + + this.rendererMessageEmitters.set(rendererId, emitter); + + return emitter; + } +} diff --git a/packages/plugin-ext/src/plugin/notebook/notebooks.ts b/packages/plugin-ext/src/plugin/notebook/notebooks.ts new file mode 100644 index 0000000000000..d27b3df60b36b --- /dev/null +++ b/packages/plugin-ext/src/plugin/notebook/notebooks.ts @@ -0,0 +1,385 @@ +// ***************************************************************************** +// 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 { CancellationToken, Disposable, DisposableCollection, Emitter, Event, URI } from '@theia/core'; +import { URI as TheiaURI } from '../types-impl'; +import * as theia from '@theia/plugin'; +import { + CommandRegistryExt, NotebookCellStatusBarListDto, NotebookDataDto, + NotebookDocumentsAndEditorsDelta, NotebookDocumentShowOptions, NotebookDocumentsMain, NotebookEditorAddData, NotebookEditorsMain, NotebooksExt, NotebooksMain, Plugin, + PLUGIN_RPC_CONTEXT +} from '../../common'; +import { Cache } from '../../common/cache'; +import { RPCProtocol } from '../../common/rpc-protocol'; +import { UriComponents } from '../../common/uri-components'; +import { CommandsConverter } from '../command-registry'; +import * as typeConverters from '../type-converters'; +import { BinaryBuffer } from '@theia/core/lib/common/buffer'; +import { NotebookDocument } from './notebook-document'; +import { NotebookEditor } from './notebook-editor'; +import { EditorsAndDocumentsExtImpl } from '../editors-and-documents'; +import { DocumentsExtImpl } from '../documents'; + +export class NotebooksExtImpl implements NotebooksExt { + + private readonly notebookStatusBarItemProviders = new Map(); + private readonly commandsConverter: CommandsConverter; + + private readonly onDidChangeActiveNotebookEditorEmitter = new Emitter(); + readonly onDidChangeActiveNotebookEditor = this.onDidChangeActiveNotebookEditorEmitter.event; + + private readonly onDidOpenNotebookDocumentEmitter = new Emitter(); + onDidOpenNotebookDocument: Event = this.onDidOpenNotebookDocumentEmitter.event; + private readonly onDidCloseNotebookDocumentEmitter = new Emitter(); + onDidCloseNotebookDocument: Event = this.onDidCloseNotebookDocumentEmitter.event; + + private readonly onDidChangeVisibleNotebookEditorsEmitter = new Emitter(); + onDidChangeVisibleNotebookEditors = this.onDidChangeVisibleNotebookEditorsEmitter.event; + + 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; + private notebookEditors: NotebookEditorsMain; + + constructor( + rpc: RPCProtocol, + commands: CommandRegistryExt, + private textDocumentsAndEditors: EditorsAndDocumentsExtImpl, + private textDocuments: DocumentsExtImpl + ) { + this.notebookProxy = rpc.getProxy(PLUGIN_RPC_CONTEXT.NOTEBOOKS_MAIN); + this.notebookDocumentsProxy = rpc.getProxy(PLUGIN_RPC_CONTEXT.NOTEBOOK_DOCUMENTS_MAIN); + this.notebookEditors = rpc.getProxy(PLUGIN_RPC_CONTEXT.NOTEBOOK_EDITORS_MAIN); + + 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.toString()); + if (!document || !provider) { + return; + } + + const cell = document.getCellFromIndex(index); + if (!cell) { + return; + } + + const result = await provider.provideCellStatusBarItems(cell.apiCell, token); + if (!result) { + return undefined; + } + + const disposables = new DisposableCollection(); + const cacheId = this.statusBarRegistry.add([disposables]); + const resultArr = Array.isArray(result) ? result : [result]; + const items = resultArr.map(item => typeConverters.NotebookStatusBarItem.from(item, this.commandsConverter, disposables)); + return { + cacheId, + items + }; + } + + $releaseNotebookCellStatusBarItems(cacheId: number): void { + this.statusBarRegistry.delete(cacheId); + } + + // --- serialize/deserialize + + private currentSerializerHandle = 0; + private readonly notebookSerializer = new Map(); + + registerNotebookSerializer(plugin: Plugin, viewType: string, serializer: theia.NotebookSerializer, + options?: theia.NotebookDocumentContentOptions): theia.Disposable { + if (!viewType || !viewType.trim()) { + throw new Error('viewType cannot be empty or just whitespace'); + } + const handle = this.currentSerializerHandle++; + this.notebookSerializer.set(handle, serializer); + this.notebookProxy.$registerNotebookSerializer( + handle, + { id: plugin.model.id, location: plugin.pluginUri }, + viewType, + typeConverters.NotebookDocumentContentOptions.from(options), + ); + return Disposable.create(() => { + this.notebookProxy.$unregisterNotebookSerializer(handle); + }); + } + + async $dataToNotebook(handle: number, bytes: BinaryBuffer, token: CancellationToken): Promise { + const serializer = this.notebookSerializer.get(handle); + if (!serializer) { + throw new Error('No serializer found'); + } + const data = await serializer.deserializeNotebook(bytes.buffer, token); + return typeConverters.NotebookData.from(data); + } + + async $notebookToData(handle: number, data: NotebookDataDto, token: CancellationToken): Promise { + const serializer = this.notebookSerializer.get(handle); + if (!serializer) { + throw new Error('No serializer found'); + } + const bytes = await serializer.serializeNotebook(typeConverters.NotebookData.to(data), token); + return BinaryBuffer.wrap(bytes); + } + + registerNotebookCellStatusBarItemProvider(notebookType: string, provider: theia.NotebookCellStatusBarItemProvider): theia.Disposable { + + const handle = this.currentSerializerHandle++; + const eventHandle = typeof provider.onDidChangeCellStatusBarItems === 'function' ? this.currentSerializerHandle++ : undefined; + + this.notebookStatusBarItemProviders.set(handle, provider); + this.notebookProxy.$registerNotebookCellStatusBarItemProvider(handle, eventHandle, notebookType); + + let subscription: theia.Disposable | undefined; + if (eventHandle !== undefined) { + subscription = provider.onDidChangeCellStatusBarItems!(_ => this.notebookProxy.$emitCellStatusBarEvent(eventHandle)); + } + + return Disposable.create(() => { + this.notebookStatusBarItemProviders.delete(handle); + this.notebookProxy.$unregisterNotebookCellStatusBarItemProvider(handle, eventHandle); + subscription?.dispose(); + }); + } + + getEditorById(editorId: string): NotebookEditor { + const editor = this.editors.get(editorId); + if (!editor) { + throw new Error(`unknown text editor: ${editorId}. known editors: ${[...this.editors.keys()]} `); + } + return editor; + } + + getAllApiDocuments(): theia.NotebookDocument[] { + return [...this.documents.values()].map(doc => doc.apiNotebook); + } + + 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.onDidCloseNotebookDocumentEmitter.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) { + 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, + this.textDocuments, + uri, + modelData + ); + + this.documents.get(uri.toString())?.dispose(); + this.documents.set(uri.toString(), document); + + this.onDidOpenNotebookDocumentEmitter.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.onDidChangeVisibleNotebookEditorsEmitter.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.onDidChangeActiveNotebookEditorEmitter.fire(this.activeNotebookEditor?.apiEditor); + } + } + + getNotebookDocument(uri: TheiaURI, relaxed: true): NotebookDocument | undefined; + getNotebookDocument(uri: TheiaURI): NotebookDocument; + getNotebookDocument(uri: TheiaURI, relaxed?: true): NotebookDocument | undefined { + 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 EXISTS: ${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); + } + + async createNotebookDocument(options: { viewType: string; content?: theia.NotebookData }): Promise { + const canonicalUri = await this.notebookDocumentsProxy.$tryCreateNotebook({ + viewType: options.viewType, + content: options.content && typeConverters.NotebookData.from(options.content) + }); + return TheiaURI.from(canonicalUri); + } + + async openNotebookDocument(uri: TheiaURI): Promise { + const cached = this.documents.get(uri.toString()); + if (cached) { + return cached.apiNotebook; + } + const canonicalUri = await this.notebookDocumentsProxy.$tryOpenNotebook(uri); + const document = this.documents.get(URI.fromComponents(canonicalUri).toString()); + return document?.apiNotebook!; + } + + async showNotebookDocument(notebookOrUri: theia.NotebookDocument | TheiaURI, options?: theia.NotebookDocumentShowOptions): Promise { + + if (URI.isUri(notebookOrUri)) { + notebookOrUri = await this.openNotebookDocument(notebookOrUri as TheiaURI); + } + + const notebook = notebookOrUri as theia.NotebookDocument; + + let resolvedOptions: NotebookDocumentShowOptions; + if (typeof options === 'object') { + resolvedOptions = { + position: typeConverters.ViewColumn.from(options.viewColumn), + preserveFocus: options.preserveFocus, + selections: options.selections && options.selections.map(typeConverters.NotebookRange.from), + pinned: typeof options.preview === 'boolean' ? !options.preview : undefined + }; + } else { + resolvedOptions = { + preserveFocus: false + }; + } + + const editorId = await this.notebookEditors.$tryShowNotebookDocument(notebook.uri, notebook.notebookType, resolvedOptions); + const editor = editorId && this.editors.get(editorId)?.apiEditor; + + if (editor) { + return editor; + } + + if (editorId) { + throw new Error(`Could NOT open editor for "${notebook.uri.toString()}" because another editor opened in the meantime.`); + } else { + throw new Error(`Could NOT open editor for "${notebook.uri.toString()}".`); + } + } + +} diff --git a/packages/plugin-ext/src/plugin/plugin-context.ts b/packages/plugin-ext/src/plugin/plugin-context.ts index 90ca33946d3dc..f3ecf5dbb862c 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, @@ -174,10 +174,11 @@ import { NotebookCellOutput, NotebookCellOutputItem, NotebookData, - NotebookDocument, NotebookRange, NotebookCellStatusBarItem, NotebookEdit, + NotebookKernelSourceAction, + NotebookRendererScript, TestRunProfileKind, TestTag, TestRunRequest, @@ -242,7 +243,13 @@ import { Endpoint } from '@theia/core/lib/browser/endpoint'; import { FilePermission } from '@theia/filesystem/lib/common/files'; import { TabsExtImpl } from './tabs'; import { LocalizationExtImpl } from './localization-ext'; +import { NotebooksExtImpl } from './notebook/notebooks'; import { TelemetryExtImpl } from './telemetry-ext'; +import { NotebookDocument } from './notebook/notebook-document'; +import { NotebookRenderersExtImpl } from './notebook/notebook-renderers'; +import { NotebookKernelsExtImpl } from './notebook/notebook-kernels'; +import { NotebookDocumentsExtImpl } from './notebook/notebook-documents'; +import { NotebookEditorsExtImpl } from './notebook/notebook-editors'; export function createAPIFactory( rpc: RPCProtocol, @@ -266,6 +273,11 @@ 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, commandRegistry, editorsAndDocumentsExt, documents)); + const notebookEditors = rpc.set(MAIN_RPC_CONTEXT.NOTEBOOK_EDITORS_EXT, new NotebookEditorsExtImpl(notebooksExt)); + 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, commandRegistry)); + const notebookDocuments = rpc.set(MAIN_RPC_CONTEXT.NOTEBOOK_DOCUMENTS_EXT, new NotebookDocumentsExtImpl(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)); @@ -427,24 +439,24 @@ export function createAPIFactory( } }, get visibleNotebookEditors(): theia.NotebookEditor[] { - return [] as theia.NotebookEditor[]; + return notebooksExt.visibleApiNotebookEditors; }, onDidChangeVisibleNotebookEditors(listener, thisArg?, disposables?) { - return Disposable.NULL; + return notebooksExt.onDidChangeVisibleNotebookEditors(listener, thisArg, disposables); }, 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; + return notebookEditors.onDidChangeNotebookEditorSelection(listener, thisArg, disposables); }, onDidChangeNotebookEditorVisibleRanges(listener, thisArg?, disposables?) { - return Disposable.NULL; + return notebookEditors.onDidChangeNotebookEditorVisibleRanges(listener, thisArg, disposables); }, - showNotebookDocument(document: NotebookDocument, options?: theia.NotebookDocumentShowOptions) { - return Promise.resolve({} as theia.NotebookEditor); + showNotebookDocument(document: theia.NotebookDocument, options?: theia.NotebookDocumentShowOptions) { + return notebooksExt.showNotebookDocument(document, options); }, // eslint-disable-next-line @typescript-eslint/no-explicit-any showQuickPick(items: any, options?: theia.QuickPickOptions, token?: theia.CancellationToken): any { @@ -616,7 +628,7 @@ export function createAPIFactory( return workspaceExt.onDidChangeWorkspaceFolders(listener, thisArg, disposables); }, get notebookDocuments(): theia.NotebookDocument[] { - return [] as theia.NotebookDocument[]; + return notebooksExt.getAllApiDocuments(); }, get textDocuments(): theia.TextDocument[] { return documents.getAllDocumentData().map(data => data.document); @@ -628,19 +640,19 @@ export function createAPIFactory( return documents.onDidRemoveDocument(listener, thisArg, disposables); }, onDidOpenNotebookDocument(listener, thisArg?, disposables?) { - return Disposable.NULL; + return notebooksExt.onDidOpenNotebookDocument(listener, thisArg, disposables); }, onDidCloseNotebookDocument(listener, thisArg?, disposables?) { - return Disposable.NULL; - }, - onDidChangeNotebookDocument(listener, thisArg?, disposables?) { - return Disposable.NULL; + return notebooksExt.onDidCloseNotebookDocument(listener, thisArg, disposables); }, onWillSaveNotebookDocument(listener, thisArg?, disposables?) { return Disposable.NULL; }, - onDidSaveNotebookDocument(listener, thisArg?, disposables?) { - return Disposable.NULL; + onDidSaveNotebookDocument(listener, thisArg, disposables) { + return notebookDocuments.onDidSaveNotebookDocument(listener, thisArg, disposables); + }, + onDidChangeNotebookDocument(listener, thisArg, disposables) { + return notebookDocuments.onDidChangeNotebookDocument(listener, thisArg, disposables); }, onDidOpenTextDocument(listener, thisArg?, disposables?) { return documents.onDidAddDocument(listener, thisArg, disposables); @@ -686,8 +698,18 @@ export function createAPIFactory( const data = await documents.openDocument(uri); return data && data.document; }, - openNotebookDocument(uriOrString: theia.Uri | string, content?: NotebookData): Promise { - return Promise.reject(new Error('Notebook API is stubbed')); + async openNotebookDocument(uriOrType: theia.Uri | string, content?: NotebookData): Promise { + let uri: URI; + if (URI.isUri(uriOrType)) { + uri = uriOrType; + await notebooksExt.openNotebookDocument(uriOrType as URI); + } else if (typeof uriOrType === 'string') { + uri = URI.revive(await notebooksExt.createNotebookDocument({ viewType: uriOrType, content })); + } else { + throw new Error('Invalid arguments'); + } + return notebooksExt.getNotebookDocument(uri).apiNotebook; + }, createFileSystemWatcher: (pattern, ignoreCreate, ignoreChange, ignoreDelete): theia.FileSystemWatcher => extHostFileSystemEvent.createFileSystemWatcher(fromGlobPattern(pattern), ignoreCreate, ignoreChange, ignoreDelete), @@ -729,7 +751,7 @@ export function createAPIFactory( return timelineExt.registerTimelineProvider(plugin, scheme, provider); }, registerNotebookSerializer(notebookType: string, serializer: theia.NotebookSerializer, options?: theia.NotebookDocumentContentOptions): theia.Disposable { - return Disposable.NULL; + return notebooksExt.registerNotebookSerializer(plugin, notebookType, serializer, options); }, get isTrusted(): boolean { return workspaceExt.trusted; @@ -1156,54 +1178,27 @@ export function createAPIFactory( label, handler?: (cells: theia.NotebookCell[], notebook: theia.NotebookDocument, - controller: theia.NotebookController) => void | Thenable + controller: theia.NotebookController) => void | Thenable, + rendererScripts?: NotebookRendererScript[] ) { - 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, rendererScripts); }, - createRendererMessaging( - rendererId - ) { - return { - rendererId, - onDidReceiveMessage: () => Disposable.create(() => { }), - postMessage: () => Promise.resolve({}), - }; + createRendererMessaging(rendererId) { + return notebookRenderers.createRendererMessaging(rendererId); }, registerNotebookCellStatusBarItemProvider( notebookType, provider ) { - return { - notebookType, - provider, - dispose: () => undefined, - }; + 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); } }; @@ -1357,6 +1352,7 @@ export function createAPIFactory( InlayHintLabelPart, TelemetryTrustedValue, NotebookCellData, + NotebookCellExecutionState, NotebookCellKind, NotebookCellOutput, NotebookCellOutputItem, @@ -1368,6 +1364,8 @@ export function createAPIFactory( NotebookDocument, NotebookRange, NotebookEdit, + NotebookKernelSourceAction, + NotebookRendererScript, TestRunProfileKind, TestTag, TestRunRequest, @@ -1420,6 +1418,8 @@ export interface ExtensionPlugin extends theia.Plugin { } export class Plugin implements theia.Plugin { + #pluginManager: PluginManager; + id: string; pluginPath: string; pluginUri: theia.Uri; @@ -1427,7 +1427,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; @@ -1442,26 +1444,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; @@ -1470,14 +1475,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/plugin-manager.ts b/packages/plugin-ext/src/plugin/plugin-manager.ts index 0fe8976a33568..3d3c5296bdc7a 100644 --- a/packages/plugin-ext/src/plugin/plugin-manager.ts +++ b/packages/plugin-ext/src/plugin/plugin-manager.ts @@ -43,6 +43,7 @@ import { WebviewsExtImpl } from './webviews'; import { URI as Uri } from './types-impl'; import { SecretsExtImpl, SecretStorageExt } from '../plugin/secrets-ext'; import { PluginExt } from './plugin-context'; +import { Deferred } from '@theia/core/lib/common/promise-util'; export interface PluginHost { @@ -96,7 +97,9 @@ export class PluginManagerExtImpl implements PluginManagerExt, PluginManager { 'onFileSystem', 'onCustomEditor', 'onStartupFinished', - 'onAuthenticationRequest' + 'onAuthenticationRequest', + 'onNotebook', + 'onNotebookSerializer' ]); private configStorage: ConfigStorage | undefined; @@ -115,6 +118,7 @@ export class PluginManagerExtImpl implements PluginManagerExt, PluginManager { } protected jsonValidation: PluginJsonValidationContribution[] = []; + protected ready = new Deferred(); constructor( private readonly host: PluginHost, @@ -230,6 +234,8 @@ export class PluginManagerExtImpl implements PluginManagerExt, PluginManager { this.registerPlugin(plugin); } + // ensure plugins are registered before running activation events + this.ready.resolve(); // run eager plugins await this.$activateByEvent('*'); for (const activationEvent of params.activationEvents) { @@ -333,6 +339,8 @@ export class PluginManagerExtImpl implements PluginManagerExt, PluginManager { } async $activateByEvent(activationEvent: string): Promise { + // Prevent the plugin manager from performing activations before plugins are registered + await this.ready.promise; if (activationEvent.endsWith(':*')) { const baseEvent = activationEvent.substring(0, activationEvent.length - 2); await this.activateByBaseEvent(baseEvent); diff --git a/packages/plugin-ext/src/plugin/type-converters.ts b/packages/plugin-ext/src/plugin/type-converters.ts index f3c123fb0cec7..7a86f3aa42028 100644 --- a/packages/plugin-ext/src/plugin/type-converters.ts +++ b/packages/plugin-ext/src/plugin/type-converters.ts @@ -28,7 +28,12 @@ import * as types from './types-impl'; import { UriComponents } from '../common/uri-components'; import { isReadonlyArray } from '../common/arrays'; import { MarkdownString as MarkdownStringDTO } from '@theia/core/lib/common/markdown-rendering'; -import { isObject } from '@theia/core/lib/common'; +import { DisposableCollection, isEmptyObject, isObject } from '@theia/core/lib/common'; +import * as notebooks from '@theia/notebook/lib/common'; +import { CommandsConverter } from './command-registry'; +import { BinaryBuffer } from '@theia/core/lib/common/buffer'; +import { CellData, CellExecutionUpdateType, CellOutput, CellOutputItem, CellRange, isTextStreamMime } from '@theia/notebook/lib/common'; +import { CellExecuteUpdate, CellExecutionComplete } from '@theia/notebook/lib/browser'; const SIDE_GROUP = -2; const ACTIVE_GROUP = -1; @@ -571,26 +576,46 @@ export function fromWorkspaceEdit(value: theia.WorkspaceEdit, documents?: any): edits: [] }; for (const entry of (value as types.WorkspaceEdit)._allEntries()) { - const [uri, uriOrEdits] = entry; - if (Array.isArray(uriOrEdits)) { + if (entry?._type === types.FileEditType.Text) { // text edits - const doc = documents ? documents.getDocument(uri.toString()) : undefined; + const doc = documents ? documents.getDocument(entry.uri.toString()) : undefined; const workspaceTextEditDto: WorkspaceTextEditDto = { - resource: uri, + resource: entry.uri, modelVersionId: doc?.version, - textEdit: uriOrEdits.map(edit => (edit instanceof types.TextEdit) ? fromTextEdit(edit) : fromSnippetTextEdit(edit))[0], - metadata: entry[2] as types.WorkspaceEditMetadata + textEdit: (entry.edit instanceof types.TextEdit) ? fromTextEdit(entry.edit) : fromSnippetTextEdit(entry.edit), + metadata: entry.metadata }; result.edits.push(workspaceTextEditDto); - } else { + } else if (entry?._type === types.FileEditType.File) { // resource edits const workspaceFileEditDto: WorkspaceFileEditDto = { - oldResource: uri, - newResource: uriOrEdits, - options: entry[2] as types.FileOperationOptions, - metadata: entry[3] + oldResource: entry.from, + newResource: entry.to, + options: entry.options, + metadata: entry.metadata }; result.edits.push(workspaceFileEditDto); + } else if (entry?._type === types.FileEditType.Cell) { + // cell edit + if (entry.edit) { + result.edits.push({ + metadata: entry.metadata, + resource: entry.uri, + cellEdit: entry.edit, + }); + } + } else if (entry?._type === types.FileEditType.CellReplace) { + // cell replace + result.edits.push({ + metadata: entry.metadata, + resource: entry.uri, + cellEdit: { + editType: notebooks.CellEditType.Replace, + index: entry.index, + count: entry.count, + cells: entry.cells.map(NotebookCellData.from) + } + }); } } return result; @@ -1387,3 +1412,326 @@ export namespace DataTransfer { return dataTransfer; } } + +export namespace NotebookDocumentContentOptions { + export function from(options: theia.NotebookDocumentContentOptions | undefined): notebooks.TransientOptions { + return { + transientOutputs: options?.transientOutputs ?? false, + transientCellMetadata: options?.transientCellMetadata ?? {}, + transientDocumentMetadata: options?.transientDocumentMetadata ?? {}, + }; + } +} + +export namespace NotebookStatusBarItem { + export function from(item: theia.NotebookCellStatusBarItem, commandsConverter: CommandsConverter, disposables: DisposableCollection): notebooks.NotebookCellStatusBarItem { + const command = typeof item.command === 'string' ? { title: '', command: item.command } : item.command; + return { + alignment: item.alignment === types.NotebookCellStatusBarAlignment.Left ? notebooks.CellStatusbarAlignment.Left : notebooks.CellStatusbarAlignment.Right, + command: commandsConverter.toSafeCommand(command, disposables), + text: item.text, + tooltip: item.tooltip, + priority: item.priority + }; + } +} + +export namespace NotebookData { + + export function from(data: theia.NotebookData): rpc.NotebookDataDto { + const res: rpc.NotebookDataDto = { + metadata: data.metadata ?? Object.create(null), + cells: [], + }; + for (const cell of data.cells) { + // types.NotebookCellData.validate(cell); + res.cells.push(NotebookCellData.from(cell)); + } + return res; + } + + export function to(data: rpc.NotebookDataDto): theia.NotebookData { + const res = new types.NotebookData( + data.cells.map(NotebookCellData.to), + ); + if (!isEmptyObject(data.metadata)) { + res.metadata = data.metadata; + } + return res; + } +} + +export namespace NotebookCellData { + + export function from(data: theia.NotebookCellData): rpc.NotebookCellDataDto { + return { + cellKind: NotebookCellKind.from(data.kind), + language: data.languageId, + source: data.value, + // metadata: data.metadata, + // internalMetadata: NotebookCellExecutionSummary.from(data.executionSummary ?? {}), + outputs: data.outputs ? data.outputs.map(NotebookCellOutputConverter.from) : [] + }; + } + + export function to(data: rpc.NotebookCellDataDto): theia.NotebookCellData { + return new types.NotebookCellData( + NotebookCellKind.to(data.cellKind), + data.source, + data.language, + data.outputs ? data.outputs.map(NotebookCellOutput.to) : undefined, + data.metadata, + data.internalMetadata ? NotebookCellExecutionSummary.to(data.internalMetadata) : undefined + ); + } +} + +export namespace NotebookCellKind { + export function from(data: theia.NotebookCellKind): notebooks.CellKind { + switch (data) { + case types.NotebookCellKind.Markup: + return notebooks.CellKind.Markup; + case types.NotebookCellKind.Code: + default: + return notebooks.CellKind.Code; + } + } + + export function to(data: notebooks.CellKind): theia.NotebookCellKind { + switch (data) { + case notebooks.CellKind.Markup: + return types.NotebookCellKind.Markup; + case notebooks.CellKind.Code: + default: + return types.NotebookCellKind.Code; + } + } +} + +export namespace NotebookCellOutput { + export function from(output: theia.NotebookCellOutput & { outputId: string }): rpc.NotebookOutputDto { + return { + outputId: output.outputId, + items: output.items.map(NotebookCellOutputItem.from), + metadata: output.metadata + }; + } + + export function to(output: rpc.NotebookOutputDto): theia.NotebookCellOutput { + const items = output.items.map(NotebookCellOutputItem.to); + return new types.NotebookCellOutput(items, output.outputId, output.metadata); + } +} + +export namespace NotebookCellOutputItem { + export function from(item: types.NotebookCellOutputItem): rpc.NotebookOutputItemDto { + return { + mime: item.mime, + valueBytes: BinaryBuffer.wrap(item.data), + }; + } + + export function to(item: rpc.NotebookOutputItemDto): types.NotebookCellOutputItem { + return new types.NotebookCellOutputItem(item.valueBytes.buffer, item.mime); + } +} + +export namespace NotebookCellOutputConverter { + export function from(output: types.NotebookCellOutput): rpc.NotebookOutputDto { + return { + outputId: output.outputId, + items: output.items.map(NotebookCellOutputItem.from), + metadata: output.metadata + }; + } + + export function to(output: rpc.NotebookOutputDto): types.NotebookCellOutput { + const items = output.items.map(NotebookCellOutputItem.to); + return new types.NotebookCellOutput(items, output.outputId, output.metadata); + } + + export function ensureUniqueMimeTypes(items: types.NotebookCellOutputItem[], warn: boolean = false): types.NotebookCellOutputItem[] { + const seen = new Set(); + const removeIdx = new Set(); + for (let i = 0; i < items.length; i++) { + const item = items[i]; + // We can have multiple text stream mime types in the same output. + if (!seen.has(item.mime) || isTextStreamMime(item.mime)) { + seen.add(item.mime); + continue; + } + // duplicated mime types... first has won + removeIdx.add(i); + if (warn) { + console.warn(`DUPLICATED mime type '${item.mime}' will be dropped`); + } + } + if (removeIdx.size === 0) { + return items; + } + return items.filter((_, index) => !removeIdx.has(index)); + } +} + +export namespace NotebookCellExecutionSummary { + export function to(data: notebooks.NotebookCellInternalMetadata): theia.NotebookCellExecutionSummary { + return { + timing: typeof data.runStartTime === 'number' && typeof data.runEndTime === 'number' ? { startTime: data.runStartTime, endTime: data.runEndTime } : undefined, + executionOrder: data.executionOrder, + success: data.lastRunSuccess + }; + } + + export function from(data: theia.NotebookCellExecutionSummary): Partial { + return { + lastRunSuccess: data.success, + runStartTime: data.timing?.startTime, + runEndTime: data.timing?.endTime, + executionOrder: data.executionOrder + }; + } +} + +export namespace NotebookRange { + + export function from(range: theia.NotebookRange): CellRange { + return { start: range.start, end: range.end }; + } + + export function to(range: CellRange): types.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 + }; + } +} + +export namespace NotebookDto { + + export function toNotebookOutputItemDto(item: CellOutputItem): rpc.NotebookOutputItemDto { + return { + mime: item.mime, + valueBytes: item.data + }; + } + + export function toNotebookOutputDto(output: CellOutput): rpc.NotebookOutputDto { + return { + outputId: output.outputId, + metadata: output.metadata, + items: output.outputs.map(toNotebookOutputItemDto) + }; + } + + export function toNotebookCellDataDto(cell: CellData): rpc.NotebookCellDataDto { + return { + cellKind: cell.cellKind, + language: cell.language, + source: cell.source, + internalMetadata: cell.internalMetadata, + metadata: cell.metadata, + outputs: cell.outputs.map(toNotebookOutputDto) + }; + } + + // export function toNotebookDataDto(data: NotebookData): rpc.NotebookDataDto { + // return { + // metadata: data.metadata, + // cells: data.cells.map(toNotebookCellDataDto) + // }; + // } + + export function fromNotebookOutputItemDto(item: rpc.NotebookOutputItemDto): CellOutputItem { + return { + mime: item.mime, + data: item.valueBytes + }; + } + + export function fromNotebookOutputDto(output: rpc.NotebookOutputDto): CellOutput { + return { + outputId: output.outputId, + metadata: output.metadata, + outputs: output.items.map(fromNotebookOutputItemDto) + }; + } + + export function fromNotebookCellDataDto(cell: rpc.NotebookCellDataDto): CellData { + return { + cellKind: cell.cellKind, + language: cell.language, + source: cell.source, + outputs: cell.outputs.map(fromNotebookOutputDto), + metadata: cell.metadata, + internalMetadata: cell.internalMetadata + }; + } + + // export function fromNotebookDataDto(data: rpc.NotebookDataDto): NotebookData { + // return { + // metadata: data.metadata, + // cells: data.cells.map(fromNotebookCellDataDto) + // }; + // } + + // export function toNotebookCellDto(cell: Cell): rpc.NotebookCellDto { + // return { + // handle: cell.handle, + // uri: cell.uri, + // source: cell.textBuffer.getLinesContent(), + // eol: cell.textBuffer.getEOL(), + // language: cell.language, + // cellKind: cell.cellKind, + // outputs: cell.outputs.map(toNotebookOutputDto), + // metadata: cell.metadata, + // internalMetadata: cell.internalMetadata, + // }; + // } + + export function fromCellExecuteUpdateDto(data: rpc.CellExecuteUpdateDto): CellExecuteUpdate { + if (data.editType === CellExecutionUpdateType.Output) { + return { + editType: data.editType, + cellHandle: data.cellHandle, + append: data.append, + outputs: data.outputs.map(fromNotebookOutputDto) + }; + } else if (data.editType === CellExecutionUpdateType.OutputItems) { + return { + editType: data.editType, + append: data.append, + items: data.items.map(fromNotebookOutputItemDto) + }; + } else { + return data; + } + } + + export function fromCellExecuteCompleteDto(data: rpc.CellExecutionCompleteDto): CellExecutionComplete { + return data; + } + + // export function fromCellEditOperationDto(edit: rpc.CellEditOperationDto): CellEditOperation { + // if (edit.editType === CellEditType.Replace) { + // return { + // editType: edit.editType, + // index: edit.index, + // count: edit.count, + // cells: edit.cells.map(fromNotebookCellDataDto) + // }; + // } else { + // return edit; + // } + // } +} diff --git a/packages/plugin-ext/src/plugin/types-impl.ts b/packages/plugin-ext/src/plugin/types-impl.ts index 012e8f128e90b..c088318986648 100644 --- a/packages/plugin-ext/src/plugin/types-impl.ts +++ b/packages/plugin-ext/src/plugin/types-impl.ts @@ -32,6 +32,7 @@ import { FileSystemProviderErrorCode, markAsFileSystemProviderError } from '@the import * as paths from 'path'; import { es5ClassCompat } from '../common/types'; import { isObject, isStringArray } from '@theia/core/lib/common'; +import { CellEditType, CellMetadataEdit, NotebookDocumentMetadataEdit } from '@theia/notebook/lib/common'; /** * This is an implementation of #theia.Uri based on vscode-uri. @@ -1130,6 +1131,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; @@ -1140,23 +1166,34 @@ export class NotebookCellData implements theia.NotebookCellData { metadata?: { [key: string]: any }; executionSummary?: theia.NotebookCellExecutionSummary; - constructor(kind: NotebookCellKind, value: string, languageId: string) { + constructor(kind: NotebookCellKind, value: string, languageId: string, + outputs?: theia.NotebookCellOutput[], metadata?: Record, executionSummary?: theia.NotebookCellExecutionSummary) { this.kind = kind; this.value = value; this.languageId = languageId; + this.outputs = outputs ?? []; + this.metadata = metadata; + this.executionSummary = executionSummary; } } @es5ClassCompat export class NotebookCellOutput implements theia.NotebookCellOutput { + outputId: string; items: theia.NotebookCellOutputItem[]; // eslint-disable-next-line @typescript-eslint/no-explicit-any metadata?: { [key: string]: any }; // eslint-disable-next-line @typescript-eslint/no-explicit-any - constructor(items: theia.NotebookCellOutputItem[], metadata?: { [key: string]: any }) { + constructor(items: theia.NotebookCellOutputItem[], idOrMetadata?: string | Record, metadata?: { [key: string]: any }) { this.items = items; - this.metadata = metadata; + if (typeof idOrMetadata === 'string') { + this.outputId = idOrMetadata; + this.metadata = metadata; + } else { + this.outputId = UUID.uuid4(); + this.metadata = idOrMetadata ?? metadata; + } } } @@ -1227,41 +1264,18 @@ export class NotebookData implements theia.NotebookData { } } -export class NotebookDocument implements theia.NotebookDocument { - readonly uri: theia.Uri; - readonly notebookType: string; - readonly version: number; - readonly isDirty: boolean; - readonly isUntitled: boolean; - readonly isClosed: boolean; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - readonly metadata: { [key: string]: any }; - readonly cellCount: number; - - cellAt(index: number): theia.NotebookCell { - return {} as theia.NotebookCell; - } - save(): theia.Thenable { - return Promise.resolve(false); - } - - getCells(range?: theia.NotebookRange | undefined): theia.NotebookCell[] { - return [] as NotebookCell[]; +export class NotebookRange implements theia.NotebookRange { + static isNotebookRange(thing: unknown): thing is theia.NotebookRange { + if (thing instanceof NotebookRange) { + return true; + } + if (!thing) { + return false; + } + return typeof (thing).start === 'number' + && typeof (thing).end === 'number'; } -} -export class NotebookCell implements theia.NotebookCell { - readonly index: number; - readonly notebook: theia.NotebookDocument; - readonly kind: theia.NotebookCellKind; - readonly document: theia.TextDocument; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - readonly metadata: { readonly [key: string]: any; }; - readonly outputs: readonly theia.NotebookCellOutput[]; - readonly executionSummary: theia.NotebookCellExecutionSummary | undefined; - -} -export class NotebookRange implements theia.NotebookRange { readonly start: number; readonly end: number; readonly isEmpty: boolean; @@ -1322,27 +1336,58 @@ export class NotebookEdit implements theia.NotebookEdit { // eslint-disable-next-line @typescript-eslint/no-explicit-any newNotebookMetadata?: { [key: string]: any; } | undefined; + static isNotebookCellEdit(thing: unknown): thing is NotebookEdit { + if (thing instanceof NotebookEdit) { + return true; + } + if (!thing) { + return false; + } + return NotebookRange.isNotebookRange((thing)) + && Array.isArray((thing).newCells); + } + static replaceCells(range: NotebookRange, newCells: NotebookCellData[]): NotebookEdit { - return new NotebookEdit(); + return new NotebookEdit(range, newCells); } static insertCells(index: number, newCells: NotebookCellData[]): NotebookEdit { - return new NotebookEdit(); + return new NotebookEdit(new NotebookRange(index, index), newCells); } static deleteCells(range: NotebookRange): NotebookEdit { - return new NotebookEdit(); + return new NotebookEdit(range, []); } // eslint-disable-next-line @typescript-eslint/no-explicit-any static updateCellMetadata(index: number, newCellMetadata: { [key: string]: any }): NotebookEdit { - return new NotebookEdit(); + return new NotebookEdit(new NotebookRange(index, index), [], newCellMetadata); } // eslint-disable-next-line @typescript-eslint/no-explicit-any static updateNotebookMetadata(newNotebookMetadata: { [key: string]: any }): NotebookEdit { - return new NotebookEdit(); + return new NotebookEdit(new NotebookRange(0, 0), [], undefined, newNotebookMetadata); + } + + constructor(range: NotebookRange, newCells: NotebookCellData[], newCellMetadata?: { [key: string]: unknown }, newNotebookMetadata?: { [key: string]: unknown }) { + this.range = range; + this.newCells = newCells; + this.newCellMetadata = newCellMetadata; + this.newNotebookMetadata = newNotebookMetadata; } + +} + +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 @@ -1681,6 +1726,9 @@ export interface WorkspaceEditMetadata { export const enum FileEditType { File = 1, Text = 2, + // eslint-disable-next-line @typescript-eslint/no-shadow + Cell = 3, + CellReplace = 5, Snippet = 6, } @@ -1707,7 +1755,24 @@ export interface FileSnippetTextEdit { readonly metadata?: theia.WorkspaceEditEntryMetadata; } -type WorkspaceEditEntry = FileOperation | FileTextEdit | FileSnippetTextEdit | undefined; +export interface FileCellEdit { + readonly _type: FileEditType.Cell; + readonly uri: URI; + readonly edit?: CellMetadataEdit | NotebookDocumentMetadataEdit; + readonly notebookMetadata?: Record; + readonly metadata?: theia.WorkspaceEditEntryMetadata; +} + +export interface CellEdit { + readonly _type: FileEditType.CellReplace; + readonly metadata?: theia.WorkspaceEditEntryMetadata; + readonly uri: URI; + readonly index: number; + readonly count: number; + readonly cells: theia.NotebookCellData[]; +} + +type WorkspaceEditEntry = FileOperation | FileTextEdit | FileSnippetTextEdit | FileCellEdit | CellEdit | undefined; @es5ClassCompat export class WorkspaceEdit implements theia.WorkspaceEdit { @@ -1749,8 +1814,12 @@ export class WorkspaceEdit implements theia.WorkspaceEdit { set(uri: URI, edits: ReadonlyArray): void; set(uri: URI, edits: ReadonlyArray<[TextEdit | SnippetTextEdit, theia.WorkspaceEditEntryMetadata]>): void; + set(uri: URI, edits: ReadonlyArray): void; + set(uri: URI, edits: ReadonlyArray<[NotebookEdit, theia.WorkspaceEditEntryMetadata]>): void; - set(uri: URI, edits: ReadonlyArray): void { + set(uri: URI, edits: ReadonlyArray): void { if (!edits) { // remove all text edits for `uri` for (let i = 0; i < this._edits.length; i++) { @@ -1769,7 +1838,7 @@ export class WorkspaceEdit implements theia.WorkspaceEdit { continue; } - let edit: TextEdit | SnippetTextEdit; + let edit: TextEdit | SnippetTextEdit | NotebookEdit; let metadata: theia.WorkspaceEditEntryMetadata | undefined; if (Array.isArray(editOrTuple)) { edit = editOrTuple[0]; @@ -1778,7 +1847,27 @@ export class WorkspaceEdit implements theia.WorkspaceEdit { edit = editOrTuple; } - if (SnippetTextEdit.isSnippetTextEdit(edit)) { + if (NotebookEdit.isNotebookCellEdit(edit)) { + if (edit.newCellMetadata) { + this._edits.push({ + _type: FileEditType.Cell, metadata, uri, + edit: { editType: CellEditType.Metadata, index: edit.range.start, metadata: edit.newCellMetadata } + }); + } else if (edit.newNotebookMetadata) { + this._edits.push({ + _type: FileEditType.Cell, metadata, uri, + edit: { editType: CellEditType.DocumentMetadata, metadata: edit.newNotebookMetadata }, notebookMetadata: edit.newNotebookMetadata + }); + } else { + const start = edit.range.start; + const end = edit.range.end; + + if (start !== end || edit.newCells.length > 0) { + this._edits.push({ _type: FileEditType.CellReplace, uri, index: start, count: end - start, cells: edit.newCells, metadata }); + } + } + + } else if (SnippetTextEdit.isSnippetTextEdit(edit)) { this._edits.push({ _type: FileEditType.Snippet, uri, range: edit.range, edit, metadata }); } else { this._edits.push({ _type: FileEditType.Text, uri, edit }); @@ -1817,19 +1906,23 @@ export class WorkspaceEdit implements theia.WorkspaceEdit { return result; } - _allEntries(): ([URI, Array, theia.WorkspaceEditEntryMetadata] | [URI, URI, FileOperationOptions, WorkspaceEditMetadata])[] { - const res: ([URI, Array, theia.WorkspaceEditEntryMetadata] | [URI, URI, FileOperationOptions, WorkspaceEditMetadata])[] = []; - for (const edit of this._edits) { - if (!edit) { - continue; - } - if (edit._type === FileEditType.File) { - res.push([edit.from!, edit.to!, edit.options!, edit.metadata!]); - } else { - res.push([edit.uri, [edit.edit], edit.metadata!]); - } - } - return res; + // _allEntries(): ([URI, Array, theia.WorkspaceEditEntryMetadata] | [URI, URI, FileOperationOptions, WorkspaceEditMetadata])[] { + // const res: ([URI, Array, theia.WorkspaceEditEntryMetadata] | [URI, URI, FileOperationOptions, WorkspaceEditMetadata])[] = []; + // for (const edit of this._edits) { + // if (!edit) { + // continue; + // } + // if (edit._type === FileEditType.File) { + // res.push([edit.from!, edit.to!, edit.options!, edit.metadata!]); + // } else { + // res.push([edit.uri, [edit.edit], edit.metadata!]); + // } + // } + // return res; + // } + + _allEntries(): ReadonlyArray { + return this._edits; } get size(): number { diff --git a/packages/plugin-ext/tsconfig.json b/packages/plugin-ext/tsconfig.json index 91c36231e1784..86db68c9500f8 100644 --- a/packages/plugin-ext/tsconfig.json +++ b/packages/plugin-ext/tsconfig.json @@ -53,6 +53,9 @@ { "path": "../navigator" }, + { + "path": "../notebook" + }, { "path": "../output" }, diff --git a/packages/plugin/src/theia.d.ts b/packages/plugin/src/theia.d.ts index 46655c88d1b19..a629da81fe4a8 100644 --- a/packages/plugin/src/theia.d.ts +++ b/packages/plugin/src/theia.d.ts @@ -30,6 +30,9 @@ import './theia.proposed.dropMetadata'; import './theia.proposed.editSessionIdentityProvider'; 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'; @@ -4941,14 +4944,12 @@ export module '@theia/plugin' { /** * The currently visible {@link NotebookEditor notebook editors} or an empty array. - * @stubbed */ export const visibleNotebookEditors: readonly NotebookEditor[]; /** * An {@link Event} which fires when the {@link window.visibleNotebookEditors visible notebook editors} * has changed. - * @stubbed */ export const onDidChangeVisibleNotebookEditors: Event; @@ -4956,7 +4957,6 @@ export module '@theia/plugin' { * The currently active {@link NotebookEditor notebook editor} or `undefined`. The active editor is the one * that currently has focus or, when none has focus, the one that has changed * input most recently. - * @stubbed */ export const activeNotebookEditor: NotebookEditor | undefined; @@ -4964,21 +4964,18 @@ export module '@theia/plugin' { * An {@link Event} which fires when the {@link window.activeNotebookEditor active notebook editor} * has changed. *Note* that the event also fires when the active editor changes * to `undefined`. - * @stubbed */ export const onDidChangeActiveNotebookEditor: Event; /** * An {@link Event} which fires when the {@link NotebookEditor.selections notebook editor selections} * have changed. - * @stubbed */ export const onDidChangeNotebookEditorSelection: Event; /** * An {@link Event} which fires when the {@link NotebookEditor.visibleRanges notebook editor visible ranges} * have changed. - * @stubbed */ export const onDidChangeNotebookEditorVisibleRanges: Event; @@ -4989,7 +4986,6 @@ export module '@theia/plugin' { * @param options {@link NotebookDocumentShowOptions Editor options} to configure the behavior of showing the {@link NotebookEditor notebook editor}. * * @return A promise that resolves to an {@link NotebookEditor notebook editor}. - * @stubbed */ export function showNotebookDocument(document: NotebookDocument, options?: NotebookDocumentShowOptions): Thenable; @@ -7079,7 +7075,6 @@ export module '@theia/plugin' { * All notebook documents currently known to the editor. * * @readonly - * @stubbed */ export let notebookDocuments: readonly NotebookDocument[]; @@ -7096,7 +7091,6 @@ export module '@theia/plugin' { /** * An event that is emitted when a {@link NotebookDocument notebook} is opened. - * @stubbed */ export const onDidOpenNotebookDocument: Event; @@ -7106,13 +7100,11 @@ export module '@theia/plugin' { * Note 1: There is no guarantee that this event fires when an editor tab is closed. * * Note 2: A notebook can be open but not shown in an editor which means this event can fire for a notebook that has not been shown in an editor. - * @stubbed */ export const onDidCloseNotebookDocument: Event; /** * An event that is emitted when a {@link NotebookDocument notebook} is saved. - * @stubbed */ export const onDidSaveNotebookDocument: Event; @@ -7124,7 +7116,6 @@ export module '@theia/plugin' { * @param serializer a notebook serializer. * @param options Optional context options that define what parts of a notebook should be persisted * @return A {@link Disposable disposable} that unregisters this serializer when being disposed. - * @stubbed */ export function registerNotebookSerializer(notebookType: string, serializer: NotebookSerializer, options?: NotebookDocumentContentOptions): Disposable; @@ -7178,7 +7169,6 @@ export module '@theia/plugin' { /** * An event that is emitted when a {@link Notebook notebook} has changed. - * @stubbed */ export const onDidChangeNotebookDocument: Event; @@ -7317,7 +7307,6 @@ export module '@theia/plugin' { * * @param uri The resource to open. * @return A promise that resolves to a {@link NotebookDocument notebook}. - * @stubbed */ export function openNotebookDocument(uri: Uri): Thenable | undefined; @@ -7327,7 +7316,6 @@ export module '@theia/plugin' { * @param notebookType The notebook type that should be used. * @param content The initial contents of the notebook. * @return A promise that resolves to a {@link NotebookDocument notebook}. - * @stubbed */ export function openNotebookDocument(notebookType: string, content?: NotebookData): Thenable | undefined; @@ -10051,6 +10039,30 @@ export module '@theia/plugin' { */ set(uri: Uri, edits: ReadonlyArray<[TextEdit | SnippetTextEdit, WorkspaceEditEntryMetadata]>): void; + /** + * Set (and replace) text edits or snippet edits with metadata for a resource. + * + * @param uri A resource identifier. + * @param edits An array of edits. + */ + set(uri: Uri, edits: ReadonlyArray<[TextEdit | SnippetTextEdit, WorkspaceEditEntryMetadata]>): void; + + /** + * Set (and replace) notebook edits for a resource. + * + * @param uri A resource identifier. + * @param edits An array of edits. + */ + set(uri: Uri, edits: readonly NotebookEdit[]): void; + + /** + * Set (and replace) notebook edits with metadata for a resource. + * + * @param uri A resource identifier. + * @param edits An array of edits. + */ + set(uri: Uri, edits: ReadonlyArray<[NotebookEdit, WorkspaceEditEntryMetadata]>): void; + /** * Get the text edits for a resource. * @@ -14438,13 +14450,11 @@ export module '@theia/plugin' { /** * The {@link NotebookDocument notebook document} associated with this notebook editor. - * @stubbed */ readonly notebook: NotebookDocument; /** * The primary selection in this notebook editor. - * @stubbed */ selection: NotebookRange; @@ -14452,19 +14462,16 @@ export module '@theia/plugin' { * All selections in this notebook editor. * * The primary selection (or focused range) is `selections[0]`. When the document has no cells, the primary selection is empty `{ start: 0, end: 0 }`; - * @stubbed */ selections: readonly NotebookRange[]; /** * The current visible ranges in the editor (vertically). - * @stubbed */ readonly visibleRanges: readonly NotebookRange[]; /** * The column in which this editor shows. - * @stubbed */ readonly viewColumn?: ViewColumn; @@ -14484,7 +14491,6 @@ export module '@theia/plugin' { export interface NotebookRendererMessaging { /** * An event that fires when a message is received from a renderer. - * @stubbed */ readonly onDidReceiveMessage: Event<{ readonly editor: NotebookEditor; @@ -14499,7 +14505,6 @@ export module '@theia/plugin' { * message is sent to all renderers. * @returns a boolean indicating whether the message was successfully * delivered to any renderer. - * @stubbed */ postMessage(message: any, editor?: NotebookEditor): Thenable; } @@ -14533,7 +14538,6 @@ export module '@theia/plugin' { * The index of this cell in its {@link NotebookDocument.cellAt containing notebook}. The * index is updated when a cell is moved within its notebook. The index is `-1` * when the cell has been removed from its notebook. - * @stubbed */ readonly index: number; @@ -14544,31 +14548,26 @@ export module '@theia/plugin' { /** * The kind of this cell. - * @stubbed */ readonly kind: NotebookCellKind; /** * The {@link TextDocument text} of this cell, represented as text document. - * @stubbed */ readonly document: TextDocument; /** * The metadata of this cell. Can be anything but must be JSON-stringifyable. - * @stubbed */ readonly metadata: { readonly [key: string]: any }; /** * The outputs of this cell. - * @stubbed */ readonly outputs: readonly NotebookCellOutput[]; /** * The most recent {@link NotebookCellExecutionSummary execution summary} for this cell. - * @stubbed */ readonly executionSummary: NotebookCellExecutionSummary | undefined; } @@ -14584,7 +14583,6 @@ export module '@theia/plugin' { * * *Note* that most notebooks use the `file`-scheme, which means they are files on disk. However, **not** all notebooks are * saved on disk and therefore the `scheme` must be checked before trying to access the underlying file or siblings on disk. - * @stubbed * * @see {@link FileSystemProvider} */ @@ -14592,7 +14590,6 @@ export module '@theia/plugin' { /** * The type of notebook. - * @stubbed */ readonly notebookType: string; @@ -14620,13 +14617,11 @@ export module '@theia/plugin' { /** * Arbitrary metadata for this notebook. Can be anything but must be JSON-stringifyable. - * @stubbed */ readonly metadata: { [key: string]: any }; /** * The number of cells in the notebook. - * @stubbed */ readonly cellCount: number; @@ -14635,7 +14630,6 @@ export module '@theia/plugin' { * * @param index - The index of the cell to retrieve. * @return A {@link NotebookCell cell}. - * @stubbed */ cellAt(index: number): NotebookCell; @@ -14645,7 +14639,6 @@ export module '@theia/plugin' { * * @param range A notebook range. * @returns The cells contained by the range or all cells. - * @stubbed */ getCells(range?: NotebookRange): NotebookCell[]; @@ -14654,7 +14647,6 @@ export module '@theia/plugin' { * * @return A promise that will resolve to true when the document * has been saved. Will return false if the file was not dirty or when save failed. - * @stubbed */ save(): Thenable; } @@ -14668,7 +14660,6 @@ export module '@theia/plugin' { /** * The affected cell. - * @stubbed */ readonly cell: NotebookCell; @@ -14677,25 +14668,21 @@ export module '@theia/plugin' { * * *Note* that you should use the {@link workspace.onDidChangeTextDocument onDidChangeTextDocument}-event * for detailed change information, like what edits have been performed. - * @stubbed */ readonly document: TextDocument | undefined; /** * The new metadata of the cell or `undefined` when it did not change. - * @stubbed */ readonly metadata: { [key: string]: any } | undefined; /** * The new outputs of the cell or `undefined` when they did not change. - * @stubbed */ readonly outputs: readonly NotebookCellOutput[] | undefined; /** * The new execution summary of the cell or `undefined` when it did not change. - * @stubbed */ readonly executionSummary: NotebookCellExecutionSummary | undefined; } @@ -14712,19 +14699,16 @@ export module '@theia/plugin' { * * Note that no cells have been {@link NotebookDocumentContentChange.removedCells removed} * when this range is {@link NotebookRange.isEmpty empty}. - * @stubbed */ readonly range: NotebookRange; /** * Cells that have been added to the document. - * @stubbed */ readonly addedCells: readonly NotebookCell[]; /** * Cells that have been removed from the document. - * @stubbed */ readonly removedCells: readonly NotebookCell[]; } @@ -14736,25 +14720,21 @@ export module '@theia/plugin' { /** * The affected notebook. - * @stubbed */ readonly notebook: NotebookDocument; /** * The new metadata of the notebook or `undefined` when it did not change. - * @stubbed */ readonly metadata: { [key: string]: any } | undefined; /** * An array of content changes describing added or removed {@link NotebookCell cells}. - * @stubbed */ readonly contentChanges: readonly NotebookDocumentContentChange[]; /** * An array of {@link NotebookDocumentCellChange cell changes}. - * @stubbed */ readonly cellChanges: readonly NotebookDocumentCellChange[]; } @@ -14769,7 +14749,6 @@ export module '@theia/plugin' { export interface NotebookDocumentWillSaveEvent { /** * A cancellation token. - * @stubbed */ readonly token: CancellationToken; @@ -14826,19 +14805,16 @@ export module '@theia/plugin' { /** * The order in which the execution happened. - * @stubbed */ readonly executionOrder?: number; /** * If the execution finished successfully. - * @stubbed */ readonly success?: boolean; /** * The times at which execution started and ended, as unix timestamps - * @stubbed */ readonly timing?: { readonly startTime: number; readonly endTime: number }; } @@ -14851,19 +14827,16 @@ export module '@theia/plugin' { /** * The zero-based start index of this range. - * @stubbed */ readonly start: number; /** * The exclusive end index of this range (zero-based). - * @stubbed */ readonly end: number; /** * `true` if `start` and `end` are equal. - * @stubbed */ readonly isEmpty: boolean; @@ -14873,7 +14846,6 @@ export module '@theia/plugin' { * * @param start start index * @param end end index. - * @stubbed */ constructor(start: number, end: number); @@ -14883,7 +14855,6 @@ export module '@theia/plugin' { * @param change An object that describes a change to this range. * @return A range that reflects the given change. Will return `this` range if the change * is not changing anything. - * @stubbed */ with(change: { start?: number; end?: number }): NotebookRange; } @@ -14987,13 +14958,11 @@ export module '@theia/plugin' { * vscode.NotebookCellOutputItem.text('Hey', 'text/plain'), // INVALID: repeated type, editor will pick just one * ]) * ``` - * @stubbed */ items: NotebookCellOutputItem[]; /** * Arbitrary metadata for this cell output. Can be anything but must be JSON-stringifyable. - * @stubbed */ metadata?: { [key: string]: any }; @@ -15002,7 +14971,6 @@ export module '@theia/plugin' { * * @param items Notebook output items. * @param metadata Optional metadata. - * @stubbed */ constructor(items: NotebookCellOutputItem[], metadata?: { [key: string]: any }); } @@ -15014,26 +14982,22 @@ export module '@theia/plugin' { /** * The {@link NotebookCellKind kind} of this cell data. - * @stubbed */ kind: NotebookCellKind; /** * The source value of this cell data - either source code or formatted text. - * @stubbed */ value: string; /** * The language identifier of the source value of this cell data. Any value from * {@linkcode languages.getLanguages getLanguages} is possible. - * @stubbed */ languageId: string; /** * The outputs of this cell data. - * @stubbed */ outputs?: NotebookCellOutput[]; @@ -15044,7 +15008,6 @@ export module '@theia/plugin' { /** * The execution summary of this cell data. - * @stubbed */ executionSummary?: NotebookCellExecutionSummary; @@ -15055,7 +15018,6 @@ export module '@theia/plugin' { * @param kind The kind. * @param value The source value. * @param languageId The language identifier of the source value. - * @stubbed */ constructor(kind: NotebookCellKind, value: string, languageId: string); } @@ -15071,13 +15033,11 @@ export module '@theia/plugin' { export class NotebookData { /** * The cell data of this notebook data. - * @stubbed */ cells: NotebookCellData[]; /** * Arbitrary metadata of notebook data. - * @stubbed */ metadata?: { [key: string]: any }; @@ -15085,7 +15045,6 @@ export module '@theia/plugin' { * Create new notebook data. * * @param cells An array of cell data. - * @stubbed */ constructor(cells: NotebookCellData[]); } @@ -15106,7 +15065,6 @@ export module '@theia/plugin' { * @param content Contents of a notebook file. * @param token A cancellation token. * @return Notebook data or a thenable that resolves to such. - * @stubbed */ deserializeNotebook(content: Uint8Array, token: CancellationToken): NotebookData | Thenable; @@ -15116,7 +15074,6 @@ export module '@theia/plugin' { * @param data A notebook data structure. * @param token A cancellation token. * @returns An array of bytes or a thenable that resolves to such. - * @stubbed */ serializeNotebook(data: NotebookData, token: CancellationToken): Uint8Array | Thenable; } @@ -15132,7 +15089,6 @@ export module '@theia/plugin' { * Controls if output change events will trigger notebook document content change events and * if it will be used in the diff editor, defaults to false. If the content provider doesn't * persist the outputs in the file document, this should be set to true. - * @stubbed */ transientOutputs?: boolean; @@ -15141,7 +15097,6 @@ export module '@theia/plugin' { * change events and if it will be used in the diff editor, defaults to false. If the * content provider doesn't persist a metadata property in the file document, it should be * set to true. - * @stubbed */ transientCellMetadata?: { [key: string]: boolean | undefined }; @@ -15150,7 +15105,6 @@ export module '@theia/plugin' { * content change event and if it will be used in the diff editor, defaults to false. If the * content provider doesn't persist a metadata property in the file document, it should be * set to true. - * @stubbed */ transientDocumentMetadata?: { [key: string]: boolean | undefined }; } @@ -15165,13 +15119,11 @@ export module '@theia/plugin' { * will be created as needed up to the maximum of {@linkcode ViewColumn.Nine}. * Use {@linkcode ViewColumn.Beside} to open the editor to the side of the currently * active one. - * @stubbed */ readonly viewColumn?: ViewColumn; /** * An optional flag that when `true` will stop the {@link NotebookEditor notebook editor} from taking focus. - * @stubbed */ readonly preserveFocus?: boolean; @@ -15179,13 +15131,11 @@ export module '@theia/plugin' { * An optional flag that controls if an {@link NotebookEditor notebook editor}-tab shows as preview. Preview tabs will * be replaced and reused until set to stay - either explicitly or through editing. The default behaviour depends * on the `workbench.editor.enablePreview`-setting. - * @stubbed */ readonly preview?: boolean; /** * An optional selection to apply for the document in the {@link NotebookEditor notebook editor}. - * @stubbed */ readonly selections?: readonly NotebookRange[]; } @@ -15248,7 +15198,6 @@ export module '@theia/plugin' { * * @param range The range of cells to replace * @param newCells The new notebook cells. - * @stubbed */ static replaceCells(range: NotebookRange, newCells: NotebookCellData[]): NotebookEdit; @@ -15257,7 +15206,6 @@ export module '@theia/plugin' { * * @param index The index to insert cells at. * @param newCells The new notebook cells. - * @stubbed */ static insertCells(index: number, newCells: NotebookCellData[]): NotebookEdit; @@ -15265,7 +15213,6 @@ export module '@theia/plugin' { * Utility to create an edit that deletes cells in a notebook. * * @param range The range of cells to delete. - * @stubbed */ static deleteCells(range: NotebookRange): NotebookEdit; @@ -15274,7 +15221,6 @@ export module '@theia/plugin' { * * @param index The index of the cell to update. * @param newCellMetadata The new metadata for the cell. - * @stubbed */ static updateCellMetadata(index: number, newCellMetadata: { [key: string]: any }): NotebookEdit; @@ -15282,31 +15228,26 @@ export module '@theia/plugin' { * Utility to create an edit that updates the notebook's metadata. * * @param newNotebookMetadata The new metadata for the notebook. - * @stubbed */ static updateNotebookMetadata(newNotebookMetadata: { [key: string]: any }): NotebookEdit; /** * Range of the cells being edited. May be empty. - * @stubbed */ range: NotebookRange; /** * New cells being inserted. May be empty. - * @stubbed */ newCells: NotebookCellData[]; /** * Optional new metadata for the cells. - * @stubbed */ newCellMetadata?: { [key: string]: any }; /** * Optional new metadata for the notebook. - * @stubbed */ newNotebookMetadata?: { [key: string]: any }; @@ -15319,13 +15260,11 @@ export module '@theia/plugin' { export interface NotebookEditorSelectionChangeEvent { /** * The {@link NotebookEditor notebook editor} for which the selections have changed. - * @stubbed */ readonly notebookEditor: NotebookEditor; /** * The new value for the {@link NotebookEditor.selections notebook editor's selections}. - * @stubbed */ readonly selections: readonly NotebookRange[]; } @@ -15336,13 +15275,11 @@ export module '@theia/plugin' { export interface NotebookEditorVisibleRangesChangeEvent { /** * The {@link NotebookEditor notebook editor} for which the visible ranges have changed. - * @stubbed */ readonly notebookEditor: NotebookEditor; /** * The new value for the {@link NotebookEditor.visibleRanges notebook editor's visibleRanges}. - * @stubbed */ readonly visibleRanges: readonly NotebookRange[]; } @@ -15383,13 +15320,11 @@ export module '@theia/plugin' { * * _Note_ that controllers are remembered by their identifier and that extensions should use * stable identifiers across sessions. - * @stubbed */ readonly id: string; /** * The notebook type this controller is for. - * @stubbed */ readonly notebookType: string; @@ -15407,32 +15342,27 @@ export module '@theia/plugin' { * myController.supportedLanguages = undefined; // falsy * myController.supportedLanguages = []; // falsy * ``` - * @stubbed */ supportedLanguages?: string[]; /** * The human-readable label of this notebook controller. - * @stubbed */ label: string; /** * The human-readable description which is rendered less prominent. - * @stubbed */ description?: string; /** * The human-readable detail which is rendered less prominent. - * @stubbed */ detail?: string; /** * Whether this controller supports execution order so that the * editor can render placeholders for them. - * @stubbed */ supportsExecutionOrder?: boolean; @@ -15448,14 +15378,12 @@ export module '@theia/plugin' { * * @param cell The notebook cell for which to create the execution. * @returns A notebook cell execution. - * @stubbed */ createNotebookCellExecution(cell: NotebookCell): NotebookCellExecution; /** * The execute handler is invoked when the run gestures in the UI are selected, e.g Run Cell, Run All, * Run Selection etc. The execute handler is responsible for creating and managing {@link NotebookCellExecution execution}-objects. - * @stubbed */ executeHandler: (cells: NotebookCell[], notebook: NotebookDocument, controller: NotebookController) => void | Thenable; @@ -15470,7 +15398,6 @@ export module '@theia/plugin' { * * _Note_ that supporting {@link NotebookCellExecution.token cancellation tokens} is preferred and that interrupt handlers should * only be used when tokens cannot be supported. - * @stubbed */ interruptHandler?: (notebook: NotebookDocument) => void | Thenable; @@ -15483,7 +15410,6 @@ export module '@theia/plugin' { * * _Note_ that controller selection is persisted (by the controllers {@link NotebookController.id id}) and restored as soon as a * controller is re-created or as a notebook is {@link workspace.onDidOpenNotebookDocument opened}. - * @stubbed */ readonly onDidChangeSelectedNotebooks: Event<{ readonly notebook: NotebookDocument; readonly selected: boolean }>; @@ -15493,13 +15419,11 @@ export module '@theia/plugin' { * * @param notebook The notebook for which a priority is set. * @param affinity A controller affinity - * @stubbed */ updateNotebookAffinity(notebook: NotebookDocument, affinity: NotebookControllerAffinity): void; /** * Dispose and free associated resources. - * @stubbed */ dispose(): void; } @@ -15516,7 +15440,6 @@ export module '@theia/plugin' { /** * The {@link NotebookCell cell} for which this execution has been created. - * @stubbed */ readonly cell: NotebookCell; @@ -15526,13 +15449,11 @@ export module '@theia/plugin' { * * _Note_ that the cancellation token will not be triggered when the {@link NotebookController controller} * that created this execution uses an {@link NotebookController.interruptHandler interrupt-handler}. - * @stubbed */ readonly token: CancellationToken; /** * Set and unset the order of this cell execution. - * @stubbed */ executionOrder: number | undefined; @@ -15541,7 +15462,6 @@ export module '@theia/plugin' { * * @param startTime The time that execution began, in milliseconds in the Unix epoch. Used to drive the clock * that shows for how long a cell has been running. If not given, the clock won't be shown. - * @stubbed */ start(startTime?: number): void; @@ -15552,7 +15472,6 @@ export module '@theia/plugin' { * If false, a red X is shown. * If undefined, no check or X icon is shown. * @param endTime The time that execution finished, in milliseconds in the Unix epoch. - * @stubbed */ end(success: boolean | undefined, endTime?: number): void; @@ -15562,7 +15481,6 @@ export module '@theia/plugin' { * @param cell Cell for which output is cleared. Defaults to the {@link NotebookCellExecution.cell cell} of * this execution. * @return A thenable that resolves when the operation finished. - * @stubbed */ clearOutput(cell?: NotebookCell): Thenable; @@ -15573,7 +15491,6 @@ export module '@theia/plugin' { * @param cell Cell for which output is cleared. Defaults to the {@link NotebookCellExecution.cell cell} of * this execution. * @return A thenable that resolves when the operation finished. - * @stubbed */ replaceOutput(out: NotebookCellOutput | readonly NotebookCellOutput[], cell?: NotebookCell): Thenable; @@ -15584,7 +15501,6 @@ export module '@theia/plugin' { * @param cell Cell for which output is cleared. Defaults to the {@link NotebookCellExecution.cell cell} of * this execution. * @return A thenable that resolves when the operation finished. - * @stubbed */ appendOutput(out: NotebookCellOutput | readonly NotebookCellOutput[], cell?: NotebookCell): Thenable; @@ -15594,7 +15510,6 @@ export module '@theia/plugin' { * @param items Output items that replace the items of existing output. * @param output Output object that already exists. * @return A thenable that resolves when the operation finished. - * @stubbed */ replaceOutputItems(items: NotebookCellOutputItem | readonly NotebookCellOutputItem[], output: NotebookCellOutput): Thenable; @@ -15604,7 +15519,6 @@ export module '@theia/plugin' { * @param items Output items that are append to existing output. * @param output Output object that already exists. * @return A thenable that resolves when the operation finished. - * @stubbed */ appendOutputItems(items: NotebookCellOutputItem | readonly NotebookCellOutputItem[], output: NotebookCellOutput): Thenable; } @@ -15631,7 +15545,6 @@ export module '@theia/plugin' { export class NotebookCellStatusBarItem { /** * The text to show for the item. - * @stubbed */ text: string; @@ -15648,25 +15561,21 @@ export module '@theia/plugin' { * * Note that if this is a {@linkcode Command} object, only the {@linkcode Command.command command} and {@linkcode Command.arguments arguments} * are used by the editor. - * @stubbed */ command?: string | Command; /** * A tooltip to show when the item is hovered. - * @stubbed */ tooltip?: string; /** * The priority of the item. A higher value item will be shown more to the left. - * @stubbed */ priority?: number; /** * Accessibility information used when a screen reader interacts with this item. - * @stubbed */ accessibilityInformation?: AccessibilityInformation; @@ -15674,7 +15583,6 @@ export module '@theia/plugin' { * Creates a new NotebookCellStatusBarItem. * @param text The text to show for the item. * @param alignment Whether the item is aligned to the left or right. - * @stubbed */ constructor(text: string, alignment: NotebookCellStatusBarAlignment); } @@ -15685,7 +15593,6 @@ export module '@theia/plugin' { export interface NotebookCellStatusBarItemProvider { /** * An optional event to signal that statusbar items have changed. The provide method will be called again. - * @stubbed */ onDidChangeCellStatusBarItems?: Event; @@ -15694,7 +15601,6 @@ export module '@theia/plugin' { * @param cell The cell for which to return items. * @param token A token triggered if this request should be cancelled. * @return One or more {@link NotebookCellStatusBarItem cell statusbar items} - * @stubbed */ provideCellStatusBarItems(cell: NotebookCell, token: CancellationToken): ProviderResult; } @@ -15716,7 +15622,6 @@ export module '@theia/plugin' { * @param label The label of the controller. * @param handler The execute-handler of the controller. * @returns a new instance of {@link NotebookController} - * @stubbed */ export function createNotebookController( id: string, @@ -15732,7 +15637,6 @@ export module '@theia/plugin' { * - Note 2: A renderer only has access to messaging if requiresMessaging is set to always or optional in its notebookRenderer contribution. * @param rendererId The renderer ID to communicate with * @returns A new notebook renderer messaging object. - * @stubbed */ export function createRendererMessaging(rendererId: string): NotebookRendererMessaging; @@ -15741,630 +15645,629 @@ export module '@theia/plugin' { * @param notebookType The notebook type to register for. * @param provider A cell status bar provider. * @returns A Disposable that unregisters this provider when being disposed. - * @stubbed */ export function registerNotebookCellStatusBarItemProvider(notebookType: string, provider: NotebookCellStatusBarItemProvider): Disposable; } -} -/** - * Namespace for testing functionality. Tests are published by registering - * {@link TestController} instances, then adding {@link TestItem TestItems}. - * Controllers may also describe how to run tests by creating one or more - * {@link TestRunProfile} instances. - */ -export namespace tests { /** - * Creates a new test controller. - * - * @param id Identifier for the controller, must be globally unique. - * @param label A human-readable label for the controller. - * @returns An instance of the {@link TestController}. - * @stubbed + * Namespace for testing functionality. Tests are published by registering + * {@link TestController} instances, then adding {@link TestItem TestItems}. + * Controllers may also describe how to run tests by creating one or more + * {@link TestRunProfile} instances. */ - export function createTestController(id: string, label: string): TestController; -} - -/** - * The kind of executions that {@link TestRunProfile TestRunProfiles} control. - */ -export enum TestRunProfileKind { - Run = 1, - Debug = 2, - Coverage = 3, -} + export namespace tests { + /** + * Creates a new test controller. + * + * @param id Identifier for the controller, must be globally unique. + * @param label A human-readable label for the controller. + * @returns An instance of the {@link TestController}. + * @stubbed + */ + export function createTestController(id: string, label: string): TestController; + } -/** - * Tags can be associated with {@link TestItem TestItems} and - * {@link TestRunProfile TestRunProfiles}. A profile with a tag can only - * execute tests that include that tag in their {@link TestItem.tags} array. - */ -export class TestTag { /** - * ID of the test tag. `TestTag` instances with the same ID are considered - * to be identical. + * The kind of executions that {@link TestRunProfile TestRunProfiles} control. */ - readonly id: string; + export enum TestRunProfileKind { + Run = 1, + Debug = 2, + Coverage = 3, + } /** - * Creates a new TestTag instance. - * @param id ID of the test tag. + * Tags can be associated with {@link TestItem TestItems} and + * {@link TestRunProfile TestRunProfiles}. A profile with a tag can only + * execute tests that include that tag in their {@link TestItem.tags} array. */ - constructor(id: string); -} + export class TestTag { + /** + * ID of the test tag. `TestTag` instances with the same ID are considered + * to be identical. + */ + readonly id: string; -/** - * A TestRunProfile describes one way to execute tests in a {@link TestController}. - */ -export interface TestRunProfile { - /** - * Label shown to the user in the UI. - * - * Note that the label has some significance if the user requests that - * tests be re-run in a certain way. For example, if tests were run - * normally and the user requests to re-run them in debug mode, the editor - * will attempt use a configuration with the same label of the `Debug` - * kind. If there is no such configuration, the default will be used. - * @stubbed - */ - label: string; + /** + * Creates a new TestTag instance. + * @param id ID of the test tag. + */ + constructor(id: string); + } /** - * Configures what kind of execution this profile controls. If there - * are no profiles for a kind, it will not be available in the UI. - * @stubbed + * A TestRunProfile describes one way to execute tests in a {@link TestController}. */ - readonly kind: TestRunProfileKind; + export interface TestRunProfile { + /** + * Label shown to the user in the UI. + * + * Note that the label has some significance if the user requests that + * tests be re-run in a certain way. For example, if tests were run + * normally and the user requests to re-run them in debug mode, the editor + * will attempt use a configuration with the same label of the `Debug` + * kind. If there is no such configuration, the default will be used. + * @stubbed + */ + label: string; - /** - * Controls whether this profile is the default action that will - * be taken when its kind is actioned. For example, if the user clicks - * the generic "run all" button, then the default profile for - * {@link TestRunProfileKind.Run} will be executed, although the - * user can configure this. - * @stubbed - */ - isDefault: boolean; + /** + * Configures what kind of execution this profile controls. If there + * are no profiles for a kind, it will not be available in the UI. + * @stubbed + */ + readonly kind: TestRunProfileKind; - /** - * Whether this profile supports continuous running of requests. If so, - * then {@link TestRunRequest.continuous} may be set to `true`. Defaults - * to false. - * @stubbed - */ - supportsContinuousRun: boolean; + /** + * Controls whether this profile is the default action that will + * be taken when its kind is actioned. For example, if the user clicks + * the generic "run all" button, then the default profile for + * {@link TestRunProfileKind.Run} will be executed, although the + * user can configure this. + * @stubbed + */ + isDefault: boolean; - /** - * Associated tag for the profile. If this is set, only {@link TestItem} - * instances with the same tag will be eligible to execute in this profile. - * @stubbed - */ - tag: TestTag | undefined; + /** + * Whether this profile supports continuous running of requests. If so, + * then {@link TestRunRequest.continuous} may be set to `true`. Defaults + * to false. + * @stubbed + */ + supportsContinuousRun: boolean; - /** - * If this method is present, a configuration gear will be present in the - * UI, and this method will be invoked when it's clicked. When called, - * you can take other editor actions, such as showing a quick pick or - * opening a configuration file. - * @stubbed - */ - configureHandler: (() => void) | undefined; + /** + * Associated tag for the profile. If this is set, only {@link TestItem} + * instances with the same tag will be eligible to execute in this profile. + * @stubbed + */ + tag: TestTag | undefined; - /** - * Handler called to start a test run. When invoked, the function should call - * {@link TestController.createTestRun} at least once, and all test runs - * associated with the request should be created before the function returns - * or the returned promise is resolved. - * - * If {@link supportsContinuousRun} is set, then {@link TestRunRequest.continuous} - * may be `true`. In this case, the profile should observe changes to - * source code and create new test runs by calling {@link TestController.createTestRun}, - * until the cancellation is requested on the `token`. - * - * @param request Request information for the test run. - * @param cancellationToken Token that signals the used asked to abort the - * test run. If cancellation is requested on this token, all {@link TestRun} - * instances associated with the request will be - * automatically cancelled as well. - * @stubbed - */ - runHandler: (request: TestRunRequest, token: CancellationToken) => Thenable | void; + /** + * If this method is present, a configuration gear will be present in the + * UI, and this method will be invoked when it's clicked. When called, + * you can take other editor actions, such as showing a quick pick or + * opening a configuration file. + * @stubbed + */ + configureHandler: (() => void) | undefined; - /** - * Deletes the run profile. - * @stubbed - */ - dispose(): void; -} + /** + * Handler called to start a test run. When invoked, the function should call + * {@link TestController.createTestRun} at least once, and all test runs + * associated with the request should be created before the function returns + * or the returned promise is resolved. + * + * If {@link supportsContinuousRun} is set, then {@link TestRunRequest.continuous} + * may be `true`. In this case, the profile should observe changes to + * source code and create new test runs by calling {@link TestController.createTestRun}, + * until the cancellation is requested on the `token`. + * + * @param request Request information for the test run. + * @param cancellationToken Token that signals the used asked to abort the + * test run. If cancellation is requested on this token, all {@link TestRun} + * instances associated with the request will be + * automatically cancelled as well. + * @stubbed + */ + runHandler: (request: TestRunRequest, token: CancellationToken) => Thenable | void; -/** - * Entry point to discover and execute tests. It contains {@link TestController.items} which - * are used to populate the editor UI, and is associated with - * {@link TestController.createRunProfile run profiles} to allow - * for tests to be executed. - */ -export interface TestController { - /** - * The id of the controller passed in {@link vscode.tests.createTestController}. - * This must be globally unique. - * @stubbed - */ - readonly id: string; + /** + * Deletes the run profile. + * @stubbed + */ + dispose(): void; + } /** - * Human-readable label for the test controller. - * @stubbed + * Entry point to discover and execute tests. It contains {@link TestController.items} which + * are used to populate the editor UI, and is associated with + * {@link TestController.createRunProfile run profiles} to allow + * for tests to be executed. */ - label: string; + export interface TestController { + /** + * The id of the controller passed in {@link vscode.tests.createTestController}. + * This must be globally unique. + * @stubbed + */ + readonly id: string; - /** - * A collection of "top-level" {@link TestItem} instances, which can in - * turn have their own {@link TestItem.children children} to form the - * "test tree." - * - * The extension controls when to add tests. For example, extensions should - * add tests for a file when {@link vscode.workspace.onDidOpenTextDocument} - * fires in order for decorations for tests within a file to be visible. - * - * However, the editor may sometimes explicitly request children using the - * {@link resolveHandler} See the documentation on that method for more details. - * @stubbed - */ - readonly items: TestItemCollection; + /** + * Human-readable label for the test controller. + * @stubbed + */ + label: string; - /** - * Creates a profile used for running tests. Extensions must create - * at least one profile in order for tests to be run. - * @param label A human-readable label for this profile. - * @param kind Configures what kind of execution this profile manages. - * @param runHandler Function called to start a test run. - * @param isDefault Whether this is the default action for its kind. - * @param tag Profile test tag. - * @param supportsContinuousRun Whether the profile supports continuous running. - * @returns An instance of a {@link TestRunProfile}, which is automatically - * associated with this controller. - * @stubbed - */ - createRunProfile(label: string, kind: TestRunProfileKind, runHandler: (request: TestRunRequest, token: CancellationToken) => Thenable | void, isDefault?: boolean, tag?: TestTag, supportsContinuousRun?: boolean): TestRunProfile; + /** + * A collection of "top-level" {@link TestItem} instances, which can in + * turn have their own {@link TestItem.children children} to form the + * "test tree." + * + * The extension controls when to add tests. For example, extensions should + * add tests for a file when {@link vscode.workspace.onDidOpenTextDocument} + * fires in order for decorations for tests within a file to be visible. + * + * However, the editor may sometimes explicitly request children using the + * {@link resolveHandler} See the documentation on that method for more details. + * @stubbed + */ + readonly items: TestItemCollection; - /** - * A function provided by the extension that the editor may call to request - * children of a test item, if the {@link TestItem.canResolveChildren} is - * `true`. When called, the item should discover children and call - * {@link vscode.tests.createTestItem} as children are discovered. - * - * Generally the extension manages the lifecycle of test items, but under - * certain conditions the editor may request the children of a specific - * item to be loaded. For example, if the user requests to re-run tests - * after reloading the editor, the editor may need to call this method - * to resolve the previously-run tests. - * - * The item in the explorer will automatically be marked as "busy" until - * the function returns or the returned thenable resolves. - * - * @param item An unresolved test item for which children are being - * requested, or `undefined` to resolve the controller's initial {@link TestController.items items}. - * @stubbed - */ - resolveHandler?: (item: TestItem | undefined) => Thenable | void; + /** + * Creates a profile used for running tests. Extensions must create + * at least one profile in order for tests to be run. + * @param label A human-readable label for this profile. + * @param kind Configures what kind of execution this profile manages. + * @param runHandler Function called to start a test run. + * @param isDefault Whether this is the default action for its kind. + * @param tag Profile test tag. + * @param supportsContinuousRun Whether the profile supports continuous running. + * @returns An instance of a {@link TestRunProfile}, which is automatically + * associated with this controller. + * @stubbed + */ + createRunProfile(label: string, kind: TestRunProfileKind, runHandler: (request: TestRunRequest, token: CancellationToken) => Thenable | void, isDefault?: boolean, tag?: TestTag, supportsContinuousRun?: boolean): TestRunProfile; - /** - * If this method is present, a refresh button will be present in the - * UI, and this method will be invoked when it's clicked. When called, - * the extension should scan the workspace for any new, changed, or - * removed tests. - * - * It's recommended that extensions try to update tests in realtime, using - * a {@link FileSystemWatcher} for example, and use this method as a fallback. - * - * @returns A thenable that resolves when tests have been refreshed. - * @stubbed - */ - refreshHandler: ((token: CancellationToken) => Thenable | void) | undefined; + /** + * A function provided by the extension that the editor may call to request + * children of a test item, if the {@link TestItem.canResolveChildren} is + * `true`. When called, the item should discover children and call + * {@link vscode.tests.createTestItem} as children are discovered. + * + * Generally the extension manages the lifecycle of test items, but under + * certain conditions the editor may request the children of a specific + * item to be loaded. For example, if the user requests to re-run tests + * after reloading the editor, the editor may need to call this method + * to resolve the previously-run tests. + * + * The item in the explorer will automatically be marked as "busy" until + * the function returns or the returned thenable resolves. + * + * @param item An unresolved test item for which children are being + * requested, or `undefined` to resolve the controller's initial {@link TestController.items items}. + * @stubbed + */ + resolveHandler?: (item: TestItem | undefined) => Thenable | void; - /** - * Creates a {@link TestRun}. This should be called by the - * {@link TestRunProfile} when a request is made to execute tests, and may - * also be called if a test run is detected externally. Once created, tests - * that are included in the request will be moved into the queued state. - * - * All runs created using the same `request` instance will be grouped - * together. This is useful if, for example, a single suite of tests is - * run on multiple platforms. - * - * @param request Test run request. Only tests inside the `include` may be - * modified, and tests in its `exclude` are ignored. - * @param name The human-readable name of the run. This can be used to - * disambiguate multiple sets of results in a test run. It is useful if - * tests are run across multiple platforms, for example. - * @param persist Whether the results created by the run should be - * persisted in the editor. This may be false if the results are coming from - * a file already saved externally, such as a coverage information file. - * @returns An instance of the {@link TestRun}. It will be considered "running" - * from the moment this method is invoked until {@link TestRun.end} is called. - * @stubbed - */ - createTestRun(request: TestRunRequest, name?: string, persist?: boolean): TestRun; - - /** - * Creates a new managed {@link TestItem} instance. It can be added into - * the {@link TestItem.children} of an existing item, or into the - * {@link TestController.items}. - * - * @param id Identifier for the TestItem. The test item's ID must be unique - * in the {@link TestItemCollection} it's added to. - * @param label Human-readable label of the test item. - * @param uri URI this TestItem is associated with. May be a file or directory. - * @stubbed - */ - createTestItem(id: string, label: string, uri?: Uri): TestItem; + /** + * If this method is present, a refresh button will be present in the + * UI, and this method will be invoked when it's clicked. When called, + * the extension should scan the workspace for any new, changed, or + * removed tests. + * + * It's recommended that extensions try to update tests in realtime, using + * a {@link FileSystemWatcher} for example, and use this method as a fallback. + * + * @returns A thenable that resolves when tests have been refreshed. + * @stubbed + */ + refreshHandler: ((token: CancellationToken) => Thenable | void) | undefined; - /** - * Unregisters the test controller, disposing of its associated tests - * and unpersisted results. - * @stubbed - */ - dispose(): void; -} + /** + * Creates a {@link TestRun}. This should be called by the + * {@link TestRunProfile} when a request is made to execute tests, and may + * also be called if a test run is detected externally. Once created, tests + * that are included in the request will be moved into the queued state. + * + * All runs created using the same `request` instance will be grouped + * together. This is useful if, for example, a single suite of tests is + * run on multiple platforms. + * + * @param request Test run request. Only tests inside the `include` may be + * modified, and tests in its `exclude` are ignored. + * @param name The human-readable name of the run. This can be used to + * disambiguate multiple sets of results in a test run. It is useful if + * tests are run across multiple platforms, for example. + * @param persist Whether the results created by the run should be + * persisted in the editor. This may be false if the results are coming from + * a file already saved externally, such as a coverage information file. + * @returns An instance of the {@link TestRun}. It will be considered "running" + * from the moment this method is invoked until {@link TestRun.end} is called. + * @stubbed + */ + createTestRun(request: TestRunRequest, name?: string, persist?: boolean): TestRun; -/** - * A TestRunRequest is a precursor to a {@link TestRun}, which in turn is - * created by passing a request to {@link tests.runTests}. The TestRunRequest - * contains information about which tests should be run, which should not be - * run, and how they are run (via the {@link TestRunRequest.profile profile}). - * - * In general, TestRunRequests are created by the editor and pass to - * {@link TestRunProfile.runHandler}, however you can also create test - * requests and runs outside of the `runHandler`. - */ -export class TestRunRequest { - /** - * A filter for specific tests to run. If given, the extension should run - * all of the included tests and all their children, excluding any tests - * that appear in {@link TestRunRequest.exclude}. If this property is - * undefined, then the extension should simply run all tests. - * - * The process of running tests should resolve the children of any test - * items who have not yet been resolved. - */ - readonly include: readonly TestItem[] | undefined; + /** + * Creates a new managed {@link TestItem} instance. It can be added into + * the {@link TestItem.children} of an existing item, or into the + * {@link TestController.items}. + * + * @param id Identifier for the TestItem. The test item's ID must be unique + * in the {@link TestItemCollection} it's added to. + * @param label Human-readable label of the test item. + * @param uri URI this TestItem is associated with. May be a file or directory. + * @stubbed + */ + createTestItem(id: string, label: string, uri?: Uri): TestItem; + + /** + * Unregisters the test controller, disposing of its associated tests + * and unpersisted results. + * @stubbed + */ + dispose(): void; + } /** - * An array of tests the user has marked as excluded from the test included - * in this run; exclusions should apply after inclusions. + * A TestRunRequest is a precursor to a {@link TestRun}, which in turn is + * created by passing a request to {@link tests.runTests}. The TestRunRequest + * contains information about which tests should be run, which should not be + * run, and how they are run (via the {@link TestRunRequest.profile profile}). * - * May be omitted if no exclusions were requested. Test controllers should - * not run excluded tests or any children of excluded tests. + * In general, TestRunRequests are created by the editor and pass to + * {@link TestRunProfile.runHandler}, however you can also create test + * requests and runs outside of the `runHandler`. */ - readonly exclude: readonly TestItem[] | undefined; + export class TestRunRequest { + /** + * A filter for specific tests to run. If given, the extension should run + * all of the included tests and all their children, excluding any tests + * that appear in {@link TestRunRequest.exclude}. If this property is + * undefined, then the extension should simply run all tests. + * + * The process of running tests should resolve the children of any test + * items who have not yet been resolved. + */ + readonly include: readonly TestItem[] | undefined; - /** - * The profile used for this request. This will always be defined - * for requests issued from the editor UI, though extensions may - * programmatically create requests not associated with any profile. - */ - readonly profile: TestRunProfile | undefined; + /** + * An array of tests the user has marked as excluded from the test included + * in this run; exclusions should apply after inclusions. + * + * May be omitted if no exclusions were requested. Test controllers should + * not run excluded tests or any children of excluded tests. + */ + readonly exclude: readonly TestItem[] | undefined; - /** - * Whether the profile should run continuously as source code changes. Only - * relevant for profiles that set {@link TestRunProfile.supportsContinuousRun}. - */ - readonly continuous?: boolean; + /** + * The profile used for this request. This will always be defined + * for requests issued from the editor UI, though extensions may + * programmatically create requests not associated with any profile. + */ + readonly profile: TestRunProfile | undefined; - /** - * @param include Array of specific tests to run, or undefined to run all tests - * @param exclude An array of tests to exclude from the run. - * @param profile The run profile used for this request. - * @param continuous Whether to run tests continuously as source changes. - */ - constructor(include?: readonly TestItem[], exclude?: readonly TestItem[], profile?: TestRunProfile, continuous?: boolean); -} + /** + * Whether the profile should run continuously as source code changes. Only + * relevant for profiles that set {@link TestRunProfile.supportsContinuousRun}. + */ + readonly continuous?: boolean; -/** - * Options given to {@link TestController.runTests} - */ -export interface TestRun { - /** - * The human-readable name of the run. This can be used to - * disambiguate multiple sets of results in a test run. It is useful if - * tests are run across multiple platforms, for example. - * @stubbed - */ - readonly name: string | undefined; + /** + * @param include Array of specific tests to run, or undefined to run all tests + * @param exclude An array of tests to exclude from the run. + * @param profile The run profile used for this request. + * @param continuous Whether to run tests continuously as source changes. + */ + constructor(include?: readonly TestItem[], exclude?: readonly TestItem[], profile?: TestRunProfile, continuous?: boolean); + } /** - * A cancellation token which will be triggered when the test run is - * canceled from the UI. - * @stubbed + * Options given to {@link TestController.runTests} */ - readonly token: CancellationToken; + export interface TestRun { + /** + * The human-readable name of the run. This can be used to + * disambiguate multiple sets of results in a test run. It is useful if + * tests are run across multiple platforms, for example. + * @stubbed + */ + readonly name: string | undefined; - /** - * Whether the test run will be persisted across reloads by the editor. - * @stubbed - */ - readonly isPersisted: boolean; + /** + * A cancellation token which will be triggered when the test run is + * canceled from the UI. + * @stubbed + */ + readonly token: CancellationToken; - /** - * Indicates a test is queued for later execution. - * @param test Test item to update. - * @stubbed - */ - enqueued(test: TestItem): void; + /** + * Whether the test run will be persisted across reloads by the editor. + * @stubbed + */ + readonly isPersisted: boolean; - /** - * Indicates a test has started running. - * @param test Test item to update. - * @stubbed - */ - started(test: TestItem): void; + /** + * Indicates a test is queued for later execution. + * @param test Test item to update. + * @stubbed + */ + enqueued(test: TestItem): void; - /** - * Indicates a test has been skipped. - * @param test Test item to update. - * @stubbed - */ - skipped(test: TestItem): void; + /** + * Indicates a test has started running. + * @param test Test item to update. + * @stubbed + */ + started(test: TestItem): void; - /** - * Indicates a test has failed. You should pass one or more - * {@link TestMessage TestMessages} to describe the failure. - * @param test Test item to update. - * @param message Messages associated with the test failure. - * @param duration How long the test took to execute, in milliseconds. - * @stubbed - */ - failed(test: TestItem, message: TestMessage | readonly TestMessage[], duration?: number): void; + /** + * Indicates a test has been skipped. + * @param test Test item to update. + * @stubbed + */ + skipped(test: TestItem): void; - /** - * Indicates a test has errored. You should pass one or more - * {@link TestMessage TestMessages} to describe the failure. This differs - * from the "failed" state in that it indicates a test that couldn't be - * executed at all, from a compilation error for example. - * @param test Test item to update. - * @param message Messages associated with the test failure. - * @param duration How long the test took to execute, in milliseconds. - * @stubbed - */ - errored(test: TestItem, message: TestMessage | readonly TestMessage[], duration?: number): void; + /** + * Indicates a test has failed. You should pass one or more + * {@link TestMessage TestMessages} to describe the failure. + * @param test Test item to update. + * @param message Messages associated with the test failure. + * @param duration How long the test took to execute, in milliseconds. + * @stubbed + */ + failed(test: TestItem, message: TestMessage | readonly TestMessage[], duration?: number): void; - /** - * Indicates a test has passed. - * @param test Test item to update. - * @param duration How long the test took to execute, in milliseconds. - * @stubbed - */ - passed(test: TestItem, duration?: number): void; + /** + * Indicates a test has errored. You should pass one or more + * {@link TestMessage TestMessages} to describe the failure. This differs + * from the "failed" state in that it indicates a test that couldn't be + * executed at all, from a compilation error for example. + * @param test Test item to update. + * @param message Messages associated with the test failure. + * @param duration How long the test took to execute, in milliseconds. + * @stubbed + */ + errored(test: TestItem, message: TestMessage | readonly TestMessage[], duration?: number): void; - /** - * Appends raw output from the test runner. On the user's request, the - * output will be displayed in a terminal. ANSI escape sequences, - * such as colors and text styles, are supported. - * - * @param output Output text to append. - * @param location Indicate that the output was logged at the given - * location. - * @param test Test item to associate the output with. - * @stubbed - */ - appendOutput(output: string, location?: Location, test?: TestItem): void; + /** + * Indicates a test has passed. + * @param test Test item to update. + * @param duration How long the test took to execute, in milliseconds. + * @stubbed + */ + passed(test: TestItem, duration?: number): void; - /** - * Signals that the end of the test run. Any tests included in the run whose - * states have not been updated will have their state reset. - * @stubbed - */ - end(): void; -} + /** + * Appends raw output from the test runner. On the user's request, the + * output will be displayed in a terminal. ANSI escape sequences, + * such as colors and text styles, are supported. + * + * @param output Output text to append. + * @param location Indicate that the output was logged at the given + * location. + * @param test Test item to associate the output with. + * @stubbed + */ + appendOutput(output: string, location?: Location, test?: TestItem): void; -/** - * Collection of test items, found in {@link TestItem.children} and - * {@link TestController.items}. - */ -export interface TestItemCollection extends Iterable<[id: string, testItem: TestItem]> { - /** - * Gets the number of items in the collection. - * @stubbed - */ - readonly size: number; + /** + * Signals that the end of the test run. Any tests included in the run whose + * states have not been updated will have their state reset. + * @stubbed + */ + end(): void; + } /** - * Replaces the items stored by the collection. - * @param items Items to store. - * @stubbed + * Collection of test items, found in {@link TestItem.children} and + * {@link TestController.items}. */ - replace(items: readonly TestItem[]): void; + export interface TestItemCollection extends Iterable<[id: string, testItem: TestItem]> { + /** + * Gets the number of items in the collection. + * @stubbed + */ + readonly size: number; - /** - * Iterate over each entry in this collection. - * - * @param callback Function to execute for each entry. - * @param thisArg The `this` context used when invoking the handler function. - * @stubbed - */ - forEach(callback: (item: TestItem, collection: TestItemCollection) => unknown, thisArg?: any): void; + /** + * Replaces the items stored by the collection. + * @param items Items to store. + * @stubbed + */ + replace(items: readonly TestItem[]): void; - /** - * Adds the test item to the children. If an item with the same ID already - * exists, it'll be replaced. - * @param item Item to add. - * @stubbed - */ - add(item: TestItem): void; + /** + * Iterate over each entry in this collection. + * + * @param callback Function to execute for each entry. + * @param thisArg The `this` context used when invoking the handler function. + * @stubbed + */ + forEach(callback: (item: TestItem, collection: TestItemCollection) => unknown, thisArg?: any): void; - /** - * Removes a single test item from the collection. - * @param itemId Item ID to delete. - * @stubbed - */ - delete(itemId: string): void; + /** + * Adds the test item to the children. If an item with the same ID already + * exists, it'll be replaced. + * @param item Item to add. + * @stubbed + */ + add(item: TestItem): void; - /** - * Efficiently gets a test item by ID, if it exists, in the children. - * @param itemId Item ID to get. - * @returns The found item or undefined if it does not exist. - * @stubbed - */ - get(itemId: string): TestItem | undefined; -} + /** + * Removes a single test item from the collection. + * @param itemId Item ID to delete. + * @stubbed + */ + delete(itemId: string): void; -/** - * An item shown in the "test explorer" view. - * - * A `TestItem` can represent either a test suite or a test itself, since - * they both have similar capabilities. - */ -export interface TestItem { - /** - * Identifier for the `TestItem`. This is used to correlate - * test results and tests in the document with those in the workspace - * (test explorer). This cannot change for the lifetime of the `TestItem`, - * and must be unique among its parent's direct children. - * @stubbed - */ - readonly id: string; + /** + * Efficiently gets a test item by ID, if it exists, in the children. + * @param itemId Item ID to get. + * @returns The found item or undefined if it does not exist. + * @stubbed + */ + get(itemId: string): TestItem | undefined; + } /** - * URI this `TestItem` is associated with. May be a file or directory. - * @stubbed + * An item shown in the "test explorer" view. + * + * A `TestItem` can represent either a test suite or a test itself, since + * they both have similar capabilities. */ - readonly uri: Uri | undefined; + export interface TestItem { + /** + * Identifier for the `TestItem`. This is used to correlate + * test results and tests in the document with those in the workspace + * (test explorer). This cannot change for the lifetime of the `TestItem`, + * and must be unique among its parent's direct children. + * @stubbed + */ + readonly id: string; - /** - * The children of this test item. For a test suite, this may contain the - * individual test cases or nested suites. - * @stubbed - */ - readonly children: TestItemCollection; + /** + * URI this `TestItem` is associated with. May be a file or directory. + * @stubbed + */ + readonly uri: Uri | undefined; - /** - * The parent of this item. It's set automatically, and is undefined - * top-level items in the {@link TestController.items} and for items that - * aren't yet included in another item's {@link TestItem.children children}. - * @stubbed - */ - readonly parent: TestItem | undefined; + /** + * The children of this test item. For a test suite, this may contain the + * individual test cases or nested suites. + * @stubbed + */ + readonly children: TestItemCollection; - /** - * Tags associated with this test item. May be used in combination with - * {@link TestRunProfile.tags}, or simply as an organizational feature. - * @stubbed - */ - tags: readonly TestTag[]; + /** + * The parent of this item. It's set automatically, and is undefined + * top-level items in the {@link TestController.items} and for items that + * aren't yet included in another item's {@link TestItem.children children}. + * @stubbed + */ + readonly parent: TestItem | undefined; - /** - * Indicates whether this test item may have children discovered by resolving. - * - * If true, this item is shown as expandable in the Test Explorer view and - * expanding the item will cause {@link TestController.resolveHandler} - * to be invoked with the item. - * - * Default to `false`. - * @stubbed - */ - canResolveChildren: boolean; + /** + * Tags associated with this test item. May be used in combination with + * {@link TestRunProfile.tags}, or simply as an organizational feature. + * @stubbed + */ + tags: readonly TestTag[]; - /** - * Controls whether the item is shown as "busy" in the Test Explorer view. - * This is useful for showing status while discovering children. - * - * Defaults to `false`. - * @stubbed - */ - busy: boolean; + /** + * Indicates whether this test item may have children discovered by resolving. + * + * If true, this item is shown as expandable in the Test Explorer view and + * expanding the item will cause {@link TestController.resolveHandler} + * to be invoked with the item. + * + * Default to `false`. + * @stubbed + */ + canResolveChildren: boolean; - /** - * Display name describing the test case. - * @stubbed - */ - label: string; + /** + * Controls whether the item is shown as "busy" in the Test Explorer view. + * This is useful for showing status while discovering children. + * + * Defaults to `false`. + * @stubbed + */ + busy: boolean; - /** - * Optional description that appears next to the label. - * @stubbed - */ - description?: string; + /** + * Display name describing the test case. + * @stubbed + */ + label: string; - /** - * A string that should be used when comparing this item - * with other items. When `falsy` the {@link TestItem.label label} - * is used. - * @stubbed - */ - sortText?: string | undefined; + /** + * Optional description that appears next to the label. + * @stubbed + */ + description?: string; - /** - * Location of the test item in its {@link TestItem.uri uri}. - * - * This is only meaningful if the `uri` points to a file. - * @stubbed - */ - range: Range | undefined; + /** + * A string that should be used when comparing this item + * with other items. When `falsy` the {@link TestItem.label label} + * is used. + * @stubbed + */ + sortText?: string | undefined; - /** - * Optional error encountered while loading the test. - * - * Note that this is not a test result and should only be used to represent errors in - * test discovery, such as syntax errors. - * @stubbed - */ - error: string | MarkdownString | undefined; -} + /** + * Location of the test item in its {@link TestItem.uri uri}. + * + * This is only meaningful if the `uri` points to a file. + * @stubbed + */ + range: Range | undefined; -/** - * Message associated with the test state. Can be linked to a specific - * source range -- useful for assertion failures, for example. - */ -export class TestMessage { - /** - * Human-readable message text to display. - */ - message: string | MarkdownString; + /** + * Optional error encountered while loading the test. + * + * Note that this is not a test result and should only be used to represent errors in + * test discovery, such as syntax errors. + * @stubbed + */ + error: string | MarkdownString | undefined; + } /** - * Expected test output. If given with {@link TestMessage.actualOutput actualOutput }, a diff view will be shown. + * Message associated with the test state. Can be linked to a specific + * source range -- useful for assertion failures, for example. */ - expectedOutput?: string; + export class TestMessage { + /** + * Human-readable message text to display. + */ + message: string | MarkdownString; - /** - * Actual test output. If given with {@link TestMessage.expectedOutput expectedOutput }, a diff view will be shown. - */ - actualOutput?: string; + /** + * Expected test output. If given with {@link TestMessage.actualOutput actualOutput }, a diff view will be shown. + */ + expectedOutput?: string; - /** - * Associated file location. - */ - location?: Location; + /** + * Actual test output. If given with {@link TestMessage.expectedOutput expectedOutput }, a diff view will be shown. + */ + actualOutput?: string; - /** - * Creates a new TestMessage that will present as a diff in the editor. - * @param message Message to display to the user. - * @param expected Expected output. - * @param actual Actual output. - */ - static diff(message: string | MarkdownString, expected: string, actual: string): TestMessage; + /** + * Associated file location. + */ + location?: Location; + + /** + * Creates a new TestMessage that will present as a diff in the editor. + * @param message Message to display to the user. + * @param expected Expected output. + * @param actual Actual output. + */ + static diff(message: string | MarkdownString, expected: string, actual: string): TestMessage; + + /** + * Creates a new TestMessage instance. + * @param message The message to show to the user. + */ + constructor(message: string | MarkdownString); + } /** - * Creates a new TestMessage instance. - * @param message The message to show to the user. + * Thenable is a common denominator between ES6 promises, Q, jquery.Deferred, WinJS.Promise, + * and others. This API makes no assumption about what promise library is being used which + * enables reusing existing code without migrating to a specific promise implementation. Still, + * we recommend the use of native promises which are available in this editor. */ - constructor(message: string | MarkdownString); -} + interface Thenable { + /** + * Attaches callbacks for the resolution and/or rejection of the Promise. + * @param onfulfilled The callback to execute when the Promise is resolved. + * @param onrejected The callback to execute when the Promise is rejected. + * @returns A Promise for the completion of which ever callback is executed. + */ + then(onfulfilled?: (value: T) => TResult | Thenable, onrejected?: (reason: any) => TResult | Thenable): Thenable; + then(onfulfilled?: (value: T) => TResult | Thenable, onrejected?: (reason: any) => void): Thenable; + } -/** - * Thenable is a common denominator between ES6 promises, Q, jquery.Deferred, WinJS.Promise, - * and others. This API makes no assumption about what promise library is being used which - * enables reusing existing code without migrating to a specific promise implementation. Still, - * we recommend the use of native promises which are available in this editor. - */ -interface Thenable { - /** - * Attaches callbacks for the resolution and/or rejection of the Promise. - * @param onfulfilled The callback to execute when the Promise is resolved. - * @param onrejected The callback to execute when the Promise is rejected. - * @returns A Promise for the completion of which ever callback is executed. - */ - then(onfulfilled?: (value: T) => TResult | Thenable, onrejected?: (reason: any) => TResult | Thenable): 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..ab8279581a8a8 --- /dev/null +++ b/packages/plugin/src/theia.proposed.notebookCellExecutionState.d.ts @@ -0,0 +1,68 @@ +// ***************************************************************************** +// 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/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..5ef2fcd67e35f --- /dev/null +++ b/packages/plugin/src/theia.proposed.notebookKernelSource.d.ts @@ -0,0 +1,62 @@ +// ***************************************************************************** +// 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' { + 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; + } +} 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; + } +} diff --git a/tsconfig.json b/tsconfig.json index b1a72fda40d8b..c007028138e43 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -108,6 +108,9 @@ { "path": "packages/navigator" }, + { + "path": "packages/notebook" + }, { "path": "packages/outline-view" }, diff --git a/yarn.lock b/yarn.lock index c89541b2757fd..f63a819a3f8d1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2197,6 +2197,11 @@ resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-7.0.5.tgz#b1d2f772142a301538fae9bdf9cf15b9f2573a29" integrity sha512-hKB88y3YHL8oPOs/CNlaXtjWn93+Bs48sDQR37ZUqG2tLeCS7EA1cmnkKsuQsub9OKEB/y/Rw9zqJqqNSbqVlQ== +"@types/vscode-notebook-renderer@^1.72.0": + version "1.72.0" + resolved "https://registry.yarnpkg.com/@types/vscode-notebook-renderer/-/vscode-notebook-renderer-1.72.0.tgz#8943dc3cef0ced2dfb1e04c0a933bd289e7d5199" + integrity sha512-5iTjb39DpLn03ULUwrDR3L2Dy59RV4blSUHy0oLdQuIY11PhgWO4mXIcoFS0VxY1GZQ4IcjSf3ooT2Jrrcahnw== + "@types/vscode@^1.50.0": version "1.80.0" resolved "https://registry.yarnpkg.com/@types/vscode/-/vscode-1.80.0.tgz#e004dd6cde74dafdb7fab64a6e1754bf8165b981"