Skip to content

Commit 66cea21

Browse files
author
Kartik Raj
authored
Show notification when deactivate command is run in terminal (#22133)
Closes #22121
1 parent ab6ab06 commit 66cea21

18 files changed

+490
-45
lines changed

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@
2222
"quickPickSortByLabel",
2323
"testObserver",
2424
"quickPickItemTooltip",
25-
"saveEditor"
25+
"saveEditor",
26+
"terminalDataWriteEvent"
2627
],
2728
"author": {
2829
"name": "Microsoft Corporation"

src/client/common/application/applicationShell.ts

+11-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
DocumentSelector,
1111
env,
1212
Event,
13+
EventEmitter,
1314
InputBox,
1415
InputBoxOptions,
1516
languages,
@@ -37,7 +38,8 @@ import {
3738
WorkspaceFolder,
3839
WorkspaceFolderPickOptions,
3940
} from 'vscode';
40-
import { IApplicationShell } from './types';
41+
import { traceError } from '../../logging';
42+
import { IApplicationShell, TerminalDataWriteEvent } from './types';
4143

4244
@injectable()
4345
export class ApplicationShell implements IApplicationShell {
@@ -172,4 +174,12 @@ export class ApplicationShell implements IApplicationShell {
172174
public createLanguageStatusItem(id: string, selector: DocumentSelector): LanguageStatusItem {
173175
return languages.createLanguageStatusItem(id, selector);
174176
}
177+
public get onDidWriteTerminalData(): Event<TerminalDataWriteEvent> {
178+
try {
179+
return window.onDidWriteTerminalData;
180+
} catch (ex) {
181+
traceError('Failed to get proposed API onDidWriteTerminalData', ex);
182+
return new EventEmitter<TerminalDataWriteEvent>().event;
183+
}
184+
}
175185
}

src/client/common/application/types.ts

+18
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,17 @@ import { Resource } from '../types';
6767
import { ICommandNameArgumentTypeMapping } from './commands';
6868
import { ExtensionContextKey } from './contextKeys';
6969

70+
export interface TerminalDataWriteEvent {
71+
/**
72+
* The {@link Terminal} for which the data was written.
73+
*/
74+
readonly terminal: Terminal;
75+
/**
76+
* The data being written.
77+
*/
78+
readonly data: string;
79+
}
80+
7081
export const IApplicationShell = Symbol('IApplicationShell');
7182
export interface IApplicationShell {
7283
/**
@@ -75,6 +86,13 @@ export interface IApplicationShell {
7586
*/
7687
readonly onDidChangeWindowState: Event<WindowState>;
7788

89+
/**
90+
* An event which fires when the terminal's child pseudo-device is written to (the shell).
91+
* In other words, this provides access to the raw data stream from the process running
92+
* within the terminal, including VT sequences.
93+
*/
94+
readonly onDidWriteTerminalData: Event<TerminalDataWriteEvent>;
95+
7896
showInformationMessage(message: string, ...items: string[]): Thenable<string | undefined>;
7997

8098
/**

src/client/common/utils/localize.ts

+4
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,10 @@ export namespace Interpreters {
201201
export const terminalEnvVarCollectionPrompt = l10n.t(
202202
'The Python extension automatically activates all terminals using the selected environment, even when the name of the environment{0} is not present in the terminal prompt. [Learn more](https://aka.ms/vscodePythonTerminalActivation).',
203203
);
204+
export const terminalDeactivatePrompt = l10n.t(
205+
'Deactivating virtual environments may not work by default due to a technical limitation in our activation approach, but it can be resolved with a few simple steps.',
206+
);
207+
export const deactivateDoneButton = l10n.t('Done, it works');
204208
export const activatedCondaEnvLaunch = l10n.t(
205209
'We noticed VS Code was launched from an activated conda environment, would you like to select it?',
206210
);

src/client/interpreter/activation/types.ts

-8
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,3 @@ 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

+1-12
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,7 @@
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';
10-
import { TerminalEnvVarCollectionService } from './activation/terminalEnvVarCollectionService';
11-
import { IEnvironmentActivationService, ITerminalEnvVarCollectionService } from './activation/types';
9+
import { IEnvironmentActivationService } from './activation/types';
1210
import { InterpreterAutoSelectionService } from './autoSelection/index';
1311
import { InterpreterAutoSelectionProxyService } from './autoSelection/proxy';
1412
import { IInterpreterAutoSelectionService, IInterpreterAutoSelectionProxyService } from './autoSelection/types';
@@ -110,13 +108,4 @@ export function registerTypes(serviceManager: IServiceManager): void {
110108
IEnvironmentActivationService,
111109
EnvironmentActivationService,
112110
);
113-
serviceManager.addSingleton<ITerminalEnvVarCollectionService>(
114-
ITerminalEnvVarCollectionService,
115-
TerminalEnvVarCollectionService,
116-
);
117-
serviceManager.addBinding(ITerminalEnvVarCollectionService, IExtensionActivationService);
118-
serviceManager.addSingleton<IExtensionSingleActivationService>(
119-
IExtensionSingleActivationService,
120-
TerminalEnvVarCollectionPrompt,
121-
);
122111
}

src/client/telemetry/constants.ts

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export enum EventName {
2929
TERMINAL_SHELL_IDENTIFICATION = 'TERMINAL_SHELL_IDENTIFICATION',
3030
PYTHON_INTERPRETER_ACTIVATE_ENVIRONMENT_PROMPT = 'PYTHON_INTERPRETER_ACTIVATE_ENVIRONMENT_PROMPT',
3131
PYTHON_NOT_INSTALLED_PROMPT = 'PYTHON_NOT_INSTALLED_PROMPT',
32+
TERMINAL_DEACTIVATE_PROMPT = 'TERMINAL_DEACTIVATE_PROMPT',
3233
CONDA_INHERIT_ENV_PROMPT = 'CONDA_INHERIT_ENV_PROMPT',
3334
REQUIRE_JUPYTER_PROMPT = 'REQUIRE_JUPYTER_PROMPT',
3435
ACTIVATED_CONDA_ENV_LAUNCH = 'ACTIVATED_CONDA_ENV_LAUNCH',

src/client/telemetry/index.ts

+18
Original file line numberDiff line numberDiff line change
@@ -1328,6 +1328,24 @@ export interface IEventNamePropertyMapping {
13281328
*/
13291329
selection: 'Allow' | 'Close' | undefined;
13301330
};
1331+
/**
1332+
* Telemetry event sent with details when user clicks the prompt with the following message:
1333+
*
1334+
* 'Deactivating virtual environments may not work by default due to a technical limitation in our activation approach, but it can be resolved with a few simple steps.'
1335+
*/
1336+
/* __GDPR__
1337+
"terminal_deactivate_prompt" : {
1338+
"selection" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karrtikr" }
1339+
}
1340+
*/
1341+
[EventName.TERMINAL_DEACTIVATE_PROMPT]: {
1342+
/**
1343+
* `See Instructions` When 'See Instructions' option is selected
1344+
* `Done, it works` When 'Done, it works' option is selected
1345+
* `Don't show again` When 'Don't show again' option is selected
1346+
*/
1347+
selection: 'See Instructions' | 'Done, it works' | "Don't show again" | undefined;
1348+
};
13311349
/**
13321350
* Telemetry event sent with details when user attempts to run in interactive window when Jupyter is not installed.
13331351
*/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
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 { IApplicationEnvironment, IApplicationShell } from '../../common/application/types';
7+
import { IBrowserService, IDisposableRegistry, IExperimentService, IPersistentStateFactory } from '../../common/types';
8+
import { Common, Interpreters } from '../../common/utils/localize';
9+
import { IExtensionSingleActivationService } from '../../activation/types';
10+
import { inTerminalEnvVarExperiment } from '../../common/experiments/helpers';
11+
import { IInterpreterService } from '../../interpreter/contracts';
12+
import { PythonEnvType } from '../../pythonEnvironments/base/info';
13+
import { identifyShellFromShellPath } from '../../common/terminal/shellDetectors/baseShellDetector';
14+
import { TerminalShellType } from '../../common/terminal/types';
15+
import { sendTelemetryEvent } from '../../telemetry';
16+
import { EventName } from '../../telemetry/constants';
17+
18+
export const terminalDeactivationPromptKey = 'TERMINAL_DEACTIVATION_PROMPT_KEY';
19+
20+
@injectable()
21+
export class TerminalDeactivateLimitationPrompt 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(IDisposableRegistry) private readonly disposableRegistry: IDisposableRegistry,
28+
@inject(IInterpreterService) private readonly interpreterService: IInterpreterService,
29+
@inject(IBrowserService) private readonly browserService: IBrowserService,
30+
@inject(IApplicationEnvironment) private readonly appEnvironment: IApplicationEnvironment,
31+
@inject(IExperimentService) private readonly experimentService: IExperimentService,
32+
) {}
33+
34+
public async activate(): Promise<void> {
35+
if (!inTerminalEnvVarExperiment(this.experimentService)) {
36+
return;
37+
}
38+
this.disposableRegistry.push(
39+
this.appShell.onDidWriteTerminalData(async (e) => {
40+
if (!e.data.includes('deactivate')) {
41+
return;
42+
}
43+
const shellType = identifyShellFromShellPath(this.appEnvironment.shell);
44+
if (shellType === TerminalShellType.commandPrompt) {
45+
return;
46+
}
47+
const { terminal } = e;
48+
const cwd =
49+
'cwd' in terminal.creationOptions && terminal.creationOptions.cwd
50+
? terminal.creationOptions.cwd
51+
: undefined;
52+
const resource = typeof cwd === 'string' ? Uri.file(cwd) : cwd;
53+
const interpreter = await this.interpreterService.getActiveInterpreter(resource);
54+
if (interpreter?.type !== PythonEnvType.Virtual) {
55+
return;
56+
}
57+
await this.notifyUsers();
58+
}),
59+
);
60+
}
61+
62+
private async notifyUsers(): Promise<void> {
63+
const notificationPromptEnabled = this.persistentStateFactory.createGlobalPersistentState(
64+
terminalDeactivationPromptKey,
65+
true,
66+
);
67+
if (!notificationPromptEnabled.value) {
68+
return;
69+
}
70+
const prompts = [Common.seeInstructions, Interpreters.deactivateDoneButton, Common.doNotShowAgain];
71+
const telemetrySelections: ['See Instructions', 'Done, it works', "Don't show again"] = [
72+
'See Instructions',
73+
'Done, it works',
74+
"Don't show again",
75+
];
76+
const selection = await this.appShell.showWarningMessage(Interpreters.terminalDeactivatePrompt, ...prompts);
77+
if (!selection) {
78+
return;
79+
}
80+
sendTelemetryEvent(EventName.TERMINAL_DEACTIVATE_PROMPT, undefined, {
81+
selection: selection ? telemetrySelections[prompts.indexOf(selection)] : undefined,
82+
});
83+
if (selection === prompts[0]) {
84+
const url = `https://aka.ms/AAmx2ft`;
85+
this.browserService.launch(url);
86+
}
87+
if (selection === prompts[1] || selection === prompts[2]) {
88+
await notificationPromptEnabled.updateValue(false);
89+
}
90+
}
91+
}

