Skip to content

Commit fcfdedf

Browse files
eleanorjboydCopilotCopilot
authored
Refactor environment change events to make getEnvironment a pure read and add refreshEnvironment event (#1495)
fixes #1492 --------- Co-authored-by: Copilot <copilot@github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent da846f3 commit fcfdedf

6 files changed

Lines changed: 456 additions & 82 deletions

File tree

src/extension.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -508,13 +508,13 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
508508
window.onDidChangeActiveTextEditor(async () => {
509509
updateViewsAndStatus(statusBar, workspaceView, managerView, api);
510510
}),
511-
envManagers.onDidChangeEnvironment(async () => {
511+
envManagers.onDidChangeManagerEnvironment(async () => {
512512
updateViewsAndStatus(statusBar, workspaceView, managerView, api);
513513
}),
514514
envManagers.onDidChangeEnvironments(async () => {
515515
updateViewsAndStatus(statusBar, workspaceView, managerView, api);
516516
}),
517-
envManagers.onDidChangeEnvironmentFiltered(async (e) => {
517+
envManagers.onDidChangeActiveEnvironment(async (e) => {
518518
managerView.environmentChanged(e);
519519
const location = e.uri?.fsPath ?? 'global';
520520
traceInfo(

src/features/envManagers.ts

Lines changed: 66 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -51,23 +51,42 @@ function generateId(name: string, extensionId?: string): string {
5151
export class PythonEnvironmentManagers implements EnvironmentManagers {
5252
private _environmentManagers: Map<string, InternalEnvironmentManager> = new Map();
5353
private _packageManagers: Map<string, InternalPackageManager> = new Map();
54-
private readonly _previousEnvironments = new Map<string, PythonEnvironment | undefined>();
54+
55+
/**
56+
* The last environment announced as "active" for each scope.
57+
* Keyed by project URI string or 'global'.
58+
*
59+
* Used for: (1) change detection before firing onDidChangeActiveEnvironment,
60+
* (2) fallback manager routing when no explicit setting exists.
61+
*
62+
* Only mutated by setEnvironment() / setEnvironments() / refreshEnvironment().
63+
*/
64+
private readonly _activeSelection = new Map<string, PythonEnvironment | undefined>();
5565

5666
private _onDidChangeEnvironmentManager = new EventEmitter<DidChangeEnvironmentManagerEventArgs>();
5767
private _onDidChangePackageManager = new EventEmitter<DidChangePackageManagerEventArgs>();
5868
private _onDidChangeEnvironments = new EventEmitter<InternalDidChangeEnvironmentsEventArgs>();
59-
private _onDidChangeEnvironment = new EventEmitter<DidChangeEnvironmentEventArgs>();
60-
private _onDidChangeEnvironmentFiltered = new EventEmitter<DidChangeEnvironmentEventArgs>();
69+
70+
/** Fires when ANY manager reports a selection change, regardless of whether that manager is selected. */
71+
private _onDidChangeManagerEnvironment = new EventEmitter<DidChangeEnvironmentEventArgs>();
72+
73+
/** Fires when the active (selected) environment for a scope actually changes. */
74+
private _onDidChangeActiveEnvironment = new EventEmitter<DidChangeEnvironmentEventArgs>();
6175
private _onDidChangePackages = new EventEmitter<InternalDidChangePackagesEventArgs>();
6276

6377
public onDidChangeEnvironmentManager: Event<DidChangeEnvironmentManagerEventArgs> =
6478
this._onDidChangeEnvironmentManager.event;
6579
public onDidChangePackageManager: Event<DidChangePackageManagerEventArgs> = this._onDidChangePackageManager.event;
6680
public onDidChangeEnvironments: Event<InternalDidChangeEnvironmentsEventArgs> = this._onDidChangeEnvironments.event;
67-
public onDidChangeEnvironment: Event<DidChangeEnvironmentEventArgs> = this._onDidChangeEnvironment.event;
81+
82+
/** Fires when any registered manager reports a change — even if that manager is not the selected one. */
83+
public onDidChangeManagerEnvironment: Event<DidChangeEnvironmentEventArgs> =
84+
this._onDidChangeManagerEnvironment.event;
6885
public onDidChangePackages: Event<InternalDidChangePackagesEventArgs> = this._onDidChangePackages.event;
69-
public onDidChangeEnvironmentFiltered: Event<DidChangeEnvironmentEventArgs> =
70-
this._onDidChangeEnvironmentFiltered.event;
86+
87+
/** Fires only when the *selected* manager's environment for a scope actually changes. */
88+
public onDidChangeActiveEnvironment: Event<DidChangeEnvironmentEventArgs> =
89+
this._onDidChangeActiveEnvironment.event;
7190

7291
constructor(private readonly pm: PythonProjectManager) {}
7392

@@ -99,7 +118,7 @@ export class PythonEnvironmentManagers implements EnvironmentManagers {
99118
return;
100119
}
101120

102-
setImmediate(() => this._onDidChangeEnvironment.fire(e));
121+
setImmediate(() => this._onDidChangeManagerEnvironment.fire(e));
103122
}),
104123
);
105124

@@ -165,6 +184,8 @@ export class PythonEnvironmentManagers implements EnvironmentManagers {
165184
this._onDidChangeEnvironmentManager.dispose();
166185
this._onDidChangePackageManager.dispose();
167186
this._onDidChangeEnvironments.dispose();
187+
this._onDidChangeManagerEnvironment.dispose();
188+
this._onDidChangeActiveEnvironment.dispose();
168189
this._onDidChangePackages.dispose();
169190
}
170191

@@ -196,7 +217,7 @@ export class PythonEnvironmentManagers implements EnvironmentManagers {
196217
// Fall back to cached environment's manager if no user-configured settings
197218
const project = context ? this.pm.get(context) : undefined;
198219
const key = project ? project.uri.toString() : 'global';
199-
const cachedEnv = this._previousEnvironments.get(key);
220+
const cachedEnv = this._activeSelection.get(key);
200221
if (cachedEnv) {
201222
const cachedManager = this._environmentManagers.get(cachedEnv.envId.managerId);
202223
if (cachedManager) {
@@ -339,11 +360,11 @@ export class PythonEnvironmentManagers implements EnvironmentManagers {
339360
}
340361

341362
const key = project ? project.uri.toString() : 'global';
342-
const oldEnv = this._previousEnvironments.get(key);
363+
const oldEnv = this._activeSelection.get(key);
343364
if (oldEnv?.envId.id !== environment?.envId.id) {
344-
this._previousEnvironments.set(key, environment);
365+
this._activeSelection.set(key, environment);
345366
setImmediate(() =>
346-
this._onDidChangeEnvironmentFiltered.fire({ uri: project?.uri, new: environment, old: oldEnv }),
367+
this._onDidChangeActiveEnvironment.fire({ uri: project?.uri, new: environment, old: oldEnv }),
347368
);
348369
}
349370
}
@@ -393,9 +414,9 @@ export class PythonEnvironmentManagers implements EnvironmentManagers {
393414

394415
const project = this.pm.get(uri);
395416
const key = project ? project.uri.toString() : 'global';
396-
const oldEnv = this._previousEnvironments.get(key);
417+
const oldEnv = this._activeSelection.get(key);
397418
if (oldEnv?.envId.id !== environment?.envId.id) {
398-
this._previousEnvironments.set(key, environment);
419+
this._activeSelection.set(key, environment);
399420
events.push({ uri: project?.uri, new: environment, old: oldEnv });
400421
}
401422
});
@@ -411,9 +432,9 @@ export class PythonEnvironmentManagers implements EnvironmentManagers {
411432
});
412433
}
413434

414-
const oldEnv = this._previousEnvironments.get('global');
435+
const oldEnv = this._activeSelection.get('global');
415436
if (oldEnv?.envId.id !== environment?.envId.id) {
416-
this._previousEnvironments.set('global', environment);
437+
this._activeSelection.set('global', environment);
417438
events.push({ uri: undefined, new: environment, old: oldEnv });
418439
}
419440
}
@@ -422,7 +443,7 @@ export class PythonEnvironmentManagers implements EnvironmentManagers {
422443
if (shouldPersistSettings) {
423444
await setAllManagerSettings(settings);
424445
}
425-
setImmediate(() => events.forEach((e) => this._onDidChangeEnvironmentFiltered.fire(e)));
446+
setImmediate(() => events.forEach((e) => this._onDidChangeActiveEnvironment.fire(e)));
426447
} else {
427448
const promises: Promise<void>[] = [];
428449
const events: DidChangeEnvironmentEventArgs[] = [];
@@ -439,9 +460,9 @@ export class PythonEnvironmentManagers implements EnvironmentManagers {
439460
// events. But it ensures that we always get the latest environment at the time of this call.
440461
const newEnv = await manager.get(uri);
441462
const key = project ? project.uri.toString() : 'global';
442-
const oldEnv = this._previousEnvironments.get(key);
463+
const oldEnv = this._activeSelection.get(key);
443464
if (oldEnv?.envId.id !== newEnv?.envId.id) {
444-
this._previousEnvironments.set(key, newEnv);
465+
this._activeSelection.set(key, newEnv);
445466
events.push({ uri: project?.uri, new: newEnv, old: oldEnv });
446467
}
447468
};
@@ -457,17 +478,17 @@ export class PythonEnvironmentManagers implements EnvironmentManagers {
457478
// Always get the new first, then compare with the old. This has minor impact on the ordering of
458479
// events. But it ensures that we always get the latest environment at the time of this call.
459480
const newEnv = await manager.get(undefined);
460-
const oldEnv = this._previousEnvironments.get('global');
481+
const oldEnv = this._activeSelection.get('global');
461482
if (oldEnv?.envId.id !== newEnv?.envId.id) {
462-
this._previousEnvironments.set('global', newEnv);
483+
this._activeSelection.set('global', newEnv);
463484
events.push({ uri: undefined, new: newEnv, old: oldEnv });
464485
}
465486
};
466487
promises.push(setAndAddEvent());
467488
}
468489
}
469490
await Promise.all(promises);
470-
setImmediate(() => events.forEach((e) => this._onDidChangeEnvironmentFiltered.fire(e)));
491+
setImmediate(() => events.forEach((e) => this._onDidChangeActiveEnvironment.fire(e)));
471492
}
472493
}
473494

@@ -503,8 +524,9 @@ export class PythonEnvironmentManagers implements EnvironmentManagers {
503524
/**
504525
* Gets the current Python environment for the given scope URI or undefined for 'global'.
505526
*
506-
* This method queries the appropriate environment manager for the latest environment for the scope.
507-
* It also updates the internal cache and fires an event if the environment has changed since last check.
527+
* This is a pure read: it queries the environment manager but does NOT update the
528+
* internal selection cache or fire change events. Selection state is only mutated
529+
* through setEnvironment() / setEnvironments() / refreshEnvironment().
508530
*
509531
* @param scope The scope to get the environment.
510532
* @returns The current PythonEnvironment for the scope, or undefined if none is set.
@@ -519,21 +541,35 @@ export class PythonEnvironmentManagers implements EnvironmentManagers {
519541
return undefined;
520542
}
521543

522-
const project = scope ? this.pm.get(scope) : undefined;
544+
return manager.get(scope);
545+
}
546+
547+
/**
548+
* Refreshes the cached environment for the given scope by querying the selected manager.
549+
* If the manager's current environment differs from the cache, updates the cache and
550+
* fires onDidChangeActiveEnvironment. Use this when a manager signals that its
551+
* selection has changed (e.g., via onDidChangeManagerEnvironment).
552+
*
553+
* Unlike getEnvironment(), this IS a mutation — it updates internal state.
554+
* Unlike setEnvironment(), it does NOT call manager.set() or persist to settings.
555+
*/
556+
async refreshEnvironment(scope: GetEnvironmentScope): Promise<void> {
557+
const manager = this.getEnvironmentManager(scope);
558+
if (!manager) {
559+
return;
560+
}
523561

524-
// Always get the new first, then compare with the old. This has minor impact on the ordering of
525-
// events. But it ensures that we always get the latest environment at the time of this call.
562+
const project = scope ? this.pm.get(scope) : undefined;
526563
const newEnv = await manager.get(scope);
527564

528565
const key = project ? project.uri.toString() : 'global';
529-
const oldEnv = this._previousEnvironments.get(key);
566+
const oldEnv = this._activeSelection.get(key);
530567
if (oldEnv?.envId.id !== newEnv?.envId.id) {
531-
this._previousEnvironments.set(key, newEnv);
568+
this._activeSelection.set(key, newEnv);
532569
setImmediate(() =>
533-
this._onDidChangeEnvironmentFiltered.fire({ uri: project?.uri, new: newEnv, old: oldEnv }),
570+
this._onDidChangeActiveEnvironment.fire({ uri: project?.uri, new: newEnv, old: oldEnv }),
534571
);
535572
}
536-
return newEnv;
537573
}
538574

539575
getProjectEnvManagers(uris: Uri[]): InternalEnvironmentManager[] {

src/features/pythonApi.ts

Lines changed: 38 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,40 @@
1-
import { Uri, Disposable, Event, EventEmitter, Terminal, TaskExecution } from 'vscode';
1+
import { Disposable, Event, EventEmitter, TaskExecution, Terminal, Uri } from 'vscode';
22
import {
3-
PythonEnvironmentApi,
4-
PythonEnvironment,
5-
EnvironmentManager,
6-
PackageManager,
3+
CreateEnvironmentOptions,
4+
CreateEnvironmentScope,
75
DidChangeEnvironmentEventArgs,
86
DidChangeEnvironmentsEventArgs,
7+
DidChangeEnvironmentVariablesEventArgs,
8+
DidChangePackagesEventArgs,
99
DidChangePythonProjectsEventArgs,
10+
EnvironmentManager,
11+
GetEnvironmentScope,
1012
GetEnvironmentsScope,
1113
Package,
12-
PythonEnvironmentInfo,
13-
PythonProject,
14-
RefreshEnvironmentsScope,
15-
DidChangePackagesEventArgs,
16-
PythonEnvironmentId,
17-
CreateEnvironmentScope,
18-
SetEnvironmentScope,
19-
GetEnvironmentScope,
20-
PackageInfo,
2114
PackageId,
22-
PythonProjectCreator,
23-
ResolveEnvironmentContext,
15+
PackageInfo,
2416
PackageManagementOptions,
17+
PackageManager,
18+
PythonBackgroundRunOptions,
19+
PythonEnvironment,
20+
PythonEnvironmentApi,
21+
PythonEnvironmentId,
22+
PythonEnvironmentInfo,
2523
PythonProcess,
24+
PythonProject,
25+
PythonProjectCreator,
2626
PythonTaskExecutionOptions,
27-
PythonTerminalExecutionOptions,
28-
PythonBackgroundRunOptions,
2927
PythonTerminalCreateOptions,
30-
DidChangeEnvironmentVariablesEventArgs,
31-
CreateEnvironmentOptions,
28+
PythonTerminalExecutionOptions,
29+
RefreshEnvironmentsScope,
30+
ResolveEnvironmentContext,
31+
SetEnvironmentScope,
3232
} from '../api';
33+
import { traceError, traceInfo } from '../common/logging';
34+
import { pickEnvironmentManager } from '../common/pickers/managers';
35+
import { createDeferred } from '../common/utils/deferred';
36+
import { checkUri } from '../common/utils/pathUtils';
37+
import { handlePythonPath } from '../common/utils/pythonPath';
3338
import {
3439
EnvironmentManagers,
3540
InternalEnvironmentManager,
@@ -38,17 +43,12 @@ import {
3843
PythonPackageImpl,
3944
PythonProjectManager,
4045
} from '../internal.api';
41-
import { createDeferred } from '../common/utils/deferred';
42-
import { traceInfo } from '../common/logging';
43-
import { pickEnvironmentManager } from '../common/pickers/managers';
44-
import { handlePythonPath } from '../common/utils/pythonPath';
45-
import { TerminalManager } from './terminal/terminalManager';
46+
import { waitForAllEnvManagers, waitForEnvManager, waitForEnvManagerId } from './common/managerReady';
47+
import { EnvVarManager } from './execution/envVariableManager';
4648
import { runAsTask } from './execution/runAsTask';
47-
import { runInTerminal } from './terminal/runInTerminal';
4849
import { runInBackground } from './execution/runInBackground';
49-
import { EnvVarManager } from './execution/envVariableManager';
50-
import { checkUri } from '../common/utils/pathUtils';
51-
import { waitForAllEnvManagers, waitForEnvManager, waitForEnvManagerId } from './common/managerReady';
50+
import { runInTerminal } from './terminal/runInTerminal';
51+
import { TerminalManager } from './terminal/terminalManager';
5252

5353
class PythonEnvironmentApiImpl implements PythonEnvironmentApi {
5454
private readonly _onDidChangeEnvironments = new EventEmitter<DidChangeEnvironmentsEventArgs>();
@@ -71,7 +71,7 @@ class PythonEnvironmentApiImpl implements PythonEnvironmentApi {
7171
this._onDidChangePythonProjects,
7272
this._onDidChangePackages,
7373
this._onDidChangeEnvironmentVariables,
74-
this.envManagers.onDidChangeEnvironmentFiltered((e) => {
74+
this.envManagers.onDidChangeActiveEnvironment((e) => {
7575
this._onDidChangeEnvironment.fire(e);
7676
const location = e.uri?.fsPath ?? 'global';
7777
traceInfo(
@@ -91,14 +91,14 @@ class PythonEnvironmentApiImpl implements PythonEnvironmentApi {
9191
if (manager.onDidChangeEnvironment) {
9292
disposables.push(
9393
manager.onDidChangeEnvironment((e) => {
94-
setImmediate(async () => {
95-
// This will ensure that we use the right manager and only trigger the event
96-
// if the user selected manager decided to change the environment.
97-
// This ensures that if a unselected manager changes environment and raises events
98-
// we don't trigger the Python API event which can cause issues with the consumers.
99-
// This will trigger onDidChangeEnvironmentFiltered event in envManagers, which the Python
100-
// API listens to, and re-triggers the onDidChangeEnvironment event.
101-
await this.envManagers.getEnvironment(e.uri);
94+
setImmediate(() => {
95+
// Refresh the central cache for this scope. This ensures that only the
96+
// *selected* manager's changes propagate (refreshEnvironment checks
97+
// getEnvironmentManager(scope) internally). It updates the cache and
98+
// fires onDidChangeActiveEnvironment, which the Python API listens to.
99+
this.envManagers.refreshEnvironment(e.uri).catch((err) =>
100+
traceError('Failed to refresh environment on change:', err),
101+
);
102102
});
103103
}),
104104
);

src/features/views/projectView.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ export class ProjectView implements TreeDataProvider<ProjectTreeItem> {
5959
this.projectManager.onDidChangeProjects(() => {
6060
this.debouncedUpdateProject.trigger();
6161
}),
62-
this.envManagers.onDidChangeEnvironment(() => {
62+
this.envManagers.onDidChangeManagerEnvironment(() => {
6363
this.debouncedUpdateProject.trigger();
6464
}),
6565
this.envManagers.onDidChangeEnvironments(() => {

0 commit comments

Comments
 (0)