Skip to content

Commit eb3478a

Browse files
author
Kartik Raj
authored
Add prompt to select virtual environment for the worskpace (microsoft#5092)
* Added functionality * News entry * Temp * Code reviews * Make resource mandatory * Added tests * Code reviews * Update src/client/interpreter/virtualEnvs/virtualEnvPrompt.ts Co-Authored-By: karrtikr <karraj@microsoft.com> * Update src/client/interpreter/virtualEnvs/virtualEnvPrompt.ts Co-Authored-By: karrtikr <karraj@microsoft.com> * Correct code and test the main scenario * Wait for locator to get the latest interpreter * Update src/client/interpreter/virtualEnvs/virtualEnvPrompt.ts Co-Authored-By: karrtikr <karraj@microsoft.com>
1 parent 796d943 commit eb3478a

File tree

12 files changed

+354
-27
lines changed

12 files changed

+354
-27
lines changed

news/1 Enhancements/4908.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add prompt to select virtual environment for the worskpace

package.nls.json

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,8 @@
115115
"DataScience.connectingToJupyter": "Connecting to Jupyter server",
116116
"Interpreters.RefreshingInterpreters": "Refreshing Python Interpreters",
117117
"Interpreters.LoadingInterpreters": "Loading Python Interpreters",
118+
"Interpreters.doNotShowAgain": "Do not show again",
119+
"Interpreters.environmentPromptMessage": "We noticed a new virtual environment has been created. Do you want to select it for the workspace folder?",
118120
"DataScience.restartKernelMessage": "Do you want to restart the iPython kernel? All variables will be lost.",
119121
"DataScience.restartKernelMessageYes": "Restart",
120122
"DataScience.restartKernelMessageNo": "Cancel",
@@ -225,11 +227,11 @@
225227
"LanguageService.extractionCompletedOutputMessage": "complete",
226228
"LanguageService.extractionDoneOutputMessage": "done",
227229
"LanguageService.reloadVSCodeIfSeachPathHasChanged": "Search paths have changed for this Python interpreter. Please reload the extension to ensure that the IntelliSense works correctly",
228-
"DataScience.dataExplorerInvalidVariableFormat" : "'{0}' is not an active variable.",
229-
"DataScience.jupyterGetVariablesExecutionError" : "Failure during variable extraction:\r\n{0}",
230-
"DataScience.loadingMessage" : "loading ...",
231-
"DataScience.noRowsInDataViewer" : "Fetching data ...",
232-
"DataScience.pandasTooOldForViewingFormat" : "Python package 'pandas' is version {0}. Version 0.20 or greater is required for viewing data.",
233-
"DataScience.pandasRequiredForViewing" : "Python package 'pandas' is required for viewing data.",
230+
"DataScience.dataExplorerInvalidVariableFormat": "'{0}' is not an active variable.",
231+
"DataScience.jupyterGetVariablesExecutionError": "Failure during variable extraction:\r\n{0}",
232+
"DataScience.loadingMessage": "loading ...",
233+
"DataScience.noRowsInDataViewer": "Fetching data ...",
234+
"DataScience.pandasTooOldForViewingFormat": "Python package 'pandas' is version {0}. Version 0.20 or greater is required for viewing data.",
235+
"DataScience.pandasRequiredForViewing": "Python package 'pandas' is required for viewing data.",
234236
"DataScience.valuesColumn": "values"
235237
}

