Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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%",
Expand Down
1 change: 1 addition & 0 deletions package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
1 change: 1 addition & 0 deletions src/client/common/configSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,7 @@ export class PythonSettings implements IPythonSettings {
activateEnvInCurrentTerminal: false,
shellIntegration: {
enabled: false,
activate: false,
},
};

Expand Down
1 change: 1 addition & 0 deletions src/client/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ export interface ITerminalSettings {
readonly activateEnvInCurrentTerminal: boolean;
readonly shellIntegration: {
enabled: boolean;
activate: boolean;
};
}

Expand Down
134 changes: 134 additions & 0 deletions src/client/terminals/envCollectionActivation/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,15 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ

public async activate(resource: Resource): Promise<void> {
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();
Expand Down Expand Up @@ -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<void> {
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<void> {
const workspaceFolder = this.getWorkspaceFolder(resource);
const envVarCollection = this.getEnvironmentVariableCollection({ workspaceFolder });
await this.applyActivateEnvVar(resource, shell, envVarCollection);
}

public async _applyCollection(resource: Resource, shell?: string): Promise<void> {
this.progressService.showProgress({
location: ProgressLocation.Window,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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<typeof this.getEnvironmentVariableCollection>,
): Promise<void> {
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<number | undefined, boolean>();

// eslint-disable-next-line class-methods-use-this
Expand Down
Loading