diff --git a/.vscode/settings.json b/.vscode/settings.json index 06011b3d13cd..97910dfd5a77 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -72,6 +72,7 @@ "main", "release/*" ], + "git.branchProtectionPrompt": "alwaysCommitToNewBranch", "git.pullBeforeCheckout": true, // Open merge editor for resolving conflicts. "git.mergeEditor": true, diff --git a/package-lock.json b/package-lock.json index 70205a90f00e..15012a162b7d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,6 +42,7 @@ "vscode-languageserver": "^8.1.0", "vscode-languageserver-protocol": "^3.17.3", "vscode-tas-client": "^0.1.63", + "vscode-uri": "^3.0.7", "which": "^2.0.2", "winreg": "^1.2.4", "xml2js": "^0.5.0" @@ -14698,9 +14699,9 @@ } }, "node_modules/vscode-uri": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.6.tgz", - "integrity": "sha512-fmL7V1eiDBFRRnu+gfRWTzyPpNIHJTc4mWnFkwBUmO9U3KPgJAmTx7oxi2bl/Rh6HLdU7+4C9wlj0k2E4AdKFQ==" + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.7.tgz", + "integrity": "sha512-eOpPHogvorZRobNqJGhapa0JdwaxpjVvyBp0QIUMRMSf8ZAlqOdEquKuRmw9Qwu0qXtJIWqFtMkmvJjUZmMjVA==" }, "node_modules/watchpack": { "version": "2.4.0", @@ -26929,9 +26930,9 @@ } }, "vscode-uri": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.6.tgz", - "integrity": "sha512-fmL7V1eiDBFRRnu+gfRWTzyPpNIHJTc4mWnFkwBUmO9U3KPgJAmTx7oxi2bl/Rh6HLdU7+4C9wlj0k2E4AdKFQ==" + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.7.tgz", + "integrity": "sha512-eOpPHogvorZRobNqJGhapa0JdwaxpjVvyBp0QIUMRMSf8ZAlqOdEquKuRmw9Qwu0qXtJIWqFtMkmvJjUZmMjVA==" }, "watchpack": { "version": "2.4.0", diff --git a/package.json b/package.json index 7e044a95f7af..047261a5f2d7 100644 --- a/package.json +++ b/package.json @@ -506,7 +506,7 @@ ] }, "python.condaPath": { - "default": "", + "default": "conda", "description": "%python.condaPath.description%", "scope": "machine", "type": "string" @@ -2138,7 +2138,8 @@ "vscode-tas-client": "^0.1.63", "which": "^2.0.2", "winreg": "^1.2.4", - "xml2js": "^0.5.0" + "xml2js": "^0.5.0", + "vscode-uri":"^3.0.7" }, "devDependencies": { "@istanbuljs/nyc-config-typescript": "^1.0.2", @@ -2221,4 +2222,4 @@ "webpack-require-from": "^1.8.6", "yargs": "^15.3.1" } -} \ No newline at end of file +} diff --git a/src/client/common/platform/fileSystemWatcher.ts b/src/client/common/platform/fileSystemWatcher.ts index ef35988d147b..e151dd969f78 100644 --- a/src/client/common/platform/fileSystemWatcher.ts +++ b/src/client/common/platform/fileSystemWatcher.ts @@ -1,8 +1,9 @@ +/* eslint-disable global-require */ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { RelativePattern, workspace } from 'vscode'; -import { traceVerbose } from '../../logging'; +import type * as vscodeTypes from 'vscode'; +import { traceError, traceVerbose } from '../../logging'; import { IDisposable } from '../types'; import { Disposables } from '../utils/resourceLifecycle'; @@ -20,12 +21,20 @@ export function watchLocationForPattern( pattern: string, callback: (type: FileChangeType, absPath: string) => void, ): IDisposable { - const globPattern = new RelativePattern(baseDir, pattern); - const disposables = new Disposables(); - traceVerbose(`Start watching: ${baseDir} with pattern ${pattern} using VSCode API`); - const watcher = workspace.createFileSystemWatcher(globPattern); - disposables.push(watcher.onDidCreate((e) => callback(FileChangeType.Created, e.fsPath))); - disposables.push(watcher.onDidChange((e) => callback(FileChangeType.Changed, e.fsPath))); - disposables.push(watcher.onDidDelete((e) => callback(FileChangeType.Deleted, e.fsPath))); - return disposables; + try { + const vscode = require('vscode') as typeof vscodeTypes; + const globPattern = new vscode.RelativePattern(baseDir, pattern); + const disposables = new Disposables(); + traceVerbose(`Start watching: ${baseDir} with pattern ${pattern} using VSCode API`); + const watcher = vscode.workspace.createFileSystemWatcher(globPattern); + disposables.push(watcher.onDidCreate((e) => callback(FileChangeType.Created, e.fsPath))); + disposables.push(watcher.onDidChange((e) => callback(FileChangeType.Changed, e.fsPath))); + disposables.push(watcher.onDidDelete((e) => callback(FileChangeType.Deleted, e.fsPath))); + return disposables; + } catch (ex) { + traceError('Watcher', (ex as Error).message); + console.log('Watcher', (ex as Error).message); + // eslint-disable-next-line @typescript-eslint/no-empty-function + return { dispose: () => {} }; + } } diff --git a/src/client/common/utils/filesystem.ts b/src/client/common/utils/filesystem.ts index f2708e523bfb..436dfc7cf67b 100644 --- a/src/client/common/utils/filesystem.ts +++ b/src/client/common/utils/filesystem.ts @@ -2,10 +2,14 @@ // Licensed under the MIT License. import * as fs from 'fs'; -import * as vscode from 'vscode'; import { traceError } from '../../logging'; -export import FileType = vscode.FileType; +export enum FileType { + Unknown = 0, + File = 1, + Directory = 2, + SymbolicLink = 64, +} export type DirEntry = { filename: string; diff --git a/src/client/pythonEnvironments/base/info/env.ts b/src/client/pythonEnvironments/base/info/env.ts index 12b3e519b944..99d32aefe30a 100644 --- a/src/client/pythonEnvironments/base/info/env.ts +++ b/src/client/pythonEnvironments/base/info/env.ts @@ -4,7 +4,6 @@ import { cloneDeep, isEqual } from 'lodash'; import * as path from 'path'; import { Uri } from 'vscode'; -import { getArchitectureDisplayName } from '../../../common/platform/registry'; import { Architecture } from '../../../common/utils/platform'; import { arePathsSame, isParentPath, normCasePath } from '../../common/externalDependencies'; import { getKindDisplayName } from './envKind'; @@ -77,6 +76,17 @@ export function buildEnvInfo(init?: { return env; } +export function getArchitectureDisplayName(arch?: Architecture): string { + switch (arch) { + case Architecture.x64: + return '64-bit'; + case Architecture.x86: + return '32-bit'; + default: + return ''; + } +} + export function areEnvsDeepEqual(env1: PythonEnvInfo, env2: PythonEnvInfo): boolean { const env1Clone = cloneDeep(env1); const env2Clone = cloneDeep(env2); diff --git a/src/client/pythonEnvironments/base/locator.ts b/src/client/pythonEnvironments/base/locator.ts index ab3b17629bc5..a7b3a9dbca9a 100644 --- a/src/client/pythonEnvironments/base/locator.ts +++ b/src/client/pythonEnvironments/base/locator.ts @@ -195,6 +195,24 @@ interface IResolver { export interface IResolvingLocator extends IResolver, ICompositeLocator {} +export type EnvIteratorId = number; + +export interface IMiddleware { + iterInitialize(query?: PythonLocatorQuery): Promise; + iterNext(id: EnvIteratorId): Promise; + resolveEnv(path: string): Promise; +} + +export interface IEnvsMiddleware extends IMiddleware { + iterOnUpdated(id: EnvIteratorId): Event | undefined; + readonly onChanged: Event; +} + +export interface IWorkerMiddleWare extends IMiddleware { + iterOnUpdated(id: EnvIteratorId): Event | undefined; + readonly onChanged: Event; +} + export interface GetRefreshEnvironmentsOptions { /** * Get refresh promise which resolves once the following stage has been reached for the list of known environments. diff --git a/src/client/pythonEnvironments/base/locators/composite/envsCollectionService.ts b/src/client/pythonEnvironments/base/locators/composite/envsCollectionService.ts index 2567168c6325..043da9a757ff 100644 --- a/src/client/pythonEnvironments/base/locators/composite/envsCollectionService.ts +++ b/src/client/pythonEnvironments/base/locators/composite/envsCollectionService.ts @@ -14,7 +14,7 @@ import { getEnvPath } from '../../info/env'; import { GetRefreshEnvironmentsOptions, IDiscoveryAPI, - IResolvingLocator, + IEnvsMiddleware, isProgressEvent, ProgressNotificationEvent, ProgressReportStage, @@ -54,9 +54,9 @@ export class EnvsCollectionService extends PythonEnvsWatcher { + this.middleware.onChanged((event) => { const query: PythonLocatorQuery | undefined = event.providerId ? { providerId: event.providerId, envPath: event.envPath } : undefined; // We can also form a query based on the event, but skip that for simplicity. @@ -91,7 +91,7 @@ export class EnvsCollectionService extends PythonEnvsWatcher { + const resolved = await this.middleware.resolveEnv(path).catch((ex) => { traceError(`Failed to resolve ${path}`, ex); return undefined; }); @@ -133,16 +133,16 @@ export class EnvsCollectionService extends PythonEnvsWatcher(); - - if (iterator.onUpdated !== undefined) { - const listener = iterator.onUpdated(async (event) => { + const itOnUpdated = this.middleware.iterOnUpdated(iteratorId); + if (itOnUpdated !== undefined) { + const listener = itOnUpdated(async (event) => { if (isProgressEvent(event)) { switch (event.stage) { case ProgressReportStage.discoveryFinished: @@ -175,9 +175,11 @@ export class EnvsCollectionService extends PythonEnvsWatcher(); + + constructor(folders: readonly WorkspaceFolder[] | undefined, settings: PythonDiscoverySettings) { + super(); + const { locator, disposables, workspaceLocator } = createSubLocators(folders, settings); + this.disposables = disposables; + this.locator = locator; + this.workspaceLocator = workspaceLocator; + this.locator.onChanged((e) => { + this.fire(e); + }); + } + + public dispose(): void { + this.disposables.forEach((d) => d.dispose()); + } + + public onDidChangeWorkspaceFolders(event: WorkspaceFoldersChangeEvent): void { + this.workspaceLocator.onDidChangeWorkspaceFolders(event); + } + + public async resolveEnv(path: string): Promise { + return this.locator.resolveEnv(path); + } + + public async iterInitialize(query?: PythonLocatorQuery | undefined): Promise { + const it = this.locator.iterEnvs(query); + const id = this.idCount; + this.iterators.set(id, it); + return id; + } + + public async iterNext(id: EnvIteratorId): Promise { + const it = this.getIterator(id); + const result = await it?.next(); + if (result?.done) { + return undefined; + } + return result?.value; + } + + public iterOnUpdated(id: EnvIteratorId): Event | undefined { + const it = this.getIterator(id); + return it?.onUpdated; + } + + private getIterator(id: EnvIteratorId) { + return this.iterators.get(id); + } +} diff --git a/src/client/pythonEnvironments/base/locators/composite/envsReducer.ts b/src/client/pythonEnvironments/base/locators/composite/envsReducer.ts index 49f5b619694e..d140cf602d6e 100644 --- a/src/client/pythonEnvironments/base/locators/composite/envsReducer.ts +++ b/src/client/pythonEnvironments/base/locators/composite/envsReducer.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License. import { cloneDeep, isEqual, uniq } from 'lodash'; -import { Event, EventEmitter } from 'vscode'; +import { Event } from 'vscode'; import { traceVerbose } from '../../../../logging'; import { PythonEnvKind } from '../../info'; import { areSameEnv } from '../../info/env'; @@ -19,6 +19,7 @@ import { PythonLocatorQuery, } from '../../locator'; import { PythonEnvsChangedEvent } from '../../watcher'; +import { EventEmitter } from '../../../common/eventEmitter'; /** * Combines duplicate environments received from the incoming locator into one and passes on unique environments diff --git a/src/client/pythonEnvironments/base/locators/composite/envsResolver.ts b/src/client/pythonEnvironments/base/locators/composite/envsResolver.ts index 2ba54e07ed9c..bf544a4ab689 100644 --- a/src/client/pythonEnvironments/base/locators/composite/envsResolver.ts +++ b/src/client/pythonEnvironments/base/locators/composite/envsResolver.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License. import { cloneDeep } from 'lodash'; -import { Event, EventEmitter } from 'vscode'; +import { Event, WorkspaceFolder } from 'vscode'; import { identifyEnvironment } from '../../../common/environmentIdentifier'; import { IEnvironmentInfoService } from '../../info/environmentInfoService'; import { PythonEnvInfo, PythonEnvKind } from '../../info'; @@ -24,6 +24,7 @@ import { resolveBasicEnv } from './resolverUtils'; import { traceVerbose, traceWarn } from '../../../../logging'; import { getEnvironmentDirFromPath, getInterpreterPathFromDir, isPythonExecutable } from '../../../common/commonUtils'; import { getEmptyVersion } from '../../info/pythonVersion'; +import { EventEmitter } from '../../../common/eventEmitter'; /** * Calls environment info service which runs `interpreterInfo.py` script on environments received @@ -37,6 +38,7 @@ export class PythonEnvsResolver implements IResolvingLocator { constructor( private readonly parentLocator: ICompositeLocator, private readonly environmentInfoService: IEnvironmentInfoService, + private readonly workspaceFolders: readonly WorkspaceFolder[] | undefined, ) { this.parentLocator.onChanged((event) => { if (event.type && event.searchLocation !== undefined) { @@ -50,7 +52,7 @@ export class PythonEnvsResolver implements IResolvingLocator { const [executablePath, envPath] = await getExecutablePathAndEnvPath(path); path = executablePath.length ? executablePath : envPath; const kind = await identifyEnvironment(path); - const environment = await resolveBasicEnv({ kind, executablePath, envPath }); + const environment = await resolveBasicEnv({ kind, executablePath, envPath }, this.workspaceFolders); const info = await this.environmentInfoService.getEnvironmentInfo(environment); traceVerbose( `Environment resolver resolved ${path} for ${JSON.stringify(environment)} to ${JSON.stringify(info)}`, @@ -98,7 +100,7 @@ export class PythonEnvsResolver implements IResolvingLocator { } else if (seen[event.index] !== undefined) { const old = seen[event.index]; await setKind(event.update, environmentKinds); - seen[event.index] = await resolveBasicEnv(event.update); + seen[event.index] = await resolveBasicEnv(event.update, this.workspaceFolders); didUpdate.fire({ old, index: event.index, update: seen[event.index] }); this.resolveInBackground(event.index, state, didUpdate, seen).ignoreErrors(); } else { @@ -116,7 +118,7 @@ export class PythonEnvsResolver implements IResolvingLocator { while (!result.done) { // Use cache from the current refresh where possible. await setKind(result.value, environmentKinds); - const currEnv = await resolveBasicEnv(result.value); + const currEnv = await resolveBasicEnv(result.value, this.workspaceFolders); seen.push(currEnv); yield currEnv; this.resolveInBackground(seen.indexOf(currEnv), state, didUpdate, seen).ignoreErrors(); diff --git a/src/client/pythonEnvironments/base/locators/composite/resolverUtils.ts b/src/client/pythonEnvironments/base/locators/composite/resolverUtils.ts index 0cca49e2b4c5..fb8c3cfe9305 100644 --- a/src/client/pythonEnvironments/base/locators/composite/resolverUtils.ts +++ b/src/client/pythonEnvironments/base/locators/composite/resolverUtils.ts @@ -2,8 +2,9 @@ // Licensed under the MIT License. import * as path from 'path'; -import { Uri } from 'vscode'; +import { URI as Uri } from 'vscode-uri'; import { uniq } from 'lodash'; +import { WorkspaceFolder } from 'vscode'; import { PythonEnvInfo, PythonEnvKind, @@ -29,7 +30,6 @@ import { BasicEnvInfo } from '../../locator'; import { parseVersionFromExecutable } from '../../info/executable'; import { traceError, traceWarn } from '../../../../logging'; import { isVirtualEnvironment } from '../../../common/environmentManagers/simplevirtualenvs'; -import { getWorkspaceFolderPaths } from '../../../../common/vscodeApis/workspaceApis'; import { ActiveState } from '../../../common/environmentManagers/activestate'; function getResolvers(): Map Promise> { @@ -52,12 +52,15 @@ function getResolvers(): Map Promise { +export async function resolveBasicEnv( + env: BasicEnvInfo, + folders: readonly WorkspaceFolder[] | undefined, +): Promise { const { kind, source } = env; const resolvers = getResolvers(); const resolverForKind = resolvers.get(kind)!; const resolvedEnv = await resolverForKind(env); - resolvedEnv.searchLocation = getSearchLocation(resolvedEnv); + resolvedEnv.searchLocation = getSearchLocation(resolvedEnv, folders); resolvedEnv.source = uniq(resolvedEnv.source.concat(source ?? [])); if (getOSType() === OSType.Windows && resolvedEnv.source?.includes(PythonEnvSource.WindowsRegistry)) { // We can update env further using information we can get from the Windows registry. @@ -87,9 +90,11 @@ async function getEnvType(env: PythonEnvInfo) { return undefined; } -function getSearchLocation(env: PythonEnvInfo): Uri | undefined { - const folders = getWorkspaceFolderPaths(); - const isRootedEnv = folders.some((f) => isParentPath(env.executable.filename, f) || isParentPath(env.location, f)); +function getSearchLocation(env: PythonEnvInfo, folders: readonly WorkspaceFolder[] | undefined): Uri | undefined { + const folderPaths = folders?.map((w) => w.uri.fsPath) ?? []; + const isRootedEnv = folderPaths.some( + (f) => isParentPath(env.executable.filename, f) || isParentPath(env.location, f), + ); if (isRootedEnv) { // For environments inside roots, we need to set search location so they can be queried accordingly. // In certain usecases environment directory can itself be a root, for eg. `python -m venv .`. diff --git a/src/client/pythonEnvironments/base/locators/composite/worker.ts b/src/client/pythonEnvironments/base/locators/composite/worker.ts new file mode 100644 index 000000000000..9491bc54e8cd --- /dev/null +++ b/src/client/pythonEnvironments/base/locators/composite/worker.ts @@ -0,0 +1,56 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/ban-types */ +/* eslint-disable no-restricted-globals */ +import { parentPort, workerData } from 'worker_threads'; +import { WorkspaceFolder } from 'vscode'; +import { URI as Uri } from 'vscode-uri'; +import { EnvsMiddleWare } from './envsMiddleware'; +import { traceError } from '../../../../logging'; + +const { folders, settings } = workerData; +const workspaceFolders = (folders as WorkspaceFolder[]).map((w) => { + const wuri = w.uri; + const workspaceFolder = { + name: w.name, + uri: Uri.parse((w.uri as unknown) as string), + index: w.index, + }; + if (typeof wuri === 'string') { + workspaceFolder.uri = Uri.parse(wuri); + } else if ('scheme' in wuri && 'path' in wuri) { + workspaceFolder.uri = Uri.parse(`${wuri.scheme}://${wuri.path}`); + } else { + traceError('Unexpected search location', JSON.stringify(wuri)); + } + return workspaceFolder; +}); + +const envsMiddleware = new EnvsMiddleWare(workspaceFolders, settings); + +if (!parentPort) { + throw new Error('Not in a worker thread'); +} + +console.log('Worker thread started'); + +// // Listen for messages from the main thread +parentPort.on('message', async (event) => { + // console.log(JSON.stringify(settings)); + console.log('Worker thread received message', event); + if (!parentPort) { + throw new Error('Not in a worker thread'); + } + type EventType = { methodName: string; args: any[] }; + const { methodName, args } = event as EventType; + if (methodName && typeof envsMiddleware[methodName as keyof EnvsMiddleWare] === 'function') { + const method = envsMiddleware[methodName as keyof EnvsMiddleWare] as Function; + try { + const result = await method.apply(envsMiddleware, args); + parentPort.postMessage({ methodName, result }); + } catch (error) { + parentPort.postMessage({ methodName, error: (error as Error).message }); + } + } else { + parentPort.postMessage({ methodName, error: 'Invalid method name' }); + } +}); diff --git a/src/client/pythonEnvironments/base/locators/composite/workerMiddleware.ts b/src/client/pythonEnvironments/base/locators/composite/workerMiddleware.ts new file mode 100644 index 000000000000..130521c36e87 --- /dev/null +++ b/src/client/pythonEnvironments/base/locators/composite/workerMiddleware.ts @@ -0,0 +1,101 @@ +import { Event, EventEmitter, Uri, WorkspaceFolder, WorkspaceFoldersChangeEvent } from 'vscode'; +import * as path from 'path'; +import { Worker } from 'worker_threads'; +import { PythonEnvInfo } from '../../info'; +import { + EnvIteratorId, + IWorkerMiddleWare, + ProgressNotificationEvent, + PythonEnvUpdatedEvent, + PythonLocatorQuery, +} from '../../locator'; +import { PythonEnvsWatcher } from '../../watcher'; +import { PythonDiscoverySettings } from '../../../common/settings'; + +/** + * A service acts as a bridge between Env Resolver and Env Collection. + */ +export class WorkerThreadMiddleWare extends PythonEnvsWatcher implements IWorkerMiddleWare { + private worker: Worker; + + private onUpdatedMap = new Map>(); + + constructor(folders: readonly WorkspaceFolder[] | undefined, settings: PythonDiscoverySettings) { + super(); + this.worker = new Worker(path.join(__dirname, 'worker.js'), { + workerData: { folders: convertWorkspaceFolders(folders), settings }, + }); + this.worker.addListener('message', (event) => { + const { methodName, result } = event; + if (methodName === 'onChanged') { + this.fire(result); + } + }); + } + + public async resolveEnv(p: string): Promise { + return this.callMethod('resolveEnv', [p]); + } + + public async iterInitialize(query?: PythonLocatorQuery | undefined): Promise { + const id = await this.callMethod('iterInitialize', [query]); + const eventEmitter = new EventEmitter(); + this.onUpdatedMap.set(id, eventEmitter.event); + this.worker.addListener('message', (event) => { + const { methodName, result } = event; + if (methodName === 'iterOnUpdated' && result.id === id) { + eventEmitter.fire(result.eventData); + } + }); + return id; + } + + public async iterNext(id: EnvIteratorId): Promise { + return this.callMethod('iterNext', [id]); + } + + public async onDidChangeWorkspaceFolders(event: WorkspaceFoldersChangeEvent): Promise { + await this.callMethod('onDidChangeWorkspaceFolders', [event]); + } + + public async dispose(): Promise { + await this.callMethod('dispose', []); + } + + public iterOnUpdated(id: EnvIteratorId): Event | undefined { + return this.onUpdatedMap.get(id); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private async callMethod(currMethod: string, args: any[]): Promise { + return new Promise((resolve, reject) => { + this.worker.addListener('message', (event) => { + const { methodName, result, error } = event; + if (currMethod !== methodName) { + return; + } + if (result !== undefined) { + resolve(result); + } else if (error) { + reject(new Error(error)); + } + }); + + this.worker.postMessage({ methodName: currMethod, args }); + }); + } +} + +function convertWorkspaceFolders(workspaceFolders: readonly WorkspaceFolder[] | undefined) { + if (!workspaceFolders) { + return []; + } + return workspaceFolders.map((w) => { + const workspaceFolder: WorkspaceFolder = { + name: w.name, + uri: (w.uri.toString() as unknown) as Uri, + index: w.index, + }; + return workspaceFolder; + }); +} diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/activeStateLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/activeStateLocator.ts index dc507b9c94bd..76949e8dcc64 100644 --- a/src/client/pythonEnvironments/base/locators/lowLevel/activeStateLocator.ts +++ b/src/client/pythonEnvironments/base/locators/lowLevel/activeStateLocator.ts @@ -9,13 +9,18 @@ import { BasicEnvInfo, IPythonEnvsIterator } from '../../locator'; import { traceError, traceVerbose } from '../../../../logging'; import { LazyResourceBasedLocator } from '../common/resourceBasedLocator'; import { findInterpretersInDir } from '../../../common/commonUtils'; +import { PythonDiscoverySettings } from '../../../common/settings'; export class ActiveStateLocator extends LazyResourceBasedLocator { public readonly providerId: string = 'activestate'; + public constructor(private readonly settings: PythonDiscoverySettings) { + super(); + } + // eslint-disable-next-line class-methods-use-this public async *doIterEnvs(): IPythonEnvsIterator { - const state = await ActiveState.getState(); + const state = await ActiveState.getState(this.settings); if (state === undefined) { traceVerbose(`Couldn't locate the state binary.`); return; diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/condaLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/condaLocator.ts index 7cac0cb7df90..cdc2b37ec5f9 100644 --- a/src/client/pythonEnvironments/base/locators/lowLevel/condaLocator.ts +++ b/src/client/pythonEnvironments/base/locators/lowLevel/condaLocator.ts @@ -6,11 +6,12 @@ import { BasicEnvInfo, IPythonEnvsIterator } from '../../locator'; import { Conda, getCondaEnvironmentsTxt } from '../../../common/environmentManagers/conda'; import { traceError, traceVerbose } from '../../../../logging'; import { FSWatchingLocator } from './fsWatchingLocator'; +import { PythonDiscoverySettings } from '../../../common/settings'; export class CondaEnvironmentLocator extends FSWatchingLocator { public readonly providerId: string = 'conda-envs'; - public constructor() { + public constructor(private readonly settings: PythonDiscoverySettings) { super( () => getCondaEnvironmentsTxt(), async () => PythonEnvKind.Conda, @@ -20,7 +21,7 @@ export class CondaEnvironmentLocator extends FSWatchingLocator { // eslint-disable-next-line class-methods-use-this public async *doIterEnvs(): IPythonEnvsIterator { - const conda = await Conda.getConda(); + const conda = await Conda.getConda(undefined, this.settings); if (conda === undefined) { traceVerbose(`Couldn't locate the conda binary.`); return; diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/customVirtualEnvLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/customVirtualEnvLocator.ts index 57ae9187cdc2..2a31653870f5 100644 --- a/src/client/pythonEnvironments/base/locators/lowLevel/customVirtualEnvLocator.ts +++ b/src/client/pythonEnvironments/base/locators/lowLevel/customVirtualEnvLocator.ts @@ -9,12 +9,7 @@ import { PythonEnvKind } from '../../info'; import { BasicEnvInfo, IPythonEnvsIterator } from '../../locator'; import { FSWatchingLocator } from './fsWatchingLocator'; import { findInterpretersInDir, looksLikeBasicVirtualPython } from '../../../common/commonUtils'; -import { - getPythonSetting, - onDidChangePythonSetting, - pathExists, - untildify, -} from '../../../common/externalDependencies'; +import { onDidChangePythonSetting, pathExists, untildify } from '../../../common/externalDependencies'; import { isPipenvEnvironment } from '../../../common/environmentManagers/pipenv'; import { isVenvEnvironment, @@ -24,6 +19,7 @@ import { import '../../../../common/extensions'; import { asyncFilter } from '../../../../common/utils/arrayUtils'; import { traceError, traceVerbose } from '../../../../logging'; +import { PythonDiscoverySettings } from '../../../common/settings'; /** * Default number of levels of sub-directories to recurse when looking for interpreters. */ @@ -35,13 +31,13 @@ export const VENVFOLDERS_SETTING_KEY = 'venvFolders'; /** * Gets all custom virtual environment locations to look for environments. */ -async function getCustomVirtualEnvDirs(): Promise { +async function getCustomVirtualEnvDirs(settings: PythonDiscoverySettings): Promise { const venvDirs: string[] = []; - const venvPath = getPythonSetting(VENVPATH_SETTING_KEY); + const venvPath = settings[VENVPATH_SETTING_KEY]; if (venvPath) { venvDirs.push(untildify(venvPath)); } - const venvFolders = getPythonSetting(VENVFOLDERS_SETTING_KEY) ?? []; + const venvFolders: string[] = settings[VENVFOLDERS_SETTING_KEY]; const homeDir = getUserHomeDir(); if (homeDir && (await pathExists(homeDir))) { venvFolders.map((item) => path.join(homeDir, item)).forEach((d) => venvDirs.push(d)); @@ -81,8 +77,8 @@ async function getVirtualEnvKind(interpreterPath: string): Promise getCustomVirtualEnvDirs(settings), getVirtualEnvKind, { // Note detecting kind of virtual env depends on the file structure around the // executable, so we need to wait before attempting to detect it. However even // if the type detected is incorrect, it doesn't do any practical harm as kinds @@ -98,8 +94,8 @@ export class CustomVirtualEnvironmentLocator extends FSWatchingLocator { // eslint-disable-next-line class-methods-use-this protected doIterEnvs(): IPythonEnvsIterator { - async function* iterator() { - const envRootDirs = await getCustomVirtualEnvDirs(); + async function* iterator(settings: PythonDiscoverySettings) { + const envRootDirs = await getCustomVirtualEnvDirs(settings); const envGenerators = envRootDirs.map((envRootDir) => { async function* generator() { traceVerbose(`Searching for custom virtual envs in: ${envRootDir}`); @@ -135,6 +131,6 @@ export class CustomVirtualEnvironmentLocator extends FSWatchingLocator { traceVerbose(`Finished searching for custom virtual envs`); } - return iterator(); + return iterator(this.settings); } } diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/fsWatchingLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/fsWatchingLocator.ts index 0eb1d125200c..3eedf7b6681b 100644 --- a/src/client/pythonEnvironments/base/locators/lowLevel/fsWatchingLocator.ts +++ b/src/client/pythonEnvironments/base/locators/lowLevel/fsWatchingLocator.ts @@ -3,7 +3,7 @@ import * as fs from 'fs'; import * as path from 'path'; -import { Uri } from 'vscode'; +import { URI as Uri } from 'vscode-uri'; import { FileChangeType, watchLocationForPattern } from '../../../../common/platform/fileSystemWatcher'; import { sleep } from '../../../../common/utils/async'; import { traceError, traceVerbose } from '../../../../logging'; diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/poetryLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/poetryLocator.ts index 4084c7a5cfbc..f1e6c04fb296 100644 --- a/src/client/pythonEnvironments/base/locators/lowLevel/poetryLocator.ts +++ b/src/client/pythonEnvironments/base/locators/lowLevel/poetryLocator.ts @@ -14,13 +14,14 @@ import '../../../../common/extensions'; import { asyncFilter } from '../../../../common/utils/arrayUtils'; import { traceError, traceVerbose } from '../../../../logging'; import { LazyResourceBasedLocator } from '../common/resourceBasedLocator'; +import { PythonDiscoverySettings } from '../../../common/settings'; /** * Gets all default virtual environment locations to look for in a workspace. */ -async function getVirtualEnvDirs(root: string): Promise { +async function getVirtualEnvDirs(root: string, settings: PythonDiscoverySettings): Promise { const envDirs = [path.join(root, localPoetryEnvDirName)]; - const poetry = await Poetry.getPoetry(root); + const poetry = await Poetry.getPoetry(root, settings); const virtualenvs = await poetry?.getEnvList(); if (virtualenvs) { envDirs.push(...virtualenvs); @@ -42,13 +43,13 @@ async function getVirtualEnvKind(interpreterPath: string): Promise { - async function* iterator(root: string) { - const envDirs = await getVirtualEnvDirs(root); + async function* iterator(root: string, settings: PythonDiscoverySettings) { + const envDirs = await getVirtualEnvDirs(root, settings); const envGenerators = envDirs.map((envDir) => { async function* generator() { traceVerbose(`Searching for poetry virtual envs in: ${envDir}`); @@ -73,6 +74,6 @@ export class PoetryLocator extends LazyResourceBasedLocator { traceVerbose(`Finished searching for poetry envs`); } - return iterator(this.root); + return iterator(this.root, this.settings); } } diff --git a/src/client/pythonEnvironments/base/locators/wrappers.ts b/src/client/pythonEnvironments/base/locators/wrappers.ts index bfaede584f6f..7e31156bb7a2 100644 --- a/src/client/pythonEnvironments/base/locators/wrappers.ts +++ b/src/client/pythonEnvironments/base/locators/wrappers.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License. // eslint-disable-next-line max-classes-per-file -import { Uri } from 'vscode'; +import { Uri, WorkspaceFolder, WorkspaceFoldersChangeEvent } from 'vscode'; import { IDisposable } from '../../../common/types'; import { iterEmpty } from '../../../common/utils/async'; import { getURIFilter } from '../../../common/utils/misc'; @@ -42,13 +42,6 @@ type WorkspaceLocatorFactoryResult = ILocator & Partial WorkspaceLocatorFactoryResult[]; type RootURI = string; -export type WatchRootsArgs = { - initRoot(root: Uri): void; - addRoot(root: Uri): void; - removeRoot(root: Uri): void; -}; -type WatchRootsFunc = (args: WatchRootsArgs) => IDisposable; -// XXX Factor out RootedLocators and MultiRootedLocators. /** * The collection of all workspace-specific locators used by the extension. * @@ -62,7 +55,24 @@ export class WorkspaceLocators extends LazyResourceBasedLocator { private readonly roots: Record = {}; - constructor(private readonly watchRoots: WatchRootsFunc, private readonly factories: WorkspaceLocatorFactory[]) { + private watchArgs = { + initRoot: (root: Uri) => this.addRoot(root), + addRoot: (root: Uri) => { + // Drop the old one, if necessary. + this.removeRoot(root); + this.addRoot(root); + this.emitter.fire({ searchLocation: root }); + }, + removeRoot: (root: Uri) => { + this.removeRoot(root); + this.emitter.fire({ searchLocation: root }); + }, + }; + + constructor( + private readonly folders: readonly WorkspaceFolder[] | undefined, + private readonly factories: WorkspaceLocatorFactory[], + ) { super(); this.activate().ignoreErrors(); } @@ -99,20 +109,18 @@ export class WorkspaceLocators extends LazyResourceBasedLocator { } protected async initResources(): Promise { - const disposable = this.watchRoots({ - initRoot: (root: Uri) => this.addRoot(root), - addRoot: (root: Uri) => { - // Drop the old one, if necessary. - this.removeRoot(root); - this.addRoot(root); - this.emitter.fire({ searchLocation: root }); - }, - removeRoot: (root: Uri) => { - this.removeRoot(root); - this.emitter.fire({ searchLocation: root }); - }, - }); - this.disposables.push(disposable); + if (this.folders) { + this.folders.map((f) => f.uri).forEach(this.watchArgs.initRoot); + } + } + + public onDidChangeWorkspaceFolders(event: WorkspaceFoldersChangeEvent): void { + for (const root of event.removed) { + this.watchArgs.removeRoot(root.uri); + } + for (const root of event.added) { + this.watchArgs.addRoot(root.uri); + } } private addRoot(root: Uri): void { diff --git a/src/client/pythonEnvironments/base/watcher.ts b/src/client/pythonEnvironments/base/watcher.ts index a9d0ef65595e..af13d99bed05 100644 --- a/src/client/pythonEnvironments/base/watcher.ts +++ b/src/client/pythonEnvironments/base/watcher.ts @@ -1,9 +1,10 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { Event, EventEmitter, Uri } from 'vscode'; +import { Event, Uri } from 'vscode'; import { FileChangeType } from '../../common/platform/fileSystemWatcher'; import { PythonEnvInfo, PythonEnvKind } from './info'; +import { EventEmitter } from '../common/eventEmitter'; // The use cases for `BasicPythonEnvsChangedEvent` are currently // hypothetical. However, there's a real chance they may prove @@ -84,9 +85,11 @@ export class PythonEnvsWatcher implements IPythonEnv */ public readonly onChanged: Event; - private readonly didChange = new EventEmitter(); + private readonly didChange: EventEmitter; constructor() { + this.didChange = new EventEmitter(); + this.onChanged = this.didChange.event; } diff --git a/src/client/pythonEnvironments/common/environmentManagers/activestate.ts b/src/client/pythonEnvironments/common/environmentManagers/activestate.ts index 75b34f41176c..e81677f11203 100644 --- a/src/client/pythonEnvironments/common/environmentManagers/activestate.ts +++ b/src/client/pythonEnvironments/common/environmentManagers/activestate.ts @@ -15,6 +15,7 @@ import { import { cache } from '../../../common/utils/decorators'; import { traceError, traceVerbose } from '../../../logging'; import { getOSType, getUserHomeDir, OSType } from '../../../common/utils/platform'; +import { PythonDiscoverySettings } from '../settings'; export const ACTIVESTATETOOLPATH_SETTING_KEY = 'activeStateToolPath'; @@ -36,9 +37,9 @@ export async function isActiveStateEnvironment(interpreterPath: string): Promise export class ActiveState { private static statePromise: Promise | undefined; - public static async getState(): Promise { + public static async getState(settings?: PythonDiscoverySettings): Promise { if (ActiveState.statePromise === undefined) { - ActiveState.statePromise = ActiveState.locate(); + ActiveState.statePromise = ActiveState.locate(settings); } return ActiveState.statePromise; } @@ -59,10 +60,11 @@ export class ActiveState { : path.join(home, '.local', 'ActiveState', 'StateTool'); } - private static async locate(): Promise { + private static async locate(settings?: PythonDiscoverySettings): Promise { const stateToolDir = this.getStateToolDir(); - const stateCommand = - getPythonSetting(ACTIVESTATETOOLPATH_SETTING_KEY) ?? ActiveState.defaultStateCommand; + const stateCommand = settings + ? settings[ACTIVESTATETOOLPATH_SETTING_KEY] + : getPythonSetting(ACTIVESTATETOOLPATH_SETTING_KEY); if (stateToolDir && ((await pathExists(stateToolDir)) || stateCommand !== this.defaultStateCommand)) { return new ActiveState(); } diff --git a/src/client/pythonEnvironments/common/environmentManagers/conda.ts b/src/client/pythonEnvironments/common/environmentManagers/conda.ts index 88178d02d58a..473046819679 100644 --- a/src/client/pythonEnvironments/common/environmentManagers/conda.ts +++ b/src/client/pythonEnvironments/common/environmentManagers/conda.ts @@ -23,6 +23,7 @@ import { traceError, traceVerbose } from '../../../logging'; import { OUTPUT_MARKER_SCRIPT } from '../../../common/process/internal/scripts'; import { splitLines } from '../../../common/stringUtils'; import { SpawnOptions } from '../../../common/process/types'; +import { PythonDiscoverySettings } from '../settings'; export const AnacondaCompanyName = 'Anaconda, Inc.'; export const CONDAPATH_SETTING_KEY = 'condaPath'; @@ -271,9 +272,9 @@ export class Conda { }); } - public static async getConda(shellPath?: string): Promise { + public static async getConda(shellPath?: string, settings?: PythonDiscoverySettings): Promise { if (Conda.condaPromise.get(shellPath) === undefined || isTestExecution()) { - Conda.condaPromise.set(shellPath, Conda.locate(shellPath)); + Conda.condaPromise.set(shellPath, Conda.locate(shellPath, settings)); } return Conda.condaPromise.get(shellPath); } @@ -284,10 +285,12 @@ export class Conda { * * @return A Conda instance corresponding to the binary, if successful; otherwise, undefined. */ - private static async locate(shellPath?: string): Promise { + private static async locate(shellPath?: string, settings?: PythonDiscoverySettings): Promise { traceVerbose(`Searching for conda.`); const home = getUserHomeDir(); - const customCondaPath = getPythonSetting(CONDAPATH_SETTING_KEY); + const customCondaPath = settings + ? settings[CONDAPATH_SETTING_KEY] + : getPythonSetting(CONDAPATH_SETTING_KEY); const suffix = getOSType() === OSType.Windows ? 'Scripts\\conda.exe' : 'bin/conda'; // Produce a list of candidate binaries to be probed by exec'ing them. diff --git a/src/client/pythonEnvironments/common/environmentManagers/poetry.ts b/src/client/pythonEnvironments/common/environmentManagers/poetry.ts index 48199b5bdc8f..a456b87c53d8 100644 --- a/src/client/pythonEnvironments/common/environmentManagers/poetry.ts +++ b/src/client/pythonEnvironments/common/environmentManagers/poetry.ts @@ -20,6 +20,7 @@ import { cache } from '../../../common/utils/decorators'; import { isTestExecution } from '../../../common/constants'; import { traceError, traceVerbose } from '../../../logging'; import { splitLines } from '../../../common/stringUtils'; +import { PythonDiscoverySettings } from '../settings'; /** * Global virtual env dir for a project is named as: @@ -124,19 +125,19 @@ export class Poetry { * execution as soon as possible. To do that we need to ensure the operations before the command are * performed synchronously. */ - public static async getPoetry(cwd: string): Promise { + public static async getPoetry(cwd: string, settings?: PythonDiscoverySettings): Promise { // Following check should be performed synchronously so we trigger poetry execution as soon as possible. if (!hasValidPyprojectToml(cwd)) { // This check is not expensive and may change during a session, so we need not cache it. return undefined; } if (Poetry.poetryPromise.get(cwd) === undefined || isTestExecution()) { - Poetry.poetryPromise.set(cwd, Poetry.locate(cwd)); + Poetry.poetryPromise.set(cwd, Poetry.locate(cwd, settings)); } return Poetry.poetryPromise.get(cwd); } - private static async locate(cwd: string): Promise { + private static async locate(cwd: string, settings?: PythonDiscoverySettings): Promise { // First thing this method awaits on should be poetry command execution, hence perform all operations // before that synchronously. @@ -144,7 +145,7 @@ export class Poetry { // Produce a list of candidate binaries to be probed by exec'ing them. function* getCandidates() { try { - const customPoetryPath = getPythonSetting('poetryPath'); + const customPoetryPath = settings ? settings.poetryPath : getPythonSetting('poetryPath'); if (customPoetryPath && customPoetryPath !== 'poetry') { // If user has specified a custom poetry path, use it first. yield customPoetryPath; diff --git a/src/client/pythonEnvironments/common/eventEmitter.ts b/src/client/pythonEnvironments/common/eventEmitter.ts new file mode 100644 index 000000000000..8b5a82813e55 --- /dev/null +++ b/src/client/pythonEnvironments/common/eventEmitter.ts @@ -0,0 +1,107 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +export interface IDisposable { + dispose(): void; +} + +/** + * Adds a handler that handles one event on the emitter, then disposes itself. + */ +export const once = (event: Event, listener: (data: T) => void): IDisposable => { + const disposable = event((value) => { + listener(value); + disposable.dispose(); + }); + + return disposable; +}; + +export interface Event { + (listener: (e: T) => any, thisArgs?: any, disposables?: IDisposable[]): IDisposable; +} + +/** + * Base event emitter. Calls listeners when data is emitted. + */ +export class EventEmitter { + private listeners?: Array<(data: T) => void> | ((data: T) => void); + + /** + * Event function. + */ + public readonly event: Event = (listener, thisArgs, disposables) => { + const d = this.add(thisArgs ? listener.bind(thisArgs) : listener); + disposables?.push(d); + return d; + }; + + /** + * Gets the number of event listeners. + */ + public get size(): number { + if (!this.listeners) { + return 0; + } + if (typeof this.listeners === 'function') { + return 1; + } + return this.listeners.length; + } + + /** + * Emits event data. + */ + public fire(value: T): void { + if (!this.listeners) { + // no-op + } else if (typeof this.listeners === 'function') { + this.listeners(value); + } else { + for (const listener of this.listeners) { + listener(value); + } + } + } + + /** + * Disposes of the emitter. + */ + public dispose(): void { + this.listeners = undefined; + } + + private add(listener: (data: T) => void): IDisposable { + if (!this.listeners) { + this.listeners = listener; + } else if (typeof this.listeners === 'function') { + this.listeners = [this.listeners, listener]; + } else { + this.listeners.push(listener); + } + + return { dispose: () => this.rm(listener) }; + } + + private rm(listener: (data: T) => void) { + if (!this.listeners) { + return; + } + + if (typeof this.listeners === 'function') { + if (this.listeners === listener) { + this.listeners = undefined; + } + return; + } + + const index = this.listeners.indexOf(listener); + if (index === -1) { + return; + } + + if (this.listeners.length === 2) { + this.listeners = index === 0 ? this.listeners[1] : this.listeners[0]; + } else { + this.listeners = this.listeners.slice(0, index).concat(this.listeners.slice(index + 1)); + } + } +} diff --git a/src/client/pythonEnvironments/common/externalDependencies.ts b/src/client/pythonEnvironments/common/externalDependencies.ts index ecb6f2212aba..6df2bb196a9c 100644 --- a/src/client/pythonEnvironments/common/externalDependencies.ts +++ b/src/client/pythonEnvironments/common/externalDependencies.ts @@ -3,7 +3,6 @@ import * as fsapi from 'fs-extra'; import * as path from 'path'; -import * as vscode from 'vscode'; import { IWorkspaceService } from '../../common/application/types'; import { ExecutionResult, IProcessServiceFactory, ShellOptions, SpawnOptions } from '../../common/process/types'; import { IDisposable, IConfigurationService } from '../../common/types'; @@ -187,9 +186,18 @@ export function getPythonSetting(name: string): T | undefined { * @param callback The listener function to be called when the setting changes. */ export function onDidChangePythonSetting(name: string, callback: () => void): IDisposable { - return vscode.workspace.onDidChangeConfiguration((event: vscode.ConfigurationChangeEvent) => { - if (event.affectsConfiguration(`python.${name}`)) { - callback(); - } - }); + try { + // eslint-disable-next-line global-require + const vscode = require('vscode'); + return vscode.workspace.onDidChangeConfiguration((event: ConfigurationChangeEvent) => { + if (event.affectsConfiguration(`python.${name}`)) { + callback(); + } + }); + } catch (ex) { + // traceError('Setting', (ex as Error).message); + // console.log('Setting', (ex as Error).message); + // eslint-disable-next-line @typescript-eslint/no-empty-function + return { dispose: () => {} }; + } } diff --git a/src/client/pythonEnvironments/common/settings.ts b/src/client/pythonEnvironments/common/settings.ts new file mode 100644 index 000000000000..865511f4ce9d --- /dev/null +++ b/src/client/pythonEnvironments/common/settings.ts @@ -0,0 +1,31 @@ +import { workspace } from 'vscode'; + +export const VENVPATH_SETTING_KEY = 'venvPath'; +export const VENVFOLDERS_SETTING_KEY = 'venvFolders'; +export const CONDAPATH_SETTING_KEY = 'condaPath'; +export const POETRYSETTING_KEY = 'poetryPath'; +export const ACTIVESTATETOOLPATH_SETTING_KEY = 'activeStateToolPath'; + +export type PythonDiscoverySettings = { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [key: string]: any; +}; + +/** + * Returns the value for setting `python.`. + * @param name The name of the setting. + */ +export function getPythonDiscoverySettings(): PythonDiscoverySettings { + const settingNames = [ + VENVPATH_SETTING_KEY, + VENVFOLDERS_SETTING_KEY, + CONDAPATH_SETTING_KEY, + POETRYSETTING_KEY, + ACTIVESTATETOOLPATH_SETTING_KEY, + ]; + const settings: PythonDiscoverySettings = {}; + for (const name of settingNames) { + settings[name] = workspace.getConfiguration('python').get(name); + } + return settings; +} diff --git a/src/client/pythonEnvironments/index.ts b/src/client/pythonEnvironments/index.ts index 8065811a8a62..733b0f7d691b 100644 --- a/src/client/pythonEnvironments/index.ts +++ b/src/client/pythonEnvironments/index.ts @@ -5,38 +5,23 @@ import * as vscode from 'vscode'; import { Uri } from 'vscode'; import { cloneDeep } from 'lodash'; import { getGlobalStorage, IPersistentStorage } from '../common/persistentState'; -import { getOSType, OSType } from '../common/utils/platform'; import { ActivationResult, ExtensionState } from '../components'; import { PythonEnvInfo } from './base/info'; -import { BasicEnvInfo, IDiscoveryAPI, ILocator } from './base/locator'; -import { PythonEnvsReducer } from './base/locators/composite/envsReducer'; -import { PythonEnvsResolver } from './base/locators/composite/envsResolver'; -import { WindowsPathEnvVarLocator } from './base/locators/lowLevel/windowsKnownPathsLocator'; -import { WorkspaceVirtualEnvironmentLocator } from './base/locators/lowLevel/workspaceVirtualEnvLocator'; +import { IDiscoveryAPI } from './base/locator'; import { initializeExternalDependencies as initializeLegacyExternalDependencies, normCasePath, } from './common/externalDependencies'; -import { ExtensionLocators, WatchRootsArgs, WorkspaceLocators } from './base/locators/wrappers'; -import { CustomVirtualEnvironmentLocator } from './base/locators/lowLevel/customVirtualEnvLocator'; -import { CondaEnvironmentLocator } from './base/locators/lowLevel/condaLocator'; -import { GlobalVirtualEnvironmentLocator } from './base/locators/lowLevel/globalVirtualEnvronmentLocator'; -import { PosixKnownPathsLocator } from './base/locators/lowLevel/posixKnownPathsLocator'; -import { PyenvLocator } from './base/locators/lowLevel/pyenvLocator'; -import { WindowsRegistryLocator } from './base/locators/lowLevel/windowsRegistryLocator'; -import { MicrosoftStoreLocator } from './base/locators/lowLevel/microsoftStoreLocator'; -import { getEnvironmentInfoService } from './base/info/environmentInfoService'; import { registerNewDiscoveryForIOC } from './legacyIOC'; -import { PoetryLocator } from './base/locators/lowLevel/poetryLocator'; import { createPythonEnvironments } from './api'; import { createCollectionCache as createCache, IEnvsCollectionCache, } from './base/locators/composite/envsCollectionCache'; import { EnvsCollectionService } from './base/locators/composite/envsCollectionService'; -import { IDisposable } from '../common/types'; import { traceError } from '../logging'; -import { ActiveStateLocator } from './base/locators/lowLevel/activeStateLocator'; +import { WorkerThreadMiddleWare } from './base/locators/composite/workerMiddleware'; +import { getPythonDiscoverySettings } from './common/settings'; /** * Set up the Python environments component (during extension activation).' @@ -107,88 +92,17 @@ async function createLocator( ext: ExtensionState, // This is shared. ): Promise { - // Create the low-level locators. - const locators: ILocator = new ExtensionLocators( - // Here we pull the locators together. - createNonWorkspaceLocators(ext), - createWorkspaceLocator(ext), - ); - - // Create the env info service used by ResolvingLocator and CachingLocator. - const envInfoService = getEnvironmentInfoService(ext.disposables); - - // Build the stack of composite locators. - const reducer = new PythonEnvsReducer(locators); - const resolvingLocator = new PythonEnvsResolver( - reducer, - // These are shared. - envInfoService, - ); - const caching = new EnvsCollectionService( - await createCollectionCache(ext), - // This is shared. - resolvingLocator, + const [workspaceFolders, settings] = [vscode.workspace.workspaceFolders, getPythonDiscoverySettings()]; + const middleware = new WorkerThreadMiddleWare(workspaceFolders, settings); + // const middleware = new EnvsMiddleWare(workspaceFolders, settings); + ext.disposables.push( + vscode.workspace.onDidChangeWorkspaceFolders((event) => middleware.onDidChangeWorkspaceFolders(event)), ); + ext.disposables.push(middleware); + const caching = new EnvsCollectionService(await createCollectionCache(ext), middleware); return caching; } -function createNonWorkspaceLocators(ext: ExtensionState): ILocator[] { - const locators: (ILocator & Partial)[] = []; - locators.push( - // OS-independent locators go here. - new PyenvLocator(), - new CondaEnvironmentLocator(), - new ActiveStateLocator(), - new GlobalVirtualEnvironmentLocator(), - new CustomVirtualEnvironmentLocator(), - ); - - if (getOSType() === OSType.Windows) { - locators.push( - // Windows specific locators go here. - new WindowsRegistryLocator(), - new MicrosoftStoreLocator(), - new WindowsPathEnvVarLocator(), - ); - } else { - locators.push( - // Linux/Mac locators go here. - new PosixKnownPathsLocator(), - ); - } - - const disposables = locators.filter((d) => d.dispose !== undefined) as IDisposable[]; - ext.disposables.push(...disposables); - return locators; -} - -function watchRoots(args: WatchRootsArgs): IDisposable { - const { initRoot, addRoot, removeRoot } = args; - - const folders = vscode.workspace.workspaceFolders; - if (folders) { - folders.map((f) => f.uri).forEach(initRoot); - } - - return vscode.workspace.onDidChangeWorkspaceFolders((event) => { - for (const root of event.removed) { - removeRoot(root.uri); - } - for (const root of event.added) { - addRoot(root.uri); - } - }); -} - -function createWorkspaceLocator(ext: ExtensionState): WorkspaceLocators { - const locators = new WorkspaceLocators(watchRoots, [ - (root: vscode.Uri) => [new WorkspaceVirtualEnvironmentLocator(root.fsPath), new PoetryLocator(root.fsPath)], - // Add an ILocator factory func here for each kind of workspace-rooted locator. - ]); - ext.disposables.push(locators); - return locators; -} - function getFromStorage(storage: IPersistentStorage): PythonEnvInfo[] { return storage.get().map((e) => { if (e.searchLocation) { diff --git a/src/client/pythonEnvironments/locator.ts b/src/client/pythonEnvironments/locator.ts new file mode 100644 index 000000000000..c8d22524d4db --- /dev/null +++ b/src/client/pythonEnvironments/locator.ts @@ -0,0 +1,106 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as vscode from 'vscode'; +import { getOSType, OSType } from '../common/utils/platform'; +import { BasicEnvInfo, ILocator, IResolvingLocator } from './base/locator'; +import { PythonEnvsReducer } from './base/locators/composite/envsReducer'; +import { PythonEnvsResolver } from './base/locators/composite/envsResolver'; +import { WindowsPathEnvVarLocator } from './base/locators/lowLevel/windowsKnownPathsLocator'; +import { WorkspaceVirtualEnvironmentLocator } from './base/locators/lowLevel/workspaceVirtualEnvLocator'; +import { ExtensionLocators, WorkspaceLocators } from './base/locators/wrappers'; +import { CustomVirtualEnvironmentLocator } from './base/locators/lowLevel/customVirtualEnvLocator'; +import { CondaEnvironmentLocator } from './base/locators/lowLevel/condaLocator'; +import { GlobalVirtualEnvironmentLocator } from './base/locators/lowLevel/globalVirtualEnvronmentLocator'; +import { PosixKnownPathsLocator } from './base/locators/lowLevel/posixKnownPathsLocator'; +import { PyenvLocator } from './base/locators/lowLevel/pyenvLocator'; +import { WindowsRegistryLocator } from './base/locators/lowLevel/windowsRegistryLocator'; +import { MicrosoftStoreLocator } from './base/locators/lowLevel/microsoftStoreLocator'; +import { getEnvironmentInfoService } from './base/info/environmentInfoService'; +import { PoetryLocator } from './base/locators/lowLevel/poetryLocator'; +import { IDisposable, IDisposableRegistry } from '../common/types'; +import { ActiveStateLocator } from './base/locators/lowLevel/activeStateLocator'; +import { PythonDiscoverySettings } from './common/settings'; + +/** + * Get the locator to use in the component. + */ +export function createSubLocators( + folders: readonly vscode.WorkspaceFolder[] | undefined, + settings: PythonDiscoverySettings, +): { + locator: IResolvingLocator; + disposables: IDisposableRegistry; + workspaceLocator: WorkspaceLocators; +} { + const disposables: IDisposableRegistry = []; + // Create the low-level locators. + const workspaceLocator = createWorkspaceLocator(folders, disposables, settings); + const locators: ILocator = new ExtensionLocators( + // Here we pull the locators together. + createNonWorkspaceLocators(disposables, settings), + workspaceLocator, + ); + + // Create the env info service used by ResolvingLocator and CachingLocator. + const envInfoService = getEnvironmentInfoService(disposables); + + // Build the stack of composite locators. + const reducer = new PythonEnvsReducer(locators); + const resolvingLocator = new PythonEnvsResolver( + reducer, + // These are shared. + envInfoService, + folders, + ); + return { locator: resolvingLocator, disposables, workspaceLocator }; +} + +function createNonWorkspaceLocators( + disposables: IDisposableRegistry, + settings: PythonDiscoverySettings, +): ILocator[] { + const locators: (ILocator & Partial)[] = []; + locators.push( + // OS-independent locators go here. + new PyenvLocator(), + new CondaEnvironmentLocator(settings), + new ActiveStateLocator(settings), + new GlobalVirtualEnvironmentLocator(), + new CustomVirtualEnvironmentLocator(settings), + ); + + if (getOSType() === OSType.Windows) { + locators.push( + // Windows specific locators go here. + new WindowsRegistryLocator(), + new MicrosoftStoreLocator(), + new WindowsPathEnvVarLocator(), + ); + } else { + locators.push( + // Linux/Mac locators go here. + new PosixKnownPathsLocator(), + ); + } + + const more = locators.filter((d) => d.dispose !== undefined) as IDisposable[]; + disposables.push(...more); + return locators; +} + +function createWorkspaceLocator( + folders: readonly vscode.WorkspaceFolder[] | undefined, + disposables: IDisposableRegistry, + settings: PythonDiscoverySettings, +): WorkspaceLocators { + const locators = new WorkspaceLocators(folders, [ + (root: vscode.Uri) => [ + new WorkspaceVirtualEnvironmentLocator(root.fsPath), + new PoetryLocator(root.fsPath, settings), + ], + // Add an ILocator factory func here for each kind of workspace-rooted locator. + ]); + disposables.push(locators); + return locators; +}