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
9 changes: 8 additions & 1 deletion src/common/localize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -136,6 +137,13 @@ export namespace SysManagerStrings {
export const packageRefreshError = l10n.t('Error refreshing packages');
}

export namespace PixiStrings {
export const pixiExtensionRecommendation = l10n.t(
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image confirming @karthiknadig you are good with this as the notif

'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');
Expand Down Expand Up @@ -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');
Expand Down
6 changes: 4 additions & 2 deletions src/common/utils/pythonPath.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,19 @@ 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);
if (aIndex === -1 && bIndex === -1) {
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;
});
Expand Down
85 changes: 64 additions & 21 deletions src/managers/builtin/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -99,6 +106,38 @@ function getPythonInfo(env: NativeEnvInfo): PythonEnvironmentInfo {
}
}

async function recommendPixiExtension(): Promise<void> {
if (pixiRecommendationShown) {
return;
}
pixiRecommendationShown = true;

if (getExtension(PIXI_EXTENSION_ID)) {
return;
}

const state = await getGlobalPersistentState();
const dontAsk = await state.get<boolean>(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,
Expand All @@ -109,24 +148,28 @@ export async function refreshPythons(
): Promise<PythonEnvironment[]> {
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);
Expand Down
6 changes: 3 additions & 3 deletions src/managers/builtin/uvPythonInstaller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions src/managers/common/nativePythonFinder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
8 changes: 4 additions & 4 deletions src/test/managers/builtin/uvPythonInstaller.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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',
);
Expand All @@ -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',
);
Expand All @@ -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);

Expand Down