From ac0c93cee7f728af973b7c5bfd1f05910aee7a16 Mon Sep 17 00:00:00 2001 From: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Fri, 27 Feb 2026 08:18:09 -0800 Subject: [PATCH 1/2] feat: add Pixi extension recommendation and support for Pixi environments --- src/common/localize.ts | 8 +++ src/common/utils/pythonPath.ts | 6 +- src/managers/builtin/utils.ts | 85 +++++++++++++++++------ src/managers/common/nativePythonFinder.ts | 1 + 4 files changed, 77 insertions(+), 23 deletions(-) diff --git a/src/common/localize.ts b/src/common/localize.ts index dd7c637f..ae171b1d 100644 --- a/src/common/localize.ts +++ b/src/common/localize.ts @@ -136,6 +136,14 @@ export namespace SysManagerStrings { export const packageRefreshError = l10n.t('Error refreshing packages'); } +export namespace PixiStrings { + export const pixiExtensionRecommendation = l10n.t( + 'Pixi environments were detected. Install the Pixi extension for full support including activation and environment management.', + ); + export const install = l10n.t('Install Pixi Extension'); + export const dontAskAgain = l10n.t("Don't Ask Again"); +} + export namespace CondaStrings { export const condaManager = l10n.t('Manages Conda environments'); export const condaDiscovering = l10n.t('Discovering Conda environments'); diff --git a/src/common/utils/pythonPath.ts b/src/common/utils/pythonPath.ts index b3cf96d5..c85055ed 100644 --- a/src/common/utils/pythonPath.ts +++ b/src/common/utils/pythonPath.ts @@ -16,6 +16,7 @@ const priorityOrder = [ `${PYTHON_EXTENSION_ID}:system`, ]; function sortManagersByPriority(managers: InternalEnvironmentManager[]): InternalEnvironmentManager[] { + const systemId = priorityOrder[priorityOrder.length - 1]; return managers.sort((a, b) => { const aIndex = priorityOrder.indexOf(a.id); const bIndex = priorityOrder.indexOf(b.id); @@ -23,10 +24,11 @@ function sortManagersByPriority(managers: InternalEnvironmentManager[]): Interna return 0; } if (aIndex === -1) { - return 1; + // Unknown managers should come before system (last resort) but after other known managers + return b.id === systemId ? -1 : 1; } if (bIndex === -1) { - return -1; + return a.id === systemId ? 1 : -1; } return aIndex - bIndex; }); diff --git a/src/managers/builtin/utils.ts b/src/managers/builtin/utils.ts index a43bdd14..8b645cce 100644 --- a/src/managers/builtin/utils.ts +++ b/src/managers/builtin/utils.ts @@ -9,9 +9,12 @@ import { PythonEnvironmentInfo, } from '../../api'; import { showErrorMessageWithLogs } from '../../common/errors/utils'; -import { SysManagerStrings } from '../../common/localize'; -import { traceVerbose } from '../../common/logging'; -import { withProgress } from '../../common/window.apis'; +import { getExtension } from '../../common/extension.apis'; +import { PixiStrings, SysManagerStrings } from '../../common/localize'; +import { traceInfo, traceVerbose } from '../../common/logging'; +import { getGlobalPersistentState } from '../../common/persistentState'; +import { showInformationMessage, withProgress } from '../../common/window.apis'; +import { installExtension } from '../../common/workbenchCommands'; import { isNativeEnvInfo, NativeEnvInfo, @@ -22,6 +25,10 @@ import { shortVersion, sortEnvironments } from '../common/utils'; import { runPython, runUV, shouldUseUv } from './helpers'; import { parsePipList, PipPackage } from './pipListUtils'; +const PIXI_EXTENSION_ID = 'renan-r-santos.pixi-code'; +const PIXI_RECOMMEND_DONT_ASK_KEY = 'pixi-extension-recommend-dont-ask'; +let pixiRecommendationShown = false; + function asPackageQuickPickItem(name: string, version?: string): QuickPickItem { return { label: name, @@ -99,6 +106,38 @@ function getPythonInfo(env: NativeEnvInfo): PythonEnvironmentInfo { } } +async function recommendPixiExtension(): Promise { + if (pixiRecommendationShown) { + return; + } + pixiRecommendationShown = true; + + if (getExtension(PIXI_EXTENSION_ID)) { + return; + } + + const state = await getGlobalPersistentState(); + const dontAsk = await state.get(PIXI_RECOMMEND_DONT_ASK_KEY); + if (dontAsk) { + traceInfo('Skipping Pixi extension recommendation: user selected "Don\'t ask again"'); + return; + } + + const result = await showInformationMessage( + PixiStrings.pixiExtensionRecommendation, + PixiStrings.install, + PixiStrings.dontAskAgain, + ); + + if (result === PixiStrings.install) { + traceInfo(`Installing extension: ${PIXI_EXTENSION_ID}`); + await installExtension(PIXI_EXTENSION_ID); + } else if (result === PixiStrings.dontAskAgain) { + await state.set(PIXI_RECOMMEND_DONT_ASK_KEY, true); + traceInfo('User selected "Don\'t ask again" for Pixi extension recommendation'); + } +} + export async function refreshPythons( hardRefresh: boolean, nativeFinder: NativePythonFinder, @@ -109,24 +148,28 @@ export async function refreshPythons( ): Promise { const collection: PythonEnvironment[] = []; const data = await nativeFinder.refresh(hardRefresh, uris); - const envs = data - .filter((e) => isNativeEnvInfo(e)) - .map((e) => e as NativeEnvInfo) - .filter( - (e) => - e.kind === undefined || - (e.kind && - [ - NativePythonEnvironmentKind.globalPaths, - NativePythonEnvironmentKind.homebrew, - NativePythonEnvironmentKind.linuxGlobal, - NativePythonEnvironmentKind.macCommandLineTools, - NativePythonEnvironmentKind.macPythonOrg, - NativePythonEnvironmentKind.macXCode, - NativePythonEnvironmentKind.windowsRegistry, - NativePythonEnvironmentKind.windowsStore, - ].includes(e.kind)), - ); + const allNativeEnvs = data.filter((e) => isNativeEnvInfo(e)).map((e) => e as NativeEnvInfo); + + const hasPixiEnvs = allNativeEnvs.some((e) => e.kind === NativePythonEnvironmentKind.pixi); + if (hasPixiEnvs) { + recommendPixiExtension().catch((e) => log.error('Error recommending Pixi extension', e)); + } + + const envs = allNativeEnvs.filter( + (e) => + e.kind === undefined || + (e.kind && + [ + NativePythonEnvironmentKind.globalPaths, + NativePythonEnvironmentKind.homebrew, + NativePythonEnvironmentKind.linuxGlobal, + NativePythonEnvironmentKind.macCommandLineTools, + NativePythonEnvironmentKind.macPythonOrg, + NativePythonEnvironmentKind.macXCode, + NativePythonEnvironmentKind.windowsRegistry, + NativePythonEnvironmentKind.windowsStore, + ].includes(e.kind)), + ); envs.forEach((env) => { try { const envInfo = getPythonInfo(env); diff --git a/src/managers/common/nativePythonFinder.ts b/src/managers/common/nativePythonFinder.ts index 33f690e7..2aae8475 100644 --- a/src/managers/common/nativePythonFinder.ts +++ b/src/managers/common/nativePythonFinder.ts @@ -124,6 +124,7 @@ export function isNativeEnvInfo(info: NativeInfo): info is NativeEnvInfo { export enum NativePythonEnvironmentKind { conda = 'Conda', homebrew = 'Homebrew', + pixi = 'Pixi', pyenv = 'Pyenv', globalPaths = 'GlobalPaths', pyenvVirtualEnv = 'PyenvVirtualEnv', From 0b84741c712c75cdfa6a38e0bb12e25809614dc9 Mon Sep 17 00:00:00 2001 From: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Fri, 27 Feb 2026 08:33:26 -0800 Subject: [PATCH 2/2] create common string for don't ask again --- src/common/localize.ts | 3 +-- src/managers/builtin/utils.ts | 6 +++--- src/managers/builtin/uvPythonInstaller.ts | 6 +++--- src/test/managers/builtin/uvPythonInstaller.unit.test.ts | 8 ++++---- 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/common/localize.ts b/src/common/localize.ts index ae171b1d..e33c805c 100644 --- a/src/common/localize.ts +++ b/src/common/localize.ts @@ -15,6 +15,7 @@ export namespace Common { export const ok = l10n.t('Ok'); export const quickCreate = l10n.t('Quick Create'); export const installPython = l10n.t('Install Python'); + export const dontAskAgain = l10n.t("Don't ask again"); } export namespace WorkbenchStrings { @@ -141,7 +142,6 @@ export namespace PixiStrings { 'Pixi environments were detected. Install the Pixi extension for full support including activation and environment management.', ); export const install = l10n.t('Install Pixi Extension'); - export const dontAskAgain = l10n.t("Don't Ask Again"); } export namespace CondaStrings { @@ -248,7 +248,6 @@ export namespace UvInstallStrings { export const uvInstallRestartRequired = l10n.t( 'uv was installed but may not be available in the current terminal. Please restart VS Code or open a new terminal and try again.', ); - export const dontAskAgain = l10n.t("Don't ask again"); export const clickToInstallPython = l10n.t('No Python found, click to install'); export const selectPythonVersion = l10n.t('Select Python version to install'); export const installed = l10n.t('installed'); diff --git a/src/managers/builtin/utils.ts b/src/managers/builtin/utils.ts index 8b645cce..8f628f50 100644 --- a/src/managers/builtin/utils.ts +++ b/src/managers/builtin/utils.ts @@ -10,7 +10,7 @@ import { } from '../../api'; import { showErrorMessageWithLogs } from '../../common/errors/utils'; import { getExtension } from '../../common/extension.apis'; -import { PixiStrings, SysManagerStrings } from '../../common/localize'; +import { Common, PixiStrings, SysManagerStrings } from '../../common/localize'; import { traceInfo, traceVerbose } from '../../common/logging'; import { getGlobalPersistentState } from '../../common/persistentState'; import { showInformationMessage, withProgress } from '../../common/window.apis'; @@ -126,13 +126,13 @@ async function recommendPixiExtension(): Promise { const result = await showInformationMessage( PixiStrings.pixiExtensionRecommendation, PixiStrings.install, - PixiStrings.dontAskAgain, + Common.dontAskAgain, ); if (result === PixiStrings.install) { traceInfo(`Installing extension: ${PIXI_EXTENSION_ID}`); await installExtension(PIXI_EXTENSION_ID); - } else if (result === PixiStrings.dontAskAgain) { + } else if (result === Common.dontAskAgain) { await state.set(PIXI_RECOMMEND_DONT_ASK_KEY, true); traceInfo('User selected "Don\'t ask again" for Pixi extension recommendation'); } diff --git a/src/managers/builtin/uvPythonInstaller.ts b/src/managers/builtin/uvPythonInstaller.ts index 00c99d8f..9b1fcdcc 100644 --- a/src/managers/builtin/uvPythonInstaller.ts +++ b/src/managers/builtin/uvPythonInstaller.ts @@ -9,7 +9,7 @@ import { TaskScope, } from 'vscode'; import { spawnProcess } from '../../common/childProcess.apis'; -import { UvInstallStrings } from '../../common/localize'; +import { Common, UvInstallStrings } from '../../common/localize'; import { traceError, traceInfo, traceLog } from '../../common/logging'; import { getGlobalPersistentState } from '../../common/persistentState'; import { executeTask, onDidEndTaskProcess } from '../../common/tasks.apis'; @@ -350,10 +350,10 @@ export async function promptInstallPythonViaUv( promptMessage, { modal: true }, UvInstallStrings.installPython, - UvInstallStrings.dontAskAgain, + Common.dontAskAgain, ); - if (result === UvInstallStrings.dontAskAgain) { + if (result === Common.dontAskAgain) { await state.set(UV_INSTALL_PYTHON_DONT_ASK_KEY, true); traceLog('User selected "Don\'t ask again" for Python install prompt'); return undefined; diff --git a/src/test/managers/builtin/uvPythonInstaller.unit.test.ts b/src/test/managers/builtin/uvPythonInstaller.unit.test.ts index 310f6962..147eeb4e 100644 --- a/src/test/managers/builtin/uvPythonInstaller.unit.test.ts +++ b/src/test/managers/builtin/uvPythonInstaller.unit.test.ts @@ -2,7 +2,7 @@ import assert from 'assert'; import * as sinon from 'sinon'; import { LogOutputChannel } from 'vscode'; import * as childProcessApis from '../../../common/childProcess.apis'; -import { UvInstallStrings } from '../../../common/localize'; +import { Common, UvInstallStrings } from '../../../common/localize'; import * as persistentState from '../../../common/persistentState'; import { EventNames } from '../../../common/telemetry/constants'; import * as telemetrySender from '../../../common/telemetry/sender'; @@ -67,7 +67,7 @@ suite('uvPythonInstaller - promptInstallPythonViaUv', () => { UvInstallStrings.installPythonPrompt, { modal: true }, UvInstallStrings.installPython, - UvInstallStrings.dontAskAgain, + Common.dontAskAgain, ), 'Should show install Python prompt when uv is installed', ); @@ -85,7 +85,7 @@ suite('uvPythonInstaller - promptInstallPythonViaUv', () => { UvInstallStrings.installPythonAndUvPrompt, { modal: true }, UvInstallStrings.installPython, - UvInstallStrings.dontAskAgain, + Common.dontAskAgain, ), 'Should show install Python AND uv prompt when uv is not installed', ); @@ -94,7 +94,7 @@ suite('uvPythonInstaller - promptInstallPythonViaUv', () => { test('should set persistent state when user clicks "Don\'t ask again"', async () => { mockState.get.resolves(false); isUvInstalledStub.resolves(true); - showInformationMessageStub.resolves(UvInstallStrings.dontAskAgain); + showInformationMessageStub.resolves(Common.dontAskAgain); const result = await promptInstallPythonViaUv('activation', mockLog);