Skip to content
Open
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
11 changes: 4 additions & 7 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,6 @@ 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 { openSearchSettings } from './features/views/envManagerSearch';
import { EnvManagerView } from './features/views/envManagersView';
Expand All @@ -97,15 +96,16 @@ import { TemporaryStateManager } from './features/views/temporaryStateManager';
import { ProjectItem, PythonEnvTreeItem } from './features/views/treeViewItems';
import { collectEnvironmentInfo, getEnvManagerAndPackageManagerConfigLevels, runPetInTerminalImpl } from './helpers';
import { EnvironmentManagers, ProjectCreators, PythonProjectManager } from './internal.api';
import { registerSystemPythonFeatures } from './managers/builtin/main';
import { registerInlineScriptFeatures } from './managers/builtin/inlineScript/main';
import { registerSystemPythonFeatures } from './managers/builtin/main';
import { SysPythonManager } from './managers/builtin/sysPythonManager';
import {
createNativePythonFinder,
getNativePythonToolsPathAndSource,
getNativePythonToolsVersion,
NativePythonFinder,
} from './managers/common/nativePythonFinder';
import { registerPackageWatchers } from './managers/common/packageWatcher';
import { IDisposable } from './managers/common/types';
import { registerCondaFeatures } from './managers/conda/main';
import { registerPipenvFeatures } from './managers/pipenv/main';
Expand Down Expand Up @@ -670,6 +670,8 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
safeRegister('shellStartupVars', shellStartupVarsMgr.initialize()),
]);

context.subscriptions.push(registerPackageWatchers(envManagers, outputChannel));

failureStage = 'envSelection';
stageWatch.reset();
await applyInitialEnvironmentSelection(
Expand All @@ -681,11 +683,6 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
globalScopeDeferredRef,
);

// Register manager-agnostic terminal watcher for package-modifying commands
failureStage = 'terminalWatcher';
stageWatch.reset();
registerTerminalPackageWatcher(api, terminalActivation, outputChannel, context.subscriptions);

// Register listener for interpreter settings changes for interpreter re-selection
failureStage = 'settingsListener';
stageWatch.reset();
Expand Down
111 changes: 0 additions & 111 deletions src/features/terminal/terminalPackageWatcher.ts

This file was deleted.

6 changes: 5 additions & 1 deletion src/internal.api.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Pep440Version } from '@renovatebot/pep440';
import { CancellationError, Disposable, Event, LogOutputChannel, MarkdownString, Uri } from 'vscode';
import { CancellationError, Disposable, Event, LogOutputChannel, MarkdownString, RelativePattern, Uri } from 'vscode';
import {
CreateEnvironmentOptions,
CreateEnvironmentScope,
Expand Down Expand Up @@ -380,6 +380,10 @@ export class InternalPackageManager implements PackageManager {
return this.manager.getPackages(environment, options);
}

getPackageWatchTargets(environment: PythonEnvironment): RelativePattern[] {
return this.manager.getPackageWatchTargets?.(environment) ?? [];
}

onDidChangePackages(handler: (e: DidChangePackagesEventArgs) => void): Disposable {
return this.manager.onDidChangePackages ? this.manager.onDidChangePackages(handler) : new Disposable(() => {});
}
Expand Down
6 changes: 0 additions & 6 deletions src/managers/builtin/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { createSimpleDebounce } from '../../common/utils/debounce';
import { createFileSystemWatcher, onDidDeleteFiles } from '../../common/workspace.apis';
import { getPythonApi } from '../../features/pythonApi';
import { NativePythonFinder } from '../common/nativePythonFinder';
import { registerPackageWatcherForManager } from '../common/packageWatcher';
import { PipPackageManager } from './pipPackageManager';
import { SysPythonManager } from './sysPythonManager';
import { VenvManager } from './venvManager';
Expand Down Expand Up @@ -41,9 +40,4 @@ export async function registerSystemPythonFeatures(
venvDebouncedRefresh.trigger();
}),
);

