diff --git a/src/common/localize.ts b/src/common/localize.ts index dd7c637f..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 { @@ -136,6 +137,13 @@ 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 namespace CondaStrings { export const condaManager = l10n.t('Manages Conda environments'); export const condaDiscovering = l10n.t('Discovering Conda environments'); @@ -240,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/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..8f628f50 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 { Common, 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, + Common.dontAskAgain, + ); + + if (result === PixiStrings.install) { + traceInfo(`Installing extension: ${PIXI_EXTENSION_ID}`); + await installExtension(PIXI_EXTENSION_ID); + } 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'); + } +} + 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/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/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', 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);