diff --git a/src/api.ts b/src/api.ts index ec63a156..cb17ec72 100644 --- a/src/api.ts +++ b/src/api.ts @@ -333,7 +333,7 @@ export interface QuickCreateConfig { */ export interface EnvironmentManager { /** - * The name of the environment manager. + * The name of the environment manager. Allowed characters (a-z, A-Z, 0-9, -, _). */ readonly name: string; @@ -564,7 +564,7 @@ export interface DidChangePackagesEventArgs { */ export interface PackageManager { /** - * The name of the package manager. + * The name of the package manager. Allowed characters (a-z, A-Z, 0-9, -, _). */ name: string; diff --git a/src/common/localize.ts b/src/common/localize.ts index ae509bc4..04313f24 100644 --- a/src/common/localize.ts +++ b/src/common/localize.ts @@ -17,6 +17,10 @@ export namespace Common { export const installPython = l10n.t('Install Python'); } +export namespace WorkbenchStrings { + export const installExtension = l10n.t('Install Extension'); +} + export namespace Interpreter { export const statusBarSelect = l10n.t('Select Interpreter'); export const browsePath = l10n.t('Browse...'); diff --git a/src/common/workbenchCommands.ts b/src/common/workbenchCommands.ts new file mode 100644 index 00000000..0efe870e --- /dev/null +++ b/src/common/workbenchCommands.ts @@ -0,0 +1,12 @@ +import { commands, Uri } from 'vscode'; + +export async function installExtension( + extensionId: Uri | string, + options?: { + installOnlyNewlyAddedFromExtensionPackVSIX?: boolean; + installPreReleaseVersion?: boolean; + donotSync?: boolean; + }, +): Promise { + await commands.executeCommand('workbench.extensions.installExtension', extensionId, options); +} diff --git a/src/extension.ts b/src/extension.ts index c2630f58..099bdcd7 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -16,6 +16,7 @@ import { onDidChangeActiveTextEditor, onDidChangeTerminalShellIntegration, } from './common/window.apis'; +import { createManagerReady } from './features/common/managerReady'; import { GetEnvironmentInfoTool, InstallPackageTool } from './features/copilotTools'; import { AutoFindProjects } from './features/creators/autoFindProjects'; import { ExistingProjects } from './features/creators/existingProjects'; @@ -88,6 +89,7 @@ export async function activate(context: ExtensionContext): Promise; + waitForEnvManagerId(managerIds: string[]): Promise; + waitForAllEnvManagers(): Promise; + waitForPkgManager(uris?: Uri[]): Promise; + waitForPkgManagerId(managerIds: string[]): Promise; +} + +function getExtensionId(managerId: string): string | undefined { + // format : + const regex = /^(.*):([a-zA-Z0-9-_]*)$/; + const parts = regex.exec(managerId); + return parts ? parts[1] : undefined; +} + +class ManagerReadyImpl implements ManagerReady { + private readonly envManagers: Map> = new Map(); + private readonly pkgManagers: Map> = new Map(); + private readonly checked: Set = new Set(); + private readonly disposables: Disposable[] = []; + + constructor(em: EnvironmentManagers, private readonly pm: PythonProjectManager) { + this.disposables.push( + em.onDidChangeEnvironmentManager((e) => { + if (this.envManagers.has(e.manager.id)) { + this.envManagers.get(e.manager.id)?.resolve(); + } else { + const deferred = createDeferred(); + this.envManagers.set(e.manager.id, deferred); + deferred.resolve(); + } + }), + em.onDidChangePackageManager((e) => { + if (this.pkgManagers.has(e.manager.id)) { + this.pkgManagers.get(e.manager.id)?.resolve(); + } else { + const deferred = createDeferred(); + this.pkgManagers.set(e.manager.id, deferred); + deferred.resolve(); + } + }), + ); + } + + private checkExtension(managerId: string) { + const installed = allExtensions().some((ext) => managerId.startsWith(`${ext.id}:`)); + if (this.checked.has(managerId)) { + return; + } + this.checked.add(managerId); + const extId = getExtensionId(managerId); + if (extId) { + setImmediate(async () => { + if (installed) { + const ext = getExtension(extId); + if (ext && !ext.isActive) { + traceInfo(`Extension for manager ${managerId} is not active: Activating...`); + try { + await ext.activate(); + traceInfo(`Extension for manager ${managerId} is now active.`); + } catch (err) { + traceError(`Failed to activate extension ${extId}, required for: ${managerId}`, err); + } + } + } else { + traceError(`Extension for manager ${managerId} is not installed.`); + const result = await showErrorMessage( + l10n.t(`Do you want to install extension {0} to enable {1} support.`, extId, managerId), + WorkbenchStrings.installExtension, + ); + if (result === WorkbenchStrings.installExtension) { + traceInfo(`Installing extension: ${extId}`); + try { + await installExtension(extId); + traceInfo(`Extension ${extId} installed.`); + } catch (err) { + traceError(`Failed to install extension: ${extId}`, err); + } + + try { + const ext = getExtension(extId); + if (ext && !ext.isActive) { + traceInfo(`Extension for manager ${managerId} is not active: Activating...`); + await ext.activate(); + } + } catch (err) { + traceError(`Failed to activate extension ${extId}, required for: ${managerId}`, err); + } + } + } + }); + } else { + showErrorMessage(l10n.t(`Extension for {0} is not installed or enabled for this workspace.`, managerId)); + } + } + + public dispose(): void { + this.disposables.forEach((d) => d.dispose()); + this.envManagers.clear(); + this.pkgManagers.clear(); + } + + private _waitForEnvManager(managerId: string): Promise { + if (this.envManagers.has(managerId)) { + return this.envManagers.get(managerId)!.promise; + } + const deferred = createDeferred(); + this.envManagers.set(managerId, deferred); + return deferred.promise; + } + + public async waitForEnvManager(uris?: Uri[]): Promise { + const ids: Set = new Set(); + if (uris) { + uris.forEach((uri) => { + const m = getDefaultEnvManagerSetting(this.pm, uri); + if (!ids.has(m)) { + ids.add(m); + } + }); + } else { + const m = getDefaultEnvManagerSetting(this.pm, undefined); + if (m) { + ids.add(m); + } + } + + await this.waitForEnvManagerId(Array.from(ids)); + } + + public async waitForEnvManagerId(managerIds: string[]): Promise { + managerIds.forEach((managerId) => this.checkExtension(managerId)); + await Promise.all(managerIds.map((managerId) => this._waitForEnvManager(managerId))); + } + + public async waitForAllEnvManagers(): Promise { + const ids: Set = new Set(); + this.pm.getProjects().forEach((project) => { + const m = getDefaultEnvManagerSetting(this.pm, project.uri); + if (m && !ids.has(m)) { + ids.add(m); + } + }); + + const m = getDefaultEnvManagerSetting(this.pm, undefined); + if (m) { + ids.add(m); + } + await this.waitForEnvManagerId(Array.from(ids)); + } + + private _waitForPkgManager(managerId: string): Promise { + if (this.pkgManagers.has(managerId)) { + return this.pkgManagers.get(managerId)!.promise; + } + const deferred = createDeferred(); + this.pkgManagers.set(managerId, deferred); + return deferred.promise; + } + + public async waitForPkgManager(uris?: Uri[]): Promise { + const ids: Set = new Set(); + + if (uris) { + uris.forEach((uri) => { + const m = getDefaultPkgManagerSetting(this.pm, uri); + if (!ids.has(m)) { + ids.add(m); + } + }); + } else { + const m = getDefaultPkgManagerSetting(this.pm, undefined); + if (m) { + ids.add(m); + } + } + + await this.waitForPkgManagerId(Array.from(ids)); + } + public async waitForPkgManagerId(managerIds: string[]): Promise { + managerIds.forEach((managerId) => this.checkExtension(managerId)); + await Promise.all(managerIds.map((managerId) => this._waitForPkgManager(managerId))); + } +} + +let _deferred = createDeferred(); +export function createManagerReady(em: EnvironmentManagers, pm: PythonProjectManager, disposables: Disposable[]) { + if (!_deferred.completed) { + const mr = new ManagerReadyImpl(em, pm); + disposables.push(mr); + _deferred.resolve(mr); + } +} + +export async function waitForEnvManager(uris?: Uri[]): Promise { + const mr = await _deferred.promise; + return mr.waitForEnvManager(uris); +} + +export async function waitForEnvManagerId(managerIds: string[]): Promise { + const mr = await _deferred.promise; + return mr.waitForEnvManagerId(managerIds); +} + +export async function waitForAllEnvManagers(): Promise { + const mr = await _deferred.promise; + return mr.waitForAllEnvManagers(); +} + +export async function waitForPkgManager(uris?: Uri[]): Promise { + const mr = await _deferred.promise; + return mr.waitForPkgManager(uris); +} + +export async function waitForPkgManagerId(managerIds: string[]): Promise { + const mr = await _deferred.promise; + return mr.waitForPkgManagerId(managerIds); +} diff --git a/src/features/envManagers.ts b/src/features/envManagers.ts index cf193a93..7908efca 100644 --- a/src/features/envManagers.ts +++ b/src/features/envManagers.ts @@ -10,7 +10,7 @@ import { PythonProject, SetEnvironmentScope, } from '../api'; -import { traceError } from '../common/logging'; +import { traceError, traceVerbose } from '../common/logging'; import { EditAllManagerSettings, getDefaultEnvManagerSetting, @@ -39,7 +39,11 @@ import { sendTelemetryEvent } from '../common/telemetry/sender'; import { EventNames } from '../common/telemetry/constants'; function generateId(name: string): string { - return `${getCallingExtension()}:${name}`; + const newName = name.toLowerCase().replace(/[^a-zA-Z0-9-_]/g, '_'); + if (name !== newName) { + traceVerbose(`Environment manager name "${name}" was normalized to "${newName}"`); + } + return `${getCallingExtension()}:${newName}`; } export class PythonEnvironmentManagers implements EnvironmentManagers { diff --git a/src/features/pythonApi.ts b/src/features/pythonApi.ts index e712a4cd..9f3f2955 100644 --- a/src/features/pythonApi.ts +++ b/src/features/pythonApi.ts @@ -48,6 +48,7 @@ import { runInTerminal } from './terminal/runInTerminal'; import { runInBackground } from './execution/runInBackground'; import { EnvVarManager } from './execution/envVariableManager'; import { checkUri } from '../common/utils/pathUtils'; +import { waitForAllEnvManagers, waitForEnvManager, waitForEnvManagerId } from './common/managerReady'; class PythonEnvironmentApiImpl implements PythonEnvironmentApi { private readonly _onDidChangeEnvironments = new EventEmitter(); @@ -123,6 +124,7 @@ class PythonEnvironmentApiImpl implements PythonEnvironmentApi { options: CreateEnvironmentOptions | undefined, ): Promise { if (scope === 'global' || (!Array.isArray(scope) && scope instanceof Uri)) { + await waitForEnvManager(scope === 'global' ? undefined : [scope]); const manager = this.envManagers.getEnvironmentManager(scope === 'global' ? undefined : scope); if (!manager) { throw new Error('No environment manager found'); @@ -134,6 +136,7 @@ class PythonEnvironmentApiImpl implements PythonEnvironmentApi { } else if (Array.isArray(scope) && scope.length === 1 && scope[0] instanceof Uri) { return this.createEnvironment(scope[0], options); } else if (Array.isArray(scope) && scope.length > 0 && scope.every((s) => s instanceof Uri)) { + await waitForEnvManager(scope); const managers: InternalEnvironmentManager[] = []; scope.forEach((s) => { const manager = this.envManagers.getEnvironmentManager(s); @@ -160,7 +163,8 @@ class PythonEnvironmentApiImpl implements PythonEnvironmentApi { return result; } } - removeEnvironment(environment: PythonEnvironment): Promise { + async removeEnvironment(environment: PythonEnvironment): Promise { + await waitForEnvManagerId([environment.envId.managerId]); const manager = this.envManagers.getEnvironmentManager(environment); if (!manager) { return Promise.reject(new Error('No environment manager found')); @@ -171,9 +175,12 @@ class PythonEnvironmentApiImpl implements PythonEnvironmentApi { const currentScope = checkUri(scope) as RefreshEnvironmentsScope; if (currentScope === undefined) { + await waitForAllEnvManagers(); await Promise.all(this.envManagers.managers.map((manager) => manager.refresh(currentScope))); return Promise.resolve(); } + + await waitForEnvManager([currentScope]); const manager = this.envManagers.getEnvironmentManager(currentScope); if (!manager) { return Promise.reject(new Error(`No environment manager found for: ${currentScope.fsPath}`)); @@ -183,10 +190,13 @@ class PythonEnvironmentApiImpl implements PythonEnvironmentApi { async getEnvironments(scope: GetEnvironmentsScope): Promise { const currentScope = checkUri(scope) as GetEnvironmentsScope; if (currentScope === 'all' || currentScope === 'global') { + await waitForAllEnvManagers(); const promises = this.envManagers.managers.map((manager) => manager.getEnvironments(currentScope)); const items = await Promise.all(promises); return items.flat(); } + + await waitForEnvManager([currentScope]); const manager = this.envManagers.getEnvironmentManager(currentScope); if (!manager) { return []; @@ -196,14 +206,21 @@ class PythonEnvironmentApiImpl implements PythonEnvironmentApi { return items; } onDidChangeEnvironments: Event = this._onDidChangeEnvironments.event; - setEnvironment(scope: SetEnvironmentScope, environment?: PythonEnvironment): Promise { - return this.envManagers.setEnvironment(checkUri(scope) as SetEnvironmentScope, environment); + async setEnvironment(scope: SetEnvironmentScope, environment?: PythonEnvironment): Promise { + const currentScope = checkUri(scope) as SetEnvironmentScope; + await waitForEnvManager( + currentScope ? (currentScope instanceof Uri ? [currentScope] : currentScope) : undefined, + ); + return this.envManagers.setEnvironment(currentScope, environment); } async getEnvironment(scope: GetEnvironmentScope): Promise { - return this.envManagers.getEnvironment(checkUri(scope) as GetEnvironmentScope); + const currentScope = checkUri(scope) as GetEnvironmentScope; + await waitForEnvManager(currentScope ? [currentScope] : undefined); + return this.envManagers.getEnvironment(currentScope); } onDidChangeEnvironment: Event = this._onDidChangeEnvironment.event; async resolveEnvironment(context: ResolveEnvironmentContext): Promise { + await waitForAllEnvManagers(); const projects = this.projectManager.getProjects(); const projectEnvManagers: InternalEnvironmentManager[] = []; projects.forEach((p) => { @@ -224,21 +241,24 @@ class PythonEnvironmentApiImpl implements PythonEnvironmentApi { } return new Disposable(() => disposables.forEach((d) => d.dispose())); } - managePackages(context: PythonEnvironment, options: PackageManagementOptions): Promise { + async managePackages(context: PythonEnvironment, options: PackageManagementOptions): Promise { + await waitForEnvManagerId([context.envId.managerId]); const manager = this.envManagers.getPackageManager(context); if (!manager) { return Promise.reject(new Error('No package manager found')); } return manager.manage(context, options); } - refreshPackages(context: PythonEnvironment): Promise { + async refreshPackages(context: PythonEnvironment): Promise { + await waitForEnvManagerId([context.envId.managerId]); const manager = this.envManagers.getPackageManager(context); if (!manager) { return Promise.reject(new Error('No package manager found')); } return manager.refresh(context); } - getPackages(context: PythonEnvironment): Promise { + async getPackages(context: PythonEnvironment): Promise { + await waitForEnvManagerId([context.envId.managerId]); const manager = this.envManagers.getPackageManager(context); if (!manager) { return Promise.resolve(undefined);