disposables.push(
registerPackageWatcherForManager(envManager, pkgManager, log),
registerPackageWatcherForManager(venvManager, pkgManager, log),
);
}
127 changes: 79 additions & 48 deletions src/managers/common/packageWatcher.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import * as path from 'path';
import { Disposable, LogOutputChannel, RelativePattern } from 'vscode';
import { EnvironmentManager, PackageManager, PythonEnvironment } from '../../api';
import { Disposable, LogOutputChannel, RelativePattern, Uri } from 'vscode';
import { PackageManager, PythonEnvironment } from '../../api';
import { createSimpleDebounce } from '../../common/utils/debounce';
import { createFileSystemWatcher, getConfiguration } from '../../common/workspace.apis';
import { EnvironmentManagers } from '../../internal.api';

/**
* Derives the file system watch targets for a given Python environment.
*
* Targets include site-packages `.dist-info/METADATA` files for pip-style installs.
* Targets include site-packages `.dist-info` directories and their contents for pip-style installs.
*
* @param env - The Python environment to derive watch targets for.
* @returns An array of RelativePattern objects, one per discoverable package location.
Expand All @@ -17,17 +18,20 @@ function getDefaultPackageWatchTargets(env: PythonEnvironment): RelativePattern[
if (!env.sysPrefix) {
return [];
}
return process.platform === 'win32'
? [new RelativePattern(path.join(env.sysPrefix, 'Lib'), 'site-packages/**/*.dist-info/METADATA')] // Windows
: [new RelativePattern(path.join(env.sysPrefix, 'lib'), 'python*/site-packages/**/*.dist-info/METADATA')]; // Unix-like

const isWindows = process.platform === 'win32';
const libraryPath = path.join(env.sysPrefix, isWindows ? 'Lib' : 'lib');
const pattern = isWindows
? 'site-packages/{*.dist-info,*.dist-info/**}'
: 'python*/site-packages/{*.dist-info,*.dist-info/**}';
return [new RelativePattern(libraryPath, pattern)];
}

