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();