Skip to content
Merged
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
2 changes: 2 additions & 0 deletions .vscode-test.mjs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { defineConfig } from '@vscode/test-cli';
import { fileURLToPath } from 'node:url';

export default defineConfig({
files: 'out/test/**/*.vscode.test.js',
version: 'stable',
workspaceFolder: fileURLToPath(new URL('./test/fixtures/pixi-project/', import.meta.url)),
mocha: {
ui: 'tdd',
timeout: 20_000,
Expand Down
2 changes: 1 addition & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"name": "Run Extension",
"type": "extensionHost",
"request": "launch",
"args": ["--extensionDevelopmentPath=${workspaceFolder}"],
"args": ["${workspaceFolder}/test/fixtures/pixi-project", "--extensionDevelopmentPath=${workspaceFolder}"],
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
"preLaunchTask": "${defaultBuildTask}"
}
Expand Down
12 changes: 10 additions & 2 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
{
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "compile",
"problemMatcher": [],
"group": {
"kind": "build",
"isDefault": true
}
},
{
"type": "npm",
"script": "watch",
Expand All @@ -11,8 +20,7 @@
"group": "watchers"
},
"group": {
"kind": "build",
"isDefault": true
"kind": "build"
}
},
{
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@
"typecheck": "tsc -p . --noEmit",
"compile-tests": "node -e \"require('fs').rmSync('out', { recursive: true, force: true })\" && tsc -p . --outDir out",
"test:unit": "npm run compile-tests && node --test out/test/**/*.unit.test.js",
"test:vscode": "npm run compile-tests && npm run compile && vscode-test",
"test:vscode": "npm run compile-tests && npm run compile && pixi install --locked --manifest-path test/fixtures/pixi-project/pixi.toml && vscode-test",
"test": "npm run typecheck && npm run test:unit && npm run test:vscode",
"watch-tests": "tsc -p . -w --outDir out",
"pretest": "npm run lint && npm run format:check",
Expand Down
71 changes: 41 additions & 30 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ExtensionContext, window } from 'vscode';

import { registerLogger } from './common/logging';
import { setPersistentState } from './common/persistentState';
import { EXTENSION_ID } from './common/utils';
import { PixiEnvManager } from './pixi/envManager';
import { PixiPackageManager } from './pixi/projectManager';
import { getPixi, runPixi } from './pixi/utils';
Expand All @@ -11,38 +12,48 @@ import { getEnvExtApi } from './pythonEnvsApi';
const MINIMUM_PIXI_VERSION = '0.53.0';

export async function activate(context: ExtensionContext) {
const api = await getEnvExtApi();

const log = window.createOutputChannel('Pixi Environment Manager', { log: true });
context.subscriptions.push(log, registerLogger(log));

// Validate Pixi installation
const stdout = await runPixi(['--version']);
const versionMatch = stdout.trim().match(/^pixi (\d+\.\d+\.\d+)/);
if (!versionMatch) {
const errorMsg = `Found invalid Pixi binary at ${await getPixi()}.`;
window.showErrorMessage(errorMsg);
throw new Error(errorMsg);
}

const currentVersion = versionMatch[1];

// Check if the current version meets the minimum requirement using semver
if (!semver.gte(currentVersion, MINIMUM_PIXI_VERSION)) {
const errorMsg =
`Pixi version ${currentVersion} is too old. ` +
`This extension requires Pixi version ${MINIMUM_PIXI_VERSION} or newer. ` +
`Please update Pixi by running: pixi self-update`;
window.showErrorMessage(errorMsg);
throw new Error(errorMsg);
try {
log.info('Activating Pixi Code');
const api = await getEnvExtApi();

// Validate Pixi installation
const stdout = await runPixi(['--version']);
const versionMatch = stdout.trim().match(/^pixi (\d+\.\d+\.\d+)/);
if (!versionMatch) {
const errorMsg = `Found invalid Pixi binary at ${await getPixi()}.`;
window.showErrorMessage(errorMsg);
throw new Error(errorMsg);
}

const currentVersion = versionMatch[1];

// Check if the current version meets the minimum requirement using semver
if (!semver.gte(currentVersion, MINIMUM_PIXI_VERSION)) {
const errorMsg =
`Pixi version ${currentVersion} is too old. ` +
`This extension requires Pixi version ${MINIMUM_PIXI_VERSION} or newer. ` +
`Please update Pixi by running: pixi self-update`;
window.showErrorMessage(errorMsg);
throw new Error(errorMsg);
}

log.info(`Using Pixi ${currentVersion}`);

// Setup the persistent state for the extension.
setPersistentState(context);

const manager = new PixiEnvManager(api, log);
context.subscriptions.push(api.registerEnvironmentManager(manager, { extensionId: EXTENSION_ID }));
log.info('Registered Pixi environment manager');

const packageManager = new PixiPackageManager(api, log);
context.subscriptions.push(api.registerPackageManager(packageManager, { extensionId: EXTENSION_ID }));
log.info('Registered Pixi package manager');
} catch (error) {
log.error('Pixi Code activation failed', error);
throw error;
}

// Setup the persistent state for the extension.
setPersistentState(context);

const manager = new PixiEnvManager(api, log);
context.subscriptions.push(api.registerEnvironmentManager(manager));

const packageManager = new PixiPackageManager(api, log);
context.subscriptions.push(api.registerPackageManager(packageManager));
}
57 changes: 57 additions & 0 deletions src/test/extension.vscode.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,68 @@
import * as assert from 'node:assert/strict';
import { readFile, rm, writeFile } from 'node:fs/promises';
import * as path from 'node:path';

import { PythonEnvironmentApi } from '@vscode/python-environments';
import * as vscode from 'vscode';

import { PIXI_MANAGER_ID } from '../common/utils';

suite('Pixi Code extension', () => {
test('contributes an empty default Pixi executable setting', () => {
const setting = vscode.workspace.getConfiguration('pixi-code').inspect<string>('pixiExecutable');

assert.equal(setting?.defaultValue, '');
});

test('selects the Pixi default environment through Python Environments', async function () {
this.timeout(120_000);

const pixiExtension = vscode.extensions.getExtension('renan-r-santos.pixi-code');
const pythonEnvsExtension =
vscode.extensions.getExtension<PythonEnvironmentApi>('ms-python.vscode-python-envs');
assert.ok(pixiExtension, 'Pixi Code must be loaded in the extension test host');
assert.ok(pythonEnvsExtension, 'Python Environments must be installed in the extension test host');

const api = await pythonEnvsExtension.activate();
await pixiExtension.activate();

const environments = await api.getEnvironments('all');
const pixiEnvironment = environments.find(
(environment) =>
environment.envId.managerId === PIXI_MANAGER_ID && environment.name.startsWith('pixi-project:default'),
);
assert.ok(pixiEnvironment, 'The Pixi fixture default environment must be discoverable');

const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
assert.ok(workspaceFolder, 'The Pixi fixture must be open as the test workspace');
const scope = workspaceFolder.uri;
const settingsPath = path.join(scope.fsPath, '.vscode', 'settings.json');
const originalSettings = await readFile(settingsPath).catch((error: NodeJS.ErrnoException) => {
if (error.code === 'ENOENT') {
return undefined;
}
throw error;
});
const previousEnvironment = await api.getEnvironment(scope);
try {
await api.setEnvironment(scope, pixiEnvironment);
const selectedEnvironment = await api.getEnvironment(scope);

assert.equal(
selectedEnvironment?.envId.id,
pixiEnvironment.envId.id,
'Python Environments did not select the Pixi interpreter; check its output for a missing manager',
);
} finally {
try {
await api.setEnvironment(scope, previousEnvironment);
} finally {
if (originalSettings) {
await writeFile(settingsPath, originalSettings);
} else {
await rm(settingsPath, { force: true });
}
}
}
});
});
2 changes: 2 additions & 0 deletions test/fixtures/pixi-project/.gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# SCM syntax highlighting & preventing 3-way merges
pixi.lock merge=binary linguist-language=YAML linguist-generated=true -diff
3 changes: 3 additions & 0 deletions test/fixtures/pixi-project/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# pixi environments
.pixi/*
!.pixi/config.toml
2 changes: 2 additions & 0 deletions test/fixtures/pixi-project/hello_world.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
if __name__ == "__main__":
print("Hello, World!")
Loading
Loading