/**
* Creates a file system watcher for package changes in a single environment.
*
* Monitors default site-packages locations and any manager-specific extra locations
* for install/uninstall operations.
* and triggers a debounced package refresh when changes are detected.
* Monitors default site-packages and manager-specific locations, then triggers a
* debounced package refresh when changes are detected.
*
* @param env - The Python environment to watch.
* @param packageManager - The package manager to call refresh on when changes occur.
Expand All @@ -39,7 +43,6 @@ export function watchPackageChangesForEnvironment(
packageManager: PackageManager,
log: LogOutputChannel,
): Disposable {
// Watch targets
const watchTargets = [
...getDefaultPackageWatchTargets(env),
...(packageManager.getPackageWatchTargets?.(env) ?? []),
Expand All @@ -48,83 +51,111 @@ export function watchPackageChangesForEnvironment(
log.debug(`No watch targets for environment ${env.envId.id}`);
return new Disposable(() => undefined);
}
// Debounced refresh function
const debouncedRefresh = createSimpleDebounce(500, async () => {

const debouncedRefresh = createSimpleDebounce(500, () => {
log.debug(`Package change detected for environment ${env.envId.id}, refreshing packages.`);
packageManager.refresh(env).catch((ex) => {
void packageManager.refresh(env).catch((ex) => {
log.error(
`Failed to refresh packages for environment ${env.envId.id}: ${ex instanceof Error ? ex.message : String(ex)}`,
);
});
});
// Create watchers
const disposables: Disposable[] = [debouncedRefresh];
const trigger = debouncedRefresh.trigger.bind(debouncedRefresh);

for (const target of watchTargets) {
const watcher = createFileSystemWatcher(
target,
false, // create -> install
true, // change -> ignore
false, // delete -> uninstall
);
const watcher = createFileSystemWatcher(target, false, false, false);
log.debug(`Watching for package changes in environment ${env.envId.id} at ${target.pattern}`);
disposables.push(
watcher,
watcher.onDidChange(trigger),
watcher.onDidCreate(trigger),
watcher.onDidDelete(trigger),
);
}

return new Disposable(() => disposables.forEach((d) => d.dispose()));
return Disposable.from(...disposables);
}

/**
* Registers automatic file system watchers for the active environment managed by a given manager.
* Registers package watchers for every active environment, regardless of manager type.
*
* Creates per-environment watchers that are attached when the active environment changes
* and detached when it changes to a different environment. Ensures package changes
* (installs/uninstalls) in the active environment are detected and trigger a refresh.
* A watcher is shared when the same environment is active in multiple scopes and is
* disposed only after the final scope stops using that environment.
*
* @param envManager - The environment manager whose active environment should be watched.
* @param packageManager - The package manager to call refresh on when changes occur.
* @param envManagers - The central environment and package manager registry.
* @param log - Logger for diagnostic and error messages.
* @returns A disposable that removes all watchers and subscriptions when disposed.
*/
export function registerPackageWatcherForManager(
envManager: EnvironmentManager,
packageManager: PackageManager,
log: LogOutputChannel,
): Disposable {
export function registerPackageWatchers(envManagers: EnvironmentManagers, log: LogOutputChannel): Disposable {
const packageWatchersEnabled = getConfiguration('python-envs').get<boolean>('packageWatchers', true);
if (!packageWatchersEnabled) {
return new Disposable(() => undefined);
}

// One watcher per environment id.
const watchers = new Map<string, Disposable>();
const activeWatcherByScope = new Map<string, string>();
const sharedWatchers = new Map<string, { disposable: Disposable; references: number }>();

const releaseScope = (scopeKey: string): void => {
const watcherKey = activeWatcherByScope.get(scopeKey);
if (!watcherKey) {
return;
}

const addWatcher = (env: PythonEnvironment): void => {
if (!watchers.has(env.envId.id)) {
watchers.set(env.envId.id, watchPackageChangesForEnvironment(env, packageManager, log));
activeWatcherByScope.delete(scopeKey);
const watcher = sharedWatchers.get(watcherKey);
if (!watcher) {
return;
}

watcher.references -= 1;
if (watcher.references === 0) {
watcher.disposable.dispose();
sharedWatchers.delete(watcherKey);
}
};

const removeWatcher = (envId: string): void => {
watchers.get(envId)?.dispose();
watchers.delete(envId);
const watchEnvironment = (scopeKey: string, scope: Uri | undefined, environment: PythonEnvironment): void => {
const selectedPackageManager =
envManagers.getPackageManager(scope) ?? envManagers.getPackageManager(environment);
if (!selectedPackageManager) {
releaseScope(scopeKey);
log.debug(`No package manager found for environment ${environment.envId.id}`);
return;
}

const watcherKey = `${environment.envId.managerId}:${environment.envId.id}:${selectedPackageManager.id}`;
if (activeWatcherByScope.get(scopeKey) === watcherKey) {
return;
}

releaseScope(scopeKey);

const sharedWatcher = sharedWatchers.get(watcherKey);
if (sharedWatcher) {
sharedWatcher.references += 1;
} else {
sharedWatchers.set(watcherKey, {
disposable: watchPackageChangesForEnvironment(environment, selectedPackageManager, log),
references: 1,
});
}
activeWatcherByScope.set(scopeKey, watcherKey);
};

const envChangeDisposable = envManager.onDidChangeEnvironment?.((changes) => {
const environmentChangeDisposable = envManagers.onDidChangeActiveEnvironment((changes) => {
const scopeKey = changes.uri?.toString() ?? 'global';
if (changes.new) {
addWatcher(changes.new);
}
if (changes.old && changes.old.envId.id !== changes.new?.envId.id) {
removeWatcher(changes.old.envId.id);
watchEnvironment(scopeKey, changes.uri, changes.new);
} else {
releaseScope(scopeKey);
}
});

return new Disposable(() => {
envChangeDisposable?.dispose();
watchers.forEach((watcher) => watcher.dispose());
watchers.clear();
environmentChangeDisposable.dispose();
sharedWatchers.forEach(({ disposable }) => disposable.dispose());
sharedWatchers.clear();
activeWatcherByScope.clear();
});
}
Loading
Loading