Skip to content

Commit 9c740b9

Browse files
author
Kartik Raj
authored
Show notification reaffirming Python extension still handles activation when in pythonTerminalEnvVarActivation experiment (#21802)
Closes #21793 Only show notification when terminal prompt does not already indicate that env is activated.
1 parent b447bf1 commit 9c740b9

File tree

15 files changed

+545
-22
lines changed

15 files changed

+545
-22
lines changed

src/client/activation/extensionSurvey.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,10 +83,10 @@ export class ExtensionSurveyPrompt implements IExtensionSingleActivationService
8383
@traceDecoratorError('Failed to display prompt for extension survey')
8484
public async showSurvey() {
8585
const prompts = [ExtensionSurveyBanner.bannerLabelYes, ExtensionSurveyBanner.maybeLater, Common.doNotShowAgain];
86-
const telemetrySelections: ['Yes', 'Maybe later', 'Do not show again'] = [
86+
const telemetrySelections: ['Yes', 'Maybe later', "Don't show again"] = [
8787
'Yes',
8888
'Maybe later',
89-
'Do not show again',
89+
"Don't show again",
9090
];
9191
const selection = await this.appShell.showInformationMessage(ExtensionSurveyBanner.bannerMessage, ...prompts);
9292
sendTelemetryEvent(EventName.EXTENSION_SURVEY_PROMPT, undefined, {

src/client/common/experiments/helpers.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,16 @@
33

44
'use strict';
55

6+
import { env, workspace } from 'vscode';
67
import { IExperimentService } from '../types';
78
import { TerminalEnvVarActivation } from './groups';
9+
import { isTestExecution } from '../constants';
810

911
export function inTerminalEnvVarExperiment(experimentService: IExperimentService): boolean {
12+
if (!isTestExecution() && workspace.workspaceFile && env.remoteName) {
13+
// TODO: Remove this if statement once https://github.com/microsoft/vscode/issues/180486 is fixed.
14+
return false;
15+
}
1016
if (!experimentService.inExperimentSync(TerminalEnvVarActivation.experiment)) {
1117
return false;
1218
}

src/client/common/utils/localize.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export namespace Common {
6363
export const openOutputPanel = l10n.t('Show output');
6464
export const noIWillDoItLater = l10n.t('No, I will do it later');
6565
export const notNow = l10n.t('Not now');
66-
export const doNotShowAgain = l10n.t('Do not show again');
66+
export const doNotShowAgain = l10n.t("Don't show again");
6767
export const reload = l10n.t('Reload');
6868
export const moreInfo = l10n.t('More Info');
6969
export const learnMore = l10n.t('Learn more');
@@ -198,6 +198,9 @@ export namespace Interpreters {
198198
);
199199
export const activatingTerminals = l10n.t('Reactivating terminals...');
200200
export const activateTerminalDescription = l10n.t('Activated environment for');
201+
export const terminalEnvVarCollectionPrompt = l10n.t(
202+
'The Python extension automatically activates all terminals using the selected environment. You can hover over the terminal tab to see more information about the activation. [Learn more](https://aka.ms/vscodePythonTerminalActivation).',
203+
);
201204
export const activatedCondaEnvLaunch = l10n.t(
202205
'We noticed VS Code was launched from an activated conda environment, would you like to select it?',
203206
);
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import { inject, injectable } from 'inversify';
5+
import { Uri } from 'vscode';
6+
import { IActiveResourceService, IApplicationShell, ITerminalManager } from '../../common/application/types';
7+
import {
8+
IConfigurationService,
9+
IDisposableRegistry,
10+
IExperimentService,
11+
IPersistentStateFactory,
12+
} from '../../common/types';
13+
import { Common, Interpreters } from '../../common/utils/localize';
14+
import { IExtensionSingleActivationService } from '../../activation/types';
15+
import { ITerminalEnvVarCollectionService } from './types';
16+
import { inTerminalEnvVarExperiment } from '../../common/experiments/helpers';
17+
18+
export const terminalEnvCollectionPromptKey = 'TERMINAL_ENV_COLLECTION_PROMPT_KEY';
19+
20+
@injectable()
21+
export class TerminalEnvVarCollectionPrompt implements IExtensionSingleActivationService {
22+
public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false };
23+
24+
constructor(
25+
@inject(IApplicationShell) private readonly appShell: IApplicationShell,
26+
@inject(IPersistentStateFactory) private readonly persistentStateFactory: IPersistentStateFactory,
27+
@inject(ITerminalManager) private readonly terminalManager: ITerminalManager,
28+
@inject(IDisposableRegistry) private readonly disposableRegistry: IDisposableRegistry,
29+
@inject(IActiveResourceService) private readonly activeResourceService: IActiveResourceService,
30+
@inject(ITerminalEnvVarCollectionService)
31+
private readonly terminalEnvVarCollectionService: ITerminalEnvVarCollectionService,
32+
@inject(IConfigurationService) private readonly configurationService: IConfigurationService,
33+
@inject(IExperimentService) private readonly experimentService: IExperimentService,
34+
) {}
35+
36+
public async activate(): Promise<void> {
37+
if (!inTerminalEnvVarExperiment(this.experimentService)) {
38+
return;
39+
}
40+
this.disposableRegistry.push(
41+
this.terminalManager.onDidOpenTerminal(async (terminal) => {
42+
const cwd =
43+
'cwd' in terminal.creationOptions && terminal.creationOptions.cwd
44+
? terminal.creationOptions.cwd
45+
: this.activeResourceService.getActiveResource();
46+
const resource = typeof cwd === 'string' ? Uri.file(cwd) : cwd;
47+
const settings = this.configurationService.getSettings(resource);
48+
if (!settings.terminal.activateEnvironment) {
49+
return;
50+
}
51+
if (this.terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource)) {
52+
// No need to show notification if terminal prompt already indicates when env is activated.
53+
return;
54+
}
55+
await this.notifyUsers();
56+
}),
57+
);
58+
}
59+
60+
private async notifyUsers(): Promise<void> {
61+
const notificationPromptEnabled = this.persistentStateFactory.createGlobalPersistentState(
62+
terminalEnvCollectionPromptKey,
63+
true,
64+
);
65+
if (!notificationPromptEnabled.value) {
66+
return;
67+
}
68+
const prompts = [Common.doNotShowAgain];
69+
const selection = await this.appShell.showInformationMessage(
70+
Interpreters.terminalEnvVarCollectionPrompt,
71+
...prompts,
72+
);
73+
if (!selection) {
74+
return;
75+
}
76+
if (selection === prompts[0]) {
77+
await notificationPromptEnabled.updateValue(false);
78+
}
79+
}
80+
}

src/client/interpreter/activation/terminalEnvVarCollectionService.ts

Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,15 @@ import { Interpreters } from '../../common/utils/localize';
2323
import { traceDecoratorVerbose, traceVerbose } from '../../logging';
2424
import { IInterpreterService } from '../contracts';
2525
import { defaultShells } from './service';
26-
import { IEnvironmentActivationService } from './types';
26+
import { IEnvironmentActivationService, ITerminalEnvVarCollectionService } from './types';
2727
import { EnvironmentType } from '../../pythonEnvironments/info';
2828
import { getSearchPathEnvVarNames } from '../../common/utils/exec';
2929
import { EnvironmentVariables } from '../../common/variables/types';
30+
import { TerminalShellType } from '../../common/terminal/types';
31+
import { OSType } from '../../common/utils/platform';
3032

3133
@injectable()
32-
export class TerminalEnvVarCollectionService implements IExtensionActivationService {
34+
export class TerminalEnvVarCollectionService implements IExtensionActivationService, ITerminalEnvVarCollectionService {
3335
public readonly supportedWorkspaceTypes = {
3436
untrustedWorkspace: false,
3537
virtualWorkspace: false,
@@ -127,6 +129,7 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ
127129
await this._applyCollection(resource, defaultShell?.shell);
128130
return;
129131
}
132+
await this.trackTerminalPrompt(shell, resource, env);
130133
this.processEnvVars = undefined;
131134
return;
132135
}
@@ -146,6 +149,7 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ
146149
if (prevValue !== value) {
147150
if (value !== undefined) {
148151
if (key === 'PS1') {
152+
// We cannot have the full PS1 without executing in terminal, which we do not. Hence prepend it.
149153
traceVerbose(`Prepending environment variable ${key} in collection with ${value}`);
150154
envVarCollection.prepend(key, value, {
151155
applyAtShellIntegration: true,
@@ -165,6 +169,61 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ
165169
const displayPath = this.pathUtils.getDisplayName(settings.pythonPath, workspaceFolder?.uri.fsPath);
166170
const description = new MarkdownString(`${Interpreters.activateTerminalDescription} \`${displayPath}\``);
167171
envVarCollection.description = description;
172+
173+
await this.trackTerminalPrompt(shell, resource, env);
174+
}
175+
176+
private isPromptSet = new Map<number | undefined, boolean>();
177+
178+
// eslint-disable-next-line class-methods-use-this
179+
public isTerminalPromptSetCorrectly(resource?: Resource): boolean {
180+
const workspaceFolder = this.getWorkspaceFolder(resource);
181+
return !!this.isPromptSet.get(workspaceFolder?.index);
182+
}
183+
184+
/**
185+
* Call this once we know terminal prompt is set correctly for terminal owned by this resource.
186+
*/
187+
private terminalPromptIsCorrect(resource: Resource) {
188+
const key = this.getWorkspaceFolder(resource)?.index;
189+
this.isPromptSet.set(key, true);
190+
}
191+
192+
private terminalPromptIsUnknown(resource: Resource) {
193+
const key = this.getWorkspaceFolder(resource)?.index;
194+
this.isPromptSet.delete(key);
195+
}
196+
197+
/**
198+
* Tracks whether prompt for terminal was correctly set.
199+
*/
200+
private async trackTerminalPrompt(shell: string, resource: Resource, env: EnvironmentVariables | undefined) {
201+
this.terminalPromptIsUnknown(resource);
202+
if (!env) {
203+
this.terminalPromptIsCorrect(resource);
204+
return;
205+
}
206+
// Prompts for these shells cannot be set reliably using variables
207+
const exceptionShells = [
208+
TerminalShellType.powershell,
209+
TerminalShellType.powershellCore,
210+
TerminalShellType.fish,
211+
TerminalShellType.zsh, // TODO: Remove this once https://github.com/microsoft/vscode/issues/188875 is fixed
212+
];
213+
const customShellType = identifyShellFromShellPath(shell);
214+
if (exceptionShells.includes(customShellType)) {
215+
return;
216+
}
217+
if (this.platform.osType !== OSType.Windows) {
218+
// These shells are expected to set PS1 variable for terminal prompt for virtual/conda environments.
219+
const interpreter = await this.interpreterService.getActiveInterpreter(resource);
220+
const shouldPS1BeSet = interpreter?.type !== undefined;
221+
if (shouldPS1BeSet && !env.PS1) {
222+
// PS1 should be set but no PS1 was set.
223+
return;
224+
}
225+
}
226+
this.terminalPromptIsCorrect(resource);
168227
}
169228

170229
private async handleMicroVenv(resource: Resource) {
@@ -178,7 +237,7 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ
178237
envVarCollection.replace(
179238
'PATH',
180239
`${path.dirname(interpreter.path)}${path.delimiter}${process.env[pathVarName]}`,
181-
{ applyAtShellIntegration: true },
240+
{ applyAtShellIntegration: true, applyAtProcessCreation: true },
182241
);
183242
return;
184243
}

src/client/interpreter/activation/types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,11 @@ export interface IEnvironmentActivationService {
2121
interpreter?: PythonEnvironment,
2222
): Promise<string[] | undefined>;
2323
}
24+
25+
export const ITerminalEnvVarCollectionService = Symbol('ITerminalEnvVarCollectionService');
26+
export interface ITerminalEnvVarCollectionService {
27+
/**
28+
* Returns true if we know with high certainity the terminal prompt is set correctly for a particular resource.
29+
*/
30+
isTerminalPromptSetCorrectly(resource?: Resource): boolean;
31+
}

src/client/interpreter/serviceRegistry.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66
import { IExtensionActivationService, IExtensionSingleActivationService } from '../activation/types';
77
import { IServiceManager } from '../ioc/types';
88
import { EnvironmentActivationService } from './activation/service';
9+
import { TerminalEnvVarCollectionPrompt } from './activation/terminalEnvVarCollectionPrompt';
910
import { TerminalEnvVarCollectionService } from './activation/terminalEnvVarCollectionService';
10-
import { IEnvironmentActivationService } from './activation/types';
11+
import { IEnvironmentActivationService, ITerminalEnvVarCollectionService } from './activation/types';
1112
import { InterpreterAutoSelectionService } from './autoSelection/index';
1213
import { InterpreterAutoSelectionProxyService } from './autoSelection/proxy';
1314
import { IInterpreterAutoSelectionService, IInterpreterAutoSelectionProxyService } from './autoSelection/types';
@@ -109,8 +110,13 @@ export function registerTypes(serviceManager: IServiceManager): void {
109110
IEnvironmentActivationService,
110111
EnvironmentActivationService,
111112
);
112-
serviceManager.addSingleton<IExtensionActivationService>(
113-
IExtensionActivationService,
113+
serviceManager.addSingleton<ITerminalEnvVarCollectionService>(
114+
ITerminalEnvVarCollectionService,
114115
TerminalEnvVarCollectionService,
115116
);
117+
serviceManager.addBinding(ITerminalEnvVarCollectionService, IExtensionActivationService);
118+
serviceManager.addSingleton<IExtensionSingleActivationService>(
119+
IExtensionSingleActivationService,
120+
TerminalEnvVarCollectionPrompt,
121+
);
116122
}

src/client/pythonEnvironments/info/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
'use strict';
55

66
import { Architecture } from '../../common/utils/platform';
7+
import { PythonEnvType } from '../base/info';
78
import { PythonVersion } from './pythonVersion';
89

910
/**
@@ -85,7 +86,7 @@ export type PythonEnvironment = InterpreterInformation & {
8586
envName?: string;
8687
envPath?: string;
8788
cachedEntry?: boolean;
88-
type?: string;
89+
type?: PythonEnvType;
8990
};
9091

9192
/**

src/client/telemetry/index.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -950,7 +950,7 @@ export interface IEventNamePropertyMapping {
950950
tool?: LinterId;
951951
/**
952952
* `select` When 'Select linter' option is selected
953-
* `disablePrompt` When 'Do not show again' option is selected
953+
* `disablePrompt` When "Don't show again" option is selected
954954
* `install` When 'Install' option is selected
955955
*
956956
* @type {('select' | 'disablePrompt' | 'install')}
@@ -1374,7 +1374,7 @@ export interface IEventNamePropertyMapping {
13741374
/**
13751375
* `Yes` When 'Yes' option is selected
13761376
* `No` When 'No' option is selected
1377-
* `Ignore` When 'Do not show again' option is clicked
1377+
* `Ignore` When "Don't show again" option is clicked
13781378
*
13791379
* @type {('Yes' | 'No' | 'Ignore' | undefined)}
13801380
*/
@@ -1571,7 +1571,7 @@ export interface IEventNamePropertyMapping {
15711571
/**
15721572
* Carries the selection of user when they are asked to take the extension survey
15731573
*/
1574-
selection: 'Yes' | 'Maybe later' | 'Do not show again' | undefined;
1574+
selection: 'Yes' | 'Maybe later' | "Don't show again" | undefined;
15751575
};
15761576
/**
15771577
* Telemetry event sent when starting REPL

src/test/activation/extensionSurvey.unit.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -355,7 +355,7 @@ suite('Extension survey prompt - showSurvey()', () => {
355355
platformService.verifyAll();
356356
});
357357

358-
test("Disable prompt if 'Do not show again' option is clicked", async () => {
358+
test('Disable prompt if "Don\'t show again" option is clicked', async () => {
359359
const prompts = [ExtensionSurveyBanner.bannerLabelYes, ExtensionSurveyBanner.maybeLater, Common.doNotShowAgain];
360360
platformService.setup((p) => p.osType).verifiable(TypeMoq.Times.never());
361361
appShell

src/test/application/diagnostics/checks/macPythonInterpreter.unit.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ suite('Application Diagnostics - Checks Mac Python Interpreter', () => {
220220
expect(messagePrompt).not.be.equal(undefined, 'Message prompt not set');
221221
expect(messagePrompt!.commandPrompts).to.be.deep.equal([
222222
{ prompt: 'Select Python Interpreter', command: cmd },
223-
{ prompt: 'Do not show again', command: cmdIgnore },
223+
{ prompt: "Don't show again", command: cmdIgnore },
224224
]);
225225
});
226226
test('Should not display a message if No Interpreters diagnostic has been ignored', async () => {

src/test/common/installer/installer.unit.test.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -307,10 +307,10 @@ suite('Module Installer only', () => {
307307
TypeMoq.It.isAnyString(),
308308
TypeMoq.It.isValue('Install'),
309309
TypeMoq.It.isValue('Select Linter'),
310-
TypeMoq.It.isValue('Do not show again'),
310+
TypeMoq.It.isValue("Don't show again"),
311311
),
312312
)
313-
.returns(async () => 'Do not show again')
313+
.returns(async () => "Don't show again")
314314
.verifiable(TypeMoq.Times.once());
315315
const persistVal = TypeMoq.Mock.ofType<IPersistentState<boolean>>();
316316
let mockPersistVal = false;
@@ -367,7 +367,7 @@ suite('Module Installer only', () => {
367367
TypeMoq.It.isAnyString(),
368368
TypeMoq.It.isValue('Install'),
369369
TypeMoq.It.isValue('Select Linter'),
370-
TypeMoq.It.isValue('Do not show again'),
370+
TypeMoq.It.isValue("Don't show again"),
371371
),
372372
)
373373
.returns(async () => undefined)
@@ -864,7 +864,7 @@ suite('Module Installer only', () => {
864864

865865
test('Ensure 3 options for pylint', async () => {
866866
const product = Product.pylint;
867-
const options = ['Select Linter', 'Do not show again'];
867+
const options = ['Select Linter', "Don't show again"];
868868
const productName = ProductNames.get(product)!;
869869

870870
await installer.promptToInstallImplementation(product, resource);
@@ -875,7 +875,7 @@ suite('Module Installer only', () => {
875875
});
876876
test('Ensure select linter command is invoked', async () => {
877877
const product = Product.pylint;
878-
const options = ['Select Linter', 'Do not show again'];
878+
const options = ['Select Linter', "Don't show again"];
879879
const productName = ProductNames.get(product)!;
880880
when(
881881
appShell.showErrorMessage(`Linter ${productName} is not installed.`, 'Install', options[0], options[1]),
@@ -892,7 +892,7 @@ suite('Module Installer only', () => {
892892
});
893893
test('If install button is selected, install linter and return response', async () => {
894894
const product = Product.pylint;
895-
const options = ['Select Linter', 'Do not show again'];
895+
const options = ['Select Linter', "Don't show again"];
896896
const productName = ProductNames.get(product)!;
897897
when(
898898
appShell.showErrorMessage(`Linter ${productName} is not installed.`, 'Install', options[0], options[1]),

0 commit comments

Comments
 (0)