Skip to content

Commit 7d18e5d

Browse files
committed
Shell integration activate from Python extension
1 parent e2681d5 commit 7d18e5d

File tree

5 files changed

+239
-0
lines changed

5 files changed

+239
-0
lines changed

package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -622,6 +622,12 @@
622622
"scope": "resource",
623623
"type": "boolean"
624624
},
625+
"python.terminal.shellIntegration.activate": {
626+
"default": false,
627+
"markdownDescription": "%python.terminal.shellIntegration.activate.description%",
628+
"scope": "resource",
629+
"type": "boolean"
630+
},
625631
"python.terminal.executeInFileDir": {
626632
"default": false,
627633
"description": "%python.terminal.executeInFileDir.description%",

package.nls.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@
7676
"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.",
7777
"python.terminal.activateEnvInCurrentTerminal.description": "Activate Python Environment in the current Terminal on load of the Extension.",
7878
"python.terminal.activateEnvironment.description": "Activate Python Environment in all Terminals created.",
79+
"python.terminal.shellIntegration.activate.description": "When enabled, activates Python environments by contributing shell-specific environment variables (e.g., `VSCODE_PYTHON_BASH_ACTIVATE`, `VSCODE_PYTHON_ZSH_ACTIVATE`, `VSCODE_PYTHON_FISH_ACTIVATE`, `VSCODE_PYTHON_PS1_ACTIVATE`, `VSCODE_PYTHON_CMD_ACTIVATE`) pointing to the activate script, instead of sending activation commands directly to the terminal. Supports bash, zsh, fish, PowerShell, and Command Prompt.",
7980
"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.",
8081
"python.terminal.focusAfterLaunch.description": "When launching a python terminal, whether to focus the cursor on the terminal.",
8182
"python.terminal.launchArgs.description": "Python launch arguments to use when executing a file in the terminal.",

src/client/common/configSettings.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,7 @@ export class PythonSettings implements IPythonSettings {
379379
activateEnvInCurrentTerminal: false,
380380
shellIntegration: {
381381
enabled: false,
382+
activate: false,
382383
},
383384
};
384385

src/client/common/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ export interface ITerminalSettings {
190190
readonly activateEnvInCurrentTerminal: boolean;
191191
readonly shellIntegration: {
192192
enabled: boolean;
193+
activate: boolean;
193194
};
194195
}
195196

src/client/terminals/envCollectionActivation/service.ts

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,14 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ
9797

9898
public async activate(resource: Resource): Promise<void> {
9999
try {
100+
// Check shellIntegration.activate first - this should work regardless of
101+
// env extension or terminalEnvVar experiment
102+
const settings = this.configurationService.getSettings(resource);
103+
if (settings.terminal.shellIntegration.activate) {
104+
await this.activateUsingEnvVar(resource);
105+
return;
106+
}
107+
100108
if (useEnvExtension()) {
101109
traceVerbose('Ignoring environment variable experiment since env extension is being used');
102110
this.context.environmentVariableCollection.clear();
@@ -170,6 +178,50 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ
170178
}
171179
}
172180

181+
/**
182+
* Activates environments using shell-specific environment variables (e.g., VSCODE_PYTHON_BASH_ACTIVATE).
183+
* This method works independently of the env extension or terminalEnvVar experiment.
184+
*/
185+
private async activateUsingEnvVar(resource: Resource): Promise<void> {
186+
if (!this.registeredOnce) {
187+
this.interpreterService.onDidChangeInterpreter(
188+
async (r) => {
189+
const settings = this.configurationService.getSettings(r);
190+
if (settings.terminal.shellIntegration.activate) {
191+
await this.applyActivateEnvVarForResource(r).ignoreErrors();
192+
}
193+
},
194+
this,
195+
this.disposables,
196+
);
197+
this.applicationEnvironment.onDidChangeShell(
198+
async (shell: string) => {
199+
const settings = this.configurationService.getSettings(resource);
200+
if (settings.terminal.shellIntegration.activate) {
201+
await this.applyActivateEnvVarForResource(resource, shell).ignoreErrors();
202+
}
203+
},
204+
this,
205+
this.disposables,
206+
);
207+
this.registeredOnce = true;
208+
}
209+
await this.applyActivateEnvVarForResource(resource);
210+
await registerPythonStartup(this.context);
211+
}
212+
213+
/**
214+
* Applies the shell-specific activate environment variable for a given resource.
215+
*/
216+
private async applyActivateEnvVarForResource(
217+
resource: Resource,
218+
shell = this.applicationEnvironment.shell,
219+
): Promise<void> {
220+
const workspaceFolder = this.getWorkspaceFolder(resource);
221+
const envVarCollection = this.getEnvironmentVariableCollection({ workspaceFolder });
222+
await this.applyActivateEnvVar(resource, shell, envVarCollection);
223+
}
224+
173225
public async _applyCollection(resource: Resource, shell?: string): Promise<void> {
174226
this.progressService.showProgress({
175227
location: ProgressLocation.Window,
@@ -197,6 +249,7 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ
197249
traceVerbose('Activating environments in terminal is disabled for', resource?.fsPath);
198250
return;
199251
}
252+
200253
const activatedEnv = await this.environmentActivationService.getActivatedEnvironmentVariables(
201254
resource,
202255
undefined,
@@ -308,6 +361,183 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ
308361
});
309362
}
310363

364+
/**
365+
* Applies the VSCODE_PYTHON_*_ACTIVATE environment variables to enable activation
366+
* by contributing the full activation command via environment variable
367+
* instead of sending activation commands directly to the terminal.
368+
* Sets ALL shell-specific variables at once so any shell can be activated.
369+
* Supports bash, zsh, fish, PowerShell, and Command Prompt.
370+
*/
371+
private async applyActivateEnvVar(
372+
resource: Resource,
373+
_shell: string,
374+
envVarCollection: ReturnType<typeof this.getEnvironmentVariableCollection>,
375+
): Promise<void> {
376+
const interpreter = await this.interpreterService.getActiveInterpreter(resource);
377+
if (!interpreter) {
378+
traceVerbose('No interpreter found for shell integration activation');
379+
envVarCollection.clear();
380+
return;
381+
}
382+
383+
// For virtual environments, get the bin directory
384+
if (interpreter.envType !== EnvironmentType.Venv && interpreter.type !== PythonEnvType.Virtual) {
385+
traceVerbose('Shell integration activation only supports virtual environments');
386+
envVarCollection.clear();
387+
return;
388+
}
389+
390+
const binDir = path.dirname(interpreter.path);
391+
392+
// Clear any previously set env vars
393+
envVarCollection.clear();
394+
395+
const options = {
396+
applyAtShellIntegration: true,
397+
applyAtProcessCreation: true,
398+
};
399+
400+
// Set ALL shell-specific environment variables at once
401+
// Bash
402+
const bashActivate = path.join(binDir, 'activate');
403+
if (await pathExists(bashActivate)) {
404+
const bashCommand = `source ${bashActivate}`;
405+
traceLog(`Setting VSCODE_PYTHON_BASH_ACTIVATE to ${bashCommand}`);
406+
envVarCollection.replace('VSCODE_PYTHON_BASH_ACTIVATE', bashCommand, options);
407+
408+
// ZSH uses the same activate script
409+
traceLog(`Setting VSCODE_PYTHON_ZSH_ACTIVATE to ${bashCommand}`);
410+
envVarCollection.replace('VSCODE_PYTHON_ZSH_ACTIVATE', bashCommand, options);
411+
}
412+
413+
// Fish
414+
const fishActivate = path.join(binDir, 'activate.fish');
415+
if (await pathExists(fishActivate)) {
416+
const fishCommand = `source ${fishActivate}`;
417+
traceLog(`Setting VSCODE_PYTHON_FISH_ACTIVATE to ${fishCommand}`);
418+
envVarCollection.replace('VSCODE_PYTHON_FISH_ACTIVATE', fishCommand, options);
419+
}
420+
421+
// PowerShell
422+
const pwshActivate = path.join(binDir, 'Activate.ps1');
423+
if (await pathExists(pwshActivate)) {
424+
const pwshCommand = `& ${pwshActivate}`;
425+
traceLog(`Setting VSCODE_PYTHON_PWSH_ACTIVATE to ${pwshCommand}`);
426+
envVarCollection.replace('VSCODE_PYTHON_PWSH_ACTIVATE', pwshCommand, options);
427+
}
428+
429+
// Command Prompt
430+
const cmdActivate = path.join(binDir, 'activate.bat');
431+
if (await pathExists(cmdActivate)) {
432+
traceLog(`Setting VSCODE_PYTHON_CMD_ACTIVATE to ${cmdActivate}`);
433+
envVarCollection.replace('VSCODE_PYTHON_CMD_ACTIVATE', cmdActivate, options);
434+
}
435+
436+
const workspaceFolder = this.getWorkspaceFolder(resource);
437+
const settings = this.configurationService.getSettings(resource);
438+
const displayPath = this.pathUtils.getDisplayName(settings.pythonPath, workspaceFolder?.uri.fsPath);
439+
const description = new MarkdownString(
440+
`${Interpreters.activateTerminalDescription} \`${displayPath}\` (via shell integration)`,
441+
);
442+
envVarCollection.description = description;
443+
}
444+
445+
/**
446+
* Builds the full activation command for the given shell type and script path.
447+
*/
448+
private buildActivateCommand(shellType: TerminalShellType, scriptPath: string): string {
449+
switch (shellType) {
450+
case TerminalShellType.bash:
451+
case TerminalShellType.gitbash:
452+
case TerminalShellType.wsl:
453+
case TerminalShellType.zsh:
454+
case TerminalShellType.fish:
455+
return `source ${scriptPath}`;
456+
case TerminalShellType.powershell:
457+
case TerminalShellType.powershellCore:
458+
return `& ${scriptPath}`;
459+
case TerminalShellType.commandPrompt:
460+
return scriptPath;
461+
default:
462+
return `source ${scriptPath}`;
463+
}
464+
}
465+
466+
/**
467+
* Returns the environment variable name for shell integration activation based on shell type.
468+
* Only supports bash, fish, PowerShell, and Command Prompt.
469+
*/
470+
private getShellActivateEnvVarName(shellType: TerminalShellType): string | undefined {
471+
switch (shellType) {
472+
case TerminalShellType.bash:
473+
case TerminalShellType.gitbash:
474+
case TerminalShellType.wsl:
475+
return 'VSCODE_PYTHON_BASH_ACTIVATE';
476+
case TerminalShellType.zsh:
477+
return 'VSCODE_PYTHON_ZSH_ACTIVATE';
478+
case TerminalShellType.fish:
479+
return 'VSCODE_PYTHON_FISH_ACTIVATE';
480+
case TerminalShellType.powershell:
481+
case TerminalShellType.powershellCore:
482+
return 'VSCODE_PYTHON_PS1_ACTIVATE';
483+
case TerminalShellType.commandPrompt:
484+
return 'VSCODE_PYTHON_CMD_ACTIVATE';
485+
default:
486+
return undefined;
487+
}
488+
}
489+
490+
/**
491+
* Gets the path to the activate script for the given interpreter.
492+
*/
493+
private async getActivateScriptPath(interpreter: PythonEnvironment, shell: string): Promise<string | undefined> {
494+
const shellType = identifyShellFromShellPath(shell);
495+
496+
// For virtual environments, look for activate script in bin directory
497+
if (interpreter.envType === EnvironmentType.Venv || interpreter.type === PythonEnvType.Virtual) {
498+
const binDir = path.dirname(interpreter.path);
499+
const activateScripts = this.getActivateScriptNames(shellType);
500+
501+
if (!activateScripts) {
502+
return undefined;
503+
}
504+
505+
for (const scriptName of activateScripts) {
506+
const scriptPath = path.join(binDir, scriptName);
507+
if (await pathExists(scriptPath)) {
508+
return scriptPath;
509+
}
510+
}
511+
}
512+
513+
// For conda environments, we would need a different approach
514+
// For now, return undefined for unsupported environment types
515+
return undefined;
516+
}
517+
518+
/**
519+
* Returns the activate script names to look for based on shell type.
520+
* Only supports bash, fish, PowerShell, and Command Prompt.
521+
*/
522+
private getActivateScriptNames(shellType: TerminalShellType): string[] | undefined {
523+
switch (shellType) {
524+
case TerminalShellType.bash:
525+
case TerminalShellType.gitbash:
526+
case TerminalShellType.zsh:
527+
case TerminalShellType.wsl:
528+
return ['activate', 'activate.sh'];
529+
case TerminalShellType.fish:
530+
return ['activate.fish'];
531+
case TerminalShellType.powershell:
532+
case TerminalShellType.powershellCore:
533+
return ['Activate.ps1'];
534+
case TerminalShellType.commandPrompt:
535+
return ['activate.bat'];
536+
default:
537+
return undefined;
538+
}
539+
}
540+
311541
private isPromptSet = new Map<number | undefined, boolean>();
312542

313543
// eslint-disable-next-line class-methods-use-this

0 commit comments

Comments
 (0)