diff --git a/src/vs/workbench/api/browser/mainThreadNotebook.ts b/src/vs/workbench/api/browser/mainThreadNotebook.ts index a4db909201e..5c8d9209449 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebook.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebook.ts @@ -529,6 +529,8 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo const emitter = new Emitter(); const that = this; const provider = this._notebookService.registerNotebookKernelProvider({ + providerExtensionId: extension.id.value, + providerDescription: extension.description, onDidChangeKernels: emitter.event, selector: documentFilter, provideKernels: async (uri: URI, token: CancellationToken) => { diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 84d7a43ba63..e278c674cb5 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -597,6 +597,7 @@ export interface WebviewExtensionDescription { export interface NotebookExtensionDescription { readonly id: ExtensionIdentifier; readonly location: UriComponents; + readonly description?: string; } export enum WebviewEditorCapabilities { diff --git a/src/vs/workbench/api/common/extHostNotebook.ts b/src/vs/workbench/api/common/extHostNotebook.ts index 326d28ef1fd..9d3cb5a628d 100644 --- a/src/vs/workbench/api/common/extHostNotebook.ts +++ b/src/vs/workbench/api/common/extHostNotebook.ts @@ -958,7 +958,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN let extHostRenderer = new ExtHostNotebookOutputRenderer(type, filter, renderer); this._notebookOutputRenderers.set(extHostRenderer.type, extHostRenderer); - this._proxy.$registerNotebookRenderer({ id: extension.identifier, location: extension.extensionLocation }, type, filter, renderer.preloads || []); + this._proxy.$registerNotebookRenderer({ id: extension.identifier, location: extension.extensionLocation, description: extension.description }, type, filter, renderer.preloads || []); return new extHostTypes.Disposable(() => { this._notebookOutputRenderers.delete(extHostRenderer.type); this._proxy.$unregisterNotebookRenderer(extHostRenderer.type); @@ -1083,7 +1083,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN const supportBackup = !!provider.backupNotebook; - this._proxy.$registerNotebookProvider({ id: extension.identifier, location: extension.extensionLocation }, viewType, supportBackup, provider.kernel ? { id: viewType, label: provider.kernel.label, extensionLocation: extension.extensionLocation, preloads: provider.kernel.preloads } : undefined); + this._proxy.$registerNotebookProvider({ id: extension.identifier, location: extension.extensionLocation, description: extension.description }, viewType, supportBackup, provider.kernel ? { id: viewType, label: provider.kernel.label, extensionLocation: extension.extensionLocation, preloads: provider.kernel.preloads } : undefined); return new extHostTypes.Disposable(() => { listener.dispose(); @@ -1096,7 +1096,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN const handle = ExtHostNotebookController._notebookKernelProviderHandlePool++; const adapter = new ExtHostNotebookKernelProviderAdapter(this._proxy, handle, extension, provider); this._notebookKernelProviders.set(handle, adapter); - this._proxy.$registerNotebookKernelProvider({ id: extension.identifier, location: extension.extensionLocation }, handle, { + this._proxy.$registerNotebookKernelProvider({ id: extension.identifier, location: extension.extensionLocation, description: extension.description }, handle, { viewType: selector.viewType, filenamePattern: selector.filenamePattern ? typeConverters.GlobPattern.from(selector.filenamePattern) : undefined, excludeFileNamePattern: selector.excludeFileNamePattern ? typeConverters.GlobPattern.from(selector.excludeFileNamePattern) : undefined, @@ -1149,7 +1149,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN this._notebookKernels.set(id, { kernel, extension }); const transformedSelectors = selectors.map(selector => typeConverters.GlobPattern.from(selector)); - this._proxy.$registerNotebookKernel({ id: extension.identifier, location: extension.extensionLocation }, id, kernel.label, transformedSelectors, kernel.preloads || []); + this._proxy.$registerNotebookKernel({ id: extension.identifier, location: extension.extensionLocation, description: extension.description }, id, kernel.label, transformedSelectors, kernel.preloads || []); return new extHostTypes.Disposable(() => { this._notebookKernels.delete(id); this._proxy.$unregisterNotebookKernel(id); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index b5a93ab2017..cb56c3475d7 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -49,6 +49,8 @@ import { URI } from 'vs/base/common/uri'; import { PANEL_BORDER } from 'vs/workbench/common/theme'; import { debugIconStartForeground } from 'vs/workbench/contrib/debug/browser/debugToolBar'; import { CellContextKeyManager } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellContextKeys'; +import { NotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookProvider'; +import { notebookKernelProviderAssociationsSettingId, NotebookKernelProviderAssociations } from 'vs/workbench/contrib/notebook/browser/notebookKernelAssociation'; const $ = DOM.$; @@ -586,6 +588,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this.multipleKernelsAvailable = false; } + // @deprecated if (provider && provider.kernel) { // it has a builtin kernel, don't automatically choose a kernel this._loadKernelPreloads(provider.providerExtensionLocation, provider.kernel); @@ -596,21 +599,14 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor const activeKernelStillExist = [...availableKernels2, ...availableKernels].find(kernel => kernel.id === this.activeKernel?.id && this.activeKernel?.id !== undefined); if (activeKernelStillExist) { + // the kernel still exist, we don't want to modify the selection otherwise user's temporary preference is lost return; } - // choose a preferred kernel - const kernelsFromSameExtension = availableKernels2.filter(kernel => kernel.extension.value === provider.providerId); - if (kernelsFromSameExtension.length) { - const preferedKernel = kernelsFromSameExtension.find(kernel => kernel.isPreferred) || kernelsFromSameExtension[0]; - this.activeKernel = preferedKernel; - await preferedKernel.resolve(this.viewModel!.uri, this.getId(), tokenSource.token); - - tokenSource.dispose(); - return; + if (availableKernels2.length) { + return this._setKernelsFromProviders(provider, availableKernels2, tokenSource); } - // the provider doesn't have a builtin kernel, choose a kernel this.activeKernel = availableKernels[0]; if (this.activeKernel) { @@ -620,6 +616,49 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor tokenSource.dispose(); } + private async _setKernelsFromProviders(provider: NotebookProviderInfo, kernels: INotebookKernelInfo2[], tokenSource: CancellationTokenSource) { + const rawAssociations = this.configurationService.getValue(notebookKernelProviderAssociationsSettingId) || []; + const userSetKernelProvider = rawAssociations.filter(e => e.viewType === this.viewModel?.viewType)[0]?.kernelProvider; + + if (userSetKernelProvider) { + const filteredKernels = kernels.filter(kernel => kernel.extension.value === userSetKernelProvider); + + if (filteredKernels.length) { + this.activeKernel = filteredKernels.find(kernel => kernel.isPreferred) || filteredKernels[0]; + } else { + this.activeKernel = undefined; + } + + if (this.activeKernel) { + this._loadKernelPreloads(this.activeKernel.extensionLocation, this.activeKernel); + await this.activeKernel.resolve(this.viewModel!.uri, this.getId(), tokenSource.token); + } + + tokenSource.dispose(); + return; + } + + // choose a preferred kernel + const kernelsFromSameExtension = kernels.filter(kernel => kernel.extension.value === provider.providerExtensionId); + if (kernelsFromSameExtension.length) { + const preferedKernel = kernelsFromSameExtension.find(kernel => kernel.isPreferred) || kernelsFromSameExtension[0]; + this.activeKernel = preferedKernel; + this._loadKernelPreloads(this.activeKernel.extensionLocation, this.activeKernel); + await preferedKernel.resolve(this.viewModel!.uri, this.getId(), tokenSource.token); + tokenSource.dispose(); + return; + } + + // the provider doesn't have a builtin kernel, choose a kernel + this.activeKernel = kernels[0]; + if (this.activeKernel) { + this._loadKernelPreloads(this.activeKernel.extensionLocation, this.activeKernel); + await this.activeKernel.resolve(this.viewModel!.uri, this.getId(), tokenSource.token); + } + + tokenSource.dispose(); + } + private _loadKernelPreloads(extensionLocation: URI, kernel: INotebookKernelInfoDto) { if (kernel.preloads) { this._webview?.updateKernelPreloads([extensionLocation], kernel.preloads.map(preload => URI.revive(preload))); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookKernelAssociation.ts b/src/vs/workbench/contrib/notebook/browser/notebookKernelAssociation.ts new file mode 100644 index 00000000000..c98c49112af --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/notebookKernelAssociation.ts @@ -0,0 +1,76 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as nls from 'vs/nls'; +import { IJSONSchema } from 'vs/base/common/jsonSchema'; +import { IConfigurationNode, IConfigurationRegistry, Extensions } from 'vs/platform/configuration/common/configurationRegistry'; +import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuration'; +import { Registry } from 'vs/platform/registry/common/platform'; + +export class NotebookKernelProviderAssociationRegistry { + static extensionIds: (string | null)[] = []; + static extensionDescriptions: string[] = []; +} + +export class NotebookViewTypesExtensionRegistry { + static viewTypes: string[] = []; + static viewTypeDescriptions: string[] = []; +} + +export type NotebookKernelProviderAssociation = { + readonly viewType: string; + readonly kernelProvider?: string; +}; + +export type NotebookKernelProviderAssociations = readonly NotebookKernelProviderAssociation[]; + + +export const notebookKernelProviderAssociationsSettingId = 'notebook.kernelProviderAssociations'; + +export const viewTypeSchamaAddition: IJSONSchema = { + type: 'string', + enum: [] +}; + +export const notebookKernelProviderAssociationsConfigurationNode: IConfigurationNode = { + ...workbenchConfigurationNodeBase, + properties: { + [notebookKernelProviderAssociationsSettingId]: { + type: 'array', + markdownDescription: nls.localize('notebook.kernelProviderAssociations', "Defines a default kernel provider which takes precedence over all other kernel providers settings. Must be the identifier of an extension contributing a kernel provider."), + items: { + type: 'object', + defaultSnippets: [{ + body: { + 'viewType': '$1', + 'kernelProvider': '$2' + } + }], + properties: { + 'viewType': { + type: ['string', 'null'], + default: null, + enum: NotebookViewTypesExtensionRegistry.viewTypes, + markdownEnumDescriptions: NotebookViewTypesExtensionRegistry.viewTypeDescriptions + }, + 'kernelProvider': { + type: ['string', 'null'], + default: null, + enum: NotebookKernelProviderAssociationRegistry.extensionIds, + markdownEnumDescriptions: NotebookKernelProviderAssociationRegistry.extensionDescriptions + } + } + } + } + } +}; + +export function updateNotebookKernelProvideAssociationSchema(): void { + Registry.as(Extensions.Configuration) + .notifyConfigurationSchemaUpdated(notebookKernelProviderAssociationsConfigurationNode); +} + +Registry.as(Extensions.Configuration) + .registerConfiguration(notebookKernelProviderAssociationsConfigurationNode); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts index 36264fb1929..d4bce90bc8b 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts @@ -30,11 +30,50 @@ import { IExtensionPointUser } from 'vs/workbench/services/extensions/common/ext import { generateUuid } from 'vs/base/common/uuid'; import { flatten } from 'vs/base/common/arrays'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { NotebookKernelProviderAssociationRegistry, updateNotebookKernelProvideAssociationSchema, NotebookViewTypesExtensionRegistry } from 'vs/workbench/contrib/notebook/browser/notebookKernelAssociation'; function MODEL_ID(resource: URI): string { return resource.toString(); } +export class NotebookKernelProviderInfoStore extends Disposable { + private readonly _notebookKernelProviders: INotebookKernelProvider[] = []; + + constructor() { + super(); + } + + add(provider: INotebookKernelProvider) { + this._notebookKernelProviders.push(provider); + this._updateProviderExtensionsInfo(); + + return toDisposable(() => { + let idx = this._notebookKernelProviders.indexOf(provider); + if (idx >= 0) { + this._notebookKernelProviders.splice(idx, 1); + } + + this._updateProviderExtensionsInfo(); + }); + } + + get(viewType: string, resource: URI) { + return this._notebookKernelProviders.filter(provider => notebookDocumentFilterMatch(provider.selector, viewType, resource)); + } + + private _updateProviderExtensionsInfo() { + NotebookKernelProviderAssociationRegistry.extensionIds.length = 0; + NotebookKernelProviderAssociationRegistry.extensionDescriptions.length = 0; + + this._notebookKernelProviders.forEach(provider => { + NotebookKernelProviderAssociationRegistry.extensionIds.push(provider.providerExtensionId); + NotebookKernelProviderAssociationRegistry.extensionDescriptions.push(provider.providerDescription || ''); + }); + + updateNotebookKernelProvideAssociationSchema(); + } +} + export class NotebookProviderInfoStore extends Disposable { private static readonly CUSTOM_EDITORS_STORAGE_ID = 'notebookEditors'; private static readonly CUSTOM_EDITORS_ENTRY_ID = 'editors'; @@ -54,6 +93,8 @@ export class NotebookProviderInfoStore extends Disposable { this.add(new NotebookProviderInfo(info)); } + this._updateProviderExtensionsInfo(); + this._register(extensionService.onDidRegisterExtensions(() => { if (!this._handled) { // there is no extension point registered for notebook content provider @@ -61,6 +102,8 @@ export class NotebookProviderInfoStore extends Disposable { this.clear(); mementoObject[NotebookProviderInfoStore.CUSTOM_EDITORS_ENTRY_ID] = []; this._memento.saveMemento(); + + this._updateProviderExtensionsInfo(); } })); } @@ -76,7 +119,8 @@ export class NotebookProviderInfoStore extends Disposable { displayName: notebookContribution.displayName, selector: notebookContribution.selector || [], priority: this._convertPriority(notebookContribution.priority), - providerId: extension.description.identifier.value, + providerExtensionId: extension.description.identifier.value, + providerDescription: extension.description.description, providerDisplayName: extension.description.isBuiltin ? nls.localize('builtinProviderDisplayName', "Built-in") : extension.description.displayName || extension.description.identifier.value, providerExtensionLocation: extension.description.extensionLocation })); @@ -86,6 +130,22 @@ export class NotebookProviderInfoStore extends Disposable { const mementoObject = this._memento.getMemento(StorageScope.GLOBAL); mementoObject[NotebookProviderInfoStore.CUSTOM_EDITORS_ENTRY_ID] = Array.from(this._contributedEditors.values()); this._memento.saveMemento(); + + this._updateProviderExtensionsInfo(); + } + + private _updateProviderExtensionsInfo() { + NotebookViewTypesExtensionRegistry.viewTypes.length = 0; + NotebookViewTypesExtensionRegistry.viewTypeDescriptions.length = 0; + + for (const contribute of this._contributedEditors) { + if (contribute[1].providerExtensionId) { + NotebookViewTypesExtensionRegistry.viewTypes.push(contribute[1].id); + NotebookViewTypesExtensionRegistry.viewTypeDescriptions.push(`${contribute[1].displayName}`); + } + } + + updateNotebookKernelProvideAssociationSchema(); } private _convertPriority(priority?: string) { @@ -173,6 +233,7 @@ export class NotebookService extends Disposable implements INotebookService, ICu private readonly _notebookKernels = new Map(); notebookProviderInfoStore: NotebookProviderInfoStore; notebookRenderersInfoStore: NotebookOutputRendererInfoStore = new NotebookOutputRendererInfoStore(); + notebookKernelProviderInfoStore: NotebookKernelProviderInfoStore = new NotebookKernelProviderInfoStore(); private readonly _models = new Map(); private _onDidChangeActiveEditor = new Emitter(); onDidChangeActiveEditor: Event = this._onDidChangeActiveEditor.event; @@ -200,7 +261,6 @@ export class NotebookService extends Disposable implements INotebookService, ICu private _lastClipboardIsCopy: boolean = true; private _displayOrder: { userOrder: string[], defaultOrder: string[] } = Object.create(null); - private readonly _notebookKernelProviders: INotebookKernelProvider[] = []; constructor( @IExtensionService private readonly _extensionService: IExtensionService, @@ -307,21 +367,18 @@ export class NotebookService extends Disposable implements INotebookService, ICu } registerNotebookKernelProvider(provider: INotebookKernelProvider): IDisposable { - this._notebookKernelProviders.push(provider); + const d = this.notebookKernelProviderInfoStore.add(provider); const kernelChangeEventListener = provider.onDidChangeKernels(() => { this._onDidChangeKernels.fire(); }); return toDisposable(() => { kernelChangeEventListener.dispose(); - let idx = this._notebookKernelProviders.indexOf(provider); - if (idx >= 0) { - this._notebookKernelProviders.splice(idx, 1); - } + d.dispose(); }); } async getContributedNotebookKernels2(viewType: string, resource: URI, token: CancellationToken): Promise { - const filteredProvider = this._notebookKernelProviders.filter(provider => notebookDocumentFilterMatch(provider.selector, viewType, resource)); + const filteredProvider = this.notebookKernelProviderInfoStore.get(viewType, resource); const result = new Array(filteredProvider.length); const promises = filteredProvider.map(async (provider, index) => { diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index 33642bc910c..6f459d81629 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -663,6 +663,8 @@ export interface INotebookKernelInfo2 extends INotebookKernelInfoDto2 { } export interface INotebookKernelProvider { + providerExtensionId: string; + providerDescription?: string; selector: INotebookDocumentFilter; onDidChangeKernels: Event; provideKernels(uri: URI, token: CancellationToken): Promise; diff --git a/src/vs/workbench/contrib/notebook/common/notebookProvider.ts b/src/vs/workbench/contrib/notebook/common/notebookProvider.ts index 7756d95221d..2ffcad9a909 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookProvider.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookProvider.ts @@ -18,7 +18,8 @@ export interface NotebookEditorDescriptor { readonly displayName: string; readonly selector: readonly NotebookSelector[]; readonly priority: NotebookEditorPriority; - readonly providerId?: string; + readonly providerExtensionId?: string; + readonly providerDescription?: string; readonly providerDisplayName: string; readonly providerExtensionLocation: URI; kernel?: INotebookKernelInfoDto; @@ -31,7 +32,8 @@ export class NotebookProviderInfo implements NotebookEditorDescriptor { readonly selector: readonly NotebookSelector[]; readonly priority: NotebookEditorPriority; // it's optional as the memento might not have it - readonly providerId?: string; + readonly providerExtensionId?: string; + readonly providerDescription?: string; readonly providerDisplayName: string; readonly providerExtensionLocation: URI; kernel?: INotebookKernelInfoDto; @@ -41,7 +43,8 @@ export class NotebookProviderInfo implements NotebookEditorDescriptor { this.displayName = descriptor.displayName; this.selector = descriptor.selector; this.priority = descriptor.priority; - this.providerId = descriptor.providerId; + this.providerExtensionId = descriptor.providerExtensionId; + this.providerDescription = descriptor.providerDescription; this.providerDisplayName = descriptor.providerDisplayName; this.providerExtensionLocation = descriptor.providerExtensionLocation; }