diff --git a/news/2 Fixes/5682.md b/news/2 Fixes/5682.md new file mode 100644 index 000000000000..ca11f285eda2 --- /dev/null +++ b/news/2 Fixes/5682.md @@ -0,0 +1 @@ +The extension will now prompt to auto install jupyter in case its not found. diff --git a/package.nls.json b/package.nls.json index 52148fe8a43a..ecaef58aacf6 100644 --- a/package.nls.json +++ b/package.nls.json @@ -142,6 +142,8 @@ "Linter.enableLinter": "Enable {0}", "Linter.enablePylint": "You have a pylintrc file in your workspace. Do you want to enable pylint?", "Linter.replaceWithSelectedLinter": "Multiple linters are enabled in settings. Replace with '{0}'?", + "DataScience.libraryNotInstalled": "Data Science library {0} is not installed. Install?", + "DataScience.jupyterInstall": "Install", "DataScience.jupyterSelectURILaunchLocal": "Launch a local Jupyter server when needed", "DataScience.jupyterSelectURISpecifyURI": "Type in the URI to connect to a running Jupyter server", "DataScience.jupyterSelectURIPrompt": "Enter the URI of a Jupyter server", diff --git a/src/client/common/installer/productInstaller.ts b/src/client/common/installer/productInstaller.ts index 02dd60ee3393..37e534903cd6 100644 --- a/src/client/common/installer/productInstaller.ts +++ b/src/client/common/installer/productInstaller.ts @@ -4,6 +4,7 @@ import { inject, injectable, named } from 'inversify'; import * as os from 'os'; import { OutputChannel, Uri } from 'vscode'; import '../../common/extensions'; +import * as localize from '../../common/utils/localize'; import { IServiceContainer } from '../../ioc/types'; import { LinterId } from '../../linters/types'; import { sendTelemetryEvent } from '../../telemetry'; @@ -76,7 +77,7 @@ export abstract class BaseInstaller { } public async isInstalled(product: Product, resource?: Uri): Promise { - if (product === Product.unittest) { + if (product === Product.unittest || product === Product.jupyter) { return true; } // User may have customized the module name or provided the fully qualified path. @@ -271,6 +272,14 @@ export class RefactoringLibraryInstaller extends BaseInstaller { } } +export class DataScienceInstaller extends BaseInstaller { + protected async promptToInstallImplementation(product: Product, resource?: Uri): Promise { + const productName = ProductNames.get(product)!; + const item = await this.appShell.showErrorMessage(localize.DataScience.libraryNotInstalled().format(productName), 'Yes', 'No'); + return item === 'Yes' ? this.install(product, resource) : InstallerResponse.Ignore; + } +} + @injectable() export class ProductInstaller implements IInstaller { private readonly productService: IProductService; @@ -307,6 +316,8 @@ export class ProductInstaller implements IInstaller { return new TestFrameworkInstaller(this.serviceContainer, this.outputChannel); case ProductType.RefactoringLibrary: return new RefactoringLibraryInstaller(this.serviceContainer, this.outputChannel); + case ProductType.DataScience: + return new DataScienceInstaller(this.serviceContainer, this.outputChannel); default: break; } @@ -333,6 +344,7 @@ function translateProductToModule(product: Product, purpose: ModuleNamePurpose): case Product.unittest: return 'unittest'; case Product.rope: return 'rope'; case Product.bandit: return 'bandit'; + case Product.jupyter: return 'jupyter'; default: { throw new Error(`Product ${product} cannot be installed as a Python Module.`); } diff --git a/src/client/common/installer/productNames.ts b/src/client/common/installer/productNames.ts index f8800d347d73..43004aa82e1b 100644 --- a/src/client/common/installer/productNames.ts +++ b/src/client/common/installer/productNames.ts @@ -19,3 +19,4 @@ ProductNames.set(Product.pylint, 'pylint'); ProductNames.set(Product.pytest, 'pytest'); ProductNames.set(Product.yapf, 'yapf'); ProductNames.set(Product.rope, 'rope'); +ProductNames.set(Product.jupyter, 'jupyter'); diff --git a/src/client/common/installer/productPath.ts b/src/client/common/installer/productPath.ts index 474c28c5e14f..43c7aac53d2e 100644 --- a/src/client/common/installer/productPath.ts +++ b/src/client/common/installer/productPath.ts @@ -99,3 +99,13 @@ export class RefactoringLibraryProductPathService extends BaseProductPathsServic return this.productInstaller.translateProductToModuleName(product, ModuleNamePurpose.run); } } + +@injectable() +export class DataScienceProductPathService extends BaseProductPathsService { + constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { + super(serviceContainer); + } + public getExecutableNameFromSettings(product: Product, _?: Uri): string { + return this.productInstaller.translateProductToModuleName(product, ModuleNamePurpose.run); + } +} diff --git a/src/client/common/installer/productService.ts b/src/client/common/installer/productService.ts index a6e0c7a53c0d..98a2f190956d 100644 --- a/src/client/common/installer/productService.ts +++ b/src/client/common/installer/productService.ts @@ -28,6 +28,7 @@ export class ProductService implements IProductService { this.ProductTypes.set(Product.black, ProductType.Formatter); this.ProductTypes.set(Product.yapf, ProductType.Formatter); this.ProductTypes.set(Product.rope, ProductType.RefactoringLibrary); + this.ProductTypes.set(Product.jupyter, ProductType.DataScience); } public getProductType(product: Product): ProductType { return this.ProductTypes.get(product)!; diff --git a/src/client/common/installer/serviceRegistry.ts b/src/client/common/installer/serviceRegistry.ts index 8ae4beffa549..0083fada0fd1 100644 --- a/src/client/common/installer/serviceRegistry.ts +++ b/src/client/common/installer/serviceRegistry.ts @@ -11,7 +11,7 @@ import { CondaInstaller } from './condaInstaller'; import { PipEnvInstaller } from './pipEnvInstaller'; import { PipInstaller } from './pipInstaller'; import { PoetryInstaller } from './poetryInstaller'; -import { CTagsProductPathService, FormatterProductPathService, LinterProductPathService, RefactoringLibraryProductPathService, TestFrameworkProductPathService } from './productPath'; +import { CTagsProductPathService, DataScienceProductPathService, FormatterProductPathService, LinterProductPathService, RefactoringLibraryProductPathService, TestFrameworkProductPathService } from './productPath'; import { ProductService } from './productService'; import { IInstallationChannelManager, IModuleInstaller, IProductPathService, IProductService } from './types'; @@ -28,5 +28,6 @@ export function registerTypes(serviceManager: IServiceManager) { serviceManager.addSingleton(IProductPathService, LinterProductPathService, ProductType.Linter); serviceManager.addSingleton(IProductPathService, TestFrameworkProductPathService, ProductType.TestFramework); serviceManager.addSingleton(IProductPathService, RefactoringLibraryProductPathService, ProductType.RefactoringLibrary); + serviceManager.addSingleton(IProductPathService, DataScienceProductPathService, ProductType.DataScience); serviceManager.addSingleton(IWebPanelProvider, WebPanelProvider); } diff --git a/src/client/common/types.ts b/src/client/common/types.ts index eee6c32b4ee3..3d29874cb52e 100644 --- a/src/client/common/types.ts +++ b/src/client/common/types.ts @@ -75,7 +75,8 @@ export enum ProductType { Formatter = 'Formatter', TestFramework = 'TestFramework', RefactoringLibrary = 'RefactoringLibrary', - WorkspaceSymbols = 'WorkspaceSymbols' + WorkspaceSymbols = 'WorkspaceSymbols', + DataScience = 'DataScience' } export enum Product { @@ -95,7 +96,8 @@ export enum Product { rope = 14, isort = 15, black = 16, - bandit = 17 + bandit = 17, + jupyter = 18 } export enum ModuleNamePurpose { diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index 4781b8492e9a..4101ac6a4210 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -100,6 +100,8 @@ export namespace DataScience { export const notebookCheckForImportYes = localize('DataScience.notebookCheckForImportYes', 'Import'); export const notebookCheckForImportNo = localize('DataScience.notebookCheckForImportNo', 'Later'); export const notebookCheckForImportDontAskAgain = localize('DataScience.notebookCheckForImportDontAskAgain', 'Don\'t Ask Again'); + export const libraryNotInstalled = localize('DataScience.libraryNotInstalled', 'Data Science library {0} is not installed. Install?'); + export const jupyterInstall = localize('DataScience.jupyterInstall', 'Install'); export const jupyterNotSupported = localize('DataScience.jupyterNotSupported', 'Jupyter is not installed'); export const jupyterNotSupportedBecauseOfEnvironment = localize('DataScience.jupyterNotSupportedBecauseOfEnvironment', 'Activating {0} to run Jupyter failed with {1}'); export const jupyterNbConvertNotSupported = localize('DataScience.jupyterNbConvertNotSupported', 'Jupyter nbconvert is not installed'); diff --git a/src/client/datascience/editor-integration/codewatcher.ts b/src/client/datascience/editor-integration/codewatcher.ts index de0d65adda8e..42e630d30fb1 100644 --- a/src/client/datascience/editor-integration/codewatcher.ts +++ b/src/client/datascience/editor-integration/codewatcher.ts @@ -4,17 +4,18 @@ import { inject, injectable } from 'inversify'; import { CodeLens, Position, Range, Selection, TextDocument, TextEditor, TextEditorRevealType } from 'vscode'; -import { IApplicationShell, IDocumentManager } from '../../common/application/types'; +import { IDocumentManager } from '../../common/application/types'; import { IFileSystem } from '../../common/platform/types'; -import { IConfigurationService, IDataScienceSettings, ILogger } from '../../common/types'; -import { noop } from '../../common/utils/misc'; +import { IConfigurationService, IDataScienceSettings } from '../../common/types'; +// import * as localize from '../../common/utils/localize'; import { StopWatch } from '../../common/utils/stopWatch'; +import { IServiceContainer } from '../../ioc/types'; import { captureTelemetry } from '../../telemetry'; import { ICodeExecutionHelper } from '../../terminals/types'; import { Commands, Telemetry } from '../constants'; -import { JupyterInstallError } from '../jupyter/jupyterInstallError'; -import { JupyterSelfCertsError } from '../jupyter/jupyterSelfCertsError'; -import { ICodeLensFactory, ICodeWatcher, IInteractiveWindowProvider } from '../types'; +// import { JupyterInstallError } from '../jupyter/jupyterInstallError'; +// import { JupyterSelfCertsError } from '../jupyter/jupyterSelfCertsError'; +import { ICodeLensFactory, ICodeWatcher, IDataScienceErrorHandler, IInteractiveWindowProvider } from '../types'; @injectable() export class CodeWatcher implements ICodeWatcher { @@ -24,15 +25,15 @@ export class CodeWatcher implements ICodeWatcher { private codeLenses: CodeLens[] = []; private cachedSettings: IDataScienceSettings | undefined; - constructor(@inject(IApplicationShell) private applicationShell: IApplicationShell, - @inject(ILogger) private logger: ILogger, - @inject(IInteractiveWindowProvider) private interactiveWindowProvider : IInteractiveWindowProvider, - @inject(IFileSystem) private fileSystem: IFileSystem, - @inject(IConfigurationService) private configService: IConfigurationService, - @inject(IDocumentManager) private documentManager : IDocumentManager, - @inject(ICodeExecutionHelper) private executionHelper: ICodeExecutionHelper, - @inject(ICodeLensFactory) private codeLensFactory: ICodeLensFactory - ) { + constructor(@inject(IInteractiveWindowProvider) private interactiveWindowProvider: IInteractiveWindowProvider, + @inject(IFileSystem) private fileSystem: IFileSystem, + @inject(IConfigurationService) private configService: IConfigurationService, + @inject(IDocumentManager) private documentManager: IDocumentManager, + @inject(ICodeExecutionHelper) private executionHelper: ICodeExecutionHelper, + @inject(IDataScienceErrorHandler) protected dataScienceErrorHandler: IDataScienceErrorHandler, + @inject(ICodeLensFactory) private codeLensFactory: ICodeLensFactory, + @inject(IServiceContainer) protected serviceContainer: IServiceContainer + ) { } public setDocument(document: TextDocument) { @@ -57,7 +58,7 @@ export class CodeWatcher implements ICodeWatcher { return this.version; } - public getCachedSettings() : IDataScienceSettings | undefined { + public getCachedSettings(): IDataScienceSettings | undefined { return this.cachedSettings; } @@ -143,13 +144,13 @@ export class CodeWatcher implements ICodeWatcher { } @captureTelemetry(Telemetry.RunSelectionOrLine) - public async runSelectionOrLine(activeEditor : TextEditor | undefined) { + public async runSelectionOrLine(activeEditor: TextEditor | undefined) { if (this.document && activeEditor && this.fileSystem.arePathsSame(activeEditor.document.fileName, this.document.fileName)) { // Get just the text of the selection or the current line if none const codeToExecute = await this.executionHelper.getSelectedTextToExecute(activeEditor); if (!codeToExecute) { - return ; + return; } const normalizedCode = await this.executionHelper.normalizeLines(codeToExecute!); if (!normalizedCode || normalizedCode.trim().length === 0) { @@ -184,7 +185,7 @@ export class CodeWatcher implements ICodeWatcher { } @captureTelemetry(Telemetry.RunCell) - public runCell(range: Range) : Promise { + public runCell(range: Range): Promise { if (!this.documentManager.activeTextEditor || !this.documentManager.activeTextEditor.document) { return Promise.resolve(); } @@ -195,7 +196,7 @@ export class CodeWatcher implements ICodeWatcher { } @captureTelemetry(Telemetry.DebugCurrentCell) - public debugCell(range: Range) : Promise { + public debugCell(range: Range): Promise { if (!this.documentManager.activeTextEditor || !this.documentManager.activeTextEditor.document) { return Promise.resolve(); } @@ -205,7 +206,7 @@ export class CodeWatcher implements ICodeWatcher { } @captureTelemetry(Telemetry.RunCurrentCell) - public runCurrentCell() : Promise { + public runCurrentCell(): Promise { if (!this.documentManager.activeTextEditor || !this.documentManager.activeTextEditor.document) { return Promise.resolve(); } @@ -224,7 +225,7 @@ export class CodeWatcher implements ICodeWatcher { return this.runMatchingCell(this.documentManager.activeTextEditor.selection, true); } - public async addEmptyCellToBottom() : Promise { + public async addEmptyCellToBottom(): Promise { const editor = this.documentManager.activeTextEditor; if (editor) { editor.edit((editBuilder) => { @@ -236,7 +237,7 @@ export class CodeWatcher implements ICodeWatcher { } } - private async addCode(code: string, file: string, line: number, editor?: TextEditor, debug?: boolean) : Promise { + private async addCode(code: string, file: string, line: number, editor?: TextEditor, debug?: boolean): Promise { try { const stopWatch = new StopWatch(); const activeInteractiveWindow = await this.interactiveWindowProvider.getOrCreateActive(); @@ -246,7 +247,7 @@ export class CodeWatcher implements ICodeWatcher { await activeInteractiveWindow.addCode(code, file, line, editor, stopWatch); } } catch (err) { - this.handleError(err); + this.dataScienceErrorHandler.handleError(err); } } @@ -279,11 +280,11 @@ export class CodeWatcher implements ICodeWatcher { } } - private getCurrentCellLens(pos: Position) : CodeLens | undefined { + private getCurrentCellLens(pos: Position): CodeLens | undefined { return this.codeLenses.find(l => l.range.contains(pos) && l.command !== undefined && l.command.command === Commands.RunCell); } - private getNextCellLens(pos: Position) : CodeLens | undefined { + private getNextCellLens(pos: Position): CodeLens | undefined { const currentIndex = this.codeLenses.findIndex(l => l.range.contains(pos) && l.command !== undefined && l.command.command === Commands.RunCell); if (currentIndex >= 0) { return this.codeLenses.find((l: CodeLens, i: number) => l.command !== undefined && l.command.command === Commands.RunCell && i > currentIndex); @@ -298,29 +299,6 @@ export class CodeWatcher implements ICodeWatcher { } } - // tslint:disable-next-line:no-any - private handleError = (err : any) => { - if (err instanceof JupyterInstallError) { - const jupyterError = err as JupyterInstallError; - - // This is a special error that shows a link to open for more help - this.applicationShell.showErrorMessage(jupyterError.message, jupyterError.actionTitle).then(v => { - // User clicked on the link, open it. - if (v === jupyterError.actionTitle) { - this.applicationShell.openUrl(jupyterError.action); - } - }); - } else if (err instanceof JupyterSelfCertsError) { - // Don't show the message for self cert errors - noop(); - } else if (err.message) { - this.applicationShell.showErrorMessage(err.message); - } else { - this.applicationShell.showErrorMessage(err.toString()); - } - this.logger.logError(err); - } - // User has picked run and advance on the last cell of a document // Create a new cell at the bottom and put their selection there, ready to type private createNewCell(currentRange: Range): Range { diff --git a/src/client/datascience/errorHandler/errorHandler.ts b/src/client/datascience/errorHandler/errorHandler.ts new file mode 100644 index 000000000000..fe68d314092a --- /dev/null +++ b/src/client/datascience/errorHandler/errorHandler.ts @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { inject, injectable } from 'inversify'; +import { IApplicationShell } from '../../common/application/types'; +import { IInstallationChannelManager } from '../../common/installer/types'; +import { ILogger, Product } from '../../common/types'; +import * as localize from '../../common/utils/localize'; +import { noop } from '../../common/utils/misc'; +import { JupyterInstallError } from '../jupyter/jupyterInstallError'; +import { JupyterSelfCertsError } from '../jupyter/jupyterSelfCertsError'; +import { IDataScienceErrorHandler } from '../types'; + +@injectable() +export class DataScienceErrorHandler implements IDataScienceErrorHandler { + constructor(@inject(IApplicationShell) private applicationShell: IApplicationShell, + @inject(ILogger) private logger: ILogger, + @inject(IInstallationChannelManager) protected channels: IInstallationChannelManager) { + } + + public handleError(err: Error) { + if (err instanceof JupyterInstallError) { + this.applicationShell.showInformationMessage( + localize.DataScience.jupyterNotSupported(), + localize.DataScience.jupyterInstall(), + localize.DataScience.notebookCheckForImportNo()) + .then(response => { + if (response === localize.DataScience.jupyterInstall()) { + return this.channels.getInstallationChannel(Product.jupyter); + } else { + const jupyterError = err as JupyterInstallError; + + // This is a special error that shows a link to open for more help + this.applicationShell.showErrorMessage(jupyterError.message, jupyterError.actionTitle).then(v => { + // User clicked on the link, open it. + if (v === jupyterError.actionTitle) { + this.applicationShell.openUrl(jupyterError.action); + } + }); + } + }).then(installer => { + if (installer) { + installer.installModule('jupyter') + .catch(e => this.applicationShell.showErrorMessage(e.message, localize.DataScience.pythonInteractiveHelpLink())); + } + }); + } else if (err instanceof JupyterSelfCertsError) { + // Don't show the message for self cert errors + noop(); + } else if (err.message) { + this.applicationShell.showErrorMessage(err.message); + } else { + this.applicationShell.showErrorMessage(err.toString()); + } + this.logger.logError(err); + } +} diff --git a/src/client/datascience/interactive-window/interactiveWindow.ts b/src/client/datascience/interactive-window/interactiveWindow.ts index e58856adf778..897b240fbdfa 100644 --- a/src/client/datascience/interactive-window/interactiveWindow.ts +++ b/src/client/datascience/interactive-window/interactiveWindow.ts @@ -77,8 +77,8 @@ import { } from './interactiveWindowTypes'; @injectable() -export class InteractiveWindow extends WebViewHost implements IInteractiveWindow { - private static sentExecuteCellTelemetry : boolean = false; +export class InteractiveWindow extends WebViewHost implements IInteractiveWindow { + private static sentExecuteCellTelemetry: boolean = false; private disposed: boolean = false; private loadPromise: Promise; private interpreterChangedDisposable: Disposable; @@ -89,14 +89,14 @@ export class InteractiveWindow extends WebViewHost im private addSysInfoPromise: Deferred | undefined; private waitingForExportCells: boolean = false; private jupyterServer: INotebookServer | undefined; - private id : string; + private id: string; private executeEvent: EventEmitter = new EventEmitter(); private variableRequestStopWatch: StopWatch | undefined; private variableRequestPendingCount: number = 0; constructor( @multiInject(IInteractiveWindowListener) private readonly listeners: IInteractiveWindowListener[], - @inject(ILiveShareApi) private liveShare : ILiveShareApi, + @inject(ILiveShareApi) private liveShare: ILiveShareApi, @inject(IApplicationShell) private applicationShell: IApplicationShell, @inject(IDocumentManager) private documentManager: IDocumentManager, @inject(IInterpreterService) private interpreterService: IInterpreterService, @@ -117,7 +117,7 @@ export class InteractiveWindow extends WebViewHost im @inject(IJupyterVariables) private jupyterVariables: IJupyterVariables, @inject(INotebookImporter) private jupyterImporter: INotebookImporter, @inject(IJupyterDebugger) private jupyterDebugger: IJupyterDebugger - ) { + ) { super( configuration, provider, @@ -153,7 +153,7 @@ export class InteractiveWindow extends WebViewHost im this.listeners.forEach(l => l.postMessage((e) => this.postMessageInternal(e.message, e.payload))); } - public get ready() : Promise { + public get ready(): Promise { // We need this to ensure the interactive window is up and ready to receive messages. return this.loadPromise; } @@ -175,16 +175,16 @@ export class InteractiveWindow extends WebViewHost im return this.closedEvent.event; } - public get onExecutedCode() : Event { + public get onExecutedCode(): Event { return this.executeEvent.event; } - public addCode(code: string, file: string, line: number, editor?: TextEditor, runningStopWatch?: StopWatch) : Promise { + public addCode(code: string, file: string, line: number, editor?: TextEditor, runningStopWatch?: StopWatch): Promise { // Call the internal method. return this.submitCode(code, file, line, undefined, editor, runningStopWatch, false); } - public debugCode(code: string, file: string, line: number, editor?: TextEditor, runningStopWatch?: StopWatch) : Promise { + public debugCode(code: string, file: string, line: number, editor?: TextEditor, runningStopWatch?: StopWatch): Promise { // Call the internal method. return this.submitCode(code, file, line, undefined, editor, runningStopWatch, true); } @@ -364,7 +364,7 @@ export class InteractiveWindow extends WebViewHost im } @captureTelemetry(Telemetry.RestartKernel) - public async restartKernel() : Promise { + public async restartKernel(): Promise { if (this.jupyterServer && !this.restartingKernel) { if (this.shouldAskForRestart()) { // Ask the user if they want us to restart or not. @@ -389,7 +389,7 @@ export class InteractiveWindow extends WebViewHost im } @captureTelemetry(Telemetry.Interrupt) - public async interruptKernel() : Promise { + public async interruptKernel(): Promise { if (this.jupyterServer && !this.restartingKernel) { const status = this.statusProvider.set(localize.DataScience.interruptKernelStatus()); @@ -421,7 +421,7 @@ export class InteractiveWindow extends WebViewHost im } } - public async previewNotebook(file: string) : Promise { + public async previewNotebook(file: string): Promise { try { // First convert to a python file to verify this file is valid. This is // an easy way to have something else verify the validity of the file. @@ -503,8 +503,8 @@ export class InteractiveWindow extends WebViewHost im } } - private addMessage(message: string, type: 'preview' | 'execute') : void { - const cell : ICell = { + private addMessage(message: string, type: 'preview' | 'execute'): void { + const cell: ICell = { id: uuid(), file: Identifiers.EmptyFileName, line: 0, @@ -522,17 +522,17 @@ export class InteractiveWindow extends WebViewHost im this.onAddCodeEvent([cell]); } - private addPreviewHeader(file: string) : void { + private addPreviewHeader(file: string): void { const message = localize.DataScience.previewHeader().format(file); this.addMessage(message, 'preview'); } - private addPreviewFooter(file: string) : void { + private addPreviewFooter(file: string): void { const message = localize.DataScience.previewFooter().format(file); this.addMessage(message, 'preview'); } - private async checkPandas() : Promise { + private async checkPandas(): Promise { const pandasVersion = await this.dataExplorerProvider.getPandasVersion(); if (!pandasVersion) { sendTelemetryEvent(Telemetry.PandasNotInstalled); @@ -562,7 +562,7 @@ export class InteractiveWindow extends WebViewHost im } } - private async checkColumnSize(columnSize: number) : Promise { + private async checkColumnSize(columnSize: number): Promise { if (columnSize > ColumnWarningSize && this.shouldAskForLargeData()) { const message = localize.DataScience.tooManyColumnsMessage(); const yes = localize.DataScience.tooManyColumnsYes(); @@ -578,7 +578,7 @@ export class InteractiveWindow extends WebViewHost im return true; } - private async showDataViewer(request: IShowDataViewer) : Promise { + private async showDataViewer(request: IShowDataViewer): Promise { try { if (await this.checkPandas() && await this.checkColumnSize(request.columnSize)) { await this.dataExplorerProvider.create(request.variableName); @@ -589,13 +589,13 @@ export class InteractiveWindow extends WebViewHost im } // tslint:disable-next-line:no-any - private dispatchMessage(_message: T, payload: any, handler: (args : M[T]) => void) { + private dispatchMessage(_message: T, payload: any, handler: (args: M[T]) => void) { const args = payload as M[T]; handler.bind(this)(args); } // tslint:disable-next-line:no-any - private onAddedSysInfo(sysInfo : IAddedSysInfo) { + private onAddedSysInfo(sysInfo: IAddedSysInfo) { // See if this is from us or not. if (sysInfo.id !== this.id) { @@ -701,12 +701,12 @@ export class InteractiveWindow extends WebViewHost im // Activate the other side, and send as if came from a file this.interactiveWindowProvider.getOrCreateActive().then(_v => { - this.shareMessage(InteractiveWindowMessages.RemoteAddCode, {code: info.code, file: Identifiers.EmptyFileName, line: 0, id: info.id, originator: this.id}); + this.shareMessage(InteractiveWindowMessages.RemoteAddCode, { code: info.code, file: Identifiers.EmptyFileName, line: 0, id: info.id, originator: this.id }); }).ignoreErrors(); } } - private async submitCode(code: string, file: string, line: number, id?: string, _editor?: TextEditor, runningStopWatch?: StopWatch, debug?: boolean) : Promise { + private async submitCode(code: string, file: string, line: number, id?: string, _editor?: TextEditor, runningStopWatch?: StopWatch, debug?: boolean): Promise { this.logger.logInformation(`Submitting code for ${this.id}`); // Start a status item @@ -715,7 +715,7 @@ export class InteractiveWindow extends WebViewHost im // Transmit this submission to all other listeners (in a live share session) if (!id) { id = uuid(); - this.shareMessage(InteractiveWindowMessages.RemoteAddCode, {code, file, line, id, originator: this.id}); + this.shareMessage(InteractiveWindowMessages.RemoteAddCode, { code, file, line, id, originator: this.id }); } // Create a deferred object that will wait until the status is disposed @@ -790,11 +790,11 @@ export class InteractiveWindow extends WebViewHost im traceInfo(`Finished execution for ${id}`); } } catch (err) { - status.dispose(); - const message = localize.DataScience.executingCodeFailure().format(err); this.applicationShell.showErrorMessage(message); } finally { + status.dispose(); + if (debug) { if (this.jupyterServer) { await this.jupyterDebugger.stopDebugging(this.jupyterServer); @@ -820,7 +820,7 @@ export class InteractiveWindow extends WebViewHost im return result; } - private logTelemetry = (event : Telemetry) => { + private logTelemetry = (event: Telemetry) => { sendTelemetryEvent(event); } @@ -871,7 +871,7 @@ export class InteractiveWindow extends WebViewHost im this.loadPromise = this.reloadWithNew(); } - private async reloadWithNew() : Promise { + private async reloadWithNew(): Promise { const status = this.setStatus(localize.DataScience.startingJupyter()); try { // Not the same as reload, we need to actually dispose the server. @@ -890,7 +890,7 @@ export class InteractiveWindow extends WebViewHost im } } - private async reloadAfterShutdown() : Promise { + private async reloadAfterShutdown(): Promise { try { if (this.loadPromise) { await this.loadPromise; @@ -1003,7 +1003,7 @@ export class InteractiveWindow extends WebViewHost im } } - private showInformationMessage(message: string, question?: string) : Thenable { + private showInformationMessage(message: string, question?: string): Thenable { if (question) { return this.applicationShell.showInformationMessage(message, question); } else { @@ -1161,8 +1161,8 @@ export class InteractiveWindow extends WebViewHost im } } - private async checkUsable() : Promise { - let activeInterpreter : PythonInterpreter | undefined; + private async checkUsable(): Promise { + let activeInterpreter: PythonInterpreter | undefined; try { activeInterpreter = await this.interpreterService.getActiveInterpreter(); const usableInterpreter = await this.jupyterExecution.getUsableJupyterPython(); @@ -1206,14 +1206,13 @@ export class InteractiveWindow extends WebViewHost im if (!usable) { // Not loading anymore status.dispose(); + this.dispose(); // Indicate failing. throw new JupyterInstallError(localize.DataScience.jupyterNotSupported(), localize.DataScience.pythonInteractiveHelpLink()); } - // Then load the jupyter server await this.loadJupyterServer(); - } catch (e) { if (e instanceof JupyterSelfCertsError) { // On a self cert error, warn the user and ask if they want to change the setting @@ -1243,7 +1242,7 @@ export class InteractiveWindow extends WebViewHost im // Request our new list of variables const vars: IJupyterVariable[] = await this.jupyterVariables.getVariables(); - const variablesResponse: IJupyterVariablesResponse = {executionCount: requestExecutionCount, variables: vars }; + const variablesResponse: IJupyterVariablesResponse = { executionCount: requestExecutionCount, variables: vars }; // Tag all of our jupyter variables with the execution count of the request variablesResponse.variables.forEach((value: IJupyterVariable) => { @@ -1256,7 +1255,7 @@ export class InteractiveWindow extends WebViewHost im if (excludeString) { const excludeArray = excludeString.split(';'); variablesResponse.variables = variablesResponse.variables.filter((value) => { - return excludeArray.indexOf(value.type) === -1; + return excludeArray.indexOf(value.type) === -1; }); } this.variableRequestPendingCount = variablesResponse.variables.length; @@ -1305,7 +1304,7 @@ export class InteractiveWindow extends WebViewHost im }); } - private async requestOnigasm() : Promise { + private async requestOnigasm(): Promise { // Look for the file next or our current file (this is where it's installed in the vsix) let filePath = path.join(__dirname, 'node_modules', 'onigasm', 'lib', 'onigasm.wasm'); traceInfo(`Request for onigasm file at ${filePath}`); diff --git a/src/client/datascience/interactive-window/interactiveWindowCommandListener.ts b/src/client/datascience/interactive-window/interactiveWindowCommandListener.ts index 723b19930ab7..709e7a65200b 100644 --- a/src/client/datascience/interactive-window/interactiveWindowCommandListener.ts +++ b/src/client/datascience/interactive-window/interactiveWindowCommandListener.ts @@ -19,8 +19,10 @@ import { captureTelemetry } from '../../telemetry'; import { CommandSource } from '../../testing/common/constants'; import { generateCellRanges, generateCellsFromDocument } from '../cellFactory'; import { Commands, Telemetry } from '../constants'; +import { JupyterInstallError } from '../jupyter/jupyterInstallError'; import { IDataScienceCommandListener, + IDataScienceErrorHandler, IInteractiveWindowProvider, IJupyterExecution, INotebookExporter, @@ -41,9 +43,10 @@ export class InteractiveWindowCommandListener implements IDataScienceCommandList @inject(IFileSystem) private fileSystem: IFileSystem, @inject(ILogger) private logger: ILogger, @inject(IConfigurationService) private configuration: IConfigurationService, - @inject(IStatusProvider) private statusProvider : IStatusProvider, - @inject(INotebookImporter) private jupyterImporter : INotebookImporter - ) { + @inject(IStatusProvider) private statusProvider: IStatusProvider, + @inject(INotebookImporter) private jupyterImporter: INotebookImporter, + @inject(IDataScienceErrorHandler) private dataScienceErrorHandler: IDataScienceErrorHandler + ) { // Listen to document open commands. We want to ask the user if they want to import. const disposable = this.documentManager.onDidOpenTextDocument(this.onOpenedDocument); this.disposableRegistry.push(disposable); @@ -102,7 +105,7 @@ export class InteractiveWindowCommandListener implements IDataScienceCommandList } // tslint:disable:no-any - private async listenForErrors(promise: () => Promise) : Promise { + private async listenForErrors(promise: () => Promise): Promise { let result: any; try { result = await promise(); @@ -123,7 +126,7 @@ export class InteractiveWindowCommandListener implements IDataScienceCommandList return result; } - private showInformationMessage(message: string, question?: string) : Thenable { + private showInformationMessage(message: string, question?: string): Thenable { if (question) { return this.applicationShell.showInformationMessage(message, question); } else { @@ -222,25 +225,26 @@ export class InteractiveWindowCommandListener implements IDataScienceCommandList } } } else { - this.applicationShell.showErrorMessage(localize.DataScience.jupyterNotSupported()); + this.dataScienceErrorHandler.handleError( + new JupyterInstallError(localize.DataScience.jupyterNotSupported(), localize.DataScience.pythonInteractiveHelpLink())); } } - private async exportCellsWithOutput(ranges: {range: Range; title: string}[], document: TextDocument, file: string, cancelToken: CancellationToken) : Promise { + private async exportCellsWithOutput(ranges: { range: Range; title: string }[], document: TextDocument, file: string, cancelToken: CancellationToken): Promise { let server: INotebookServer | undefined; try { const settings = this.configuration.getSettings(); - const useDefaultConfig : boolean | undefined = settings.datascience.useDefaultConfigForJupyter; + const useDefaultConfig: boolean | undefined = settings.datascience.useDefaultConfigForJupyter; // Try starting a server. Purpose should be unique so we // create a brand new one. - server = await this.jupyterExecution.connectToNotebookServer({ useDefaultConfig, purpose: uuid()}, cancelToken); + server = await this.jupyterExecution.connectToNotebookServer({ useDefaultConfig, purpose: uuid() }, cancelToken); // If that works, then execute all of the cells. const cells = Array.prototype.concat(... await Promise.all(ranges.map(r => { - const code = document.getText(r.range); - return server ? server.execute(code, document.fileName, r.range.start.line, uuid(), cancelToken) : []; - }))); + const code = document.getText(r.range); + return server ? server.execute(code, document.fileName, r.range.start.line, uuid(), cancelToken) : []; + }))); // Then save them to the file let directoryChange; @@ -258,7 +262,7 @@ export class InteractiveWindowCommandListener implements IDataScienceCommandList } } - private async showExportDialog() : Promise { + private async showExportDialog(): Promise { const filtersKey = localize.DataScience.exportDialogFilter(); const filtersObject: { [name: string]: string[] } = {}; filtersObject[filtersKey] = ['ipynb']; @@ -347,7 +351,7 @@ export class InteractiveWindowCommandListener implements IDataScienceCommandList private onOpenedDocument = async (document: TextDocument) => { // Preview and import the document if necessary. - const results = await Promise.all([this.previewNotebook(document.fileName), this.askForImportDocument(document)]); + const results = await Promise.all([this.previewNotebook(document.fileName), this.askForImportDocument(document)]); // When done, make sure the current document is still the active editor if we did // not do an import. Otherwise subsequent opens will cover up the interactive pane. @@ -356,7 +360,7 @@ export class InteractiveWindowCommandListener implements IDataScienceCommandList } } - private async previewNotebook(fileName: string) : Promise { + private async previewNotebook(fileName: string): Promise { if (fileName && fileName.endsWith('.ipynb') && this.autoPreviewNotebooks()) { // Get history before putting up status so that we show a busy message when we // start the preview. @@ -373,7 +377,7 @@ export class InteractiveWindowCommandListener implements IDataScienceCommandList return false; } - private async askForImportDocument(document: TextDocument) : Promise { + private async askForImportDocument(document: TextDocument): Promise { if (document.fileName.endsWith('.ipynb') && this.canImportFromOpenedFile()) { const yes = localize.DataScience.notebookCheckForImportYes(); const no = localize.DataScience.notebookCheckForImportNo(); @@ -399,18 +403,18 @@ export class InteractiveWindowCommandListener implements IDataScienceCommandList } @captureTelemetry(Telemetry.ShowHistoryPane, undefined, false) - private async showInteractiveWindow() : Promise{ + private async showInteractiveWindow(): Promise { const active = await this.interactiveWindowProvider.getOrCreateActive(); return active.show(); } - private waitForStatus(promise: () => Promise, format: string, file?: string, canceled?: () => void, skipHistory?: boolean) : Promise { + private waitForStatus(promise: () => Promise, format: string, file?: string, canceled?: () => void, skipHistory?: boolean): Promise { const message = file ? format.format(file) : format; return this.statusProvider.waitWithStatus(promise, message, undefined, canceled, skipHistory); } @captureTelemetry(Telemetry.ImportNotebook, { scope: 'command' }, false) - private async importNotebook() : Promise { + private async importNotebook(): Promise { const filtersKey = localize.DataScience.importDialogFilter(); const filtersObject: { [name: string]: string[] } = {}; filtersObject[filtersKey] = ['ipynb']; @@ -434,7 +438,7 @@ export class InteractiveWindowCommandListener implements IDataScienceCommandList } @captureTelemetry(Telemetry.ImportNotebook, { scope: 'file' }, false) - private async importNotebookOnFile(file: string, preview: boolean) : Promise { + private async importNotebookOnFile(file: string, preview: boolean): Promise { if (file && file.length > 0) { // Preview a file whenever we import if not already previewed if (preview) { @@ -442,14 +446,14 @@ export class InteractiveWindowCommandListener implements IDataScienceCommandList } await this.waitForStatus(async () => { - const contents = await this.jupyterImporter.importFromFile(file); + const contents = await this.jupyterImporter.importFromFile(file); await this.viewDocument(contents); }, localize.DataScience.importingFormat(), file); } } - private viewDocument = async (contents: string) : Promise => { - const doc = await this.documentManager.openTextDocument({language: 'python', content: contents}); + private viewDocument = async (contents: string): Promise => { + const doc = await this.documentManager.openTextDocument({ language: 'python', content: contents }); const editor = await this.documentManager.showTextDocument(doc, ViewColumn.One); // Edit the document so that it is dirty (add a space at the end) diff --git a/src/client/datascience/jupyter/jupyterExecution.ts b/src/client/datascience/jupyter/jupyterExecution.ts index c5eb82e1a1ac..316b8bc614bd 100644 --- a/src/client/datascience/jupyter/jupyterExecution.ts +++ b/src/client/datascience/jupyter/jupyterExecution.ts @@ -36,6 +36,7 @@ import { INotebookServerOptions } from '../types'; import { JupyterConnection, JupyterServerInfo } from './jupyterConnection'; +import { JupyterInstallError } from './jupyterInstallError'; import { JupyterKernelSpec } from './jupyterKernelSpec'; import { JupyterSelfCertsError } from './jupyterSelfCertsError'; import { JupyterWaitForIdleError } from './jupyterWaitForIdleError'; @@ -204,7 +205,7 @@ export class JupyterExecutionBase implements IJupyterExecution { // First we find a way to start a notebook server const notebookCommand = await this.findBestCommandTimed(JupyterCommands.NotebookCommand); if (!notebookCommand) { - throw new Error(localize.DataScience.jupyterNotSupported()); + throw new JupyterInstallError(localize.DataScience.jupyterNotSupported(), localize.DataScience.pythonInteractiveHelpLink()); } const args: string[] = [`--NotebookApp.file_to_run=${file}`]; @@ -337,7 +338,7 @@ export class JupyterExecutionBase implements IJupyterExecution { // First we find a way to start a notebook server const notebookCommand = await this.findBestCommandTimed(JupyterCommands.NotebookCommand, cancelToken); if (!notebookCommand) { - throw new Error(localize.DataScience.jupyterNotSupported()); + throw new JupyterInstallError(localize.DataScience.jupyterNotSupported(), localize.DataScience.pythonInteractiveHelpLink()); } // Now actually launch it @@ -775,7 +776,7 @@ export class JupyterExecutionBase implements IJupyterExecution { return true; } - private async findBestCommandTimed(command: string, cancelToken?: CancellationToken) : Promise { + private async findBestCommandTimed(command: string, cancelToken?: CancellationToken): Promise { // Only log telemetry if not already found (meaning the first time) let timer: StopWatch | undefined; if (!this.commands.hasOwnProperty(command)) { diff --git a/src/client/datascience/serviceRegistry.ts b/src/client/datascience/serviceRegistry.ts index c181344ad02c..d28dae8f10bd 100644 --- a/src/client/datascience/serviceRegistry.ts +++ b/src/client/datascience/serviceRegistry.ts @@ -16,6 +16,7 @@ import { CodeLensFactory } from './editor-integration/codeLensFactory'; import { DataScienceCodeLensProvider } from './editor-integration/codelensprovider'; import { CodeWatcher } from './editor-integration/codewatcher'; import { Decorator } from './editor-integration/decorator'; +import { DataScienceErrorHandler } from './errorHandler/errorHandler'; import { DebugListener } from './interactive-window/debugListener'; import { DotNetIntellisenseProvider } from './interactive-window/intellisense/dotNetIntellisenseProvider'; import { JediIntellisenseProvider } from './interactive-window/intellisense/jediIntellisenseProvider'; @@ -45,6 +46,7 @@ import { IDataScience, IDataScienceCodeLensProvider, IDataScienceCommandListener, + IDataScienceErrorHandler, IDataViewer, IDataViewerProvider, IInteractiveWindow, @@ -66,7 +68,7 @@ import { } from './types'; // tslint:disable:no-any -function wrapType(ctor: ClassType) : ClassType { +function wrapType(ctor: ClassType): ClassType { return class extends ctor { constructor(...args: any[]) { const stopWatch = new StopWatch(); @@ -111,6 +113,7 @@ export function registerTypes(serviceManager: IServiceManager) { serviceManager.addSingleton(IPlotViewerProvider, wrapType(PlotViewerProvider)); serviceManager.add(IPlotViewer, wrapType(PlotViewer)); serviceManager.addSingleton(IJupyterDebugger, wrapType(JupyterDebugger)); + serviceManager.add(IDataScienceErrorHandler, wrapType(DataScienceErrorHandler)); serviceManager.addSingleton(ICodeLensFactory, wrapType(CodeLensFactory)); serviceManager.addSingleton(ICellHashProvider, wrapType(CellHashProvider)); serviceManager.addBinding(ICellHashProvider, IInteractiveWindowListener); diff --git a/src/client/datascience/types.ts b/src/client/datascience/types.ts index f9f58d8c5204..807b5e15a721 100644 --- a/src/client/datascience/types.ts +++ b/src/client/datascience/types.ts @@ -172,6 +172,11 @@ export interface IInteractiveWindowProvider { getNotebookOptions(): Promise; } +export const IDataScienceErrorHandler = Symbol('IDataScienceErrorHandler'); +export interface IDataScienceErrorHandler { + handleError(err: Error): void; +} + export const IInteractiveWindow = Symbol('IInteractiveWindow'); export interface IInteractiveWindow extends Disposable { closed: Event; diff --git a/src/test/common/installer.test.ts b/src/test/common/installer.test.ts index 8eb649d073c3..04e008783ef6 100644 --- a/src/test/common/installer.test.ts +++ b/src/test/common/installer.test.ts @@ -7,7 +7,7 @@ import { WorkspaceService } from '../../client/common/application/workspace'; import { ConfigurationService } from '../../client/common/configuration/service'; import { InstallationChannelManager } from '../../client/common/installer/channelManager'; import { ProductInstaller } from '../../client/common/installer/productInstaller'; -import { CTagsProductPathService, FormatterProductPathService, LinterProductPathService, RefactoringLibraryProductPathService, TestFrameworkProductPathService } from '../../client/common/installer/productPath'; +import { CTagsProductPathService, DataScienceProductPathService, FormatterProductPathService, LinterProductPathService, RefactoringLibraryProductPathService, TestFrameworkProductPathService } from '../../client/common/installer/productPath'; import { ProductService } from '../../client/common/installer/productService'; import { IInstallationChannelManager, IModuleInstaller, IProductPathService, IProductService } from '../../client/common/installer/types'; import { Logger } from '../../client/common/logger'; @@ -74,6 +74,7 @@ suite('Installer', () => { ioc.serviceManager.addSingleton(IProductPathService, LinterProductPathService, ProductType.Linter); ioc.serviceManager.addSingleton(IProductPathService, TestFrameworkProductPathService, ProductType.TestFramework); ioc.serviceManager.addSingleton(IProductPathService, RefactoringLibraryProductPathService, ProductType.RefactoringLibrary); + ioc.serviceManager.addSingleton(IProductPathService, DataScienceProductPathService, ProductType.DataScience); } async function resetSettings() { await updateSetting('linting.pylintEnabled', true, rootWorkspaceUri, ConfigurationTarget.Workspace); @@ -98,7 +99,7 @@ suite('Installer', () => { ioc.serviceManager.addSingletonInstance(IModuleInstaller, new MockModuleInstaller('one', false)); ioc.serviceManager.addSingletonInstance(IModuleInstaller, new MockModuleInstaller('two', true)); ioc.serviceManager.addSingletonInstance(ITerminalHelper, instance(mock(TerminalHelper))); - if (prod.value === Product.ctags || prod.value === Product.unittest || prod.value === Product.isort) { + if (prod.value === Product.ctags || prod.value === Product.unittest || prod.value === Product.isort || prod.value === Product.jupyter) { return; } await testCheckingIfProductIsInstalled(prod.value); @@ -125,7 +126,7 @@ suite('Installer', () => { ioc.serviceManager.addSingletonInstance(IModuleInstaller, new MockModuleInstaller('one', false)); ioc.serviceManager.addSingletonInstance(IModuleInstaller, new MockModuleInstaller('two', true)); ioc.serviceManager.addSingletonInstance(ITerminalHelper, instance(mock(TerminalHelper))); - if (prod.value === Product.unittest || prod.value === Product.ctags || prod.value === Product.isort) { + if (prod.value === Product.unittest || prod.value === Product.ctags || prod.value === Product.isort || prod.value === Product.jupyter) { return; } await testInstallingProduct(prod.value); diff --git a/src/test/common/installer/installer.invalidPath.unit.test.ts b/src/test/common/installer/installer.invalidPath.unit.test.ts index a95b55d81421..51ea5007646a 100644 --- a/src/test/common/installer/installer.invalidPath.unit.test.ts +++ b/src/test/common/installer/installer.invalidPath.unit.test.ts @@ -55,7 +55,8 @@ suite('Module Installer - Invalid Paths', () => { case Product.isort: case Product.ctags: case Product.rope: - case Product.unittest: { + case Product.unittest: + case Product.jupyter: { return; } default: { diff --git a/src/test/common/installer/productPath.unit.test.ts b/src/test/common/installer/productPath.unit.test.ts index 4ba0017a7cb4..3c769f380d06 100644 --- a/src/test/common/installer/productPath.unit.test.ts +++ b/src/test/common/installer/productPath.unit.test.ts @@ -12,7 +12,7 @@ import * as TypeMoq from 'typemoq'; import { OutputChannel, Uri } from 'vscode'; import '../../../client/common/extensions'; import { ProductInstaller } from '../../../client/common/installer/productInstaller'; -import { CTagsProductPathService, FormatterProductPathService, LinterProductPathService, RefactoringLibraryProductPathService, TestFrameworkProductPathService } from '../../../client/common/installer/productPath'; +import { CTagsProductPathService, DataScienceProductPathService, FormatterProductPathService, LinterProductPathService, RefactoringLibraryProductPathService, TestFrameworkProductPathService } from '../../../client/common/installer/productPath'; import { ProductService } from '../../../client/common/installer/productService'; import { IProductService } from '../../../client/common/installer/types'; import { IConfigurationService, IFormattingSettings, IInstaller, IPythonSettings, ITestingSettings, IWorkspaceSymbolSettings, ModuleNamePurpose, Product, ProductType } from '../../../client/common/types'; @@ -179,6 +179,16 @@ suite('Product Path', () => { }); break; } + case ProductType.DataScience: { + test(`Ensure path is returned for ${product.name} (${resource ? 'With a resource' : 'without a resource'})`, async () => { + const productPathService = new DataScienceProductPathService(serviceContainer.object); + + const value = productPathService.getExecutableNameFromSettings(product.value, resource); + const moduleName = productInstaller.translateProductToModuleName(product.value, ModuleNamePurpose.run); + expect(value).to.be.equal(moduleName); + }); + break; + } default: { test(`No tests for Product Path of this Product Type ${product.name}`, () => { fail('No tests for Product Path of this Product Type'); diff --git a/src/test/datascience/dataScienceIocContainer.ts b/src/test/datascience/dataScienceIocContainer.ts index 25d6d59d4ce7..86d32d250185 100644 --- a/src/test/datascience/dataScienceIocContainer.ts +++ b/src/test/datascience/dataScienceIocContainer.ts @@ -41,6 +41,8 @@ import { import { AsyncDisposableRegistry } from '../../client/common/asyncDisposableRegistry'; import { PythonSettings } from '../../client/common/configSettings'; import { EXTENSION_ROOT_DIR } from '../../client/common/constants'; +import { InstallationChannelManager } from '../../client/common/installer/channelManager'; +import { IInstallationChannelManager } from '../../client/common/installer/types'; import { Logger } from '../../client/common/logger'; import { PersistentStateFactory } from '../../client/common/persistentState'; import { IS_WINDOWS } from '../../client/common/platform/constants'; @@ -91,6 +93,7 @@ import { DataViewerProvider } from '../../client/datascience/data-viewing/dataVi import { CellHashProvider } from '../../client/datascience/editor-integration/cellhashprovider'; import { CodeLensFactory } from '../../client/datascience/editor-integration/codeLensFactory'; import { CodeWatcher } from '../../client/datascience/editor-integration/codewatcher'; +import { DataScienceErrorHandler } from '../../client/datascience/errorHandler/errorHandler'; import { DotNetIntellisenseProvider } from '../../client/datascience/interactive-window/intellisense/dotNetIntellisenseProvider'; @@ -119,6 +122,7 @@ import { ICodeWatcher, IDataScience, IDataScienceCommandListener, + IDataScienceErrorHandler, IDataViewer, IDataViewerProvider, IInteractiveWindow, @@ -227,7 +231,7 @@ export class DataScienceIocContainer extends UnitTestIocContainer { public wrapperCreatedPromise: Deferred | undefined; public postMessage: ((ev: MessageEvent) => void) | undefined; // tslint:disable-next-line:no-any - private missedMessages : any[] = []; + private missedMessages: any[] = []; private pythonSettings = new class extends PythonSettings { public fireChangeEvent() { this.changed.fire(); @@ -325,6 +329,8 @@ export class DataScienceIocContainer extends UnitTestIocContainer { this.serviceManager.add(ICodeWatcher, CodeWatcher); this.serviceManager.add(ICodeExecutionHelper, CodeExecutionHelper); this.serviceManager.add(IDataScienceCommandListener, InteractiveWindowCommandListener); + this.serviceManager.add(IDataScienceErrorHandler, DataScienceErrorHandler); + this.serviceManager.add(IInstallationChannelManager, InstallationChannelManager); this.serviceManager.addSingleton(IJupyterVariables, JupyterVariables); this.serviceManager.addSingleton(IJupyterDebugger, JupyterDebugger); @@ -552,23 +558,23 @@ export class DataScienceIocContainer extends UnitTestIocContainer { // Setup the webpanel provider so that it returns our dummy web panel. It will have to talk to our global JSDOM window so that the react components can link into it webPanelProvider.setup(p => p.create(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAnyString(), TypeMoq.It.isAnyString(), TypeMoq.It.isAnyString(), TypeMoq.It.isAny())).returns( (_viewColumn: ViewColumn, listener: IWebPanelMessageListener, _title: string, _script: string, _css: string) => { - // Keep track of the current listener. It listens to messages through the vscode api - this.webPanelListener = listener; - - // Send messages that were already posted but were missed. - // During normal operation, the react control will not be created before - // the webPanelListener - if (this.missedMessages.length && this.webPanelListener) { - this.missedMessages.forEach(m => this.webPanelListener ? this.webPanelListener.onMessage(m.type, m.payload) : noop()); - - // Note, you might think we should clean up the messages. However since the mount only occurs once, we might - // create multiple webpanels with the same mount. We need to resend these messages to - // other webpanels that get created with the same mount. - } + // Keep track of the current listener. It listens to messages through the vscode api + this.webPanelListener = listener; + + // Send messages that were already posted but were missed. + // During normal operation, the react control will not be created before + // the webPanelListener + if (this.missedMessages.length && this.webPanelListener) { + this.missedMessages.forEach(m => this.webPanelListener ? this.webPanelListener.onMessage(m.type, m.payload) : noop()); + + // Note, you might think we should clean up the messages. However since the mount only occurs once, we might + // create multiple webpanels with the same mount. We need to resend these messages to + // other webpanels that get created with the same mount. + } - // Return our dummy web panel - return webPanel.object; - }); + // Return our dummy web panel + return webPanel.object; + }); webPanel.setup(p => p.postMessage(TypeMoq.It.isAny())).callback((m: WebPanelMessage) => { const message = createMessageEvent(m); if (this.postMessage) { @@ -605,7 +611,7 @@ export class DataScienceIocContainer extends UnitTestIocContainer { this.pythonSettings.pythonPath = newPath; this.pythonSettings.fireChangeEvent(); this.configChangeEvent.fire({ - affectsConfiguration(_s: string, _r?: Uri) : boolean { + affectsConfiguration(_s: string, _r?: Uri): boolean { return true; } }); @@ -615,7 +621,7 @@ export class DataScienceIocContainer extends UnitTestIocContainer { return this.jupyterMock; } - public get(serviceIdentifier: interfaces.ServiceIdentifier, name?: string | number | symbol) : T { + public get(serviceIdentifier: interfaces.ServiceIdentifier, name?: string | number | symbol): T { return this.serviceManager.get(serviceIdentifier, name); } diff --git a/src/test/datascience/editor-integration/codewatcher.unit.test.ts b/src/test/datascience/editor-integration/codewatcher.unit.test.ts index 483de93a8cbb..956f0a000e9d 100644 --- a/src/test/datascience/editor-integration/codewatcher.unit.test.ts +++ b/src/test/datascience/editor-integration/codewatcher.unit.test.ts @@ -6,21 +6,19 @@ import { expect } from 'chai'; import * as TypeMoq from 'typemoq'; import { CancellationTokenSource, CodeLens, Disposable, Range, Selection, TextEditor } from 'vscode'; - import { - IApplicationShell, ICommandManager, IDebugService, IDocumentManager } from '../../../client/common/application/types'; import { PythonSettings } from '../../../client/common/configSettings'; import { IFileSystem } from '../../../client/common/platform/types'; -import { IConfigurationService, ILogger } from '../../../client/common/types'; +import { IConfigurationService } from '../../../client/common/types'; import { Commands, EditorContexts } from '../../../client/datascience/constants'; import { CodeLensFactory } from '../../../client/datascience/editor-integration/codeLensFactory'; import { DataScienceCodeLensProvider } from '../../../client/datascience/editor-integration/codelensprovider'; import { CodeWatcher } from '../../../client/datascience/editor-integration/codewatcher'; -import { ICodeWatcher, IInteractiveWindow, IInteractiveWindowProvider } from '../../../client/datascience/types'; +import { ICodeWatcher, IDataScienceErrorHandler, IInteractiveWindow, IInteractiveWindowProvider } from '../../../client/datascience/types'; import { IServiceContainer } from '../../../client/ioc/types'; import { ICodeExecutionHelper } from '../../../client/terminals/types'; import { MockAutoSelectionService } from '../../mocks/autoSelector'; @@ -30,8 +28,6 @@ import { createDocument } from './helpers'; suite('DataScience Code Watcher Unit Tests', () => { let codeWatcher: CodeWatcher; - let appShell: TypeMoq.IMock; - let logger: TypeMoq.IMock; let interactiveWindowProvider: TypeMoq.IMock; let activeInteractiveWindow: TypeMoq.IMock; let documentManager: TypeMoq.IMock; @@ -39,11 +35,12 @@ suite('DataScience Code Watcher Unit Tests', () => { let textEditor: TypeMoq.IMock; let fileSystem: TypeMoq.IMock; let configService: TypeMoq.IMock; - let serviceContainer : TypeMoq.IMock; + let dataScienceErrorHandler: TypeMoq.IMock; + let serviceContainer: TypeMoq.IMock; let helper: TypeMoq.IMock; - let tokenSource : CancellationTokenSource; + let tokenSource: CancellationTokenSource; let debugService: TypeMoq.IMock; - const contexts : Map = new Map(); + const contexts: Map = new Map(); const pythonSettings = new class extends PythonSettings { public fireChangeEvent() { this.changed.fire(); @@ -53,8 +50,6 @@ suite('DataScience Code Watcher Unit Tests', () => { setup(() => { tokenSource = new CancellationTokenSource(); - appShell = TypeMoq.Mock.ofType(); - logger = TypeMoq.Mock.ofType(); interactiveWindowProvider = TypeMoq.Mock.ofType(); activeInteractiveWindow = createTypeMoq('history'); documentManager = TypeMoq.Mock.ofType(); @@ -94,8 +89,12 @@ suite('DataScience Code Watcher Unit Tests', () => { // Setup the service container to return code watchers serviceContainer = TypeMoq.Mock.ofType(); + const codeLensFactory = new CodeLensFactory(configService.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(ICodeWatcher))).returns(() => new CodeWatcher(appShell.object, logger.object, interactiveWindowProvider.object, fileSystem.object, configService.object, documentManager.object, helper.object, codeLensFactory)); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(ICodeWatcher))).returns(() => new CodeWatcher(interactiveWindowProvider.object, fileSystem.object, configService.object, documentManager.object, helper.object, dataScienceErrorHandler.object, codeLensFactory, serviceContainer.object)); + + // Setup our error handler + dataScienceErrorHandler = TypeMoq.Mock.ofType(); // Setup our active history instance interactiveWindowProvider.setup(h => h.getOrCreateActive()).returns(() => Promise.resolve(activeInteractiveWindow.object)); @@ -118,7 +117,7 @@ suite('DataScience Code Watcher Unit Tests', () => { const codeLens = new CodeLensFactory(configService.object); - codeWatcher = new CodeWatcher(appShell.object, logger.object, interactiveWindowProvider.object, fileSystem.object, configService.object, documentManager.object, helper.object, codeLens); + codeWatcher = new CodeWatcher(interactiveWindowProvider.object, fileSystem.object, configService.object, documentManager.object, helper.object, dataScienceErrorHandler.object, codeLens, serviceContainer.object); }); function createTypeMoq(tag: string): TypeMoq.IMock { @@ -195,7 +194,7 @@ suite('DataScience Code Watcher Unit Tests', () => { const fileName = 'test.py'; const version = 1; const inputText = -`first line + `first line second line #%% @@ -226,7 +225,7 @@ fourth line`; const fileName = 'test.py'; const version = 1; const inputText = -`first line + `first line second line # @@ -264,7 +263,7 @@ fourth line const fileName = 'test.py'; const version = 1; const inputText = -`first line + `first line second line # @@ -309,12 +308,12 @@ fourth line // Set up our expected call to add code activeInteractiveWindow.setup(h => h.addCode(TypeMoq.It.isValue(testString), - TypeMoq.It.isValue(fileName), - TypeMoq.It.isValue(0), - TypeMoq.It.is((ed: TextEditor) => { - return textEditor.object === ed; - }), - TypeMoq.It.isAny())).verifiable(TypeMoq.Times.once()); + TypeMoq.It.isValue(fileName), + TypeMoq.It.isValue(0), + TypeMoq.It.is((ed: TextEditor) => { + return textEditor.object === ed; + }), + TypeMoq.It.isAny())).verifiable(TypeMoq.Times.once()); // Try our RunCell command await codeWatcher.runCell(testRange); @@ -328,7 +327,7 @@ fourth line const fileName = 'test.py'; const version = 1; const inputText = -`#%% + `#%% testing1 #%% testing2`; // Command tests override getText, so just need the ranges here @@ -339,11 +338,11 @@ testing2`; // Command tests override getText, so just need the ranges here // Set up our expected calls to add code // RunFileInteractive should run the entire file in one block, not cell by cell like RunAllCells activeInteractiveWindow.setup(h => h.addCode(TypeMoq.It.isValue(inputText), - TypeMoq.It.isValue('test.py'), - TypeMoq.It.isValue(0), - TypeMoq.It.isAny(), - TypeMoq.It.isAny() - )).verifiable(TypeMoq.Times.once()); + TypeMoq.It.isValue('test.py'), + TypeMoq.It.isValue(0), + TypeMoq.It.isAny(), + TypeMoq.It.isAny() + )).verifiable(TypeMoq.Times.once()); await codeWatcher.runFileInteractive(); @@ -356,7 +355,7 @@ testing2`; // Command tests override getText, so just need the ranges here const fileName = 'test.py'; const version = 1; const inputText = -`#%% + `#%% testing1 #%% testing2`; // Command tests override getText, so just need the ranges here @@ -374,18 +373,18 @@ testing2`; // Command tests override getText, so just need the ranges here // Set up our expected calls to add code activeInteractiveWindow.setup(h => h.addCode(TypeMoq.It.isValue(testString1), - TypeMoq.It.isValue('test.py'), - TypeMoq.It.isValue(0), - TypeMoq.It.isAny(), - TypeMoq.It.isAny() - )).verifiable(TypeMoq.Times.once()); + TypeMoq.It.isValue('test.py'), + TypeMoq.It.isValue(0), + TypeMoq.It.isAny(), + TypeMoq.It.isAny() + )).verifiable(TypeMoq.Times.once()); activeInteractiveWindow.setup(h => h.addCode(TypeMoq.It.isValue(testString2), - TypeMoq.It.isValue('test.py'), - TypeMoq.It.isValue(2), - TypeMoq.It.isAny(), - TypeMoq.It.isAny() - )).verifiable(TypeMoq.Times.once()); + TypeMoq.It.isValue('test.py'), + TypeMoq.It.isValue(2), + TypeMoq.It.isAny(), + TypeMoq.It.isAny() + )).verifiable(TypeMoq.Times.once()); await codeWatcher.runAllCells(); @@ -398,7 +397,7 @@ testing2`; // Command tests override getText, so just need the ranges here const fileName = 'test.py'; const version = 1; const inputText = -`#%% + `#%% testing1 #%% testing2`; @@ -409,13 +408,13 @@ testing2`; // Set up our expected calls to add code activeInteractiveWindow.setup(h => h.addCode(TypeMoq.It.isValue('testing2'), - TypeMoq.It.isValue(fileName), - TypeMoq.It.isValue(2), - TypeMoq.It.is((ed: TextEditor) => { - return textEditor.object === ed; - }), - TypeMoq.It.isAny() - )).verifiable(TypeMoq.Times.once()); + TypeMoq.It.isValue(fileName), + TypeMoq.It.isValue(2), + TypeMoq.It.is((ed: TextEditor) => { + return textEditor.object === ed; + }), + TypeMoq.It.isAny() + )).verifiable(TypeMoq.Times.once()); // For this test we need to set up a document selection point textEditor.setup(te => te.selection).returns(() => new Selection(2, 0, 2, 0)); @@ -431,18 +430,18 @@ testing2`; const fileName = 'test.py'; const version = 1; const inputText = -`#%% + `#%% testing1 #%% testing2 #%% testing3`; const targetText1 = -`#%% + `#%% testing2`; const targetText2 = -`#%% + `#%% testing3`; const document = createDocument(inputText, fileName, version, TypeMoq.Times.atLeastOnce(), true); @@ -451,18 +450,18 @@ testing3`; // Set up our expected calls to add code activeInteractiveWindow.setup(h => h.addCode(TypeMoq.It.isValue(targetText1), - TypeMoq.It.isValue(fileName), - TypeMoq.It.isValue(2), - TypeMoq.It.isAny(), - TypeMoq.It.isAny() - )).verifiable(TypeMoq.Times.once()); + TypeMoq.It.isValue(fileName), + TypeMoq.It.isValue(2), + TypeMoq.It.isAny(), + TypeMoq.It.isAny() + )).verifiable(TypeMoq.Times.once()); activeInteractiveWindow.setup(h => h.addCode(TypeMoq.It.isValue(targetText2), - TypeMoq.It.isValue(fileName), - TypeMoq.It.isValue(4), - TypeMoq.It.isAny(), - TypeMoq.It.isAny() - )).verifiable(TypeMoq.Times.once()); + TypeMoq.It.isValue(fileName), + TypeMoq.It.isValue(4), + TypeMoq.It.isAny(), + TypeMoq.It.isAny() + )).verifiable(TypeMoq.Times.once()); await codeWatcher.runCellAndAllBelow(2, 0); @@ -475,18 +474,18 @@ testing3`; const fileName = 'test.py'; const version = 1; const inputText = -`#%% + `#%% testing1 #%% testing2 #%% testing3`; const targetText1 = -`#%% + `#%% testing1`; const targetText2 = -`#%% + `#%% testing2`; const document = createDocument(inputText, fileName, version, TypeMoq.Times.atLeastOnce(), true); @@ -495,18 +494,18 @@ testing2`; // Set up our expected calls to add code activeInteractiveWindow.setup(h => h.addCode(TypeMoq.It.isValue(targetText1), - TypeMoq.It.isValue(fileName), - TypeMoq.It.isValue(0), - TypeMoq.It.isAny(), - TypeMoq.It.isAny() - )).verifiable(TypeMoq.Times.once()); + TypeMoq.It.isValue(fileName), + TypeMoq.It.isValue(0), + TypeMoq.It.isAny(), + TypeMoq.It.isAny() + )).verifiable(TypeMoq.Times.once()); activeInteractiveWindow.setup(h => h.addCode(TypeMoq.It.isValue(targetText2), - TypeMoq.It.isValue(fileName), - TypeMoq.It.isValue(2), - TypeMoq.It.isAny(), - TypeMoq.It.isAny() - )).verifiable(TypeMoq.Times.once()); + TypeMoq.It.isValue(fileName), + TypeMoq.It.isValue(2), + TypeMoq.It.isAny(), + TypeMoq.It.isAny() + )).verifiable(TypeMoq.Times.once()); await codeWatcher.runAllCellsAbove(4, 0); @@ -519,14 +518,14 @@ testing2`; const fileName = 'test.py'; const version = 1; const inputText = -`#%% + `#%% testing1 #%% testing2 #%% testing3`; const targetText = -`#%% + `#%% testing1`; const document = createDocument(inputText, fileName, version, TypeMoq.Times.atLeastOnce(), true); @@ -535,11 +534,11 @@ testing1`; // Set up our expected calls to add code activeInteractiveWindow.setup(h => h.addCode(TypeMoq.It.isValue(targetText), - TypeMoq.It.isValue(fileName), - TypeMoq.It.isValue(0), - TypeMoq.It.isAny(), - TypeMoq.It.isAny() - )).verifiable(TypeMoq.Times.once()); + TypeMoq.It.isValue(fileName), + TypeMoq.It.isValue(0), + TypeMoq.It.isAny(), + TypeMoq.It.isAny() + )).verifiable(TypeMoq.Times.once()); await codeWatcher.runToLine(2); @@ -552,7 +551,7 @@ testing1`; const fileName = 'test.py'; const version = 1; const inputText = -` + ` print('testing')`; @@ -563,11 +562,11 @@ print('testing')`; // If adding empty lines nothing should be added and history should not be started interactiveWindowProvider.setup(h => h.getOrCreateActive()).returns(() => Promise.resolve(activeInteractiveWindow.object)).verifiable(TypeMoq.Times.never()); activeInteractiveWindow.setup(h => h.addCode(TypeMoq.It.isAny(), - TypeMoq.It.isValue(fileName), - TypeMoq.It.isAnyNumber(), - TypeMoq.It.isAny(), - TypeMoq.It.isAny() - )).verifiable(TypeMoq.Times.never()); + TypeMoq.It.isValue(fileName), + TypeMoq.It.isAnyNumber(), + TypeMoq.It.isAny(), + TypeMoq.It.isAny() + )).verifiable(TypeMoq.Times.never()); await codeWatcher.runToLine(2); @@ -581,14 +580,14 @@ print('testing')`; const fileName = 'test.py'; const version = 1; const inputText = -`#%% + `#%% testing1 #%% testing2 #%% testing3`; const targetText = -`#%% + `#%% testing2 #%% testing3`; @@ -599,11 +598,11 @@ testing3`; // Set up our expected calls to add code activeInteractiveWindow.setup(h => h.addCode(TypeMoq.It.isValue(targetText), - TypeMoq.It.isValue(fileName), - TypeMoq.It.isValue(2), - TypeMoq.It.isAny(), - TypeMoq.It.isAny() - )).verifiable(TypeMoq.Times.once()); + TypeMoq.It.isValue(fileName), + TypeMoq.It.isValue(2), + TypeMoq.It.isAny(), + TypeMoq.It.isAny() + )).verifiable(TypeMoq.Times.once()); // Try our RunCell command with the first selection point await codeWatcher.runFromLine(2); @@ -617,7 +616,7 @@ testing3`; const fileName = 'test.py'; const version = 1; const inputText = -`#%% + `#%% testing1 #%% testing2`; @@ -631,13 +630,13 @@ testing2`; // Set up our expected calls to add code activeInteractiveWindow.setup(h => h.addCode(TypeMoq.It.isValue('testing2'), - TypeMoq.It.isValue(fileName), - TypeMoq.It.isValue(3), - TypeMoq.It.is((ed: TextEditor) => { - return textEditor.object === ed; - }), - TypeMoq.It.isAny() - )).verifiable(TypeMoq.Times.once()); + TypeMoq.It.isValue(fileName), + TypeMoq.It.isValue(3), + TypeMoq.It.is((ed: TextEditor) => { + return textEditor.object === ed; + }), + TypeMoq.It.isAny() + )).verifiable(TypeMoq.Times.once()); // For this test we need to set up a document selection point textEditor.setup(te => te.document).returns(() => document.object); @@ -655,7 +654,7 @@ testing2`; const fileName = 'test.py'; const version = 1; const inputText = -`#%% + `#%% testing1 #%% testing2`; // Command tests override getText, so just need the ranges here @@ -668,13 +667,13 @@ testing2`; // Command tests override getText, so just need the ranges here // Set up our expected calls to add code activeInteractiveWindow.setup(h => h.addCode(TypeMoq.It.isValue(testString), - TypeMoq.It.isValue('test.py'), - TypeMoq.It.isValue(0), - TypeMoq.It.is((ed: TextEditor) => { - return textEditor.object === ed; - }), - TypeMoq.It.isAny() - )).verifiable(TypeMoq.Times.once()); + TypeMoq.It.isValue('test.py'), + TypeMoq.It.isValue(0), + TypeMoq.It.is((ed: TextEditor) => { + return textEditor.object === ed; + }), + TypeMoq.It.isAny() + )).verifiable(TypeMoq.Times.once()); // For this test we need to set up a document selection point const selection = new Selection(0, 0, 0, 0); diff --git a/src/test/datascience/interactiveWindowCommandListener.unit.test.ts b/src/test/datascience/interactiveWindowCommandListener.unit.test.ts index 0f9eef3ab9b4..0bf51769050e 100644 --- a/src/test/datascience/interactiveWindowCommandListener.unit.test.ts +++ b/src/test/datascience/interactiveWindowCommandListener.unit.test.ts @@ -18,6 +18,7 @@ import { IFileSystem } from '../../client/common/platform/types'; import { IConfigurationService, IDisposable, ILogger } from '../../client/common/types'; import { generateCells } from '../../client/datascience/cellFactory'; import { Commands } from '../../client/datascience/constants'; +import { DataScienceErrorHandler } from '../../client/datascience/errorHandler/errorHandler'; import { InteractiveWindowCommandListener } from '../../client/datascience/interactive-window/interactiveWindowCommandListener'; import { InteractiveWindowProvider } from '../../client/datascience/interactive-window/interactiveWindowProvider'; import { JupyterExecutionFactory } from '../../client/datascience/jupyter/jupyterExecutionFactory'; @@ -69,6 +70,7 @@ suite('Interactive window command listener', async () => { const pythonSettings = new PythonSettings(undefined, new MockAutoSelectionService()); const disposableRegistry: IDisposable[] = []; const interactiveWindowProvider = mock(InteractiveWindowProvider); + const dataScienceErrorHandler = mock(DataScienceErrorHandler); const notebookImporter = mock(JupyterImporter); const notebookExporter = mock(JupyterExporter); const applicationShell = mock(ApplicationShell); @@ -216,7 +218,8 @@ suite('Interactive window command listener', async () => { instance(logger), instance(configService), statusProvider, - instance(notebookImporter)); + instance(notebookImporter), + instance(dataScienceErrorHandler)); result.register(commandManager); return result; diff --git a/src/test/linters/lint.multiroot.test.ts b/src/test/linters/lint.multiroot.test.ts index fb328c00b914..2fda48c4a7b6 100644 --- a/src/test/linters/lint.multiroot.test.ts +++ b/src/test/linters/lint.multiroot.test.ts @@ -2,7 +2,7 @@ import * as assert from 'assert'; import * as path from 'path'; import { CancellationTokenSource, ConfigurationTarget, OutputChannel, Uri, workspace } from 'vscode'; import { PythonSettings } from '../../client/common/configSettings'; -import { CTagsProductPathService, FormatterProductPathService, LinterProductPathService, RefactoringLibraryProductPathService, TestFrameworkProductPathService } from '../../client/common/installer/productPath'; +import { CTagsProductPathService, DataScienceProductPathService, FormatterProductPathService, LinterProductPathService, RefactoringLibraryProductPathService, TestFrameworkProductPathService } from '../../client/common/installer/productPath'; import { ProductService } from '../../client/common/installer/productService'; import { IProductPathService, IProductService } from '../../client/common/installer/types'; import { IConfigurationService, IOutputChannel, Product, ProductType } from '../../client/common/types'; @@ -50,7 +50,7 @@ suite('Multiroot Linting', () => { ioc.serviceManager.addSingleton(IProductPathService, LinterProductPathService, ProductType.Linter); ioc.serviceManager.addSingleton(IProductPathService, TestFrameworkProductPathService, ProductType.TestFramework); ioc.serviceManager.addSingleton(IProductPathService, RefactoringLibraryProductPathService, ProductType.RefactoringLibrary); - + ioc.serviceManager.addSingleton(IProductPathService, DataScienceProductPathService, ProductType.DataScience); } async function createLinter(product: Product, resource?: Uri): Promise { diff --git a/src/test/linters/lint.test.ts b/src/test/linters/lint.test.ts index 0e09b5604809..5f3ef1c1da8e 100644 --- a/src/test/linters/lint.test.ts +++ b/src/test/linters/lint.test.ts @@ -8,7 +8,7 @@ import { ConfigurationTarget, Uri } from 'vscode'; import { WorkspaceService } from '../../client/common/application/workspace'; import { Product } from '../../client/common/installer/productInstaller'; import { - CTagsProductPathService, FormatterProductPathService, LinterProductPathService, + CTagsProductPathService, DataScienceProductPathService, FormatterProductPathService, LinterProductPathService, RefactoringLibraryProductPathService, TestFrameworkProductPathService } from '../../client/common/installer/productPath'; import { ProductService } from '../../client/common/installer/productService'; @@ -64,6 +64,7 @@ suite('Linting Settings', () => { ioc.serviceManager.addSingleton(IProductPathService, LinterProductPathService, ProductType.Linter); ioc.serviceManager.addSingleton(IProductPathService, TestFrameworkProductPathService, ProductType.TestFramework); ioc.serviceManager.addSingleton(IProductPathService, RefactoringLibraryProductPathService, ProductType.RefactoringLibrary); + ioc.serviceManager.addSingleton(IProductPathService, DataScienceProductPathService, ProductType.DataScience); } async function resetSettings(lintingEnabled = true) {