Skip to content

Commit da4a6a8

Browse files
Copiloteleanorjboyd
andcommitted
Implement configuration change listeners and smart notifications for env settings
Co-authored-by: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com>
1 parent 969135c commit da4a6a8

File tree

4 files changed

+163
-51
lines changed

4 files changed

+163
-51
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@
106106
},
107107
"python.terminal.useEnvFile": {
108108
"type": "boolean",
109-
"description": "Controls whether environment variables from .env files and python.envFile setting are injected into terminals",
109+
"description": "%python-envs.terminal.useEnvFile.description%",
110110
"default": false,
111111
"scope": "resource"
112112
}

package.nls.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,5 +36,6 @@
3636
"python-envs.terminal.deactivate.title": "Deactivate Environment in Current Terminal",
3737
"python-envs.uninstallPackage.title": "Uninstall Package",
3838
"python-envs.revealProjectInExplorer.title": "Reveal Project in Explorer",
39-
"python-envs.runPetInTerminal.title": "Run Python Environment Tool (PET) in Terminal"
39+
"python-envs.runPetInTerminal.title": "Run Python Environment Tool (PET) in Terminal",
40+
"python-envs.terminal.useEnvFile.description": "Controls whether environment variables from .env files and python.envFile setting are injected into terminals"
4041
}

src/features/terminal/terminalEnvVarInjector.ts

