Skip to content

Commit 30c5fb1

Browse files
committed
feat: add activation using shell startup
1 parent e08c49d commit 30c5fb1

11 files changed

Lines changed: 291 additions & 93 deletions

File tree

package.json

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,14 +90,14 @@
9090
},
9191
"python-envs.terminal.autoActivationType": {
9292
"type": "string",
93-
"description": "%python-envs.terminal.autoActivationType.description%",
93+
"markdownDescription": "%python-envs.terminal.autoActivationType.description%",
9494
"default": "command",
9595
"enum": [
9696
"command",
9797
"shellStartup",
9898
"off"
9999
],
100-
"enumDescriptions": [
100+
"markdownEnumDescriptions": [
101101
"%python-envs.terminal.autoActivationType.command%",
102102
"%python-envs.terminal.autoActivationType.shellStartup%",
103103
"%python-envs.terminal.autoActivationType.off%"
@@ -247,6 +247,12 @@
247247
"title": "%python-envs.copyProjectPath.title%",
248248
"category": "Python Envs",
249249
"icon": "$(copy)"
250+
},
251+
{
252+
"command": "python-envs.terminal.revertStartupScriptChanges",
253+
"title": "%python-envs.terminal.revertStartupScriptChanges.title%",
254+
"category": "Python Envs",
255+
"icon": "$(discard)"
250256
}
251257
],
252258
"menus": {

package.nls.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@
88
"python-envs.terminal.showActivateButton.description": "Whether to show the 'Activate' button in the terminal menu",
99
"python-envs.terminal.autoActivationType.description": "The type of activation to use when activating an environment in the terminal",
1010
"python-envs.terminal.autoActivationType.command": "Activation by executing a command in the terminal.",
11-
"python-envs.terminal.autoActivationType.shellStartup": "Activation by modifying the terminal shell startup script.",
11+
"python-envs.terminal.autoActivationType.shellStartup": "Activation by modifying the terminal shell startup script. To use this feature we will need to modify your shell startup scripts.",
1212
"python-envs.terminal.autoActivationType.off": "No automatic activation of environments.",
13+
"python-envs.terminal.revertStartupScriptChanges.title": "Revert Shell Startup Script Changes",
1314
"python-envs.setEnvManager.title": "Set Environment Manager",
1415
"python-envs.setPkgManager.title": "Set Package Manager",
1516
"python-envs.addPythonProject.title": "Add Python Project",

src/common/localize.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,3 +144,13 @@ export namespace EnvViewStrings {
144144
export const selectedGlobalTooltip = l10n.t('This environment is selected for non-workspace files');
145145
export const selectedWorkspaceTooltip = l10n.t('This environment is selected for workspace files');
146146
}
147+
148+
export namespace ShellStartupActivationStrings {
149+
export const shellStartupScriptEditPrompt = l10n.t(
150+
'To support `shellStartup` we need to modify your shell profile. Do you want to proceed?',
151+
);
152+
export const updateScript = l10n.t('Update Shell Profile');
153+
export const revertToCommandActivation = l10n.t(
154+
'Auto Shell Activation type set to "command", due to removing shell startup from profile.',
155+
);
156+
}

src/common/window.apis.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import {
77
InputBox,
88
InputBoxOptions,
99
LogOutputChannel,
10+
MessageItem,
11+
MessageOptions,
1012
OpenDialogOptions,
1113
OutputChannel,
1214
Progress,
@@ -284,6 +286,21 @@ export async function showInputBoxWithButtons(
284286
}
285287
}
286288

289+
export function showInformationMessage<T extends string>(message: string, ...items: T[]): Thenable<T | undefined>;
290+
export function showInformationMessage<T extends MessageItem>(message: string, ...items: T[]): Thenable<T | undefined>;
291+
export function showInformationMessage<T extends string>(
292+
message: string,
293+
options: MessageOptions,
294+
...items: T[]
295+
): Thenable<T | undefined>;
296+
export function showInformationMessage<T extends MessageItem>(
297+
message: string,
298+
options: MessageOptions,
299+
...items: T[]
300+
): Thenable<T | undefined> {
301+
return window.showInformationMessage(message, options, ...items);
302+
}
303+
287304
export function showWarningMessage(message: string, ...items: string[]): Thenable<string | undefined> {
288305
return window.showWarningMessage(message, ...items);
289306
}

src/extension.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ import { registerTools } from './common/lm.apis';
5757
import { GetPackagesTool } from './features/copilotTools';
5858
import { TerminalActivationImpl } from './features/terminal/terminalActivationState';
5959
import { getEnvironmentForTerminal } from './features/terminal/utils';
60+
import { PowershellStartupProvider } from './features/terminal/startup/powershellStartup';
61+
import { ShellStartupActivationManagerImpl } from './features/terminal/startup/activateUsingShellStartup';
6062

6163
export async function activate(context: ExtensionContext): Promise<PythonEnvironmentApi> {
6264
const start = new StopWatch();
@@ -83,8 +85,14 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
8385
context.subscriptions.push(envManagers);
8486

8587
const terminalActivation = new TerminalActivationImpl();
86-
const terminalManager: TerminalManager = new TerminalManagerImpl(terminalActivation);
87-
context.subscriptions.push(terminalActivation, terminalManager);
88+
const shellStartupProviders = [new PowershellStartupProvider()];
89+
const shellStartupActivationManager = new ShellStartupActivationManagerImpl(
90+
context.environmentVariableCollection,
91+
shellStartupProviders,
92+
envManagers,
93+
);
94+
const terminalManager: TerminalManager = new TerminalManagerImpl(terminalActivation, shellStartupProviders);
95+
context.subscriptions.push(terminalActivation, terminalManager, shellStartupActivationManager);
8896

8997
const projectCreators: ProjectCreators = new ProjectCreatorsImpl();
9098
context.subscriptions.push(
@@ -94,21 +102,23 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
94102
);
95103

96104
setPythonApi(envManagers, projectManager, projectCreators, terminalManager, envVarManager);
105+
const api = await getPythonApi();
97106

98107
const managerView = new EnvManagerView(envManagers);
99108
context.subscriptions.push(managerView);
100109

101110
const workspaceView = new ProjectView(envManagers, projectManager);
102111
context.subscriptions.push(workspaceView);
103-
104112
workspaceView.initialize();
105-
const api = await getPythonApi();
106113

107114
const monitoredTerminals = new Map<Terminal, PythonEnvironment>();
108115

109116
context.subscriptions.push(
110117
registerCompletionProvider(envManagers),
111118
registerTools('python_get_packages', new GetPackagesTool(api)),
119+
commands.registerCommand('python-envs.terminal.revertStartupScriptChanges', async () => {
120+
await shellStartupActivationManager.cleanupStartupScripts();
121+
}),
112122
commands.registerCommand('python-envs.viewLogs', () => outputChannel.show()),
113123
commands.registerCommand('python-envs.refreshManager', async (item) => {
114124
await refreshManagerCommand(item);
@@ -262,6 +272,7 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
262272
await Promise.all([
263273
registerSystemPythonFeatures(nativeFinder, context.subscriptions, outputChannel),
264274
registerCondaFeatures(nativeFinder, context.subscriptions, outputChannel),
275+
shellStartupActivationManager.initialize(),
265276
]);
266277
sendTelemetryEvent(EventNames.EXTENSION_MANAGER_REGISTRATION_DURATION, start.elapsedTime);
267278
await terminalManager.initialize(api);

src/features/common/activation.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,13 @@ export function getActivationCommand(
1515
environment: PythonEnvironment,
1616
): PythonCommandRunConfiguration[] | undefined {
1717
const shell = identifyTerminalShell(terminal);
18+
return getActivationCommandForShell(environment, shell);
19+
}
1820

21+
export function getActivationCommandForShell(
22+
environment: PythonEnvironment,
23+
shell: TerminalShellType,
24+
): PythonCommandRunConfiguration[] | undefined {
1925
let activation: PythonCommandRunConfiguration[] | undefined;
2026
if (environment.execInfo?.shellActivation) {
2127
activation = environment.execInfo.shellActivation.get(shell);
Lines changed: 126 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,85 +1,158 @@
1-
import { Disposable, GlobalEnvironmentVariableCollection } from 'vscode';
2-
import { onDidChangeConfiguration } from '../../../common/workspace.apis';
3-
import { registerCommand } from '../../../common/command.api';
4-
import { getAutoActivationType } from '../utils';
1+
import { ConfigurationChangeEvent, Disposable, GlobalEnvironmentVariableCollection } from 'vscode';
2+
import { getWorkspaceFolder, getWorkspaceFolders, onDidChangeConfiguration } from '../../../common/workspace.apis';
3+
import { getAutoActivationType, setAutoActivationType } from '../utils';
4+
import { ShellStartupProvider } from './startupProvider';
5+
import { DidChangeEnvironmentEventArgs } from '../../../api';
56
import { EnvironmentManagers } from '../../../internal.api';
7+
import { traceError, traceInfo } from '../../../common/logging';
8+
import { ShellStartupActivationStrings } from '../../../common/localize';
9+
import { showInformationMessage } from '../../../common/window.apis';
610

7-
export interface ActivateUsingShellStartup extends Disposable {}
11+
export interface ShellStartupActivationManager extends Disposable {
12+
initialize(): Promise<void>;
13+
updateStartupScripts(): Promise<void>;
14+
cleanupStartupScripts(): Promise<void>;
15+
}
816

9-
class ActivateUsingShellStartupImpl implements ActivateUsingShellStartup {
17+
export class ShellStartupActivationManagerImpl implements ShellStartupActivationManager {
1018
private readonly disposables: Disposable[] = [];
1119
constructor(
1220
private readonly envCollection: GlobalEnvironmentVariableCollection,
21+
private readonly shellStartupProviders: ShellStartupProvider[],
1322
private readonly em: EnvironmentManagers,
1423
) {
1524
this.disposables.push(
16-
onDidChangeConfiguration((e) => {
25+
onDidChangeConfiguration((e: ConfigurationChangeEvent) => {
1726
this.handleConfigurationChange(e);
1827
}),
28+
this.em.onDidChangeEnvironmentFiltered((e: DidChangeEnvironmentEventArgs) => {
29+
this.handleEnvironmentChange(e);
30+
}),
1931
);
2032
}
2133

22-
private handleConfigurationChange(e) {
23-
if (e.affectsConfiguration('python.terminal.autoActivationType')) {
34+
private async handleConfigurationChange(e: ConfigurationChangeEvent) {
35+
if (e.affectsConfiguration('python-envs.terminal.autoActivationType')) {
2436
const autoActType = getAutoActivationType();
2537
if (autoActType === 'shellStartup') {
26-
s;
38+
await this.initialize();
2739
} else {
40+
// remove any contributed environment variables
41+
const workspaces = getWorkspaceFolders() ?? [];
42+
if (workspaces.length > 0) {
43+
// User has one or more workspaces open
44+
const promises: Promise<void>[] = [];
45+
workspaces.forEach((workspace) => {
46+
const collection = this.envCollection.getScoped({ workspaceFolder: workspace });
47+
promises.push(
48+
...this.shellStartupProviders.map((provider) => provider.removeEnvVariables(collection)),
49+
);
50+
});
51+
await Promise.all(promises);
52+
} else {
53+
// User has no workspaces open
54+
await Promise.all(
55+
this.shellStartupProviders.map((provider) => provider.removeEnvVariables(this.envCollection)),
56+
);
57+
}
2858
}
2959
}
3060
}
3161

32-
private async addActivationVariables(): Promise<void> {}
33-
34-
private async removeActivationVariables(): Promise<void> {}
62+
private async handleEnvironmentChange(e: DidChangeEnvironmentEventArgs) {
63+
const autoActType = getAutoActivationType();
64+
if (autoActType !== 'shellStartup') {
65+
return;
66+
}
3567

36-
dispose() {
37-
this.disposables.forEach((disposable) => disposable.dispose());
68+
if (e.uri) {
69+
const wf = getWorkspaceFolder(e.uri);
70+
if (wf) {
71+
const envVars = this.envCollection.getScoped({ workspaceFolder: wf });
72+
if (envVars) {
73+
await Promise.all(
74+
this.shellStartupProviders.map(async (provider) => {
75+
if (e.new) {
76+
await provider.updateEnvVariables(envVars, e.new);
77+
} else {
78+
await provider.removeEnvVariables(envVars);
79+
}
80+
}),
81+
);
82+
}
83+
}
84+
}
3885
}
39-
}
40-
41-
export async function checkAndUpdateStartupScripts(): Promise<void> {
42-
// Implement the logic to check startup scripts
43-
return Promise.resolve();
44-
}
4586

46-
export async function removeAllStartupScripts(): Promise<void> {
47-
// Implement the logic to remove all startup scripts
48-
return Promise.resolve();
49-
}
87+
private async isSetupRequired(): Promise<boolean> {
88+
const results = await Promise.all(this.shellStartupProviders.map((provider) => provider.isSetup()));
89+
return results.some((result) => !result);
90+
}
5091

51-
export function registerActivateUsingShellStartup(
52-
disposables: Disposable[],
53-
environmentVariableCollection: GlobalEnvironmentVariableCollection,
54-
em: EnvironmentManagers,
55-
) {
56-
let activateUsingShellStartup: ActivateUsingShellStartup | undefined;
92+
public async initialize(): Promise<void> {
93+
const autoActType = getAutoActivationType();
94+
if (autoActType === 'shellStartup') {
95+
if (await this.isSetupRequired()) {
96+
const result = await showInformationMessage(
97+
ShellStartupActivationStrings.shellStartupScriptEditPrompt,
98+
{ modal: true },
99+
ShellStartupActivationStrings.updateScript,
100+
);
57101

58-
disposables.push(
59-
onDidChangeConfiguration((e) => {
60-
if (e.affectsConfiguration('python.terminal.autoActivationType')) {
61-
const autoActType = getAutoActivationType();
62-
if (autoActType === 'shellStartup') {
63-
if (!activateUsingShellStartup) {
64-
activateUsingShellStartup = new ActivateUsingShellStartupImpl(environmentVariableCollection);
65-
}
102+
if (ShellStartupActivationStrings.updateScript === result) {
103+
await this.updateStartupScripts();
66104
} else {
67-
activateUsingShellStartup?.dispose();
68-
activateUsingShellStartup = undefined;
105+
traceError('User declined to edit shell startup scripts. See <doc-link> for more information.');
106+
traceInfo('Setting `python-envs.terminal.autoActivationType` to `command`.');
107+
setAutoActivationType('command');
108+
return;
69109
}
70110
}
71-
}),
72-
new Disposable(() => activateUsingShellStartup?.dispose()),
73-
registerCommand('python-envs.removeStartupScripts', async () => {
74-
await removeAllStartupScripts();
75-
}),
76-
);
77111

78-
const autoActType = getAutoActivationType();
79-
if (autoActType === 'shellStartup') {
80-
activateUsingShellStartup = new ActivateUsingShellStartupImpl(environmentVariableCollection);
81-
setImmediate(async () => {
82-
await checkAndUpdateStartupScripts();
83-
});
112+
const workspaces = getWorkspaceFolders() ?? [];
113+
114+
if (workspaces.length > 0) {
115+
const promises: Promise<void>[] = [];
116+
workspaces.forEach((workspace) => {
117+
const collection = this.envCollection.getScoped({ workspaceFolder: workspace });
118+
promises.push(
119+
...this.shellStartupProviders.map(async (provider) => {
120+
const env = await this.em.getEnvironment(workspace.uri);
121+
if (env) {
122+
await provider.updateEnvVariables(collection, env);
123+
} else {
124+
await provider.removeEnvVariables(collection);
125+
}
126+
}),
127+
);
128+
});
129+
await Promise.all(promises);
130+
} else {
131+
await Promise.all(
132+
this.shellStartupProviders.map(async (provider) => {
133+
const env = await this.em.getEnvironment(undefined);
134+
if (env) {
135+
await provider.updateEnvVariables(this.envCollection, env);
136+
} else {
137+
await provider.removeEnvVariables(this.envCollection);
138+
}
139+
}),
140+
);
141+
}
142+
}
143+
}
144+
145+
public async updateStartupScripts(): Promise<void> {
146+
await Promise.all(this.shellStartupProviders.map((provider) => provider.setupScripts()));
147+
}
148+
149+
public async cleanupStartupScripts(): Promise<void> {
150+
await Promise.all(this.shellStartupProviders.map((provider) => provider.teardownScripts()));
151+
setAutoActivationType('command');
152+
showInformationMessage(ShellStartupActivationStrings.revertToCommandActivation);
153+
}
154+
155+
dispose() {
156+
this.disposables.forEach((disposable) => disposable.dispose());
84157
}
85158
}

0 commit comments

Comments
 (0)