diff --git a/package.nls.json b/package.nls.json index 1894c63a689e..d1013d8101ff 100644 --- a/package.nls.json +++ b/package.nls.json @@ -59,24 +59,26 @@ "LanguageServiceSurveyBanner.bannerMessage": "Can you please take 2 minutes to tell us how the Python Language Server is working for you?", "LanguageServiceSurveyBanner.bannerLabelYes": "Yes, take survey now", "LanguageServiceSurveyBanner.bannerLabelNo": "No, thanks", - "DataScience.unknownMimeType" : "Unknown mime type for data", - "DataScience.historyTitle" : "Python Interactive", + "DataScience.unknownMimeType": "Unknown mime type for data", + "DataScience.historyTitle": "Python Interactive", "DataScience.badWebPanelFormatString": "

{0} is not a valid file name

", - "DataScience.sessionDisposed" : "Cannot execute code, session has been disposed.", - "DataScience.exportDialogTitle" : "Export to Jupyter Notebook", - "DataScience.exportDialogFilter" : "Jupyter Notebooks", - "DataScience.exportDialogComplete" : "Notebook written to {0}", - "DataScience.exportDialogFailed" : "Failed to export notebook. {0}", - "DataScience.exportOpenQuestion" : "Open in browser", - "DataScience.collapseInputTooltip" : "Collapse input block", - "DataScience.importDialogTitle" : "Import Jupyter Notebook", - "DataScience.importDialogFilter" : "Jupyter Notebooks", - "DataScience.notebookCheckForImportYes" : "Import", - "DataScience.notebookCheckForImportNo" : "Later", - "DataScience.notebookCheckForImportDontAskAgain" : "Don't Ask Again", - "DataScience.notebookCheckForImportTitle" : "Do you want to import the Jupyter Notebook into Python code?", - "DataScience.jupyterNotSupported" : "Jupyter is not installed", - "DataScience.jupyterNbConvertNotSupported" : "Jupyter nbconvert is not installed", - "DataScience.importingFormat" : "Importing {0}", - "DataScience.startingJupyter" : "Starting Jupyter backend" + "DataScience.sessionDisposed": "Cannot execute code, session has been disposed.", + "DataScience.exportDialogTitle": "Export to Jupyter Notebook", + "DataScience.exportDialogFilter": "Jupyter Notebooks", + "DataScience.exportDialogComplete": "Notebook written to {0}", + "DataScience.exportDialogFailed": "Failed to export notebook. {0}", + "DataScience.exportOpenQuestion": "Open in browser", + "DataScience.collapseInputTooltip": "Collapse input block", + "DataScience.importDialogTitle": "Import Jupyter Notebook", + "DataScience.importDialogFilter": "Jupyter Notebooks", + "DataScience.notebookCheckForImportYes": "Import", + "DataScience.notebookCheckForImportNo": "Later", + "DataScience.notebookCheckForImportDontAskAgain": "Don't Ask Again", + "DataScience.notebookCheckForImportTitle": "Do you want to import the Jupyter Notebook into Python code?", + "DataScience.jupyterNotSupported": "Jupyter is not installed", + "DataScience.jupyterNbConvertNotSupported": "Jupyter nbconvert is not installed", + "DataScience.importingFormat": "Importing {0}", + "DataScience.startingJupyter": "Starting Jupyter backend", + "Interpreters.RefreshingInterpreters": "Refreshing Python Interpreters", + "Interpreters.LoadingInterpreters": "Loading Python Interpreters" } diff --git a/src/client/activation/interpreterDataService.ts b/src/client/activation/interpreterDataService.ts index daa595ebf383..1e082bb171d5 100644 --- a/src/client/activation/interpreterDataService.ts +++ b/src/client/activation/interpreterDataService.ts @@ -40,7 +40,7 @@ export class InterpreterDataService { } const cacheKey = `InterpreterData-${interpreterPath}`; - let interpreterData = this.context.globalState.get(cacheKey) as InterpreterData; + let interpreterData = this.context.globalState.get(cacheKey); let interpreterChanged = false; if (interpreterData) { // Check if interpreter executable changed diff --git a/src/client/activation/languageServer/languageServerFolderService.ts b/src/client/activation/languageServer/languageServerFolderService.ts index 20e88669462d..57be87a36c50 100644 --- a/src/client/activation/languageServer/languageServerFolderService.ts +++ b/src/client/activation/languageServer/languageServerFolderService.ts @@ -65,8 +65,8 @@ export class LanguageServerFolderService implements ILanguageServerFolderService if (dirs.length === 0) { return; } - const sortedDirs = dirs.sort((a, b) => a.version.compare(b.version)); - return sortedDirs[sortedDirs.length - 1]; + dirs.sort((a, b) => a.version.compare(b.version)); + return dirs[dirs.length - 1]; } public async getExistingLanguageServerDirectories(): Promise { const fs = this.serviceContainer.get(IFileSystem); diff --git a/src/client/application/diagnostics/checks/invalidPythonPathInDebugger.ts b/src/client/application/diagnostics/checks/invalidPythonPathInDebugger.ts index 643ea97ed586..1e31ca4f6fcd 100644 --- a/src/client/application/diagnostics/checks/invalidPythonPathInDebugger.ts +++ b/src/client/application/diagnostics/checks/invalidPythonPathInDebugger.ts @@ -16,7 +16,7 @@ import { DiagnosticCodes } from '../constants'; import { DiagnosticCommandPromptHandlerServiceId, MessageCommandPrompt } from '../promptHandler'; import { DiagnosticScope, IDiagnostic, IDiagnosticHandlerService, IInvalidPythonPathInDebuggerService } from '../types'; -const InvalidPythonPathInDebuggerMessage = 'You need to select a Python interpreter before you start debugging.\n\nTip: click on "Select Python Environment" in the status bar.'; +const InvalidPythonPathInDebuggerMessage = 'You need to select a Python interpreter before you start debugging.\n\nTip: click on "Select Python Interpreter" in the status bar.'; export class InvalidPythonPathInDebuggerDiagnostic extends BaseDiagnostic { constructor() { diff --git a/src/client/common/application/applicationShell.ts b/src/client/common/application/applicationShell.ts index e956becd2204..a66e5c4e9594 100644 --- a/src/client/common/application/applicationShell.ts +++ b/src/client/common/application/applicationShell.ts @@ -6,7 +6,7 @@ const opn = require('opn'); import { injectable } from 'inversify'; -import { CancellationToken, Disposable, InputBoxOptions, MessageItem, MessageOptions, OpenDialogOptions, QuickPickItem, QuickPickOptions, SaveDialogOptions, StatusBarAlignment, StatusBarItem, Uri, window, WorkspaceFolder, WorkspaceFolderPickOptions } from 'vscode'; +import { CancellationToken, Disposable, InputBoxOptions, MessageItem, MessageOptions, OpenDialogOptions, Progress, ProgressOptions, QuickPickItem, QuickPickOptions, SaveDialogOptions, StatusBarAlignment, StatusBarItem, Uri, window, WorkspaceFolder, WorkspaceFolderPickOptions } from 'vscode'; import { IApplicationShell } from './types'; @injectable() @@ -67,5 +67,7 @@ export class ApplicationShell implements IApplicationShell { public showWorkspaceFolderPick(options?: WorkspaceFolderPickOptions): Thenable { return window.showWorkspaceFolderPick(options); } - + public withProgress(options: ProgressOptions, task: (progress: Progress<{ message?: string; increment?: number }>, token: CancellationToken) => Thenable): Thenable { + return window.withProgress(options, task); + } } diff --git a/src/client/common/application/types.ts b/src/client/common/application/types.ts index 1e045842ed99..070c4a6e0f67 100644 --- a/src/client/common/application/types.ts +++ b/src/client/common/application/types.ts @@ -7,9 +7,9 @@ import { Breakpoint, BreakpointsChangeEvent, CancellationToken, ConfigurationChangeEvent, DebugConfiguration, DebugConfigurationProvider, DebugConsole, DebugSession, DebugSessionCustomEvent, Disposable, Event, FileSystemWatcher, GlobPattern, InputBoxOptions, MessageItem, - MessageOptions, OpenDialogOptions, QuickPickItem, QuickPickOptions, SaveDialogOptions, StatusBarAlignment, StatusBarItem, - Terminal, TerminalOptions, TextDocument, TextDocumentShowOptions, TextEditor, TextEditorEdit, TextEditorOptionsChangeEvent, TextEditorSelectionChangeEvent, - TextEditorViewColumnChangeEvent, Uri, ViewColumn, WorkspaceConfiguration, WorkspaceEdit, WorkspaceFolder, WorkspaceFolderPickOptions, WorkspaceFoldersChangeEvent + MessageOptions, OpenDialogOptions, Progress, ProgressOptions, QuickPickItem, QuickPickOptions, SaveDialogOptions, + StatusBarAlignment, StatusBarItem, Terminal, TerminalOptions, TextDocument, TextDocumentShowOptions, TextEditor, TextEditorEdit, + TextEditorOptionsChangeEvent, TextEditorSelectionChangeEvent, TextEditorViewColumnChangeEvent, Uri, ViewColumn, WorkspaceConfiguration, WorkspaceEdit, WorkspaceFolder, WorkspaceFolderPickOptions, WorkspaceFoldersChangeEvent } from 'vscode'; export const IApplicationShell = Symbol('IApplicationShell'); @@ -248,6 +248,27 @@ export interface IApplicationShell { * @return A promise that resolves to the workspace folder or `undefined`. */ showWorkspaceFolderPick(options?: WorkspaceFolderPickOptions): Thenable; + + /** + * Show progress in the editor. Progress is shown while running the given callback + * and while the promise it returned isn't resolved nor rejected. The location at which + * progress should show (and other details) is defined via the passed [`ProgressOptions`](#ProgressOptions). + * + * @param task A callback returning a promise. Progress state can be reported with + * the provided [progress](#Progress)-object. + * + * To report discrete progress, use `increment` to indicate how much work has been completed. Each call with + * a `increment` value will be summed up and reflected as overall progress until 100% is reached (a value of + * e.g. `10` accounts for `10%` of work done). + * Note that currently only `ProgressLocation.Notification` is capable of showing discrete progress. + * + * To monitor if the operation has been cancelled by the user, use the provided [`CancellationToken`](#CancellationToken). + * Note that currently only `ProgressLocation.Notification` is supporting to show a cancel button to cancel the + * long running operation. + * + * @return The thenable the task-callback returned. + */ + withProgress(options: ProgressOptions, task: (progress: Progress<{ message?: string; increment?: number }>, token: CancellationToken) => Thenable): Thenable; } export const ICommandManager = Symbol('ICommandManager'); diff --git a/src/client/common/editor.ts b/src/client/common/editor.ts index 8f70a2714f48..696b3b90fe63 100644 --- a/src/client/common/editor.ts +++ b/src/client/common/editor.ts @@ -158,7 +158,7 @@ function getTextEditsInternal(before: string, diffs: [number, string][], startLi let line = startLine; let character = 0; if (line > 0) { - const beforeLines = before.split(/\r?\n/g); + const beforeLines = before.split(/\r?\n/g); beforeLines.filter((l, i) => i < line).forEach(l => character += l.length + NEW_LINE_LENGTH); } const edits: Edit[] = []; diff --git a/src/client/common/envFileParser.ts b/src/client/common/envFileParser.ts index 18ac1655ee51..52d2234c21b8 100644 --- a/src/client/common/envFileParser.ts +++ b/src/client/common/envFileParser.ts @@ -9,7 +9,7 @@ function parseEnvironmentVariables(contents: string): EnvironmentVariables | und return undefined; } - const env = {} as EnvironmentVariables; + const env: EnvironmentVariables = {}; contents.split('\n').forEach(line => { const match = line.match(/^\s*([\w\.\-]+)\s*=\s*(.*)?\s*$/); if (match !== null) { @@ -40,7 +40,9 @@ export function parseEnvFile(envFile: string, mergeWithProcessEnvVars: boolean = export function mergeEnvVariables(targetEnvVars: EnvironmentVariables, sourceEnvVars: EnvironmentVariables = process.env): EnvironmentVariables { const service = new EnvironmentVariablesService(new PathUtils(IS_WINDOWS)); service.mergeVariables(sourceEnvVars, targetEnvVars); - service.appendPythonPath(targetEnvVars, sourceEnvVars.PYTHONPATH); + if (sourceEnvVars.PYTHONPATH) { + service.appendPythonPath(targetEnvVars, sourceEnvVars.PYTHONPATH); + } return targetEnvVars; } @@ -57,6 +59,6 @@ export function mergePythonPath(env: EnvironmentVariables, currentPythonPath: st return env; } const service = new EnvironmentVariablesService(new PathUtils(IS_WINDOWS)); - service.appendPythonPath(env, currentPythonPath!); + service.appendPythonPath(env, currentPythonPath); return env; } diff --git a/src/client/common/installer/channelManager.ts b/src/client/common/installer/channelManager.ts index 7c5780d9152e..d145f6216b10 100644 --- a/src/client/common/installer/channelManager.ts +++ b/src/client/common/installer/channelManager.ts @@ -41,13 +41,13 @@ export class InstallationChannelManager implements IInstallationChannelManager { } public async getInstallationChannels(resource?: Uri): Promise { - let installers = this.serviceContainer.getAll(IModuleInstaller); + const installers = this.serviceContainer.getAll(IModuleInstaller); const supportedInstallers: IModuleInstaller[] = []; if (installers.length === 0) { return []; } // group by priority and pick supported from the highest priority - installers = installers.sort((a, b) => b.priority - a.priority); + installers.sort((a, b) => b.priority - a.priority); let currentPri = installers[0].priority; for (const mi of installers) { if (mi.priority !== currentPri) { diff --git a/src/client/common/utils/async.ts b/src/client/common/utils/async.ts index ed7d6b4869cf..9c5c53dc7892 100644 --- a/src/client/common/utils/async.ts +++ b/src/client/common/utils/async.ts @@ -64,3 +64,12 @@ class DeferredImpl implements Deferred { export function createDeferred(scope: any = null): Deferred { return new DeferredImpl(scope); } + +export function createDeferredFrom(...promises: Promise[]): Deferred { + const deferred = createDeferred(); + Promise.all(promises) + .then(deferred.resolve.bind(deferred)) + .catch(deferred.reject.bind(deferred)); + + return deferred; +} diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index 107a1c0a0d51..86a87663dcad 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -14,6 +14,11 @@ export namespace LanguageServiceSurveyBanner { export const bannerLabelNo = localize('LanguageServiceSurveyBanner.bannerLabelNo', 'No, thanks'); } +export namespace Interpreters { + export const loading = localize('Interpreters.LoadingInterpreters', 'Loading Python Interpreters'); + export const refreshing = localize('Interpreters.RefreshingInterpreters', 'Refreshing Python Interpreters'); +} + export namespace DataScience { export const historyTitle = localize('DataScience.historyTitle', 'Python Interactive'); export const badWebPanelFormatString = localize('DataScience.badWebPanelFormatString', '

