diff --git a/.editorconfig b/.editorconfig index ecb0a87..731a625 100644 --- a/.editorconfig +++ b/.editorconfig @@ -7,6 +7,6 @@ charset = utf-8 indent_style = tab indent_size = 4 -[*.{yml,yaml}] +[*.{yml,yaml,md}] indent_style = space indent_size = 2 diff --git a/README.md b/README.md index 0e50d90..449b8e6 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ To make use of it, make sure your user settings contain `"python.useEnvironments - List all configured [Hatch environments] - Provide controls to set them as active environment for your project, activate them in a terminal, and delete them from disk - Temporarily modify an environment’s packages using the configured [`installer`] +- Define a `hatch.envInterpreter` command for use in `launch.json` or `tasks.json`, see [below](#commands) ![screenshot](./screenshot.png) @@ -18,6 +19,40 @@ Persistent modifications to the installed packages should be done by editing Hat [`installer`]: https://hatch.pypa.io/latest/how-to/environment/select-installer/ +## Commands +- `hatch.envInterpreter`: not an interactive command, but rather for use in `launch.json` or `tasks.json` via [variable substitution], e.g. for `command` in `tasks.json` or `python` in `launch.json`: + + ```jsonc + { // tasks.json + "version": "0.2.0", + "tasks": [ + { + "label": "Build docs", + "type": "process", + "command": "${input:docsInterpreter}", + "args": ["-m", "sphinx", "docs", "docs/_build"], + "problemMatcher": [], + }, + ], + "inputs": [ + { + "id": "docsInterpreter", + "type": "command", + "command": "hatch.envInterpreter", + "args": { "env": "docs" }, + }, + ], + } + ``` + + The command supports the following `args`: + - `env`: name of the environment (defaults to `"default"`) + - `workspace`: path to the workspace root (defaults to the first currently open workspace) + + It can be used without going through `inputs` using just `${command:hatch.envInterpreter}` to always use the `default` environment instead of the currently active one. + +[variable substitution]: https://code.visualstudio.com/docs/reference/variables-reference + ## Extension Settings - `hatch.executable`: path to the `hatch` executable (supports `~` expansion). Defaults to the output of `which hatch`. diff --git a/package.json b/package.json index 5ba3c28..b7e413a 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,13 @@ } } }, + "commands": [ + { + "command": "hatch.envInterpreter", + "title": "Hatch: Get an environment’s interpreter path", + "enablement": false + } + ], "icons": { "hatch-logo": { "description": "Hatch Logo", diff --git a/src/commands.ts b/src/commands.ts new file mode 100644 index 0000000..a562bf0 --- /dev/null +++ b/src/commands.ts @@ -0,0 +1,33 @@ +import { Uri, workspace } from 'vscode' +import type { HatchEnvManager } from './hatch-env-manager.js' + +export interface CommandOptions { + /** environment name */ + env?: string | undefined + /** workspace directory */ + workspace?: string | undefined +} + +/** Command to get the interpreter for a given environment. + * + * Modeled after [`python.interpreterPath`]. + * + * [`python.interpreterPath`]: https://github.com/microsoft/vscode-python/blob/9ded8032f6a455289113026ed1dca4c5ed81e6e8/src/client/interpreter/interpreterPathCommand.ts + */ +export async function getEnvInterpreter( + envManager: HatchEnvManager, + { env: envName = 'default', workspace: wsDir }: CommandOptions = {}, +): Promise { + const workspaceUri = wsDir + ? Uri.file(wsDir) + : workspace.workspaceFolders?.[0]?.uri + if (!workspaceUri) throw new Error('No workspace open') + await envManager.refresh(workspaceUri) + const envs = await envManager.getEnvironments(workspaceUri) + const env = envs.find((e) => e.name === envName) + if (!env) + throw new Error( + `Environment “${envName}” not found in workspace “${workspaceUri.fsPath}”`, + ) + return env.execInfo.run.executable +} diff --git a/src/common/constants.ts b/src/common/constants.ts index 1ab58ed..3de1c72 100644 --- a/src/common/constants.ts +++ b/src/common/constants.ts @@ -8,3 +8,5 @@ export const EXE_CONFIG_SECTION = 'hatch' // dotted section name export const EXE_CONFIG_SETTING = 'executable' // last element export const ENVS_EXT_ID = 'ms-python.vscode-python-envs' + +export const CMD_ENV_INTERPRETER = 'hatch.envInterpreter' diff --git a/src/extension.ts b/src/extension.ts index cc866cb..b575f13 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,6 +1,7 @@ -import { type ExtensionContext, window } from 'vscode' +import { commands, type ExtensionContext, window } from 'vscode' import { HatchExecutableTracker } from './cli/index.js' -import { EXTENSION_ID } from './common/constants.js' +import { type CommandOptions, getEnvInterpreter } from './commands.js' +import { CMD_ENV_INTERPRETER, EXTENSION_ID } from './common/constants.js' import { registerLogger } from './common/logging.js' import { setWorkspacePersistentState } from './common/persistent-state.js' import { HatchEnvManager } from './hatch-env-manager.js' @@ -28,6 +29,9 @@ export async function activate(context: ExtensionContext): Promise { extensionId: EXTENSION_ID, }), api.registerPackageManager(pkgManager, { extensionId: EXTENSION_ID }), + commands.registerCommand(CMD_ENV_INTERPRETER, (args?: CommandOptions) => + getEnvInterpreter(envManager, args), + ), ) return { diff --git a/test/extension.test.ts b/test/extension.test.ts index c256ca8..1ee40f0 100644 --- a/test/extension.test.ts +++ b/test/extension.test.ts @@ -5,7 +5,12 @@ import type { } from '@vscode/python-environments' import { before, beforeEach } from 'mocha' import * as vscode from 'vscode' -import { ENVS_EXT_ID, EXTENSION_ID } from '../src/common/constants' +import type { CommandOptions } from '../src/commands' +import { + CMD_ENV_INTERPRETER, + ENVS_EXT_ID, + EXTENSION_ID, +} from '../src/common/constants' import type * as extension from '../src/extension' import MockExec from './mock-exec' import { tmpdir, waitForCondition } from './test-utils' @@ -69,14 +74,18 @@ describe('Env Manager', () => { envManager = ext.envManager }) - it('should return environments', async () => { - await using dir = await tmpdir('hatch-') + async function testProj(): ReturnType { + const dir = await tmpdir('hatch-') api.addPythonProject({ name: 'test', uri: dir.uri }) - exec.reset( [['env', 'show', '--json'], { mockenv: { type: 'virtual' } }], [['env', 'find', 'mockenv'], 'mockpath\n'], ) + return dir + } + + it('should return environments', async () => { + await using dir = await testProj() //This gets called automatically: await envManager.refresh(dir.uri) const envs = await envManager.getEnvironments(dir.uri) @@ -84,4 +93,13 @@ describe('Env Manager', () => { assert.equal(envs[0].name, 'mockenv') assert.equal(envs[0].sysPrefix, 'mockpath') }) + + it('should implement an env interpreter path command', async () => { + await using _ = await testProj() + //This gets called automatically: await envManager.refresh(dir.uri) + const intp = await vscode.commands.executeCommand(CMD_ENV_INTERPRETER, { + env: 'mockenv', + } satisfies CommandOptions) + assert.equal(intp, 'mockpath/bin/python') + }) })