Skip to content

Commit 771f9c3

Browse files
authored
feat: add Pixi extension recommendation and support for Pixi environments (#1291)
fixes #1252
1 parent f8d3243 commit 771f9c3

File tree

7 files changed

+89
-32
lines changed

7 files changed

+89
-32
lines changed

src/common/localize.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export namespace Common {
1515
export const ok = l10n.t('Ok');
1616
export const quickCreate = l10n.t('Quick Create');
1717
export const installPython = l10n.t('Install Python');
18+
export const dontAskAgain = l10n.t("Don't ask again");
1819
}
1920

2021
export namespace WorkbenchStrings {
@@ -136,6 +137,13 @@ export namespace SysManagerStrings {
136137
export const packageRefreshError = l10n.t('Error refreshing packages');
137138
}
138139

140+
export namespace PixiStrings {
141+
export const pixiExtensionRecommendation = l10n.t(
142+
'Pixi environments were detected. Install the Pixi extension for full support including activation and environment management.',
143+
);
144+
export const install = l10n.t('Open on Marketplace');
145+
}
146+
139147
export namespace CondaStrings {
140148
export const condaManager = l10n.t('Manages Conda environments');
141149
export const condaDiscovering = l10n.t('Discovering Conda environments');
@@ -240,7 +248,6 @@ export namespace UvInstallStrings {
240248
export const uvInstallRestartRequired = l10n.t(
241249
'uv was installed but may not be available in the current terminal. Please restart VS Code or open a new terminal and try again.',
242250
);
243-
export const dontAskAgain = l10n.t("Don't ask again");
244251
export const clickToInstallPython = l10n.t('No Python found, click to install');
245252
export const selectPythonVersion = l10n.t('Select Python version to install');
246253
export const installed = l10n.t('installed');

src/common/utils/pythonPath.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,19 @@ const priorityOrder = [
1616
`${PYTHON_EXTENSION_ID}:system`,
1717
];
1818
function sortManagersByPriority(managers: InternalEnvironmentManager[]): InternalEnvironmentManager[] {
19+
const systemId = priorityOrder[priorityOrder.length - 1];
1920
return managers.sort((a, b) => {
2021
const aIndex = priorityOrder.indexOf(a.id);
2122
const bIndex = priorityOrder.indexOf(b.id);
2223
if (aIndex === -1 && bIndex === -1) {
2324
return 0;
2425
}
2526
if (aIndex === -1) {
26-
return 1;
27+
// Unknown managers should come before system (last resort) but after other known managers
28+
return b.id === systemId ? -1 : 1;
2729
}
2830
if (bIndex === -1) {
29-
return -1;
31+
return a.id === systemId ? 1 : -1;
3032
}
3133
return aIndex - bIndex;
3234
});

src/common/workbenchCommands.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,7 @@ export async function installExtension(
1010
): Promise<void> {
1111
await commands.executeCommand('workbench.extensions.installExtension', extensionId, options);
1212
}
13+
14+
export async function openExtension(extensionId: string): Promise<void> {
15+
await commands.executeCommand('extension.open', extensionId);
16+
}

src/managers/builtin/utils.ts

Lines changed: 65 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,12 @@ import {
99
PythonEnvironmentInfo,
1010
} from '../../api';
1111
import { showErrorMessageWithLogs } from '../../common/errors/utils';
12-
import { SysManagerStrings } from '../../common/localize';
13-
import { traceVerbose } from '../../common/logging';
14-
import { withProgress } from '../../common/window.apis';
12+
import { getExtension } from '../../common/extension.apis';
13+
import { Common, PixiStrings, SysManagerStrings } from '../../common/localize';
14+
import { traceInfo, traceVerbose } from '../../common/logging';
15+
import { getGlobalPersistentState } from '../../common/persistentState';
16+
import { showInformationMessage, withProgress } from '../../common/window.apis';
17+
import { openExtension } from '../../common/workbenchCommands';
1518
import {
1619
isNativeEnvInfo,
1720
NativeEnvInfo,
@@ -22,6 +25,10 @@ import { shortVersion, sortEnvironments } from '../common/utils';
2225
import { runPython, runUV, shouldUseUv } from './helpers';
2326
import { parsePipList, PipPackage } from './pipListUtils';
2427

28+
const PIXI_EXTENSION_ID = 'renan-r-santos.pixi-code';
29+
const PIXI_RECOMMEND_DONT_ASK_KEY = 'pixi-extension-recommend-dont-ask';
30+
let pixiRecommendationShown = false;
31+
2532
function asPackageQuickPickItem(name: string, version?: string): QuickPickItem {
2633
return {
2734
label: name,
@@ -99,6 +106,38 @@ function getPythonInfo(env: NativeEnvInfo): PythonEnvironmentInfo {
99106
}
100107
}
101108

109+
async function recommendPixiExtension(): Promise<void> {
110+
if (pixiRecommendationShown) {
111+
return;
112+
}
113+
pixiRecommendationShown = true;
114+
115+
if (getExtension(PIXI_EXTENSION_ID)) {
116+
return;
117+
}
118+
119+
const state = await getGlobalPersistentState();
120+
const dontAsk = await state.get<boolean>(PIXI_RECOMMEND_DONT_ASK_KEY);
121+
if (dontAsk) {
122+
traceInfo('Skipping Pixi extension recommendation: user selected "Don\'t ask again"');
123+
return;
124+
}
125+
126+
const result = await showInformationMessage(
127+
PixiStrings.pixiExtensionRecommendation,
128+
PixiStrings.install,
129+
Common.dontAskAgain,
130+
);
131+
132+
if (result === PixiStrings.install) {
133+
traceInfo(`Opening extension page: ${PIXI_EXTENSION_ID}`);
134+
await openExtension(PIXI_EXTENSION_ID);
135+
} else if (result === Common.dontAskAgain) {
136+
await state.set(PIXI_RECOMMEND_DONT_ASK_KEY, true);
137+
traceInfo('User selected "Don\'t ask again" for Pixi extension recommendation');
138+
}
139+
}
140+
102141
export async function refreshPythons(
103142
hardRefresh: boolean,
104143
nativeFinder: NativePythonFinder,
@@ -109,25 +148,29 @@ export async function refreshPythons(
109148
): Promise<PythonEnvironment[]> {
110149
const collection: PythonEnvironment[] = [];
111150
const data = await nativeFinder.refresh(hardRefresh, uris);
112-
const envs = data
113-
.filter((e) => isNativeEnvInfo(e))
114-
.map((e) => e as NativeEnvInfo)
115-
.filter(
116-
(e) =>
117-
e.kind === undefined ||
118-
(e.kind &&
119-
[
120-
NativePythonEnvironmentKind.globalPaths,
121-
NativePythonEnvironmentKind.homebrew,
122-
NativePythonEnvironmentKind.linuxGlobal,
123-
NativePythonEnvironmentKind.macCommandLineTools,
124-
NativePythonEnvironmentKind.macPythonOrg,
125-
NativePythonEnvironmentKind.macXCode,
126-
NativePythonEnvironmentKind.windowsRegistry,
127-
NativePythonEnvironmentKind.windowsStore,
128-
NativePythonEnvironmentKind.winpython,
129-
].includes(e.kind)),
130-
);
151+
const allNativeEnvs = data.filter((e) => isNativeEnvInfo(e)).map((e) => e as NativeEnvInfo);
152+
153+
const hasPixiEnvs = allNativeEnvs.some((e) => e.kind === NativePythonEnvironmentKind.pixi);
154+
if (hasPixiEnvs) {
155+
recommendPixiExtension().catch((e) => log.error('Error recommending Pixi extension', e));
156+
}
157+
158+
const envs = allNativeEnvs.filter(
159+
(e) =>
160+
e.kind === undefined ||
161+
(e.kind &&
162+
[
163+
NativePythonEnvironmentKind.globalPaths,
164+
NativePythonEnvironmentKind.homebrew,
165+
NativePythonEnvironmentKind.linuxGlobal,
166+
NativePythonEnvironmentKind.macCommandLineTools,
167+
NativePythonEnvironmentKind.macPythonOrg,
168+
NativePythonEnvironmentKind.macXCode,
169+
NativePythonEnvironmentKind.windowsRegistry,
170+
NativePythonEnvironmentKind.windowsStore,
171+
NativePythonEnvironmentKind.winpython,
172+
].includes(e.kind)),
173+
);
131174
envs.forEach((env) => {
132175
try {
133176
const envInfo = getPythonInfo(env);

src/managers/builtin/uvPythonInstaller.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
TaskScope,
1010
} from 'vscode';
1111
import { spawnProcess } from '../../common/childProcess.apis';
12-
import { UvInstallStrings } from '../../common/localize';
12+
import { Common, UvInstallStrings } from '../../common/localize';
1313
import { traceError, traceInfo, traceLog } from '../../common/logging';
1414
import { getGlobalPersistentState } from '../../common/persistentState';
1515
import { executeTask, onDidEndTaskProcess } from '../../common/tasks.apis';
@@ -350,10 +350,10 @@ export async function promptInstallPythonViaUv(
350350
promptMessage,
351351
{ modal: true },
352352
UvInstallStrings.installPython,
353-
UvInstallStrings.dontAskAgain,
353+
Common.dontAskAgain,
354354
);
355355

356-
if (result === UvInstallStrings.dontAskAgain) {
356+
if (result === Common.dontAskAgain) {
357357
await state.set(UV_INSTALL_PYTHON_DONT_ASK_KEY, true);
358358
traceLog('User selected "Don\'t ask again" for Python install prompt');
359359
return undefined;

src/managers/common/nativePythonFinder.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ export function isNativeEnvInfo(info: NativeInfo): info is NativeEnvInfo {
126126
export enum NativePythonEnvironmentKind {
127127
conda = 'Conda',
128128
homebrew = 'Homebrew',
129+
pixi = 'Pixi',
129130
pyenv = 'Pyenv',
130131
globalPaths = 'GlobalPaths',
131132
pyenvVirtualEnv = 'PyenvVirtualEnv',

src/test/managers/builtin/uvPythonInstaller.unit.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import assert from 'assert';
22
import * as sinon from 'sinon';
33
import { LogOutputChannel } from 'vscode';
44
import * as childProcessApis from '../../../common/childProcess.apis';
5-
import { UvInstallStrings } from '../../../common/localize';
5+
import { Common, UvInstallStrings } from '../../../common/localize';
66
import * as persistentState from '../../../common/persistentState';
77
import { EventNames } from '../../../common/telemetry/constants';
88
import * as telemetrySender from '../../../common/telemetry/sender';
@@ -67,7 +67,7 @@ suite('uvPythonInstaller - promptInstallPythonViaUv', () => {
6767
UvInstallStrings.installPythonPrompt,
6868
{ modal: true },
6969
UvInstallStrings.installPython,
70-
UvInstallStrings.dontAskAgain,
70+
Common.dontAskAgain,
7171
),
7272
'Should show install Python prompt when uv is installed',
7373
);
@@ -85,7 +85,7 @@ suite('uvPythonInstaller - promptInstallPythonViaUv', () => {
8585
UvInstallStrings.installPythonAndUvPrompt,
8686
{ modal: true },
8787
UvInstallStrings.installPython,
88-
UvInstallStrings.dontAskAgain,
88+
Common.dontAskAgain,
8989
),
9090
'Should show install Python AND uv prompt when uv is not installed',
9191
);
@@ -94,7 +94,7 @@ suite('uvPythonInstaller - promptInstallPythonViaUv', () => {
9494
test('should set persistent state when user clicks "Don\'t ask again"', async () => {
9595
mockState.get.resolves(false);
9696
isUvInstalledStub.resolves(true);
97-
showInformationMessageStub.resolves(UvInstallStrings.dontAskAgain);
97+
showInformationMessageStub.resolves(Common.dontAskAgain);
9898

9999
const result = await promptInstallPythonViaUv('activation', mockLog);
100100

0 commit comments

Comments
 (0)