Lines changed: 150 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import * as fse from 'fs-extra';
55
import * as path from 'path';
66
import {
7+
ConfigurationChangeEvent,
78
Disposable,
89
EnvironmentVariableScope,
910
GlobalEnvironmentVariableCollection,
@@ -12,7 +13,7 @@ import {
1213
} from 'vscode';
1314
import { traceError, traceVerbose } from '../../common/logging';
1415
import { resolveVariables } from '../../common/utils/internalVariables';
15-
import { getConfiguration, getWorkspaceFolder } from '../../common/workspace.apis';
16+
import { getConfiguration, getWorkspaceFolder, onDidChangeConfiguration } from '../../common/workspace.apis';
1617
import { showInformationMessage } from '../../common/window.apis';
1718
import { EnvVarManager } from '../execution/envVariableManager';
1819

@@ -22,6 +23,7 @@ import { EnvVarManager } from '../execution/envVariableManager';
2223
*/
2324
export class TerminalEnvVarInjector implements Disposable {
2425
private disposables: Disposable[] = [];
26+
private readonly previousSettingsState = new Map<string, { useEnvFile: boolean | undefined; envFile: string | undefined }>();
2527

2628
constructor(
2729
private readonly envVarCollection: GlobalEnvironmentVariableCollection,
@@ -36,6 +38,21 @@ export class TerminalEnvVarInjector implements Disposable {
3638
private async initialize(): Promise<void> {
3739
traceVerbose('TerminalEnvVarInjector: Initializing environment variable injection');
3840

41+
// Initialize previous settings state for all workspaces
42+
const workspaceFolders = workspace.workspaceFolders;
43+
if (workspaceFolders) {
44+
for (const folder of workspaceFolders) {
45+
this.updatePreviousSettingsState(folder);
46+
}
47+
}
48+
49+
// Listen for configuration changes to show notifications when settings change
50+
this.disposables.push(
51+
onDidChangeConfiguration((event: ConfigurationChangeEvent) => {
52+
this.handleConfigurationChange(event);
53+
}),
54+
);
55+
3956
// Listen for environment variable changes from the manager
4057
this.disposables.push(
4158
this.envVarManager.onDidChangeEnvironmentVariables((args) => {
@@ -71,6 +88,109 @@ export class TerminalEnvVarInjector implements Disposable {
7188
await this.updateEnvironmentVariables();
7289
}
7390

91+
/**
92+
* Handle configuration changes and show notifications when relevant settings change.
93+
*/
94+
private handleConfigurationChange(event: ConfigurationChangeEvent): void {
95+
const workspaceFolders = workspace.workspaceFolders;
96+
if (!workspaceFolders) {
97+
return;
98+
}
99+
100+
for (const folder of workspaceFolders) {
101+
if (event.affectsConfiguration('python.terminal.useEnvFile', folder.uri) ||
102+
event.affectsConfiguration('python.envFile', folder.uri)) {
103+
104+
const folderKey = folder.uri.toString();
105+
const previousState = this.previousSettingsState.get(folderKey);
106+
const currentState = this.getCurrentSettingsState(folder);
107+
108+
if (previousState && this.shouldShowNotification(previousState, currentState)) {
109+
this.showSettingsChangeNotification(currentState);
110+
}
111+
112+
this.previousSettingsState.set(folderKey, currentState);
113+
114+
// Update environment variables for this workspace
115+
this.updateEnvironmentVariables(folder).catch((error) => {
116+
traceError('Failed to update environment variables after configuration change:', error);
117+
});
118+
}
119+
}
120+
}
121+
122+
/**
123+
* Get current settings state for a workspace.
124+
*/
125+
private getCurrentSettingsState(workspaceFolder: WorkspaceFolder): { useEnvFile: boolean | undefined; envFile: string | undefined } {
126+
const config = getConfiguration('python', workspaceFolder.uri);
127+
return {
128+
useEnvFile: config.get<boolean>('terminal.useEnvFile'),
129+
envFile: config.get<string>('envFile')
130+
};
131+
}
132+
133+
/**
134+
* Update the previous settings state for a workspace.
135+
*/
136+
private updatePreviousSettingsState(workspaceFolder: WorkspaceFolder): void {
137+
const folderKey = workspaceFolder.uri.toString();
138+
this.previousSettingsState.set(folderKey, this.getCurrentSettingsState(workspaceFolder));
139+
}
140+
141+
/**
142+
* Determine if we should show a notification based on settings changes.
143+
*/
144+
private shouldShowNotification(
145+
previousState: { useEnvFile: boolean | undefined; envFile: string | undefined },
146+
currentState: { useEnvFile: boolean | undefined; envFile: string | undefined }
147+
): boolean {
148+
// Show notification if:
149+
// 1. useEnvFile changed from undefined/false to true
150+
// 2. envFile changed from undefined to set (any string value)
151+
// And ensure both settings are configured correctly
152+
153+
const useEnvFileChanged = !previousState.useEnvFile && currentState.useEnvFile;
154+
const envFileChanged = !previousState.envFile && !!currentState.envFile;
155+
156+
if (useEnvFileChanged || envFileChanged) {
157+
// If one is set but the other isn't, show notification
158+
if ((currentState.envFile && !currentState.useEnvFile) ||
159+
(currentState.useEnvFile && !currentState.envFile)) {
160+
return true;
161+
}
162+
}
163+
164+
return false;
165+
}
166+
167+
/**
168+
* Show notification about settings configuration.
169+
*/
170+
private showSettingsChangeNotification(currentState: { useEnvFile: boolean | undefined; envFile: string | undefined }): void {
171+
let message: string;
172+
173+
if (currentState.envFile && !currentState.useEnvFile) {
174+
message = 'The python.envFile setting is configured but will not take effect in terminals. Enable the "python.terminal.useEnvFile" setting to use environment variables from .env files in terminals.';
175+
} else if (currentState.useEnvFile && !currentState.envFile) {
176+
message = 'The python.terminal.useEnvFile setting is enabled. Consider setting "python.envFile" to specify a custom .env file path, or ensure a .env file exists in your workspace root.';
177+
} else {
178+
return; // Both are properly configured, no notification needed
179+
}
180+
181+
showInformationMessage(message, 'Open Settings').then((selection) => {
182+
if (selection === 'Open Settings') {
183+
// Open VS Code settings to the relevant setting
184+
import('vscode').then(vscode => {
185+
const settingToOpen = currentState.envFile && !currentState.useEnvFile
186+
? 'python.terminal.useEnvFile'
187+
: 'python.envFile';
188+
vscode.commands.executeCommand('workbench.action.openSettings', settingToOpen);
189+
});
190+
}
191+
});
192+
}
193+
74194
/**
75195
* Update environment variables in the terminal collection.
76196
*/
@@ -113,64 +233,47 @@ export class TerminalEnvVarInjector implements Disposable {
113233
// Check if environment variable injection is enabled
114234
const config = getConfiguration('python', workspaceUri);
115235
const useEnvFile = config.get<boolean>('terminal.useEnvFile', false);
116-
const envFilePath = config.get<string>('envFile');
117236

118237
// use scoped environment variable collection
119238
const envVarScope = this.getEnvironmentVariableCollectionScoped({ workspaceFolder });
120239
envVarScope.clear(); // Clear existing variables for this workspace
121240

122-
// Check if python.envFile is set but useEnvFile is false (default) and show notification
123-
if (!useEnvFile && envFilePath) {
241+
// Only inject if useEnvFile is true
242+
if (useEnvFile) {
124243
traceVerbose(
125-
`TerminalEnvVarInjector: python.envFile is set but python.terminal.useEnvFile is false for workspace: ${workspaceUri.fsPath}`,
244+
`TerminalEnvVarInjector: Environment variable injection enabled for workspace: ${workspaceUri.fsPath}`,
126245
);
127-
128-
// Show information message to user
129-
showInformationMessage(
130-
'The python.envFile setting is configured but will not take effect in terminals. Enable the "python.terminal.useEnvFile" setting to use environment variables from .env files in terminals.',
131-
'Open Settings'
132-
).then((selection) => {
133-
if (selection === 'Open Settings') {
134-
// Open VS Code settings to the python.terminal.useEnvFile setting
135-
import('vscode').then(vscode => {
136-
vscode.commands.executeCommand('workbench.action.openSettings', 'python.terminal.useEnvFile');
137-
});
138-
}
139-
});
140-
}
141-
142-
if (!useEnvFile) {
143-
traceVerbose(
144-
`TerminalEnvVarInjector: Environment variable injection disabled for workspace: ${workspaceUri.fsPath}`,
145-
);
146-
return; // Injection is disabled
147-
}
148246

149-
const envVars = await this.envVarManager.getEnvironmentVariables(workspaceUri);
247+
const envVars = await this.envVarManager.getEnvironmentVariables(workspaceUri);
150248

151-
// Track which .env file is being used for logging
152-
const resolvedEnvFilePath: string | undefined = envFilePath
153-
? path.resolve(resolveVariables(envFilePath, workspaceUri))
154-
: undefined;
155-
const defaultEnvFilePath: string = path.join(workspaceUri.fsPath, '.env');
249+
// Track which .env file is being used for logging
250+
const envFilePath = config.get<string>('envFile');
251+
const resolvedEnvFilePath: string | undefined = envFilePath
252+
? path.resolve(resolveVariables(envFilePath, workspaceUri))
253+
: undefined;
254+
const defaultEnvFilePath: string = path.join(workspaceUri.fsPath, '.env');
156255

157-
let activeEnvFilePath: string = resolvedEnvFilePath || defaultEnvFilePath;
158-
if (activeEnvFilePath && (await fse.pathExists(activeEnvFilePath))) {
159-
traceVerbose(`TerminalEnvVarInjector: Using env file: ${activeEnvFilePath}`);
256+
let activeEnvFilePath: string = resolvedEnvFilePath || defaultEnvFilePath;
257+
if (activeEnvFilePath && (await fse.pathExists(activeEnvFilePath))) {
258+
traceVerbose(`TerminalEnvVarInjector: Using env file: ${activeEnvFilePath}`);
259+
260+
for (const [key, value] of Object.entries(envVars)) {
261+
if (value === undefined) {
262+
// Remove the environment variable if the value is undefined
263+
envVarScope.delete(key);
264+
} else {
265+
envVarScope.replace(key, value);
266+
}
267+
}
268+
} else {
269+
traceVerbose(
270+
`TerminalEnvVarInjector: No .env file found for workspace: ${workspaceUri.fsPath}, not injecting environment variables.`,
271+
);
272+
}
160273
} else {
161274
traceVerbose(
162-
`TerminalEnvVarInjector: No .env file found for workspace: ${workspaceUri.fsPath}, not injecting environment variables.`,
275+
`TerminalEnvVarInjector: Environment variable injection disabled for workspace: ${workspaceUri.fsPath}`,
163276
);
164-
return; // No .env file to inject
165-
}
166-
167-
for (const [key, value] of Object.entries(envVars)) {
168-
if (value === undefined) {
169-
// Remove the environment variable if the value is undefined
170-
envVarScope.delete(key);
171-
} else {
172-
envVarScope.replace(key, value);
173-
}
174277
}
175278
} catch (error) {
176279
traceError(
@@ -185,7 +288,7 @@ export class TerminalEnvVarInjector implements Disposable {
185288
*/
186289
dispose(): void {
187290
traceVerbose('TerminalEnvVarInjector: Disposing');
188-
this.disposables.forEach((disposable) => disposable.dispose());
291+
this.disposables.forEach((disposable) => disposable?.dispose());
189292
this.disposables = [];
190293

191294
// Clear all environment variables from the collection

src/test/features/terminalEnvVarInjectorBasic.unit.test.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@
33

44
import * as sinon from 'sinon';
55
import * as typeMoq from 'typemoq';
6-
import { GlobalEnvironmentVariableCollection, workspace } from 'vscode';
6+
import { GlobalEnvironmentVariableCollection, workspace, EnvironmentVariableCollection } from 'vscode';
77
import { EnvVarManager } from '../../features/execution/envVariableManager';
88
import { TerminalEnvVarInjector } from '../../features/terminal/terminalEnvVarInjector';
9+
import * as workspaceApi from '../../common/workspace.apis';
910

1011
interface MockScopedCollection {
1112
clear: sinon.SinonStub;
@@ -20,6 +21,7 @@ suite('TerminalEnvVarInjector Basic Tests', () => {
2021
let mockScopedCollection: MockScopedCollection;
2122
// eslint-disable-next-line @typescript-eslint/no-explicit-any
2223
let workspaceFoldersStub: any;
24+
let onDidChangeConfigurationStub: sinon.SinonStub;
2325

2426
setup(() => {
2527
envVarCollection = typeMoq.Mock.ofType<GlobalEnvironmentVariableCollection>();
@@ -40,9 +42,15 @@ suite('TerminalEnvVarInjector Basic Tests', () => {
4042
};
4143

4244
// Setup environment variable collection to return scoped collection
43-
envVarCollection.setup((x) => x.getScoped(typeMoq.It.isAny())).returns(() => mockScopedCollection as any);
45+
envVarCollection.setup((x) => x.getScoped(typeMoq.It.isAny())).returns(() => mockScopedCollection as unknown as EnvironmentVariableCollection);
4446
envVarCollection.setup((x) => x.clear()).returns(() => {});
4547

48+
// Mock onDidChangeConfiguration to return a disposable
49+
onDidChangeConfigurationStub = sinon.stub(workspaceApi, 'onDidChangeConfiguration');
50+
onDidChangeConfigurationStub.returns({
51+
dispose: () => {}
52+
});
53+
4654
// Setup minimal mocks for event subscriptions
4755
envVarManager
4856
.setup((m) => m.onDidChangeEnvironmentVariables)

0 commit comments

Comments
 (0)