├── .nvmrc ├── files ├── templates │ ├── newPackageTemplate │ │ ├── package_name │ │ │ ├── __init__.py │ │ │ └── __main__.py │ │ ├── tests │ │ │ └── test_package_name.py │ │ ├── dev-requirements.txt │ │ └── pyproject.toml │ ├── new723ScriptTemplate │ │ └── script.py │ └── copilot-instructions-text │ │ ├── script-copilot-instructions.md │ │ └── package-copilot-instructions.md └── logo.svg ├── icon.png ├── .gitignore ├── src ├── common │ ├── commands.ts │ ├── utils │ │ ├── platformUtils.ts │ │ ├── testing.ts │ │ ├── asyncUtils.ts │ │ ├── debounce.ts │ │ ├── internalVariables.ts │ │ ├── deferred.ts │ │ ├── fileNameUtils.ts │ │ ├── frameUtils.ts │ │ ├── pythonPath.ts │ │ └── pathUtils.ts │ ├── vscodeEnv.apis.ts │ ├── tasks.apis.ts │ ├── lm.apis.ts │ ├── errors │ │ ├── types.ts │ │ ├── NotSupportedError.ts │ │ ├── AlreadyRegisteredError.ts │ │ └── utils.ts │ ├── workspace.fs.apis.ts │ ├── env.apis.ts │ ├── stopWatch.ts │ ├── extension.apis.ts │ ├── workbenchCommands.ts │ ├── command.api.ts │ ├── extVersion.ts │ ├── telemetry │ │ ├── reporter.ts │ │ └── helpers.ts │ ├── constants.ts │ ├── childProcess.apis.ts │ ├── logging.ts │ ├── pickers │ │ └── projects.ts │ ├── workspace.apis.ts │ └── persistentState.ts ├── features │ ├── terminal │ │ ├── shells │ │ │ ├── cmd │ │ │ │ ├── cmdConstants.ts │ │ │ │ └── cmdEnvs.ts │ │ │ ├── fish │ │ │ │ ├── fishConstants.ts │ │ │ │ └── fishEnvs.ts │ │ │ ├── pwsh │ │ │ │ ├── pwshConstants.ts │ │ │ │ └── pwshEnvs.ts │ │ │ ├── bash │ │ │ │ ├── bashConstants.ts │ │ │ │ └── bashEnvs.ts │ │ │ ├── utils.ts │ │ │ ├── startupProvider.ts │ │ │ ├── providers.ts │ │ │ └── common │ │ │ │ └── editUtils.ts │ │ ├── activateMenuButton.ts │ │ ├── runInTerminal.ts │ │ └── shellStartupSetupHandlers.ts │ ├── common │ │ ├── shellConstants.ts │ │ └── activation.ts │ ├── views │ │ ├── utils.ts │ │ ├── revealHandler.ts │ │ └── pythonStatusBar.ts │ ├── creators │ │ └── projectCreators.ts │ ├── execution │ │ ├── execUtils.ts │ │ ├── envVarUtils.ts │ │ ├── runAsTask.ts │ │ ├── runInBackground.ts │ │ └── envVariableManager.ts │ └── settings │ │ └── settingCompletions.ts ├── test │ ├── constants.ts │ ├── mocks │ │ ├── vsc │ │ │ ├── telemetryReporter.ts │ │ │ ├── README.md │ │ │ ├── strings.ts │ │ │ ├── copilotTools.ts │ │ │ ├── htmlContent.ts │ │ │ ├── uuid.ts │ │ │ └── position.ts │ │ ├── mementos.ts │ │ ├── mockWorkspaceConfig.ts │ │ └── helper.ts │ ├── managers │ │ └── builtin │ │ │ ├── piplist3.actual.txt │ │ │ ├── piplist3.expected.json │ │ │ ├── piplist2.actual.txt │ │ │ ├── piplist1.actual.txt │ │ │ ├── piplist1.expected.json │ │ │ ├── piplist2.expected.json │ │ │ └── pipListUtils.unit.test.ts │ ├── common │ │ ├── internalVariables.unit.test.ts │ │ └── environmentPicker.unit.test.ts │ └── features │ │ ├── terminal │ │ └── shells │ │ │ └── common │ │ │ └── shellUtils.unit.test.ts │ │ ├── terminalEnvVarInjectorBasic.unit.test.ts │ │ └── common │ │ └── shellDetector.unit.test.ts ├── managers │ ├── builtin │ │ ├── pipListUtils.ts │ │ ├── uvEnvironments.ts │ │ ├── cache.ts │ │ └── main.ts │ ├── pyenv │ │ └── main.ts │ ├── pipenv │ │ └── main.ts │ ├── common │ │ └── types.ts │ ├── conda │ │ └── main.ts │ └── poetry │ │ └── main.ts ├── vscode.proposed.terminalDataWriteEvent.d.ts └── vscode.proposed.terminalShellEnv.d.ts ├── images ├── trust_relationships.png ├── python-envs-overview.gif ├── extension_relationships.png └── environment-managers-quick-start.png ├── .vscode-test.mjs ├── .vscode ├── extensions.json ├── settings.json ├── tasks.json └── launch.json ├── examples ├── sample1 │ ├── .vscodeignore │ ├── CHANGELOG.md │ ├── README.md │ ├── .vscode │ │ ├── extensions.json │ │ ├── launch.json │ │ ├── settings.json │ │ └── tasks.json │ ├── tsconfig.json │ ├── eslint.config.mjs │ ├── src │ │ ├── pythonEnvsApi.ts │ │ ├── extension.ts │ │ └── sampleEnvManager.ts │ ├── package.json │ └── webpack.config.js └── README.md ├── .vscodeignore ├── .prettierrc.js ├── CODE_OF_CONDUCT.md ├── .github ├── workflows │ ├── remove-needs-labels.yml │ ├── pr-labels.yml │ ├── issue-labels.yml │ ├── test_plan_item_validator.yml │ ├── info-needed-closer.yml │ ├── community-feedback-auto-comment.yml │ ├── pr-check.yml │ ├── push-check.yml │ └── triage-info-needed.yml ├── actions │ └── build-vsix │ │ └── action.yml ├── instructions │ ├── generic.instructions.md │ └── issue-format.instructions.md └── prompts │ └── add-telemetry.prompt.md ├── .eslintrc.json ├── tsconfig.json ├── SUPPORT.md ├── LICENSE ├── eslint.config.mjs ├── webpack.config.js ├── CONTRIBUTING.md └── SECURITY.md /.nvmrc: -------------------------------------------------------------------------------- 1 | 22.21.1 2 | -------------------------------------------------------------------------------- /files/templates/newPackageTemplate/package_name/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/vscode-python-environments/HEAD/icon.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | dist 3 | node_modules 4 | .vscode-test/ 5 | *.vsix 6 | .nox/ 7 | .venv/ 8 | **/__pycache__/ -------------------------------------------------------------------------------- /files/templates/newPackageTemplate/tests/test_package_name.py: -------------------------------------------------------------------------------- 1 | # TODO: Write tests for your package using pytest 2 | -------------------------------------------------------------------------------- /src/common/commands.ts: -------------------------------------------------------------------------------- 1 | export namespace Commands { 2 | export const viewLogs = 'python-envs.viewLogs'; 3 | } 4 | -------------------------------------------------------------------------------- /src/common/utils/platformUtils.ts: -------------------------------------------------------------------------------- 1 | export function isWindows(): boolean { 2 | return process.platform === 'win32'; 3 | } 4 | -------------------------------------------------------------------------------- /images/trust_relationships.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/vscode-python-environments/HEAD/images/trust_relationships.png -------------------------------------------------------------------------------- /src/common/utils/testing.ts: -------------------------------------------------------------------------------- 1 | export function isTestExecution(): boolean { 2 | return !!process.env.VSC_PYTHON_CI_TEST; 3 | } 4 | -------------------------------------------------------------------------------- /images/python-envs-overview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/vscode-python-environments/HEAD/images/python-envs-overview.gif -------------------------------------------------------------------------------- /images/extension_relationships.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/vscode-python-environments/HEAD/images/extension_relationships.png -------------------------------------------------------------------------------- /src/common/vscodeEnv.apis.ts: -------------------------------------------------------------------------------- 1 | import { env } from 'vscode'; 2 | 3 | export function vscodeShell(): string { 4 | return env.shell; 5 | } 6 | -------------------------------------------------------------------------------- /.vscode-test.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from '@vscode/test-cli'; 2 | 3 | export default defineConfig({ 4 | files: 'out/test/**/*.test.js', 5 | }); 6 | -------------------------------------------------------------------------------- /files/templates/newPackageTemplate/dev-requirements.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | setuptools 3 | wheel 4 | # TODO: update the necessary requirements to develop this package -------------------------------------------------------------------------------- /images/environment-managers-quick-start.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/vscode-python-environments/HEAD/images/environment-managers-quick-start.png -------------------------------------------------------------------------------- /src/features/terminal/shells/cmd/cmdConstants.ts: -------------------------------------------------------------------------------- 1 | export const CMD_ENV_KEY = 'VSCODE_PYTHON_CMD_ACTIVATE'; 2 | export const CMD_SCRIPT_VERSION = '0.1.0'; 3 | -------------------------------------------------------------------------------- /src/common/utils/asyncUtils.ts: -------------------------------------------------------------------------------- 1 | export async function timeout(milliseconds: number): Promise { 2 | return new Promise((resolve) => setTimeout(resolve, milliseconds)); 3 | } 4 | -------------------------------------------------------------------------------- /src/common/tasks.apis.ts: -------------------------------------------------------------------------------- 1 | import { Task, TaskExecution, tasks } from 'vscode'; 2 | 3 | export async function executeTask(task: Task): Promise { 4 | return tasks.executeTask(task); 5 | } 6 | -------------------------------------------------------------------------------- /src/test/constants.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | 3 | export const EXTENSION_ROOT = path.dirname(path.dirname(__dirname)); 4 | export const EXTENSION_TEST_ROOT = path.join(EXTENSION_ROOT, 'src', 'test'); 5 | -------------------------------------------------------------------------------- /src/common/lm.apis.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | export function registerTools(name: string, tool: vscode.LanguageModelTool): vscode.Disposable { 3 | return vscode.lm.registerTool(name, tool); 4 | } 5 | -------------------------------------------------------------------------------- /src/features/terminal/shells/fish/fishConstants.ts: -------------------------------------------------------------------------------- 1 | export const FISH_ENV_KEY = 'VSCODE_PYTHON_FISH_ACTIVATE'; 2 | export const FISH_OLD_ENV_KEY = 'VSCODE_FISH_ACTIVATE'; 3 | export const FISH_SCRIPT_VERSION = '0.1.1'; 4 | -------------------------------------------------------------------------------- /src/features/terminal/shells/pwsh/pwshConstants.ts: -------------------------------------------------------------------------------- 1 | export const POWERSHELL_ENV_KEY = 'VSCODE_PYTHON_PWSH_ACTIVATE'; 2 | export const POWERSHELL_OLD_ENV_KEY = 'VSCODE_PWSH_ACTIVATE'; 3 | export const PWSH_SCRIPT_VERSION = '0.1.1'; 4 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": ["dbaeumer.vscode-eslint", "amodio.tsl-problem-matcher", "esbenp.prettier-vscode"] 5 | } 6 | -------------------------------------------------------------------------------- /files/templates/newPackageTemplate/package_name/__main__.py: -------------------------------------------------------------------------------- 1 | # TODO: Update the main function to your needs or remove it. 2 | 3 | 4 | def main() -> None: 5 | print("Start coding in Python today!") 6 | 7 | 8 | if __name__ == "__main__": 9 | main() 10 | -------------------------------------------------------------------------------- /src/test/mocks/vsc/telemetryReporter.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | export class vscMockTelemetryReporter { 5 | public sendTelemetryEvent(): void { 6 | // Noop. 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /examples/sample1/.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | out/** 4 | node_modules/** 5 | src/** 6 | .gitignore 7 | .yarnrc 8 | webpack.config.js 9 | vsc-extension-quickstart.md 10 | **/tsconfig.json 11 | **/eslint.config.mjs 12 | **/*.map 13 | **/*.ts 14 | **/.vscode-test.* 15 | -------------------------------------------------------------------------------- /examples/sample1/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to the "sample1" extension will be documented in this file. 4 | 5 | Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file. 6 | 7 | ## [Unreleased] 8 | 9 | - Initial release -------------------------------------------------------------------------------- /src/common/errors/types.ts: -------------------------------------------------------------------------------- 1 | export type ErrorCategory = 'NotSupported' | 'InvalidArgument'; 2 | 3 | export abstract class BaseError extends Error { 4 | constructor(public readonly category: ErrorCategory, message: string) { 5 | super(message); 6 | this.name = this.constructor.name; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/common/workspace.fs.apis.ts: -------------------------------------------------------------------------------- 1 | import { FileStat, Uri, workspace } from 'vscode'; 2 | 3 | export function readFile(uri: Uri): Thenable { 4 | return workspace.fs.readFile(uri); 5 | } 6 | 7 | export function stat(uri: Uri): Thenable { 8 | return workspace.fs.stat(uri); 9 | } 10 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | out/** 4 | node_modules/** 5 | src/** 6 | .gitignore 7 | .vscode-test.mjs 8 | .yarnrc 9 | webpack.config.js 10 | vsc-extension-quickstart.md 11 | **/tsconfig.json 12 | **/.eslintrc.json 13 | **/*.map 14 | **/*.ts 15 | .nox/ 16 | .venv/ 17 | **/__pycache__/ 18 | examples/** 19 | -------------------------------------------------------------------------------- /src/features/terminal/shells/bash/bashConstants.ts: -------------------------------------------------------------------------------- 1 | export const BASH_ENV_KEY = 'VSCODE_PYTHON_BASH_ACTIVATE'; 2 | export const ZSH_ENV_KEY = 'VSCODE_PYTHON_ZSH_ACTIVATE'; 3 | export const BASH_OLD_ENV_KEY = 'VSCODE_BASH_ACTIVATE'; 4 | export const ZSH_OLD_ENV_KEY = 'VSCODE_ZSH_ACTIVATE'; 5 | export const BASH_SCRIPT_VERSION = '0.1.1'; 6 | -------------------------------------------------------------------------------- /src/common/env.apis.ts: -------------------------------------------------------------------------------- 1 | import { env, Uri } from 'vscode'; 2 | 3 | export function launchBrowser(uri: string | Uri): Thenable { 4 | return env.openExternal(uri instanceof Uri ? uri : Uri.parse(uri)); 5 | } 6 | 7 | export function clipboardWriteText(text: string): Thenable { 8 | return env.clipboard.writeText(text); 9 | } 10 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true, 3 | printWidth: 120, 4 | tabWidth: 4, 5 | endOfLine: 'auto', 6 | trailingComma: 'all', 7 | overrides: [ 8 | { 9 | files: ['*.yml', '*.yaml'], 10 | options: { 11 | tabWidth: 2 12 | } 13 | } 14 | ] 15 | }; 16 | -------------------------------------------------------------------------------- /examples/sample1/README.md: -------------------------------------------------------------------------------- 1 | ### Template example for Environment Manager 2 | 3 | This is a template for an environment manager extension. It demonstrates how to use the Python Environments API to register your custom environment manager. You can look at implementations for `venv`, and `conda` (here https://github.com/microsoft/vscode-python-environments/blob/main/src/managers) for more examples. 4 | -------------------------------------------------------------------------------- /src/common/stopWatch.ts: -------------------------------------------------------------------------------- 1 | export class StopWatch implements IStopWatch { 2 | private started = new Date().getTime(); 3 | public get elapsedTime() { 4 | return new Date().getTime() - this.started; 5 | } 6 | public reset() { 7 | this.started = new Date().getTime(); 8 | } 9 | } 10 | 11 | export interface IStopWatch { 12 | elapsedTime: number; 13 | } 14 | -------------------------------------------------------------------------------- /src/common/extension.apis.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import { Extension, extensions } from 'vscode'; 3 | 4 | export function getExtension(extensionId: string): Extension | undefined { 5 | return extensions.getExtension(extensionId); 6 | } 7 | 8 | export function allExtensions(): readonly Extension[] { 9 | return extensions.all; 10 | } 11 | -------------------------------------------------------------------------------- /examples/sample1/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "dbaeumer.vscode-eslint", 6 | "amodio.tsl-problem-matcher", 7 | "ms-vscode.extension-test-runner", 8 | "ms-python.vscode-python-envs", 9 | "esbenp.prettier-vscode" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /src/common/errors/NotSupportedError.ts: -------------------------------------------------------------------------------- 1 | import { BaseError } from './types'; 2 | 3 | export class CreateEnvironmentNotSupported extends BaseError { 4 | constructor(message: string) { 5 | super('NotSupported', message); 6 | } 7 | } 8 | 9 | export class RemoveEnvironmentNotSupported extends BaseError { 10 | constructor(message: string) { 11 | super('NotSupported', message); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/common/errors/AlreadyRegisteredError.ts: -------------------------------------------------------------------------------- 1 | import { BaseError } from './types'; 2 | 3 | export class EnvironmentManagerAlreadyRegisteredError extends BaseError { 4 | constructor(message: string) { 5 | super('InvalidArgument', message); 6 | } 7 | } 8 | 9 | export class PackageManagerAlreadyRegisteredError extends BaseError { 10 | constructor(message: string) { 11 | super('InvalidArgument', message); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/test/managers/builtin/piplist3.actual.txt: -------------------------------------------------------------------------------- 1 | Package Version 2 | ---------- ------- 3 | altgraph 0.17.2 4 | future 0.18.2 5 | macholib 1.15.2 6 | pip 21.2.4 7 | setuptools 58.0.4 8 | six 1.15.0 9 | wheel 0.37.0 10 | WARNING: You are using pip version 21.2.4; however, version 25.2 is available. 11 | You should consider upgrading via the '/Library/Developer/CommandLineTools/usr/bin/python3 -m pip install --upgrade pip' command. 12 | -------------------------------------------------------------------------------- /src/common/workbenchCommands.ts: -------------------------------------------------------------------------------- 1 | import { commands, Uri } from 'vscode'; 2 | 3 | export async function installExtension( 4 | extensionId: Uri | string, 5 | options?: { 6 | installOnlyNewlyAddedFromExtensionPackVSIX?: boolean; 7 | installPreReleaseVersion?: boolean; 8 | donotSync?: boolean; 9 | }, 10 | ): Promise { 11 | await commands.executeCommand('workbench.extensions.installExtension', extensionId, options); 12 | } 13 | -------------------------------------------------------------------------------- /files/templates/new723ScriptTemplate/script.py: -------------------------------------------------------------------------------- 1 | # /// script_name 2 | # requires-python = ">=X.XX" TODO: Update this to the minimum Python version you want to support 3 | # dependencies = [ 4 | # TODO: Add any dependencies your script requires 5 | # ] 6 | # /// 7 | 8 | # TODO: Update the main function to your needs or remove it. 9 | 10 | 11 | def main() -> None: 12 | print("Start coding in Python today!") 13 | 14 | 15 | if __name__ == "__main__": 16 | main() 17 | -------------------------------------------------------------------------------- /src/test/managers/builtin/piplist3.expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | { "name": "altgraph", "version": "0.17.2" }, 4 | { "name": "future", "version": "0.18.2" }, 5 | { "name": "macholib", "version": "1.15.2" }, 6 | { "name": "pip", "version": "21.2.4" }, 7 | { "name": "setuptools", "version": "58.0.4" }, 8 | { "name": "six", "version": "1.15.0" }, 9 | { "name": "wheel", "version": "0.37.0" } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /src/common/command.api.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import { commands } from 'vscode'; 3 | import { Disposable } from 'vscode-jsonrpc'; 4 | 5 | export function registerCommand(command: string, callback: (...args: any[]) => any, thisArg?: any): Disposable { 6 | return commands.registerCommand(command, callback, thisArg); 7 | } 8 | 9 | export function executeCommand(command: string, ...rest: any[]): Thenable { 10 | return commands.executeCommand(command, ...rest); 11 | } 12 | -------------------------------------------------------------------------------- /src/features/common/shellConstants.ts: -------------------------------------------------------------------------------- 1 | export namespace ShellConstants { 2 | export const PWSH = 'pwsh'; 3 | export const BASH = 'bash'; 4 | export const ZSH = 'zsh'; 5 | export const FISH = 'fish'; 6 | export const CMD = 'cmd'; 7 | export const SH = 'sh'; 8 | export const NU = 'nu'; 9 | export const GITBASH = 'gitbash'; 10 | export const WSL = 'wsl'; 11 | export const CSH = 'csh'; 12 | export const TCSH = 'tcsh'; 13 | export const KSH = 'ksh'; 14 | export const XONSH = 'xonsh'; 15 | } 16 | -------------------------------------------------------------------------------- /src/features/views/utils.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import { PythonProject } from '../../api'; 3 | import { getWorkspaceFolder } from '../../common/workspace.apis'; 4 | 5 | export function removable(project: PythonProject): boolean { 6 | const workspace = getWorkspaceFolder(project.uri); 7 | if (workspace) { 8 | // If the project path is same as the workspace path, then we cannot remove the project. 9 | return path.normalize(workspace?.uri.fsPath).toLowerCase() !== path.normalize(project.uri.fsPath).toLowerCase(); 10 | } 11 | return true; 12 | } 13 | -------------------------------------------------------------------------------- /.github/workflows/remove-needs-labels.yml: -------------------------------------------------------------------------------- 1 | name: 'Remove Needs Label' 2 | on: 3 | issues: 4 | types: [closed] 5 | 6 | jobs: 7 | classify: 8 | name: 'Remove needs labels on issue closing' 9 | runs-on: ubuntu-latest 10 | permissions: 11 | issues: write 12 | steps: 13 | - name: 'Removes needs labels on issue close' 14 | uses: actions-ecosystem/action-remove-labels@2ce5d41b4b6aa8503e285553f75ed56e0a40bae0 # v1.3.0 15 | with: 16 | labels: | 17 | needs PR 18 | needs spike 19 | needs community feedback 20 | needs proposal 21 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "ecmaVersion": 6, 6 | "sourceType": "module" 7 | }, 8 | "plugins": [ 9 | "@typescript-eslint" 10 | ], 11 | "rules": { 12 | "@typescript-eslint/naming-convention": "warn", 13 | "@typescript-eslint/semi": "warn", 14 | "curly": "warn", 15 | "eqeqeq": "warn", 16 | "no-throw-literal": "warn", 17 | "semi": "off" 18 | }, 19 | "ignorePatterns": [ 20 | "out", 21 | "dist", 22 | "**/*.d.ts" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /examples/sample1/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "NodeNext", 4 | "moduleResolution": "NodeNext", 5 | "target": "ES2020", 6 | "lib": ["ES2020"], 7 | "sourceMap": true, 8 | "rootDir": "src", 9 | "experimentalDecorators": true, 10 | "allowSyntheticDefaultImports": true, 11 | "strict": true, 12 | "noImplicitAny": true, 13 | "noImplicitThis": true, 14 | "noUnusedLocals": true, 15 | "noUnusedParameters": true, 16 | "noFallthroughCasesInSwitch": true, 17 | "resolveJsonModule": true, 18 | "removeComments": true 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.github/workflows/pr-labels.yml: -------------------------------------------------------------------------------- 1 | name: 'PR labels' 2 | on: 3 | pull_request: 4 | types: 5 | - 'opened' 6 | - 'reopened' 7 | - 'labeled' 8 | - 'unlabeled' 9 | - 'synchronize' 10 | 11 | jobs: 12 | add-pr-label: 13 | name: 'Ensure Required Labels' 14 | runs-on: ubuntu-latest 15 | permissions: 16 | issues: write 17 | pull-requests: write 18 | steps: 19 | - name: 'PR impact specified' 20 | uses: mheap/github-action-required-labels@388fd6af37b34cdfe5a23b37060e763217e58b03 # v5.5.0 21 | with: 22 | mode: exactly 23 | count: 1 24 | labels: 'bug, debt, feature-request, no-changelog' 25 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "NodeNext", 4 | "moduleResolution": "NodeNext", 5 | "target": "ES2020", 6 | "lib": ["ES2020"], 7 | "sourceMap": true, 8 | "rootDir": "src", 9 | "experimentalDecorators": true, 10 | "allowSyntheticDefaultImports": true, 11 | "strict": true, 12 | "noImplicitAny": true, 13 | "noImplicitThis": true, 14 | "noUnusedLocals": true, 15 | "noUnusedParameters": true, 16 | "noFallthroughCasesInSwitch": true, 17 | "resolveJsonModule": true, 18 | "removeComments": true 19 | }, 20 | "exclude": ["examples"] 21 | } 22 | -------------------------------------------------------------------------------- /files/templates/newPackageTemplate/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "package_name" 3 | version = "1.0.0" 4 | description = "" #TODO: Add a short description of your package 5 | authors = [{name = "Your Name", email = "your@email.com"}] #TODO: Add your name and email 6 | requires-python = ">=3.9" 7 | dynamic = ["optional-dependencies"] 8 | 9 | [build-system] 10 | requires = ["setuptools", "wheel"] 11 | build-backend = "setuptools.build_meta" 12 | 13 | [tool.setuptools.dynamic] 14 | optional-dependencies = {dev = {file = ["dev-requirements.txt"]}} # add packages for development in the dev-requirements.txt file 15 | 16 | [project.scripts] 17 | # TODO: add your CLI entry points here 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /examples/sample1/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Run Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "args": ["--extensionDevelopmentPath=${workspaceFolder}"], 13 | "outFiles": ["${workspaceFolder}/dist/**/*.js"], 14 | "preLaunchTask": "${defaultBuildTask}" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /SUPPORT.md: -------------------------------------------------------------------------------- 1 | # Support 2 | 3 | ## How to file issues and get help 4 | 5 | This project uses GitHub Issues to track bugs and feature requests. Please search the [existing issues](https://github.com/microsoft/vscode-python/issues) before filing new issues to avoid duplicates. For new issues, file your bug or feature request as a new Issue. 6 | 7 | For help and questions about using this project, please see the [`python`+`visual-studio-code` labels on Stack Overflow](https://stackoverflow.com/questions/tagged/visual-studio-code+python) or the `#vscode` channel on the [`microsoft-python` server on Discord](https://aka.ms/python-discord-invite). 8 | 9 | ## Microsoft Support Policy 10 | 11 | Support for this project is limited to the resources listed above. 12 | -------------------------------------------------------------------------------- /examples/sample1/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import typescriptEslint from "@typescript-eslint/eslint-plugin"; 2 | import tsParser from "@typescript-eslint/parser"; 3 | 4 | export default [{ 5 | files: ["**/*.ts"], 6 | }, { 7 | plugins: { 8 | "@typescript-eslint": typescriptEslint, 9 | }, 10 | 11 | languageOptions: { 12 | parser: tsParser, 13 | ecmaVersion: 2022, 14 | sourceType: "module", 15 | }, 16 | 17 | rules: { 18 | "@typescript-eslint/naming-convention": ["warn", { 19 | selector: "import", 20 | format: ["camelCase", "PascalCase"], 21 | }], 22 | curly: "warn", 23 | eqeqeq: "warn", 24 | "no-throw-literal": "warn", 25 | semi: "warn", 26 | }, 27 | }]; -------------------------------------------------------------------------------- /src/features/creators/projectCreators.ts: -------------------------------------------------------------------------------- 1 | import { Disposable } from 'vscode'; 2 | import { PythonProjectCreator } from '../../api'; 3 | import { ProjectCreators } from '../../internal.api'; 4 | 5 | export class ProjectCreatorsImpl implements ProjectCreators { 6 | private _creators: PythonProjectCreator[] = []; 7 | 8 | registerPythonProjectCreator(creator: PythonProjectCreator): Disposable { 9 | this._creators.push(creator); 10 | return new Disposable(() => { 11 | this._creators = this._creators.filter((item) => item !== creator); 12 | }); 13 | } 14 | getProjectCreators(): PythonProjectCreator[] { 15 | return this._creators; 16 | } 17 | 18 | dispose() { 19 | this._creators = []; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/features/terminal/shells/utils.ts: -------------------------------------------------------------------------------- 1 | import * as cp from 'child_process'; 2 | import { traceError, traceInfo } from '../../../common/logging'; 3 | import { StopWatch } from '../../../common/stopWatch'; 4 | 5 | export async function runCommand(command: string): Promise { 6 | return new Promise((resolve) => { 7 | const timer = new StopWatch(); 8 | cp.exec(command, (err, stdout) => { 9 | if (err) { 10 | traceError(`Error running command: ${command} (${timer.elapsedTime})`, err); 11 | resolve(undefined); 12 | } else { 13 | traceInfo(`Ran ${command} in ${timer.elapsedTime}`); 14 | resolve(stdout?.trim()); 15 | } 16 | }); 17 | }); 18 | } 19 | -------------------------------------------------------------------------------- /examples/sample1/src/pythonEnvsApi.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { PythonEnvironmentApi } from './api'; 3 | 4 | let _extApi: PythonEnvironmentApi | undefined; 5 | export async function getEnvExtApi(): Promise { 6 | if (_extApi) { 7 | return _extApi; 8 | } 9 | const extension = vscode.extensions.getExtension('ms-python.vscode-python-envs'); 10 | if (!extension) { 11 | throw new Error('Python Environments extension not found.'); 12 | } 13 | if (extension?.isActive) { 14 | _extApi = extension.exports as PythonEnvironmentApi; 15 | return _extApi; 16 | } 17 | 18 | await extension.activate(); 19 | 20 | _extApi = extension.exports as PythonEnvironmentApi; 21 | return _extApi; 22 | } 23 | -------------------------------------------------------------------------------- /src/features/terminal/activateMenuButton.ts: -------------------------------------------------------------------------------- 1 | import { Terminal } from 'vscode'; 2 | import { PythonEnvironment } from '../../api'; 3 | import { isActivatableEnvironment } from '../common/activation'; 4 | import { executeCommand } from '../../common/command.api'; 5 | import { isTaskTerminal } from './utils'; 6 | 7 | export async function setActivateMenuButtonContext( 8 | terminal: Terminal, 9 | env: PythonEnvironment, 10 | activated?: boolean, 11 | ): Promise { 12 | const activatable = !isTaskTerminal(terminal) && isActivatableEnvironment(env); 13 | await executeCommand('setContext', 'pythonTerminalActivation', activatable); 14 | 15 | if (activated !== undefined) { 16 | await executeCommand('setContext', 'pythonTerminalActivated', activated); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/test/mocks/vsc/README.md: -------------------------------------------------------------------------------- 1 | # This folder contains classes exposed by VS Code required in running the unit tests. 2 | 3 | - These classes are only used when running unit tests that are not hosted by VS Code. 4 | - So even if these classes were buggy, it doesn't matter, running the tests under VS Code host will ensure the right classes are available. 5 | - The purpose of these classes are to avoid having to use VS Code as the hosting environment for the tests, making it faster to run the tests and not have to rely on VS Code host to run the tests. 6 | - Everything in here must either be within a namespace prefixed with `vscMock` or exported types must be prefixed with `vscMock`. 7 | This is to prevent developers from accidentally importing them into their Code. Even if they did, the extension would fail to load and tests would fail. 8 | -------------------------------------------------------------------------------- /src/features/execution/execUtils.ts: -------------------------------------------------------------------------------- 1 | export function quoteStringIfNecessary(arg: string): string { 2 | // Always return if already quoted to avoid double-quoting 3 | if (arg.startsWith('"') && arg.endsWith('"')) { 4 | return arg; 5 | } 6 | 7 | // Don't quote single shell operators/special characters 8 | if (arg.length === 1 && /[&|<>;()[\]{}$]/.test(arg)) { 9 | return arg; 10 | } 11 | 12 | // Quote if contains common shell special characters that are problematic across multiple shells 13 | // Includes: space, &, |, <, >, ;, ', ", `, (, ), [, ], {, }, $ 14 | const needsQuoting = /[\s&|<>;'"`()\[\]{}$]/.test(arg); 15 | 16 | return needsQuoting ? `"${arg}"` : arg; 17 | } 18 | 19 | export function quoteArgs(args: string[]): string[] { 20 | return args.map(quoteStringIfNecessary); 21 | } 22 | -------------------------------------------------------------------------------- /src/common/extVersion.ts: -------------------------------------------------------------------------------- 1 | import { PYTHON_EXTENSION_ID } from './constants'; 2 | import { getExtension } from './extension.apis'; 3 | import { traceError } from './logging'; 4 | 5 | export function ensureCorrectVersion() { 6 | const extension = getExtension(PYTHON_EXTENSION_ID); 7 | if (!extension) { 8 | return; 9 | } 10 | 11 | const version = extension.packageJSON.version; 12 | const parts = version.split('.'); 13 | const major = parseInt(parts[0]); 14 | const minor = parseInt(parts[1]); 15 | if (major >= 2025 || (major === 2024 && minor >= 23)) { 16 | return; 17 | } 18 | traceError('Incompatible Python extension. Please update `ms-python.python` to version 2024.23 or later.'); 19 | throw new Error('Incompatible Python extension. Please update `ms-python.python` to version 2024.23 or later.'); 20 | } 21 | -------------------------------------------------------------------------------- /src/common/telemetry/reporter.ts: -------------------------------------------------------------------------------- 1 | import type TelemetryReporter from '@vscode/extension-telemetry'; 2 | 3 | class ReporterImpl { 4 | private static telemetryReporter: TelemetryReporter | undefined; 5 | static getTelemetryReporter() { 6 | const tel = require('@vscode/extension-telemetry'); 7 | const Reporter = tel.default as typeof TelemetryReporter; 8 | ReporterImpl.telemetryReporter = new Reporter( 9 | '0c6ae279ed8443289764825290e4f9e2-1a736e7c-1324-4338-be46-fc2a58ae4d14-7255', 10 | [ 11 | { 12 | lookup: /(errorName|errorMessage|errorStack)/g, 13 | }, 14 | ], 15 | ); 16 | 17 | return ReporterImpl.telemetryReporter; 18 | } 19 | } 20 | 21 | export function getTelemetryReporter() { 22 | return ReporterImpl.getTelemetryReporter(); 23 | } 24 | -------------------------------------------------------------------------------- /.github/actions/build-vsix/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Build VSIX' 2 | description: "Build the extension's VSIX" 3 | 4 | inputs: 5 | node_version: 6 | description: 'Version of Node to install' 7 | required: true 8 | 9 | runs: 10 | using: 'composite' 11 | steps: 12 | - name: Install Node 13 | uses: actions/setup-node@v4 14 | with: 15 | node-version: ${{ inputs.node_version }} 16 | cache: 'npm' 17 | 18 | - name: Run npm ci 19 | run: npm ci --prefer-offline 20 | shell: bash 21 | 22 | - name: Build VSIX 23 | run: npx vsce package --out ms-python-envs-insiders.vsix --pre-release 24 | shell: bash 25 | 26 | - name: Upload VSIX 27 | uses: actions/upload-artifact@v4 28 | with: 29 | name: vsix 30 | path: ms-python-envs-insiders.vsix 31 | if-no-files-found: error 32 | retention-days: 7 33 | -------------------------------------------------------------------------------- /src/common/utils/debounce.ts: -------------------------------------------------------------------------------- 1 | import { Disposable } from 'vscode'; 2 | export interface SimpleDebounce extends Disposable { 3 | trigger(): void; 4 | } 5 | 6 | class SimpleDebounceImpl implements SimpleDebounce { 7 | private timeout: NodeJS.Timeout | undefined; 8 | 9 | constructor(private readonly ms: number, private readonly callback: () => void) {} 10 | 11 | public trigger(): void { 12 | if (this.timeout) { 13 | clearTimeout(this.timeout); 14 | } 15 | this.timeout = setTimeout(() => { 16 | this.callback(); 17 | }, this.ms); 18 | } 19 | 20 | public dispose(): void { 21 | if (this.timeout) { 22 | clearTimeout(this.timeout); 23 | } 24 | } 25 | } 26 | 27 | export function createSimpleDebounce(ms: number, callback: () => void): SimpleDebounce { 28 | return new SimpleDebounceImpl(ms, callback); 29 | } 30 | -------------------------------------------------------------------------------- /examples/sample1/src/extension.ts: -------------------------------------------------------------------------------- 1 | // The module 'vscode' contains the VS Code extensibility API 2 | // Import the module and reference it with the alias vscode in your code below 3 | import * as vscode from 'vscode'; 4 | import { getEnvExtApi } from './pythonEnvsApi'; 5 | import { SampleEnvManager } from './sampleEnvManager'; 6 | 7 | // This method is called when your extension is activated 8 | // Your extension is activated the very first time the command is executed 9 | export async function activate(context: vscode.ExtensionContext) { 10 | const api = await getEnvExtApi(); 11 | 12 | const log = vscode.window.createOutputChannel('Sample Environment Manager', { log: true }); 13 | context.subscriptions.push(log); 14 | 15 | const manager = new SampleEnvManager(log); 16 | context.subscriptions.push(api.registerEnvironmentManager(manager)); 17 | } 18 | 19 | // This method is called when your extension is deactivated 20 | export function deactivate() {} 21 | -------------------------------------------------------------------------------- /src/test/mocks/vsc/strings.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | 'use strict'; 5 | 6 | /** 7 | * Determines if haystack starts with needle. 8 | */ 9 | export function startsWith(haystack: string, needle: string): boolean { 10 | if (haystack.length < needle.length) { 11 | return false; 12 | } 13 | 14 | for (let i = 0; i < needle.length; i += 1) { 15 | if (haystack[i] !== needle[i]) { 16 | return false; 17 | } 18 | } 19 | 20 | return true; 21 | } 22 | 23 | /** 24 | * Determines if haystack ends with needle. 25 | */ 26 | export function endsWith(haystack: string, needle: string): boolean { 27 | const diff = haystack.length - needle.length; 28 | if (diff > 0) { 29 | return haystack.indexOf(needle, diff) === diff; 30 | } 31 | if (diff === 0) { 32 | return haystack === needle; 33 | } 34 | return false; 35 | } 36 | -------------------------------------------------------------------------------- /files/templates/copilot-instructions-text/script-copilot-instructions.md: -------------------------------------------------------------------------------- 1 | # Copilot Instructions for 2 | 3 | - The script `` is a Python python project within the workspace. 4 | - It has inline script metadata (as proposed by PEP 723) that defines the script name, required python version, and dependencies. 5 | - If imports which require a specific Python version or dependencies are added, keep the inline script metadata up to date. 6 | - You need to call the `Get Python Environment Information` tool on the `` path to get the Python executable details. 7 | - Substitute the Python executable you get from the `Get Python Environment Information` tool anywhere you see `` in these instructions. 8 | - Run command for ``: ` ` 9 | - Script can be easily debugged from the Integrated Terminal when activated with the command `debugpy ` after the necessary environment is activated. 10 | -------------------------------------------------------------------------------- /.github/workflows/issue-labels.yml: -------------------------------------------------------------------------------- 1 | name: Issue labels 2 | 3 | on: 4 | issues: 5 | types: [opened, reopened] 6 | 7 | env: 8 | TRIAGERS: '["karthiknadig","eleanorjboyd"]' 9 | 10 | permissions: 11 | issues: write 12 | 13 | jobs: 14 | # From https://github.com/marketplace/actions/github-script#apply-a-label-to-an-issue. 15 | add-classify-label: 16 | name: "Add 'triage-needed' and remove assignees" 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Checkout Actions 20 | uses: actions/checkout@v4 21 | with: 22 | repository: 'microsoft/vscode-github-triage-actions' 23 | ref: stable 24 | path: ./actions 25 | 26 | - name: Install Actions 27 | run: npm install --production --prefix ./actions 28 | 29 | - name: "Add 'triage-needed' and remove assignees" 30 | uses: ./actions/python-issue-labels 31 | with: 32 | triagers: ${{ env.TRIAGERS }} 33 | token: ${{secrets.GITHUB_TOKEN}} 34 | -------------------------------------------------------------------------------- /examples/sample1/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false, // set this to true to hide the "out" folder with the compiled JS files 5 | "dist": false // set this to true to hide the "dist" folder with the compiled JS files 6 | }, 7 | "search.exclude": { 8 | "out": true, // set this to false to include "out" folder in search results 9 | "dist": true // set this to false to include "dist" folder in search results 10 | }, 11 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 12 | "typescript.tsc.autoDetect": "off", 13 | "editor.formatOnSave": true, 14 | "[typescript]": { 15 | "editor.defaultFormatter": "esbenp.prettier-vscode" 16 | }, 17 | "[python]": { 18 | "editor.defaultFormatter": "charliermarsh.ruff", 19 | "diffEditor.ignoreTrimWhitespace": false 20 | }, 21 | "prettier.tabWidth": 4 22 | } 23 | -------------------------------------------------------------------------------- /src/features/terminal/shells/startupProvider.ts: -------------------------------------------------------------------------------- 1 | import { EnvironmentVariableCollection } from 'vscode'; 2 | import { PythonEnvironment } from '../../../api'; 3 | 4 | export enum ShellSetupState { 5 | NotSetup, 6 | Setup, 7 | NotInstalled, 8 | } 9 | 10 | export enum ShellScriptEditState { 11 | NotEdited, 12 | Edited, 13 | NotInstalled, 14 | } 15 | 16 | export interface ShellStartupScriptProvider { 17 | name: string; 18 | readonly shellType: string; 19 | isSetup(): Promise; 20 | setupScripts(): Promise; 21 | teardownScripts(): Promise; 22 | clearCache(): Promise; 23 | } 24 | 25 | export interface ShellEnvsProvider { 26 | readonly shellType: string; 27 | updateEnvVariables(envVars: EnvironmentVariableCollection, env: PythonEnvironment): void; 28 | removeEnvVariables(envVars: EnvironmentVariableCollection): void; 29 | getEnvVariables(env?: PythonEnvironment): Map | undefined; 30 | } 31 | -------------------------------------------------------------------------------- /src/common/constants.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | 3 | export const ENVS_EXTENSION_ID = 'ms-python.vscode-python-envs'; 4 | export const PYTHON_EXTENSION_ID = 'ms-python.python'; 5 | export const JUPYTER_EXTENSION_ID = 'ms-toolsai.jupyter'; 6 | export const EXTENSION_ROOT_DIR = path.dirname(__dirname); 7 | 8 | export const DEFAULT_PACKAGE_MANAGER_ID = 'ms-python.python:pip'; 9 | export const DEFAULT_ENV_MANAGER_ID = 'ms-python.python:venv'; 10 | 11 | export const KNOWN_FILES = [ 12 | 'requirements.txt', 13 | 'requirements.in', 14 | '.condarc', 15 | '.python-version', 16 | 'environment.yml', 17 | 'pyproject.toml', 18 | 'meta.yaml', 19 | '.flake8', 20 | '.pep8', 21 | '.pylintrc', 22 | '.pypirc', 23 | 'Pipfile', 24 | 'poetry.lock', 25 | 'Pipfile.lock', 26 | ]; 27 | 28 | export const KNOWN_TEMPLATE_ENDINGS = ['.j2', '.jinja2']; 29 | 30 | export const NEW_PROJECT_TEMPLATES_FOLDER = path.join(EXTENSION_ROOT_DIR, 'files', 'templates'); 31 | export const NotebookCellScheme = 'vscode-notebook-cell'; 32 | -------------------------------------------------------------------------------- /files/templates/copilot-instructions-text/package-copilot-instructions.md: -------------------------------------------------------------------------------- 1 | # Copilot Instructions for 2 | 3 | - The package `` is a Python Project located in the folder `-folder`. 4 | - You need to call the `Get Python Environment Information` tool on the `` path to get the Python executable details. 5 | - Substitute the Python executable you get from the `Get Python Environment Information` tool anywhere you see `` in these instructions. 6 | - Run command for ``: ` -m ` 7 | - Command to run tests for ``: ` -m pytest /tests` 8 | - To run an editable install for the package ``, use the `Install Python Package` tool with the `-folder` path and arguments `['-e', '.']`. 9 | - In the workspace `launch.json` file, configurations related to this package have the prefix ``. 10 | - The package `` has a defined `pyproject.toml` file that you should use and keep up to date. 11 | -------------------------------------------------------------------------------- /src/test/managers/builtin/piplist2.actual.txt: -------------------------------------------------------------------------------- 1 | Package Version 2 | ------------------ -------- 3 | argcomplete 3.1.2 4 | black 23.12.1 5 | build 1.2.1 6 | click 8.1.7 7 | colorama 0.4.6 8 | colorlog 6.7.0 9 | coverage 7.6.1 10 | distlib 0.3.7 11 | exceptiongroup 1.1.3 12 | filelock 3.13.1 13 | importlib_metadata 7.1.0 14 | iniconfig 2.0.0 15 | isort 5.13.2 16 | mypy-extensions 1.0.0 17 | namedpipe 0.1.1 18 | nox 2024.3.2 19 | packaging 23.2 20 | pathspec 0.12.1 21 | pip 24.0 22 | pip-tools 7.4.1 23 | platformdirs 3.11.0 24 | pluggy 1.4.0 25 | pyproject_hooks 1.1.0 26 | pytest 8.1.1 27 | pytest-cov 5.0.0 28 | pywin32 306 29 | ruff 0.7.4 30 | setuptools 56.0.0 31 | tomli 2.0.1 32 | typing_extensions 4.9.0 33 | virtualenv 20.24.6 34 | wheel 0.43.0 35 | zipp 3.19.2 -------------------------------------------------------------------------------- /src/common/childProcess.apis.ts: -------------------------------------------------------------------------------- 1 | import * as cp from 'child_process'; 2 | 3 | /** 4 | * Spawns a new process using the specified command and arguments. 5 | * This function abstracts cp.spawn to make it easier to mock in tests. 6 | * 7 | * When stdio: 'pipe' is used, returns ChildProcessWithoutNullStreams. 8 | * Otherwise returns the standard ChildProcess. 9 | */ 10 | 11 | // Overload for stdio: 'pipe' - guarantees non-null streams 12 | export function spawnProcess( 13 | command: string, 14 | args: string[], 15 | options: cp.SpawnOptions & { stdio: 'pipe' }, 16 | ): cp.ChildProcessWithoutNullStreams; 17 | 18 | // Overload for general case 19 | export function spawnProcess(command: string, args: string[], options?: cp.SpawnOptions): cp.ChildProcess; 20 | 21 | // Implementation - delegates to cp.spawn to preserve its typing magic 22 | export function spawnProcess( 23 | command: string, 24 | args: string[], 25 | options?: cp.SpawnOptions, 26 | ): cp.ChildProcess | cp.ChildProcessWithoutNullStreams { 27 | return cp.spawn(command, args, options ?? {}); 28 | } 29 | -------------------------------------------------------------------------------- /src/common/telemetry/helpers.ts: -------------------------------------------------------------------------------- 1 | import { getDefaultEnvManagerSetting, getDefaultPkgManagerSetting } from '../../features/settings/settingHelpers'; 2 | import { PythonProjectManager } from '../../internal.api'; 3 | import { EventNames } from './constants'; 4 | import { sendTelemetryEvent } from './sender'; 5 | 6 | export function sendManagerSelectionTelemetry(pm: PythonProjectManager) { 7 | const ems: Set = new Set(); 8 | const ps: Set = new Set(); 9 | pm.getProjects().forEach((project) => { 10 | const m = getDefaultEnvManagerSetting(pm, project.uri); 11 | if (m) { 12 | ems.add(m); 13 | } 14 | 15 | const p = getDefaultPkgManagerSetting(pm, project.uri); 16 | if (p) { 17 | ps.add(p); 18 | } 19 | }); 20 | 21 | ems.forEach((em) => { 22 | sendTelemetryEvent(EventNames.ENVIRONMENT_MANAGER_SELECTED, undefined, { managerId: em }); 23 | }); 24 | 25 | ps.forEach((pkg) => { 26 | sendTelemetryEvent(EventNames.PACKAGE_MANAGER_SELECTED, undefined, { managerId: pkg }); 27 | }); 28 | } 29 | -------------------------------------------------------------------------------- /.github/workflows/test_plan_item_validator.yml: -------------------------------------------------------------------------------- 1 | name: Test Plan Item Validator 2 | on: 3 | issues: 4 | types: [edited, labeled] 5 | 6 | permissions: 7 | issues: write 8 | 9 | jobs: 10 | main: 11 | runs-on: ubuntu-latest 12 | if: contains(github.event.issue.labels.*.name, 'testplan-item') || contains(github.event.issue.labels.*.name, 'invalid-testplan-item') 13 | steps: 14 | - name: Checkout Actions 15 | uses: actions/checkout@v4 16 | with: 17 | repository: 'microsoft/vscode-github-triage-actions' 18 | path: ./actions 19 | persist-credentials: false 20 | ref: stable 21 | 22 | - name: Install Actions 23 | run: npm install --production --prefix ./actions 24 | 25 | - name: Run Test Plan Item Validator 26 | uses: ./actions/test-plan-item-validator 27 | with: 28 | label: testplan-item 29 | invalidLabel: invalid-testplan-item 30 | comment: Invalid test plan item. See errors below and the [test plan item spec](https://github.com/microsoft/vscode/wiki/Writing-Test-Plan-Items) for more information. This comment will go away when the issues are resolved. 31 | -------------------------------------------------------------------------------- /examples/sample1/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "watch", 9 | "problemMatcher": "$ts-webpack-watch", 10 | "isBackground": true, 11 | "presentation": { 12 | "reveal": "never", 13 | "group": "watchers" 14 | }, 15 | "group": { 16 | "kind": "build", 17 | "isDefault": true 18 | } 19 | }, 20 | { 21 | "type": "npm", 22 | "script": "watch-tests", 23 | "problemMatcher": "$tsc-watch", 24 | "isBackground": true, 25 | "presentation": { 26 | "reveal": "never", 27 | "group": "watchers" 28 | }, 29 | "group": "build" 30 | }, 31 | { 32 | "label": "tasks: watch-tests", 33 | "dependsOn": ["npm: watch", "npm: watch-tests"], 34 | "problemMatcher": [] 35 | } 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /src/test/managers/builtin/piplist1.actual.txt: -------------------------------------------------------------------------------- 1 | Package Version 2 | ------------------ -------- 3 | argcomplete 3.1.2 4 | black 23.12.1 5 | build 1.2.1 6 | click 8.1.7 7 | colorama 0.4.6 8 | colorlog 6.7.0 9 | coverage 7.6.1 10 | distlib 0.3.7 11 | exceptiongroup 1.1.3 12 | filelock 3.13.1 13 | importlib_metadata 7.1.0 14 | iniconfig 2.0.0 15 | isort 5.13.2 16 | mypy-extensions 1.0.0 17 | namedpipe 0.1.1 18 | nox 2024.3.2 19 | packaging 23.2 20 | pathspec 0.12.1 21 | pip 24.0 22 | pip-tools 7.4.1 23 | platformdirs 3.11.0 24 | pluggy 1.4.0 25 | pyproject_hooks 1.1.0 26 | pytest 8.1.1 27 | pytest-cov 5.0.0 28 | pywin32 306 29 | ruff 0.7.4 30 | setuptools 56.0.0 31 | tomli 2.0.1 32 | typing_extensions 4.9.0 33 | virtualenv 20.24.6 34 | wheel 0.43.0 35 | zipp 3.19.2 36 | 37 | [notice] A new release of pip is available: 24.0 -> 24.3.1 38 | [notice] To update, run: python.exe -m pip install --upgrade pip -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /src/features/execution/envVarUtils.ts: -------------------------------------------------------------------------------- 1 | import { Uri } from 'vscode'; 2 | import { readFile } from '../../common/workspace.fs.apis'; 3 | import { parse } from 'dotenv'; 4 | 5 | export function mergeEnvVariables( 6 | base: { [key: string]: string | undefined }, 7 | other: { [key: string]: string | undefined }, 8 | ) { 9 | const env: { [key: string]: string | undefined } = {}; 10 | 11 | Object.keys(other).forEach((otherKey) => { 12 | let value = other[otherKey]; 13 | if (value === undefined || value === '') { 14 | // SOME_ENV_VAR= 15 | delete env[otherKey]; 16 | } else { 17 | Object.keys(base).forEach((baseKey) => { 18 | const baseValue = base[baseKey]; 19 | if (baseValue) { 20 | value = value?.replace(`\${${baseKey}}`, baseValue); 21 | } 22 | }); 23 | env[otherKey] = value; 24 | } 25 | }); 26 | 27 | return env; 28 | } 29 | 30 | export async function parseEnvFile(envFile: Uri): Promise<{ [key: string]: string | undefined }> { 31 | const raw = await readFile(envFile); 32 | const contents = Buffer.from(raw).toString('utf-8'); 33 | return parse(contents); 34 | } 35 | -------------------------------------------------------------------------------- /.github/instructions/generic.instructions.md: -------------------------------------------------------------------------------- 1 | --- 2 | applyTo: '**' 3 | --- 4 | 5 | Provide project context and coding guidelines that AI should follow when generating code, answering questions, or reviewing changes.# Coding Instructions for vscode-python-environments 6 | 7 | ## Localization 8 | 9 | - Localize all user-facing messages using VS Code’s `l10n` API. 10 | - Internal log messages do not require localization. 11 | 12 | ## Logging 13 | 14 | - Use the extension’s logging utilities (`traceLog`, `traceVerbose`) for internal logs. 15 | - Do not use `console.log` or `console.warn` for logging. 16 | 17 | ## Settings Precedence 18 | 19 | - Always consider VS Code settings precedence: 20 | 1. Workspace folder 21 | 2. Workspace 22 | 3. User/global 23 | - Remove or update settings from the highest precedence scope first. 24 | 25 | ## Error Handling & User Notifications 26 | 27 | - Avoid showing the same error message multiple times in a session; track state with a module-level variable. 28 | - Use clear, actionable error messages and offer relevant buttons (e.g., "Open settings", "Close"). 29 | 30 | ## Documentation 31 | 32 | - Add clear docstrings to public functions, describing their purpose, parameters, and behavior. 33 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import typescriptEslint from "@typescript-eslint/eslint-plugin"; 2 | import tsParser from "@typescript-eslint/parser"; 3 | 4 | export default [{ 5 | files: ["**/*.ts"], 6 | }, { 7 | plugins: { 8 | "@typescript-eslint": typescriptEslint, 9 | }, 10 | 11 | languageOptions: { 12 | parser: tsParser, 13 | ecmaVersion: 2022, 14 | sourceType: "module", 15 | }, 16 | 17 | rules: { 18 | "@typescript-eslint/naming-convention": ["warn", { 19 | selector: "import", 20 | format: ["camelCase", "PascalCase"], 21 | }], 22 | "@typescript-eslint/no-unused-vars": [ 23 | "error", 24 | { 25 | "args": "all", 26 | "argsIgnorePattern": "^_", 27 | "caughtErrors": "all", 28 | "caughtErrorsIgnorePattern": "^_", 29 | "destructuredArrayIgnorePattern": "^_", 30 | "varsIgnorePattern": "^_", 31 | "ignoreRestSiblings": true 32 | } 33 | ], 34 | curly: "warn", 35 | eqeqeq: "warn", 36 | "no-throw-literal": "warn", 37 | semi: "warn", 38 | "@typescript-eslint/no-explicit-any": "error", 39 | }, 40 | }]; -------------------------------------------------------------------------------- /src/common/errors/utils.ts: -------------------------------------------------------------------------------- 1 | import * as stackTrace from 'stack-trace'; 2 | import { commands, LogOutputChannel } from 'vscode'; 3 | import { Common } from '../localize'; 4 | import { showErrorMessage, showWarningMessage } from '../window.apis'; 5 | 6 | export function parseStack(ex: Error) { 7 | if (ex.stack && Array.isArray(ex.stack)) { 8 | const concatenated = { ...ex, stack: ex.stack.join('\n') }; 9 | return stackTrace.parse.call(stackTrace, concatenated); 10 | } 11 | return stackTrace.parse.call(stackTrace, ex); 12 | } 13 | 14 | export async function showErrorMessageWithLogs(message: string, log?: LogOutputChannel) { 15 | const result = await showErrorMessage(message, Common.viewLogs); 16 | if (result === Common.viewLogs) { 17 | if (log) { 18 | log.show(); 19 | } else { 20 | commands.executeCommand('python-envs.viewLogs'); 21 | } 22 | } 23 | } 24 | 25 | export async function showWarningMessageWithLogs(message: string, log?: LogOutputChannel) { 26 | const result = await showWarningMessage(message, Common.viewLogs); 27 | if (result === Common.viewLogs) { 28 | if (log) { 29 | log.show(); 30 | } else { 31 | commands.executeCommand('python-envs.viewLogs'); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/managers/builtin/pipListUtils.ts: -------------------------------------------------------------------------------- 1 | export interface PipPackage { 2 | name: string; 3 | version: string; 4 | displayName: string; 5 | description: string; 6 | } 7 | export function isValidVersion(version: string): boolean { 8 | return /^([1-9][0-9]*!)?(0|[1-9][0-9]*)(\.(0|[1-9][0-9]*))*((a|b|rc)(0|[1-9][0-9]*))?(\.post(0|[1-9][0-9]*))?(\.dev(0|[1-9][0-9]*))?$/.test( 9 | version, 10 | ); 11 | } 12 | export function parsePipList(data: string): PipPackage[] { 13 | const collection: PipPackage[] = []; 14 | 15 | const lines = data.split('\n').splice(2); 16 | for (let line of lines) { 17 | if (line.trim() === '' || line.startsWith('Package') || line.startsWith('----') || line.startsWith('[')) { 18 | continue; 19 | } 20 | const parts = line.split(' ').filter((e) => e); 21 | if (parts.length === 2) { 22 | const name = parts[0].trim(); 23 | const version = parts[1].trim(); 24 | if (!isValidVersion(version)) { 25 | continue; 26 | } 27 | const pkg = { 28 | name, 29 | version, 30 | displayName: name, 31 | description: version, 32 | }; 33 | collection.push(pkg); 34 | } 35 | } 36 | return collection; 37 | } 38 | -------------------------------------------------------------------------------- /src/test/mocks/mementos.ts: -------------------------------------------------------------------------------- 1 | import { Memento } from 'vscode'; 2 | 3 | export class MockMemento implements Memento { 4 | // Note: This has to be called _value so that it matches 5 | // what VS code has for a memento. We use this to eliminate a bad bug 6 | // with writing too much data to global storage. See bug https://github.com/microsoft/vscode-python/issues/9159 7 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 8 | private _value: Record = {}; 9 | 10 | public keys(): string[] { 11 | return Object.keys(this._value); 12 | } 13 | 14 | // @ts-ignore Ignore the return value warning 15 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 16 | public get(key: any, defaultValue?: any); 17 | 18 | public get(key: string, defaultValue?: T): T { 19 | const exists = this._value.hasOwnProperty(key); 20 | 21 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 22 | return exists ? this._value[key] : (defaultValue! as any); 23 | } 24 | 25 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 26 | public update(key: string, value: any): Thenable { 27 | this._value[key] = value; 28 | return Promise.resolve(); 29 | } 30 | 31 | public clear(): void { 32 | this._value = {}; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/features/views/revealHandler.ts: -------------------------------------------------------------------------------- 1 | import { PythonEnvironmentApi } from '../../api'; 2 | import { isPythonProjectFile } from '../../common/utils/fileNameUtils'; 3 | import { activeTextEditor } from '../../common/window.apis'; 4 | import { EnvManagerView } from './envManagersView'; 5 | import { ProjectView } from './projectView'; 6 | import { PythonStatusBar } from './pythonStatusBar'; 7 | 8 | export function updateViewsAndStatus( 9 | statusBar: PythonStatusBar, 10 | workspaceView: ProjectView, 11 | managerView: EnvManagerView, 12 | api: PythonEnvironmentApi, 13 | ) { 14 | workspaceView.updateProject(); 15 | 16 | const activeDocument = activeTextEditor()?.document; 17 | if (!activeDocument || activeDocument.isUntitled || activeDocument.uri.scheme !== 'file') { 18 | statusBar.hide(); 19 | return; 20 | } 21 | 22 | if ( 23 | activeDocument.languageId !== 'python' && 24 | activeDocument.languageId !== 'pip-requirements' && 25 | !isPythonProjectFile(activeDocument.uri.fsPath) 26 | ) { 27 | statusBar.hide(); 28 | return; 29 | } 30 | 31 | workspaceView.reveal(activeDocument.uri); 32 | setImmediate(async () => { 33 | const env = await api.getEnvironment(activeDocument.uri); 34 | statusBar.show(env); 35 | managerView.reveal(env); 36 | }); 37 | } 38 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false, // set this to true to hide the "out" folder with the compiled JS files 5 | "dist": false // set this to true to hide the "dist" folder with the compiled JS files 6 | }, 7 | "search.exclude": { 8 | "out": true, // set this to false to include "out" folder in search results 9 | "dist": true // set this to false to include "dist" folder in search results 10 | }, 11 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 12 | "typescript.tsc.autoDetect": "off", 13 | "editor.formatOnSave": true, 14 | "[typescript]": { 15 | "editor.defaultFormatter": "esbenp.prettier-vscode", 16 | "editor.codeActionsOnSave": { 17 | "source.organizeImports": "explicit" 18 | } 19 | }, 20 | "[python]": { 21 | "editor.defaultFormatter": "charliermarsh.ruff", 22 | "diffEditor.ignoreTrimWhitespace": false, 23 | "editor.codeActionsOnSave": { 24 | "source.organizeImports": "explicit" 25 | } 26 | }, 27 | "prettier.tabWidth": 4, 28 | "python-envs.defaultEnvManager": "ms-python.python:venv", 29 | "python-envs.pythonProjects": [], 30 | "git.branchRandomName.enable": true 31 | } 32 | -------------------------------------------------------------------------------- /.github/workflows/info-needed-closer.yml: -------------------------------------------------------------------------------- 1 | name: Info-Needed Closer 2 | on: 3 | schedule: 4 | - cron: 20 12 * * * # 5:20am Redmond 5 | repository_dispatch: 6 | types: [trigger-needs-more-info] 7 | workflow_dispatch: 8 | 9 | permissions: 10 | issues: write 11 | 12 | jobs: 13 | main: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout Actions 17 | uses: actions/checkout@v4 18 | with: 19 | repository: 'microsoft/vscode-github-triage-actions' 20 | path: ./actions 21 | persist-credentials: false 22 | ref: stable 23 | - name: Install Actions 24 | run: npm install --production --prefix ./actions 25 | - name: Run info-needed Closer 26 | uses: ./actions/needs-more-info-closer 27 | with: 28 | token: ${{secrets.GITHUB_TOKEN}} 29 | label: info-needed 30 | closeDays: 14 31 | closeComment: "Because we have not heard back with the information we requested, we are closing this issue for now. If you are able to provide the info later on, then we will be happy to re-open this issue to pick up where we left off. \n\nHappy Coding!" 32 | pingDays: 14 33 | pingComment: "Hey @${assignee}, this issue might need further attention.\n\n@${author}, you can help us out by closing this issue if the problem no longer exists, or adding more information." 34 | -------------------------------------------------------------------------------- /.github/workflows/community-feedback-auto-comment.yml: -------------------------------------------------------------------------------- 1 | name: Community Feedback Auto Comment 2 | 3 | on: 4 | issues: 5 | types: 6 | - labeled 7 | jobs: 8 | add-comment: 9 | if: github.event.label.name == 'needs community feedback' 10 | runs-on: ubuntu-latest 11 | permissions: 12 | issues: write 13 | steps: 14 | - name: Check For Existing Comment 15 | uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e # v3.1.0 16 | id: finder 17 | with: 18 | issue-number: ${{ github.event.issue.number }} 19 | comment-author: 'github-actions[bot]' 20 | body-includes: 'Thanks for the feature request! We are going to give the community' 21 | 22 | - name: Add Community Feedback Comment 23 | if: steps.finder.outputs.comment-id == '' 24 | uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0 25 | with: 26 | issue-number: ${{ github.event.issue.number }} 27 | body: | 28 | Thanks for the feature request! We are going to give the community 60 days from when this issue was created to provide 7 👍 upvotes on the opening comment to gauge general interest in this idea. If there's enough upvotes then we will consider this feature request in our future planning. If there's unfortunately not enough upvotes then we will close this issue. 29 | -------------------------------------------------------------------------------- /examples/sample1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sample1", 3 | "displayName": "Sample1", 4 | "description": "A sample environment manager", 5 | "version": "0.0.1", 6 | "engines": { 7 | "vscode": "^1.95.0" 8 | }, 9 | "categories": [ 10 | "Other" 11 | ], 12 | "activationEvents": [], 13 | "main": "./dist/extension.js", 14 | "contributes": {}, 15 | "scripts": { 16 | "vscode:prepublish": "npm run package", 17 | "compile": "webpack", 18 | "watch": "webpack --watch", 19 | "package": "webpack --mode production --devtool hidden-source-map", 20 | "compile-tests": "tsc -p . --outDir out", 21 | "watch-tests": "tsc -p . -w --outDir out", 22 | "pretest": "npm run compile-tests && npm run compile && npm run lint", 23 | "lint": "eslint src", 24 | "test": "vscode-test" 25 | }, 26 | "devDependencies": { 27 | "@types/vscode": "^1.95.0", 28 | "@types/mocha": "^10.0.7", 29 | "@types/node": "20.x", 30 | "@typescript-eslint/eslint-plugin": "^8.3.0", 31 | "@typescript-eslint/parser": "^8.3.0", 32 | "eslint": "^9.9.1", 33 | "typescript": "^5.5.4", 34 | "ts-loader": "^9.5.1", 35 | "webpack": "^5.94.0", 36 | "webpack-cli": "^5.1.4", 37 | "@vscode/test-cli": "^0.0.10", 38 | "@vscode/test-electron": "^2.4.1" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/vscode.proposed.terminalDataWriteEvent.d.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | declare module 'vscode' { 7 | // https://github.com/microsoft/vscode/issues/78502 8 | // 9 | // This API is still proposed but we don't intent on promoting it to stable due to problems 10 | // around performance. See #145234 for a more likely API to get stabilized. 11 | 12 | export interface TerminalDataWriteEvent { 13 | /** 14 | * The {@link Terminal} for which the data was written. 15 | */ 16 | readonly terminal: Terminal; 17 | /** 18 | * The data being written. 19 | */ 20 | readonly data: string; 21 | } 22 | 23 | namespace window { 24 | /** 25 | * An event which fires when the terminal's child pseudo-device is written to (the shell). 26 | * In other words, this provides access to the raw data stream from the process running 27 | * within the terminal, including VT sequences. 28 | */ 29 | export const onDidWriteTerminalData: Event; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/features/common/activation.ts: -------------------------------------------------------------------------------- 1 | import { Terminal } from 'vscode'; 2 | import { PythonEnvironment } from '../../api'; 3 | import { 4 | getShellActivationCommand, 5 | getShellCommandAsString, 6 | getShellDeactivationCommand, 7 | } from '../terminal/shells/common/shellUtils'; 8 | import { identifyTerminalShell } from './shellDetector'; 9 | 10 | export function isActivatableEnvironment(environment: PythonEnvironment): boolean { 11 | return !!environment.execInfo?.activation || !!environment.execInfo?.shellActivation; 12 | } 13 | 14 | export function isActivatedRunAvailable(environment: PythonEnvironment): boolean { 15 | return !!environment.execInfo?.activatedRun; 16 | } 17 | 18 | export function getActivationCommand(terminal: Terminal, environment: PythonEnvironment): string | undefined { 19 | const shell = identifyTerminalShell(terminal); 20 | const command = getShellActivationCommand(shell, environment); 21 | if (command) { 22 | return getShellCommandAsString(shell, command); 23 | } 24 | return undefined; 25 | } 26 | 27 | export function getDeactivationCommand(terminal: Terminal, environment: PythonEnvironment): string | undefined { 28 | const shell = identifyTerminalShell(terminal); 29 | const command = getShellDeactivationCommand(shell, environment); 30 | if (command) { 31 | return getShellCommandAsString(shell, command); 32 | } 33 | return undefined; 34 | } 35 | -------------------------------------------------------------------------------- /src/managers/pyenv/main.ts: -------------------------------------------------------------------------------- 1 | import { Disposable } from 'vscode'; 2 | import { PythonEnvironmentApi } from '../../api'; 3 | import { traceInfo } from '../../common/logging'; 4 | import { getPythonApi } from '../../features/pythonApi'; 5 | import { PythonProjectManager } from '../../internal.api'; 6 | import { NativePythonFinder } from '../common/nativePythonFinder'; 7 | import { notifyMissingManagerIfDefault } from '../common/utils'; 8 | import { PyEnvManager } from './pyenvManager'; 9 | import { getPyenv } from './pyenvUtils'; 10 | 11 | export async function registerPyenvFeatures( 12 | nativeFinder: NativePythonFinder, 13 | disposables: Disposable[], 14 | projectManager: PythonProjectManager, 15 | ): Promise { 16 | const api: PythonEnvironmentApi = await getPythonApi(); 17 | 18 | try { 19 | const pyenv = await getPyenv(nativeFinder); 20 | 21 | if (pyenv) { 22 | const mgr = new PyEnvManager(nativeFinder, api); 23 | disposables.push(mgr, api.registerEnvironmentManager(mgr)); 24 | } else { 25 | traceInfo('Pyenv not found, turning off pyenv features.'); 26 | await notifyMissingManagerIfDefault('ms-python.python:pyenv', projectManager, api); 27 | } 28 | } catch (ex) { 29 | traceInfo('Pyenv not found, turning off pyenv features.', ex); 30 | await notifyMissingManagerIfDefault('ms-python.python:pyenv', projectManager, api); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/managers/pipenv/main.ts: -------------------------------------------------------------------------------- 1 | import { Disposable } from 'vscode'; 2 | import { PythonEnvironmentApi } from '../../api'; 3 | import { traceInfo } from '../../common/logging'; 4 | import { getPythonApi } from '../../features/pythonApi'; 5 | import { PythonProjectManager } from '../../internal.api'; 6 | import { NativePythonFinder } from '../common/nativePythonFinder'; 7 | import { PipenvManager } from './pipenvManager'; 8 | import { getPipenv } from './pipenvUtils'; 9 | 10 | import { notifyMissingManagerIfDefault } from '../common/utils'; 11 | 12 | export async function registerPipenvFeatures( 13 | nativeFinder: NativePythonFinder, 14 | disposables: Disposable[], 15 | projectManager: PythonProjectManager, 16 | ): Promise { 17 | const api: PythonEnvironmentApi = await getPythonApi(); 18 | 19 | try { 20 | const pipenv = await getPipenv(nativeFinder); 21 | 22 | if (pipenv) { 23 | const mgr = new PipenvManager(nativeFinder, api); 24 | disposables.push(mgr, api.registerEnvironmentManager(mgr)); 25 | } else { 26 | traceInfo('Pipenv not found, turning off pipenv features.'); 27 | await notifyMissingManagerIfDefault('ms-python.python:pipenv', projectManager, api); 28 | } 29 | } catch (ex) { 30 | traceInfo('Pipenv not found, turning off pipenv features.', ex); 31 | await notifyMissingManagerIfDefault('ms-python.python:pipenv', projectManager, api); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/common/utils/internalVariables.ts: -------------------------------------------------------------------------------- 1 | import { Uri } from 'vscode'; 2 | import { getWorkspaceFolder, getWorkspaceFolders } from '../workspace.apis'; 3 | 4 | export function resolveVariables(value: string, project?: Uri, env?: { [key: string]: string }): string { 5 | const substitutions = new Map(); 6 | const home = process.env.HOME || process.env.USERPROFILE; 7 | if (home) { 8 | substitutions.set('${userHome}', home); 9 | } 10 | 11 | if (project) { 12 | substitutions.set('${pythonProject}', project.fsPath); 13 | } 14 | 15 | const workspace = project ? getWorkspaceFolder(project) : undefined; 16 | if (workspace) { 17 | substitutions.set('${workspaceFolder}', workspace.uri.fsPath); 18 | } 19 | substitutions.set('${cwd}', process.cwd()); 20 | (getWorkspaceFolders() ?? []).forEach((w) => { 21 | substitutions.set('${workspaceFolder:' + w.name + '}', w.uri.fsPath); 22 | }); 23 | 24 | const substEnv = env || process.env; 25 | if (substEnv) { 26 | for (const [key, value] of Object.entries(substEnv)) { 27 | if (value && key.length > 0) { 28 | substitutions.set('${env:' + key + '}', value); 29 | } 30 | } 31 | } 32 | 33 | let result = value; 34 | substitutions.forEach((v, k) => { 35 | while (k.length > 0 && result.indexOf(k) >= 0) { 36 | result = result.replace(k, v); 37 | } 38 | }); 39 | return result; 40 | } 41 | -------------------------------------------------------------------------------- /src/common/utils/deferred.ts: -------------------------------------------------------------------------------- 1 | export interface Deferred { 2 | readonly promise: Promise; 3 | readonly resolved: boolean; 4 | readonly rejected: boolean; 5 | readonly completed: boolean; 6 | resolve(value?: T | PromiseLike): void; 7 | reject(reason?: string | Error | Record | unknown): void; 8 | } 9 | 10 | class DeferredImpl implements Deferred { 11 | promise: Promise; 12 | private _resolve!: (value: T | PromiseLike) => void; 13 | private _reject!: (reason?: string | Error | Record | unknown) => void; 14 | resolved: boolean = false; 15 | rejected: boolean = false; 16 | completed: boolean = false; 17 | 18 | constructor() { 19 | this.promise = new Promise((resolve, reject) => { 20 | this._resolve = resolve; 21 | this._reject = reject; 22 | }); 23 | } 24 | 25 | resolve(value: T | PromiseLike): void { 26 | if (!this.completed) { 27 | this._resolve(value); 28 | this.resolved = true; 29 | this.completed = true; 30 | } 31 | } 32 | 33 | reject(reason?: string | Error | Record | unknown): void { 34 | if (!this.completed) { 35 | this._reject(reason); 36 | this.rejected = true; 37 | this.completed = true; 38 | } 39 | } 40 | } 41 | 42 | export function createDeferred(): Deferred { 43 | return new DeferredImpl(); 44 | } 45 | -------------------------------------------------------------------------------- /.github/prompts/add-telemetry.prompt.md: -------------------------------------------------------------------------------- 1 | --- 2 | mode: agent 3 | --- 4 | 5 | If the user does not specify an event name or properties, pick an informative and descriptive name for the telemetry event based on the task or feature. Add properties as you see fit to collect the necessary information to achieve the telemetry goal, ensuring they are relevant and useful for diagnostics or analytics. 6 | 7 | When adding telemetry: 8 | 9 | - If the user wants to record when an action is started (such as a command invocation), place the telemetry call at the start of the handler or function. 10 | - If the user wants to record successful completions or outcomes, place the telemetry call at the end of the action, after the operation has succeeded (and optionally, record errors or failures as well). 11 | 12 | Instructions to add a new telemetry event: 13 | 14 | 1. Add a new event name to the `EventNames` enum in `src/common/telemetry/constants.ts`. 15 | 2. Add a corresponding entry to the `IEventNamePropertyMapping` interface in the same file, including a GDPR comment and the expected properties. 16 | 3. In the relevant code location, call `sendTelemetryEvent` with the new event name and required properties. Example: 17 | ```typescript 18 | sendTelemetryEvent(EventNames.YOUR_EVENT_NAME, undefined, { property: value }); 19 | ``` 20 | 4. If the event is triggered by a command, ensure the call is placed at the start of the command handler. 21 | 22 | Expected output: The new event is tracked in telemetry and follows the GDPR and codebase conventions. 23 | -------------------------------------------------------------------------------- /src/managers/common/types.ts: -------------------------------------------------------------------------------- 1 | import { Uri } from 'vscode'; 2 | 3 | export interface Installable { 4 | /** 5 | * The name of the package, requirements, lock files, or step name. 6 | */ 7 | readonly name: string; 8 | 9 | /** 10 | * The name of the package, requirements, pyproject.toml or any other project file, etc. 11 | */ 12 | readonly displayName: string; 13 | 14 | /** 15 | * Arguments passed to the package manager to install the package. 16 | * 17 | * @example 18 | * ['debugpy==1.8.7'] for `pip install debugpy==1.8.7`. 19 | * ['--pre', 'debugpy'] for `pip install --pre debugpy`. 20 | * ['-r', 'requirements.txt'] for `pip install -r requirements.txt`. 21 | */ 22 | readonly args?: string[]; 23 | 24 | /** 25 | * Installable group name, this will be used to group installable items in the UI. 26 | * 27 | * @example 28 | * `Requirements` for any requirements file. 29 | * `Packages` for any package. 30 | */ 31 | readonly group?: string; 32 | 33 | /** 34 | * Description about the installable item. This can also be path to the requirements, 35 | * version of the package, or any other project file path. 36 | */ 37 | readonly description?: string; 38 | 39 | /** 40 | * External Uri to the package on pypi or docs. 41 | * @example 42 | * https://pypi.org/project/debugpy/ for `debugpy`. 43 | */ 44 | readonly uri?: Uri; 45 | } 46 | export interface IDisposable { 47 | dispose(): void | undefined | Promise; 48 | } 49 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "watch", 9 | "problemMatcher": "$ts-webpack-watch", 10 | "isBackground": true, 11 | "presentation": { 12 | "reveal": "never", 13 | "group": "watchers" 14 | }, 15 | "group": { 16 | "kind": "build", 17 | "isDefault": true 18 | } 19 | }, 20 | { 21 | "type": "npm", 22 | "script": "watch-tests", 23 | "problemMatcher": "$tsc-watch", 24 | "isBackground": true, 25 | "presentation": { 26 | "reveal": "never", 27 | "group": "watchers" 28 | } 29 | }, 30 | { 31 | "label": "tasks: build", 32 | "dependsOn": ["npm: watch", "npm: watch-tests"], 33 | "problemMatcher": [], 34 | "presentation": { 35 | "reveal": "never", 36 | "group": "watchers" 37 | } 38 | }, 39 | { 40 | "type": "npm", 41 | "script": "unittest", 42 | "dependsOn": ["tasks: watch-tests"], 43 | "problemMatcher": "$tsc", 44 | "presentation": { 45 | "reveal": "never", 46 | "group": "test" 47 | }, 48 | "group": { 49 | "kind": "test", 50 | "isDefault": false 51 | } 52 | } 53 | ] 54 | } 55 | -------------------------------------------------------------------------------- /.github/workflows/pr-check.yml: -------------------------------------------------------------------------------- 1 | name: PR/CI Check 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches-ignore: 7 | - main 8 | - release* 9 | 10 | env: 11 | NODE_VERSION: '22.21.1' 12 | 13 | jobs: 14 | build-vsix: 15 | name: Create VSIX 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | 22 | - name: Build VSIX 23 | uses: ./.github/actions/build-vsix 24 | with: 25 | node_version: ${{ env.NODE_VERSION }} 26 | 27 | lint: 28 | name: Lint 29 | runs-on: ubuntu-latest 30 | 31 | steps: 32 | - name: Checkout 33 | uses: actions/checkout@v4 34 | 35 | - name: Install Node 36 | uses: actions/setup-node@v4 37 | with: 38 | node-version: ${{ env.NODE_VERSION }} 39 | cache: 'npm' 40 | 41 | - name: Install Dependencies 42 | run: npm ci 43 | 44 | - name: Run Linter 45 | run: npm run lint 46 | 47 | ts-unit-tests: 48 | name: TypeScript Unit Tests 49 | runs-on: ${{ matrix.os }} 50 | strategy: 51 | fail-fast: false 52 | matrix: 53 | os: [ubuntu-latest, windows-latest] 54 | 55 | steps: 56 | - name: Checkout 57 | uses: actions/checkout@v4 58 | 59 | - name: Install Node 60 | uses: actions/setup-node@v4 61 | with: 62 | node-version: ${{ env.NODE_VERSION }} 63 | cache: 'npm' 64 | 65 | - name: Install Dependencies 66 | run: npm ci 67 | 68 | - name: Compile Tests 69 | run: npm run pretest 70 | 71 | - name: Localization 72 | run: npx @vscode/l10n-dev@latest export ./src 73 | 74 | - name: Run Tests 75 | run: npm run unittest 76 | -------------------------------------------------------------------------------- /src/vscode.proposed.terminalShellEnv.d.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | declare module 'vscode' { 7 | // @anthonykim1 @tyriar https://github.com/microsoft/vscode/issues/227467 8 | 9 | export interface TerminalShellIntegrationEnvironment { 10 | /** 11 | * The dictionary of environment variables. 12 | */ 13 | value: { [key: string]: string | undefined } | undefined; 14 | 15 | /** 16 | * Whether the environment came from a trusted source and is therefore safe to use its 17 | * values in a manner that could lead to execution of arbitrary code. If this value is 18 | * `false`, {@link value} should either not be used for something that could lead to arbitrary 19 | * code execution, or the user should be warned beforehand. 20 | * 21 | * This is `true` only when the environment was reported explicitly and it used a nonce for 22 | * verification. 23 | */ 24 | isTrusted: boolean; 25 | } 26 | 27 | export interface TerminalShellIntegration { 28 | /** 29 | * The environment of the shell process. This is undefined if the shell integration script 30 | * does not send the environment. 31 | */ 32 | readonly env: TerminalShellIntegrationEnvironment; 33 | } 34 | 35 | // TODO: Is it fine that this shares onDidChangeTerminalShellIntegration with cwd and the shellIntegration object itself? 36 | } 37 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | 3 | 'use strict'; 4 | 5 | const path = require('path'); 6 | 7 | //@ts-check 8 | /** @typedef {import('webpack').Configuration} WebpackConfig **/ 9 | 10 | /** @type WebpackConfig */ 11 | const extensionConfig = { 12 | target: 'node', // VS Code extensions run in a Node.js-context 📖 -> https://webpack.js.org/configuration/node/ 13 | mode: 'none', // this leaves the source code as close as possible to the original (when packaging we set this to 'production') 14 | 15 | entry: './src/extension.ts', // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/ 16 | output: { 17 | // the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/ 18 | path: path.resolve(__dirname, 'dist'), 19 | filename: 'extension.js', 20 | libraryTarget: 'commonjs2' 21 | }, 22 | externals: { 23 | vscode: 'commonjs vscode' // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/ 24 | // modules added here also need to be added in the .vscodeignore file 25 | }, 26 | resolve: { 27 | // support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader 28 | extensions: ['.ts', '.js'] 29 | }, 30 | module: { 31 | rules: [ 32 | { 33 | test: /\.ts$/, 34 | exclude: /node_modules/, 35 | use: [ 36 | { 37 | loader: 'ts-loader' 38 | } 39 | ] 40 | } 41 | ] 42 | }, 43 | devtool: 'source-map', 44 | infrastructureLogging: { 45 | level: "log", // enables logging required for problem matchers 46 | }, 47 | }; 48 | module.exports = [ extensionConfig ]; -------------------------------------------------------------------------------- /.github/workflows/push-check.yml: -------------------------------------------------------------------------------- 1 | name: Push Check 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'main' 7 | - 'release' 8 | - 'release/*' 9 | - 'release-*' 10 | 11 | env: 12 | NODE_VERSION: '22.21.1' 13 | 14 | jobs: 15 | build-vsix: 16 | name: Create VSIX 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v4 22 | 23 | - name: Build VSIX 24 | uses: ./.github/actions/build-vsix 25 | with: 26 | node_version: ${{ env.NODE_VERSION }} 27 | 28 | lint: 29 | name: Lint 30 | runs-on: ubuntu-latest 31 | 32 | steps: 33 | - name: Checkout 34 | uses: actions/checkout@v4 35 | 36 | - name: Install Node 37 | uses: actions/setup-node@v4 38 | with: 39 | node-version: ${{ env.NODE_VERSION }} 40 | cache: 'npm' 41 | 42 | - name: Install Dependencies 43 | run: npm ci 44 | 45 | - name: Run Linter 46 | run: npm run lint 47 | 48 | ts-unit-tests: 49 | name: TypeScript Unit Tests 50 | runs-on: ${{ matrix.os }} 51 | strategy: 52 | fail-fast: false 53 | matrix: 54 | os: [ubuntu-latest, windows-latest] 55 | 56 | steps: 57 | - name: Checkout 58 | uses: actions/checkout@v4 59 | 60 | - name: Install Node 61 | uses: actions/setup-node@v4 62 | with: 63 | node-version: ${{ env.NODE_VERSION }} 64 | cache: 'npm' 65 | 66 | - name: Install Dependencies 67 | run: npm ci 68 | 69 | - name: Compile Tests 70 | run: npm run pretest 71 | 72 | - name: Localization 73 | run: npx @vscode/l10n-dev@latest export ./src 74 | 75 | - name: Run Tests 76 | run: npm run unittest 77 | -------------------------------------------------------------------------------- /src/features/views/pythonStatusBar.ts: -------------------------------------------------------------------------------- 1 | import { Disposable, StatusBarAlignment, StatusBarItem, ThemeColor } from 'vscode'; 2 | import { PythonEnvironment } from '../../api'; 3 | import { createStatusBarItem } from '../../common/window.apis'; 4 | 5 | export interface PythonStatusBar extends Disposable { 6 | show(env?: PythonEnvironment): void; 7 | hide(): void; 8 | } 9 | 10 | export class PythonStatusBarImpl implements Disposable { 11 | private disposables: Disposable[] = []; 12 | private readonly statusBarItem: StatusBarItem; 13 | constructor() { 14 | this.statusBarItem = createStatusBarItem('python.interpreterDisplay', StatusBarAlignment.Right, 100); 15 | this.statusBarItem.command = 'python-envs.set'; 16 | this.statusBarItem.name = 'Python Interpreter'; 17 | this.statusBarItem.tooltip = 'Select Python Interpreter'; 18 | this.statusBarItem.text = '$(loading~spin)'; 19 | this.statusBarItem.show(); 20 | this.disposables.push(this.statusBarItem); 21 | } 22 | 23 | public show(env?: PythonEnvironment) { 24 | if (env) { 25 | this.statusBarItem.text = env.displayName ?? 'Select Python Interpreter'; 26 | this.statusBarItem.tooltip = env.environmentPath?.fsPath ?? ''; 27 | } else { 28 | this.statusBarItem.text = 'Select Python Interpreter'; 29 | this.statusBarItem.tooltip = 'Select Python Interpreter'; 30 | } 31 | this.statusBarItem.backgroundColor = env ? undefined : new ThemeColor('statusBarItem.warningBackground'); 32 | this.statusBarItem.show(); 33 | } 34 | 35 | public hide() { 36 | this.statusBarItem.hide(); 37 | } 38 | 39 | dispose() { 40 | this.disposables.forEach((d) => d.dispose()); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/test/mocks/mockWorkspaceConfig.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | import { ConfigurationTarget, WorkspaceConfiguration } from 'vscode'; 4 | 5 | type SectionType = { 6 | key: string; 7 | defaultValue?: T | undefined; 8 | globalValue?: T | undefined; 9 | globalLanguageValue?: T | undefined; 10 | workspaceValue?: T | undefined; 11 | workspaceLanguageValue?: T | undefined; 12 | workspaceFolderValue?: T | undefined; 13 | workspaceFolderLanguageValue?: T | undefined; 14 | }; 15 | 16 | export class MockWorkspaceConfiguration implements WorkspaceConfiguration { 17 | private values = new Map(); 18 | 19 | constructor(defaultSettings?: { [key: string]: unknown }) { 20 | if (defaultSettings) { 21 | const keys = [...Object.keys(defaultSettings)]; 22 | keys.forEach((k) => this.values.set(k, defaultSettings[k])); 23 | } 24 | } 25 | 26 | public get(key: string, defaultValue?: T): T | undefined { 27 | if (this.values.has(key)) { 28 | return this.values.get(key) as T; 29 | } 30 | 31 | return arguments.length > 1 ? defaultValue : undefined; 32 | } 33 | 34 | public has(section: string): boolean { 35 | return this.values.has(section); 36 | } 37 | 38 | public inspect(section: string): SectionType | undefined { 39 | return this.values.get(section) as SectionType; 40 | } 41 | 42 | public update( 43 | section: string, 44 | value: unknown, 45 | 46 | _configurationTarget?: boolean | ConfigurationTarget | undefined, 47 | ): Promise { 48 | this.values.set(section, value); 49 | return Promise.resolve(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /examples/sample1/webpack.config.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | 3 | 'use strict'; 4 | 5 | const path = require('path'); 6 | 7 | //@ts-check 8 | /** @typedef {import('webpack').Configuration} WebpackConfig **/ 9 | 10 | /** @type WebpackConfig */ 11 | const extensionConfig = { 12 | target: 'node', // VS Code extensions run in a Node.js-context 📖 -> https://webpack.js.org/configuration/node/ 13 | mode: 'none', // this leaves the source code as close as possible to the original (when packaging we set this to 'production') 14 | 15 | entry: './src/extension.ts', // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/ 16 | output: { 17 | // the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/ 18 | path: path.resolve(__dirname, 'dist'), 19 | filename: 'extension.js', 20 | libraryTarget: 'commonjs2' 21 | }, 22 | externals: { 23 | vscode: 'commonjs vscode' // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/ 24 | // modules added here also need to be added in the .vscodeignore file 25 | }, 26 | resolve: { 27 | // support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader 28 | extensions: ['.ts', '.js'] 29 | }, 30 | module: { 31 | rules: [ 32 | { 33 | test: /\.ts$/, 34 | exclude: /node_modules/, 35 | use: [ 36 | { 37 | loader: 'ts-loader' 38 | } 39 | ] 40 | } 41 | ] 42 | }, 43 | devtool: 'nosources-source-map', 44 | infrastructureLogging: { 45 | level: "log", // enables logging required for problem matchers 46 | }, 47 | }; 48 | module.exports = [ extensionConfig ]; -------------------------------------------------------------------------------- /src/common/logging.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import * as util from 'util'; 5 | import { Disposable, LogOutputChannel } from 'vscode'; 6 | 7 | type Arguments = unknown[]; 8 | class OutputChannelLogger { 9 | constructor(private readonly channel: LogOutputChannel) {} 10 | 11 | public traceLog(...data: Arguments): void { 12 | this.channel.appendLine(util.format(...data)); 13 | } 14 | 15 | public traceError(...data: Arguments): void { 16 | this.channel.error(util.format(...data)); 17 | } 18 | 19 | public traceWarn(...data: Arguments): void { 20 | this.channel.warn(util.format(...data)); 21 | } 22 | 23 | public traceInfo(...data: Arguments): void { 24 | this.channel.info(util.format(...data)); 25 | } 26 | 27 | public traceVerbose(...data: Arguments): void { 28 | this.channel.debug(util.format(...data)); 29 | } 30 | } 31 | 32 | let channel: OutputChannelLogger | undefined; 33 | export function registerLogger(logChannel: LogOutputChannel): Disposable { 34 | channel = new OutputChannelLogger(logChannel); 35 | return { 36 | dispose: () => { 37 | channel = undefined; 38 | }, 39 | }; 40 | } 41 | 42 | export function traceLog(...args: Arguments): void { 43 | channel?.traceLog(...args); 44 | } 45 | 46 | export function traceError(...args: Arguments): void { 47 | channel?.traceError(...args); 48 | } 49 | 50 | export function traceWarn(...args: Arguments): void { 51 | channel?.traceWarn(...args); 52 | } 53 | 54 | export function traceInfo(...args: Arguments): void { 55 | channel?.traceInfo(...args); 56 | } 57 | 58 | export function traceVerbose(...args: Arguments): void { 59 | channel?.traceVerbose(...args); 60 | } 61 | -------------------------------------------------------------------------------- /src/test/mocks/vsc/copilotTools.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A language model response part containing a piece of text, returned from a {@link LanguageModelChatResponse}. 3 | */ 4 | export class LanguageModelTextPart { 5 | /** 6 | * The text content of the part. 7 | */ 8 | value: string; 9 | 10 | /** 11 | * Construct a text part with the given content. 12 | * @param value The text content of the part. 13 | */ 14 | constructor(value: string) { 15 | this.value = value; 16 | } 17 | } 18 | 19 | /** 20 | * A result returned from a tool invocation. If using `@vscode/prompt-tsx`, this result may be rendered using a `ToolResult`. 21 | */ 22 | export class LanguageModelToolResult { 23 | /** 24 | * A list of tool result content parts. Includes `unknown` becauses this list may be extended with new content types in 25 | * the future. 26 | * @see {@link lm.invokeTool}. 27 | */ 28 | content: Array; 29 | 30 | /** 31 | * Create a LanguageModelToolResult 32 | * @param content A list of tool result content parts 33 | */ 34 | constructor(content: Array) { 35 | this.content = content; 36 | } 37 | } 38 | 39 | /** 40 | * A language model response part containing a PromptElementJSON from `@vscode/prompt-tsx`. 41 | * @see {@link LanguageModelToolResult} 42 | */ 43 | export class LanguageModelPromptTsxPart { 44 | /** 45 | * The value of the part. 46 | */ 47 | value: unknown; 48 | 49 | /** 50 | * Construct a prompt-tsx part with the given content. 51 | * @param value The value of the part, the result of `renderPromptElementJSON` from `@vscode/prompt-tsx`. 52 | */ 53 | constructor(value: unknown) { 54 | this.value = value; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /.github/workflows/triage-info-needed.yml: -------------------------------------------------------------------------------- 1 | name: Triage "info-needed" label 2 | 3 | on: 4 | issue_comment: 5 | types: [created] 6 | 7 | env: 8 | TRIAGERS: '["karthiknadig","eleanorjboyd","anthonykim1"]' 9 | 10 | jobs: 11 | add_label: 12 | if: contains(github.event.issue.labels.*.name, 'triage-needed') && !contains(github.event.issue.labels.*.name, 'info-needed') 13 | runs-on: ubuntu-latest 14 | permissions: 15 | issues: write 16 | steps: 17 | - name: Checkout Actions 18 | uses: actions/checkout@v4 19 | with: 20 | repository: 'microsoft/vscode-github-triage-actions' 21 | ref: stable 22 | path: ./actions 23 | persist-credentials: false 24 | 25 | - name: Install Actions 26 | run: npm install --production --prefix ./actions 27 | 28 | - name: Add "info-needed" label 29 | uses: ./actions/python-triage-info-needed 30 | with: 31 | triagers: ${{ env.TRIAGERS }} 32 | action: 'add' 33 | token: ${{secrets.GITHUB_TOKEN}} 34 | 35 | remove_label: 36 | if: contains(github.event.issue.labels.*.name, 'info-needed') && contains(github.event.issue.labels.*.name, 'triage-needed') 37 | runs-on: ubuntu-latest 38 | permissions: 39 | issues: write 40 | steps: 41 | - name: Checkout Actions 42 | uses: actions/checkout@v4 43 | with: 44 | repository: 'microsoft/vscode-github-triage-actions' 45 | ref: stable 46 | path: ./actions 47 | persist-credentials: false 48 | 49 | - name: Install Actions 50 | run: npm install --production --prefix ./actions 51 | 52 | - name: Remove "info-needed" label 53 | uses: ./actions/python-triage-info-needed 54 | with: 55 | triagers: ${{ env.TRIAGERS }} 56 | action: 'remove' 57 | token: ${{secrets.GITHUB_TOKEN}} 58 | -------------------------------------------------------------------------------- /src/test/managers/builtin/piplist1.expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | { "name": "argcomplete", "version": "3.1.2" }, 4 | { "name": "black", "version": "23.12.1" }, 5 | { "name": "build", "version": "1.2.1" }, 6 | { "name": "click", "version": "8.1.7" }, 7 | { "name": "colorama", "version": "0.4.6" }, 8 | { "name": "colorlog", "version": "6.7.0" }, 9 | { "name": "coverage", "version": "7.6.1" }, 10 | { "name": "distlib", "version": "0.3.7" }, 11 | { "name": "exceptiongroup", "version": "1.1.3" }, 12 | { "name": "filelock", "version": "3.13.1" }, 13 | { "name": "importlib_metadata", "version": "7.1.0" }, 14 | { "name": "iniconfig", "version": "2.0.0" }, 15 | { "name": "isort", "version": "5.13.2" }, 16 | { "name": "mypy-extensions", "version": "1.0.0" }, 17 | { "name": "namedpipe", "version": "0.1.1" }, 18 | { "name": "nox", "version": "2024.3.2" }, 19 | { "name": "packaging", "version": "23.2" }, 20 | { "name": "pathspec", "version": "0.12.1" }, 21 | { "name": "pip", "version": "24.0" }, 22 | { "name": "pip-tools", "version": "7.4.1" }, 23 | { "name": "platformdirs", "version": "3.11.0" }, 24 | { "name": "pluggy", "version": "1.4.0" }, 25 | { "name": "pyproject_hooks", "version": "1.1.0" }, 26 | { "name": "pytest", "version": "8.1.1" }, 27 | { "name": "pytest-cov", "version": "5.0.0" }, 28 | { "name": "pywin32", "version": "306" }, 29 | { "name": "ruff", "version": "0.7.4" }, 30 | { "name": "setuptools", "version": "56.0.0" }, 31 | { "name": "tomli", "version": "2.0.1" }, 32 | { "name": "typing_extensions", "version": "4.9.0" }, 33 | { "name": "virtualenv", "version": "20.24.6" }, 34 | { "name": "wheel", "version": "0.43.0" }, 35 | { "name": "zipp", "version": "3.19.2" } 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /src/test/managers/builtin/piplist2.expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | { "name": "argcomplete", "version": "3.1.2" }, 4 | { "name": "black", "version": "23.12.1" }, 5 | { "name": "build", "version": "1.2.1" }, 6 | { "name": "click", "version": "8.1.7" }, 7 | { "name": "colorama", "version": "0.4.6" }, 8 | { "name": "colorlog", "version": "6.7.0" }, 9 | { "name": "coverage", "version": "7.6.1" }, 10 | { "name": "distlib", "version": "0.3.7" }, 11 | { "name": "exceptiongroup", "version": "1.1.3" }, 12 | { "name": "filelock", "version": "3.13.1" }, 13 | { "name": "importlib_metadata", "version": "7.1.0" }, 14 | { "name": "iniconfig", "version": "2.0.0" }, 15 | { "name": "isort", "version": "5.13.2" }, 16 | { "name": "mypy-extensions", "version": "1.0.0" }, 17 | { "name": "namedpipe", "version": "0.1.1" }, 18 | { "name": "nox", "version": "2024.3.2" }, 19 | { "name": "packaging", "version": "23.2" }, 20 | { "name": "pathspec", "version": "0.12.1" }, 21 | { "name": "pip", "version": "24.0" }, 22 | { "name": "pip-tools", "version": "7.4.1" }, 23 | { "name": "platformdirs", "version": "3.11.0" }, 24 | { "name": "pluggy", "version": "1.4.0" }, 25 | { "name": "pyproject_hooks", "version": "1.1.0" }, 26 | { "name": "pytest", "version": "8.1.1" }, 27 | { "name": "pytest-cov", "version": "5.0.0" }, 28 | { "name": "pywin32", "version": "306" }, 29 | { "name": "ruff", "version": "0.7.4" }, 30 | { "name": "setuptools", "version": "56.0.0" }, 31 | { "name": "tomli", "version": "2.0.1" }, 32 | { "name": "typing_extensions", "version": "4.9.0" }, 33 | { "name": "virtualenv", "version": "20.24.6" }, 34 | { "name": "wheel", "version": "0.43.0" }, 35 | { "name": "zipp", "version": "3.19.2" } 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /src/managers/conda/main.ts: -------------------------------------------------------------------------------- 1 | import { Disposable, LogOutputChannel } from 'vscode'; 2 | import { PythonEnvironmentApi } from '../../api'; 3 | import { traceInfo } from '../../common/logging'; 4 | import { getPythonApi } from '../../features/pythonApi'; 5 | import { PythonProjectManager } from '../../internal.api'; 6 | import { NativePythonFinder } from '../common/nativePythonFinder'; 7 | import { notifyMissingManagerIfDefault } from '../common/utils'; 8 | import { CondaEnvManager } from './condaEnvManager'; 9 | import { CondaPackageManager } from './condaPackageManager'; 10 | import { CondaSourcingStatus, constructCondaSourcingStatus } from './condaSourcingUtils'; 11 | import { getConda } from './condaUtils'; 12 | 13 | export async function registerCondaFeatures( 14 | nativeFinder: NativePythonFinder, 15 | disposables: Disposable[], 16 | log: LogOutputChannel, 17 | projectManager: PythonProjectManager, 18 | ): Promise { 19 | const api: PythonEnvironmentApi = await getPythonApi(); 20 | 21 | try { 22 | // get Conda will return only ONE conda manager, that correlates to a single conda install 23 | const condaPath: string = await getConda(nativeFinder); 24 | const sourcingStatus: CondaSourcingStatus = await constructCondaSourcingStatus(condaPath); 25 | traceInfo(sourcingStatus.toString()); 26 | 27 | const envManager = new CondaEnvManager(nativeFinder, api, log); 28 | const packageManager = new CondaPackageManager(api, log); 29 | 30 | envManager.sourcingInformation = sourcingStatus; 31 | 32 | disposables.push( 33 | envManager, 34 | packageManager, 35 | api.registerEnvironmentManager(envManager), 36 | api.registerPackageManager(packageManager), 37 | ); 38 | } catch (ex) { 39 | traceInfo('Conda not found, turning off conda features.', ex); 40 | await notifyMissingManagerIfDefault('ms-python.python:conda', projectManager, api); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/common/utils/fileNameUtils.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as fsapi from 'fs-extra'; 3 | import { KNOWN_FILES, KNOWN_TEMPLATE_ENDINGS } from '../constants'; 4 | import { Uri } from 'vscode'; 5 | import { getWorkspaceFolders } from '../workspace.apis'; 6 | 7 | export function isPythonProjectFile(fileName: string): boolean { 8 | const baseName = path.basename(fileName).toLowerCase(); 9 | const fsPath = path.normalize(fileName).toLowerCase(); 10 | return ( 11 | KNOWN_FILES.some((file) => baseName === file) || 12 | KNOWN_TEMPLATE_ENDINGS.some((ending) => baseName.endsWith(ending)) || 13 | (baseName.endsWith('.txt') && (baseName.includes('requirements') || baseName.includes('constraints'))) || 14 | (baseName.endsWith('.in') && baseName.includes('requirements')) || 15 | (fsPath.includes(`requirements${path.sep}`) && (fsPath.endsWith('.txt') || fsPath.endsWith('.in'))) 16 | ); 17 | } 18 | 19 | export async function getAbsolutePath(fsPath: string): Promise { 20 | if (path.isAbsolute(fsPath)) { 21 | return Uri.file(fsPath); 22 | } 23 | 24 | const workspaceFolders = getWorkspaceFolders() ?? []; 25 | if (workspaceFolders.length > 0) { 26 | if (workspaceFolders.length === 1) { 27 | const absPath = path.resolve(workspaceFolders[0].uri.fsPath, fsPath); 28 | if (await fsapi.pathExists(absPath)) { 29 | return Uri.file(absPath); 30 | } 31 | } else { 32 | const workspaces = Array.from(workspaceFolders) 33 | .sort((a, b) => a.uri.fsPath.length - b.uri.fsPath.length) 34 | .reverse(); 35 | for (const folder of workspaces) { 36 | const absPath = path.resolve(folder.uri.fsPath, fsPath); 37 | if (await fsapi.pathExists(absPath)) { 38 | return Uri.file(absPath); 39 | } 40 | } 41 | } 42 | } 43 | return undefined; 44 | } 45 | -------------------------------------------------------------------------------- /src/test/managers/builtin/pipListUtils.unit.test.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import * as fs from 'fs-extra'; 3 | import * as path from 'path'; 4 | import { parsePipList } from '../../../managers/builtin/pipListUtils'; 5 | import { EXTENSION_TEST_ROOT } from '../../constants'; 6 | 7 | const TEST_DATA_ROOT = path.join(EXTENSION_TEST_ROOT, 'managers', 'builtin'); 8 | 9 | suite('Pip List Parser tests', () => { 10 | const testNames = ['piplist1', 'piplist2', 'piplist3']; 11 | 12 | testNames.forEach((testName) => { 13 | test(`Test parsing pip list output ${testName}`, async () => { 14 | const pipListOutput = await fs.readFile(path.join(TEST_DATA_ROOT, `${testName}.actual.txt`), 'utf8'); 15 | const expected = JSON.parse( 16 | await fs.readFile(path.join(TEST_DATA_ROOT, `${testName}.expected.json`), 'utf8'), 17 | ); 18 | 19 | const actualPackages = parsePipList(pipListOutput); 20 | 21 | assert.equal(actualPackages.length, expected.packages.length, 'Unexpected number of packages'); 22 | actualPackages.forEach((actualPackage) => { 23 | const expectedPackage = expected.packages.find( 24 | (item: { name: string }) => item.name === actualPackage.name, 25 | ); 26 | assert.ok(expectedPackage, `Package ${actualPackage.name} not found in expected packages`); 27 | assert.equal(actualPackage.version, expectedPackage.version, 'Version mismatch'); 28 | }); 29 | 30 | expected.packages.forEach((expectedPackage: { name: string; version: string }) => { 31 | const actualPackage = actualPackages.find((item) => item.name === expectedPackage.name); 32 | assert.ok(actualPackage, `Package ${expectedPackage.name} not found in actual packages`); 33 | assert.equal(actualPackage.version, expectedPackage.version, 'Version mismatch'); 34 | }); 35 | }); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Run Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "args": ["--extensionDevelopmentPath=${workspaceFolder}"], 13 | "outFiles": ["${workspaceFolder}/dist/**/*.js"], 14 | "preLaunchTask": "npm: watch" 15 | }, 16 | { 17 | "name": "Unit Tests", 18 | "type": "node", 19 | "request": "launch", 20 | "args": [ 21 | "-u=tdd", 22 | "--timeout=180000", 23 | "--colors", 24 | "--recursive", 25 | //"--grep", "", 26 | "--require=out/test/unittests.js", 27 | "./out/test/**/*.unit.test.js" 28 | ], 29 | "internalConsoleOptions": "openOnSessionStart", 30 | "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", 31 | "skipFiles": ["/**"], 32 | "outFiles": ["${workspaceFolder}/out/**/*.js", "!${workspaceFolder}/**/node_modules**/*"], 33 | "preLaunchTask": "npm: watch-tests" 34 | }, 35 | { 36 | "name": "Extension Tests", 37 | "type": "extensionHost", 38 | "request": "launch", 39 | "args": [ 40 | "--extensionDevelopmentPath=${workspaceFolder}", 41 | "--extensionTestsPath=${workspaceFolder}/out/test/" 42 | ], 43 | "outFiles": ["${workspaceFolder}/out/**/*.js", "${workspaceFolder}/dist/**/*.js"], 44 | "preLaunchTask": "${defaultBuildTask}" 45 | } 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /src/common/pickers/projects.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { QuickPickItem } from 'vscode'; 3 | import { PythonProject } from '../../api'; 4 | import { showQuickPick, showQuickPickWithButtons } from '../window.apis'; 5 | import { Pickers } from '../localize'; 6 | 7 | interface ProjectQuickPickItem extends QuickPickItem { 8 | project: PythonProject; 9 | } 10 | 11 | export async function pickProject(projects: ReadonlyArray): Promise { 12 | if (projects.length > 1) { 13 | const items: ProjectQuickPickItem[] = projects.map((pw) => ({ 14 | label: path.basename(pw.uri.fsPath), 15 | description: pw.uri.fsPath, 16 | project: pw, 17 | })); 18 | const item = await showQuickPick(items, { 19 | placeHolder: Pickers.Project.selectProject, 20 | ignoreFocusOut: true, 21 | }); 22 | if (item) { 23 | return item.project; 24 | } 25 | } else if (projects.length === 1) { 26 | return projects[0]; 27 | } 28 | return undefined; 29 | } 30 | 31 | export async function pickProjectMany( 32 | projects: readonly PythonProject[], 33 | showBackButton?: boolean, 34 | ): Promise { 35 | if (projects.length > 1) { 36 | const items: ProjectQuickPickItem[] = projects.map((pw) => ({ 37 | label: path.basename(pw.uri.fsPath), 38 | description: pw.uri.fsPath, 39 | project: pw, 40 | })); 41 | const item = await showQuickPickWithButtons(items, { 42 | placeHolder: Pickers.Project.selectProjects, 43 | ignoreFocusOut: true, 44 | canPickMany: true, 45 | showBackButton: showBackButton, 46 | }); 47 | if (Array.isArray(item)) { 48 | return item.map((p) => p.project); 49 | } 50 | } else if (projects.length === 1) { 51 | return [...projects]; 52 | } else if (projects.length === 0) { 53 | return []; 54 | } 55 | return undefined; 56 | } 57 | -------------------------------------------------------------------------------- /files/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/features/terminal/shells/providers.ts: -------------------------------------------------------------------------------- 1 | import { isWindows } from '../../../common/utils/platformUtils'; 2 | import { ShellConstants } from '../../common/shellConstants'; 3 | import { BashEnvsProvider, ZshEnvsProvider } from './bash/bashEnvs'; 4 | import { BashStartupProvider, GitBashStartupProvider, ZshStartupProvider } from './bash/bashStartup'; 5 | import { CmdEnvsProvider } from './cmd/cmdEnvs'; 6 | import { CmdStartupProvider } from './cmd/cmdStartup'; 7 | import { FishEnvsProvider } from './fish/fishEnvs'; 8 | import { FishStartupProvider } from './fish/fishStartup'; 9 | import { PowerShellEnvsProvider } from './pwsh/pwshEnvs'; 10 | import { PwshStartupProvider } from './pwsh/pwshStartup'; 11 | import { ShellEnvsProvider, ShellStartupScriptProvider } from './startupProvider'; 12 | 13 | export function createShellStartupProviders(): ShellStartupScriptProvider[] { 14 | if (isWindows()) { 15 | return [ 16 | // PowerShell classic is the default on Windows, so it is included here explicitly. 17 | // pwsh is the new PowerShell Core, which is cross-platform and preferred. 18 | new PwshStartupProvider([ShellConstants.PWSH, 'powershell']), 19 | new GitBashStartupProvider(), 20 | new CmdStartupProvider(), 21 | ]; 22 | } 23 | return [ 24 | new PwshStartupProvider([ShellConstants.PWSH]), 25 | new BashStartupProvider(), 26 | new FishStartupProvider(), 27 | new ZshStartupProvider(), 28 | ]; 29 | } 30 | 31 | export function createShellEnvProviders(): ShellEnvsProvider[] { 32 | if (isWindows()) { 33 | return [new PowerShellEnvsProvider(), new BashEnvsProvider(ShellConstants.GITBASH), new CmdEnvsProvider()]; 34 | } 35 | return [ 36 | new PowerShellEnvsProvider(), 37 | new BashEnvsProvider(ShellConstants.BASH), 38 | new FishEnvsProvider(), 39 | new ZshEnvsProvider(), 40 | ]; 41 | } 42 | 43 | export async function clearShellProfileCache(providers: ShellStartupScriptProvider[]): Promise { 44 | await Promise.all(providers.map((provider) => provider.clearCache())); 45 | } 46 | -------------------------------------------------------------------------------- /src/test/common/internalVariables.unit.test.ts: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import { resolveVariables } from '../../common/utils/internalVariables'; 3 | import * as workspaceApi from '../../common/workspace.apis'; 4 | import * as sinon from 'sinon'; 5 | import * as path from 'path'; 6 | import { Uri } from 'vscode'; 7 | 8 | suite('Internal Variable substitution', () => { 9 | let getWorkspaceFolderStub: sinon.SinonStub; 10 | let getWorkspaceFoldersStub: sinon.SinonStub; 11 | 12 | const home = process.env.HOME ?? process.env.USERPROFILE ?? ''; 13 | const project = { name: 'project1', uri: { fsPath: path.join(home, 'workspace1', 'project1') } }; 14 | const workspaceFolder = { name: 'workspace1', uri: { fsPath: path.join(home, 'workspace1') } }; 15 | 16 | setup(() => { 17 | getWorkspaceFolderStub = sinon.stub(workspaceApi, 'getWorkspaceFolder'); 18 | getWorkspaceFoldersStub = sinon.stub(workspaceApi, 'getWorkspaceFolders'); 19 | 20 | getWorkspaceFolderStub.returns(workspaceFolder); 21 | getWorkspaceFoldersStub.returns([workspaceFolder]); 22 | }); 23 | 24 | teardown(() => { 25 | sinon.restore(); 26 | }); 27 | 28 | [ 29 | { variable: '${userHome}', substitution: home }, 30 | { variable: '${pythonProject}', substitution: project.uri.fsPath }, 31 | { variable: '${workspaceFolder}', substitution: workspaceFolder.uri.fsPath }, 32 | { variable: '${workspaceFolder:workspace1}', substitution: workspaceFolder.uri.fsPath }, 33 | { variable: '${cwd}', substitution: process.cwd() }, 34 | process.platform === 'win32' 35 | ? { variable: '${env:USERPROFILE}', substitution: home } 36 | : { variable: '${env:HOME}', substitution: home }, 37 | ].forEach((item) => { 38 | test(`Resolve ${item.variable}`, () => { 39 | // Two times here to ensure that both instances are handled 40 | const value = `Some ${item.variable} text ${item.variable}`; 41 | const result = resolveVariables(value, project.uri as unknown as Uri); 42 | assert.equal(result, `Some ${item.substitution} text ${item.substitution}`); 43 | }); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /src/common/workspace.apis.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import { 3 | CancellationToken, 4 | ConfigurationChangeEvent, 5 | ConfigurationScope, 6 | Disposable, 7 | FileDeleteEvent, 8 | FileSystemWatcher, 9 | GlobPattern, 10 | Uri, 11 | workspace, 12 | WorkspaceConfiguration, 13 | WorkspaceFolder, 14 | WorkspaceFoldersChangeEvent, 15 | } from 'vscode'; 16 | 17 | export function getWorkspaceFolder(uri: Uri): WorkspaceFolder | undefined { 18 | return workspace.getWorkspaceFolder(uri); 19 | } 20 | 21 | export function getWorkspaceFolders(): readonly WorkspaceFolder[] | undefined { 22 | return workspace.workspaceFolders; 23 | } 24 | 25 | export function getWorkspaceFile(): Uri | undefined { 26 | return workspace.workspaceFile; 27 | } 28 | 29 | export function getConfiguration(section?: string, scope?: ConfigurationScope | null): WorkspaceConfiguration { 30 | return workspace.getConfiguration(section, scope); 31 | } 32 | 33 | export function onDidChangeConfiguration(listener: (e: ConfigurationChangeEvent) => any): Disposable { 34 | return workspace.onDidChangeConfiguration(listener); 35 | } 36 | 37 | export function onDidChangeWorkspaceFolders(listener: (e: WorkspaceFoldersChangeEvent) => any): Disposable { 38 | return workspace.onDidChangeWorkspaceFolders(listener); 39 | } 40 | 41 | export function findFiles( 42 | include: GlobPattern, 43 | exclude?: GlobPattern | null, 44 | maxResults?: number, 45 | token?: CancellationToken, 46 | ): Thenable { 47 | return workspace.findFiles(include, exclude, maxResults, token); 48 | } 49 | 50 | export function createFileSystemWatcher( 51 | globPattern: GlobPattern, 52 | ignoreCreateEvents?: boolean, 53 | ignoreChangeEvents?: boolean, 54 | ignoreDeleteEvents?: boolean, 55 | ): FileSystemWatcher { 56 | return workspace.createFileSystemWatcher(globPattern, ignoreCreateEvents, ignoreChangeEvents, ignoreDeleteEvents); 57 | } 58 | 59 | export function onDidDeleteFiles( 60 | listener: (e: FileDeleteEvent) => any, 61 | thisArgs?: any, 62 | disposables?: Disposable[], 63 | ): Disposable { 64 | return workspace.onDidDeleteFiles(listener, thisArgs, disposables); 65 | } 66 | -------------------------------------------------------------------------------- /src/managers/builtin/uvEnvironments.ts: -------------------------------------------------------------------------------- 1 | import { ENVS_EXTENSION_ID } from '../../common/constants'; 2 | import { getWorkspacePersistentState } from '../../common/persistentState'; 3 | 4 | /** 5 | * Persistent storage key for UV-managed virtual environments. 6 | * 7 | * This key is used to store a list of environment paths that were created or identified 8 | * as UV-managed virtual environments. The stored paths correspond to the 9 | * PythonEnvironmentInfo.environmentPath.fsPath values. 10 | */ 11 | export const UV_ENVS_KEY = `${ENVS_EXTENSION_ID}:uv:UV_ENVIRONMENTS`; 12 | 13 | /** 14 | * @returns Array of environment paths (PythonEnvironmentInfo.environmentPath.fsPath values) 15 | * that are known to be UV-managed virtual environments 16 | */ 17 | export async function getUvEnvironments(): Promise { 18 | const state = await getWorkspacePersistentState(); 19 | return (await state.get(UV_ENVS_KEY)) ?? []; 20 | } 21 | 22 | /** 23 | * @param environmentPath The environment path (should be PythonEnvironmentInfo.environmentPath.fsPath) 24 | * to mark as UV-managed. Duplicates are automatically ignored. 25 | */ 26 | export async function addUvEnvironment(environmentPath: string): Promise { 27 | const state = await getWorkspacePersistentState(); 28 | const uvEnvs = await getUvEnvironments(); 29 | if (!uvEnvs.includes(environmentPath)) { 30 | uvEnvs.push(environmentPath); 31 | await state.set(UV_ENVS_KEY, uvEnvs); 32 | } 33 | } 34 | 35 | /** 36 | * @param environmentPath The environment path (PythonEnvironmentInfo.environmentPath.fsPath) 37 | * to remove from UV tracking. No-op if path not found. 38 | */ 39 | export async function removeUvEnvironment(environmentPath: string): Promise { 40 | const state = await getWorkspacePersistentState(); 41 | const uvEnvs = await getUvEnvironments(); 42 | const filtered = uvEnvs.filter((path) => path !== environmentPath); 43 | await state.set(UV_ENVS_KEY, filtered); 44 | } 45 | 46 | /** 47 | * Clears all UV-managed environment paths from the tracking list. 48 | */ 49 | export async function clearUvEnvironments(): Promise { 50 | const state = await getWorkspacePersistentState(); 51 | await state.set(UV_ENVS_KEY, []); 52 | } 53 | -------------------------------------------------------------------------------- /src/features/terminal/shells/cmd/cmdEnvs.ts: -------------------------------------------------------------------------------- 1 | import { EnvironmentVariableCollection } from 'vscode'; 2 | import { PythonEnvironment } from '../../../../api'; 3 | import { traceError } from '../../../../common/logging'; 4 | import { ShellConstants } from '../../../common/shellConstants'; 5 | import { getShellActivationCommand, getShellCommandAsString } from '../common/shellUtils'; 6 | import { ShellEnvsProvider } from '../startupProvider'; 7 | import { CMD_ENV_KEY } from './cmdConstants'; 8 | 9 | export class CmdEnvsProvider implements ShellEnvsProvider { 10 | readonly shellType: string = ShellConstants.CMD; 11 | updateEnvVariables(collection: EnvironmentVariableCollection, env: PythonEnvironment): void { 12 | try { 13 | const cmdActivation = getShellActivationCommand(this.shellType, env); 14 | if (cmdActivation) { 15 | const command = getShellCommandAsString(this.shellType, cmdActivation); 16 | const v = collection.get(CMD_ENV_KEY); 17 | if (v?.value === command) { 18 | return; 19 | } 20 | collection.replace(CMD_ENV_KEY, command); 21 | } else { 22 | collection.delete(CMD_ENV_KEY); 23 | } 24 | } catch (err) { 25 | traceError('Failed to update CMD environment variables', err); 26 | collection.delete(CMD_ENV_KEY); 27 | } 28 | } 29 | 30 | removeEnvVariables(envCollection: EnvironmentVariableCollection): void { 31 | envCollection.delete(CMD_ENV_KEY); 32 | } 33 | 34 | getEnvVariables(env?: PythonEnvironment): Map | undefined { 35 | if (!env) { 36 | return new Map([[CMD_ENV_KEY, undefined]]); 37 | } 38 | 39 | try { 40 | const cmdActivation = getShellActivationCommand(this.shellType, env); 41 | if (cmdActivation) { 42 | return new Map([[CMD_ENV_KEY, getShellCommandAsString(this.shellType, cmdActivation)]]); 43 | } 44 | return undefined; 45 | } catch (err) { 46 | traceError('Failed to get CMD environment variables', err); 47 | return undefined; 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/features/execution/runAsTask.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ShellExecution, 3 | Task, 4 | TaskExecution, 5 | TaskPanelKind, 6 | TaskRevealKind, 7 | TaskScope, 8 | Uri, 9 | WorkspaceFolder, 10 | } from 'vscode'; 11 | import { PythonEnvironment, PythonTaskExecutionOptions } from '../../api'; 12 | import { traceInfo, traceWarn } from '../../common/logging'; 13 | import { executeTask } from '../../common/tasks.apis'; 14 | import { getWorkspaceFolder } from '../../common/workspace.apis'; 15 | import { quoteStringIfNecessary } from './execUtils'; 16 | 17 | function getWorkspaceFolderOrDefault(uri?: Uri): WorkspaceFolder | TaskScope { 18 | const workspace = uri ? getWorkspaceFolder(uri) : undefined; 19 | return workspace ?? TaskScope.Global; 20 | } 21 | 22 | export async function runAsTask( 23 | environment: PythonEnvironment, 24 | options: PythonTaskExecutionOptions, 25 | extra?: { reveal?: TaskRevealKind }, 26 | ): Promise { 27 | const workspace: WorkspaceFolder | TaskScope = getWorkspaceFolderOrDefault(options.project?.uri); 28 | 29 | let executable = environment.execInfo?.activatedRun?.executable ?? environment.execInfo?.run.executable; 30 | if (!executable) { 31 | traceWarn('No Python executable found in environment; falling back to "python".'); 32 | executable = 'python'; 33 | } 34 | // Check and quote the executable path if necessary 35 | executable = quoteStringIfNecessary(executable); 36 | 37 | const args = environment.execInfo?.activatedRun?.args ?? environment.execInfo?.run.args ?? []; 38 | const allArgs = [...args, ...options.args]; 39 | traceInfo(`Running as task: ${executable} ${allArgs.join(' ')}`); 40 | 41 | const task = new Task( 42 | { type: 'python' }, 43 | workspace, 44 | options.name, 45 | 'Python', 46 | new ShellExecution(executable, allArgs, { cwd: options.cwd, env: options.env }), 47 | '$python', 48 | ); 49 | 50 | task.presentationOptions = { 51 | reveal: extra?.reveal ?? TaskRevealKind.Silent, 52 | echo: true, 53 | panel: TaskPanelKind.Shared, 54 | close: false, 55 | showReuseMessage: true, 56 | }; 57 | 58 | return executeTask(task); 59 | } 60 | -------------------------------------------------------------------------------- /src/features/terminal/shells/fish/fishEnvs.ts: -------------------------------------------------------------------------------- 1 | import { EnvironmentVariableCollection } from 'vscode'; 2 | import { PythonEnvironment } from '../../../../api'; 3 | import { traceError } from '../../../../common/logging'; 4 | import { ShellConstants } from '../../../common/shellConstants'; 5 | import { getShellActivationCommand, getShellCommandAsString } from '../common/shellUtils'; 6 | import { ShellEnvsProvider } from '../startupProvider'; 7 | import { FISH_ENV_KEY } from './fishConstants'; 8 | 9 | export class FishEnvsProvider implements ShellEnvsProvider { 10 | readonly shellType: string = ShellConstants.FISH; 11 | updateEnvVariables(collection: EnvironmentVariableCollection, env: PythonEnvironment): void { 12 | try { 13 | const fishActivation = getShellActivationCommand(this.shellType, env); 14 | if (fishActivation) { 15 | const command = getShellCommandAsString(this.shellType, fishActivation); 16 | const v = collection.get(FISH_ENV_KEY); 17 | if (v?.value === command) { 18 | return; 19 | } 20 | collection.replace(FISH_ENV_KEY, command); 21 | } else { 22 | collection.delete(FISH_ENV_KEY); 23 | } 24 | } catch (err) { 25 | traceError('Failed to update Fish environment variables', err); 26 | collection.delete(FISH_ENV_KEY); 27 | } 28 | } 29 | 30 | removeEnvVariables(envCollection: EnvironmentVariableCollection): void { 31 | envCollection.delete(FISH_ENV_KEY); 32 | } 33 | 34 | getEnvVariables(env?: PythonEnvironment): Map | undefined { 35 | if (!env) { 36 | return new Map([[FISH_ENV_KEY, undefined]]); 37 | } 38 | 39 | try { 40 | const fishActivation = getShellActivationCommand(this.shellType, env); 41 | if (fishActivation) { 42 | return new Map([[FISH_ENV_KEY, getShellCommandAsString(this.shellType, fishActivation)]]); 43 | } 44 | return undefined; 45 | } catch (err) { 46 | traceError('Failed to get Fish environment variables', err); 47 | return undefined; 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/features/terminal/shells/pwsh/pwshEnvs.ts: -------------------------------------------------------------------------------- 1 | import { EnvironmentVariableCollection } from 'vscode'; 2 | import { PythonEnvironment } from '../../../../api'; 3 | import { traceError } from '../../../../common/logging'; 4 | import { ShellConstants } from '../../../common/shellConstants'; 5 | import { getShellActivationCommand, getShellCommandAsString } from '../common/shellUtils'; 6 | import { ShellEnvsProvider } from '../startupProvider'; 7 | import { POWERSHELL_ENV_KEY } from './pwshConstants'; 8 | 9 | export class PowerShellEnvsProvider implements ShellEnvsProvider { 10 | public readonly shellType: string = ShellConstants.PWSH; 11 | 12 | updateEnvVariables(collection: EnvironmentVariableCollection, env: PythonEnvironment): void { 13 | try { 14 | const pwshActivation = getShellActivationCommand(this.shellType, env); 15 | if (pwshActivation) { 16 | const command = getShellCommandAsString(this.shellType, pwshActivation); 17 | const v = collection.get(POWERSHELL_ENV_KEY); 18 | if (v?.value === command) { 19 | return; 20 | } 21 | collection.replace(POWERSHELL_ENV_KEY, command); 22 | } else { 23 | collection.delete(POWERSHELL_ENV_KEY); 24 | } 25 | } catch (err) { 26 | traceError('Failed to update PowerShell environment variables', err); 27 | collection.delete(POWERSHELL_ENV_KEY); 28 | } 29 | } 30 | 31 | removeEnvVariables(envCollection: EnvironmentVariableCollection): void { 32 | envCollection.delete(POWERSHELL_ENV_KEY); 33 | } 34 | 35 | getEnvVariables(env?: PythonEnvironment): Map | undefined { 36 | if (!env) { 37 | return new Map([[POWERSHELL_ENV_KEY, undefined]]); 38 | } 39 | 40 | try { 41 | const pwshActivation = getShellActivationCommand(this.shellType, env); 42 | if (pwshActivation) { 43 | return new Map([[POWERSHELL_ENV_KEY, getShellCommandAsString(this.shellType, pwshActivation)]]); 44 | } 45 | return undefined; 46 | } catch (err) { 47 | traceError('Failed to get PowerShell environment variables', err); 48 | return undefined; 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/managers/builtin/cache.ts: -------------------------------------------------------------------------------- 1 | import { ENVS_EXTENSION_ID } from '../../common/constants'; 2 | import { getWorkspacePersistentState } from '../../common/persistentState'; 3 | 4 | export const SYSTEM_WORKSPACE_KEY = `${ENVS_EXTENSION_ID}:system:WORKSPACE_SELECTED`; 5 | export const SYSTEM_GLOBAL_KEY = `${ENVS_EXTENSION_ID}:system:GLOBAL_SELECTED`; 6 | 7 | export async function clearSystemEnvCache(): Promise { 8 | const keys = [SYSTEM_WORKSPACE_KEY, SYSTEM_GLOBAL_KEY]; 9 | const state = await getWorkspacePersistentState(); 10 | await state.clear(keys); 11 | } 12 | 13 | export async function getSystemEnvForWorkspace(fsPath: string): Promise { 14 | const state = await getWorkspacePersistentState(); 15 | const data: { [key: string]: string } | undefined = await state.get(SYSTEM_WORKSPACE_KEY); 16 | if (data) { 17 | try { 18 | return data[fsPath]; 19 | } catch { 20 | return undefined; 21 | } 22 | } 23 | return undefined; 24 | } 25 | 26 | export async function setSystemEnvForWorkspace(fsPath: string, envPath: string | undefined): Promise { 27 | const state = await getWorkspacePersistentState(); 28 | const data: { [key: string]: string } = (await state.get(SYSTEM_WORKSPACE_KEY)) ?? {}; 29 | if (envPath) { 30 | data[fsPath] = envPath; 31 | } else { 32 | delete data[fsPath]; 33 | } 34 | await state.set(SYSTEM_WORKSPACE_KEY, data); 35 | } 36 | 37 | export async function setSystemEnvForWorkspaces(fsPath: string[], envPath: string | undefined): Promise { 38 | const state = await getWorkspacePersistentState(); 39 | const data: { [key: string]: string } = (await state.get(SYSTEM_WORKSPACE_KEY)) ?? {}; 40 | fsPath.forEach((s) => { 41 | if (envPath) { 42 | data[s] = envPath; 43 | } else { 44 | delete data[s]; 45 | } 46 | }); 47 | await state.set(SYSTEM_WORKSPACE_KEY, data); 48 | } 49 | 50 | export async function getSystemEnvForGlobal(): Promise { 51 | const state = await getWorkspacePersistentState(); 52 | return await state.get(SYSTEM_GLOBAL_KEY); 53 | } 54 | 55 | export async function setSystemEnvForGlobal(envPath: string | undefined): Promise { 56 | const state = await getWorkspacePersistentState(); 57 | await state.set(SYSTEM_GLOBAL_KEY, envPath); 58 | } 59 | -------------------------------------------------------------------------------- /src/managers/builtin/main.ts: -------------------------------------------------------------------------------- 1 | import { Disposable, LogOutputChannel } from 'vscode'; 2 | import { PythonEnvironmentApi } from '../../api'; 3 | import { createSimpleDebounce } from '../../common/utils/debounce'; 4 | import { onDidEndTerminalShellExecution } from '../../common/window.apis'; 5 | import { createFileSystemWatcher, onDidDeleteFiles } from '../../common/workspace.apis'; 6 | import { getPythonApi } from '../../features/pythonApi'; 7 | import { NativePythonFinder } from '../common/nativePythonFinder'; 8 | import { PipPackageManager } from './pipManager'; 9 | import { isPipInstallCommand } from './pipUtils'; 10 | import { SysPythonManager } from './sysPythonManager'; 11 | import { VenvManager } from './venvManager'; 12 | 13 | export async function registerSystemPythonFeatures( 14 | nativeFinder: NativePythonFinder, 15 | disposables: Disposable[], 16 | log: LogOutputChannel, 17 | envManager: SysPythonManager, 18 | ): Promise { 19 | const api: PythonEnvironmentApi = await getPythonApi(); 20 | const venvManager = new VenvManager(nativeFinder, api, envManager, log); 21 | const pkgManager = new PipPackageManager(api, log, venvManager); 22 | 23 | disposables.push( 24 | api.registerPackageManager(pkgManager), 25 | api.registerEnvironmentManager(envManager), 26 | api.registerEnvironmentManager(venvManager), 27 | ); 28 | 29 | const venvDebouncedRefresh = createSimpleDebounce(500, () => { 30 | venvManager.watcherRefresh(); 31 | }); 32 | const watcher = createFileSystemWatcher('{**/activate}', false, true, false); 33 | disposables.push( 34 | watcher, 35 | watcher.onDidCreate(() => { 36 | venvDebouncedRefresh.trigger(); 37 | }), 38 | watcher.onDidDelete(() => { 39 | venvDebouncedRefresh.trigger(); 40 | }), 41 | onDidDeleteFiles(() => { 42 | venvDebouncedRefresh.trigger(); 43 | }), 44 | ); 45 | 46 | disposables.push( 47 | onDidEndTerminalShellExecution(async (e) => { 48 | const cwd = e.terminal.shellIntegration?.cwd; 49 | if (isPipInstallCommand(e.execution.commandLine.value) && cwd) { 50 | const env = await venvManager.get(cwd); 51 | if (env) { 52 | await pkgManager.refresh(env); 53 | } 54 | } 55 | }), 56 | ); 57 | } 58 | -------------------------------------------------------------------------------- /src/managers/poetry/main.ts: -------------------------------------------------------------------------------- 1 | import { Disposable, LogOutputChannel } from 'vscode'; 2 | import { PythonEnvironmentApi } from '../../api'; 3 | import { traceInfo } from '../../common/logging'; 4 | import { getPythonApi } from '../../features/pythonApi'; 5 | import { PythonProjectManager } from '../../internal.api'; 6 | import { NativePythonFinder } from '../common/nativePythonFinder'; 7 | import { notifyMissingManagerIfDefault } from '../common/utils'; 8 | import { PoetryManager } from './poetryManager'; 9 | import { PoetryPackageManager } from './poetryPackageManager'; 10 | import { getPoetry, getPoetryVersion } from './poetryUtils'; 11 | 12 | export async function registerPoetryFeatures( 13 | nativeFinder: NativePythonFinder, 14 | disposables: Disposable[], 15 | outputChannel: LogOutputChannel, 16 | projectManager: PythonProjectManager, 17 | ): Promise { 18 | const api: PythonEnvironmentApi = await getPythonApi(); 19 | 20 | try { 21 | const poetryPath = await getPoetry(nativeFinder); 22 | if (poetryPath) { 23 | traceInfo( 24 | 'The `shell` command is not available by default in Poetry versions 2.0.0 and above. Therefore all shell activation will be handled by calling `source `. If you face any problems with shell activation, please file an issue at https://github.com/microsoft/vscode-python-environments/issues to help us improve this implementation.', 25 | ); 26 | const version = await getPoetryVersion(poetryPath); 27 | traceInfo(`Poetry found at ${poetryPath}, version: ${version}`); 28 | const envManager = new PoetryManager(nativeFinder, api); 29 | const pkgManager = new PoetryPackageManager(api, outputChannel, envManager); 30 | 31 | disposables.push( 32 | envManager, 33 | pkgManager, 34 | api.registerEnvironmentManager(envManager), 35 | api.registerPackageManager(pkgManager), 36 | ); 37 | } else { 38 | traceInfo('Poetry not found, turning off poetry features.'); 39 | await notifyMissingManagerIfDefault('ms-python.python:poetry', projectManager, api); 40 | } 41 | } catch (ex) { 42 | traceInfo('Poetry not found, turning off poetry features.', ex); 43 | await notifyMissingManagerIfDefault('ms-python.python:poetry', projectManager, api); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/common/persistentState.ts: -------------------------------------------------------------------------------- 1 | import { ExtensionContext, Memento } from 'vscode'; 2 | import { traceError } from './logging'; 3 | import { createDeferred, Deferred } from './utils/deferred'; 4 | 5 | export interface PersistentState { 6 | get(key: string, defaultValue?: T): Promise; 7 | set(key: string, value: T): Promise; 8 | clear(keys?: string[]): Promise; 9 | } 10 | 11 | class PersistentStateImpl implements PersistentState { 12 | private clearing: Deferred; 13 | constructor(private readonly momento: Memento) { 14 | this.clearing = createDeferred(); 15 | this.clearing.resolve(); 16 | } 17 | async get(key: string, defaultValue?: T): Promise { 18 | await this.clearing.promise; 19 | if (defaultValue === undefined) { 20 | return this.momento.get(key); 21 | } 22 | return this.momento.get(key, defaultValue); 23 | } 24 | async set(key: string, value: T): Promise { 25 | await this.clearing.promise; 26 | await this.momento.update(key, value); 27 | 28 | const before = JSON.stringify(value); 29 | const after = JSON.stringify(await this.momento.get(key)); 30 | if (before !== after) { 31 | await this.momento.update(key, undefined); 32 | traceError('Error while updating state for key:', key); 33 | } 34 | } 35 | async clear(keys?: string[]): Promise { 36 | if (this.clearing.completed) { 37 | this.clearing = createDeferred(); 38 | const _keys = keys ?? this.momento.keys(); 39 | await Promise.all(_keys.map((key) => this.momento.update(key, undefined))); 40 | this.clearing.resolve(); 41 | } 42 | return this.clearing.promise; 43 | } 44 | } 45 | 46 | const _workspace = createDeferred(); 47 | const _global = createDeferred(); 48 | 49 | export function setPersistentState(context: ExtensionContext): void { 50 | _workspace.resolve(new PersistentStateImpl(context.workspaceState)); 51 | _global.resolve(new PersistentStateImpl(context.globalState)); 52 | } 53 | 54 | export function getWorkspacePersistentState(): Promise { 55 | return _workspace.promise; 56 | } 57 | 58 | export function getGlobalPersistentState(): Promise { 59 | return _global.promise; 60 | } 61 | 62 | export async function clearPersistentState(): Promise { 63 | const [workspace, global] = await Promise.all([_workspace.promise, _global.promise]); 64 | await Promise.all([workspace.clear(), global.clear()]); 65 | return undefined; 66 | } 67 | -------------------------------------------------------------------------------- /src/features/settings/settingCompletions.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CancellationToken, 3 | CompletionContext, 4 | CompletionItem, 5 | CompletionItemKind, 6 | CompletionItemProvider, 7 | Disposable, 8 | Position, 9 | Range, 10 | TextDocument, 11 | languages, 12 | } from 'vscode'; 13 | import { EnvironmentManagers } from '../../internal.api'; 14 | 15 | const ENV_PATTERN = /\s*\"python-envs\.defaultEnvManager\"\s*:\s*\"/gm; 16 | const PKG_PATTERN = /\s*\"python-envs\.defaultPackageManager\"\s*:\s*\"/gm; 17 | 18 | const ENV_PATTERN2 = /\s*\"envManager\"\s*:\s*\"/gm; 19 | const PKG_PATTERN2 = /\s*\"packageManager\"\s*:\s*\"/gm; 20 | 21 | function getRange(pos: Position, quoteIndex: number): { inserting: Range; replacing: Range } | undefined { 22 | if (quoteIndex === -1 || quoteIndex < pos.character) { 23 | return undefined; 24 | } 25 | return { 26 | inserting: new Range(pos.line, pos.character, pos.line, quoteIndex), 27 | replacing: new Range(pos.line, pos.character, pos.line, quoteIndex), 28 | }; 29 | } 30 | 31 | function getCompletionItem( 32 | label: string, 33 | insertText: string, 34 | doc?: string, 35 | range?: { inserting: Range; replacing: Range }, 36 | ): CompletionItem { 37 | const item = new CompletionItem(label); 38 | item.insertText = insertText; 39 | item.documentation = doc; 40 | item.kind = CompletionItemKind.Value; 41 | item.range = range; 42 | return item; 43 | } 44 | 45 | class ManagerSettingsProvider implements CompletionItemProvider { 46 | constructor(private readonly em: EnvironmentManagers) {} 47 | 48 | provideCompletionItems(doc: TextDocument, pos: Position, _token: CancellationToken, _context: CompletionContext) { 49 | const line = doc.lineAt(pos.line).text; 50 | const linePrefix = line.substring(0, pos.character); 51 | const range = getRange(pos, line.lastIndexOf('"')); 52 | 53 | let results: CompletionItem[] = []; 54 | if (ENV_PATTERN.test(linePrefix) || ENV_PATTERN2.test(linePrefix)) { 55 | results = this.em.managers.map((m) => getCompletionItem(m.id, m.id, m.description, range)); 56 | } else if (PKG_PATTERN.test(linePrefix) || PKG_PATTERN2.test(linePrefix)) { 57 | results = this.em.packageManagers.map((m) => getCompletionItem(m.id, m.id, m.description, range)); 58 | } 59 | return results; 60 | } 61 | } 62 | 63 | export function registerCompletionProvider(em: EnvironmentManagers): Disposable { 64 | return languages.registerCompletionItemProvider( 65 | { scheme: 'file', language: 'jsonc', pattern: '**/settings.json' }, 66 | new ManagerSettingsProvider(em), 67 | '"', 68 | ); 69 | } 70 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Python Environments Extension 2 | 3 | Thank you for your interest in contributing to the Python Environments extension! This guide will help you get started. 4 | 5 | ## Prerequisites 6 | 7 | - Node.js (LTS version recommended) 8 | - npm 9 | - VS Code Insiders (recommended for development) 10 | - Git 11 | - Python 12 | 13 | ## Getting Started 14 | 15 | 1. **Clone the repository** 16 | ```bash 17 | cd vscode-python-environments 18 | ``` 19 | 20 | 2. **Create a Python virtual environment** 21 | 22 | 3. **Install dependencies** 23 | ```bash 24 | npm install 25 | ``` 26 | 27 | 4. **Build and watch** 28 | ```bash 29 | npm run watch 30 | ``` 31 | 32 | 5. **Run tests** 33 | ```bash 34 | npm run unittest 35 | ``` 36 | 37 | ## Development Workflow 38 | 39 | ### Running the Extension 40 | 41 | 1. Open the project in VS Code 42 | 2. Press `F5` to launch the Extension Development Host 43 | 3. The extension will be loaded in the new VS Code window 44 | 45 | ### Making Changes 46 | 47 | - **Localization**: Use VS Code's `l10n` API for all user-facing messages 48 | - **Logging**: Use `traceLog` or `traceVerbose` instead of `console.log` 49 | - **Error Handling**: Track error state to avoid duplicate notifications 50 | - **Documentation**: Add clear docstrings to public functions 51 | 52 | ### Testing 53 | Run unit tests with the different configurations in the "Run and Debug" panel 54 | 55 | ## Contributor License Agreement (CLA) 56 | 57 | This project requires contributors to sign a Contributor License Agreement (CLA). When you submit a pull request, a CLA bot will automatically check if you need to provide a CLA and guide you through the process. You only need to do this once across all Microsoft repositories. 58 | 59 | ## Code of Conduct 60 | 61 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information, see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions. 62 | 63 | ## Questions or Issues? 64 | 65 | - **Questions**: Start a [discussion](https://github.com/microsoft/vscode-python/discussions/categories/q-a) 66 | - **Bugs**: File an [issue](https://github.com/microsoft/vscode-python-environments/issues) 67 | - **Feature Requests**: Start a [discussion](https://github.com/microsoft/vscode-python/discussions/categories/ideas) 68 | 69 | ## Additional Resources 70 | 71 | - [Development Process](https://github.com/Microsoft/vscode-python/blob/main/CONTRIBUTING.md#development-process) 72 | - [API Documentation](./src/api.ts) 73 | - [Project Documentation](./docs/projects-api-reference.md) 74 | 75 | Thank you for contributing! 🎉 76 | -------------------------------------------------------------------------------- /src/features/terminal/runInTerminal.ts: -------------------------------------------------------------------------------- 1 | import { Terminal, TerminalShellExecution } from 'vscode'; 2 | import { PythonEnvironment, PythonTerminalExecutionOptions } from '../../api'; 3 | import { createDeferred } from '../../common/utils/deferred'; 4 | import { onDidEndTerminalShellExecution } from '../../common/window.apis'; 5 | import { ShellConstants } from '../common/shellConstants'; 6 | import { identifyTerminalShell } from '../common/shellDetector'; 7 | import { quoteArgs } from '../execution/execUtils'; 8 | import { normalizeShellPath } from './shells/common/shellUtils'; 9 | 10 | export async function runInTerminal( 11 | environment: PythonEnvironment, 12 | terminal: Terminal, 13 | options: PythonTerminalExecutionOptions, 14 | ): Promise { 15 | if (options.show) { 16 | terminal.show(); 17 | } 18 | 19 | let executable = environment.execInfo?.activatedRun?.executable ?? environment.execInfo?.run.executable ?? 'python'; 20 | const args = environment.execInfo?.activatedRun?.args ?? environment.execInfo?.run.args ?? []; 21 | const allArgs = [...args, ...(options.args ?? [])]; 22 | const shellType = identifyTerminalShell(terminal); 23 | 24 | // Normalize executable path for Git Bash on Windows 25 | if (shellType === ShellConstants.GITBASH) { 26 | executable = normalizeShellPath(executable, shellType); 27 | } 28 | if (terminal.shellIntegration) { 29 | let execution: TerminalShellExecution | undefined; 30 | const deferred = createDeferred(); 31 | const disposable = onDidEndTerminalShellExecution((e) => { 32 | if (e.execution === execution) { 33 | disposable.dispose(); 34 | deferred.resolve(); 35 | } 36 | }); 37 | 38 | const shouldSurroundWithQuotes = 39 | executable.includes(' ') && !executable.startsWith('"') && !executable.endsWith('"'); 40 | // Handle case where executable contains white-spaces. 41 | if (shouldSurroundWithQuotes) { 42 | executable = `"${executable}"`; 43 | } 44 | 45 | if (shellType === ShellConstants.PWSH && !executable.startsWith('&')) { 46 | // PowerShell requires commands to be prefixed with '&' to run them. 47 | executable = `& ${executable}`; 48 | } 49 | execution = terminal.shellIntegration.executeCommand(executable, allArgs); 50 | await deferred.promise; 51 | } else { 52 | let text = quoteArgs([executable, ...allArgs]).join(' '); 53 | if (shellType === ShellConstants.PWSH && !text.startsWith('&')) { 54 | // PowerShell requires commands to be prefixed with '&' to run them. 55 | text = `& ${text}`; 56 | } 57 | terminal.sendText(`${text}\n`); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/test/mocks/helper.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | // Copyright (c) Microsoft Corporation. All rights reserved. 3 | // Licensed under the MIT License. 4 | import * as sinon from 'sinon'; 5 | import { Readable } from 'stream'; 6 | import * as TypeMoq from 'typemoq'; 7 | import * as common from 'typemoq/Common/_all'; 8 | import { LogOutputChannel } from 'vscode'; 9 | 10 | export class FakeReadableStream extends Readable { 11 | _read(_size: unknown): void | null { 12 | // custom reading logic here 13 | this.push(null); // end the stream 14 | } 15 | } 16 | 17 | /** 18 | * Creates a mock LogOutputChannel for testing. 19 | * @returns A mock LogOutputChannel with stubbed methods 20 | */ 21 | export function createMockLogOutputChannel(): LogOutputChannel { 22 | return { 23 | info: sinon.stub(), 24 | error: sinon.stub(), 25 | warn: sinon.stub(), 26 | append: sinon.stub(), 27 | debug: sinon.stub(), 28 | trace: sinon.stub(), 29 | show: sinon.stub(), 30 | hide: sinon.stub(), 31 | dispose: sinon.stub(), 32 | clear: sinon.stub(), 33 | replace: sinon.stub(), 34 | appendLine: sinon.stub(), 35 | name: 'test-log', 36 | logLevel: 1, 37 | onDidChangeLogLevel: sinon.stub() as LogOutputChannel['onDidChangeLogLevel'], 38 | } as unknown as LogOutputChannel; 39 | } 40 | 41 | /** 42 | * Type helper for accessing the `.then` property on mocks. 43 | * Used to prevent TypeMoq from treating mocks as thenables (Promise-like objects). 44 | * See: https://github.com/florinn/typemoq/issues/67 45 | */ 46 | export type Thenable = { then?: unknown }; 47 | 48 | /** 49 | * Sets up a mock to not be treated as a thenable (Promise-like object). 50 | * This is necessary due to a TypeMoq limitation where mocks can be confused with Promises. 51 | * 52 | * @param mock - The TypeMoq mock to configure 53 | * @example 54 | * const mock = TypeMoq.Mock.ofType(); 55 | * setupNonThenable(mock); 56 | */ 57 | export function setupNonThenable(mock: TypeMoq.IMock): void { 58 | mock.setup((x) => (x as unknown as Thenable).then).returns(() => undefined); 59 | } 60 | 61 | export function createTypeMoq( 62 | targetCtor?: common.CtorWithArgs, 63 | behavior?: TypeMoq.MockBehavior, 64 | shouldOverrideTarget?: boolean, 65 | ...targetCtorArgs: any[] 66 | ): TypeMoq.IMock { 67 | // Use typemoqs for those things that are resolved as promises. mockito doesn't allow nesting of mocks. ES6 Proxy class 68 | // is the problem. We still need to make it thenable though. See this issue: https://github.com/florinn/typemoq/issues/67 69 | const result = TypeMoq.Mock.ofType(targetCtor, behavior, shouldOverrideTarget, ...targetCtorArgs); 70 | setupNonThenable(result); 71 | return result; 72 | } 73 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets Microsoft's [Microsoft's definition of a security vulnerability]() of a security vulnerability, please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | - Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | - Full paths of source file(s) related to the manifestation of the issue 23 | - The location of the affected source code (tag/branch/commit or direct URL) 24 | - Any special configuration required to reproduce the issue 25 | - Step-by-step instructions to reproduce the issue 26 | - Proof-of-concept or exploit code (if possible) 27 | - Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/features/execution/runInBackground.ts: -------------------------------------------------------------------------------- 1 | import { PythonBackgroundRunOptions, PythonEnvironment, PythonProcess } from '../../api'; 2 | import { spawnProcess } from '../../common/childProcess.apis'; 3 | import { traceError, traceInfo, traceWarn } from '../../common/logging'; 4 | import { quoteStringIfNecessary } from './execUtils'; 5 | 6 | export async function runInBackground( 7 | environment: PythonEnvironment, 8 | options: PythonBackgroundRunOptions, 9 | ): Promise { 10 | let executable = environment.execInfo?.activatedRun?.executable ?? environment.execInfo?.run.executable; 11 | if (!executable) { 12 | traceWarn('No Python executable found in environment; falling back to "python".'); 13 | executable = 'python'; 14 | } 15 | 16 | // Don't quote the executable path for spawn - it handles spaces correctly on its own 17 | // Remove any existing quotes that might cause issues 18 | // see https://github.com/nodejs/node/issues/7367 for more details on cp.spawn and quoting 19 | if (executable.startsWith('"') && executable.endsWith('"')) { 20 | executable = executable.substring(1, executable.length - 1); 21 | } 22 | 23 | const args = environment.execInfo?.activatedRun?.args ?? environment.execInfo?.run.args ?? []; 24 | const allArgs = [...args, ...options.args]; 25 | 26 | // Log the command for debugging 27 | traceInfo(`Running in background: "${executable}" ${allArgs.join(' ')}`); 28 | 29 | // Check if the file exists before trying to spawn it 30 | try { 31 | const fs = require('fs'); 32 | if (!fs.existsSync(executable)) { 33 | traceError( 34 | `Python executable does not exist: ${executable}. Attempting to quote the path as a workaround...`, 35 | ); 36 | executable = quoteStringIfNecessary(executable); 37 | } 38 | } catch (err) { 39 | traceWarn(`Error checking if executable exists: ${err instanceof Error ? err.message : String(err)}`); 40 | } 41 | 42 | const proc = spawnProcess(executable, allArgs, { 43 | stdio: 'pipe', 44 | cwd: options.cwd, 45 | env: options.env, 46 | }); 47 | 48 | return { 49 | pid: proc.pid, 50 | stdin: proc.stdin, 51 | stdout: proc.stdout, 52 | stderr: proc.stderr, 53 | kill: () => { 54 | if (!proc.killed) { 55 | proc.kill(); 56 | } 57 | }, 58 | onExit: (listener: (code: number | null, signal: NodeJS.Signals | null, error?: Error | null) => void) => { 59 | proc.on('exit', (code, signal) => { 60 | if (code && code !== 0) { 61 | traceError(`Process exited with error code: ${code}, signal: ${signal}`); 62 | } 63 | listener(code, signal, null); 64 | }); 65 | proc.on('error', (error) => { 66 | traceError(`Process error: ${error?.message || error}${error?.stack ? '\n' + error.stack : ''}`); 67 | listener(null, null, error); 68 | }); 69 | }, 70 | }; 71 | } 72 | -------------------------------------------------------------------------------- /src/common/utils/frameUtils.ts: -------------------------------------------------------------------------------- 1 | import { Uri } from 'vscode'; 2 | import { ENVS_EXTENSION_ID, PYTHON_EXTENSION_ID } from '../constants'; 3 | import { parseStack } from '../errors/utils'; 4 | import { allExtensions, getExtension } from '../extension.apis'; 5 | import { normalizePath } from './pathUtils'; 6 | interface FrameData { 7 | filePath: string; 8 | functionName: string; 9 | } 10 | 11 | function getFrameData(): FrameData[] { 12 | const frames = parseStack(new Error()); 13 | return frames.map((frame) => ({ 14 | filePath: frame.getFileName(), 15 | functionName: frame.getFunctionName(), 16 | })); 17 | } 18 | 19 | function getPathFromFrame(frame: FrameData): string { 20 | if (frame.filePath && frame.filePath.startsWith('file://')) { 21 | return Uri.parse(frame.filePath).fsPath; 22 | } 23 | return frame.filePath; 24 | } 25 | 26 | export function getCallingExtension(): string { 27 | const pythonExts = [ENVS_EXTENSION_ID, PYTHON_EXTENSION_ID]; 28 | const extensions = allExtensions(); 29 | const otherExts = extensions.filter((ext) => !pythonExts.includes(ext.id)); 30 | const frames = getFrameData(); 31 | 32 | const registerEnvManagerFrameIndex = frames.findIndex( 33 | (frame) => 34 | frame.functionName && 35 | (frame.functionName.includes('registerEnvironmentManager') || 36 | frame.functionName.includes('registerPackageManager')), 37 | ); 38 | 39 | const relevantFrames = 40 | registerEnvManagerFrameIndex !== -1 ? frames.slice(registerEnvManagerFrameIndex + 1) : frames; 41 | 42 | const filePaths: string[] = []; 43 | for (const frame of relevantFrames) { 44 | if (!frame || !frame.filePath) { 45 | continue; 46 | } 47 | const filePath = normalizePath(getPathFromFrame(frame)); 48 | if (!filePath) { 49 | continue; 50 | } 51 | 52 | if (filePath.toLowerCase().endsWith('extensionhostprocess.js')) { 53 | continue; 54 | } 55 | 56 | if (filePath.startsWith('node:')) { 57 | continue; 58 | } 59 | 60 | filePaths.push(filePath); 61 | 62 | const ext = otherExts.find((ext) => filePath.includes(ext.id)); 63 | if (ext) { 64 | return ext.id; 65 | } 66 | } 67 | 68 | const envExt = getExtension(ENVS_EXTENSION_ID); 69 | const pythonExt = getExtension(PYTHON_EXTENSION_ID); 70 | if (!envExt || !pythonExt) { 71 | throw new Error('Something went wrong with feature registration'); 72 | } 73 | const envsExtPath = normalizePath(envExt.extensionPath); 74 | 75 | if (filePaths.every((filePath) => filePath.startsWith(envsExtPath))) { 76 | return PYTHON_EXTENSION_ID; 77 | } 78 | 79 | for (const ext of otherExts) { 80 | const extPath = normalizePath(ext.extensionPath); 81 | if (filePaths.some((filePath) => filePath.startsWith(extPath))) { 82 | return ext.id; 83 | } 84 | } 85 | 86 | // Fallback - we're likely being called from Python extension in conda registration 87 | return PYTHON_EXTENSION_ID; 88 | } 89 | -------------------------------------------------------------------------------- /src/test/mocks/vsc/htmlContent.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import * as vscMockArrays from './arrays'; 5 | 6 | export interface IMarkdownString { 7 | value: string; 8 | isTrusted?: boolean; 9 | } 10 | 11 | export class MarkdownString implements IMarkdownString { 12 | value: string; 13 | 14 | isTrusted?: boolean; 15 | 16 | constructor(value = '') { 17 | this.value = value; 18 | } 19 | 20 | appendText(value: string): MarkdownString { 21 | // escape markdown syntax tokens: http://daringfireball.net/projects/markdown/syntax#backslash 22 | this.value += value.replace(/[\\`*_{}[\]()#+\-.!]/g, '\\$&'); 23 | return this; 24 | } 25 | 26 | appendMarkdown(value: string): MarkdownString { 27 | this.value += value; 28 | return this; 29 | } 30 | 31 | appendCodeblock(langId: string, code: string): MarkdownString { 32 | this.value += '\n```'; 33 | this.value += langId; 34 | this.value += '\n'; 35 | this.value += code; 36 | this.value += '\n```\n'; 37 | return this; 38 | } 39 | } 40 | 41 | export function isEmptyMarkdownString(oneOrMany: IMarkdownString | IMarkdownString[]): boolean { 42 | if (isMarkdownString(oneOrMany)) { 43 | return !oneOrMany.value; 44 | } 45 | if (Array.isArray(oneOrMany)) { 46 | return oneOrMany.every(isEmptyMarkdownString); 47 | } 48 | return true; 49 | } 50 | 51 | export function isMarkdownString(thing: unknown): thing is IMarkdownString { 52 | if (thing instanceof MarkdownString) { 53 | return true; 54 | } 55 | if (thing && typeof thing === 'object') { 56 | return ( 57 | typeof (thing).value === 'string' && 58 | (typeof (thing).isTrusted === 'boolean' || 59 | (thing).isTrusted === undefined) 60 | ); 61 | } 62 | return false; 63 | } 64 | 65 | export function markedStringsEquals( 66 | a: IMarkdownString | IMarkdownString[], 67 | b: IMarkdownString | IMarkdownString[], 68 | ): boolean { 69 | if (!a && !b) { 70 | return true; 71 | } 72 | if (!a || !b) { 73 | return false; 74 | } 75 | if (Array.isArray(a) && Array.isArray(b)) { 76 | return vscMockArrays.equals(a, b, markdownStringEqual); 77 | } 78 | if (isMarkdownString(a) && isMarkdownString(b)) { 79 | return markdownStringEqual(a, b); 80 | } 81 | return false; 82 | } 83 | 84 | function markdownStringEqual(a: IMarkdownString, b: IMarkdownString): boolean { 85 | if (a === b) { 86 | return true; 87 | } 88 | if (!a || !b) { 89 | return false; 90 | } 91 | return a.value === b.value && a.isTrusted === b.isTrusted; 92 | } 93 | 94 | export function removeMarkdownEscapes(text: string): string { 95 | if (!text) { 96 | return text; 97 | } 98 | return text.replace(/\\([\\`*_{}[\]()#+\-.!])/g, '$1'); 99 | } 100 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | ## Requirements 2 | 3 | 1. `node` >= 22.21.1 4 | 2. `npm` >= 10.9.0 5 | 3. `yo` >= 5.0.0 (installed via `npm install -g yo`) 6 | 4. `generator-code` >= 1.11.4 (installed via `npm install -g generator-code`) 7 | 8 | ## Create your extension 9 | 10 | ### Scaffolding 11 | 12 | Run `yo code` in your terminal and follow the instructions to create a new extension. The following were the choices made for this example: 13 | 14 | ``` 15 | > yo code 16 | ? What type of extension do you want to create? New Extension (TypeScript) 17 | ? What's the name of your extension? Sample1 18 | ? What's the identifier of your extension? sample1 19 | ? What's the description of your extension? A sample environment manager 20 | ? Initialize a git repository? Yes 21 | ? Which bundler to use? webpack 22 | ? Which package manager to use? npm 23 | ``` 24 | 25 | Follow the generator's additional instructions to install the required dependencies and build your extension. 26 | 27 | ### Update extension dependency 28 | 29 | Add the following dependency to your extension `package.json` file: 30 | 31 | ```json 32 | "extensionDependencies": [ 33 | "ms-python.vscode-python-envs" 34 | ], 35 | ``` 36 | 37 | ### Set up the Python Envs API 38 | 39 | The Python environments API is available via the extension export. First, add the following file to your extension [api.ts](https://github.com/microsoft/vscode-python-environments/blob/main/src/api.ts). You can rename the file as you see fit for your extension. 40 | 41 | Add a `pythonEnvsApi.ts` file to get the API and insert the following code: 42 | 43 | ```typescript 44 | import * as vscode from 'vscode'; 45 | import { PythonEnvironmentApi } from './api'; 46 | 47 | let _extApi: PythonEnvironmentApi | undefined; 48 | export async function getEnvExtApi(): Promise { 49 | if (_extApi) { 50 | return _extApi; 51 | } 52 | const extension = vscode.extensions.getExtension('ms-python.vscode-python-envs'); 53 | if (!extension) { 54 | throw new Error('Python Environments extension not found.'); 55 | } 56 | if (extension?.isActive) { 57 | _extApi = extension.exports as PythonEnvironmentApi; 58 | return _extApi; 59 | } 60 | 61 | await extension.activate(); 62 | 63 | _extApi = extension.exports as PythonEnvironmentApi; 64 | return _extApi; 65 | } 66 | ``` 67 | 68 | Now you are ready to use it to register your environment manager. 69 | 70 | ### Registering the environment manager 71 | 72 | Add the following code to your extension's `extension.ts` file: 73 | 74 | ```typescript 75 | import { ExtensionContext } from 'vscode'; 76 | import { getEnvExtApi } from './pythonEnvsApi'; 77 | import { SampleEnvManager } from './sampleEnvManager'; 78 | 79 | export async function activate(context: ExtensionContext): Promise { 80 | const api = await getEnvExtApi(); 81 | 82 | const envManager = new SampleEnvManager(api); 83 | context.subscriptions.push(api.registerEnvironmentManager(envManager)); 84 | } 85 | ``` 86 | 87 | See full implementations for built-in support here: https://github.com/microsoft/vscode-python-environments/blob/main/src/managers 88 | -------------------------------------------------------------------------------- /src/features/terminal/shells/common/editUtils.ts: -------------------------------------------------------------------------------- 1 | import { isWindows } from '../../../../common/utils/platformUtils'; 2 | 3 | export function hasStartupCode(content: string, start: string, end: string, keys: string[]): boolean { 4 | const normalizedContent = content.replace(/\r\n/g, '\n'); 5 | const startIndex = normalizedContent.indexOf(start); 6 | const endIndex = normalizedContent.indexOf(end); 7 | if (startIndex === -1 || endIndex === -1 || startIndex >= endIndex) { 8 | return false; 9 | } 10 | const contentBetween = normalizedContent.substring(startIndex + start.length, endIndex).trim(); 11 | return contentBetween.length > 0 && keys.every((key) => contentBetween.includes(key)); 12 | } 13 | 14 | function getLineEndings(content: string): string { 15 | if (content.includes('\r\n')) { 16 | return '\r\n'; 17 | } else if (content.includes('\n')) { 18 | return '\n'; 19 | } 20 | return isWindows() ? '\r\n' : '\n'; 21 | } 22 | 23 | export function insertStartupCode(content: string, start: string, end: string, code: string): string { 24 | let lineEnding = getLineEndings(content); 25 | const normalizedContent = content.replace(/\r\n/g, '\n'); 26 | 27 | const startIndex = normalizedContent.indexOf(start); 28 | const endIndex = normalizedContent.indexOf(end); 29 | 30 | let result: string; 31 | if (startIndex !== -1 && endIndex !== -1 && startIndex < endIndex) { 32 | result = 33 | normalizedContent.substring(0, startIndex + start.length) + 34 | '\n' + 35 | code + 36 | '\n' + 37 | normalizedContent.substring(endIndex); 38 | } else if (startIndex !== -1) { 39 | result = normalizedContent.substring(0, startIndex + start.length) + '\n' + code + '\n' + end + '\n'; 40 | } else { 41 | result = normalizedContent + '\n' + start + '\n' + code + '\n' + end + '\n'; 42 | } 43 | 44 | if (lineEnding === '\r\n') { 45 | result = result.replace(/\n/g, '\r\n'); 46 | } 47 | return result; 48 | } 49 | 50 | export function removeStartupCode(content: string, start: string, end: string): string { 51 | let lineEnding = getLineEndings(content); 52 | const normalizedContent = content.replace(/\r\n/g, '\n'); 53 | 54 | const startIndex = normalizedContent.indexOf(start); 55 | const endIndex = normalizedContent.indexOf(end); 56 | 57 | if (startIndex !== -1 && endIndex !== -1 && startIndex < endIndex) { 58 | const before = normalizedContent.substring(0, startIndex); 59 | const after = normalizedContent.substring(endIndex + end.length); 60 | 61 | let result: string; 62 | if (before === '') { 63 | result = after.startsWith('\n') ? after.substring(1) : after; 64 | } else if (after === '' || after === '\n') { 65 | result = before.endsWith('\n') ? before.substring(0, before.length - 1) : before; 66 | } else if (after.startsWith('\n') && before.endsWith('\n')) { 67 | result = before + after.substring(1); 68 | } else { 69 | result = before + after; 70 | } 71 | 72 | if (lineEnding === '\r\n') { 73 | result = result.replace(/\n/g, '\r\n'); 74 | } 75 | return result; 76 | } 77 | return content; 78 | } 79 | -------------------------------------------------------------------------------- /.github/instructions/issue-format.instructions.md: -------------------------------------------------------------------------------- 1 | --- 2 | applyTo: '**/github-issues/**' 3 | --- 4 | 5 | # Guidelines for Creating Effective GitHub Issues 6 | 7 | ## Issue Format 8 | 9 | When creating GitHub issues, use the following structure to ensure clarity and ease of verification: 10 | 11 | ### For Bug Reports 12 | 13 | 1. **Title**: Concise description of the issue (5-10 words) 14 | 15 | 2. **Problem Statement**: 16 | 17 | - 1-2 sentences describing the issue 18 | - Focus on user impact 19 | - Use clear, non-technical language when possible 20 | 21 | 3. **Steps to Verify Fix**: 22 | - Numbered list (5-7 steps maximum) 23 | - Start each step with an action verb 24 | - Include expected observations 25 | - Cover both success paths and cancellation/back button scenarios 26 | 27 | ### For Feature Requests 28 | 29 | 1. **Title**: Clear description of the requested feature 30 | 31 | 2. **Need Statement**: 32 | 33 | - 1-2 sentences describing the user need 34 | - Explain why this feature would be valuable 35 | 36 | 3. **Acceptance Criteria**: 37 | - Bulleted list of verifiable behaviors 38 | - Include how a user would confirm the feature works as expected 39 | 40 | ## Examples 41 | 42 | ### Bug Report Example 43 | 44 | ``` 45 | # Terminal opens prematurely with PET Resolve Environment command 46 | 47 | **Problem:** When using "Resolve Environment..." from the Python Environment Tool menu, 48 | the terminal opens before entering a path, creating a confusing workflow. 49 | 50 | **Steps to verify fix:** 51 | 1. Run "Python Environments: Run Python Environment Tool in Terminal" from Command Palette 52 | 2. Select "Resolve Environment..." 53 | 3. Verify no terminal opens yet 54 | 4. Enter a Python path 55 | 5. Verify terminal only appears after path entry 56 | 6. Try canceling at the input step - confirm no terminal appears 57 | ``` 58 | 59 | ### Feature Request Example 60 | 61 | ``` 62 | # Add back button support to multi-step UI flows 63 | 64 | **Problem:** The UI flows for environment creation and Python project setup lack back button 65 | functionality, forcing users to cancel and restart when they need to change a previous selection. 66 | 67 | **Steps to verify implementation:** 68 | 1. Test back button in PET workflow: Run "Python Environments: Run Python Environment Tool in Terminal", 69 | select "Resolve Environment...", press back button, confirm it returns to menu 70 | 2. Test back button in VENV creation: Run "Create environment", select VENV, press back button at various steps 71 | 3. Test back button in CONDA creation: Create CONDA environment, use back buttons to navigate between steps 72 | 4. Test back button in Python project flow: Add Python project, verify back functionality in project type selection 73 | ``` 74 | 75 | ## Best Practices 76 | 77 | 1. **Be concise**: Keep descriptions short but informative 78 | 2. **Use active voice**: "Terminal opens prematurely" rather than "The terminal is opened prematurely" 79 | 3. **Include context**: Mention relevant commands, UI elements, and workflows 80 | 4. **Focus on verification**: Make steps actionable and observable 81 | 5. **Cover edge cases**: Include cancellation paths and error scenarios 82 | 6. **Use formatting**: Bold headings and numbered lists improve readability 83 | 84 | Remember that good issues help both developers fixing problems and testers verifying solutions. 85 | -------------------------------------------------------------------------------- /src/test/mocks/vsc/uuid.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | /** 5 | * Represents a UUID as defined by rfc4122. 6 | */ 7 | 8 | export interface UUID { 9 | /** 10 | * @returns the canonical representation in sets of hexadecimal numbers separated by dashes. 11 | */ 12 | asHex(): string; 13 | } 14 | 15 | class ValueUUID implements UUID { 16 | constructor(public _value: string) { 17 | // empty 18 | } 19 | 20 | public asHex(): string { 21 | return this._value; 22 | } 23 | } 24 | 25 | class V4UUID extends ValueUUID { 26 | private static readonly _chars = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f']; 27 | 28 | private static readonly _timeHighBits = ['8', '9', 'a', 'b']; 29 | 30 | private static _oneOf(array: string[]): string { 31 | return array[Math.floor(array.length * Math.random())]; 32 | } 33 | 34 | private static _randomHex(): string { 35 | return V4UUID._oneOf(V4UUID._chars); 36 | } 37 | 38 | constructor() { 39 | super( 40 | [ 41 | V4UUID._randomHex(), 42 | V4UUID._randomHex(), 43 | V4UUID._randomHex(), 44 | V4UUID._randomHex(), 45 | V4UUID._randomHex(), 46 | V4UUID._randomHex(), 47 | V4UUID._randomHex(), 48 | V4UUID._randomHex(), 49 | '-', 50 | V4UUID._randomHex(), 51 | V4UUID._randomHex(), 52 | V4UUID._randomHex(), 53 | V4UUID._randomHex(), 54 | '-', 55 | '4', 56 | V4UUID._randomHex(), 57 | V4UUID._randomHex(), 58 | V4UUID._randomHex(), 59 | '-', 60 | V4UUID._oneOf(V4UUID._timeHighBits), 61 | V4UUID._randomHex(), 62 | V4UUID._randomHex(), 63 | V4UUID._randomHex(), 64 | '-', 65 | V4UUID._randomHex(), 66 | V4UUID._randomHex(), 67 | V4UUID._randomHex(), 68 | V4UUID._randomHex(), 69 | V4UUID._randomHex(), 70 | V4UUID._randomHex(), 71 | V4UUID._randomHex(), 72 | V4UUID._randomHex(), 73 | V4UUID._randomHex(), 74 | V4UUID._randomHex(), 75 | V4UUID._randomHex(), 76 | V4UUID._randomHex(), 77 | ].join(''), 78 | ); 79 | } 80 | } 81 | 82 | export function v4(): UUID { 83 | return new V4UUID(); 84 | } 85 | 86 | const UUIDPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; 87 | 88 | export function isUUID(value: string): boolean { 89 | return UUIDPattern.test(value); 90 | } 91 | 92 | /** 93 | * Parses a UUID that is of the format xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx. 94 | * @param value A uuid string. 95 | */ 96 | export function parse(value: string): UUID { 97 | if (!isUUID(value)) { 98 | throw new Error('invalid uuid'); 99 | } 100 | 101 | return new ValueUUID(value); 102 | } 103 | 104 | export function generateUuid(): string { 105 | return v4().asHex(); 106 | } 107 | -------------------------------------------------------------------------------- /src/common/utils/pythonPath.ts: -------------------------------------------------------------------------------- 1 | import { Uri, Progress, CancellationToken } from 'vscode'; 2 | import { PythonEnvironment } from '../../api'; 3 | import { InternalEnvironmentManager } from '../../internal.api'; 4 | import { traceVerbose, traceError } from '../logging'; 5 | import { PYTHON_EXTENSION_ID } from '../constants'; 6 | import { showErrorMessage } from '../window.apis'; 7 | 8 | const priorityOrder = [ 9 | `${PYTHON_EXTENSION_ID}:pyenv`, 10 | `${PYTHON_EXTENSION_ID}:pixi`, 11 | `${PYTHON_EXTENSION_ID}:conda`, 12 | `${PYTHON_EXTENSION_ID}:pipenv`, 13 | `${PYTHON_EXTENSION_ID}:poetry`, 14 | `${PYTHON_EXTENSION_ID}:activestate`, 15 | `${PYTHON_EXTENSION_ID}:hatch`, 16 | `${PYTHON_EXTENSION_ID}:venv`, 17 | `${PYTHON_EXTENSION_ID}:system`, 18 | ]; 19 | function sortManagersByPriority(managers: InternalEnvironmentManager[]): InternalEnvironmentManager[] { 20 | return managers.sort((a, b) => { 21 | const aIndex = priorityOrder.indexOf(a.id); 22 | const bIndex = priorityOrder.indexOf(b.id); 23 | if (aIndex === -1 && bIndex === -1) { 24 | return 0; 25 | } 26 | if (aIndex === -1) { 27 | return 1; 28 | } 29 | if (bIndex === -1) { 30 | return -1; 31 | } 32 | return aIndex - bIndex; 33 | }); 34 | } 35 | 36 | export async function handlePythonPath( 37 | interpreterUri: Uri, 38 | managers: InternalEnvironmentManager[], 39 | projectEnvManagers: InternalEnvironmentManager[], 40 | reporter?: Progress<{ message?: string; increment?: number }>, 41 | token?: CancellationToken, 42 | ): Promise { 43 | // Use the managers user has set for the project first. Likely, these 44 | // managers are the ones that should be used. 45 | for (const manager of sortManagersByPriority(projectEnvManagers)) { 46 | if (token?.isCancellationRequested) { 47 | return; 48 | } 49 | reporter?.report({ message: `Checking ${manager.displayName}` }); 50 | traceVerbose(`Checking ${manager.displayName} (${manager.id}) for ${interpreterUri.fsPath}`); 51 | const env = await manager.resolve(interpreterUri); 52 | if (env) { 53 | traceVerbose(`Using ${manager.displayName} (${manager.id}) to handle ${interpreterUri.fsPath}`); 54 | return env; 55 | } 56 | traceVerbose(`Manager ${manager.displayName} (${manager.id}) cannot handle ${interpreterUri.fsPath}`); 57 | } 58 | 59 | // If the project managers cannot handle the interpreter, then try all the managers 60 | // that user has installed. Excluding anything that is already checked. 61 | const checkedIds = projectEnvManagers.map((m) => m.id); 62 | const filtered = managers.filter((m) => !checkedIds.includes(m.id)); 63 | 64 | for (const manager of sortManagersByPriority(filtered)) { 65 | if (token?.isCancellationRequested) { 66 | return; 67 | } 68 | reporter?.report({ message: `Checking ${manager.displayName}` }); 69 | traceVerbose(`Checking ${manager.displayName} (${manager.id}) for ${interpreterUri.fsPath}`); 70 | const env = await manager.resolve(interpreterUri); 71 | if (env) { 72 | traceVerbose(`Using ${manager.displayName} (${manager.id}) to handle ${interpreterUri.fsPath}`); 73 | return env; 74 | } 75 | } 76 | 77 | traceError(`Unable to handle ${interpreterUri.fsPath}`); 78 | showErrorMessage(`Unable to handle ${interpreterUri.fsPath}`); 79 | return undefined; 80 | } 81 | -------------------------------------------------------------------------------- /src/common/utils/pathUtils.ts: -------------------------------------------------------------------------------- 1 | import * as os from 'os'; 2 | import * as path from 'path'; 3 | import { NotebookCell, NotebookDocument, Uri, workspace } from 'vscode'; 4 | import { isWindows } from './platformUtils'; 5 | 6 | export function checkUri(scope?: Uri | Uri[] | string): Uri | Uri[] | string | undefined { 7 | if (!scope) { 8 | return undefined; 9 | } 10 | 11 | if (Array.isArray(scope)) { 12 | // if the scope is an array, all items must be Uri, check each item 13 | return scope.map((item) => { 14 | const s = checkUri(item); 15 | if (s instanceof Uri) { 16 | return s; 17 | } 18 | throw new Error('Invalid entry, expected Uri.'); 19 | }); 20 | } 21 | 22 | if (scope instanceof Uri) { 23 | if (scope.scheme === 'vscode-notebook-cell') { 24 | const matchingDoc = workspace.notebookDocuments.find((doc) => findCell(scope, doc)); 25 | // If we find a matching notebook document, return the Uri of the cell. 26 | return matchingDoc ? matchingDoc.uri : scope; 27 | } 28 | } 29 | return scope; 30 | } 31 | 32 | /** 33 | * Find a notebook document by cell Uri. 34 | */ 35 | export function findCell(cellUri: Uri, notebook: NotebookDocument): NotebookCell | undefined { 36 | // Fragment is not unique to a notebook, hence ensure we compare the path as well. 37 | return notebook.getCells().find((cell) => { 38 | return isEqual(cell.document.uri, cellUri); 39 | }); 40 | } 41 | function isEqual(uri1: Uri | undefined, uri2: Uri | undefined): boolean { 42 | if (uri1 === uri2) { 43 | return true; 44 | } 45 | if (!uri1 || !uri2) { 46 | return false; 47 | } 48 | return getComparisonKey(uri1) === getComparisonKey(uri2); 49 | } 50 | 51 | function getComparisonKey(uri: Uri): string { 52 | return uri 53 | .with({ 54 | path: isWindows() ? uri.path.toLowerCase() : uri.path, 55 | fragment: undefined, 56 | }) 57 | .toString(); 58 | } 59 | 60 | export function normalizePath(fsPath: string): string { 61 | const path1 = fsPath.replace(/\\/g, '/'); 62 | if (isWindows()) { 63 | return path1.toLowerCase(); 64 | } 65 | return path1; 66 | } 67 | 68 | export function getResourceUri(resourcePath: string, root?: string): Uri | undefined { 69 | try { 70 | if (!resourcePath) { 71 | return undefined; 72 | } 73 | 74 | const normalizedPath = normalizePath(resourcePath); 75 | if (normalizedPath.includes('://')) { 76 | return Uri.parse(normalizedPath); 77 | } 78 | 79 | if (!path.isAbsolute(resourcePath) && root) { 80 | const absolutePath = path.resolve(root, resourcePath); 81 | return Uri.file(absolutePath); 82 | } 83 | return Uri.file(resourcePath); 84 | } catch (_err) { 85 | return undefined; 86 | } 87 | } 88 | 89 | export function untildify(path: string): string { 90 | return path.replace(/^~($|\/|\\)/, `${os.homedir()}$1`); 91 | } 92 | 93 | export function getUserHomeDir(): string { 94 | return os.homedir(); 95 | } 96 | 97 | /** 98 | * Applies untildify to an array of paths 99 | * @param paths Array of potentially tilde-containing paths 100 | * @returns Array of expanded paths 101 | */ 102 | export function untildifyArray(paths: string[]): string[] { 103 | return paths.map((p) => untildify(p)); 104 | } 105 | -------------------------------------------------------------------------------- /src/features/execution/envVariableManager.ts: -------------------------------------------------------------------------------- 1 | import * as fsapi from 'fs-extra'; 2 | import * as path from 'path'; 3 | import { Event, EventEmitter, FileChangeType, Uri } from 'vscode'; 4 | import { Disposable } from 'vscode-jsonrpc'; 5 | import { DidChangeEnvironmentVariablesEventArgs, PythonEnvironmentVariablesApi } from '../../api'; 6 | import { resolveVariables } from '../../common/utils/internalVariables'; 7 | import { createFileSystemWatcher, getConfiguration } from '../../common/workspace.apis'; 8 | import { PythonProjectManager } from '../../internal.api'; 9 | import { mergeEnvVariables, parseEnvFile } from './envVarUtils'; 10 | 11 | export interface EnvVarManager extends PythonEnvironmentVariablesApi, Disposable {} 12 | 13 | export class PythonEnvVariableManager implements EnvVarManager { 14 | private disposables: Disposable[] = []; 15 | 16 | private _onDidChangeEnvironmentVariables; 17 | private watcher; 18 | 19 | constructor(private pm: PythonProjectManager) { 20 | this._onDidChangeEnvironmentVariables = new EventEmitter(); 21 | this.onDidChangeEnvironmentVariables = this._onDidChangeEnvironmentVariables.event; 22 | 23 | this.watcher = createFileSystemWatcher('**/.env'); 24 | this.disposables.push( 25 | this._onDidChangeEnvironmentVariables, 26 | this.watcher, 27 | this.watcher.onDidCreate((e) => 28 | this._onDidChangeEnvironmentVariables.fire({ uri: e, changeType: FileChangeType.Created }), 29 | ), 30 | this.watcher.onDidChange((e) => 31 | this._onDidChangeEnvironmentVariables.fire({ uri: e, changeType: FileChangeType.Changed }), 32 | ), 33 | this.watcher.onDidDelete((e) => 34 | this._onDidChangeEnvironmentVariables.fire({ uri: e, changeType: FileChangeType.Deleted }), 35 | ), 36 | ); 37 | } 38 | 39 | async getEnvironmentVariables( 40 | uri: Uri | undefined, 41 | overrides?: ({ [key: string]: string | undefined } | Uri)[], 42 | baseEnvVar?: { [key: string]: string | undefined }, 43 | ): Promise<{ [key: string]: string | undefined }> { 44 | const project = uri ? this.pm.get(uri) : undefined; 45 | 46 | const base = baseEnvVar || { ...process.env }; 47 | let env = base; 48 | 49 | const config = getConfiguration('python', project?.uri ?? uri); 50 | let envFilePath = config.get('envFile'); 51 | envFilePath = envFilePath ? path.normalize(resolveVariables(envFilePath, uri)) : undefined; 52 | 53 | if (envFilePath && (await fsapi.pathExists(envFilePath))) { 54 | const other = await parseEnvFile(Uri.file(envFilePath)); 55 | env = mergeEnvVariables(env, other); 56 | } 57 | 58 | let projectEnvFilePath = project ? path.normalize(path.join(project.uri.fsPath, '.env')) : undefined; 59 | if ( 60 | projectEnvFilePath && 61 | projectEnvFilePath?.toLowerCase() !== envFilePath?.toLowerCase() && 62 | (await fsapi.pathExists(projectEnvFilePath)) 63 | ) { 64 | const other = await parseEnvFile(Uri.file(projectEnvFilePath)); 65 | env = mergeEnvVariables(env, other); 66 | } 67 | 68 | if (overrides) { 69 | for (const override of overrides) { 70 | const other = override instanceof Uri ? await parseEnvFile(override) : override; 71 | env = mergeEnvVariables(env, other); 72 | } 73 | } 74 | 75 | return env; 76 | } 77 | 78 | onDidChangeEnvironmentVariables: Event; 79 | 80 | dispose(): void { 81 | this.disposables.forEach((disposable) => disposable.dispose()); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /examples/sample1/src/sampleEnvManager.ts: -------------------------------------------------------------------------------- 1 | import { MarkdownString, LogOutputChannel, Event } from 'vscode'; 2 | import { 3 | CreateEnvironmentOptions, 4 | CreateEnvironmentScope, 5 | DidChangeEnvironmentEventArgs, 6 | DidChangeEnvironmentsEventArgs, 7 | EnvironmentManager, 8 | GetEnvironmentScope, 9 | GetEnvironmentsScope, 10 | IconPath, 11 | PythonEnvironment, 12 | QuickCreateConfig, 13 | RefreshEnvironmentsScope, 14 | ResolveEnvironmentContext, 15 | SetEnvironmentScope, 16 | } from './api'; 17 | 18 | export class SampleEnvManager implements EnvironmentManager { 19 | name: string; 20 | displayName?: string | undefined; 21 | preferredPackageManagerId: string; 22 | description?: string | undefined; 23 | tooltip?: string | MarkdownString | undefined; 24 | iconPath?: IconPath | undefined; 25 | log?: LogOutputChannel | undefined; 26 | 27 | constructor(log: LogOutputChannel) { 28 | this.name = 'sample'; 29 | this.displayName = 'Sample'; 30 | this.preferredPackageManagerId = 'my-publisher.sample:sample'; 31 | // if you want to use builtin `pip` then use this 32 | // this.preferredPackageManagerId = 'ms-python.python:pip'; 33 | this.log = log; 34 | } 35 | 36 | quickCreateConfig(): QuickCreateConfig | undefined { 37 | // Code to provide quick create configuration goes here 38 | 39 | throw new Error('Method not implemented.'); 40 | } 41 | 42 | create?(scope: CreateEnvironmentScope, options?: CreateEnvironmentOptions): Promise { 43 | // Code to handle creating environments goes here 44 | 45 | throw new Error('Method not implemented.'); 46 | } 47 | remove?(environment: PythonEnvironment): Promise { 48 | // Code to handle removing environments goes here 49 | 50 | throw new Error('Method not implemented.'); 51 | } 52 | refresh(scope: RefreshEnvironmentsScope): Promise { 53 | // Code to handle refreshing environments goes here 54 | // This is called when the user clicks on the refresh button in the UI 55 | 56 | throw new Error('Method not implemented.'); 57 | } 58 | getEnvironments(scope: GetEnvironmentsScope): Promise { 59 | // Code to get the list of environments goes here 60 | // This may be called when the python extension is activated to get the list of environments 61 | 62 | throw new Error('Method not implemented.'); 63 | } 64 | 65 | // Event to be raised with the list of available extensions changes for this manager 66 | onDidChangeEnvironments?: Event | undefined; 67 | 68 | set(scope: SetEnvironmentScope, environment?: PythonEnvironment): Promise { 69 | // User selected a environment for the given scope 70 | // undefined environment means user wants to reset the environment for the given scope 71 | 72 | throw new Error('Method not implemented.'); 73 | } 74 | get(scope: GetEnvironmentScope): Promise { 75 | // Code to get the environment for the given scope goes here 76 | 77 | throw new Error('Method not implemented.'); 78 | } 79 | 80 | // Event to be raised when the environment for any active scope changes 81 | onDidChangeEnvironment?: Event | undefined; 82 | 83 | resolve(context: ResolveEnvironmentContext): Promise { 84 | // Code to resolve the environment goes here. Resolving an environment means 85 | // to convert paths to actual environments 86 | 87 | throw new Error('Method not implemented.'); 88 | } 89 | 90 | clearCache?(): Promise { 91 | // Code to clear any cached data goes here 92 | 93 | throw new Error('Method not implemented.'); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/features/terminal/shellStartupSetupHandlers.ts: -------------------------------------------------------------------------------- 1 | import { l10n, ProgressLocation } from 'vscode'; 2 | import { executeCommand } from '../../common/command.api'; 3 | import { ActivationStrings, Common } from '../../common/localize'; 4 | import { traceInfo, traceVerbose } from '../../common/logging'; 5 | import { showErrorMessage, showInformationMessage, withProgress } from '../../common/window.apis'; 6 | import { ShellScriptEditState, ShellStartupScriptProvider } from './shells/startupProvider'; 7 | import { ACT_TYPE_COMMAND, ACT_TYPE_SHELL, getAutoActivationType, setAutoActivationType } from './utils'; 8 | 9 | export async function handleSettingUpShellProfile( 10 | providers: ShellStartupScriptProvider[], 11 | callback: (provider: ShellStartupScriptProvider, result: boolean) => void, 12 | ): Promise { 13 | const shells = providers.map((p) => p.shellType).join(', '); 14 | // Only show prompt when shell integration is not available, or disabled. 15 | const response = await showInformationMessage( 16 | l10n.t( 17 | 'To enable "{0}" activation, your shell profile(s) may need to be updated to include the necessary startup scripts. Would you like to proceed with these changes?', 18 | ACT_TYPE_SHELL, 19 | ), 20 | { modal: true, detail: l10n.t('Shells: {0}', shells) }, 21 | Common.yes, 22 | ); 23 | 24 | if (response === Common.yes) { 25 | traceVerbose(`User chose to set up shell profiles for ${shells} shells`); 26 | const states = await withProgress( 27 | { 28 | location: ProgressLocation.Notification, 29 | title: l10n.t('Setting up shell profiles for {0}', shells), 30 | }, 31 | async () => { 32 | return (await Promise.all(providers.map((provider) => provider.setupScripts()))).filter( 33 | (state) => state !== ShellScriptEditState.NotInstalled, 34 | ); 35 | }, 36 | ); 37 | if (states.every((state) => state === ShellScriptEditState.Edited)) { 38 | setImmediate(async () => { 39 | await showInformationMessage( 40 | l10n.t( 41 | 'Shell profiles have been set up successfully. Extension will use shell startup activation next time a new terminal is created.', 42 | ), 43 | ); 44 | }); 45 | providers.forEach((provider) => callback(provider, true)); 46 | } else { 47 | setImmediate(async () => { 48 | const button = await showErrorMessage( 49 | l10n.t('Failed to set up shell profiles. Please check the output panel for more details.'), 50 | Common.viewLogs, 51 | ); 52 | if (button === Common.viewLogs) { 53 | await executeCommand('python-envs.viewLogs'); 54 | } 55 | }); 56 | providers.forEach((provider) => callback(provider, false)); 57 | } 58 | } else { 59 | traceInfo(`User declined shell profile setup for ${shells}, switching to command activation`); 60 | await Promise.all(providers.map((provider) => provider.teardownScripts())); 61 | await setAutoActivationType(ACT_TYPE_COMMAND); 62 | } 63 | } 64 | 65 | export async function cleanupStartupScripts(allProviders: ShellStartupScriptProvider[]): Promise { 66 | await Promise.all(allProviders.map((provider) => provider.teardownScripts())); 67 | if (getAutoActivationType() === ACT_TYPE_SHELL) { 68 | setAutoActivationType(ACT_TYPE_COMMAND); 69 | traceInfo( 70 | 'Setting `python-envs.terminal.autoActivationType` to `command`, after removing shell startup scripts.', 71 | ); 72 | } 73 | setImmediate(async () => await showInformationMessage(ActivationStrings.revertedShellStartupScripts)); 74 | } 75 | -------------------------------------------------------------------------------- /src/test/features/terminal/shells/common/shellUtils.unit.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import { 3 | extractProfilePath, 4 | PROFILE_TAG_END, 5 | PROFILE_TAG_START, 6 | } from '../../../../../features/terminal/shells/common/shellUtils'; 7 | 8 | suite('Shell Utils', () => { 9 | suite('extractProfilePath', () => { 10 | test('should return undefined when content is empty', () => { 11 | const content = ''; 12 | const result = extractProfilePath(content); 13 | assert.strictEqual(result, undefined); 14 | }); 15 | 16 | test('should return undefined when content does not have tags', () => { 17 | const content = 'sample content without tags'; 18 | const result = extractProfilePath(content); 19 | assert.strictEqual(result, undefined); 20 | }); 21 | 22 | test('should return undefined when only start tag exists', () => { 23 | const content = `content\n${PROFILE_TAG_START}\nsome path`; 24 | const result = extractProfilePath(content); 25 | assert.strictEqual(result, undefined); 26 | }); 27 | 28 | test('should return undefined when only end tag exists', () => { 29 | const content = `content\nsome path\n${PROFILE_TAG_END}`; 30 | const result = extractProfilePath(content); 31 | assert.strictEqual(result, undefined); 32 | }); 33 | 34 | test('should return undefined when tags are in wrong order', () => { 35 | const content = `content\n${PROFILE_TAG_END}\nsome path\n${PROFILE_TAG_START}`; 36 | const result = extractProfilePath(content); 37 | assert.strictEqual(result, undefined); 38 | }); 39 | test('should return undefined when content between tags is empty', () => { 40 | const content = `content\n${PROFILE_TAG_START}\n\n${PROFILE_TAG_END}\nmore content`; 41 | const result = extractProfilePath(content); 42 | assert.strictEqual(result, undefined); 43 | }); 44 | 45 | test('should extract path when found between tags', () => { 46 | const expectedPath = '/usr/local/bin/python'; 47 | const content = `content\n${PROFILE_TAG_START}\n${expectedPath}\n${PROFILE_TAG_END}\nmore content`; 48 | const result = extractProfilePath(content); 49 | assert.strictEqual(result, expectedPath); 50 | }); 51 | 52 | test('should trim whitespace from extracted path', () => { 53 | const expectedPath = '/usr/local/bin/python'; 54 | const content = `content\n${PROFILE_TAG_START}\n ${expectedPath} \n${PROFILE_TAG_END}\nmore content`; 55 | const result = extractProfilePath(content); 56 | assert.strictEqual(result, expectedPath); 57 | }); 58 | 59 | test('should handle Windows-style line endings', () => { 60 | const expectedPath = 'C:\\Python\\python.exe'; 61 | const content = `content\r\n${PROFILE_TAG_START}\r\n${expectedPath}\r\n${PROFILE_TAG_END}\r\nmore content`; 62 | const result = extractProfilePath(content); 63 | assert.strictEqual(result, expectedPath); 64 | }); 65 | 66 | test('should extract path with special characters', () => { 67 | const expectedPath = '/path with spaces/and (parentheses)/python'; 68 | const content = `${PROFILE_TAG_START}\n${expectedPath}\n${PROFILE_TAG_END}`; 69 | const result = extractProfilePath(content); 70 | assert.strictEqual(result, expectedPath); 71 | }); 72 | 73 | test('should extract multiline content correctly', () => { 74 | const expectedPath = 'line1\nline2\nline3'; 75 | const content = `${PROFILE_TAG_START}\n${expectedPath}\n${PROFILE_TAG_END}`; 76 | const result = extractProfilePath(content); 77 | assert.strictEqual(result, expectedPath); 78 | }); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /src/test/common/environmentPicker.unit.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import assert from 'node:assert'; 5 | import { Uri } from 'vscode'; 6 | import { PythonEnvironment } from '../../api'; 7 | 8 | /** 9 | * Test the logic used in environment pickers to include interpreter paths in descriptions 10 | */ 11 | suite('Environment Picker Description Logic', () => { 12 | const createMockEnvironment = ( 13 | displayPath: string, 14 | description?: string, 15 | name: string = 'Python 3.9.0', 16 | ): PythonEnvironment => ({ 17 | envId: { id: 'test', managerId: 'test-manager' }, 18 | name, 19 | displayName: name, 20 | displayPath, 21 | version: '3.9.0', 22 | environmentPath: Uri.file(displayPath), 23 | description, 24 | sysPrefix: '/path/to/prefix', 25 | execInfo: { run: { executable: displayPath } }, 26 | }); 27 | 28 | suite('Description formatting with interpreter path', () => { 29 | test('should use displayPath as description when no original description exists', () => { 30 | const env = createMockEnvironment('/usr/local/bin/python'); 31 | 32 | // This is the logic from our updated picker 33 | const pathDescription = env.displayPath; 34 | const description = 35 | env.description && env.description.trim() ? `${env.description} (${pathDescription})` : pathDescription; 36 | 37 | assert.strictEqual(description, '/usr/local/bin/python'); 38 | }); 39 | 40 | test('should append displayPath to existing description in parentheses', () => { 41 | const env = createMockEnvironment('/home/user/.venv/bin/python', 'Virtual Environment'); 42 | 43 | // This is the logic from our updated picker 44 | const pathDescription = env.displayPath; 45 | const description = 46 | env.description && env.description.trim() ? `${env.description} (${pathDescription})` : pathDescription; 47 | 48 | assert.strictEqual(description, 'Virtual Environment (/home/user/.venv/bin/python)'); 49 | }); 50 | 51 | test('should handle complex paths correctly', () => { 52 | const complexPath = '/usr/local/anaconda3/envs/my-project-env/bin/python'; 53 | const env = createMockEnvironment(complexPath, 'Conda Environment'); 54 | 55 | // This is the logic from our updated picker 56 | const pathDescription = env.displayPath; 57 | const description = 58 | env.description && env.description.trim() ? `${env.description} (${pathDescription})` : pathDescription; 59 | 60 | assert.strictEqual(description, `Conda Environment (${complexPath})`); 61 | }); 62 | 63 | test('should handle empty description correctly', () => { 64 | const env = createMockEnvironment('/opt/python/bin/python', ''); 65 | 66 | // This is the logic from our updated picker 67 | const pathDescription = env.displayPath; 68 | const description = 69 | env.description && env.description.trim() ? `${env.description} (${pathDescription})` : pathDescription; 70 | 71 | // Empty string should be treated like no description, so just use path 72 | assert.strictEqual(description, '/opt/python/bin/python'); 73 | }); 74 | 75 | test('should handle Windows paths correctly', () => { 76 | const windowsPath = 'C:\\Python39\\python.exe'; 77 | const env = createMockEnvironment(windowsPath, 'System Python'); 78 | 79 | // This is the logic from our updated picker 80 | const pathDescription = env.displayPath; 81 | const description = 82 | env.description && env.description.trim() ? `${env.description} (${pathDescription})` : pathDescription; 83 | 84 | assert.strictEqual(description, 'System Python (C:\\Python39\\python.exe)'); 85 | }); 86 | }); 87 | }); 88 | -------------------------------------------------------------------------------- /src/features/terminal/shells/bash/bashEnvs.ts: -------------------------------------------------------------------------------- 1 | import { EnvironmentVariableCollection } from 'vscode'; 2 | import { PythonEnvironment } from '../../../../api'; 3 | import { traceError } from '../../../../common/logging'; 4 | import { ShellConstants } from '../../../common/shellConstants'; 5 | import { getShellActivationCommand, getShellCommandAsString } from '../common/shellUtils'; 6 | import { ShellEnvsProvider } from '../startupProvider'; 7 | import { BASH_ENV_KEY, ZSH_ENV_KEY } from './bashConstants'; 8 | 9 | export class BashEnvsProvider implements ShellEnvsProvider { 10 | constructor(public readonly shellType: 'bash' | 'gitbash') {} 11 | 12 | updateEnvVariables(collection: EnvironmentVariableCollection, env: PythonEnvironment): void { 13 | try { 14 | const bashActivation = getShellActivationCommand(this.shellType, env); 15 | if (bashActivation) { 16 | const command = getShellCommandAsString(this.shellType, bashActivation); 17 | const v = collection.get(BASH_ENV_KEY); 18 | if (v?.value === command) { 19 | return; 20 | } 21 | collection.replace(BASH_ENV_KEY, command); 22 | } else { 23 | collection.delete(BASH_ENV_KEY); 24 | } 25 | } catch (err) { 26 | traceError(`Failed to update env variables for ${this.shellType}`, err); 27 | collection.delete(BASH_ENV_KEY); 28 | } 29 | } 30 | 31 | removeEnvVariables(envCollection: EnvironmentVariableCollection): void { 32 | envCollection.delete(BASH_ENV_KEY); 33 | } 34 | 35 | getEnvVariables(env?: PythonEnvironment): Map | undefined { 36 | if (!env) { 37 | return new Map([[BASH_ENV_KEY, undefined]]); 38 | } 39 | 40 | try { 41 | const bashActivation = getShellActivationCommand(this.shellType, env); 42 | if (bashActivation) { 43 | const command = getShellCommandAsString(this.shellType, bashActivation); 44 | return new Map([[BASH_ENV_KEY, command]]); 45 | } 46 | return undefined; 47 | } catch (err) { 48 | traceError(`Failed to get env variables for ${this.shellType}`, err); 49 | return undefined; 50 | } 51 | } 52 | } 53 | 54 | export class ZshEnvsProvider implements ShellEnvsProvider { 55 | public readonly shellType: string = ShellConstants.ZSH; 56 | updateEnvVariables(collection: EnvironmentVariableCollection, env: PythonEnvironment): void { 57 | try { 58 | const zshActivation = getShellActivationCommand(this.shellType, env); 59 | if (zshActivation) { 60 | const command = getShellCommandAsString(this.shellType, zshActivation); 61 | const v = collection.get(ZSH_ENV_KEY); 62 | if (v?.value === command) { 63 | return; 64 | } 65 | collection.replace(ZSH_ENV_KEY, command); 66 | } else { 67 | collection.delete(ZSH_ENV_KEY); 68 | } 69 | } catch (err) { 70 | traceError('Failed to update env variables for zsh', err); 71 | collection.delete(ZSH_ENV_KEY); 72 | } 73 | } 74 | 75 | removeEnvVariables(collection: EnvironmentVariableCollection): void { 76 | collection.delete(ZSH_ENV_KEY); 77 | } 78 | 79 | getEnvVariables(env?: PythonEnvironment): Map | undefined { 80 | if (!env) { 81 | return new Map([[ZSH_ENV_KEY, undefined]]); 82 | } 83 | 84 | try { 85 | const zshActivation = getShellActivationCommand(this.shellType, env); 86 | if (zshActivation) { 87 | const command = getShellCommandAsString(this.shellType, zshActivation); 88 | return new Map([[ZSH_ENV_KEY, command]]); 89 | } 90 | return undefined; 91 | } catch (err) { 92 | traceError('Failed to get env variables for zsh', err); 93 | return undefined; 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/test/features/terminalEnvVarInjectorBasic.unit.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import * as sinon from 'sinon'; 5 | import * as typeMoq from 'typemoq'; 6 | import { GlobalEnvironmentVariableCollection, workspace } from 'vscode'; 7 | import { EnvVarManager } from '../../features/execution/envVariableManager'; 8 | import { TerminalEnvVarInjector } from '../../features/terminal/terminalEnvVarInjector'; 9 | 10 | interface MockScopedCollection { 11 | clear: sinon.SinonStub; 12 | replace: sinon.SinonStub; 13 | delete: sinon.SinonStub; 14 | } 15 | 16 | suite('TerminalEnvVarInjector Basic Tests', () => { 17 | let envVarCollection: typeMoq.IMock; 18 | let envVarManager: typeMoq.IMock; 19 | let injector: TerminalEnvVarInjector; 20 | let mockScopedCollection: MockScopedCollection; 21 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 22 | let workspaceFoldersStub: any; 23 | 24 | setup(() => { 25 | envVarCollection = typeMoq.Mock.ofType(); 26 | envVarManager = typeMoq.Mock.ofType(); 27 | 28 | // Mock workspace.workspaceFolders property 29 | workspaceFoldersStub = []; 30 | Object.defineProperty(workspace, 'workspaceFolders', { 31 | get: () => workspaceFoldersStub, 32 | configurable: true, 33 | }); 34 | 35 | // Setup scoped collection mock 36 | mockScopedCollection = { 37 | clear: sinon.stub(), 38 | replace: sinon.stub(), 39 | delete: sinon.stub(), 40 | }; 41 | 42 | // Setup environment variable collection to return scoped collection 43 | envVarCollection 44 | .setup((x) => x.getScoped(typeMoq.It.isAny())) 45 | .returns( 46 | () => mockScopedCollection as unknown as ReturnType, 47 | ); 48 | envVarCollection.setup((x) => x.clear()).returns(() => {}); 49 | 50 | // Setup minimal mocks for event subscriptions 51 | envVarManager 52 | .setup((m) => m.onDidChangeEnvironmentVariables) 53 | .returns( 54 | () => 55 | ({ 56 | dispose: () => {}, 57 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 58 | } as any), 59 | ); 60 | }); 61 | 62 | teardown(() => { 63 | sinon.restore(); 64 | injector?.dispose(); 65 | }); 66 | 67 | test('should initialize without errors', () => { 68 | // Arrange & Act 69 | injector = new TerminalEnvVarInjector(envVarCollection.object, envVarManager.object); 70 | 71 | // Assert - should not throw 72 | sinon.assert.match(injector, sinon.match.object); 73 | }); 74 | 75 | test('should dispose cleanly', () => { 76 | // Arrange 77 | injector = new TerminalEnvVarInjector(envVarCollection.object, envVarManager.object); 78 | 79 | // Act 80 | injector.dispose(); 81 | 82 | // Assert - should clear on dispose 83 | envVarCollection.verify((c) => c.clear(), typeMoq.Times.atLeastOnce()); 84 | }); 85 | 86 | test('should register environment variable change event handler', () => { 87 | // Arrange 88 | let eventHandlerRegistered = false; 89 | envVarManager.reset(); 90 | envVarManager 91 | .setup((m) => m.onDidChangeEnvironmentVariables) 92 | .returns((_handler) => { 93 | eventHandlerRegistered = true; 94 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 95 | return { dispose: () => {} } as any; 96 | }); 97 | 98 | // Act 99 | injector = new TerminalEnvVarInjector(envVarCollection.object, envVarManager.object); 100 | 101 | // Assert 102 | sinon.assert.match(eventHandlerRegistered, true); 103 | }); 104 | }); 105 | -------------------------------------------------------------------------------- /src/test/features/common/shellDetector.unit.test.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import { Terminal } from 'vscode'; 3 | import { isWindows } from '../../../common/utils/platformUtils'; 4 | import { ShellConstants } from '../../../features/common/shellConstants'; 5 | import { identifyTerminalShell } from '../../../features/common/shellDetector'; 6 | 7 | const testShellTypes: string[] = [ 8 | 'sh', 9 | 'bash', 10 | 'powershell', 11 | 'pwsh', 12 | 'powershellcore', 13 | 'cmd', 14 | 'commandPrompt', 15 | 'gitbash', 16 | 'zsh', 17 | 'ksh', 18 | 'fish', 19 | 'csh', 20 | 'cshell', 21 | 'tcsh', 22 | 'tcshell', 23 | 'nu', 24 | 'nushell', 25 | 'wsl', 26 | 'xonsh', 27 | 'unknown', 28 | ]; 29 | 30 | function getNameByShellType(shellType: string): string { 31 | return shellType === 'unknown' ? '' : shellType; 32 | } 33 | 34 | function getShellPath(shellType: string): string | undefined { 35 | switch (shellType) { 36 | case 'sh': 37 | return '/bin/sh'; 38 | case 'bash': 39 | return '/bin/bash'; 40 | case 'powershell': 41 | return 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe'; 42 | case 'pwsh': 43 | case 'powershellcore': 44 | return 'C:\\Program Files\\PowerShell\\7\\pwsh.exe'; 45 | case 'cmd': 46 | case 'commandPrompt': 47 | return 'C:\\Windows\\System32\\cmd.exe'; 48 | case 'gitbash': 49 | return isWindows() ? 'C:\\Program Files\\Git\\bin\\bash.exe' : '/usr/bin/gitbash'; 50 | case 'zsh': 51 | return '/bin/zsh'; 52 | case 'ksh': 53 | return '/bin/ksh'; 54 | case 'fish': 55 | return '/usr/bin/fish'; 56 | case 'csh': 57 | case 'cshell': 58 | return '/bin/csh'; 59 | case 'nu': 60 | case 'nushell': 61 | return '/usr/bin/nu'; 62 | case 'tcsh': 63 | case 'tcshell': 64 | return '/usr/bin/tcsh'; 65 | case 'wsl': 66 | return '/mnt/c/Windows/System32/wsl.exe'; 67 | case 'xonsh': 68 | return '/usr/bin/xonsh'; 69 | default: 70 | return undefined; 71 | } 72 | } 73 | 74 | function expectedShellType(shellType: string): string { 75 | switch (shellType) { 76 | case 'sh': 77 | return ShellConstants.SH; 78 | case 'bash': 79 | return ShellConstants.BASH; 80 | case 'pwsh': 81 | case 'powershell': 82 | case 'powershellcore': 83 | return ShellConstants.PWSH; 84 | case 'cmd': 85 | case 'commandPrompt': 86 | return ShellConstants.CMD; 87 | case 'gitbash': 88 | return ShellConstants.GITBASH; 89 | case 'zsh': 90 | return ShellConstants.ZSH; 91 | case 'ksh': 92 | return ShellConstants.KSH; 93 | case 'fish': 94 | return ShellConstants.FISH; 95 | case 'csh': 96 | case 'cshell': 97 | return ShellConstants.CSH; 98 | case 'nu': 99 | case 'nushell': 100 | return ShellConstants.NU; 101 | case 'tcsh': 102 | case 'tcshell': 103 | return ShellConstants.TCSH; 104 | case 'xonsh': 105 | return ShellConstants.XONSH; 106 | case 'wsl': 107 | return ShellConstants.WSL; 108 | default: 109 | return 'unknown'; 110 | } 111 | } 112 | 113 | suite('Shell Detector', () => { 114 | testShellTypes.forEach((shell) => { 115 | if (shell === 'unknown') { 116 | return; 117 | } 118 | 119 | const name = getNameByShellType(shell); 120 | test(`Detect ${shell}`, () => { 121 | const terminal = { 122 | name, 123 | state: { shell }, 124 | creationOptions: { 125 | shellPath: getShellPath(shell), 126 | }, 127 | } as Terminal; 128 | const detected = identifyTerminalShell(terminal); 129 | const expected = expectedShellType(shell); 130 | assert.strictEqual(detected, expected); 131 | }); 132 | }); 133 | }); 134 | -------------------------------------------------------------------------------- /src/test/mocks/vsc/position.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | /** 4 | * A position in the editor. This interface is suitable for serialization. 5 | */ 6 | export interface IPosition { 7 | /** 8 | * line number (starts at 1) 9 | */ 10 | readonly lineNumber: number; 11 | /** 12 | * column (the first character in a line is between column 1 and column 2) 13 | */ 14 | readonly column: number; 15 | } 16 | 17 | /** 18 | * A position in the editor. 19 | */ 20 | export class Position { 21 | /** 22 | * line number (starts at 1) 23 | */ 24 | public readonly lineNumber: number; 25 | 26 | /** 27 | * column (the first character in a line is between column 1 and column 2) 28 | */ 29 | public readonly column: number; 30 | 31 | constructor(lineNumber: number, column: number) { 32 | this.lineNumber = lineNumber; 33 | this.column = column; 34 | } 35 | 36 | /** 37 | * Test if this position equals other position 38 | */ 39 | public equals(other: IPosition): boolean { 40 | return Position.equals(this, other); 41 | } 42 | 43 | /** 44 | * Test if position `a` equals position `b` 45 | */ 46 | public static equals(a: IPosition, b: IPosition): boolean { 47 | if (!a && !b) { 48 | return true; 49 | } 50 | return !!a && !!b && a.lineNumber === b.lineNumber && a.column === b.column; 51 | } 52 | 53 | /** 54 | * Test if this position is before other position. 55 | * If the two positions are equal, the result will be false. 56 | */ 57 | public isBefore(other: IPosition): boolean { 58 | return Position.isBefore(this, other); 59 | } 60 | 61 | /** 62 | * Test if position `a` is before position `b`. 63 | * If the two positions are equal, the result will be false. 64 | */ 65 | public static isBefore(a: IPosition, b: IPosition): boolean { 66 | if (a.lineNumber < b.lineNumber) { 67 | return true; 68 | } 69 | if (b.lineNumber < a.lineNumber) { 70 | return false; 71 | } 72 | return a.column < b.column; 73 | } 74 | 75 | /** 76 | * Test if this position is before other position. 77 | * If the two positions are equal, the result will be true. 78 | */ 79 | public isBeforeOrEqual(other: IPosition): boolean { 80 | return Position.isBeforeOrEqual(this, other); 81 | } 82 | 83 | /** 84 | * Test if position `a` is before position `b`. 85 | * If the two positions are equal, the result will be true. 86 | */ 87 | public static isBeforeOrEqual(a: IPosition, b: IPosition): boolean { 88 | if (a.lineNumber < b.lineNumber) { 89 | return true; 90 | } 91 | if (b.lineNumber < a.lineNumber) { 92 | return false; 93 | } 94 | return a.column <= b.column; 95 | } 96 | 97 | /** 98 | * A function that compares positions, useful for sorting 99 | */ 100 | public static compare(a: IPosition, b: IPosition): number { 101 | const aLineNumber = a.lineNumber | 0; 102 | const bLineNumber = b.lineNumber | 0; 103 | 104 | if (aLineNumber === bLineNumber) { 105 | const aColumn = a.column | 0; 106 | const bColumn = b.column | 0; 107 | return aColumn - bColumn; 108 | } 109 | 110 | return aLineNumber - bLineNumber; 111 | } 112 | 113 | /** 114 | * Clone this position. 115 | */ 116 | public clone(): Position { 117 | return new Position(this.lineNumber, this.column); 118 | } 119 | 120 | /** 121 | * Convert to a human-readable representation. 122 | */ 123 | public toString(): string { 124 | return `(${this.lineNumber},${this.column})`; 125 | } 126 | 127 | // --- 128 | 129 | /** 130 | * Create a `Position` from an `IPosition`. 131 | */ 132 | public static lift(pos: IPosition): Position { 133 | return new Position(pos.lineNumber, pos.column); 134 | } 135 | 136 | /** 137 | * Test if `obj` is an `IPosition`. 138 | */ 139 | public static isIPosition(obj?: { lineNumber: unknown; column: unknown }): obj is IPosition { 140 | return obj !== undefined && typeof obj.lineNumber === 'number' && typeof obj.column === 'number'; 141 | } 142 | } 143 | --------------------------------------------------------------------------------