mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-21 00:59:03 +01:00
266 lines
8.8 KiB
TypeScript
266 lines
8.8 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* 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 'vs/base/common/event';
|
|
import { DisposableStore } from 'vs/base/common/lifecycle';
|
|
import { ExtHostNotebookKernelsShape, IMainContext, INotebookKernelDto2, MainContext, MainThreadNotebookKernelsShape } from 'vs/workbench/api/common/extHost.protocol';
|
|
import * as vscode from 'vscode';
|
|
import { ExtHostNotebookController } from 'vs/workbench/api/common/extHostNotebook';
|
|
import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions';
|
|
import { URI, UriComponents } from 'vs/base/common/uri';
|
|
import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookCommon';
|
|
import * as extHostTypeConverters from 'vs/workbench/api/common/extHostTypeConverters';
|
|
import { isNonEmptyArray } from 'vs/base/common/arrays';
|
|
import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitDataService';
|
|
import { asWebviewUri } from 'vs/workbench/api/common/shared/webview';
|
|
|
|
interface IKernelData {
|
|
extensionId: ExtensionIdentifier,
|
|
controller: vscode.NotebookController;
|
|
onDidChangeSelection: Emitter<{ selected: boolean; notebook: vscode.NotebookDocument; }>;
|
|
onDidReceiveMessage: Emitter<{ editor: vscode.NotebookEditor, message: any }>;
|
|
}
|
|
|
|
export class ExtHostNotebookKernels implements ExtHostNotebookKernelsShape {
|
|
|
|
private readonly _proxy: MainThreadNotebookKernelsShape;
|
|
|
|
private readonly _kernelData = new Map<number, IKernelData>();
|
|
private _handlePool: number = 0;
|
|
|
|
constructor(
|
|
mainContext: IMainContext,
|
|
private readonly _initData: IExtHostInitDataService,
|
|
private readonly _extHostNotebook: ExtHostNotebookController
|
|
) {
|
|
this._proxy = mainContext.getProxy(MainContext.MainThreadNotebookKernels);
|
|
}
|
|
|
|
createNotebookController(extension: IExtensionDescription, id: string, selector: vscode.NotebookSelector, label: string, handler?: vscode.NotebookExecutionHandler, preloads?: vscode.NotebookKernelPreload[]): vscode.NotebookController {
|
|
|
|
for (let data of this._kernelData.values()) {
|
|
if (data.controller.id === id) {
|
|
throw new Error(`notebook controller with id '${id}' ALREADY exist`);
|
|
}
|
|
}
|
|
|
|
const handle = this._handlePool++;
|
|
const that = this;
|
|
|
|
const _defaultSupportedLanguages = ['plaintext'];
|
|
const _defaultExecutHandler = () => console.warn(`NO execute handler from notebook controller '${data.id}' of extension: '${extension.identifier}'`);
|
|
|
|
let isDisposed = false;
|
|
const commandDisposables = new DisposableStore();
|
|
|
|
const onDidChangeSelection = new Emitter<{ selected: boolean, notebook: vscode.NotebookDocument }>();
|
|
const onDidReceiveMessage = new Emitter<{ editor: vscode.NotebookEditor, message: any }>();
|
|
|
|
const data: INotebookKernelDto2 = {
|
|
id: id,
|
|
selector: selector,
|
|
extensionId: extension.identifier,
|
|
extensionLocation: extension.extensionLocation,
|
|
label: label || extension.identifier.value,
|
|
supportedLanguages: _defaultSupportedLanguages,
|
|
preloads: preloads ? preloads.map(extHostTypeConverters.NotebookKernelPreload.from) : []
|
|
};
|
|
|
|
//
|
|
let _executeHandler: vscode.NotebookExecutionHandler = handler ?? _defaultExecutHandler;
|
|
let _interruptHandler: vscode.NotebookInterruptHandler | undefined;
|
|
|
|
// todo@jrieken the selector needs to be massaged
|
|
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);
|
|
}
|
|
});
|
|
};
|
|
|
|
const controller: vscode.NotebookController = {
|
|
get id() { return data.id; },
|
|
get selector() { return data.selector; },
|
|
onDidChangeNotebookAssociation: onDidChangeSelection.event,
|
|
get label() {
|
|
return data.label;
|
|
},
|
|
set label(value) {
|
|
data.label = value ?? extension.displayName ?? extension.name;
|
|
_update();
|
|
},
|
|
get detail() {
|
|
return data.detail ?? '';
|
|
},
|
|
set detail(value) {
|
|
data.detail = value;
|
|
_update();
|
|
},
|
|
get description() {
|
|
return data.description ?? '';
|
|
},
|
|
set description(value) {
|
|
data.description = value;
|
|
_update();
|
|
},
|
|
get isPreferred() {
|
|
return data.isPreferred ?? false;
|
|
},
|
|
set isPreferred(value) {
|
|
data.isPreferred = value;
|
|
_update();
|
|
},
|
|
get supportedLanguages() {
|
|
return data.supportedLanguages;
|
|
},
|
|
set supportedLanguages(value) {
|
|
data.supportedLanguages = isNonEmptyArray(value) ? value : _defaultSupportedLanguages;
|
|
_update();
|
|
},
|
|
get hasExecutionOrder() {
|
|
return data.hasExecutionOrder ?? false;
|
|
},
|
|
set hasExecutionOrder(value) {
|
|
data.hasExecutionOrder = value;
|
|
_update();
|
|
},
|
|
get preloads() {
|
|
return data.preloads ? data.preloads.map(extHostTypeConverters.NotebookKernelPreload.to) : [];
|
|
},
|
|
get executeHandler() {
|
|
return _executeHandler;
|
|
},
|
|
set executeHandler(value) {
|
|
_executeHandler = value ?? _defaultExecutHandler;
|
|
},
|
|
get interruptHandler() {
|
|
return _interruptHandler;
|
|
},
|
|
set interruptHandler(value) {
|
|
_interruptHandler = value;
|
|
data.supportsInterrupt = Boolean(value);
|
|
_update();
|
|
},
|
|
createNotebookCellExecutionTask(cell) {
|
|
if (isDisposed) {
|
|
throw new Error('notebook controller is DISPOSED');
|
|
}
|
|
//todo@jrieken
|
|
return that._extHostNotebook.createNotebookCellExecution(cell.notebook.uri, cell.index, data.id)!;
|
|
},
|
|
dispose: () => {
|
|
if (!isDisposed) {
|
|
isDisposed = true;
|
|
this._kernelData.delete(handle);
|
|
commandDisposables.dispose();
|
|
onDidChangeSelection.dispose();
|
|
onDidReceiveMessage.dispose();
|
|
this._proxy.$removeKernel(handle);
|
|
}
|
|
},
|
|
// --- ipc
|
|
onDidReceiveMessage: onDidReceiveMessage.event,
|
|
postMessage(message, editor) {
|
|
return that._proxy.$postMessage(handle, editor && that._extHostNotebook.getIdByEditor(editor), message);
|
|
},
|
|
asWebviewUri(uri: URI) {
|
|
return asWebviewUri(that._initData.environment, String(handle), uri);
|
|
}
|
|
};
|
|
|
|
this._kernelData.set(handle, { extensionId: extension.identifier, controller, onDidChangeSelection, onDidReceiveMessage });
|
|
return controller;
|
|
}
|
|
|
|
$acceptSelection(handle: number, uri: UriComponents, value: boolean): void {
|
|
const obj = this._kernelData.get(handle);
|
|
if (obj) {
|
|
obj.onDidChangeSelection.fire({
|
|
selected: value,
|
|
notebook: this._extHostNotebook.lookupNotebookDocument(URI.revive(uri))!.notebookDocument
|
|
});
|
|
}
|
|
}
|
|
|
|
async $executeCells(handle: number, uri: UriComponents, ranges: ICellRange[]): Promise<void> {
|
|
const obj = this._kernelData.get(handle);
|
|
if (!obj) {
|
|
// extension can dispose kernels in the meantime
|
|
return;
|
|
}
|
|
const document = this._extHostNotebook.lookupNotebookDocument(URI.revive(uri));
|
|
if (!document) {
|
|
throw new Error('MISSING notebook');
|
|
}
|
|
|
|
const cells: vscode.NotebookCell[] = [];
|
|
for (let range of ranges) {
|
|
cells.push(...document.notebookDocument.getCells(extHostTypeConverters.NotebookRange.to(range)));
|
|
}
|
|
|
|
try {
|
|
await obj.controller.executeHandler.call(obj.controller, cells, obj.controller);
|
|
} catch (err) {
|
|
//
|
|
console.error(err);
|
|
}
|
|
}
|
|
|
|
async $cancelCells(handle: number, uri: UriComponents, ranges: ICellRange[]): Promise<void> {
|
|
const obj = this._kernelData.get(handle);
|
|
if (!obj) {
|
|
// extension can dispose kernels in the meantime
|
|
return;
|
|
}
|
|
const document = this._extHostNotebook.lookupNotebookDocument(URI.revive(uri));
|
|
if (!document) {
|
|
throw new Error('MISSING notebook');
|
|
}
|
|
if (obj.controller.interruptHandler) {
|
|
await obj.controller.interruptHandler.call(obj.controller);
|
|
}
|
|
|
|
// we do both? interrupt and cancellation or should we be selective?
|
|
for (const range of ranges) {
|
|
for (let i = range.start; i < range.end; i++) {
|
|
const cell = document.getCellFromIndex(i);
|
|
if (cell) {
|
|
this._extHostNotebook.cancelOneNotebookCellExecution(cell);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
$acceptRendererMessage(handle: number, editorId: string, message: any): void {
|
|
const obj = this._kernelData.get(handle);
|
|
if (!obj) {
|
|
// extension can dispose kernels in the meantime
|
|
return;
|
|
}
|
|
|
|
const editor = this._extHostNotebook.getEditorById(editorId);
|
|
if (!editor) {
|
|
throw new Error(`send message for UNKNOWN editor: ${editorId}`);
|
|
}
|
|
|
|
obj.onDidReceiveMessage.fire(Object.freeze({ editor: editor.apiEditor, message }));
|
|
}
|
|
}
|