From c08793fad243f070bd02c9d5718153135469e051 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Wed, 4 Feb 2026 08:48:12 -0800 Subject: [PATCH] feat: add "Enter Interpreter Path" option for selecting Python interpreter --- src/common/localize.ts | 2 + src/common/pickers/managers.ts | 35 +++++++++++-- src/features/envCommands.ts | 92 ++++++++++++++++++++++++++++++++-- 3 files changed, 123 insertions(+), 6 deletions(-) diff --git a/src/common/localize.ts b/src/common/localize.ts index fd9a8843..7191a184 100644 --- a/src/common/localize.ts +++ b/src/common/localize.ts @@ -25,6 +25,8 @@ export namespace Interpreter { export const statusBarSelect = l10n.t('Select Interpreter'); export const browsePath = l10n.t('Browse...'); export const createVirtualEnvironment = l10n.t('Create Virtual Environment...'); + export const enterInterpreterPath = l10n.t('Enter Interpreter Path...'); + export const enterInterpreterPathDescription = l10n.t('Browse and select a Python interpreter from anywhere'); } export namespace PackageManagement { diff --git a/src/common/pickers/managers.ts b/src/common/pickers/managers.ts index ad37e1e4..1a323b8c 100644 --- a/src/common/pickers/managers.ts +++ b/src/common/pickers/managers.ts @@ -1,9 +1,19 @@ -import { commands, QuickInputButtons, QuickPickItem, QuickPickItemKind, workspace, WorkspaceFolder } from 'vscode'; +import { + commands, + QuickInputButtons, + QuickPickItem, + QuickPickItemKind, + ThemeIcon, + workspace, + WorkspaceFolder, +} from 'vscode'; import { PythonProjectCreator } from '../../api'; import { InternalEnvironmentManager, InternalPackageManager } from '../../internal.api'; -import { Common, Pickers } from '../localize'; +import { Common, Interpreter, Pickers } from '../localize'; import { showQuickPickWithButtons } from '../window.apis'; +export const ENTER_INTERPRETER_PATH_ID = 'EnterInterpreterPath'; + function getDescription(mgr: InternalEnvironmentManager | InternalPackageManager): string | undefined { if (mgr.description) { return mgr.description; @@ -22,17 +32,19 @@ export async function pickEnvironmentManager( managers: InternalEnvironmentManager[], defaultManagers?: InternalEnvironmentManager[], showBackButton?: boolean, + showEnterInterpreterPath?: boolean, ): Promise { if (managers.length === 0) { return; } - if (managers.length === 1 && !managers[0].supportsQuickCreate) { + if (managers.length === 1 && !managers[0].supportsQuickCreate && !showEnterInterpreterPath) { // If there's only one manager and it doesn't support quick create, return its ID directly. return managers[0].id; } const items: (QuickPickItem | (QuickPickItem & { id: string }))[] = []; + if (defaultManagers && defaultManagers.length > 0) { items.push({ label: Common.recommended, @@ -71,6 +83,23 @@ export async function pickEnvironmentManager( id: m.id, })), ); + + // Add "Enter Interpreter Path" option at the bottom if enabled + if (showEnterInterpreterPath) { + items.push( + { + label: '', + kind: QuickPickItemKind.Separator, + }, + { + label: Interpreter.enterInterpreterPath, + description: Interpreter.enterInterpreterPathDescription, + id: ENTER_INTERPRETER_PATH_ID, + iconPath: new ThemeIcon('folder-opened'), + }, + ); + } + const item = await showQuickPickWithButtons(items, { placeHolder: Pickers.Managers.selectEnvironmentManager, ignoreFocusOut: true, diff --git a/src/features/envCommands.ts b/src/features/envCommands.ts index fe02e022..f74727bb 100644 --- a/src/features/envCommands.ts +++ b/src/features/envCommands.ts @@ -1,6 +1,15 @@ import * as fs from 'fs-extra'; import * as path from 'path'; -import { commands, QuickInputButtons, TaskExecution, TaskRevealKind, Terminal, Uri, workspace } from 'vscode'; +import { + commands, + ProgressLocation, + QuickInputButtons, + TaskExecution, + TaskRevealKind, + Terminal, + Uri, + workspace, +} from 'vscode'; import { CreateEnvironmentOptions, PythonEnvironment, @@ -21,15 +30,25 @@ import { removePythonProjectSetting, setEnvironmentManager, setPackageManager } import { clipboardWriteText } from '../common/env.apis'; import {} from '../common/errors/utils'; +import { Pickers } from '../common/localize'; import { pickEnvironment } from '../common/pickers/environments'; import { + ENTER_INTERPRETER_PATH_ID, pickCreator, pickEnvironmentManager, pickPackageManager, pickWorkspaceFolder, } from '../common/pickers/managers'; import { pickProject, pickProjectMany } from '../common/pickers/projects'; -import { activeTextEditor, showErrorMessage, showInformationMessage } from '../common/window.apis'; +import { isWindows } from '../common/utils/platformUtils'; +import { handlePythonPath } from '../common/utils/pythonPath'; +import { + activeTextEditor, + showErrorMessage, + showInformationMessage, + showOpenDialog, + withProgress, +} from '../common/window.apis'; import { runAsTask } from './execution/runAsTask'; import { runInTerminal } from './terminal/runInTerminal'; import { TerminalManager } from './terminal/terminalManager'; @@ -44,6 +63,46 @@ import { PythonEnvTreeItem, } from './views/treeViewItems'; +/** + * Opens a file dialog to browse for a Python interpreter and resolves it using available managers. + */ +async function browseAndResolveInterpreter( + em: EnvironmentManagers, + projectUris?: Uri[], +): Promise { + const filters = isWindows() ? { python: ['exe'] } : undefined; + const uris = await showOpenDialog({ + canSelectFiles: true, + canSelectFolders: false, + canSelectMany: false, + filters, + title: Pickers.Environments.selectExecutable, + }); + if (!uris || uris.length === 0) { + return; + } + const interpreterUri = uris[0]; + + // Get project-specific managers if projectUris are provided + const projectEnvManagers = projectUris + ? projectUris + .map((uri) => em.getEnvironmentManager(uri)) + .filter((m): m is InternalEnvironmentManager => m !== undefined) + : []; + + const environment = await withProgress( + { + location: ProgressLocation.Notification, + cancellable: false, + }, + async (reporter, token) => { + return await handlePythonPath(interpreterUri, em.managers, projectEnvManagers, reporter, token); + }, + ); + + return environment; +} + export async function refreshManagerCommand(context: unknown): Promise { if (context instanceof EnvManagerTreeItem) { const manager = (context as EnvManagerTreeItem).manager; @@ -133,7 +192,22 @@ export async function createAnyEnvironmentCommand( const select = options?.selectEnvironment; const projects = pm.getProjects(options?.uri ? [options?.uri] : undefined); if (projects.length === 0) { - const managerId = await pickEnvironmentManager(em.managers.filter((m) => m.supportsCreate)); + const managerId = await pickEnvironmentManager( + em.managers.filter((m) => m.supportsCreate), + undefined, + undefined, + true, // showEnterInterpreterPath + ); + + // Handle "Enter Interpreter Path" selection + if (managerId === ENTER_INTERPRETER_PATH_ID) { + const env = await browseAndResolveInterpreter(em); + if (select && env) { + await em.setEnvironments('global', env); + } + return env; + } + const manager = em.managers.find((m) => m.id === managerId); if (manager) { const env = await manager.create('global', { ...options }); @@ -165,7 +239,19 @@ export async function createAnyEnvironmentCommand( em.managers.filter((m) => m.supportsCreate), defaultManagers, options?.showBackButton, + true, // showEnterInterpreterPath ); + + // Handle "Enter Interpreter Path" selection + if (managerId === ENTER_INTERPRETER_PATH_ID) { + const projectUris = selected.map((p) => p.uri); + const env = await browseAndResolveInterpreter(em, projectUris); + if (select && env) { + await em.setEnvironments(projectUris, env); + } + return env; + } + if (managerId?.startsWith('QuickCreate#')) { quickCreate = true; managerId = managerId.replace('QuickCreate#', '');