src/client/interpreter/activation/terminalEnvVarCollectionPrompt.ts src/client/terminals/envCollectionActivation/indicatorPrompt.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,15 @@ import {
1414
} from '../../common/types';
1515
import { Common, Interpreters } from '../../common/utils/localize';
1616
import { IExtensionSingleActivationService } from '../../activation/types';
17-
import { ITerminalEnvVarCollectionService } from './types';
1817
import { inTerminalEnvVarExperiment } from '../../common/experiments/helpers';
19-
import { IInterpreterService } from '../contracts';
18+
import { IInterpreterService } from '../../interpreter/contracts';
2019
import { PythonEnvironment } from '../../pythonEnvironments/info';
20+
import { ITerminalEnvVarCollectionService } from '../types';
2121

2222
export const terminalEnvCollectionPromptKey = 'TERMINAL_ENV_COLLECTION_PROMPT_KEY';
2323

2424
@injectable()
25-
export class TerminalEnvVarCollectionPrompt implements IExtensionSingleActivationService {
25+
export class TerminalIndicatorPrompt implements IExtensionSingleActivationService {
2626
public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false };
2727

2828
constructor(

src/client/interpreter/activation/terminalEnvVarCollectionService.ts src/client/terminals/envCollectionActivation/service.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -28,16 +28,17 @@ import {
2828
import { Deferred, createDeferred } from '../../common/utils/async';
2929
import { Interpreters } from '../../common/utils/localize';
3030
import { traceDecoratorVerbose, traceError, traceVerbose, traceWarn } from '../../logging';
31-
import { IInterpreterService } from '../contracts';
32-
import { defaultShells } from './service';
33-
import { IEnvironmentActivationService, ITerminalEnvVarCollectionService } from './types';
31+
import { IInterpreterService } from '../../interpreter/contracts';
32+
import { defaultShells } from '../../interpreter/activation/service';
33+
import { IEnvironmentActivationService } from '../../interpreter/activation/types';
3434
import { EnvironmentType, PythonEnvironment } from '../../pythonEnvironments/info';
3535
import { getSearchPathEnvVarNames } from '../../common/utils/exec';
3636
import { EnvironmentVariables } from '../../common/variables/types';
3737
import { TerminalShellType } from '../../common/terminal/types';
3838
import { OSType } from '../../common/utils/platform';
3939
import { normCase } from '../../common/platform/fs-paths';
4040
import { PythonEnvType } from '../../pythonEnvironments/base/info';
41+
import { ITerminalEnvVarCollectionService } from '../types';
4142

4243
@injectable()
4344
export class TerminalEnvVarCollectionService implements IExtensionActivationService, ITerminalEnvVarCollectionService {
+26-12
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,26 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33

4-
import { interfaces } from 'inversify';
5-
import { ClassType } from '../ioc/types';
4+
import { IServiceManager } from '../ioc/types';
65
import { TerminalAutoActivation } from './activation';
76
import { CodeExecutionManager } from './codeExecution/codeExecutionManager';
87
import { DjangoShellCodeExecutionProvider } from './codeExecution/djangoShellCodeExecution';
98
import { CodeExecutionHelper } from './codeExecution/helper';
109
import { ReplProvider } from './codeExecution/repl';
1110
import { TerminalCodeExecutionProvider } from './codeExecution/terminalCodeExecution';
12-
import { ICodeExecutionHelper, ICodeExecutionManager, ICodeExecutionService, ITerminalAutoActivation } from './types';
11+
import {
12+
ICodeExecutionHelper,
13+
ICodeExecutionManager,
14+
ICodeExecutionService,
15+
ITerminalAutoActivation,
16+
ITerminalEnvVarCollectionService,
17+
} from './types';
18+
import { TerminalEnvVarCollectionService } from './envCollectionActivation/service';
19+
import { IExtensionActivationService, IExtensionSingleActivationService } from '../activation/types';
20+
import { TerminalDeactivateLimitationPrompt } from './envCollectionActivation/deactivatePrompt';
21+
import { TerminalIndicatorPrompt } from './envCollectionActivation/indicatorPrompt';
1322

14-
interface IServiceRegistry {
15-
addSingleton<T>(
16-
serviceIdentifier: interfaces.ServiceIdentifier<T>,
17-
constructor: ClassType<T>,
18-
name?: string | number | symbol,
19-
): void;
20-
}
21-
22-
export function registerTypes(serviceManager: IServiceRegistry): void {
23+
export function registerTypes(serviceManager: IServiceManager): void {
2324
serviceManager.addSingleton<ICodeExecutionHelper>(ICodeExecutionHelper, CodeExecutionHelper);
2425

2526
serviceManager.addSingleton<ICodeExecutionManager>(ICodeExecutionManager, CodeExecutionManager);
@@ -37,4 +38,17 @@ export function registerTypes(serviceManager: IServiceRegistry): void {
3738
serviceManager.addSingleton<ICodeExecutionService>(ICodeExecutionService, ReplProvider, 'repl');
3839

3940
serviceManager.addSingleton<ITerminalAutoActivation>(ITerminalAutoActivation, TerminalAutoActivation);
41+
serviceManager.addSingleton<ITerminalEnvVarCollectionService>(
42+
ITerminalEnvVarCollectionService,
43+
TerminalEnvVarCollectionService,
44+
);
45+
serviceManager.addSingleton<IExtensionSingleActivationService>(
46+
IExtensionSingleActivationService,
47+
TerminalIndicatorPrompt,
48+
);
49+
serviceManager.addSingleton<IExtensionSingleActivationService>(
50+
IExtensionSingleActivationService,
51+
TerminalDeactivateLimitationPrompt,
52+
);
53+
serviceManager.addBinding(ITerminalEnvVarCollectionService, IExtensionActivationService);
4054
}

src/client/terminals/types.ts

+8
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,11 @@ export interface ITerminalAutoActivation extends IDisposable {
3333
register(): void;
3434
disableAutoActivation(terminal: Terminal): void;
3535
}
36+
37+
export const ITerminalEnvVarCollectionService = Symbol('ITerminalEnvVarCollectionService');
38+
export interface ITerminalEnvVarCollectionService {
39+
/**
40+
* Returns true if we know with high certainity the terminal prompt is set correctly for a particular resource.
41+
*/
42+
isTerminalPromptSetCorrectly(resource?: Resource): boolean;
43+
}

src/test/interpreters/activation/terminalEnvVarCollectionPrompt.unit.test.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,13 @@ import {
1313
IPersistentStateFactory,
1414
IPythonSettings,
1515
} from '../../../client/common/types';
16-
import { TerminalEnvVarCollectionPrompt } from '../../../client/interpreter/activation/terminalEnvVarCollectionPrompt';
17-
import { ITerminalEnvVarCollectionService } from '../../../client/interpreter/activation/types';
16+
import { TerminalIndicatorPrompt } from '../../../client/terminals/envCollectionActivation/indicatorPrompt';
1817
import { Common, Interpreters } from '../../../client/common/utils/localize';
1918
import { TerminalEnvVarActivation } from '../../../client/common/experiments/groups';
2019
import { sleep } from '../../core';
2120
import { IInterpreterService } from '../../../client/interpreter/contracts';
2221
import { PythonEnvironment } from '../../../client/pythonEnvironments/info';
22+
import { ITerminalEnvVarCollectionService } from '../../../client/terminals/types';
2323

2424
suite('Terminal Environment Variable Collection Prompt', () => {
2525
let shell: IApplicationShell;
@@ -28,7 +28,7 @@ suite('Terminal Environment Variable Collection Prompt', () => {
2828
let activeResourceService: IActiveResourceService;
2929
let terminalEnvVarCollectionService: ITerminalEnvVarCollectionService;
3030
let persistentStateFactory: IPersistentStateFactory;
31-
let terminalEnvVarCollectionPrompt: TerminalEnvVarCollectionPrompt;
31+
let terminalEnvVarCollectionPrompt: TerminalIndicatorPrompt;
3232
let terminalEventEmitter: EventEmitter<Terminal>;
3333
let notificationEnabled: IPersistentState<boolean>;
3434
let configurationService: IConfigurationService;
@@ -61,7 +61,7 @@ suite('Terminal Environment Variable Collection Prompt', () => {
6161
);
6262
when(experimentService.inExperimentSync(TerminalEnvVarActivation.experiment)).thenReturn(true);
6363
when(terminalManager.onDidOpenTerminal).thenReturn(terminalEventEmitter.event);
64-
terminalEnvVarCollectionPrompt = new TerminalEnvVarCollectionPrompt(
64+
terminalEnvVarCollectionPrompt = new TerminalIndicatorPrompt(
6565
instance(shell),
6666
instance(persistentStateFactory),
6767
instance(terminalManager),

src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import {
3232
import { Interpreters } from '../../../client/common/utils/localize';
3333
import { OSType, getOSType } from '../../../client/common/utils/platform';
3434
import { defaultShells } from '../../../client/interpreter/activation/service';
35-
import { TerminalEnvVarCollectionService } from '../../../client/interpreter/activation/terminalEnvVarCollectionService';
35+
import { TerminalEnvVarCollectionService } from '../../../client/terminals/envCollectionActivation/service';
3636
import { IEnvironmentActivationService } from '../../../client/interpreter/activation/types';
3737
import { IInterpreterService } from '../../../client/interpreter/contracts';
3838
import { PathUtils } from '../../../client/common/platform/pathUtils';

0 commit comments

Comments
 (0)