Skip to content

Commit 63f5c6a

Browse files
eleanorjboydCopilot
andcommitted
Refactor environment change events to make getEnvironment a pure read and add refreshEnvironment event
Co-authored-by: Copilot <copilot@github.com>
1 parent c5a1430 commit 63f5c6a

6 files changed

Lines changed: 430 additions & 81 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: 64 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

@@ -196,7 +215,7 @@ export class PythonEnvironmentManagers implements EnvironmentManagers {
196215
// Fall back to cached environment's manager if no user-configured settings
197216
const project = context ? this.pm.get(context) : undefined;
198217
const key = project ? project.uri.toString() : 'global';
199-
const cachedEnv = this._previousEnvironments.get(key);
218+
const cachedEnv = this._activeSelection.get(key);
200219
if (cachedEnv) {
201220
const cachedManager = this._environmentManagers.get(cachedEnv.envId.managerId);
202221
if (cachedManager) {
@@ -339,11 +358,11 @@ export class PythonEnvironmentManagers implements EnvironmentManagers {
339358
}
340359

341360
const key = project ? project.uri.toString() : 'global';
342-
const oldEnv = this._previousEnvironments.get(key);
361+
const oldEnv = this._activeSelection.get(key);
343362
if (oldEnv?.envId.id !== environment?.envId.id) {
344-
this._previousEnvironments.set(key, environment);
363+
this._activeSelection.set(key, environment);
345364
setImmediate(() =>
346-
this._onDidChangeEnvironmentFiltered.fire({ uri: project?.uri, new: environment, old: oldEnv }),
365+
this._onDidChangeActiveEnvironment.fire({ uri: project?.uri, new: environment, old: oldEnv }),
347366
);
348367
}
349368
}
@@ -393,9 +412,9 @@ export class PythonEnvironmentManagers implements EnvironmentManagers {
393412

394413
const project = this.pm.get(uri);
395414
const key = project ? project.uri.toString() : 'global';
396-
const oldEnv = this._previousEnvironments.get(key);
415+
const oldEnv = this._activeSelection.get(key);
397416
if (oldEnv?.envId.id !== environment?.envId.id) {
398-
this._previousEnvironments.set(key, environment);
417+
this._activeSelection.set(key, environment);
399418
events.push({ uri: project?.uri, new: environment, old: oldEnv });
400419
}
401420
});
@@ -411,9 +430,9 @@ export class PythonEnvironmentManagers implements EnvironmentManagers {
411430
});
412431
}
413432

414-
const oldEnv = this._previousEnvironments.get('global');
433+
const oldEnv = this._activeSelection.get('global');
415434
if (oldEnv?.envId.id !== environment?.envId.id) {
416-
this._previousEnvironments.set('global', environment);
435+
this._activeSelection.set('global', environment);
417436
events.push({ uri: undefined, new: environment, old: oldEnv });
418437
}
419438
}
@@ -422,7 +441,7 @@ export class PythonEnvironmentManagers implements EnvironmentManagers {
422441
if (shouldPersistSettings) {
423442
await setAllManagerSettings(settings);
424443
}
425-
setImmediate(() => events.forEach((e) => this._onDidChangeEnvironmentFiltered.fire(e)));
444+
setImmediate(() => events.forEach((e) => this._onDidChangeActiveEnvironment.fire(e)));
426445
} else {
427446
const promises: Promise<void>[] = [];
428447
const events: DidChangeEnvironmentEventArgs[] = [];
@@ -439,9 +458,9 @@ export class PythonEnvironmentManagers implements EnvironmentManagers {
439458
// events. But it ensures that we always get the latest environment at the time of this call.
440459
const newEnv = await manager.get(uri);
441460
const key = project ? project.uri.toString() : 'global';
442-
const oldEnv = this._previousEnvironments.get(key);
461+
const oldEnv = this._activeSelection.get(key);
443462
if (oldEnv?.envId.id !== newEnv?.envId.id) {
444-
this._previousEnvironments.set(key, newEnv);
463+
this._activeSelection.set(key, newEnv);
445464
events.push({ uri: project?.uri, new: newEnv, old: oldEnv });
446465
}
447466
};
@@ -457,17 +476,17 @@ export class PythonEnvironmentManagers implements EnvironmentManagers {
457476
// Always get the new first, then compare with the old. This has minor impact on the ordering of
458477
// events. But it ensures that we always get the latest environment at the time of this call.
459478
const newEnv = await manager.get(undefined);
460-
const oldEnv = this._previousEnvironments.get('global');
479+
const oldEnv = this._activeSelection.get('global');
461480
if (oldEnv?.envId.id !== newEnv?.envId.id) {
462-
this._previousEnvironments.set('global', newEnv);
481+
this._activeSelection.set('global', newEnv);
463482
events.push({ uri: undefined, new: newEnv, old: oldEnv });
464483
}
465484
};
466485
promises.push(setAndAddEvent());
467486
}
468487
}
469488
await Promise.all(promises);
470-
setImmediate(() => events.forEach((e) => this._onDidChangeEnvironmentFiltered.fire(e)));
489+
setImmediate(() => events.forEach((e) => this._onDidChangeActiveEnvironment.fire(e)));
471490
}
472491
}
473492

@@ -503,8 +522,9 @@ export class PythonEnvironmentManagers implements EnvironmentManagers {
503522
/**
504523
* Gets the current Python environment for the given scope URI or undefined for 'global'.
505524
*
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.
525+
* This is a pure read: it queries the environment manager but does NOT update the
526+
* internal selection cache or fire change events. Selection state is only mutated
527+
* through setEnvironment() / setEnvironments() / refreshEnvironment().
508528
*
509529
* @param scope The scope to get the environment.
510530
* @returns The current PythonEnvironment for the scope, or undefined if none is set.
@@ -519,21 +539,35 @@ export class PythonEnvironmentManagers implements EnvironmentManagers {
519539
return undefined;
520540
}
521541

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

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.
560+
const project = scope ? this.pm.get(scope) : undefined;
526561
const newEnv = await manager.get(scope);
527562

528563
const key = project ? project.uri.toString() : 'global';
529-
const oldEnv = this._previousEnvironments.get(key);
564+
const oldEnv = this._activeSelection.get(key);
530565
if (oldEnv?.envId.id !== newEnv?.envId.id) {
531-
this._previousEnvironments.set(key, newEnv);
566+
this._activeSelection.set(key, newEnv);
532567
setImmediate(() =>
533-
this._onDidChangeEnvironmentFiltered.fire({ uri: project?.uri, new: newEnv, old: oldEnv }),
568+
this._onDidChangeActiveEnvironment.fire({ uri: project?.uri, new: newEnv, old: oldEnv }),
534569
);
535570
}
536-
return newEnv;
537571
}
538572

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

src/features/pythonApi.ts

Lines changed: 35 additions & 37 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 { 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(
@@ -92,13 +92,11 @@ class PythonEnvironmentApiImpl implements PythonEnvironmentApi {
9292
disposables.push(
9393
manager.onDidChangeEnvironment((e) => {
9494
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);
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+
await this.envManagers.refreshEnvironment(e.uri);
102100
});
103101
}),
104102
);

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)