diff --git a/package.json b/package.json index 350565b9..19ea498e 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,16 @@ "categories": [ "Other" ], + "capabilities": { + "untrustedWorkspaces": { + "supported": false, + "description": "This extension doesn't support untrusted workspaces." + }, + "virtualWorkspaces": { + "supported": false, + "description": "This extension doesn't support virtual workspaces." + } + }, "activationEvents": [ "onLanguage:python" ], @@ -21,9 +31,6 @@ "bugs": { "url": "https://github.com/microsoft/vscode-python-environments/issues" }, - "extensionDependencies": [ - "ms-python.python" - ], "main": "./dist/extension.js", "icon": "icon.png", "contributes": { diff --git a/src/api.ts b/src/api.ts index 90c81cd9..065a4bab 100644 --- a/src/api.ts +++ b/src/api.ts @@ -240,7 +240,7 @@ export type DidChangeEnvironmentEventArgs = { /** * The URI of the environment that changed. */ - readonly uri: Uri; + readonly uri: Uri | undefined; /** * The old Python environment before the change. @@ -968,7 +968,7 @@ export interface PythonPackageManagementApi { export interface PythonPackageManagerApi extends PythonPackageManagerRegistrationApi, PythonPackageGetterApi, - PythonEnvironmentManagerApi, + PythonPackageManagementApi, PythonPackageItemApi {} export interface PythonProjectCreationApi { diff --git a/src/common/pickers/environments.ts b/src/common/pickers/environments.ts index 3d03e696..0cef6f2b 100644 --- a/src/common/pickers/environments.ts +++ b/src/common/pickers/environments.ts @@ -165,7 +165,8 @@ export async function pickEnvironment( return { label: e.displayName ?? e.name, description: e.description, - e: { selected: e, manager: manager }, + result: e, + manager: manager, iconPath: getIconPath(e.iconPath), }; }), diff --git a/src/common/pickers/packages.ts b/src/common/pickers/packages.ts index 3dde73bf..62e704a8 100644 --- a/src/common/pickers/packages.ts +++ b/src/common/pickers/packages.ts @@ -170,25 +170,30 @@ function getGroupedItems(items: Installable[]): PackageQuickPickItem[] { return result; } +async function getInstallables(packageManager: InternalPackageManager, environment: PythonEnvironment) { + const installable = await packageManager?.getInstallable(environment); + if (installable && installable.length === 0) { + traceWarn(`No installable packages found for ${packageManager.id}: ${environment.environmentPath.fsPath}`); + } + return installable; +} + async function getWorkspacePackages( - packageManager: InternalPackageManager, - environment: PythonEnvironment, + installable: Installable[] | undefined, preSelected?: PackageQuickPickItem[] | undefined, ): Promise { const items: PackageQuickPickItem[] = []; - let installable = await packageManager?.getInstallable(environment); if (installable && installable.length > 0) { items.push(...getGroupedItems(installable)); } else { - traceWarn(`No installable packages found for ${packageManager.id}: ${environment.environmentPath.fsPath}`); - installable = await getCommonPackages(); + const common = await getCommonPackages(); items.push( { label: PackageManagement.commonPackages, kind: QuickPickItemKind.Separator, }, - ...installable.map(installableToQuickPickItem), + ...common.map(installableToQuickPickItem), ); } @@ -240,7 +245,7 @@ async function getWorkspacePackages( return result; } catch (ex) { if (ex === QuickInputButtons.Back) { - return getWorkspacePackages(packageManager, environment, selected); + return getWorkspacePackages(installable, selected); } return undefined; } @@ -250,7 +255,7 @@ async function getWorkspacePackages( } } -export async function getCommonPackagesToInstall( +async function getCommonPackagesToInstall( preSelected?: PackageQuickPickItem[] | undefined, ): Promise { const common = await getCommonPackages(); @@ -315,7 +320,7 @@ export async function getCommonPackagesToInstall( } } -export async function getPackagesToInstall( +export async function getPackagesToInstallFromPackageManager( packageManager: InternalPackageManager, environment: PythonEnvironment, ): Promise { @@ -325,11 +330,12 @@ export async function getPackagesToInstall( if (packageType === PackageManagement.workspaceDependencies) { try { - const result = await getWorkspacePackages(packageManager, environment); + const installable = await getInstallables(packageManager, environment); + const result = await getWorkspacePackages(installable); return result; } catch (ex) { if (packageManager.supportsGetInstallable && ex === QuickInputButtons.Back) { - return getPackagesToInstall(packageManager, environment); + return getPackagesToInstallFromPackageManager(packageManager, environment); } if (ex === QuickInputButtons.Back) { throw ex; @@ -344,7 +350,7 @@ export async function getPackagesToInstall( return result; } catch (ex) { if (packageManager.supportsGetInstallable && ex === QuickInputButtons.Back) { - return getPackagesToInstall(packageManager, environment); + return getPackagesToInstallFromPackageManager(packageManager, environment); } if (ex === QuickInputButtons.Back) { throw ex; @@ -356,6 +362,13 @@ export async function getPackagesToInstall( return undefined; } +export async function getPackagesToInstallFromInstallable(installable: Installable[]): Promise { + if (installable.length === 0) { + return undefined; + } + return getWorkspacePackages(installable); +} + export async function getPackagesToUninstall(packages: Package[]): Promise { const items = packages.map((p) => ({ label: p.name, diff --git a/src/common/utils/pythonPath.ts b/src/common/utils/pythonPath.ts index df3cfb21..451da4b1 100644 --- a/src/common/utils/pythonPath.ts +++ b/src/common/utils/pythonPath.ts @@ -40,6 +40,8 @@ export async function handlePythonPath( reporter?: Progress<{ message?: string; increment?: number }>, token?: CancellationToken, ): Promise { + // Use the managers user has set for the project first. Likely, these + // managers are the ones that should be used. for (const manager of sortManagersByPriority(projectEnvManagers)) { if (token?.isCancellationRequested) { return; @@ -54,6 +56,8 @@ export async function handlePythonPath( traceVerbose(`Manager ${manager.displayName} (${manager.id}) cannot handle ${interpreterUri.fsPath}`); } + // If the project managers cannot handle the interpreter, then try all the managers + // that user has installed. Excluding anything that is already checked. const checkedIds = projectEnvManagers.map((m) => m.id); const filtered = managers.filter((m) => !checkedIds.includes(m.id)); @@ -70,10 +74,6 @@ export async function handlePythonPath( } } - if (token?.isCancellationRequested) { - return; - } - traceError(`Unable to handle ${interpreterUri.fsPath}`); showErrorMessage(`Unable to handle ${interpreterUri.fsPath}`); return undefined; diff --git a/src/common/window.apis.ts b/src/common/window.apis.ts index 1f706de9..9d5da869 100644 --- a/src/common/window.apis.ts +++ b/src/common/window.apis.ts @@ -4,7 +4,9 @@ import { ExtensionTerminalOptions, InputBox, InputBoxOptions, + LogOutputChannel, OpenDialogOptions, + OutputChannel, Progress, ProgressOptions, QuickInputButton, @@ -279,3 +281,11 @@ export function showWarningMessage(message: string, ...items: string[]): Thenabl export function showInputBox(options?: InputBoxOptions, token?: CancellationToken): Thenable { return window.showInputBox(options, token); } + +export function createOutputChannel(name: string, languageId?: string): OutputChannel { + return window.createOutputChannel(name, languageId); +} + +export function createLogOutputChannel(name: string): LogOutputChannel { + return window.createOutputChannel(name, { log: true }); +} diff --git a/src/common/workspace.apis.ts b/src/common/workspace.apis.ts index c4236d20..7e2c3ea4 100644 --- a/src/common/workspace.apis.ts +++ b/src/common/workspace.apis.ts @@ -3,6 +3,8 @@ import { ConfigurationChangeEvent, ConfigurationScope, Disposable, + FileDeleteEvent, + FileSystemWatcher, GlobPattern, Uri, workspace, @@ -39,3 +41,20 @@ export function findFiles( ): Thenable { return workspace.findFiles(include, exclude, maxResults, token); } + +export function createFileSystemWatcher( + globPattern: GlobPattern, + ignoreCreateEvents?: boolean, + ignoreChangeEvents?: boolean, + ignoreDeleteEvents?: boolean, +): FileSystemWatcher { + return workspace.createFileSystemWatcher(globPattern, ignoreCreateEvents, ignoreChangeEvents, ignoreDeleteEvents); +} + +export function onDidDeleteFiles( + listener: (e: FileDeleteEvent) => any, + thisArgs?: any, + disposables?: Disposable[], +): Disposable { + return workspace.onDidDeleteFiles(listener, thisArgs, disposables); +} diff --git a/src/extension.ts b/src/extension.ts index 22c00b9e..3f2330e7 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,4 +1,4 @@ -import { window, commands, ExtensionContext, LogOutputChannel, TextEditor } from 'vscode'; +import { commands, ExtensionContext, LogOutputChannel } from 'vscode'; import { PythonEnvironmentManagers } from './features/envManagers'; import { registerLogger } from './common/logging'; @@ -28,9 +28,8 @@ import { PythonProjectManagerImpl } from './features/projectManager'; import { EnvironmentManagers, ProjectCreators, PythonProjectManager } from './internal.api'; import { getPythonApi, setPythonApi } from './features/pythonApi'; import { setPersistentState } from './common/persistentState'; -import { isPythonProjectFile } from './common/utils/fileNameUtils'; import { createNativePythonFinder, NativePythonFinder } from './managers/common/nativePythonFinder'; -import { PythonEnvironmentApi, PythonProject } from './api'; +import { PythonEnvironmentApi } from './api'; import { ProjectCreatorsImpl, registerAutoProjectProvider, @@ -39,21 +38,31 @@ import { import { WorkspaceView } from './features/views/projectView'; import { registerCompletionProvider } from './features/settings/settingCompletions'; import { TerminalManager, TerminalManagerImpl } from './features/terminal/terminalManager'; -import { activeTerminal, onDidChangeActiveTerminal, onDidChangeActiveTextEditor } from './common/window.apis'; +import { + activeTerminal, + createLogOutputChannel, + onDidChangeActiveTerminal, + onDidChangeActiveTextEditor, +} from './common/window.apis'; import { getEnvironmentForTerminal, setActivateMenuButtonContext, updateActivateMenuButtonContext, } from './features/terminal/activateMenuButton'; +import { PythonStatusBarImpl } from './features/views/pythonStatusBar'; +import { updateViewsAndStatus } from './features/views/revealHandler'; export async function activate(context: ExtensionContext): Promise { // Logging should be set up before anything else. - const outputChannel: LogOutputChannel = window.createOutputChannel('Python Environments', { log: true }); + const outputChannel: LogOutputChannel = createLogOutputChannel('Python Environments'); context.subscriptions.push(outputChannel, registerLogger(outputChannel)); // Setup the persistent state for the extension. setPersistentState(context); + const statusBar = new PythonStatusBarImpl(); + context.subscriptions.push(statusBar); + const terminalManager: TerminalManager = new TerminalManagerImpl(); context.subscriptions.push(terminalManager); @@ -113,26 +122,14 @@ export async function activate(context: ExtensionContext): Promise { const result = await setEnvironmentCommand(item, envManagers, projectManager); if (result) { - const projects: PythonProject[] = []; - result.forEach((r) => { - if (r.project) { - projects.push(r.project); - } - }); - workspaceView.updateProject(projects); + workspaceView.updateProject(); await updateActivateMenuButtonContext(terminalManager, projectManager, envManagers); } }), commands.registerCommand('python-envs.setEnv', async (item) => { const result = await setEnvironmentCommand(item, envManagers, projectManager); if (result) { - const projects: PythonProject[] = []; - result.forEach((r) => { - if (r.project) { - projects.push(r.project); - } - }); - workspaceView.updateProject(projects); + workspaceView.updateProject(); await updateActivateMenuButtonContext(terminalManager, projectManager, envManagers); } }), @@ -193,38 +190,14 @@ export async function activate(context: ExtensionContext): Promise { await updateActivateMenuButtonContext(terminalManager, projectManager, envManagers, t); }), - onDidChangeActiveTextEditor(async (e: TextEditor | undefined) => { - if (e && !e.document.isUntitled && e.document.uri.scheme === 'file') { - if ( - e.document.languageId === 'python' || - e.document.languageId === 'pip-requirements' || - isPythonProjectFile(e.document.uri.fsPath) - ) { - const env = await workspaceView.reveal(e.document.uri); - await managerView.reveal(env); - } - } + onDidChangeActiveTextEditor(async () => { + updateViewsAndStatus(statusBar, workspaceView, managerView, api); }), - envManagers.onDidChangeEnvironment(async (e) => { - const activeDocument = window.activeTextEditor?.document; - if (!activeDocument || activeDocument.isUntitled || activeDocument.uri.scheme !== 'file') { - return; - } - - if ( - activeDocument.languageId !== 'python' && - activeDocument.languageId !== 'pip-requirements' && - !isPythonProjectFile(activeDocument.uri.fsPath) - ) { - return; - } - - const mgr1 = envManagers.getEnvironmentManager(e.uri); - const mgr2 = envManagers.getEnvironmentManager(activeDocument.uri); - if (mgr1 === mgr2 && e.new) { - const env = await workspaceView.reveal(activeDocument.uri); - await managerView.reveal(env); - } + envManagers.onDidChangeEnvironment(async () => { + updateViewsAndStatus(statusBar, workspaceView, managerView, api); + }), + envManagers.onDidChangeEnvironments(async () => { + updateViewsAndStatus(statusBar, workspaceView, managerView, api); }), ); diff --git a/src/features/envCommands.ts b/src/features/envCommands.ts index 0a0166e3..996b2f46 100644 --- a/src/features/envCommands.ts +++ b/src/features/envCommands.ts @@ -34,7 +34,11 @@ import { import { Common } from '../common/localize'; import { pickEnvironment } from '../common/pickers/environments'; import { pickEnvironmentManager, pickPackageManager, pickCreator } from '../common/pickers/managers'; -import { pickPackageOptions, getPackagesToInstall, getPackagesToUninstall } from '../common/pickers/packages'; +import { + pickPackageOptions, + getPackagesToInstallFromPackageManager, + getPackagesToUninstall, +} from '../common/pickers/packages'; import { pickProject, pickProjectMany } from '../common/pickers/projects'; import { TerminalManager } from './terminal/terminalManager'; import { runInTerminal } from './terminal/runInTerminal'; @@ -138,7 +142,7 @@ export async function handlePackagesCommand( if (action === Common.install) { if (!packages || packages.length === 0) { try { - packages = await getPackagesToInstall(packageManager, environment); + packages = await getPackagesToInstallFromPackageManager(packageManager, environment); } catch (ex: any) { if (ex === QuickInputButtons.Back) { return handlePackagesCommand(packageManager, environment, packages); @@ -192,7 +196,7 @@ export async function setEnvironmentCommand( return; } else if (context instanceof ProjectItem) { const view = context as ProjectItem; - return setEnvironmentCommand(view.project.uri, em, wm); + return setEnvironmentCommand([view.project.uri], em, wm); } else if (context instanceof Uri) { return setEnvironmentCommand([context], em, wm); } else if (context === undefined) { @@ -236,8 +240,8 @@ export async function setEnvironmentCommand( const settings: EditAllManagerSettings[] = []; uris.forEach((uri) => { const m = em.getEnvironmentManager(uri); + promises.push(manager.set(uri, selected)); if (manager.id !== m?.id) { - promises.push(manager.set(uri, selected)); settings.push({ project: wm.get(uri), envManager: manager.id, diff --git a/src/features/pythonApi.ts b/src/features/pythonApi.ts index e5395084..518f3775 100644 --- a/src/features/pythonApi.ts +++ b/src/features/pythonApi.ts @@ -44,12 +44,15 @@ import { TerminalManager } from './terminal/terminalManager'; import { runAsTask } from './execution/runAsTask'; import { runInTerminal } from './terminal/runInTerminal'; import { runInBackground } from './execution/runInBackground'; +import { setAllManagerSettings } from './settings/settingHelpers'; class PythonEnvironmentApiImpl implements PythonEnvironmentApi { private readonly _onDidChangeEnvironments = new EventEmitter(); private readonly _onDidChangeEnvironment = new EventEmitter(); private readonly _onDidChangePythonProjects = new EventEmitter(); private readonly _onDidChangePackages = new EventEmitter(); + + private readonly _previousEnvironments = new Map(); constructor( private readonly envManagers: EnvironmentManagers, private readonly projectManager: PythonProjectManager, @@ -159,18 +162,46 @@ class PythonEnvironmentApiImpl implements PythonEnvironmentApi { } onDidChangeEnvironments: Event = this._onDidChangeEnvironments.event; async setEnvironment(scope: SetEnvironmentScope, environment?: PythonEnvironment): Promise { - const manager = this.envManagers.getEnvironmentManager(scope); + const manager = environment + ? this.envManagers.getEnvironmentManager(environment.envId.managerId) + : this.envManagers.getEnvironmentManager(scope); + if (!manager) { throw new Error('No environment manager found'); } await manager.set(scope, environment); + if (scope) { + const project = this.projectManager.get(scope); + const packageManager = this.envManagers.getPackageManager(environment); + if (project && packageManager) { + await setAllManagerSettings([ + { + project, + envManager: manager.id, + packageManager: packageManager.id, + }, + ]); + } + } + + const oldEnv = this._previousEnvironments.get(scope?.toString() ?? 'global'); + if (oldEnv?.envId.id !== environment?.envId.id) { + this._previousEnvironments.set(scope?.toString() ?? 'global', environment); + this._onDidChangeEnvironment.fire({ uri: scope, new: environment, old: oldEnv }); + } } - async getEnvironment(context: GetEnvironmentScope): Promise { - const manager = this.envManagers.getEnvironmentManager(context); + async getEnvironment(scope: GetEnvironmentScope): Promise { + const manager = this.envManagers.getEnvironmentManager(scope); if (!manager) { return undefined; } - return await manager.get(context); + const oldEnv = this._previousEnvironments.get(scope?.toString() ?? 'global'); + const newEnv = await manager.get(scope); + if (oldEnv?.envId.id !== newEnv?.envId.id) { + this._previousEnvironments.set(scope?.toString() ?? 'global', newEnv); + this._onDidChangeEnvironment.fire({ uri: scope, new: newEnv, old: oldEnv }); + } + return newEnv; } onDidChangeEnvironment: Event = this._onDidChangeEnvironment.event; async resolveEnvironment(context: ResolveEnvironmentContext): Promise { diff --git a/src/features/statusBarPython.ts b/src/features/statusBarPython.ts deleted file mode 100644 index 2e5de938..00000000 --- a/src/features/statusBarPython.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { Disposable, StatusBarAlignment, StatusBarItem, ThemeColor, Uri } from 'vscode'; -import { createStatusBarItem } from '../common/window.apis'; -import { Interpreter } from '../common/localize'; -import { PythonProjectManager } from '../internal.api'; -import { PythonEnvironment } from '../api'; - -const STATUS_BAR_ITEM_PRIORITY = 100.09999; - -export interface PythonStatusBar extends Disposable { - update(uri: Uri | undefined, env?: PythonEnvironment): void; - show(uri: Uri): void; - hide(): void; -} - -export class PythonStatusBarImpl implements PythonStatusBar { - private _global: PythonEnvironment | undefined; - private _statusBarItem: StatusBarItem; - private _disposables: Disposable[] = []; - private _uriToEnv: Map = new Map(); - constructor(private readonly projectManager: PythonProjectManager) { - this._statusBarItem = createStatusBarItem( - 'python-envs.statusBarItem.selectedInterpreter', - StatusBarAlignment.Right, - STATUS_BAR_ITEM_PRIORITY, - ); - this._statusBarItem.command = 'python-envs.set'; - this._disposables.push(this._statusBarItem); - } - - public update(uri: Uri | undefined, env?: PythonEnvironment): void { - const project = uri ? this.projectManager.get(uri)?.uri : undefined; - if (!project) { - this._global = env; - } else { - if (env) { - this._uriToEnv.set(project.toString(), env); - } else { - this._uriToEnv.delete(project.toString()); - } - } - } - public show(uri: Uri | undefined) { - const project = uri ? this.projectManager.get(uri)?.uri : undefined; - const environment = project ? this._uriToEnv.get(project.toString()) : this._global; - if (environment) { - this._statusBarItem.text = environment.shortDisplayName ?? environment.displayName; - this._statusBarItem.tooltip = environment.environmentPath.fsPath; - this._statusBarItem.backgroundColor = undefined; - this._statusBarItem.show(); - return; - } else if (project) { - // Show alert only if it is a project file - this._statusBarItem.tooltip = ''; - this._statusBarItem.backgroundColor = new ThemeColor('statusBarItem.warningBackground'); - this._statusBarItem.text = `$(alert) ${Interpreter.statusBarSelect}`; - this._statusBarItem.show(); - return; - } - - this._statusBarItem.hide(); - } - - public hide() { - this._statusBarItem.hide(); - } - - dispose() { - this._disposables.forEach((d) => d.dispose()); - } -} diff --git a/src/features/terminal/activateMenuButton.ts b/src/features/terminal/activateMenuButton.ts index 968269ce..ac85e608 100644 --- a/src/features/terminal/activateMenuButton.ts +++ b/src/features/terminal/activateMenuButton.ts @@ -1,10 +1,28 @@ -import { Terminal } from 'vscode'; +import { Terminal, TerminalOptions, Uri } from 'vscode'; import { activeTerminal } from '../../common/window.apis'; import { TerminalActivation, TerminalEnvironment } from './terminalManager'; import { EnvironmentManagers, PythonProjectManager } from '../../internal.api'; import { PythonEnvironment } from '../../api'; import { isActivatableEnvironment } from '../common/activation'; import { executeCommand } from '../../common/command.api'; +import { getWorkspaceFolders } from '../../common/workspace.apis'; + +async function getDistinctProjectEnvs(pm: PythonProjectManager, em: EnvironmentManagers): Promise { + const projects = pm.getProjects(); + const envs: PythonEnvironment[] = []; + const projectEnvs = await Promise.all( + projects.map(async (p) => { + const manager = em.getEnvironmentManager(p.uri); + return manager?.get(p.uri); + }), + ); + projectEnvs.forEach((e) => { + if (e && !envs.find((x) => x.envId.id === e.envId.id)) { + envs.push(e); + } + }); + return envs; +} export async function getEnvironmentForTerminal( tm: TerminalEnvironment, @@ -13,15 +31,43 @@ export async function getEnvironmentForTerminal( t: Terminal, ): Promise { let env = await tm.getEnvironment(t); + if (env) { + return env; + } - if (!env) { - const projects = pm.getProjects(); - if (projects.length === 0) { - const manager = em.getEnvironmentManager(undefined); - env = await manager?.get(undefined); - } else if (projects.length === 1) { - const manager = em.getEnvironmentManager(projects[0].uri); - env = await manager?.get(projects[0].uri); + const projects = pm.getProjects(); + if (projects.length === 0) { + const manager = em.getEnvironmentManager(undefined); + env = await manager?.get(undefined); + } else if (projects.length === 1) { + const manager = em.getEnvironmentManager(projects[0].uri); + env = await manager?.get(projects[0].uri); + } else { + const envs = await getDistinctProjectEnvs(pm, em); + if (envs.length === 1) { + // If we have only one distinct environment, then use that. + env = envs[0]; + } else { + // If we have multiple distinct environments, then we can't pick one + // So skip selecting so we can try heuristic approach + } + } + if (env) { + return env; + } + + // This is a heuristic approach to attempt to find the environment for this terminal. + // This is not guaranteed to work, but is better than nothing. + let tempCwd = (t.creationOptions as TerminalOptions)?.cwd; + let cwd = typeof tempCwd === 'string' ? Uri.file(tempCwd) : tempCwd; + if (cwd) { + const manager = em.getEnvironmentManager(cwd); + env = await manager?.get(cwd); + } else { + const workspaces = getWorkspaceFolders() ?? []; + if (workspaces.length === 1) { + const manager = em.getEnvironmentManager(workspaces[0].uri); + env = await manager?.get(workspaces[0].uri); } } diff --git a/src/features/views/envManagersView.ts b/src/features/views/envManagersView.ts index 0a588489..59838550 100644 --- a/src/features/views/envManagersView.ts +++ b/src/features/views/envManagersView.ts @@ -9,7 +9,6 @@ import { InternalEnvironmentManager, InternalPackageManager, } from '../../internal.api'; -import { traceError } from '../../common/logging'; import { EnvTreeItem, EnvManagerTreeItem, @@ -21,16 +20,16 @@ import { EnvInfoTreeItem, PackageRootInfoTreeItem, } from './treeViewItems'; +import { createSimpleDebounce } from '../../common/utils/debounce'; export class EnvManagerView implements TreeDataProvider, Disposable { private treeView: TreeView; - private _treeDataChanged: EventEmitter = new EventEmitter< + private treeDataChanged: EventEmitter = new EventEmitter< EnvTreeItem | EnvTreeItem[] | null | undefined >(); - private _viewsManagers = new Map(); - private _viewsEnvironments = new Map(); - private _viewsPackageRoots = new Map(); - private _viewsPackages = new Map(); + private revealMap = new Map(); + private managerViews = new Map(); + private packageRoots = new Map(); private disposables: Disposable[] = []; public constructor(public providers: EnvironmentManagers) { @@ -39,8 +38,13 @@ export class EnvManagerView implements TreeDataProvider, Disposable }); this.disposables.push( + new Disposable(() => { + this.packageRoots.clear(); + this.revealMap.clear(); + this.managerViews.clear(); + }), this.treeView, - this._treeDataChanged, + this.treeDataChanged, this.providers.onDidChangeEnvironments((e: InternalDidChangeEnvironmentsEventArgs) => { this.onDidChangeEnvironments(e); }), @@ -58,23 +62,19 @@ export class EnvManagerView implements TreeDataProvider, Disposable } dispose() { - this._viewsManagers.clear(); - this._viewsEnvironments.clear(); - this._viewsPackages.clear(); this.disposables.forEach((d) => d.dispose()); } + private debouncedFireDataChanged = createSimpleDebounce(500, () => this.treeDataChanged.fire(undefined)); private fireDataChanged(item: EnvTreeItem | EnvTreeItem[] | null | undefined) { - if (Array.isArray(item)) { - if (item.length > 0) { - this._treeDataChanged.fire(item); - } + if (item) { + this.treeDataChanged.fire(item); } else { - this._treeDataChanged.fire(item); + this.debouncedFireDataChanged.trigger(); } } - onDidChangeTreeData: Event = this._treeDataChanged.event; + onDidChangeTreeData: Event = this.treeDataChanged.event; getTreeItem(element: EnvTreeItem): TreeItem | Thenable { return element.treeItem; @@ -82,17 +82,24 @@ export class EnvManagerView implements TreeDataProvider, Disposable async getChildren(element?: EnvTreeItem | undefined): Promise { if (!element) { - return Array.from(this._viewsManagers.values()); + const views: EnvTreeItem[] = []; + this.managerViews.clear(); + this.providers.managers.forEach((m) => { + const view = new EnvManagerTreeItem(m); + views.push(view); + this.managerViews.set(m.id, view); + }); + return views; } + if (element.kind === EnvTreeItemKind.manager) { const manager = (element as EnvManagerTreeItem).manager; const views: EnvTreeItem[] = []; const envs = await manager.getEnvironments('all'); envs.forEach((env) => { - const view = this._viewsEnvironments.get(env.envId.id); - if (view) { - views.push(view); - } + const view = new PythonEnvTreeItem(env, element as EnvManagerTreeItem); + views.push(view); + this.revealMap.set(env.envId.id, view); }); if (views.length === 0) { @@ -110,7 +117,7 @@ export class EnvManagerView implements TreeDataProvider, Disposable if (pkgManager) { const item = new PackageRootTreeItem(parent, pkgManager, environment); - this._viewsPackageRoots.set(environment.envId.id, item); + this.packageRoots.set(environment.envId.id, item); views.push(item); } else { views.push(new EnvInfoTreeItem(parent, 'No package manager found')); @@ -141,12 +148,12 @@ export class EnvManagerView implements TreeDataProvider, Disposable return element.parent; } - async reveal(environment?: PythonEnvironment) { - if (environment && this.treeView.visible) { - const view = this._viewsEnvironments.get(environment.envId.id); - if (view) { + reveal(environment?: PythonEnvironment) { + const view = environment ? this.revealMap.get(environment.envId.id) : undefined; + if (view && this.treeView.visible) { + setImmediate(async () => { await this.treeView.reveal(view); - } + }); } } @@ -154,60 +161,23 @@ export class EnvManagerView implements TreeDataProvider, Disposable return this.providers.getPackageManager(manager.preferredPackageManagerId); } - private onDidChangeEnvironmentManager(args: DidChangeEnvironmentManagerEventArgs) { - if (args.kind === 'registered') { - this._viewsManagers.set(args.manager.id, new EnvManagerTreeItem(args.manager)); - this.fireDataChanged(undefined); - } else { - if (this._viewsManagers.delete(args.manager.id)) { - this.fireDataChanged(undefined); - } - } + private onDidChangeEnvironmentManager(_args: DidChangeEnvironmentManagerEventArgs) { + this.fireDataChanged(undefined); } private onDidChangeEnvironments(args: InternalDidChangeEnvironmentsEventArgs) { - const managerView = this._viewsManagers.get(args.manager.id); - if (!managerView) { - traceError(`No manager found: ${args.manager.id}`); - traceError(`Managers: ${this.providers.managers.map((m) => m.id).join(', ')}`); - return; - } - - // All removes should happen first, then adds - const sorted = args.changes.sort((a, b) => { - if (a.kind === 'remove' && b.kind === 'add') { - return -1; - } - if (a.kind === 'add' && b.kind === 'remove') { - return 1; - } - return 0; - }); - - sorted.forEach((e) => { - if (managerView) { - if (e.kind === 'add') { - this._viewsEnvironments.set( - e.environment.envId.id, - new PythonEnvTreeItem(e.environment, managerView), - ); - } else if (e.kind === 'remove') { - this._viewsEnvironments.delete(e.environment.envId.id); - } - } - }); - this.fireDataChanged([managerView]); + this.fireDataChanged(this.managerViews.get(args.manager.id)); } private onDidChangePackages(args: InternalDidChangePackagesEventArgs) { - const pkgRoot = this._viewsPackageRoots.get(args.environment.envId.id); + const pkgRoot = this.packageRoots.get(args.environment.envId.id); if (pkgRoot) { this.fireDataChanged(pkgRoot); } } private onDidChangePackageManager(args: DidChangePackageManagerEventArgs) { - const roots = Array.from(this._viewsPackageRoots.values()).filter((r) => r.manager.id === args.manager.id); + const roots = Array.from(this.packageRoots.values()).filter((r) => r.manager.id === args.manager.id); this.fireDataChanged(roots); } } diff --git a/src/features/views/projectView.ts b/src/features/views/projectView.ts index 317ee714..7ea7a81d 100644 --- a/src/features/views/projectView.ts +++ b/src/features/views/projectView.ts @@ -9,7 +9,7 @@ import { Uri, window, } from 'vscode'; -import { PythonProject, PythonEnvironment } from '../../api'; +import { PythonEnvironment } from '../../api'; import { EnvironmentManagers, PythonProjectManager } from '../../internal.api'; import { ProjectTreeItem, @@ -22,35 +22,52 @@ import { ProjectPackage, ProjectPackageRootInfoTreeItem, } from './treeViewItems'; +import { onDidChangeConfiguration } from '../../common/workspace.apis'; +import { createSimpleDebounce } from '../../common/utils/debounce'; export class WorkspaceView implements TreeDataProvider { private treeView: TreeView; private _treeDataChanged: EventEmitter = new EventEmitter< ProjectTreeItem | ProjectTreeItem[] | null | undefined >(); - private _projectViews: Map = new Map(); - private _environmentViews: Map = new Map(); - private _viewsPackageRoots: Map = new Map(); + private projectViews: Map = new Map(); + private revealMap: Map = new Map(); + private packageRoots: Map = new Map(); private disposables: Disposable[] = []; + private debouncedUpdateProject = createSimpleDebounce(500, () => this.updateProject()); public constructor(private envManagers: EnvironmentManagers, private projectManager: PythonProjectManager) { this.treeView = window.createTreeView('python-projects', { treeDataProvider: this, }); this.disposables.push( + new Disposable(() => { + this.packageRoots.clear(); + this.revealMap.clear(); + this.projectViews.clear(); + }), this.treeView, this._treeDataChanged, this.projectManager.onDidChangeProjects(() => { - this.updateProject(); + this.debouncedUpdateProject.trigger(); }), - this.envManagers.onDidChangeEnvironment((e) => { - this.updateProject(this.projectManager.get(e.uri)); + this.envManagers.onDidChangeEnvironment(() => { + this.debouncedUpdateProject.trigger(); }), this.envManagers.onDidChangeEnvironments(() => { - this.updateProject(); + this.debouncedUpdateProject.trigger(); }), this.envManagers.onDidChangePackages((e) => { this.updatePackagesForEnvironment(e.environment); }), + onDidChangeConfiguration(async (e) => { + if ( + e.affectsConfiguration('python-envs.defaultEnvManager') || + e.affectsConfiguration('python-envs.pythonProjects') || + e.affectsConfiguration('python-envs.defaultPackageManager') + ) { + this.debouncedUpdateProject.trigger(); + } + }), ); } @@ -58,33 +75,13 @@ export class WorkspaceView implements TreeDataProvider { this.projectManager.initialize(); } - updateProject(p?: PythonProject | PythonProject[]): void { - if (Array.isArray(p)) { - const views: ProjectItem[] = []; - p.forEach((w) => { - const view = this._projectViews.get(ProjectItem.getId(w)); - if (view) { - this._environmentViews.delete(view.id); - views.push(view); - } - }); - this._treeDataChanged.fire(views); - } else if (p) { - const view = this._projectViews.get(ProjectItem.getId(p)); - if (view) { - this._environmentViews.delete(view.id); - this._treeDataChanged.fire(view); - } - } else { - this._projectViews.clear(); - this._environmentViews.clear(); - this._treeDataChanged.fire(undefined); - } + updateProject(): void { + this._treeDataChanged.fire(undefined); } private updatePackagesForEnvironment(e: PythonEnvironment): void { const views: ProjectTreeItem[] = []; - this._viewsPackageRoots.forEach((v) => { + this.packageRoots.forEach((v) => { if (v.environment.envId.id === e.envId.id) { views.push(v); } @@ -92,18 +89,31 @@ export class WorkspaceView implements TreeDataProvider { this._treeDataChanged.fire(views); } - async reveal(uri: Uri): Promise { + private revealInternal(view: ProjectEnvironment): void { if (this.treeView.visible) { - const pw = this.projectManager.get(uri); + setImmediate(async () => { + await this.treeView.reveal(view); + }); + } + } + + reveal(context: Uri | PythonEnvironment): PythonEnvironment | undefined { + if (context instanceof Uri) { + const pw = this.projectManager.get(context); if (pw) { - const view = this._environmentViews.get(ProjectItem.getId(pw)); + const view = this.revealMap.get(pw.uri.fsPath); if (view) { - await this.treeView.reveal(view); + this.revealInternal(view); + return view.environment; } - return view?.environment; + } + } else { + const view = Array.from(this.revealMap.values()).find((v) => v.environment.envId.id === context.envId.id); + if (view) { + this.revealInternal(view); + return view.environment; } } - return undefined; } @@ -116,11 +126,11 @@ export class WorkspaceView implements TreeDataProvider { async getChildren(element?: ProjectTreeItem | undefined): Promise { if (element === undefined) { + this.projectViews.clear(); const views: ProjectTreeItem[] = []; this.projectManager.getProjects().forEach((w) => { - const id = ProjectItem.getId(w); - const view = this._projectViews.get(id) ?? new ProjectItem(w); - this._projectViews.set(ProjectItem.getId(w), view); + const view = new ProjectItem(w); + this.projectViews.set(w.uri.fsPath, view); views.push(view); }); @@ -129,23 +139,43 @@ export class WorkspaceView implements TreeDataProvider { if (element.kind === ProjectTreeItemKind.project) { const projectItem = element as ProjectItem; - const envView = this._environmentViews.get(projectItem.id); + if (this.envManagers.managers.length === 0) { + return [ + new NoProjectEnvironment( + projectItem.project, + projectItem, + 'Waiting for environment managers to load', + undefined, + undefined, + '$(loading~spin)', + ), + ]; + } const manager = this.envManagers.getEnvironmentManager(projectItem.project.uri); - const environment = await manager?.get(projectItem.project.uri); - if (!manager || !environment) { - this._environmentViews.delete(projectItem.id); - return [new NoProjectEnvironment(projectItem.project, projectItem)]; + if (!manager) { + return [ + new NoProjectEnvironment( + projectItem.project, + projectItem, + 'Environment manager not found', + 'Install an environment manager to get started. If you have installed then it might be loading.', + ), + ]; } - const envItemId = ProjectEnvironment.getId(projectItem, environment); - if (envView && envView.id === envItemId) { - return [envView]; + const environment = await manager?.get(projectItem.project.uri); + if (!environment) { + return [ + new NoProjectEnvironment( + projectItem.project, + projectItem, + `No environment provided by ${manager.displayName}`, + ), + ]; } - - this._environmentViews.delete(projectItem.id); const view = new ProjectEnvironment(projectItem, environment); - this._environmentViews.set(projectItem.id, view); + this.revealMap.set(projectItem.project.uri.fsPath, view); return [view]; } @@ -159,7 +189,7 @@ export class WorkspaceView implements TreeDataProvider { if (pkgManager) { const item = new ProjectPackageRootTreeItem(environmentItem, pkgManager, environment); - this._viewsPackageRoots.set(environment.envId.id, item); + this.packageRoots.set(environmentItem.parent.project.uri.fsPath, item); views.push(item); } else { views.push(new ProjectEnvironmentInfo(environmentItem, 'No package manager found')); diff --git a/src/features/views/pythonStatusBar.ts b/src/features/views/pythonStatusBar.ts new file mode 100644 index 00000000..6f027544 --- /dev/null +++ b/src/features/views/pythonStatusBar.ts @@ -0,0 +1,35 @@ +import { Disposable, StatusBarAlignment, StatusBarItem, ThemeColor } from 'vscode'; +import { createStatusBarItem } from '../../common/window.apis'; + +export interface PythonStatusBar extends Disposable { + show(text?: string): void; + hide(): void; +} + +export class PythonStatusBarImpl implements Disposable { + private disposables: Disposable[] = []; + private readonly statusBarItem: StatusBarItem; + constructor() { + this.statusBarItem = createStatusBarItem('python.interpreterDisplay', StatusBarAlignment.Right, 100); + this.statusBarItem.command = 'python-envs.set'; + this.statusBarItem.name = 'Python Interpreter'; + this.statusBarItem.tooltip = 'Select Python Interpreter'; + this.statusBarItem.text = '$(loading~spin)'; + this.statusBarItem.show(); + this.disposables.push(this.statusBarItem); + } + + public show(text?: string) { + this.statusBarItem.text = text ?? 'Select Python Interpreter'; + this.statusBarItem.backgroundColor = text ? undefined : new ThemeColor('statusBarItem.warningBackground'); + this.statusBarItem.show(); + } + + public hide() { + this.statusBarItem.hide(); + } + + dispose() { + this.disposables.forEach((d) => d.dispose()); + } +} diff --git a/src/features/views/revealHandler.ts b/src/features/views/revealHandler.ts new file mode 100644 index 00000000..eed9ef0e --- /dev/null +++ b/src/features/views/revealHandler.ts @@ -0,0 +1,39 @@ +import { activeTextEditor } from '../../common/window.apis'; +import { WorkspaceView } from './projectView'; +import { EnvManagerView } from './envManagersView'; +import { PythonStatusBar } from './pythonStatusBar'; +import { isPythonProjectFile } from '../../common/utils/fileNameUtils'; +import { PythonEnvironmentApi } from '../../api'; + +export function updateViewsAndStatus( + statusBar: PythonStatusBar, + workspaceView: WorkspaceView, + managerView: EnvManagerView, + api: PythonEnvironmentApi, +) { + const activeDocument = activeTextEditor()?.document; + if (!activeDocument || activeDocument.isUntitled || activeDocument.uri.scheme !== 'file') { + statusBar.hide(); + return; + } + + if ( + activeDocument.languageId !== 'python' && + activeDocument.languageId !== 'pip-requirements' && + !isPythonProjectFile(activeDocument.uri.fsPath) + ) { + statusBar.hide(); + return; + } + + const env = workspaceView.reveal(activeDocument.uri); + managerView.reveal(env); + if (env) { + statusBar.show(env?.displayName); + } else { + setImmediate(async () => { + const e = await api.getEnvironment(activeDocument.uri); + statusBar.show(e?.displayName); + }); + } +} diff --git a/src/features/views/treeViewItems.ts b/src/features/views/treeViewItems.ts index 01881f8a..ba1c45df 100644 --- a/src/features/views/treeViewItems.ts +++ b/src/features/views/treeViewItems.ts @@ -259,13 +259,14 @@ export class NoProjectEnvironment implements ProjectTreeItem { constructor( public readonly project: PythonProject, public readonly parent: ProjectItem, + private readonly label: string, private readonly description?: string, private readonly tooltip?: string | MarkdownString, private readonly iconPath?: string | IconPath, ) { const randomStr1 = Math.random().toString(36).substring(2); this.id = `${this.parent.id}>>>none>>>${randomStr1}`; - const item = new TreeItem('Please select an environment', TreeItemCollapsibleState.None); + const item = new TreeItem(this.label, TreeItemCollapsibleState.None); item.contextValue = 'no-environment'; item.description = this.description; item.tooltip = this.tooltip; diff --git a/src/managers/sysPython/main.ts b/src/managers/sysPython/main.ts index b4bb1c25..4727b276 100644 --- a/src/managers/sysPython/main.ts +++ b/src/managers/sysPython/main.ts @@ -7,6 +7,8 @@ import { getPythonApi } from '../../features/pythonApi'; import { NativePythonFinder } from '../common/nativePythonFinder'; import { UvProjectCreator } from './uvProjectCreator'; import { isUvInstalled } from './utils'; +import { createFileSystemWatcher, onDidDeleteFiles } from '../../common/workspace.apis'; +import { createSimpleDebounce } from '../../common/utils/debounce'; export async function registerSystemPythonFeatures( nativeFinder: NativePythonFinder, @@ -24,6 +26,23 @@ export async function registerSystemPythonFeatures( api.registerEnvironmentManager(venvManager), ); + const venvDebouncedRefresh = createSimpleDebounce(500, () => { + venvManager.refresh(undefined); + }); + const watcher = createFileSystemWatcher('{**/pyenv.cfg,**/bin/python,**/python.exe}', false, true, false); + disposables.push( + watcher, + watcher.onDidCreate(() => { + venvDebouncedRefresh.trigger(); + }), + watcher.onDidDelete(() => { + venvDebouncedRefresh.trigger(); + }), + onDidDeleteFiles(() => { + venvDebouncedRefresh.trigger(); + }), + ); + setImmediate(async () => { if (await isUvInstalled(log)) { disposables.push(api.registerPythonProjectCreator(new UvProjectCreator(api, log))); diff --git a/src/managers/sysPython/sysPythonManager.ts b/src/managers/sysPython/sysPythonManager.ts index 43441725..65b64505 100644 --- a/src/managers/sysPython/sysPythonManager.ts +++ b/src/managers/sysPython/sysPythonManager.ts @@ -154,8 +154,8 @@ export class SysPythonManager implements EnvironmentManager { async resolve(context: ResolveEnvironmentContext): Promise { if (context instanceof Uri) { - // NOTE: `environmentPath` for envs in `this.collection` for venv always points to the python - // executable in the venv. This is set when we create the PythonEnvironment object. + // NOTE: `environmentPath` for envs in `this.collection` for system envs always points to the python + // executable. This is set when we create the PythonEnvironment object. const found = this.findEnvironmentByPath(context.fsPath); if (found) { // If it is in the collection, then it is a venv, and it should already be fully resolved. @@ -182,6 +182,10 @@ export class SysPythonManager implements EnvironmentManager { if (resolved) { // This is just like finding a new environment or creating a new one. // Add it to collection, and trigger the added event. + + // For all other env types we need to ensure that the environment is of the type managed by the manager. + // But System is a exception, this is the last resort for resolving. So we don't need to check. + // We will just add it and treat it as a non-activatable environment. this.collection.push(resolved); this._onDidChangeEnvironments.fire([{ environment: resolved, kind: EnvironmentChangeKind.add }]); } @@ -247,7 +251,7 @@ export class SysPythonManager implements EnvironmentManager { const env = await getSystemEnvForWorkspace(p); if (env) { - const found = this.findEnvironmentByPath(p); + const found = this.findEnvironmentByPath(env); if (found) { this.fsPathToEnv.set(p, found); @@ -256,7 +260,7 @@ export class SysPythonManager implements EnvironmentManager { const resolved = await resolveSystemPythonEnvironmentPath(env, this.nativeFinder, this.api, this); if (resolved) { - // If resolved add it to the collection + // If resolved add it to the collection. this.fsPathToEnv.set(p, resolved); this.collection.push(resolved); } else { diff --git a/src/managers/sysPython/venvManager.ts b/src/managers/sysPython/venvManager.ts index 7472f6fb..638a9a6b 100644 --- a/src/managers/sysPython/venvManager.ts +++ b/src/managers/sysPython/venvManager.ts @@ -29,7 +29,7 @@ import { } from './venvUtils'; import * as path from 'path'; import { NativePythonFinder } from '../common/nativePythonFinder'; -import { EXTENSION_ROOT_DIR } from '../../common/constants'; +import { ENVS_EXTENSION_ID, EXTENSION_ROOT_DIR } from '../../common/constants'; import { createDeferred, Deferred } from '../../common/utils/deferred'; import { getLatest, sortEnvironments } from '../common/utils'; @@ -100,8 +100,7 @@ export class VenvManager implements EnvironmentManager { const globals = await this.baseManager.getEnvironments('global'); const environment = await createPythonVenv(this.nativeFinder, this.api, this.log, this, globals, venvRoot); if (environment) { - this.collection.push(environment); - this._onDidChangeEnvironments.fire([{ environment, kind: EnvironmentChangeKind.add }]); + this.addEnvironment(environment, true); } return environment; } @@ -263,13 +262,41 @@ export class VenvManager implements EnvironmentManager { this.baseManager, ); if (resolved) { - // This is just like finding a new environment or creating a new one. - // Add it to collection, and trigger the added event. - this.collection.push(resolved); - this._onDidChangeEnvironments.fire([{ environment: resolved, kind: EnvironmentChangeKind.add }]); + if (resolved.envId.managerId === `${ENVS_EXTENSION_ID}:venv`) { + // This is just like finding a new environment or creating a new one. + // Add it to collection, and trigger the added event. + this.addEnvironment(resolved, true); + + // We should only return the resolved env if it is a venv. + // Fall through an return undefined if it is not a venv + return resolved; + } + } + + return undefined; + } + + private addEnvironment(environment: PythonEnvironment, raiseEvent?: boolean): void { + if (this.collection.find((e) => e.envId.id === environment.envId.id)) { + return; } - return resolved; + const oldEnv = this.findEnvironmentByPath(environment.environmentPath.fsPath); + if (oldEnv) { + this.collection = this.collection.filter((e) => e.envId.id !== oldEnv.envId.id); + this.collection.push(environment); + if (raiseEvent) { + this._onDidChangeEnvironments.fire([ + { environment: oldEnv, kind: EnvironmentChangeKind.remove }, + { environment, kind: EnvironmentChangeKind.add }, + ]); + } + } else { + this.collection.push(environment); + if (raiseEvent) { + this._onDidChangeEnvironments.fire([{ environment, kind: EnvironmentChangeKind.add }]); + } + } } private async loadEnvMap() { @@ -295,7 +322,7 @@ export class VenvManager implements EnvironmentManager { // If the environment is resolved, add it to the collection if (this.globalEnv) { - this.collection.push(this.globalEnv); + this.addEnvironment(this.globalEnv, false); } } } @@ -307,6 +334,7 @@ export class VenvManager implements EnvironmentManager { const sorted = sortEnvironments(this.collection); const paths = this.api.getPythonProjects().map((p) => path.normalize(p.uri.fsPath)); + const events: (() => void)[] = []; for (const p of paths) { const env = await getVenvForWorkspace(p); @@ -317,7 +345,9 @@ export class VenvManager implements EnvironmentManager { if (found) { this.fsPathToEnv.set(p, found); if (pw && previous?.envId.id !== found.envId.id) { - this._onDidChangeEnvironment.fire({ uri: pw.uri, old: undefined, new: found }); + events.push(() => + this._onDidChangeEnvironment.fire({ uri: pw.uri, old: undefined, new: found }), + ); } } else { const resolved = await resolveVenvPythonEnvironmentPath( @@ -330,9 +360,11 @@ export class VenvManager implements EnvironmentManager { if (resolved) { // If resolved add it to the collection this.fsPathToEnv.set(p, resolved); - this.collection.push(resolved); + this.addEnvironment(resolved, false); if (pw && previous?.envId.id !== resolved.envId.id) { - this._onDidChangeEnvironment.fire({ uri: pw.uri, old: undefined, new: resolved }); + events.push(() => + this._onDidChangeEnvironment.fire({ uri: pw.uri, old: undefined, new: resolved }), + ); } } else { this.log.error(`Failed to resolve python environment: ${env}`); @@ -355,6 +387,8 @@ export class VenvManager implements EnvironmentManager { } } } + + events.forEach((e) => e()); } private findEnvironmentByPath(fsPath: string, collection?: PythonEnvironment[]): PythonEnvironment | undefined { diff --git a/src/managers/sysPython/venvUtils.ts b/src/managers/sysPython/venvUtils.ts index ed923b69..0173b86e 100644 --- a/src/managers/sysPython/venvUtils.ts +++ b/src/managers/sysPython/venvUtils.ts @@ -34,6 +34,7 @@ import { showOpenDialog, } from '../../common/window.apis'; import { showErrorMessage } from '../../common/errors/utils'; +import { getPackagesToInstallFromInstallable } from '../../common/pickers/packages'; export const VENV_WORKSPACE_KEY = `${ENVS_EXTENSION_ID}:venv:WORKSPACE_SELECTED`; export const VENV_GLOBAL_KEY = `${ENVS_EXTENSION_ID}:venv:GLOBAL_SELECTED`; @@ -53,7 +54,11 @@ export async function getVenvForWorkspace(fsPath: string): Promise { const state = await getWorkspacePersistentState(); - return await state.get(VENV_GLOBAL_KEY); + const envPath: string | undefined = await state.get(VENV_GLOBAL_KEY); + if (envPath && (await fsapi.pathExists(envPath))) { + return envPath; + } + return undefined; } export async function setVenvForGlobal(envPath: string | undefined): Promise { @@ -277,6 +286,18 @@ export async function createPythonVenv( const pythonPath = os.platform() === 'win32' ? path.join(envPath, 'Scripts', 'python.exe') : path.join(envPath, 'bin', 'python'); + const project = api.getPythonProject(venvRoot); + const installable = await getProjectInstallable(api, project ? [project] : undefined); + + let packages: string[] = []; + if (installable && installable.length > 0) { + const packagesToInstall = await getPackagesToInstallFromInstallable(installable); + if (!packagesToInstall) { + return; + } + packages = packagesToInstall; + } + return await withProgress( { location: ProgressLocation.Notification, @@ -318,7 +339,7 @@ export async function createPythonVenv( version: resolved.version, description: pythonPath, environmentPath: Uri.file(pythonPath), - iconPath: Uri.file(path.join(EXTENSION_ROOT_DIR, 'files', 'logo.svg')), + iconPath: Uri.file(path.join(EXTENSION_ROOT_DIR, 'files', '__icon__.py')), sysPrefix: resolved.prefix, execInfo: { run: { @@ -330,6 +351,10 @@ export async function createPythonVenv( manager, ); log.info(`Created venv environment: ${name}`); + + if (packages?.length > 0) { + await api.installPackages(env, packages, { upgrade: false }); + } return env; } else { throw new Error('Could not resolve the virtual environment'); @@ -430,7 +455,7 @@ export async function getProjectInstallable( progress.report({ message: 'Searching for Requirements and TOML files' }); const results: Uri[] = ( await Promise.all([ - findFiles('**/requirements*.txt', exclude, undefined, token), + findFiles('**/*requirements*.txt', exclude, undefined, token), findFiles('**/requirements/*.txt', exclude, undefined, token), findFiles('**/pyproject.toml', exclude, undefined, token), ])