diff --git a/package.nls.json b/package.nls.json index 14874f6fdb53..fdb40cf2914e 100644 --- a/package.nls.json +++ b/package.nls.json @@ -550,5 +550,7 @@ "DataScienceRendererExtension.installationCompleteMessage": "complete.", "DataScienceRendererExtension.startingDownloadOutputMessage": "Starting download of Notebook Renderers extension.", "DataScienceRendererExtension.downloadingMessage": "Downloading Notebook Renderers Extension...", - "DataScienceRendererExtension.downloadCompletedOutputMessage": "Notebook Renderers extension download complete." + "DataScienceRendererExtension.downloadCompletedOutputMessage": "Notebook Renderers extension download complete.", + "DataScience.uriProviderDescriptionFormat": "{0} (From {1} extension)", + "DataScience.unknownPackage": "unknown" } diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index 0d3143d3e88f..36da49d18e7f 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -316,6 +316,11 @@ export namespace DataScience { 'DataScience.unknownServerUri', 'Server URI cannot be used. Did you uninstall an extension that provided a Jupyter server connection?' ); + export const uriProviderDescriptionFormat = localize( + 'DataScience.uriProviderDescriptionFormat', + '{0} (From {1} extension)' + ); + export const unknownPackage = localize('DataScience.unknownPackage', 'unknown'); export const historyTitle = localize('DataScience.historyTitle', 'Python Interactive'); export const dataExplorerTitle = localize('DataScience.dataExplorerTitle', 'Data Viewer'); export const badWebPanelFormatString = localize( diff --git a/src/client/datascience/jupyterUriProviderRegistration.ts b/src/client/datascience/jupyterUriProviderRegistration.ts index eab3c15b12b7..ae5c8beb4d1c 100644 --- a/src/client/datascience/jupyterUriProviderRegistration.ts +++ b/src/client/datascience/jupyterUriProviderRegistration.ts @@ -1,8 +1,12 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. import { inject, injectable } from 'inversify'; +import * as path from 'path'; +import { IFileSystem } from '../common/platform/types'; import { IExtensions } from '../common/types'; import * as localize from '../common/utils/localize'; +import { EXTENSION_ROOT_DIR } from '../constants'; +import { JupyterUriProviderWrapper } from './jupyterUriProviderWrapper'; import { IJupyterServerUri, IJupyterUriProvider, @@ -13,20 +17,23 @@ import { @injectable() export class JupyterUriProviderRegistration implements IJupyterUriProviderRegistration { private loadedOtherExtensionsPromise: Promise | undefined; - private providers = new Map(); + private providers = new Map>(); - constructor(@inject(IExtensions) private readonly extensions: IExtensions) {} + constructor( + @inject(IExtensions) private readonly extensions: IExtensions, + @inject(IFileSystem) private readonly fileSystem: IFileSystem + ) {} public async getProviders(): Promise> { await this.checkOtherExtensions(); // Other extensions should have registered in their activate callback - return [...this.providers.values()]; + return Promise.all([...this.providers.values()]); } public registerProvider(provider: IJupyterUriProvider) { if (!this.providers.has(provider.id)) { - this.providers.set(provider.id, provider); + this.providers.set(provider.id, this.createProvider(provider)); } else { throw new Error(`IJupyterUriProvider already exists with id ${provider.id}`); } @@ -35,8 +42,9 @@ export class JupyterUriProviderRegistration implements IJupyterUriProviderRegist public async getJupyterServerUri(id: string, handle: JupyterServerUriHandle): Promise { await this.checkOtherExtensions(); - const provider = this.providers.get(id); - if (provider) { + const providerPromise = this.providers.get(id); + if (providerPromise) { + const provider = await providerPromise; return provider.getServerUri(handle); } throw new Error(localize.DataScience.unknownServerUri()); @@ -55,4 +63,44 @@ export class JupyterUriProviderRegistration implements IJupyterUriProviderRegist .map((e) => (e.isActive ? Promise.resolve() : e.activate())); await Promise.all(list); } + + private async createProvider(provider: IJupyterUriProvider): Promise { + const packageName = await this.determineExtensionFromCallstack(); + return new JupyterUriProviderWrapper(provider, packageName); + } + + private async determineExtensionFromCallstack(): Promise { + const stack = new Error().stack; + if (stack) { + const root = EXTENSION_ROOT_DIR.toLowerCase(); + const frames = stack.split('\n').map((f) => { + const result = /\((.*)\)/.exec(f); + if (result) { + return result[1]; + } + }); + for (const frame of frames) { + if (frame && !frame.startsWith(root)) { + // This file is from a different extension. Try to find its package.json + let dirName = path.dirname(frame); + let last = frame; + while (dirName && dirName.length < last.length) { + const possiblePackageJson = path.join(dirName, 'package.json'); + if (await this.fileSystem.fileExists(possiblePackageJson)) { + const text = await this.fileSystem.readFile(possiblePackageJson); + try { + const json = JSON.parse(text); + return `${json.publisher}.${json.name}`; + } catch { + // If parse fails, then not the extension + } + } + last = dirName; + dirName = path.dirname(dirName); + } + } + } + } + return localize.DataScience.unknownPackage(); + } } diff --git a/src/client/datascience/jupyterUriProviderWrapper.ts b/src/client/datascience/jupyterUriProviderWrapper.ts new file mode 100644 index 000000000000..7c1019bb62ac --- /dev/null +++ b/src/client/datascience/jupyterUriProviderWrapper.ts @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import * as vscode from 'vscode'; +import * as localize from '../common/utils/localize'; +import { IJupyterServerUri, IJupyterUriProvider, JupyterServerUriHandle } from './types'; + +/** + * This class wraps an IJupyterUriProvider provided by another extension. It allows us to show + * extra data on the other extension's UI. + */ +export class JupyterUriProviderWrapper implements IJupyterUriProvider { + constructor(private readonly provider: IJupyterUriProvider, private packageName: string) {} + public get id() { + return this.provider.id; + } + public getQuickPickEntryItems(): vscode.QuickPickItem[] { + return this.provider.getQuickPickEntryItems().map((q) => { + return { + ...q, + // Add the package name onto the description + description: localize.DataScience.uriProviderDescriptionFormat().format( + q.description || '', + this.packageName + ), + original: q + }; + }); + } + public handleQuickPick( + item: vscode.QuickPickItem, + back: boolean + ): Promise { + // tslint:disable-next-line: no-any + if ((item as any).original) { + // tslint:disable-next-line: no-any + return this.provider.handleQuickPick((item as any).original, back); + } + return this.provider.handleQuickPick(item, back); + } + + public getServerUri(handle: JupyterServerUriHandle): Promise { + return this.provider.getServerUri(handle); + } +} diff --git a/src/test/datascience/jupyterUriProviderRegistration.unit.test.ts b/src/test/datascience/jupyterUriProviderRegistration.unit.test.ts index 603ae586a1c2..45dc28fb05eb 100644 --- a/src/test/datascience/jupyterUriProviderRegistration.unit.test.ts +++ b/src/test/datascience/jupyterUriProviderRegistration.unit.test.ts @@ -3,9 +3,10 @@ 'use strict'; import { assert } from 'chai'; -import { instance, mock, when } from 'ts-mockito'; +import { anything, instance, mock, when } from 'ts-mockito'; import * as TypeMoq from 'typemoq'; import * as vscode from 'vscode'; +import { FileSystem } from '../../client/common/platform/fileSystem'; import { JupyterUriProviderRegistration } from '../../client/datascience/jupyterUriProviderRegistration'; import { IJupyterServerUri, IJupyterUriProvider, JupyterServerUriHandle } from '../../client/datascience/types'; import { MockExtensions } from './mockExtensions'; @@ -47,6 +48,8 @@ suite('DataScience URI Picker', () => { let registration: JupyterUriProviderRegistration | undefined; const extensions = mock(MockExtensions); const extensionList: vscode.Extension[] = []; + const fileSystem = mock(FileSystem); + when(fileSystem.fileExists(anything())).thenResolve(false); providerIds.forEach((id) => { const extension = TypeMoq.Mock.ofType>(); const packageJson = TypeMoq.Mock.ofType(); @@ -64,7 +67,7 @@ suite('DataScience URI Picker', () => { extensionList.push(extension.object); }); when(extensions.all).thenReturn(extensionList); - registration = new JupyterUriProviderRegistration(instance(extensions)); + registration = new JupyterUriProviderRegistration(instance(extensions), instance(fileSystem)); return registration; }