{0} is not a valid file name

'); diff --git a/src/client/debugger/extension/configProviders/baseProvider.ts b/src/client/debugger/extension/configProviders/baseProvider.ts index 04d34d0a15fc..ad7a65769a07 100644 --- a/src/client/debugger/extension/configProviders/baseProvider.ts +++ b/src/client/debugger/extension/configProviders/baseProvider.ts @@ -68,7 +68,7 @@ export abstract class BaseConfigurationProvider implements DebugConfigurationPro debugConfiguration.cwd = workspaceFolder.fsPath; } if (typeof debugConfiguration.envFile !== 'string' && workspaceFolder) { - const envFile = workspaceFolder ? path.join(workspaceFolder.fsPath, '.env') : ''; + const envFile = path.join(workspaceFolder.fsPath, '.env'); debugConfiguration.envFile = envFile; } if (typeof debugConfiguration.stopOnEntry !== 'boolean') { diff --git a/src/client/extension.ts b/src/client/extension.ts index 1a609546f089..88f7cd240083 100644 --- a/src/client/extension.ts +++ b/src/client/extension.ts @@ -41,7 +41,7 @@ import { registerTypes as debugConfigurationRegisterTypes } from './debugger/ext import { IDebugConfigurationProvider, IDebuggerBanner } from './debugger/extension/types'; import { registerTypes as formattersRegisterTypes } from './formatters/serviceRegistry'; import { IInterpreterSelector } from './interpreter/configuration/types'; -import { ICondaService, IInterpreterService, PythonInterpreter } from './interpreter/contracts'; +import { ICondaService, IInterpreterLocatorProgressService, IInterpreterService, InterpreterLocatorProgressHandler, PythonInterpreter } from './interpreter/contracts'; import { registerTypes as interpretersRegisterTypes } from './interpreter/serviceRegistry'; import { ServiceContainer } from './ioc/container'; import { ServiceManager } from './ioc/serviceManager'; @@ -102,7 +102,8 @@ export async function activate(context: ExtensionContext): Promise(ICodeExecutionManager).registerCommands(); sendStartupTelemetry(activationDeferred.promise, serviceContainer).ignoreErrors(); - interpreterManager.refresh() + const workspaceService = serviceContainer.get(IWorkspaceService); + interpreterManager.refresh(workspaceService.hasWorkspaceFolders ? workspaceService.workspaceFolders![0].uri : undefined) .catch(ex => console.error('Python Extension: interpreterManager.refresh', ex)); const jupyterExtension = extensions.getExtension('donjayamanne.jupyter'); @@ -214,8 +215,17 @@ function initializeServices(context: ExtensionContext, serviceManager: ServiceMa const disposables = serviceManager.get(IDisposableRegistry); const dispatcher = new DebugSessionEventDispatcher(handlers, DebugService.instance, disposables); dispatcher.registerEventHandlers(); -} + // Display progress of interpreter refreshes only after extension has activated. + serviceContainer.get(InterpreterLocatorProgressHandler).register(); + serviceContainer.get(IInterpreterLocatorProgressService).register(); + + // Get latest interpreter list. + const workspaceService = serviceContainer.get(IWorkspaceService); + const mainWorkspaceUri = workspaceService.hasWorkspaceFolders ? workspaceService.workspaceFolders![0].uri : undefined; + const interpreterService = serviceContainer.get(IInterpreterService); + interpreterService.getInterpreters(mainWorkspaceUri).ignoreErrors(); +} async function sendStartupTelemetry(activatedPromise: Promise, serviceContainer: IServiceContainer) { const logger = serviceContainer.get(ILogger); try { @@ -224,12 +234,13 @@ async function sendStartupTelemetry(activatedPromise: Promise, serviceCont const terminalShellType = terminalHelper.identifyTerminalShell(terminalHelper.getTerminalShellPath()); const condaLocator = serviceContainer.get(ICondaService); const interpreterService = serviceContainer.get(IInterpreterService); + const workspaceService = serviceContainer.get(IWorkspaceService); + const mainWorkspaceUri = workspaceService.hasWorkspaceFolders ? workspaceService.workspaceFolders![0].uri : undefined; const [condaVersion, interpreter, interpreters] = await Promise.all([ condaLocator.getCondaVersion().then(ver => ver ? ver.raw : '').catch(() => ''), interpreterService.getActiveInterpreter().catch(() => undefined), - interpreterService.getInterpreters().catch(() => []) + interpreterService.getInterpreters(mainWorkspaceUri).catch(() => []) ]); - const workspaceService = serviceContainer.get(IWorkspaceService); const workspaceFolderCount = workspaceService.hasWorkspaceFolders ? workspaceService.workspaceFolders!.length : 0; const pythonVersion = interpreter ? interpreter.version_info.join('.') : undefined; const interpreterType = interpreter ? interpreter.type : undefined; diff --git a/src/client/interpreter/contracts.ts b/src/client/interpreter/contracts.ts index e687ab557bd2..932b3abceb4c 100644 --- a/src/client/interpreter/contracts.ts +++ b/src/client/interpreter/contracts.ts @@ -28,6 +28,7 @@ export interface IVirtualEnvironmentsSearchPathProvider { export const IInterpreterLocatorService = Symbol('IInterpreterLocatorService'); export interface IInterpreterLocatorService extends Disposable { + readonly onLocating: Event>; getInterpreters(resource?: Uri): Promise; } @@ -83,7 +84,7 @@ export interface IInterpreterService { autoSetInterpreter(): Promise; getActiveInterpreter(resource?: Uri): Promise; getInterpreterDetails(pythonPath: string, resoure?: Uri): Promise; - refresh(): Promise; + refresh(resource: Uri | undefined): Promise; initialize(): void; getDisplayName(interpreter: Partial): Promise; shouldAutoSetInterpreter(): Promise; @@ -126,3 +127,15 @@ export const IInterpreterWatcherBuilder = Symbol('IInterpreterWatcherBuilder'); export interface IInterpreterWatcherBuilder { getWorkspaceVirtualEnvInterpreterWatcher(resource: Uri | undefined): Promise; } + +export const InterpreterLocatorProgressHandler = Symbol('InterpreterLocatorProgressHandler'); +export interface InterpreterLocatorProgressHandler { + register(): void; +} + +export const IInterpreterLocatorProgressService = Symbol('IInterpreterLocatorProgressService'); +export interface IInterpreterLocatorProgressService { + readonly onRefreshing: Event; + readonly onRefreshed: Event; + register(): void; +} diff --git a/src/client/interpreter/display/index.ts b/src/client/interpreter/display/index.ts index bba7dc3bd39e..281ad604f940 100644 --- a/src/client/interpreter/display/index.ts +++ b/src/client/interpreter/display/index.ts @@ -47,7 +47,7 @@ export class InterpreterDisplay implements IInterpreterDisplay { } else { this.statusBar.tooltip = ''; this.statusBar.color = 'yellow'; - this.statusBar.text = '$(alert) Select Python Environment'; + this.statusBar.text = '$(alert) Select Python Interpreter'; } this.statusBar.show(); } diff --git a/src/client/interpreter/display/progressDisplay.ts b/src/client/interpreter/display/progressDisplay.ts new file mode 100644 index 000000000000..e41d49c30580 --- /dev/null +++ b/src/client/interpreter/display/progressDisplay.ts @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { Disposable, ProgressLocation, ProgressOptions } from 'vscode'; +import { IApplicationShell } from '../../common/application/types'; +import { traceVerbose } from '../../common/logger'; +import { IDisposableRegistry } from '../../common/types'; +import { createDeferred, Deferred } from '../../common/utils/async'; +import { Interpreters } from '../../common/utils/localize'; +import { IInterpreterLocatorProgressService, InterpreterLocatorProgressHandler } from '../contracts'; + +@injectable() +export class InterpreterLocatorProgressStatubarHandler implements InterpreterLocatorProgressHandler { + private deferred: Deferred | undefined; + private isFirstTimeLoadingInterpreters = true; + constructor(@inject(IApplicationShell) private readonly shell: IApplicationShell, + @inject(IInterpreterLocatorProgressService) private readonly progressService: IInterpreterLocatorProgressService, + @inject(IDisposableRegistry) private readonly disposables: Disposable[]) { } + public register() { + this.progressService.onRefreshing(() => this.showProgress(), this, this.disposables); + this.progressService.onRefreshed(() => this.hideProgress(), this, this.disposables); + } + @traceVerbose('Display locator refreshing progress') + private showProgress(): void { + if (!this.deferred) { + this.createProgress(); + } + } + @traceVerbose('Hide locator refreshing progress') + private hideProgress(): void { + if (this.deferred) { + this.deferred.resolve(); + this.deferred = undefined; + } + } + private createProgress() { + const progressOptions: ProgressOptions = { + location: ProgressLocation.Window, + title: this.isFirstTimeLoadingInterpreters ? Interpreters.loading() : Interpreters.refreshing() + }; + this.isFirstTimeLoadingInterpreters = false; + this.shell.withProgress(progressOptions, () => { + this.deferred = createDeferred(); + return this.deferred.promise; + }); + } +} diff --git a/src/client/interpreter/interpreterService.ts b/src/client/interpreter/interpreterService.ts index 11e1ebeeff65..430e1c4140a4 100644 --- a/src/client/interpreter/interpreterService.ts +++ b/src/client/interpreter/interpreterService.ts @@ -81,7 +81,8 @@ export class InterpreterService implements Disposable, IInterpreterService { return; } // Always pick the highest version by default. - const pythonPath = interpretersInWorkspace.sort((a, b) => a.version! > b.version! ? 1 : -1)[0].path; + interpretersInWorkspace.sort((a, b) => a.version! > b.version! ? 1 : -1); + const pythonPath = interpretersInWorkspace[0].path; // Ensure this new environment is at the same level as the current workspace. // In windows the interpreter is under scripts/python.exe on linux it is under bin/python. // Meaning the sub directory must be either scripts, bin or other (but only one level deep). diff --git a/src/client/interpreter/locators/index.ts b/src/client/interpreter/locators/index.ts index 521b099a588a..b922b1376252 100644 --- a/src/client/interpreter/locators/index.ts +++ b/src/client/interpreter/locators/index.ts @@ -1,6 +1,6 @@ import { inject, injectable } from 'inversify'; import * as _ from 'lodash'; -import { Disposable, Uri } from 'vscode'; +import { Disposable, Event, EventEmitter, Uri } from 'vscode'; import { IPlatformService } from '../../common/platform/types'; import { IDisposableRegistry } from '../../common/types'; import { IServiceContainer } from '../../ioc/types'; @@ -26,7 +26,6 @@ export class PythonInterpreterLocatorService implements IInterpreterLocatorServi private readonly disposables: Disposable[] = []; private readonly platform: IPlatformService; private readonly interpreterLocatorHelper: IInterpreterLocatorHelper; - constructor( @inject(IServiceContainer) private serviceContainer: IServiceContainer ) { @@ -34,6 +33,17 @@ export class PythonInterpreterLocatorService implements IInterpreterLocatorServi this.platform = serviceContainer.get(IPlatformService); this.interpreterLocatorHelper = serviceContainer.get(IInterpreterLocatorHelper); } + /** + * This class should never emit events when we're locating. + * The events will be fired by the indivitual locators retrieved in `getLocators`. + * + * @readonly + * @type {Event>} + * @memberof PythonInterpreterLocatorService + */ + public get onLocating(): Event> { + return new EventEmitter>().event; + } /** * Release any held resources. diff --git a/src/client/interpreter/locators/progressService.ts b/src/client/interpreter/locators/progressService.ts new file mode 100644 index 000000000000..cddec2f665c3 --- /dev/null +++ b/src/client/interpreter/locators/progressService.ts @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { Disposable, Event, EventEmitter } from 'vscode'; +import { traceVerbose } from '../../common/logger'; +import { IDisposableRegistry } from '../../common/types'; +import { createDeferredFrom, Deferred } from '../../common/utils/async'; +import { noop } from '../../common/utils/misc'; +import { IServiceContainer } from '../../ioc/types'; +import { IInterpreterLocatorProgressService, IInterpreterLocatorService, PythonInterpreter } from '../contracts'; + +@injectable() +export class InterpreterLocatorProgressService implements IInterpreterLocatorProgressService { + private deferreds: Deferred[] = []; + private readonly refreshing = new EventEmitter(); + private readonly refreshed = new EventEmitter(); + private readonly locators: IInterpreterLocatorService[] = []; + constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer, + @inject(IDisposableRegistry) private readonly disposables: Disposable[]) { + this.locators = serviceContainer.getAll(IInterpreterLocatorService); + } + + public get onRefreshing(): Event { + return this.refreshing.event; + } + public get onRefreshed(): Event { + return this.refreshed.event; + } + public register(): void { + this.locators.forEach(locator => { + locator.onLocating(this.handleProgress, this, this.disposables); + }); + } + @traceVerbose('Detected refreshing of Interpreters') + private handleProgress(promise: Promise) { + this.deferreds.push(createDeferredFrom(promise)); + this.notifyRefreshing(); + this.checkProgress(); + } + @traceVerbose('All locators have completed locating') + private notifyCompleted() { + this.refreshed.fire(); + } + @traceVerbose('Notify locators are locating') + private notifyRefreshing() { + this.refreshing.fire(); + } + @traceVerbose('Checking whether locactors have completed locating') + private checkProgress() { + if (this.areAllItemsCcomplete()) { + return this.notifyCompleted(); + } + Promise.all(this.deferreds.map(item => item.promise)) + .catch(noop) + .then(() => this.checkProgress()) + .ignoreErrors(); + } + private areAllItemsCcomplete() { + this.deferreds = this.deferreds.filter(item => !item.completed); + return this.deferreds.length === 0; + } +} diff --git a/src/client/interpreter/locators/services/cacheableLocatorService.ts b/src/client/interpreter/locators/services/cacheableLocatorService.ts index ca5a35ddd387..7e2d8bed685b 100644 --- a/src/client/interpreter/locators/services/cacheableLocatorService.ts +++ b/src/client/interpreter/locators/services/cacheableLocatorService.ts @@ -5,8 +5,9 @@ import { injectable, unmanaged } from 'inversify'; import * as md5 from 'md5'; -import { Disposable, Uri } from 'vscode'; +import { Disposable, Event, EventEmitter, Uri } from 'vscode'; import { IWorkspaceService } from '../../../common/application/types'; +import '../../../common/extensions'; import { Logger } from '../../../common/logger'; import { IDisposableRegistry, IPersistentStateFactory } from '../../../common/types'; import { createDeferred, Deferred } from '../../../common/utils/async'; @@ -18,11 +19,15 @@ export abstract class CacheableLocatorService implements IInterpreterLocatorServ private readonly promisesPerResource = new Map>(); private readonly handlersAddedToResource = new Set(); private readonly cacheKeyPrefix: string; + private readonly locating = new EventEmitter>(); constructor(@unmanaged() name: string, @unmanaged() protected readonly serviceContainer: IServiceContainer, @unmanaged() private cachePerWorkspace: boolean = false) { this.cacheKeyPrefix = `INTERPRETERS_CACHE_v2_${name}`; } + public get onLocating(): Event> { + return this.locating.event; + } public abstract dispose(); public async getInterpreters(resource?: Uri): Promise { const cacheKey = this.getCacheKey(resource); @@ -41,6 +46,8 @@ export abstract class CacheableLocatorService implements IInterpreterLocatorServ deferred!.resolve(items); }) .catch(ex => deferred!.reject(ex)); + + this.locating.fire(deferred.promise); } if (deferred.completed) { return deferred.promise; @@ -60,6 +67,7 @@ export abstract class CacheableLocatorService implements IInterpreterLocatorServ watcher.onDidCreate(() => { Logger.verbose(`Interpreter Watcher change handler for ${this.cacheKeyPrefix}`); this.promisesPerResource.delete(cacheKey); + this.getInterpreters(resource).ignoreErrors(); }, this, disposableRegisry); }); } diff --git a/src/client/interpreter/locators/services/workspaceVirtualEnvWatcherService.ts b/src/client/interpreter/locators/services/workspaceVirtualEnvWatcherService.ts index 7217150d392a..8d099ebebc68 100644 --- a/src/client/interpreter/locators/services/workspaceVirtualEnvWatcherService.ts +++ b/src/client/interpreter/locators/services/workspaceVirtualEnvWatcherService.ts @@ -7,20 +7,25 @@ import { inject, injectable } from 'inversify'; import * as path from 'path'; import { Disposable, Event, EventEmitter, FileSystemWatcher, RelativePattern, Uri } from 'vscode'; import { IWorkspaceService } from '../../../common/application/types'; +import '../../../common/extensions'; import { Logger, traceVerbose } from '../../../common/logger'; import { IPlatformService } from '../../../common/platform/types'; +import { IPythonExecutionFactory } from '../../../common/process/types'; import { IDisposableRegistry } from '../../../common/types'; -import { debounce } from '../../../common/utils/decorators'; import { IInterpreterWatcher } from '../../contracts'; +const maxTimeToWaitForEnvCreation = 60_000; +const timeToPollForEnvCreation = 2_000; + @injectable() export class WorkspaceVirtualEnvWatcherService implements IInterpreterWatcher, Disposable { - private readonly didCreate; - private timer?: NodeJS.Timer; + private readonly didCreate: EventEmitter; + private timers = new Map(); private fsWatchers: FileSystemWatcher[] = []; constructor(@inject(IDisposableRegistry) private readonly disposableRegistry: Disposable[], @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, - @inject(IPlatformService) private readonly platformService: IPlatformService) { + @inject(IPlatformService) private readonly platformService: IPlatformService, + @inject(IPythonExecutionFactory) private readonly pythonExecFactory: IPythonExecutionFactory) { this.didCreate = new EventEmitter(); disposableRegistry.push(this); } @@ -28,7 +33,7 @@ export class WorkspaceVirtualEnvWatcherService implements IInterpreterWatcher, D return this.didCreate.event; } public dispose() { - this.clearTimer(); + this.clearTimers(); } @traceVerbose('Register Intepreter Watcher') public async register(resource: Uri | undefined): Promise { @@ -51,22 +56,39 @@ export class WorkspaceVirtualEnvWatcherService implements IInterpreterWatcher, D this.fsWatchers.push(fsWatcher); } } - @debounce(2000) @traceVerbose('Intepreter Watcher change handler') - protected createHandler(e: Uri) { + protected async createHandler(e: Uri) { this.didCreate.fire(); - // On Windows, creation of environments are slow, hence lets notify again after 10 seconds. - this.clearTimer(); - - this.timer = setTimeout(() => { - this.timer = undefined; - this.didCreate.fire(); - }, 10000); + // On Windows, creation of environments are very slow, hence lets notify again after + // the python executable is accessible (i.e. when we can launch the process). + this.notifyCreationWhenReady(e.fsPath).ignoreErrors(); } - private clearTimer() { - if (this.timer) { - clearTimeout(this.timer); - this.timer = undefined; + protected async notifyCreationWhenReady(pythonPath: string) { + const counter = this.timers.has(pythonPath) ? this.timers.get(pythonPath)!.counter + 1 : 0; + const isValid = await this.isValidExecutable(pythonPath); + if (isValid) { + if (counter > 0) { + this.didCreate.fire(); + } + return this.timers.delete(pythonPath); } + if (counter > (maxTimeToWaitForEnvCreation / timeToPollForEnvCreation)) { + // Send notification before we give up trying. + this.didCreate.fire(); + this.timers.delete(pythonPath); + return; + } + + const timer = setTimeout(() => this.notifyCreationWhenReady(pythonPath).ignoreErrors(), timeToPollForEnvCreation); + this.timers.set(pythonPath, { timer, counter }); + } + private clearTimers() { + this.timers.forEach(item => clearTimeout(item.timer)); + this.timers.clear(); + } + private async isValidExecutable(pythonPath: string): Promise { + const execService = await this.pythonExecFactory.create({ pythonPath }); + const info = await execService.getInterpreterInformation().catch(() => undefined); + return info !== undefined; } } diff --git a/src/client/interpreter/serviceRegistry.ts b/src/client/interpreter/serviceRegistry.ts index e3828d174dd2..12ea7771f6bd 100644 --- a/src/client/interpreter/serviceRegistry.ts +++ b/src/client/interpreter/serviceRegistry.ts @@ -17,6 +17,7 @@ import { IInterpreterDisplay, IInterpreterHelper, IInterpreterLocatorHelper, + IInterpreterLocatorProgressService, IInterpreterLocatorService, IInterpreterService, IInterpreterVersionService, @@ -24,6 +25,7 @@ import { IInterpreterWatcherBuilder, IKnownSearchPathsForInterpreters, INTERPRETER_LOCATOR_SERVICE, + InterpreterLocatorProgressHandler, IPipEnvService, IShebangCodeLensProvider, IVirtualEnvironmentsSearchPathProvider, @@ -33,12 +35,14 @@ import { WORKSPACE_VIRTUAL_ENV_SERVICE } from './contracts'; import { InterpreterDisplay } from './display'; +import { InterpreterLocatorProgressStatubarHandler } from './display/progressDisplay'; import { ShebangCodeLensProvider } from './display/shebangCodeLensProvider'; import { InterpreterHelper } from './helpers'; import { InterpreterService } from './interpreterService'; import { InterpreterVersionService } from './interpreterVersion'; import { InterpreterLocatorHelper } from './locators/helpers'; import { PythonInterpreterLocatorService } from './locators/index'; +import { InterpreterLocatorProgressService } from './locators/progressService'; import { CondaEnvFileService } from './locators/services/condaEnvFileService'; import { CondaEnvService } from './locators/services/condaEnvService'; import { CondaService } from './locators/services/condaService'; @@ -90,4 +94,7 @@ export function registerTypes(serviceManager: IServiceManager) { serviceManager.addSingleton(IInterpreterHelper, InterpreterHelper); serviceManager.addSingleton(IInterpreterLocatorHelper, InterpreterLocatorHelper); serviceManager.addSingleton(IInterpreterComparer, InterpreterComparer); + + serviceManager.addSingleton(InterpreterLocatorProgressHandler, InterpreterLocatorProgressStatubarHandler); + serviceManager.addSingleton(IInterpreterLocatorProgressService, InterpreterLocatorProgressService); } diff --git a/src/client/typeFormatters/dispatcher.ts b/src/client/typeFormatters/dispatcher.ts index 0470b28c902d..86c22bc60d47 100644 --- a/src/client/typeFormatters/dispatcher.ts +++ b/src/client/typeFormatters/dispatcher.ts @@ -23,8 +23,8 @@ export class OnTypeFormattingDispatcher implements OnTypeFormattingEditProvider } public getTriggerCharacters(): { first: string; more: string[] } | undefined { - let keys = Object.keys(this.providers); - keys = keys.sort(); // Make output deterministic + const keys = Object.keys(this.providers); + keys.sort(); // Make output deterministic const first = keys.shift(); diff --git a/src/client/unittests/unittest/helper.ts b/src/client/unittests/unittest/helper.ts index a97aed3102cf..f89c4c287b39 100644 --- a/src/client/unittests/unittest/helper.ts +++ b/src/client/unittests/unittest/helper.ts @@ -30,7 +30,7 @@ export class UnitTestHelper implements IUnitTestHelper { const testIds: string[] = []; if (testsToRun && testsToRun.testFolder) { // Get test ids of files in these folders. - testsToRun.testFolder.map(folder => { + testsToRun.testFolder.forEach(folder => { tests.testFiles.forEach(f => { if (f.fullPath.startsWith(folder.name)) { testIds.push(f.nameToRun); diff --git a/src/test/interpreters/display.unit.test.ts b/src/test/interpreters/display.unit.test.ts index e3ca5f4f1f8c..8b08756d7814 100644 --- a/src/test/interpreters/display.unit.test.ts +++ b/src/test/interpreters/display.unit.test.ts @@ -136,7 +136,7 @@ suite('Interpreters Display', () => { await interpreterDisplay.refresh(resource); statusBar.verify(s => s.color = TypeMoq.It.isValue('yellow'), TypeMoq.Times.once()); - statusBar.verify(s => s.text = TypeMoq.It.isValue('$(alert) Select Python Environment'), TypeMoq.Times.once()); + statusBar.verify(s => s.text = TypeMoq.It.isValue('$(alert) Select Python Interpreter'), TypeMoq.Times.once()); }); test('Ensure we try to identify the active workspace when a resource is not provided ', async () => { const workspaceFolder = Uri.file('x'); diff --git a/src/test/interpreters/display/progressDisplay.unit.test.ts b/src/test/interpreters/display/progressDisplay.unit.test.ts new file mode 100644 index 000000000000..1d09ee3d0be0 --- /dev/null +++ b/src/test/interpreters/display/progressDisplay.unit.test.ts @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-any + +import { expect } from 'chai'; +import { anything, capture, instance, mock, when } from 'ts-mockito'; +import { CancellationToken, Disposable, Progress, ProgressOptions } from 'vscode'; +import { ApplicationShell } from '../../../client/common/application/applicationShell'; +import { Interpreters } from '../../../client/common/utils/localize'; +import { noop } from '../../../client/common/utils/misc'; +import { IInterpreterLocatorProgressService } from '../../../client/interpreter/contracts'; +import { InterpreterLocatorProgressStatubarHandler } from '../../../client/interpreter/display/progressDisplay'; + +type ProgressTask = (progress: Progress<{ message?: string; increment?: number }>, token: CancellationToken) => Thenable; + +suite('Interpreters - Display Progress', () => { + let refreshingCallback: (e: void) => any | undefined; + let refreshedCallback: (e: void) => any | undefined; + const progressService: IInterpreterLocatorProgressService = { + onRefreshing(listener: (e: void) => any): Disposable { + refreshingCallback = listener; + return { dispose: noop }; + }, + onRefreshed(listener: (e: void) => any): Disposable { + refreshedCallback = listener; + return { dispose: noop }; + }, + register(): void { + noop(); + } + }; + + test('Display loading message when refreshing interpreters for the first time', async () => { + const shell = mock(ApplicationShell); + const statusBar = new InterpreterLocatorProgressStatubarHandler(instance(shell), progressService, []); + when(shell.withProgress(anything(), anything())).thenResolve(); + + statusBar.register(); + refreshingCallback(undefined); + + const options = capture(shell.withProgress as any).last()[0] as ProgressOptions; + expect(options.title).to.be.equal(Interpreters.loading()); + }); + + test('Display refreshing message when refreshing interpreters for the second time', async () => { + const shell = mock(ApplicationShell); + const statusBar = new InterpreterLocatorProgressStatubarHandler(instance(shell), progressService, []); + when(shell.withProgress(anything(), anything())).thenResolve(); + + statusBar.register(); + refreshingCallback(undefined); + + let options = capture(shell.withProgress as any).last()[0] as ProgressOptions; + expect(options.title).to.be.equal(Interpreters.loading()); + + refreshingCallback(undefined); + + options = capture(shell.withProgress as any).last()[0] as ProgressOptions; + expect(options.title).to.be.equal(Interpreters.refreshing()); + }); + + test('Progress message is hidden when loading has completed', async () => { + const shell = mock(ApplicationShell); + const statusBar = new InterpreterLocatorProgressStatubarHandler(instance(shell), progressService, []); + when(shell.withProgress(anything(), anything())).thenResolve(); + + statusBar.register(); + refreshingCallback(undefined); + + const options = capture(shell.withProgress as any).last()[0] as ProgressOptions; + const callback = capture(shell.withProgress as any).last()[1] as ProgressTask; + const promise = callback(undefined as any, undefined as any); + + expect(options.title).to.be.equal(Interpreters.loading()); + + refreshedCallback(undefined); + // Promise must resolve when refreshed callback is invoked. + // When promise resolves, the progress message is hidden by VSC. + await promise; + }); +}); diff --git a/src/test/interpreters/locators/cacheableLocatorService.unit.test.ts b/src/test/interpreters/locators/cacheableLocatorService.unit.test.ts index 9851443aa019..1d0a210c26ca 100644 --- a/src/test/interpreters/locators/cacheableLocatorService.unit.test.ts +++ b/src/test/interpreters/locators/cacheableLocatorService.unit.test.ts @@ -145,6 +145,24 @@ suite('Interpreters - Cacheable Locator Service', () => { verify(mockedLocatorForVerification.getInterpretersImplementation()).twice(); verify(mockedLocatorForVerification.cacheInterpreters()).twice(); }); + test('Ensure locating event is raised', async () => { + const mockedLocatorForVerification = mock(MockLocator); + const locator = new class extends Locator { + protected async getInterpreterWatchers(_resource: Uri | undefined): Promise { + return []; + } + }('dummy', instance(serviceContainer), instance(mockedLocatorForVerification)); + + let locatingEventRaised = false; + locator.onLocating(() => locatingEventRaised = true); + + when(mockedLocatorForVerification.getInterpretersImplementation()).thenResolve([1, 2] as any); + when(mockedLocatorForVerification.getCacheKey()).thenReturn('xyz'); + when(mockedLocatorForVerification.getCachedInterpreters()).thenResolve(); + + await locator.getInterpreters(); + expect(locatingEventRaised).to.be.equal(true, 'Locating Event not raised'); + }); }); suite('Cache Key', () => { class Locator extends CacheableLocatorService { diff --git a/src/test/interpreters/locators/progressService.unit.test.ts b/src/test/interpreters/locators/progressService.unit.test.ts new file mode 100644 index 000000000000..6551f655e44a --- /dev/null +++ b/src/test/interpreters/locators/progressService.unit.test.ts @@ -0,0 +1,109 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-any max-classes-per-file max-func-body-length + +import { expect } from 'chai'; +import { anything, instance, mock, when } from 'ts-mockito'; +import { Disposable, Uri } from 'vscode'; +import { createDeferred } from '../../../client/common/utils/async'; +import { noop } from '../../../client/common/utils/misc'; +import { IInterpreterLocatorService, PythonInterpreter } from '../../../client/interpreter/contracts'; +import { InterpreterLocatorProgressService } from '../../../client/interpreter/locators/progressService'; +import { ServiceContainer } from '../../../client/ioc/container'; +import { sleep } from '../../core'; + +suite('Interpreters - Locator Progress', () => { + class Locator implements IInterpreterLocatorService { + public locatingCallback?: (e: Promise) => any; + public onLocating(listener: (e: Promise) => any, thisArgs?: any, disposables?: Disposable[]): Disposable { + this.locatingCallback = listener; + return { dispose: noop }; + } + public getInterpreters(resource?: Uri): Promise { + return Promise.resolve([]); + } + public dispose() { + noop(); + } + } + + test('Must raise refreshing event', async () => { + const serviceContainer = mock(ServiceContainer); + const locator = new Locator(); + when(serviceContainer.getAll(anything())).thenReturn([locator]); + const progress = new InterpreterLocatorProgressService(instance(serviceContainer), []); + progress.register(); + + let refreshingInvoked = false; + progress.onRefreshing(() => refreshingInvoked = true); + let refreshedInvoked = false; + progress.onRefreshed(() => refreshedInvoked = true); + + const locatingDeferred = createDeferred(); + locator.locatingCallback!.bind(progress)(locatingDeferred.promise); + expect(refreshingInvoked).to.be.equal(true, 'Refreshing Not invoked'); + expect(refreshedInvoked).to.be.equal(false, 'Refreshed invoked'); + }); + test('Must raise refreshed event', async () => { + const serviceContainer = mock(ServiceContainer); + const locator = new Locator(); + when(serviceContainer.getAll(anything())).thenReturn([locator]); + const progress = new InterpreterLocatorProgressService(instance(serviceContainer), []); + progress.register(); + + let refreshingInvoked = false; + progress.onRefreshing(() => refreshingInvoked = true); + let refreshedInvoked = false; + progress.onRefreshed(() => refreshedInvoked = true); + + const locatingDeferred = createDeferred(); + locator.locatingCallback!.bind(progress)(locatingDeferred.promise); + locatingDeferred.resolve(); + + await sleep(10); + expect(refreshingInvoked).to.be.equal(true, 'Refreshing Not invoked'); + expect(refreshedInvoked).to.be.equal(true, 'Refreshed not invoked'); + }); + test('Must raise refreshed event only when all locators have completed', async () => { + const serviceContainer = mock(ServiceContainer); + const locator1 = new Locator(); + const locator2 = new Locator(); + const locator3 = new Locator(); + when(serviceContainer.getAll(anything())).thenReturn([locator1, locator2, locator3]); + const progress = new InterpreterLocatorProgressService(instance(serviceContainer), []); + progress.register(); + + let refreshingInvoked = false; + progress.onRefreshing(() => refreshingInvoked = true); + let refreshedInvoked = false; + progress.onRefreshed(() => refreshedInvoked = true); + + const locatingDeferred1 = createDeferred(); + locator1.locatingCallback!.bind(progress)(locatingDeferred1.promise); + + const locatingDeferred2 = createDeferred(); + locator2.locatingCallback!.bind(progress)(locatingDeferred2.promise); + + const locatingDeferred3 = createDeferred(); + locator3.locatingCallback!.bind(progress)(locatingDeferred3.promise); + + locatingDeferred1.resolve(); + + await sleep(10); + expect(refreshingInvoked).to.be.equal(true, 'Refreshing Not invoked'); + expect(refreshedInvoked).to.be.equal(false, 'Refreshed invoked'); + + locatingDeferred2.resolve(); + + await sleep(10); + expect(refreshedInvoked).to.be.equal(false, 'Refreshed invoked'); + + locatingDeferred3.resolve(); + + await sleep(10); + expect(refreshedInvoked).to.be.equal(true, 'Refreshed not invoked'); + }); +}); diff --git a/src/test/interpreters/locators/workspaceVirtualEnvWatcherService.unit.test.ts b/src/test/interpreters/locators/workspaceVirtualEnvWatcherService.unit.test.ts index ccb7e5ded248..026704ad4415 100644 --- a/src/test/interpreters/locators/workspaceVirtualEnvWatcherService.unit.test.ts +++ b/src/test/interpreters/locators/workspaceVirtualEnvWatcherService.unit.test.ts @@ -12,6 +12,7 @@ import { Disposable, FileSystemWatcher, Uri, WorkspaceFolder } from 'vscode'; import { WorkspaceService } from '../../../client/common/application/workspace'; import { isUnitTestExecution } from '../../../client/common/constants'; import { PlatformService } from '../../../client/common/platform/platformService'; +import { PythonExecutionFactory } from '../../../client/common/process/pythonExecutionFactory'; import { sleep } from '../../../client/common/utils/async'; import { noop } from '../../../client/common/utils/misc'; import { OSType } from '../../../client/common/utils/platform'; @@ -36,7 +37,8 @@ suite('Interpreters - Workspace VirtualEnv Watcher Service', () => { async function checkForFileChanges(os: OSType, resource: Uri | undefined, hasWorkspaceFolder: boolean) { const workspaceService = mock(WorkspaceService); const platformService = mock(PlatformService); - const watcher = new WorkspaceVirtualEnvWatcherService([], instance(workspaceService), instance(platformService)); + const execFactory = mock(PythonExecutionFactory); + const watcher = new WorkspaceVirtualEnvWatcherService([], instance(workspaceService), instance(platformService), instance(execFactory)); when(platformService.isWindows).thenReturn(os === OSType.Windows); when(platformService.isLinux).thenReturn(os === OSType.Linux); @@ -80,7 +82,8 @@ suite('Interpreters - Workspace VirtualEnv Watcher Service', () => { async function ensureFileChanesAreHandled(os: OSType) { const workspaceService = mock(WorkspaceService); const platformService = mock(PlatformService); - const watcher = new WorkspaceVirtualEnvWatcherService(disposables, instance(workspaceService), instance(platformService)); + const execFactory = mock(PythonExecutionFactory); + const watcher = new WorkspaceVirtualEnvWatcherService(disposables, instance(workspaceService), instance(platformService), instance(execFactory)); when(platformService.isWindows).thenReturn(os === OSType.Windows); when(platformService.isLinux).thenReturn(os === OSType.Linux); diff --git a/src/test/vscode-mock.ts b/src/test/vscode-mock.ts index 714fc9304485..904083ce8cf2 100644 --- a/src/test/vscode-mock.ts +++ b/src/test/vscode-mock.ts @@ -68,6 +68,7 @@ mockedVSCode.DocumentLink = vscodeMocks.vscMockExtHostedTypes.DocumentLink; mockedVSCode.TextEdit = vscodeMocks.vscMockExtHostedTypes.TextEdit; mockedVSCode.WorkspaceEdit = vscodeMocks.vscMockExtHostedTypes.WorkspaceEdit; mockedVSCode.RelativePattern = vscodeMocks.vscMockExtHostedTypes.RelativePattern; +mockedVSCode.ProgressLocation = vscodeMocks.vscMockExtHostedTypes.ProgressLocation; // This API is used in src/client/telemetry/telemetry.ts const extensions = TypeMoq.Mock.ofType();