diff --git a/package.json b/package.json index 544c72dba023..b6f8782076a0 100644 --- a/package.json +++ b/package.json @@ -622,6 +622,12 @@ "scope": "resource", "type": "boolean" }, + "python.terminal.shellIntegration.activate": { + "default": false, + "markdownDescription": "%python.terminal.shellIntegration.activate.description%", + "scope": "resource", + "type": "boolean" + }, "python.terminal.executeInFileDir": { "default": false, "description": "%python.terminal.executeInFileDir.description%", diff --git a/package.nls.json b/package.nls.json index 57f2ed95b2c0..be68ad9a8fef 100644 --- a/package.nls.json +++ b/package.nls.json @@ -76,6 +76,7 @@ "python.terminal.shellIntegration.enabled.description": "Enable [shell integration](https://code.visualstudio.com/docs/terminal/shell-integration) for the terminals running python. Shell integration enhances the terminal experience by enabling command decorations, run recent command, improving accessibility among other things. Note: PyREPL (available in Python 3.13+) is automatically disabled when shell integration is enabled to avoid cursor indentation issues.", "python.terminal.activateEnvInCurrentTerminal.description": "Activate Python Environment in the current Terminal on load of the Extension.", "python.terminal.activateEnvironment.description": "Activate Python Environment in all Terminals created.", + "python.terminal.shellIntegration.activate.description": "Activate Python environment using shell integration. This means activating without sending activate script in your terminal, preserving shell integration capabilities and activated Copilot terminals.", "python.terminal.executeInFileDir.description": "When executing a file in the terminal, whether to use execute in the file's directory, instead of the current open folder.", "python.terminal.focusAfterLaunch.description": "When launching a python terminal, whether to focus the cursor on the terminal.", "python.terminal.launchArgs.description": "Python launch arguments to use when executing a file in the terminal.", diff --git a/src/client/common/configSettings.ts b/src/client/common/configSettings.ts index 91c06d9331fd..b77788d672da 100644 --- a/src/client/common/configSettings.ts +++ b/src/client/common/configSettings.ts @@ -379,6 +379,7 @@ export class PythonSettings implements IPythonSettings { activateEnvInCurrentTerminal: false, shellIntegration: { enabled: false, + activate: false, }, }; diff --git a/src/client/common/types.ts b/src/client/common/types.ts index c30ad704b6c1..08a7dd406043 100644 --- a/src/client/common/types.ts +++ b/src/client/common/types.ts @@ -190,6 +190,7 @@ export interface ITerminalSettings { readonly activateEnvInCurrentTerminal: boolean; readonly shellIntegration: { enabled: boolean; + activate: boolean; }; } diff --git a/src/client/terminals/envCollectionActivation/service.ts b/src/client/terminals/envCollectionActivation/service.ts index 2ce8d5d5d86a..bba8f6471d62 100644 --- a/src/client/terminals/envCollectionActivation/service.ts +++ b/src/client/terminals/envCollectionActivation/service.ts @@ -97,6 +97,15 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ public async activate(resource: Resource): Promise { try { + // TODO: Consider activating from somewhere else since we are removing `terminalEnvVar` experiment + // Check shellIntegration.activate first - this should work regardless of + // env extension or terminalEnvVar experiment + const settings = this.configurationService.getSettings(resource); + if (settings.terminal.shellIntegration.activate) { + await this.activateUsingShellIntegrationEnvVar(resource); + return; + } + if (useEnvExtension()) { traceVerbose('Ignoring environment variable experiment since env extension is being used'); this.context.environmentVariableCollection.clear(); @@ -170,6 +179,49 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ } } + /** + * Activates environments using shell-specific environment variables (e.g., VSCODE_PYTHON_BASH_ACTIVATE). + */ + private async activateUsingShellIntegrationEnvVar(resource: Resource): Promise { + if (!this.registeredOnce) { + this.interpreterService.onDidChangeInterpreter( + async (r) => { + const settings = this.configurationService.getSettings(r); + if (settings.terminal.shellIntegration.activate) { + await this.applyActivateEnvVarForResource(r).ignoreErrors(); + } + }, + this, + this.disposables, + ); + this.applicationEnvironment.onDidChangeShell( + async (shell: string) => { + const settings = this.configurationService.getSettings(resource); + if (settings.terminal.shellIntegration.activate) { + await this.applyActivateEnvVarForResource(resource, shell).ignoreErrors(); + } + }, + this, + this.disposables, + ); + this.registeredOnce = true; + } + await this.applyActivateEnvVarForResource(resource); + await registerPythonStartup(this.context); + } + + /** + * Applies the shell-specific activate environment variable for a given resource. + */ + private async applyActivateEnvVarForResource( + resource: Resource, + shell = this.applicationEnvironment.shell, + ): Promise { + const workspaceFolder = this.getWorkspaceFolder(resource); + const envVarCollection = this.getEnvironmentVariableCollection({ workspaceFolder }); + await this.applyActivateEnvVar(resource, shell, envVarCollection); + } + public async _applyCollection(resource: Resource, shell?: string): Promise { this.progressService.showProgress({ location: ProgressLocation.Window, @@ -197,6 +249,7 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ traceVerbose('Activating environments in terminal is disabled for', resource?.fsPath); return; } + const activatedEnv = await this.environmentActivationService.getActivatedEnvironmentVariables( resource, undefined, @@ -308,6 +361,87 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ }); } + /** + * Applies the VSCODE_PYTHON_*_ACTIVATE environment variables to enable activation + * by contributing the full activation command via environment variable + * instead of sending activation commands directly to the terminal. + * Sets ALL shell-specific variables at once so any shell can be activated. + * Supports bash, zsh, fish, PowerShell, and Command Prompt. + */ + private async applyActivateEnvVar( + resource: Resource, + _shell: string, + envVarCollection: ReturnType, + ): Promise { + const interpreter = await this.interpreterService.getActiveInterpreter(resource); + if (!interpreter) { + traceVerbose('No interpreter found for shell integration activation'); + envVarCollection.clear(); + return; + } + + // TODO, important: Make sure we test+get it working for conda, etc as well. + if (interpreter.envType !== EnvironmentType.Venv && interpreter.type !== PythonEnvType.Virtual) { + envVarCollection.clear(); + return; + } + + const binDir = path.dirname(interpreter.path); + + // Clear any previously set env vars + envVarCollection.clear(); + + const options = { + applyAtShellIntegration: true, + applyAtProcessCreation: true, + }; + + // Set ALL shell-specific environment variables at once + // Bash + const bashActivate = path.join(binDir, 'activate'); + if (await pathExists(bashActivate)) { + const bashCommand = `source ${bashActivate}`; + traceLog(`Setting VSCODE_PYTHON_BASH_ACTIVATE to ${bashCommand}`); + envVarCollection.replace('VSCODE_PYTHON_BASH_ACTIVATE', bashCommand, options); + + // ZSH uses the same activate script + traceLog(`Setting VSCODE_PYTHON_ZSH_ACTIVATE to ${bashCommand}`); + envVarCollection.replace('VSCODE_PYTHON_ZSH_ACTIVATE', bashCommand, options); + } + + // Fish + const fishActivate = path.join(binDir, 'activate.fish'); + if (await pathExists(fishActivate)) { + const fishCommand = `source ${fishActivate}`; + traceLog(`Setting VSCODE_PYTHON_FISH_ACTIVATE to ${fishCommand}`); + envVarCollection.replace('VSCODE_PYTHON_FISH_ACTIVATE', fishCommand, options); + } + + // PowerShell + const pwshActivate = path.join(binDir, 'Activate.ps1'); + if (await pathExists(pwshActivate)) { + const pwshCommand = `& ${pwshActivate}`; + traceLog(`Setting VSCODE_PYTHON_PWSH_ACTIVATE to ${pwshCommand}`); + envVarCollection.replace('VSCODE_PYTHON_PWSH_ACTIVATE', pwshCommand, options); + } + + // Command Prompt + // TODO: We need to modify user's shell init for cmd, shell integration doesnt work for cmd + const cmdActivate = path.join(binDir, 'activate.bat'); + if (await pathExists(cmdActivate)) { + traceLog(`Setting VSCODE_PYTHON_CMD_ACTIVATE to ${cmdActivate}`); + envVarCollection.replace('VSCODE_PYTHON_CMD_ACTIVATE', cmdActivate, options); + } + + const workspaceFolder = this.getWorkspaceFolder(resource); + const settings = this.configurationService.getSettings(resource); + const displayPath = this.pathUtils.getDisplayName(settings.pythonPath, workspaceFolder?.uri.fsPath); + const description = new MarkdownString( + `${Interpreters.activateTerminalDescription} \`${displayPath}\` (via shell integration)`, + ); + envVarCollection.description = description; + } + private isPromptSet = new Map(); // eslint-disable-next-line class-methods-use-this