From 01b2352628fa5c8251a4db4f05a00d62a3eab439 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Wed, 31 Oct 2018 13:49:40 -0700 Subject: [PATCH 01/16] Speed up interpreter detection --- .../services/cacheableLocatorService.ts | 2 + .../workspaceVirtualEnvWatcherService.ts | 51 ++++++++++++++----- ...spaceVirtualEnvWatcherService.unit.test.ts | 7 ++- 3 files changed, 44 insertions(+), 16 deletions(-) diff --git a/src/client/interpreter/locators/services/cacheableLocatorService.ts b/src/client/interpreter/locators/services/cacheableLocatorService.ts index ca5a35ddd387..ecac28ee6d33 100644 --- a/src/client/interpreter/locators/services/cacheableLocatorService.ts +++ b/src/client/interpreter/locators/services/cacheableLocatorService.ts @@ -7,6 +7,7 @@ import { injectable, unmanaged } from 'inversify'; import * as md5 from 'md5'; import { Disposable, 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'; @@ -60,6 +61,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..75181f901158 100644 --- a/src/client/interpreter/locators/services/workspaceVirtualEnvWatcherService.ts +++ b/src/client/interpreter/locators/services/workspaceVirtualEnvWatcherService.ts @@ -9,18 +9,23 @@ import { Disposable, Event, EventEmitter, FileSystemWatcher, RelativePattern, Ur import { IWorkspaceService } from '../../../common/application/types'; 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 = 5_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 { @@ -55,18 +60,36 @@ export class WorkspaceVirtualEnvWatcherService implements IInterpreterWatcher, D @traceVerbose('Intepreter Watcher change handler') protected 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 isValid = await this.isValidExecutable(pythonPath); + if (isValid) { + this.timers.delete(pythonPath); + return this.didCreate.fire(); + } + const counter = this.timers.has(pythonPath) ? this.timers.get(pythonPath)!.counter + 1 : 0; + if (counter > (maxTimeToWaitForEnvCreation / timeToPollForEnvCreation)) { + this.timers.delete(pythonPath); + return; } + + const timer = setTimeout(() => { + this.didCreate.fire(); + 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/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); From b598a12630dec61d9510e5ca96f1e47a8f1c0858 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Wed, 31 Oct 2018 14:20:00 -0700 Subject: [PATCH 02/16] Use separate urls for querying and downloading --- .../languageServer/languageServerPackageRepository.ts | 9 +++++---- src/client/common/nuget/azureBlobStoreNugetRepository.ts | 9 +++++---- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/client/activation/languageServer/languageServerPackageRepository.ts b/src/client/activation/languageServer/languageServerPackageRepository.ts index 710cc5d07a88..a4f38f1ed085 100644 --- a/src/client/activation/languageServer/languageServerPackageRepository.ts +++ b/src/client/activation/languageServer/languageServerPackageRepository.ts @@ -7,7 +7,8 @@ import { inject, injectable } from 'inversify'; import { AzureBlobStoreNugetRepository } from '../../common/nuget/azureBlobStoreNugetRepository'; import { IServiceContainer } from '../../ioc/types'; -const azureBlobStorageAccount = 'https://pvsc.azureedge.net'; +const azureBlobStorageAccount = 'https://pvsc.blob.core.windows.net'; +const azureCDNBlobStorageAccount = 'https://pvsc.azureedge.net'; export enum LanguageServerDownloadChannel { stable = 'stable', @@ -24,20 +25,20 @@ export enum LanguageServerPackageStorageContainers { @injectable() export class StableLanguageServerPackageRepository extends AzureBlobStoreNugetRepository { constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { - super(serviceContainer, azureBlobStorageAccount, LanguageServerPackageStorageContainers.stable); + super(serviceContainer, azureBlobStorageAccount, LanguageServerPackageStorageContainers.stable, azureCDNBlobStorageAccount); } } @injectable() export class BetaLanguageServerPackageRepository extends AzureBlobStoreNugetRepository { constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { - super(serviceContainer, azureBlobStorageAccount, LanguageServerPackageStorageContainers.beta); + super(serviceContainer, azureBlobStorageAccount, LanguageServerPackageStorageContainers.beta, azureCDNBlobStorageAccount); } } @injectable() export class DailyLanguageServerPackageRepository extends AzureBlobStoreNugetRepository { constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { - super(serviceContainer, azureBlobStorageAccount, LanguageServerPackageStorageContainers.daily); + super(serviceContainer, azureBlobStorageAccount, LanguageServerPackageStorageContainers.daily, azureCDNBlobStorageAccount); } } diff --git a/src/client/common/nuget/azureBlobStoreNugetRepository.ts b/src/client/common/nuget/azureBlobStoreNugetRepository.ts index 89e9a408079f..f3f995a4d7d3 100644 --- a/src/client/common/nuget/azureBlobStoreNugetRepository.ts +++ b/src/client/common/nuget/azureBlobStoreNugetRepository.ts @@ -15,14 +15,15 @@ import { INugetRepository, INugetService, NugetPackage } from './types'; export class AzureBlobStoreNugetRepository implements INugetRepository { constructor(@inject(IServiceContainer) private readonly serviceContainer: IServiceContainer, @unmanaged() protected readonly azureBlobStorageAccount: string, - @unmanaged() protected readonly azureBlobStorageContainer: string) { } + @unmanaged() protected readonly azureBlobStorageContainer: string, + @unmanaged() protected readonly azureCDNBlobStorageAccount: string) { } public async getPackages(packageName: string): Promise { - return this.listPackages(this.azureBlobStorageAccount, this.azureBlobStorageContainer, packageName); + return this.listPackages(this.azureBlobStorageAccount, this.azureBlobStorageContainer, packageName, this.azureCDNBlobStorageAccount); } @captureTelemetry(PYTHON_LANGUAGE_SERVER_LIST_BLOB_STORE_PACKAGES) @traceVerbose('Listing Nuget Packages') - public listPackages(azureBlobStorageAccount: string, azureBlobStorageContainer: string, packageName: string) { + protected listPackages(azureBlobStorageAccount: string, azureBlobStorageContainer: string, packageName: string, azureCDNBlobStorageAccount: string) { // tslint:disable-next-line:no-require-imports const az = require('azure-storage') as typeof azStorageTypes; const blobStore = az.createBlobServiceAnonymous(azureBlobStorageAccount); @@ -39,7 +40,7 @@ export class AzureBlobStoreNugetRepository implements INugetRepository { resolve(result.entries.map(item => { return { package: item.name, - uri: `${azureBlobStorageAccount}/${azureBlobStorageContainer}/${item.name}`, + uri: `${azureCDNBlobStorageAccount}/${azureBlobStorageContainer}/${item.name}`, version: nugetService.getVersionFromPackageFileName(item.name) }; })); From 9b940ef2e72b5fdb039f0b248d4db8dcbabe7ddc Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Wed, 31 Oct 2018 14:29:07 -0700 Subject: [PATCH 03/16] Fix tests --- .../languageServer/languageServerPackageService.test.ts | 5 ++++- src/test/common/nuget/azureBobStoreRepository.test.ts | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/test/activation/languageServer/languageServerPackageService.test.ts b/src/test/activation/languageServer/languageServerPackageService.test.ts index 677f03cfdadb..490e9c6e1bbb 100644 --- a/src/test/activation/languageServer/languageServerPackageService.test.ts +++ b/src/test/activation/languageServer/languageServerPackageService.test.ts @@ -21,6 +21,9 @@ import { PlatformService } from '../../../client/common/platform/platformService import { IPlatformService } from '../../../client/common/platform/types'; import { IServiceContainer } from '../../../client/ioc/types'; +const azureBlobStorageAccount = 'https://pvsc.blob.core.windows.net'; +const azureCDNBlobStorageAccount = 'https://pvsc.azureedge.net'; + suite('Language Server Package Service', () => { let serviceContainer: typeMoq.IMock; setup(() => { @@ -76,7 +79,7 @@ suite('Language Server Package Service', () => { const platformService = new PlatformService(); serviceContainer.setup(c => c.get(typeMoq.It.isValue(IPlatformService))).returns(() => platformService); const defaultStorageChannel = LanguageServerPackageStorageContainers.stable; - const nugetRepo = new AzureBlobStoreNugetRepository(serviceContainer.object, 'https://pvsc.azureedge.net', defaultStorageChannel); + const nugetRepo = new AzureBlobStoreNugetRepository(serviceContainer.object, azureBlobStorageAccount, defaultStorageChannel, azureCDNBlobStorageAccount); serviceContainer.setup(c => c.get(typeMoq.It.isValue(INugetRepository))).returns(() => nugetRepo); const lsPackageService = new LanguageServerPackageService(serviceContainer.object); const packageName = lsPackageService.getNugetPackageName(); diff --git a/src/test/common/nuget/azureBobStoreRepository.test.ts b/src/test/common/nuget/azureBobStoreRepository.test.ts index dbc81768f77f..ab3224591493 100644 --- a/src/test/common/nuget/azureBobStoreRepository.test.ts +++ b/src/test/common/nuget/azureBobStoreRepository.test.ts @@ -15,6 +15,9 @@ import { PlatformService } from '../../../client/common/platform/platformService import { IPlatformService } from '../../../client/common/platform/types'; import { IServiceContainer } from '../../../client/ioc/types'; +const azureBlobStorageAccount = 'https://pvsc.blob.core.windows.net'; +const azureCDNBlobStorageAccount = 'https://pvsc.azureedge.net'; + suite('Nuget Azure Storage Repository', () => { let serviceContainer: typeMoq.IMock; let httpClient: typeMoq.IMock; @@ -29,7 +32,7 @@ suite('Nuget Azure Storage Repository', () => { serviceContainer.setup(c => c.get(typeMoq.It.isValue(INugetService))).returns(() => nugetService.object); const defaultStorageChannel = LanguageServerPackageStorageContainers.stable; - repo = new AzureBlobStoreNugetRepository(serviceContainer.object, 'https://pvsc.azureedge.net', defaultStorageChannel); + repo = new AzureBlobStoreNugetRepository(serviceContainer.object, azureBlobStorageAccount, defaultStorageChannel, azureCDNBlobStorageAccount); }); test('Get all packages', async function () { From a1e248604ec0978cd2f9cbc64f0b3428846f61e6 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Wed, 31 Oct 2018 14:51:51 -0700 Subject: [PATCH 04/16] Fix tests --- .../languageServer/languageServerPackageRepository.unit.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/activation/languageServer/languageServerPackageRepository.unit.test.ts b/src/test/activation/languageServer/languageServerPackageRepository.unit.test.ts index 19bea10475da..6121dd59f46e 100644 --- a/src/test/activation/languageServer/languageServerPackageRepository.unit.test.ts +++ b/src/test/activation/languageServer/languageServerPackageRepository.unit.test.ts @@ -35,7 +35,7 @@ suite('Language Server Download Channels', () => { } const instance = new class extends classToCreate { constructor() { super(serviceContainer.object); } - public get storageAccount() { return this.azureBlobStorageAccount; } + public get storageAccount() { return this.azureCDNBlobStorageAccount; } public get storageContainer() { return this.azureBlobStorageContainer; } }(); From 332fead0306a47d16e6e15656bd869aa49ef1ef7 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Wed, 31 Oct 2018 15:49:41 -0700 Subject: [PATCH 05/16] Fire notification immediately --- .../workspaceVirtualEnvWatcherService.ts | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/client/interpreter/locators/services/workspaceVirtualEnvWatcherService.ts b/src/client/interpreter/locators/services/workspaceVirtualEnvWatcherService.ts index 75181f901158..8d099ebebc68 100644 --- a/src/client/interpreter/locators/services/workspaceVirtualEnvWatcherService.ts +++ b/src/client/interpreter/locators/services/workspaceVirtualEnvWatcherService.ts @@ -7,15 +7,15 @@ 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 = 5_000; +const timeToPollForEnvCreation = 2_000; @injectable() export class WorkspaceVirtualEnvWatcherService implements IInterpreterWatcher, Disposable { @@ -56,31 +56,30 @@ 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 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(); } 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) { - this.timers.delete(pythonPath); - return this.didCreate.fire(); + if (counter > 0) { + this.didCreate.fire(); + } + return this.timers.delete(pythonPath); } - const counter = this.timers.has(pythonPath) ? this.timers.get(pythonPath)!.counter + 1 : 0; if (counter > (maxTimeToWaitForEnvCreation / timeToPollForEnvCreation)) { + // Send notification before we give up trying. + this.didCreate.fire(); this.timers.delete(pythonPath); return; } - const timer = setTimeout(() => { - this.didCreate.fire(); - this.notifyCreationWhenReady(pythonPath).ignoreErrors(); - }, timeToPollForEnvCreation); + const timer = setTimeout(() => this.notifyCreationWhenReady(pythonPath).ignoreErrors(), timeToPollForEnvCreation); this.timers.set(pythonPath, { timer, counter }); } private clearTimers() { From 48e24e2d322bb78d6df441345d70e7e92d595fc4 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Wed, 31 Oct 2018 13:49:40 -0700 Subject: [PATCH 06/16] Speed up interpreter detection --- .../services/cacheableLocatorService.ts | 2 + .../workspaceVirtualEnvWatcherService.ts | 51 ++++++++++++++----- ...spaceVirtualEnvWatcherService.unit.test.ts | 7 ++- 3 files changed, 44 insertions(+), 16 deletions(-) diff --git a/src/client/interpreter/locators/services/cacheableLocatorService.ts b/src/client/interpreter/locators/services/cacheableLocatorService.ts index ca5a35ddd387..ecac28ee6d33 100644 --- a/src/client/interpreter/locators/services/cacheableLocatorService.ts +++ b/src/client/interpreter/locators/services/cacheableLocatorService.ts @@ -7,6 +7,7 @@ import { injectable, unmanaged } from 'inversify'; import * as md5 from 'md5'; import { Disposable, 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'; @@ -60,6 +61,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..75181f901158 100644 --- a/src/client/interpreter/locators/services/workspaceVirtualEnvWatcherService.ts +++ b/src/client/interpreter/locators/services/workspaceVirtualEnvWatcherService.ts @@ -9,18 +9,23 @@ import { Disposable, Event, EventEmitter, FileSystemWatcher, RelativePattern, Ur import { IWorkspaceService } from '../../../common/application/types'; 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 = 5_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 { @@ -55,18 +60,36 @@ export class WorkspaceVirtualEnvWatcherService implements IInterpreterWatcher, D @traceVerbose('Intepreter Watcher change handler') protected 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 isValid = await this.isValidExecutable(pythonPath); + if (isValid) { + this.timers.delete(pythonPath); + return this.didCreate.fire(); + } + const counter = this.timers.has(pythonPath) ? this.timers.get(pythonPath)!.counter + 1 : 0; + if (counter > (maxTimeToWaitForEnvCreation / timeToPollForEnvCreation)) { + this.timers.delete(pythonPath); + return; } + + const timer = setTimeout(() => { + this.didCreate.fire(); + 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/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); From 99640aad927b33e9b448bf284bf038d1a54d93a4 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Wed, 31 Oct 2018 15:49:41 -0700 Subject: [PATCH 07/16] Fire notification immediately --- .../workspaceVirtualEnvWatcherService.ts | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/client/interpreter/locators/services/workspaceVirtualEnvWatcherService.ts b/src/client/interpreter/locators/services/workspaceVirtualEnvWatcherService.ts index 75181f901158..8d099ebebc68 100644 --- a/src/client/interpreter/locators/services/workspaceVirtualEnvWatcherService.ts +++ b/src/client/interpreter/locators/services/workspaceVirtualEnvWatcherService.ts @@ -7,15 +7,15 @@ 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 = 5_000; +const timeToPollForEnvCreation = 2_000; @injectable() export class WorkspaceVirtualEnvWatcherService implements IInterpreterWatcher, Disposable { @@ -56,31 +56,30 @@ 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 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(); } 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) { - this.timers.delete(pythonPath); - return this.didCreate.fire(); + if (counter > 0) { + this.didCreate.fire(); + } + return this.timers.delete(pythonPath); } - const counter = this.timers.has(pythonPath) ? this.timers.get(pythonPath)!.counter + 1 : 0; if (counter > (maxTimeToWaitForEnvCreation / timeToPollForEnvCreation)) { + // Send notification before we give up trying. + this.didCreate.fire(); this.timers.delete(pythonPath); return; } - const timer = setTimeout(() => { - this.didCreate.fire(); - this.notifyCreationWhenReady(pythonPath).ignoreErrors(); - }, timeToPollForEnvCreation); + const timer = setTimeout(() => this.notifyCreationWhenReady(pythonPath).ignoreErrors(), timeToPollForEnvCreation); this.timers.set(pythonPath, { timer, counter }); } private clearTimers() { From 97bce590c5b3d4f09149bc695928f3dfe2e09569 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Wed, 31 Oct 2018 16:24:47 -0700 Subject: [PATCH 08/16] Update label --- .../diagnostics/checks/invalidPythonPathInDebugger.ts | 2 +- src/client/interpreter/display/index.ts | 2 +- src/test/interpreters/display.unit.test.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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/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/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'); From 299909cba90b8f941f4d80d6103a692d31fb6829 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Thu, 1 Nov 2018 09:38:23 -0700 Subject: [PATCH 09/16] Add progress display --- .../activation/interpreterDataService.ts | 2 +- .../languageServerFolderService.ts | 4 +- .../common/application/applicationShell.ts | 6 +- src/client/common/application/types.ts | 27 +++++++- src/client/common/editor.ts | 2 +- src/client/common/envFileParser.ts | 8 ++- src/client/common/installer/channelManager.ts | 4 +- src/client/common/utils/async.ts | 9 +++ src/client/common/utils/localize.ts | 4 ++ .../extension/configProviders/baseProvider.ts | 2 +- src/client/extension.ts | 13 ++-- src/client/interpreter/contracts.ts | 15 ++++- .../interpreter/display/progressDisplay.ts | 46 +++++++++++++ src/client/interpreter/interpreterService.ts | 3 +- src/client/interpreter/locators/index.ts | 14 +++- .../interpreter/locators/progressService.ts | 65 +++++++++++++++++++ .../services/cacheableLocatorService.ts | 8 ++- src/client/interpreter/serviceRegistry.ts | 7 ++ src/client/typeFormatters/dispatcher.ts | 4 +- src/client/unittests/unittest/helper.ts | 2 +- 20 files changed, 218 insertions(+), 27 deletions(-) create mode 100644 src/client/interpreter/display/progressDisplay.ts create mode 100644 src/client/interpreter/locators/progressService.ts 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/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..b3a5a5bad690 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -14,6 +14,10 @@ export namespace LanguageServiceSurveyBanner { export const bannerLabelNo = localize('LanguageServiceSurveyBanner.bannerLabelNo', 'No, thanks'); } +export namespace 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..084608a018aa 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,6 +215,9 @@ function initializeServices(context: ExtensionContext, serviceManager: ServiceMa const disposables = serviceManager.get(IDisposableRegistry); const dispatcher = new DebugSessionEventDispatcher(handlers, DebugService.instance, disposables); dispatcher.registerEventHandlers(); + + serviceManager.get(InterpreterLocatorProgressHandler).register(); + serviceManager.get(IInterpreterLocatorProgressService).register(); } async function sendStartupTelemetry(activatedPromise: Promise, serviceContainer: IServiceContainer) { @@ -224,12 +228,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/progressDisplay.ts b/src/client/interpreter/display/progressDisplay.ts new file mode 100644 index 000000000000..07d0a1da6ddb --- /dev/null +++ b/src/client/interpreter/display/progressDisplay.ts @@ -0,0 +1,46 @@ +// 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'; + +const progressOptions: ProgressOptions = { location: ProgressLocation.Window, title: Interpreters.refreshing() }; + +@injectable() +export class InterpreterLocatorProgressStatubarHandler implements InterpreterLocatorProgressHandler { + private deferred: Deferred | undefined; + 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() { + 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 ecac28ee6d33..7e2d8bed685b 100644 --- a/src/client/interpreter/locators/services/cacheableLocatorService.ts +++ b/src/client/interpreter/locators/services/cacheableLocatorService.ts @@ -5,7 +5,7 @@ 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'; @@ -19,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); @@ -42,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; 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); From b027087afdc87b77d5d1e0dc5be4234aa370c082 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Thu, 1 Nov 2018 09:54:28 -0700 Subject: [PATCH 10/16] Fix vsc fakes --- src/test/vscode-mock.ts | 1 + 1 file changed, 1 insertion(+) 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(); From e2d2b3a12ef4230229f07ca76788fa17493d0d17 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Thu, 1 Nov 2018 10:00:35 -0700 Subject: [PATCH 11/16] Fix tests --- package.nls.json | 3 ++- src/client/extension.ts | 11 +++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/package.nls.json b/package.nls.json index 1894c63a689e..41ea12af7718 100644 --- a/package.nls.json +++ b/package.nls.json @@ -78,5 +78,6 @@ "DataScience.jupyterNotSupported" : "Jupyter is not installed", "DataScience.jupyterNbConvertNotSupported" : "Jupyter nbconvert is not installed", "DataScience.importingFormat" : "Importing {0}", - "DataScience.startingJupyter" : "Starting Jupyter backend" + "DataScience.startingJupyter" : "Starting Jupyter backend", + "Interpreters.RefreshingInterpreters" : "Refreshing Python Interpreters" } diff --git a/src/client/extension.ts b/src/client/extension.ts index 084608a018aa..f74fed0342d9 100644 --- a/src/client/extension.ts +++ b/src/client/extension.ts @@ -164,6 +164,8 @@ export async function activate(context: ExtensionContext): Promise(IDisposableRegistry); const dispatcher = new DebugSessionEventDispatcher(handlers, DebugService.instance, disposables); dispatcher.registerEventHandlers(); - - serviceManager.get(InterpreterLocatorProgressHandler).register(); - serviceManager.get(IInterpreterLocatorProgressService).register(); } - +function intializeServicesPostActivation(serviceContainer: ServiceContainer) { + // Display progress of interpreter refreshes only after extension has activated. + serviceContainer.get(InterpreterLocatorProgressHandler).register(); + serviceContainer.get(IInterpreterLocatorProgressService).register(); +} async function sendStartupTelemetry(activatedPromise: Promise, serviceContainer: IServiceContainer) { const logger = serviceContainer.get(ILogger); try { From 1dae216ba5f44fcf90654442e48b663f8e7f2a88 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Thu, 1 Nov 2018 10:16:36 -0700 Subject: [PATCH 12/16] Refactor --- src/client/extension.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/client/extension.ts b/src/client/extension.ts index f74fed0342d9..12a1823d851a 100644 --- a/src/client/extension.ts +++ b/src/client/extension.ts @@ -219,6 +219,12 @@ function initializeServices(context: ExtensionContext, serviceManager: ServiceMa dispatcher.registerEventHandlers(); } function intializeServicesPostActivation(serviceContainer: ServiceContainer) { + // 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(); + // Display progress of interpreter refreshes only after extension has activated. serviceContainer.get(InterpreterLocatorProgressHandler).register(); serviceContainer.get(IInterpreterLocatorProgressService).register(); From b0b5c66124aee51357872323cde0208687e3dd3c Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Thu, 1 Nov 2018 10:18:18 -0700 Subject: [PATCH 13/16] Refactor --- src/client/extension.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/client/extension.ts b/src/client/extension.ts index 12a1823d851a..ed27592ab1e1 100644 --- a/src/client/extension.ts +++ b/src/client/extension.ts @@ -164,8 +164,6 @@ export async function activate(context: ExtensionContext): Promise(IDisposableRegistry); const dispatcher = new DebugSessionEventDispatcher(handlers, DebugService.instance, disposables); dispatcher.registerEventHandlers(); -} -function intializeServicesPostActivation(serviceContainer: ServiceContainer) { + // Get latest interpreter list. const workspaceService = serviceContainer.get(IWorkspaceService); const mainWorkspaceUri = workspaceService.hasWorkspaceFolders ? workspaceService.workspaceFolders![0].uri : undefined; From c27a619a4280ec839b6383f9d03bde9b27aff012 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Thu, 1 Nov 2018 10:22:11 -0700 Subject: [PATCH 14/16] Refactor again --- src/client/extension.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/client/extension.ts b/src/client/extension.ts index ed27592ab1e1..88f7cd240083 100644 --- a/src/client/extension.ts +++ b/src/client/extension.ts @@ -216,15 +216,15 @@ function initializeServices(context: ExtensionContext, serviceManager: ServiceMa 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(); - - // Display progress of interpreter refreshes only after extension has activated. - serviceContainer.get(InterpreterLocatorProgressHandler).register(); - serviceContainer.get(IInterpreterLocatorProgressService).register(); } async function sendStartupTelemetry(activatedPromise: Promise, serviceContainer: IServiceContainer) { const logger = serviceContainer.get(ILogger); From 012b3a2a49ba55d103222d88e98bea69ba7450e3 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Thu, 1 Nov 2018 10:37:25 -0700 Subject: [PATCH 15/16] Changes to message --- package.nls.json | 41 ++++++++++--------- src/client/common/utils/localize.ts | 1 + .../interpreter/display/progressDisplay.ts | 8 +++- 3 files changed, 28 insertions(+), 22 deletions(-) diff --git a/package.nls.json b/package.nls.json index 41ea12af7718..d1013d8101ff 100644 --- a/package.nls.json +++ b/package.nls.json @@ -59,25 +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", - "Interpreters.RefreshingInterpreters" : "Refreshing Python Interpreters" + "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/common/utils/localize.ts b/src/client/common/utils/localize.ts index b3a5a5bad690..86a87663dcad 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -15,6 +15,7 @@ export namespace LanguageServiceSurveyBanner { } export namespace Interpreters { + export const loading = localize('Interpreters.LoadingInterpreters', 'Loading Python Interpreters'); export const refreshing = localize('Interpreters.RefreshingInterpreters', 'Refreshing Python Interpreters'); } diff --git a/src/client/interpreter/display/progressDisplay.ts b/src/client/interpreter/display/progressDisplay.ts index 07d0a1da6ddb..e41d49c30580 100644 --- a/src/client/interpreter/display/progressDisplay.ts +++ b/src/client/interpreter/display/progressDisplay.ts @@ -12,11 +12,10 @@ import { createDeferred, Deferred } from '../../common/utils/async'; import { Interpreters } from '../../common/utils/localize'; import { IInterpreterLocatorProgressService, InterpreterLocatorProgressHandler } from '../contracts'; -const progressOptions: ProgressOptions = { location: ProgressLocation.Window, title: Interpreters.refreshing() }; - @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[]) { } @@ -38,6 +37,11 @@ export class InterpreterLocatorProgressStatubarHandler implements InterpreterLoc } } 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; From 4938992db5674b1c820c9f7e035b55df375c369b Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Thu, 1 Nov 2018 11:54:50 -0700 Subject: [PATCH 16/16] Added tests --- .../display/progressDisplay.unit.test.ts | 84 ++++++++++++++ .../cacheableLocatorService.unit.test.ts | 18 +++ .../locators/progressService.unit.test.ts | 109 ++++++++++++++++++ 3 files changed, 211 insertions(+) create mode 100644 src/test/interpreters/display/progressDisplay.unit.test.ts create mode 100644 src/test/interpreters/locators/progressService.unit.test.ts 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'); + }); +});