Skip to content

Commit 2fb499e

Browse files
committed
bug: terminal triggered package refresh to use correct env
1 parent 7b701b3 commit 2fb499e

File tree

4 files changed

+841
-14
lines changed

4 files changed

+841
-14
lines changed

src/extension.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ import { cleanupStartupScripts } from './features/terminal/shellStartupSetupHand
6464
import { TerminalActivationImpl } from './features/terminal/terminalActivationState';
6565
import { TerminalEnvVarInjector } from './features/terminal/terminalEnvVarInjector';
6666
import { TerminalManager, TerminalManagerImpl } from './features/terminal/terminalManager';
67+
import { registerTerminalPackageWatcher } from './features/terminal/terminalPackageWatcher';
6768
import { getEnvironmentForTerminal } from './features/terminal/utils';
6869
import { EnvManagerView } from './features/views/envManagersView';
6970
import { ProjectView } from './features/views/projectView';
@@ -461,6 +462,9 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
461462

462463
await applyInitialEnvironmentSelection(envManagers, projectManager, nativeFinder, api);
463464

465+
// Register manager-agnostic terminal watcher for package-modifying commands
466+
registerTerminalPackageWatcher(api, terminalActivation, outputChannel, context.subscriptions);
467+
464468
// Register listener for interpreter settings changes for interpreter re-selection
465469
context.subscriptions.push(
466470
registerInterpreterSettingsChangeListener(envManagers, projectManager, nativeFinder, api),
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import { Disposable, LogOutputChannel, Terminal } from 'vscode';
2+
import { PythonEnvironment, PythonEnvironmentApi } from '../../api';
3+
import { traceVerbose } from '../../common/logging';
4+
import { onDidEndTerminalShellExecution } from '../../common/window.apis';
5+
import { TerminalEnvironment } from './terminalActivationState';
6+
import { getEnvironmentForTerminal } from './utils';
7+
8+
/**
9+
* Detects if a terminal command is a package-modifying command that should
10+
* trigger a package list refresh. This is manager-agnostic - it detects
11+
* pip, uv, conda, and poetry commands.
12+
*/
13+
export function isPackageModifyingCommand(command: string): boolean {
14+
// pip install/uninstall (including python -m pip, pip3, uv pip, etc.)
15+
if (/(?:^|\s)(?:\S+\s+)*(?:pip\d*)\s+(install|uninstall)\b/.test(command)) {
16+
return true;
17+
}
18+
19+
// uv pip install/uninstall
20+
if (/(?:^|\s)uv\s+pip\s+(install|uninstall)\b/.test(command)) {
21+
return true;
22+
}
23+
24+
// conda install/remove/uninstall
25+
if (/(?:^|\s)(?:conda|mamba|micromamba)\s+(install|remove|uninstall)\b/.test(command)) {
26+
return true;
27+
}
28+
29+
// poetry add/remove
30+
if (/(?:^|\s)poetry\s+(add|remove)\b/.test(command)) {
31+
return true;
32+
}
33+
34+
// pipenv install/uninstall
35+
if (/(?:^|\s)pipenv\s+(install|uninstall)\b/.test(command)) {
36+
return true;
37+
}
38+
39+
return false;
40+
}
41+
42+
/**
43+
* Gets the environment to use for package refresh in a terminal.
44+
*
45+
* Priority order:
46+
* 1. Terminal's tracked activated environment (from terminalActivation state)
47+
* 2. Environment based on terminal cwd/workspace heuristics
48+
*
49+
* This ensures we use the actual environment activated in the terminal,
50+
* not just the workspace's selected environment.
51+
*/
52+
export async function getEnvironmentForPackageRefresh(
53+
terminal: Terminal,
54+
terminalEnv: TerminalEnvironment,
55+
api: PythonEnvironmentApi,
56+
): Promise<PythonEnvironment | undefined> {
57+
// First try to get the environment that's tracked as activated in this terminal
58+
const activatedEnv = terminalEnv.getEnvironment(terminal);
59+
if (activatedEnv) {
60+
traceVerbose(`Using terminal's activated environment: ${activatedEnv.displayName}`);
61+
return activatedEnv;
62+
}
63+
64+
// Fall back to heuristics based on terminal cwd and workspace
65+
traceVerbose('No activated environment tracked for terminal, using heuristic lookup');
66+
return getEnvironmentForTerminal(api, terminal);
67+
}
68+
69+
/**
70+
* Registers a manager-agnostic terminal watcher that listens for package-modifying
71+
* commands and triggers a refresh on the appropriate package manager for the
72+
* currently selected environment.
73+
*
74+
* This ensures that regardless of what command the user runs (pip, conda, etc.),
75+
* the refresh is performed using the configured package manager for the workspace's
76+
* selected environment.
77+
*/
78+
export function registerTerminalPackageWatcher(
79+
api: PythonEnvironmentApi,
80+
terminalEnv: TerminalEnvironment,
81+
log: LogOutputChannel,
82+
disposables: Disposable[],
83+
): void {
84+
disposables.push(
85+
onDidEndTerminalShellExecution(async (e) => {
86+
const commandLine = e.execution.commandLine.value;
87+
const terminal = e.terminal;
88+
89+
if (isPackageModifyingCommand(commandLine)) {
90+
traceVerbose(`Package-modifying command detected: ${commandLine}`);
91+
92+
try {
93+
// Get the environment for this terminal - prioritizes activated env over workspace selection
94+
const env = await getEnvironmentForPackageRefresh(terminal, terminalEnv, api);
95+
96+
if (env) {
97+
traceVerbose(
98+
`Refreshing packages for environment: ${env.displayName} (${env.envId.managerId})`,
99+
);
100+
// This delegates to the correct package manager based on the environment
101+
await api.refreshPackages(env);
102+
} else {
103+
traceVerbose('No environment found for terminal, skipping package refresh');
104+
}
105+
} catch (error) {
106+
log.error(`Error refreshing packages after terminal command: ${error}`);
107+
}
108+
}
109+
}),
110+
);
111+
}

src/managers/builtin/main.ts

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
import { Disposable, LogOutputChannel } from 'vscode';
22
import { PythonEnvironmentApi } from '../../api';
33
import { createSimpleDebounce } from '../../common/utils/debounce';
4-
import { onDidEndTerminalShellExecution } from '../../common/window.apis';
54
import { createFileSystemWatcher, onDidDeleteFiles } from '../../common/workspace.apis';
65
import { getPythonApi } from '../../features/pythonApi';
76
import { NativePythonFinder } from '../common/nativePythonFinder';
87
import { PipPackageManager } from './pipManager';
9-
import { isPipInstallCommand } from './pipUtils';
108
import { SysPythonManager } from './sysPythonManager';
119
import { VenvManager } from './venvManager';
1210

@@ -42,16 +40,4 @@ export async function registerSystemPythonFeatures(
4240
venvDebouncedRefresh.trigger();
4341
}),
4442
);
45-
46-
disposables.push(
47-
onDidEndTerminalShellExecution(async (e) => {
48-
const cwd = e.terminal.shellIntegration?.cwd;
49-
if (isPipInstallCommand(e.execution.commandLine.value) && cwd) {
50-
const env = await venvManager.get(cwd);
51-
if (env) {
52-
await pkgManager.refresh(env);
53-
}
54-
}
55-
}),
56-
);
5743
}

0 commit comments

Comments
 (0)