Skip to content

Commit e68bf72

Browse files
authored
fix: detect if extension is installed and activated for selected env/package manager (#314)
1 parent 04dac71 commit e68bf72

File tree

7 files changed

+281
-11
lines changed

7 files changed

+281
-11
lines changed

src/api.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -333,7 +333,7 @@ export interface QuickCreateConfig {
333333
*/
334334
export interface EnvironmentManager {
335335
/**
336-
* The name of the environment manager.
336+
* The name of the environment manager. Allowed characters (a-z, A-Z, 0-9, -, _).
337337
*/
338338
readonly name: string;
339339

@@ -564,7 +564,7 @@ export interface DidChangePackagesEventArgs {
564564
*/
565565
export interface PackageManager {
566566
/**
567-
* The name of the package manager.
567+
* The name of the package manager. Allowed characters (a-z, A-Z, 0-9, -, _).
568568
*/
569569
name: string;
570570

src/common/localize.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ export namespace Common {
1717
export const installPython = l10n.t('Install Python');
1818
}
1919

20+
export namespace WorkbenchStrings {
21+
export const installExtension = l10n.t('Install Extension');
22+
}
23+
2024
export namespace Interpreter {
2125
export const statusBarSelect = l10n.t('Select Interpreter');
2226
export const browsePath = l10n.t('Browse...');

src/common/workbenchCommands.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { commands, Uri } from 'vscode';
2+
3+
export async function installExtension(
4+
extensionId: Uri | string,
5+
options?: {
6+
installOnlyNewlyAddedFromExtensionPackVSIX?: boolean;
7+
installPreReleaseVersion?: boolean;
8+
donotSync?: boolean;
9+
},
10+
): Promise<void> {
11+
await commands.executeCommand('workbench.extensions.installExtension', extensionId, options);
12+
}

src/extension.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
onDidChangeActiveTextEditor,
1717
onDidChangeTerminalShellIntegration,
1818
} from './common/window.apis';
19+
import { createManagerReady } from './features/common/managerReady';
1920
import { GetEnvironmentInfoTool, InstallPackageTool } from './features/copilotTools';
2021
import { AutoFindProjects } from './features/creators/autoFindProjects';
2122
import { ExistingProjects } from './features/creators/existingProjects';
@@ -88,6 +89,7 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
8889
context.subscriptions.push(envVarManager);
8990

9091
const envManagers: EnvironmentManagers = new PythonEnvironmentManagers(projectManager);
92+
createManagerReady(envManagers, projectManager, context.subscriptions);
9193
context.subscriptions.push(envManagers);
9294

9395
const terminalActivation = new TerminalActivationImpl();
Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
import { Disposable, l10n, Uri } from 'vscode';
2+
import { EnvironmentManagers, PythonProjectManager } from '../../internal.api';
3+
import { createDeferred, Deferred } from '../../common/utils/deferred';
4+
import { allExtensions, getExtension } from '../../common/extension.apis';
5+
import { traceError, traceInfo } from '../../common/logging';
6+
import { showErrorMessage } from '../../common/window.apis';
7+
import { getDefaultEnvManagerSetting, getDefaultPkgManagerSetting } from '../settings/settingHelpers';
8+
import { WorkbenchStrings } from '../../common/localize';
9+
import { installExtension } from '../../common/workbenchCommands';
10+
11+
interface ManagerReady extends Disposable {
12+
waitForEnvManager(uris?: Uri[]): Promise<void>;
13+
waitForEnvManagerId(managerIds: string[]): Promise<void>;
14+
waitForAllEnvManagers(): Promise<void>;
15+
waitForPkgManager(uris?: Uri[]): Promise<void>;
16+
waitForPkgManagerId(managerIds: string[]): Promise<void>;
17+
}
18+
19+
function getExtensionId(managerId: string): string | undefined {
20+
// format <extension-id>:<manager-name>
21+
const regex = /^(.*):([a-zA-Z0-9-_]*)$/;
22+
const parts = regex.exec(managerId);
23+
return parts ? parts[1] : undefined;
24+
}
25+
26+
class ManagerReadyImpl implements ManagerReady {
27+
private readonly envManagers: Map<string, Deferred<void>> = new Map();
28+
private readonly pkgManagers: Map<string, Deferred<void>> = new Map();
29+
private readonly checked: Set<string> = new Set();
30+
private readonly disposables: Disposable[] = [];
31+
32+
constructor(em: EnvironmentManagers, private readonly pm: PythonProjectManager) {
33+
this.disposables.push(
34+
em.onDidChangeEnvironmentManager((e) => {
35+
if (this.envManagers.has(e.manager.id)) {
36+
this.envManagers.get(e.manager.id)?.resolve();
37+
} else {
38+
const deferred = createDeferred<void>();
39+
this.envManagers.set(e.manager.id, deferred);
40+
deferred.resolve();
41+
}
42+
}),
43+
em.onDidChangePackageManager((e) => {
44+
if (this.pkgManagers.has(e.manager.id)) {
45+
this.pkgManagers.get(e.manager.id)?.resolve();
46+
} else {
47+
const deferred = createDeferred<void>();
48+
this.pkgManagers.set(e.manager.id, deferred);
49+
deferred.resolve();
50+
}
51+
}),
52+
);
53+
}
54+
55+
private checkExtension(managerId: string) {
56+
const installed = allExtensions().some((ext) => managerId.startsWith(`${ext.id}:`));
57+
if (this.checked.has(managerId)) {
58+
return;
59+
}
60+
this.checked.add(managerId);
61+
const extId = getExtensionId(managerId);
62+
if (extId) {
63+
setImmediate(async () => {
64+
if (installed) {
65+
const ext = getExtension(extId);
66+
if (ext && !ext.isActive) {
67+
traceInfo(`Extension for manager ${managerId} is not active: Activating...`);
68+
try {
69+
await ext.activate();
70+
traceInfo(`Extension for manager ${managerId} is now active.`);
71+
} catch (err) {
72+
traceError(`Failed to activate extension ${extId}, required for: ${managerId}`, err);
73+
}
74+
}
75+
} else {
76+
traceError(`Extension for manager ${managerId} is not installed.`);
77+
const result = await showErrorMessage(
78+
l10n.t(`Do you want to install extension {0} to enable {1} support.`, extId, managerId),
79+
WorkbenchStrings.installExtension,
80+
);
81+
if (result === WorkbenchStrings.installExtension) {
82+
traceInfo(`Installing extension: ${extId}`);
83+
try {
84+
await installExtension(extId);
85+
traceInfo(`Extension ${extId} installed.`);
86+
} catch (err) {
87+
traceError(`Failed to install extension: ${extId}`, err);
88+
}
89+
90+
try {
91+
const ext = getExtension(extId);
92+
if (ext && !ext.isActive) {
93+
traceInfo(`Extension for manager ${managerId} is not active: Activating...`);
94+
await ext.activate();
95+
}
96+
} catch (err) {
97+
traceError(`Failed to activate extension ${extId}, required for: ${managerId}`, err);
98+
}
99+
}
100+
}
101+
});
102+
} else {
103+
showErrorMessage(l10n.t(`Extension for {0} is not installed or enabled for this workspace.`, managerId));
104+
}
105+
}
106+
107+
public dispose(): void {
108+
this.disposables.forEach((d) => d.dispose());
109+
this.envManagers.clear();
110+
this.pkgManagers.clear();
111+
}
112+
113+
private _waitForEnvManager(managerId: string): Promise<void> {
114+
if (this.envManagers.has(managerId)) {
115+
return this.envManagers.get(managerId)!.promise;
116+
}
117+
const deferred = createDeferred<void>();
118+
this.envManagers.set(managerId, deferred);
119+
return deferred.promise;
120+
}
121+
122+
public async waitForEnvManager(uris?: Uri[]): Promise<void> {
123+
const ids: Set<string> = new Set();
124+
if (uris) {
125+
uris.forEach((uri) => {
126+
const m = getDefaultEnvManagerSetting(this.pm, uri);
127+
if (!ids.has(m)) {
128+
ids.add(m);
129+
}
130+
});
131+
} else {
132+
const m = getDefaultEnvManagerSetting(this.pm, undefined);
133+
if (m) {
134+
ids.add(m);
135+
}
136+
}
137+
138+
await this.waitForEnvManagerId(Array.from(ids));
139+
}
140+
141+
public async waitForEnvManagerId(managerIds: string[]): Promise<void> {
142+
managerIds.forEach((managerId) => this.checkExtension(managerId));
143+
await Promise.all(managerIds.map((managerId) => this._waitForEnvManager(managerId)));
144+
}
145+
146+
public async waitForAllEnvManagers(): Promise<void> {
147+
const ids: Set<string> = new Set();
148+
this.pm.getProjects().forEach((project) => {
149+
const m = getDefaultEnvManagerSetting(this.pm, project.uri);
150+
if (m && !ids.has(m)) {
151+
ids.add(m);
152+
}
153+
});
154+
155+
const m = getDefaultEnvManagerSetting(this.pm, undefined);
156+
if (m) {
157+
ids.add(m);
158+
}
159+
await this.waitForEnvManagerId(Array.from(ids));
160+
}
161+
162+
private _waitForPkgManager(managerId: string): Promise<void> {
163+
if (this.pkgManagers.has(managerId)) {
164+
return this.pkgManagers.get(managerId)!.promise;
165+
}
166+
const deferred = createDeferred<void>();
167+
this.pkgManagers.set(managerId, deferred);
168+
return deferred.promise;
169+
}
170+
171+
public async waitForPkgManager(uris?: Uri[]): Promise<void> {
172+
const ids: Set<string> = new Set();
173+
174+
if (uris) {
175+
uris.forEach((uri) => {
176+
const m = getDefaultPkgManagerSetting(this.pm, uri);
177+
if (!ids.has(m)) {
178+
ids.add(m);
179+
}
180+
});
181+
} else {
182+
const m = getDefaultPkgManagerSetting(this.pm, undefined);
183+
if (m) {
184+
ids.add(m);
185+
}
186+
}
187+
188+
await this.waitForPkgManagerId(Array.from(ids));
189+
}
190+
public async waitForPkgManagerId(managerIds: string[]): Promise<void> {
191+
managerIds.forEach((managerId) => this.checkExtension(managerId));
192+
await Promise.all(managerIds.map((managerId) => this._waitForPkgManager(managerId)));
193+
}
194+
}
195+
196+
let _deferred = createDeferred<ManagerReady>();
197+
export function createManagerReady(em: EnvironmentManagers, pm: PythonProjectManager, disposables: Disposable[]) {
198+
if (!_deferred.completed) {
199+
const mr = new ManagerReadyImpl(em, pm);
200+
disposables.push(mr);
201+
_deferred.resolve(mr);
202+
}
203+
}
204+
205+
export async function waitForEnvManager(uris?: Uri[]): Promise<void> {
206+
const mr = await _deferred.promise;
207+
return mr.waitForEnvManager(uris);
208+
}
209+
210+
export async function waitForEnvManagerId(managerIds: string[]): Promise<void> {
211+
const mr = await _deferred.promise;
212+
return mr.waitForEnvManagerId(managerIds);
213+
}
214+
215+
export async function waitForAllEnvManagers(): Promise<void> {
216+
const mr = await _deferred.promise;
217+
return mr.waitForAllEnvManagers();
218+
}
219+
220+
export async function waitForPkgManager(uris?: Uri[]): Promise<void> {
221+
const mr = await _deferred.promise;
222+
return mr.waitForPkgManager(uris);
223+
}
224+
225+
export async function waitForPkgManagerId(managerIds: string[]): Promise<void> {
226+
const mr = await _deferred.promise;
227+
return mr.waitForPkgManagerId(managerIds);
228+
}

src/features/envManagers.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
PythonProject,
1111
SetEnvironmentScope,
1212
} from '../api';
13-
import { traceError } from '../common/logging';
13+
import { traceError, traceVerbose } from '../common/logging';
1414
import {
1515
EditAllManagerSettings,
1616
getDefaultEnvManagerSetting,
@@ -39,7 +39,11 @@ import { sendTelemetryEvent } from '../common/telemetry/sender';
3939
import { EventNames } from '../common/telemetry/constants';
4040

4141
function generateId(name: string): string {
42-
return `${getCallingExtension()}:${name}`;
42+
const newName = name.toLowerCase().replace(/[^a-zA-Z0-9-_]/g, '_');
43+
if (name !== newName) {
44+
traceVerbose(`Environment manager name "${name}" was normalized to "${newName}"`);
45+
}
46+
return `${getCallingExtension()}:${newName}`;
4347
}
4448

4549
export class PythonEnvironmentManagers implements EnvironmentManagers {

0 commit comments

Comments
 (0)