Path: blob/main/src/vs/workbench/contrib/notebook/browser/services/notebookKernelServiceImpl.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { Event, Emitter } from '../../../../../base/common/event.js';6import { Disposable, IDisposable, toDisposable } from '../../../../../base/common/lifecycle.js';7import { INotebookKernelSourceAction, INotebookTextModel } from '../../common/notebookCommon.js';8import { INotebookKernel, ISelectedNotebooksChangeEvent, INotebookKernelMatchResult, INotebookKernelService, INotebookTextModelLike, ISourceAction, INotebookSourceActionChangeEvent, INotebookKernelDetectionTask, IKernelSourceActionProvider } from '../../common/notebookKernelService.js';9import { LRUCache, ResourceMap } from '../../../../../base/common/map.js';10import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js';11import { URI } from '../../../../../base/common/uri.js';12import { INotebookService } from '../../common/notebookService.js';13import { IMenu, IMenuService, MenuId } from '../../../../../platform/actions/common/actions.js';14import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js';15import { IAction } from '../../../../../base/common/actions.js';16import { MarshalledId } from '../../../../../base/common/marshallingIds.js';17import { Schemas } from '../../../../../base/common/network.js';18import { getActiveWindow, runWhenWindowIdle } from '../../../../../base/browser/dom.js';1920class KernelInfo {2122private static _logicClock = 0;2324readonly kernel: INotebookKernel;25public score: number;26readonly time: number;2728readonly notebookPriorities = new ResourceMap<number>();2930constructor(kernel: INotebookKernel) {31this.kernel = kernel;32this.score = -1;33this.time = KernelInfo._logicClock++;34}35}3637class NotebookTextModelLikeId {38static str(k: INotebookTextModelLike): string {39return `${k.notebookType}/${k.uri.toString()}`;40}41static obj(s: string): INotebookTextModelLike {42const idx = s.indexOf('/');43return {44notebookType: s.substring(0, idx),45uri: URI.parse(s.substring(idx + 1))46};47}48}4950class SourceAction extends Disposable implements ISourceAction {51execution: Promise<void> | undefined;52private readonly _onDidChangeState = this._register(new Emitter<void>());53readonly onDidChangeState = this._onDidChangeState.event;5455constructor(56readonly action: IAction,57readonly model: INotebookTextModelLike,58readonly isPrimary: boolean59) {60super();61}6263async runAction() {64if (this.execution) {65return this.execution;66}6768this.execution = this._runAction();69this._onDidChangeState.fire();70await this.execution;71this.execution = undefined;72this._onDidChangeState.fire();73}7475private async _runAction(): Promise<void> {76try {77await this.action.run({78uri: this.model.uri,79$mid: MarshalledId.NotebookActionContext80});8182} catch (error) {83console.warn(`Kernel source command failed: ${error}`);84}85}86}8788interface IKernelInfoCache {89menu: IMenu;90actions: [ISourceAction, IDisposable][];9192}9394export class NotebookKernelService extends Disposable implements INotebookKernelService {9596declare _serviceBrand: undefined;9798private readonly _kernels = new Map<string, KernelInfo>();99100private readonly _notebookBindings = new LRUCache<string, string>(1000, 0.7);101102private readonly _onDidChangeNotebookKernelBinding = this._register(new Emitter<ISelectedNotebooksChangeEvent>());103private readonly _onDidAddKernel = this._register(new Emitter<INotebookKernel>());104private readonly _onDidRemoveKernel = this._register(new Emitter<INotebookKernel>());105private readonly _onDidChangeNotebookAffinity = this._register(new Emitter<void>());106private readonly _onDidChangeSourceActions = this._register(new Emitter<INotebookSourceActionChangeEvent>());107private readonly _onDidNotebookVariablesChange = this._register(new Emitter<URI>());108private readonly _kernelSources = new Map<string, IKernelInfoCache>();109private readonly _kernelSourceActionsUpdates = new Map<string, IDisposable>();110private readonly _kernelDetectionTasks = new Map<string, INotebookKernelDetectionTask[]>();111private readonly _onDidChangeKernelDetectionTasks = this._register(new Emitter<string>());112private readonly _kernelSourceActionProviders = new Map<string, IKernelSourceActionProvider[]>();113114readonly onDidChangeSelectedNotebooks: Event<ISelectedNotebooksChangeEvent> = this._onDidChangeNotebookKernelBinding.event;115readonly onDidAddKernel: Event<INotebookKernel> = this._onDidAddKernel.event;116readonly onDidRemoveKernel: Event<INotebookKernel> = this._onDidRemoveKernel.event;117readonly onDidChangeNotebookAffinity: Event<void> = this._onDidChangeNotebookAffinity.event;118readonly onDidChangeSourceActions: Event<INotebookSourceActionChangeEvent> = this._onDidChangeSourceActions.event;119readonly onDidChangeKernelDetectionTasks: Event<string> = this._onDidChangeKernelDetectionTasks.event;120readonly onDidNotebookVariablesUpdate: Event<URI> = this._onDidNotebookVariablesChange.event;121122private static _storageNotebookBinding = 'notebook.controller2NotebookBindings';123124125constructor(126@INotebookService private readonly _notebookService: INotebookService,127@IStorageService private readonly _storageService: IStorageService,128@IMenuService private readonly _menuService: IMenuService,129@IContextKeyService private readonly _contextKeyService: IContextKeyService130) {131super();132133// auto associate kernels to new notebook documents, also emit event when134// a notebook has been closed (but don't update the memento)135this._register(_notebookService.onDidAddNotebookDocument(this._tryAutoBindNotebook, this));136this._register(_notebookService.onWillRemoveNotebookDocument(notebook => {137const id = NotebookTextModelLikeId.str(notebook);138const kernelId = this._notebookBindings.get(id);139if (kernelId && notebook.uri.scheme === Schemas.untitled) {140this.selectKernelForNotebook(undefined, notebook);141}142this._kernelSourceActionsUpdates.get(id)?.dispose();143this._kernelSourceActionsUpdates.delete(id);144}));145146// restore from storage147try {148const data = JSON.parse(this._storageService.get(NotebookKernelService._storageNotebookBinding, StorageScope.WORKSPACE, '[]'));149this._notebookBindings.fromJSON(data);150} catch {151// ignore152}153}154155override dispose() {156this._kernels.clear();157this._kernelSources.forEach(v => {158v.menu.dispose();159v.actions.forEach(a => a[1].dispose());160});161this._kernelSourceActionsUpdates.forEach(v => {162v.dispose();163});164this._kernelSourceActionsUpdates.clear();165super.dispose();166}167168private _persistSoonHandle?: IDisposable;169170private _persistMementos(): void {171this._persistSoonHandle?.dispose();172this._persistSoonHandle = runWhenWindowIdle(getActiveWindow(), () => {173this._storageService.store(NotebookKernelService._storageNotebookBinding, JSON.stringify(this._notebookBindings), StorageScope.WORKSPACE, StorageTarget.MACHINE);174}, 100);175}176177private static _score(kernel: INotebookKernel, notebook: INotebookTextModelLike): number {178if (kernel.viewType === '*') {179return 5;180} else if (kernel.viewType === notebook.notebookType) {181return 10;182} else {183return 0;184}185}186187private _tryAutoBindNotebook(notebook: INotebookTextModel, onlyThisKernel?: INotebookKernel): void {188189const id = this._notebookBindings.get(NotebookTextModelLikeId.str(notebook));190if (!id) {191// no kernel associated192return;193}194const existingKernel = this._kernels.get(id);195if (!existingKernel || !NotebookKernelService._score(existingKernel.kernel, notebook)) {196// associated kernel not known, not matching197return;198}199if (!onlyThisKernel || existingKernel.kernel === onlyThisKernel) {200this._onDidChangeNotebookKernelBinding.fire({ notebook: notebook.uri, oldKernel: undefined, newKernel: existingKernel.kernel.id });201}202}203204notifyVariablesChange(notebookUri: URI): void {205this._onDidNotebookVariablesChange.fire(notebookUri);206}207208registerKernel(kernel: INotebookKernel): IDisposable {209if (this._kernels.has(kernel.id)) {210throw new Error(`NOTEBOOK CONTROLLER with id '${kernel.id}' already exists`);211}212213this._kernels.set(kernel.id, new KernelInfo(kernel));214this._onDidAddKernel.fire(kernel);215216// auto associate the new kernel to existing notebooks it was217// associated to in the past.218for (const notebook of this._notebookService.getNotebookTextModels()) {219this._tryAutoBindNotebook(notebook, kernel);220}221222return toDisposable(() => {223if (this._kernels.delete(kernel.id)) {224this._onDidRemoveKernel.fire(kernel);225}226for (const [key, candidate] of Array.from(this._notebookBindings)) {227if (candidate === kernel.id) {228this._onDidChangeNotebookKernelBinding.fire({ notebook: NotebookTextModelLikeId.obj(key).uri, oldKernel: kernel.id, newKernel: undefined });229}230}231});232}233234getMatchingKernel(notebook: INotebookTextModelLike): INotebookKernelMatchResult {235236// all applicable kernels237const kernels: { kernel: INotebookKernel; instanceAffinity: number; score: number }[] = [];238for (const info of this._kernels.values()) {239const score = NotebookKernelService._score(info.kernel, notebook);240if (score) {241kernels.push({242score,243kernel: info.kernel,244instanceAffinity: info.notebookPriorities.get(notebook.uri) ?? 1 /* vscode.NotebookControllerPriority.Default */,245});246}247}248249kernels250.sort((a, b) => b.instanceAffinity - a.instanceAffinity || a.score - b.score || a.kernel.label.localeCompare(b.kernel.label));251const all = kernels.map(obj => obj.kernel);252253// bound kernel254const selectedId = this._notebookBindings.get(NotebookTextModelLikeId.str(notebook));255const selected = selectedId ? this._kernels.get(selectedId)?.kernel : undefined;256const suggestions = kernels.filter(item => item.instanceAffinity > 1).map(item => item.kernel);257const hidden = kernels.filter(item => item.instanceAffinity < 0).map(item => item.kernel);258return { all, selected, suggestions, hidden };259}260261getSelectedOrSuggestedKernel(notebook: INotebookTextModel): INotebookKernel | undefined {262const info = this.getMatchingKernel(notebook);263if (info.selected) {264return info.selected;265}266267const preferred = info.all.filter(kernel => this._kernels.get(kernel.id)?.notebookPriorities.get(notebook.uri) === 2 /* vscode.NotebookControllerPriority.Preferred */);268if (preferred.length === 1) {269return preferred[0];270}271272return info.all.length === 1 ? info.all[0] : undefined;273}274275// a notebook has one kernel, a kernel has N notebooks276// notebook <-1----N-> kernel277selectKernelForNotebook(kernel: INotebookKernel | undefined, notebook: INotebookTextModelLike): void {278const key = NotebookTextModelLikeId.str(notebook);279const oldKernel = this._notebookBindings.get(key);280if (oldKernel !== kernel?.id) {281if (kernel) {282this._notebookBindings.set(key, kernel.id);283} else {284this._notebookBindings.delete(key);285}286this._onDidChangeNotebookKernelBinding.fire({ notebook: notebook.uri, oldKernel, newKernel: kernel?.id });287this._persistMementos();288}289}290291preselectKernelForNotebook(kernel: INotebookKernel, notebook: INotebookTextModelLike): void {292const key = NotebookTextModelLikeId.str(notebook);293const oldKernel = this._notebookBindings.get(key);294if (oldKernel !== kernel?.id) {295this._notebookBindings.set(key, kernel.id);296this._persistMementos();297}298}299300updateKernelNotebookAffinity(kernel: INotebookKernel, notebook: URI, preference: number | undefined): void {301const info = this._kernels.get(kernel.id);302if (!info) {303throw new Error(`UNKNOWN kernel '${kernel.id}'`);304}305if (preference === undefined) {306info.notebookPriorities.delete(notebook);307} else {308info.notebookPriorities.set(notebook, preference);309}310this._onDidChangeNotebookAffinity.fire();311}312313getRunningSourceActions(notebook: INotebookTextModelLike) {314const id = NotebookTextModelLikeId.str(notebook);315const existingInfo = this._kernelSources.get(id);316if (existingInfo) {317return existingInfo.actions.filter(action => action[0].execution).map(action => action[0]);318}319320return [];321}322323getSourceActions(notebook: INotebookTextModelLike, contextKeyService: IContextKeyService | undefined): ISourceAction[] {324contextKeyService = contextKeyService ?? this._contextKeyService;325const id = NotebookTextModelLikeId.str(notebook);326const existingInfo = this._kernelSources.get(id);327328if (existingInfo) {329return existingInfo.actions.map(a => a[0]);330}331332const sourceMenu = this._register(this._menuService.createMenu(MenuId.NotebookKernelSource, contextKeyService));333const info: IKernelInfoCache = { menu: sourceMenu, actions: [] };334335const loadActionsFromMenu = (menu: IMenu, document: INotebookTextModelLike) => {336const groups = menu.getActions({ shouldForwardArgs: true });337const sourceActions: [ISourceAction, IDisposable][] = [];338groups.forEach(group => {339const isPrimary = /^primary/.test(group[0]);340group[1].forEach(action => {341const sourceAction = new SourceAction(action, document, isPrimary);342const stateChangeListener = sourceAction.onDidChangeState(() => {343this._onDidChangeSourceActions.fire({344notebook: document.uri,345viewType: document.notebookType,346});347});348sourceActions.push([sourceAction, stateChangeListener]);349});350});351info.actions = sourceActions;352this._kernelSources.set(id, info);353this._onDidChangeSourceActions.fire({ notebook: document.uri, viewType: document.notebookType });354};355356this._kernelSourceActionsUpdates.get(id)?.dispose();357this._kernelSourceActionsUpdates.set(id, sourceMenu.onDidChange(() => {358loadActionsFromMenu(sourceMenu, notebook);359}));360361loadActionsFromMenu(sourceMenu, notebook);362363return info.actions.map(a => a[0]);364}365366registerNotebookKernelDetectionTask(task: INotebookKernelDetectionTask): IDisposable {367const notebookType = task.notebookType;368const all = this._kernelDetectionTasks.get(notebookType) ?? [];369all.push(task);370this._kernelDetectionTasks.set(notebookType, all);371this._onDidChangeKernelDetectionTasks.fire(notebookType);372return toDisposable(() => {373const all = this._kernelDetectionTasks.get(notebookType) ?? [];374const idx = all.indexOf(task);375if (idx >= 0) {376all.splice(idx, 1);377this._kernelDetectionTasks.set(notebookType, all);378this._onDidChangeKernelDetectionTasks.fire(notebookType);379}380});381}382383getKernelDetectionTasks(notebook: INotebookTextModelLike): INotebookKernelDetectionTask[] {384return this._kernelDetectionTasks.get(notebook.notebookType) ?? [];385}386387registerKernelSourceActionProvider(viewType: string, provider: IKernelSourceActionProvider): IDisposable {388const providers = this._kernelSourceActionProviders.get(viewType) ?? [];389providers.push(provider);390this._kernelSourceActionProviders.set(viewType, providers);391this._onDidChangeSourceActions.fire({ viewType: viewType });392393const eventEmitterDisposable = provider.onDidChangeSourceActions?.(() => {394this._onDidChangeSourceActions.fire({ viewType: viewType });395});396397return toDisposable(() => {398const providers = this._kernelSourceActionProviders.get(viewType) ?? [];399const idx = providers.indexOf(provider);400if (idx >= 0) {401providers.splice(idx, 1);402this._kernelSourceActionProviders.set(viewType, providers);403}404405eventEmitterDisposable?.dispose();406});407}408409/**410* Get kernel source actions from providers411*/412getKernelSourceActions2(notebook: INotebookTextModelLike): Promise<INotebookKernelSourceAction[]> {413const viewType = notebook.notebookType;414const providers = this._kernelSourceActionProviders.get(viewType) ?? [];415const promises = providers.map(provider => provider.provideKernelSourceActions());416return Promise.all(promises).then(actions => {417return actions.reduce((a, b) => a.concat(b), []);418});419}420}421422423