Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ import { cleanupStartupScripts } from './features/terminal/shellStartupSetupHand
import { TerminalActivationImpl } from './features/terminal/terminalActivationState';
import { TerminalEnvVarInjector } from './features/terminal/terminalEnvVarInjector';
import { TerminalManager, TerminalManagerImpl } from './features/terminal/terminalManager';
import { registerTerminalPackageWatcher } from './features/terminal/terminalPackageWatcher';
import { getEnvironmentForTerminal } from './features/terminal/utils';
import { EnvManagerView } from './features/views/envManagersView';
import { ProjectView } from './features/views/projectView';
Expand Down Expand Up @@ -461,6 +462,9 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron

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

// Register manager-agnostic terminal watcher for package-modifying commands
registerTerminalPackageWatcher(api, terminalActivation, outputChannel, context.subscriptions);

// Register listener for interpreter settings changes for interpreter re-selection
context.subscriptions.push(
registerInterpreterSettingsChangeListener(envManagers, projectManager, nativeFinder, api),
Expand Down
111 changes: 111 additions & 0 deletions src/features/terminal/terminalPackageWatcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { Disposable, LogOutputChannel, Terminal } from 'vscode';
import { PythonEnvironment, PythonEnvironmentApi } from '../../api';
import { traceVerbose } from '../../common/logging';
import { onDidEndTerminalShellExecution } from '../../common/window.apis';
import { TerminalEnvironment } from './terminalActivationState';
import { getEnvironmentForTerminal } from './utils';

/**
* Detects if a terminal command is a package-modifying command that should
* trigger a package list refresh. This is manager-agnostic - it detects
* pip, uv, conda, and poetry commands.
*/
export function isPackageModifyingCommand(command: string): boolean {
// pip install/uninstall (including python -m pip, pip3, uv pip, etc.)
if (/(?:^|\s)(?:\S+\s+)*(?:pip\d*)\s+(install|uninstall)\b/.test(command)) {
return true;
}

// uv pip install/uninstall
if (/(?:^|\s)uv\s+pip\s+(install|uninstall)\b/.test(command)) {
return true;
}

// conda install/remove/uninstall
if (/(?:^|\s)(?:conda|mamba|micromamba)\s+(install|remove|uninstall)\b/.test(command)) {
return true;
}

// poetry add/remove
if (/(?:^|\s)poetry\s+(add|remove)\b/.test(command)) {
return true;
}

// pipenv install/uninstall
if (/(?:^|\s)pipenv\s+(install|uninstall)\b/.test(command)) {
return true;
}

return false;
}

/**
* Gets the environment to use for package refresh in a terminal.
*
* Priority order:
* 1. Terminal's tracked activated environment (from terminalActivation state)
* 2. Environment based on terminal cwd/workspace heuristics
*
* This ensures we use the actual environment activated in the terminal,
* not just the workspace's selected environment.
*/
export async function getEnvironmentForPackageRefresh(
terminal: Terminal,
terminalEnv: TerminalEnvironment,
api: PythonEnvironmentApi,
): Promise<PythonEnvironment | undefined> {
// First try to get the environment that's tracked as activated in this terminal
const activatedEnv = terminalEnv.getEnvironment(terminal);
if (activatedEnv) {
traceVerbose(`Using terminal's activated environment: ${activatedEnv.displayName}`);
return activatedEnv;
}

// Fall back to heuristics based on terminal cwd and workspace
traceVerbose('No activated environment tracked for terminal, using heuristic lookup');
return getEnvironmentForTerminal(api, terminal);
}

/**
* Registers a manager-agnostic terminal watcher that listens for package-modifying
* commands and triggers a refresh on the appropriate package manager for the
* currently selected environment.
*
* This ensures that regardless of what command the user runs (pip, conda, etc.),
* the refresh is performed using the configured package manager for the workspace's
* selected environment.
*/
export function registerTerminalPackageWatcher(
api: PythonEnvironmentApi,
terminalEnv: TerminalEnvironment,
log: LogOutputChannel,
disposables: Disposable[],
): void {
disposables.push(
onDidEndTerminalShellExecution(async (e) => {
const commandLine = e.execution.commandLine.value;
const terminal = e.terminal;

if (isPackageModifyingCommand(commandLine)) {
traceVerbose(`Package-modifying command detected: ${commandLine}`);

try {
// Get the environment for this terminal - prioritizes activated env over workspace selection
const env = await getEnvironmentForPackageRefresh(terminal, terminalEnv, api);

if (env) {
traceVerbose(
`Refreshing packages for environment: ${env.displayName} (${env.envId.managerId})`,
);
// This delegates to the correct package manager based on the environment
await api.refreshPackages(env);
} else {
traceVerbose('No environment found for terminal, skipping package refresh');
}
} catch (error) {
log.error(`Error refreshing packages after terminal command: ${error}`);
}
}
}),
);
}
14 changes: 0 additions & 14 deletions src/managers/builtin/main.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import { Disposable, LogOutputChannel } from 'vscode';
import { PythonEnvironmentApi } from '../../api';
import { createSimpleDebounce } from '../../common/utils/debounce';
import { onDidEndTerminalShellExecution } from '../../common/window.apis';
import { createFileSystemWatcher, onDidDeleteFiles } from '../../common/workspace.apis';
import { getPythonApi } from '../../features/pythonApi';
import { NativePythonFinder } from '../common/nativePythonFinder';
import { PipPackageManager } from './pipManager';
import { isPipInstallCommand } from './pipUtils';
import { SysPythonManager } from './sysPythonManager';
import { VenvManager } from './venvManager';

Expand Down Expand Up @@ -42,16 +40,4 @@ export async function registerSystemPythonFeatures(
venvDebouncedRefresh.trigger();
}),
);

disposables.push(
onDidEndTerminalShellExecution(async (e) => {
const cwd = e.terminal.shellIntegration?.cwd;
if (isPipInstallCommand(e.execution.commandLine.value) && cwd) {
const env = await venvManager.get(cwd);
if (env) {
await pkgManager.refresh(env);
}
}
}),
);
}
Loading
Loading