src/client/common/utils/localize.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ export namespace LanguageService {
4242
export namespace Interpreters {
4343
export const loading = localize('Interpreters.LoadingInterpreters', 'Loading Python Interpreters');
4444
export const refreshing = localize('Interpreters.RefreshingInterpreters', 'Refreshing Python Interpreters');
45+
export const doNotShowAgain = localize('Interpreters.doNotShowAgain', 'Do not show again');
46+
export const environmentPromptMessage = localize('Interpreters.environmentPromptMessage', 'We noticed a new virtual environment has been created. Do you want to select it for the workspace folder?');
4547
}
4648

4749
export namespace Linters {

src/client/interpreter/contracts.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ export interface IInterpreterService {
8888
getInterpreters(resource?: Uri): Promise<PythonInterpreter[]>;
8989
getActiveInterpreter(resource?: Uri): Promise<PythonInterpreter | undefined>;
9090
getInterpreterDetails(pythonPath: string, resoure?: Uri): Promise<undefined | PythonInterpreter>;
91-
refresh(resource: Uri | undefined): Promise<void>;
91+
refresh(resource: Resource): Promise<void>;
9292
initialize(): void;
9393
getDisplayName(interpreter: Partial<PythonInterpreter>): Promise<string>;
9494
}
@@ -125,12 +125,12 @@ export interface IInterpreterLocatorHelper {
125125

126126
export const IInterpreterWatcher = Symbol('IInterpreterWatcher');
127127
export interface IInterpreterWatcher {
128-
onDidCreate: Event<void>;
128+
onDidCreate: Event<Resource>;
129129
}
130130

131131
export const IInterpreterWatcherBuilder = Symbol('IInterpreterWatcherBuilder');
132132
export interface IInterpreterWatcherBuilder {
133-
getWorkspaceVirtualEnvInterpreterWatcher(resource: Uri | undefined): Promise<IInterpreterWatcher>;
133+
getWorkspaceVirtualEnvInterpreterWatcher(resource: Resource): Promise<IInterpreterWatcher>;
134134
}
135135

136136
export const InterpreterLocatorProgressHandler = Symbol('InterpreterLocatorProgressHandler');

src/client/interpreter/locators/services/workspaceVirtualEnvWatcherService.ts

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,36 +11,37 @@ import '../../../common/extensions';
1111
import { Logger, traceDecorators } from '../../../common/logger';
1212
import { IPlatformService } from '../../../common/platform/types';
1313
import { IPythonExecutionFactory } from '../../../common/process/types';
14-
import { IDisposableRegistry } from '../../../common/types';
14+
import { IDisposableRegistry, Resource } from '../../../common/types';
1515
import { IInterpreterWatcher } from '../../contracts';
1616

1717
const maxTimeToWaitForEnvCreation = 60_000;
1818
const timeToPollForEnvCreation = 2_000;
1919

2020
@injectable()
2121
export class WorkspaceVirtualEnvWatcherService implements IInterpreterWatcher, Disposable {
22-
private readonly didCreate: EventEmitter<void>;
22+
private readonly didCreate: EventEmitter<Resource>;
2323
private timers = new Map<string, { timer: NodeJS.Timer; counter: number }>();
2424
private fsWatchers: FileSystemWatcher[] = [];
25+
private resource: Resource;
2526
constructor(@inject(IDisposableRegistry) private readonly disposableRegistry: Disposable[],
2627
@inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService,
2728
@inject(IPlatformService) private readonly platformService: IPlatformService,
2829
@inject(IPythonExecutionFactory) private readonly pythonExecFactory: IPythonExecutionFactory) {
29-
this.didCreate = new EventEmitter<void>();
30+
this.didCreate = new EventEmitter<Resource>();
3031
disposableRegistry.push(this);
3132
}
32-
public get onDidCreate(): Event<void> {
33+
public get onDidCreate(): Event<Resource> {
3334
return this.didCreate.event;
3435
}
3536
public dispose() {
3637
this.clearTimers();
3738
}
3839
@traceDecorators.verbose('Register Intepreter Watcher')
39-
public async register(resource: Uri | undefined): Promise<void> {
40+
public async register(resource: Resource): Promise<void> {
4041
if (this.fsWatchers.length > 0) {
4142
return;
4243
}
43-
44+
this.resource = resource;
4445
const workspaceFolder = resource ? this.workspaceService.getWorkspaceFolder(resource) : undefined;
4546
const executable = this.platformService.isWindows ? 'python.exe' : 'python';
4647
const patterns = [path.join('*', executable), path.join('*', '*', executable)];
@@ -58,7 +59,7 @@ export class WorkspaceVirtualEnvWatcherService implements IInterpreterWatcher, D
5859
}
5960
@traceDecorators.verbose('Intepreter Watcher change handler')
6061
public async createHandler(e: Uri) {
61-
this.didCreate.fire();
62+
this.didCreate.fire(this.resource);
6263
// On Windows, creation of environments are very slow, hence lets notify again after
6364
// the python executable is accessible (i.e. when we can launch the process).
6465
this.notifyCreationWhenReady(e.fsPath).ignoreErrors();
@@ -68,13 +69,13 @@ export class WorkspaceVirtualEnvWatcherService implements IInterpreterWatcher, D
6869
const isValid = await this.isValidExecutable(pythonPath);
6970
if (isValid) {
7071
if (counter > 0) {
71-
this.didCreate.fire();
72+
this.didCreate.fire(this.resource);
7273
}
7374
return this.timers.delete(pythonPath);
7475
}
7576
if (counter > (maxTimeToWaitForEnvCreation / timeToPollForEnvCreation)) {
7677
// Send notification before we give up trying.
77-
this.didCreate.fire();
78+
this.didCreate.fire(this.resource);
7879
this.timers.delete(pythonPath);
7980
return;
8081
}

src/client/interpreter/serviceRegistry.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33

4+
import { IExtensionActivationService } from '../activation/types';
45
import { IServiceManager } from '../ioc/types';
56
import { EnvironmentActivationService } from './activation/service';
67
import { IEnvironmentActivationService } from './activation/types';
@@ -68,6 +69,7 @@ import { WorkspaceVirtualEnvWatcherService } from './locators/services/workspace
6869
import { IPipEnvServiceHelper, IPythonInPathCommandProvider } from './locators/types';
6970
import { VirtualEnvironmentManager } from './virtualEnvs/index';
7071
import { IVirtualEnvironmentManager } from './virtualEnvs/types';
72+
import { VirtualEnvironmentPrompt } from './virtualEnvs/virtualEnvPrompt';
7173

7274
export function registerTypes(serviceManager: IServiceManager) {
7375
serviceManager.addSingleton<IKnownSearchPathsForInterpreters>(IKnownSearchPathsForInterpreters, KnownSearchPathsForInterpreters);
@@ -77,6 +79,7 @@ export function registerTypes(serviceManager: IServiceManager) {
7779
serviceManager.addSingleton<ICondaService>(ICondaService, CondaService);
7880
serviceManager.addSingleton<IPipEnvServiceHelper>(IPipEnvServiceHelper, PipEnvServiceHelper);
7981
serviceManager.addSingleton<IVirtualEnvironmentManager>(IVirtualEnvironmentManager, VirtualEnvironmentManager);
82+
serviceManager.addSingleton<IExtensionActivationService>(IExtensionActivationService, VirtualEnvironmentPrompt);
8083
serviceManager.addSingleton<IPythonInPathCommandProvider>(IPythonInPathCommandProvider, PythonInPathCommandProvider);
8184

8285
serviceManager.add<IInterpreterWatcher>(IInterpreterWatcher, WorkspaceVirtualEnvWatcherService, WORKSPACE_VIRTUAL_ENV_SERVICE);
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import { inject, injectable, named } from 'inversify';
5+
import { ConfigurationTarget, Disposable, Uri } from 'vscode';
6+
import { IExtensionActivationService } from '../../activation/types';
7+
import { IApplicationShell, IWorkspaceService } from '../../common/application/types';
8+
import { traceDecorators } from '../../common/logger';
9+
import { IDisposableRegistry, IPersistentStateFactory } from '../../common/types';
10+
import { sleep } from '../../common/utils/async';
11+
import { InteractiveShiftEnterBanner, Interpreters } from '../../common/utils/localize';
12+
import { sendTelemetryEvent } from '../../telemetry';
13+
import { EventName } from '../../telemetry/constants';
14+
import { IPythonPathUpdaterServiceManager } from '../configuration/types';
15+
import { IInterpreterHelper, IInterpreterLocatorService, IInterpreterWatcherBuilder, PythonInterpreter, WORKSPACE_VIRTUAL_ENV_SERVICE } from '../contracts';
16+
17+
const doNotDisplayPromptStateKey = 'MESSAGE_KEY_FOR_VIRTUAL_ENV';
18+
@injectable()
19+
export class VirtualEnvironmentPrompt implements IExtensionActivationService {
20+
constructor(
21+
@inject(IInterpreterWatcherBuilder) private readonly builder: IInterpreterWatcherBuilder,
22+
@inject(IPersistentStateFactory) private readonly persistentStateFactory: IPersistentStateFactory,
23+
@inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService,
24+
@inject(IInterpreterHelper) private readonly helper: IInterpreterHelper,
25+
@inject(IPythonPathUpdaterServiceManager) private readonly pythonPathUpdaterService: IPythonPathUpdaterServiceManager,
26+
@inject(IInterpreterLocatorService) @named(WORKSPACE_VIRTUAL_ENV_SERVICE) private readonly locator: IInterpreterLocatorService,
27+
@inject(IDisposableRegistry) private readonly disposableRegistry: Disposable[],
28+
@inject(IApplicationShell) private readonly appShell: IApplicationShell) { }
29+
30+
public async activate(resource: Uri): Promise<void> {
31+
const watcher = await this.builder.getWorkspaceVirtualEnvInterpreterWatcher(resource);
32+
watcher.onDidCreate(() => {
33+
this.handleNewEnvironment(resource).ignoreErrors();
34+
}, this, this.disposableRegistry);
35+
}
36+
37+
@traceDecorators.error('Error in event handler for detection of new environment')
38+
protected async handleNewEnvironment(resource: Uri): Promise<void> {
39+
// Wait for a while, to ensure environment gets created and is accessible (as this is slow on Windows)
40+
await sleep(1000);
41+
const interpreters = await this.locator.getInterpreters(resource);
42+
const interpreter = this.helper.getBestInterpreter(interpreters);
43+
if (!interpreter || this.hasUserDefinedPythonPath(resource)) {
44+
return;
45+
}
46+
await this.notifyUser(interpreter, resource);
47+
}
48+
protected async notifyUser(interpreter: PythonInterpreter, resource: Uri): Promise<void> {
49+
const notificationPromptEnabled = this.persistentStateFactory.createWorkspacePersistentState(doNotDisplayPromptStateKey, true);
50+
if (!notificationPromptEnabled.value) {
51+
return;
52+
}
53+
const prompts = [InteractiveShiftEnterBanner.bannerLabelYes(), InteractiveShiftEnterBanner.bannerLabelNo(), Interpreters.doNotShowAgain()];
54+
const telemetrySelections: ['Yes', 'No', 'Ignore'] = ['Yes', 'No', 'Ignore'];
55+
const selection = await this.appShell.showInformationMessage(Interpreters.environmentPromptMessage(), ...prompts);
56+
sendTelemetryEvent(EventName.PYTHON_INTERPRETER_ACTIVATE_ENVIRONMENT_PROMPT, undefined, { selection: selection ? telemetrySelections[prompts.indexOf(selection)] : undefined });
57+
if (!selection) {
58+
return;
59+
}
60+
if (selection === prompts[0]) {
61+
await this.pythonPathUpdaterService.updatePythonPath(interpreter.path, ConfigurationTarget.WorkspaceFolder, 'ui', resource);
62+
} else if (selection === prompts[2]) {
63+
await notificationPromptEnabled.updateValue(false);
64+
}
65+
}
66+
protected hasUserDefinedPythonPath(resource?: Uri) {
67+
const settings = this.workspaceService.getConfiguration('python', resource)!.inspect<string>('pythonPath')!;
68+
return ((settings.workspaceFolderValue && settings.workspaceFolderValue !== 'python') ||
69+
(settings.workspaceValue && settings.workspaceValue !== 'python')) ? true : false;
70+
}
71+
}

src/client/telemetry/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export enum EventName {
2929
PYTHON_INTERPRETER_ACTIVATION_ENVIRONMENT_VARIABLES = 'PYTHON_INTERPRETER_ACTIVATION_ENVIRONMENT_VARIABLES',
3030
PYTHON_INTERPRETER_ACTIVATION_FOR_RUNNING_CODE = 'PYTHON_INTERPRETER_ACTIVATION_FOR_RUNNING_CODE',
3131
PYTHON_INTERPRETER_ACTIVATION_FOR_TERMINAL = 'PYTHON_INTERPRETER_ACTIVATION_FOR_TERMINAL',
32+
PYTHON_INTERPRETER_ACTIVATE_ENVIRONMENT_PROMPT = 'PYTHON_INTERPRETER_ACTIVATE_ENVIRONMENT_PROMPT',
3233
ENVFILE_VARIABLE_SUBSTITUTION = 'ENVFILE_VARIABLE_SUBSTITUTION',
3334
WORKSPACE_SYMBOLS_BUILD = 'WORKSPACE_SYMBOLS.BUILD',
3435
WORKSPACE_SYMBOLS_GO_TO = 'WORKSPACE_SYMBOLS.GO_TO',

src/client/telemetry/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,7 @@ export interface IEventNamePropertyMapping {
285285
[EventName.PYTHON_INTERPRETER_ACTIVATION_FOR_TERMINAL]: InterpreterActivation;
286286
[EventName.PYTHON_INTERPRETER_AUTO_SELECTION]: InterpreterAutoSelection;
287287
[EventName.PYTHON_INTERPRETER_DISCOVERY]: InterpreterDiscovery;
288+
[EventName.PYTHON_INTERPRETER_ACTIVATE_ENVIRONMENT_PROMPT]: { selection: 'Yes' | 'No' | 'Ignore' | undefined };
288289
[EventName.PYTHON_LANGUAGE_SERVER_SWITCHED]: { change: 'Switch to Jedi from LS' | 'Switch to LS from Jedi' };
289290
[EventName.PYTHON_LANGUAGE_SERVER_ANALYSISTIME]: { success: boolean };
290291
[EventName.PYTHON_LANGUAGE_SERVER_DOWNLOADED]: LanguageServerVersionTelemetry;
@@ -351,7 +352,7 @@ export interface IEventNamePropertyMapping {
351352
[Telemetry.SetJupyterURIToLocal]: never | undefined;
352353
[Telemetry.SetJupyterURIToUserSpecified]: never | undefined;
353354
[Telemetry.ShiftEnterBannerShown]: never | undefined;
354-
[Telemetry.ShowDataViewer]: {rows: number | undefined};
355+
[Telemetry.ShowDataViewer]: { rows: number | undefined };
355356
[Telemetry.ShowHistoryPane]: never | undefined;
356357
[Telemetry.StartJupyter]: never | undefined;
357358
[Telemetry.SubmitCellThroughInput]: never | undefined;

src/client/telemetry/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,5 +193,5 @@ export type InterpreterActivation = {
193193

194194
export const IImportTracker = Symbol('IImportTracker');
195195
export interface IImportTracker {
196-
activate() : Promise<void>;
196+
activate(): Promise<void>;
197197
}

0 commit comments

Comments
 (0)