Skip to content

Commit d7a5ab7

Browse files
authored
Ability to track the user selected Environment (#25090)
@karthiknadig /cc
1 parent 27270db commit d7a5ab7

File tree

14 files changed

+195
-95
lines changed

14 files changed

+195
-95
lines changed

src/client/chat/installPackagesTool.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import { resolveFilePath } from './utils';
1919
import { IModuleInstaller } from '../common/installer/types';
2020
import { ModuleInstallerType } from '../pythonEnvironments/info';
2121
import { IDiscoveryAPI } from '../pythonEnvironments/base/locator';
22-
import { trackEnvUsedByTool } from './lastUsedEnvs';
2322

2423
export interface IInstallPackageArgs {
2524
resourcePath?: string;
@@ -67,7 +66,6 @@ export class InstallPackagesTool implements LanguageModelTool<IInstallPackageArg
6766
for (const packageName of options.input.packageList) {
6867
await installer.installModule(packageName, resourcePath, token, undefined, { installAsProcess: true });
6968
}
70-
trackEnvUsedByTool(resourcePath, environment);
7169
// format and return
7270
const resultMessage = `Successfully installed ${packagePlurality}: ${options.input.packageList.join(', ')}`;
7371
return new LanguageModelToolResult([new LanguageModelTextPart(resultMessage)]);

src/client/chat/lastUsedEnvs.ts

Lines changed: 0 additions & 81 deletions
This file was deleted.

src/client/chat/listPackagesTool.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import { parsePipList } from './pipListUtils';
2222
import { Conda } from '../pythonEnvironments/common/environmentManagers/conda';
2323
import { traceError } from '../logging';
2424
import { IDiscoveryAPI } from '../pythonEnvironments/base/locator';
25-
import { trackEnvUsedByTool } from './lastUsedEnvs';
2625

2726
export interface IResourceReference {
2827
resourcePath?: string;
@@ -109,7 +108,6 @@ export async function getPythonPackagesResponse(
109108
if (!packages.length) {
110109
return 'No packages found';
111110
}
112-
trackEnvUsedByTool(resourcePath, environment);
113111
// Installed Python packages, each in the format <name> or <name> (<version>). The version may be omitted if unknown. Returns an empty array if no packages are installed.
114112
const response = [
115113
'Below is a list of the Python packages, each in the format <name> or <name> (<version>). The version may be omitted if unknown: ',

src/client/chat/utils.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import { PythonExtension, ResolvedEnvironment } from '../api/types';
77
import { ITerminalHelper, TerminalShellType } from '../common/terminal/types';
88
import { TerminalCodeExecutionProvider } from '../terminals/codeExecution/terminalCodeExecution';
99
import { Conda } from '../pythonEnvironments/common/environmentManagers/conda';
10-
import { trackEnvUsedByTool } from './lastUsedEnvs';
1110

1211
export function resolveFilePath(filepath?: string): Uri | undefined {
1312
if (!filepath) {
@@ -71,7 +70,6 @@ export async function getEnvironmentDetails(
7170
getTerminalCommand(environment, resourcePath, terminalExecutionService, terminalHelper),
7271
token,
7372
);
74-
trackEnvUsedByTool(resourcePath, environment);
7573
const message = [
7674
`Following is the information about the Python environment:`,
7775
`1. Environment Type: ${environment.environment?.type || 'unknown'}`,

src/client/common/utils/async.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,3 +268,26 @@ export async function waitForCondition(
268268
}, 10);
269269
});
270270
}
271+
272+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
273+
export function isPromiseLike<T>(v: any): v is PromiseLike<T> {
274+
return typeof v?.then === 'function';
275+
}
276+
277+
export function raceTimeout<T>(timeout: number, ...promises: Promise<T>[]): Promise<T | undefined>;
278+
export function raceTimeout<T>(timeout: number, defaultValue: T, ...promises: Promise<T>[]): Promise<T>;
279+
export function raceTimeout<T>(timeout: number, defaultValue: T, ...promises: Promise<T>[]): Promise<T> {
280+
const resolveValue = isPromiseLike(defaultValue) ? undefined : defaultValue;
281+
if (isPromiseLike(defaultValue)) {
282+
promises.push((defaultValue as unknown) as Promise<T>);
283+
}
284+
285+
let promiseResolve: ((value: T) => void) | undefined = undefined;
286+
287+
const timer = setTimeout(() => promiseResolve?.((resolveValue as unknown) as T), timeout);
288+
289+
return Promise.race([
290+
Promise.race(promises).finally(() => clearTimeout(timer)),
291+
new Promise<T>((resolve) => (promiseResolve = resolve)),
292+
]);
293+
}

src/client/extension.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import { ProposedExtensionAPI } from './proposedApiTypes';
4444
import { buildProposedApi } from './proposedApi';
4545
import { GLOBAL_PERSISTENT_KEYS } from './common/persistentState';
4646
import { registerTools } from './chat';
47+
import { IRecommendedEnvironmentService } from './interpreter/configuration/types';
4748

4849
durations.codeLoadingTime = stopWatch.elapsedTime;
4950

@@ -164,6 +165,9 @@ async function activateUnsafe(
164165
);
165166
const proposedApi = buildProposedApi(components.pythonEnvs, ext.legacyIOC.serviceContainer);
166167
registerTools(context, components.pythonEnvs, api.environments, ext.legacyIOC.serviceContainer);
168+
ext.legacyIOC.serviceContainer
169+
.get<IRecommendedEnvironmentService>(IRecommendedEnvironmentService)
170+
.registerEnvApi(api.environments);
167171
return [{ ...api, ...proposedApi }, activationPromise, ext.legacyIOC.serviceContainer];
168172
}
169173

src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -565,8 +565,11 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand implem
565565
return Promise.resolve();
566566
}
567567

568+
/**
569+
* @returns true when an interpreter was set, undefined if the user cancelled the quickpick.
570+
*/
568571
@captureTelemetry(EventName.SELECT_INTERPRETER)
569-
public async setInterpreter(): Promise<void> {
572+
public async setInterpreter(): Promise<true | undefined> {
570573
const targetConfig = await this.getConfigTargets();
571574
if (!targetConfig) {
572575
return;
@@ -588,6 +591,7 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand implem
588591
if (useEnvExtension()) {
589592
await setInterpreterLegacy(interpreterState.path, wkspace);
590593
}
594+
return true;
591595
}
592596
}
593597

src/client/interpreter/configuration/pythonPathUpdaterService.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,19 @@ import { sendTelemetryEvent } from '../../telemetry';
77
import { EventName } from '../../telemetry/constants';
88
import { PythonInterpreterTelemetry } from '../../telemetry/types';
99
import { IComponentAdapter } from '../contracts';
10-
import { IPythonPathUpdaterServiceFactory, IPythonPathUpdaterServiceManager } from './types';
10+
import {
11+
IRecommendedEnvironmentService,
12+
IPythonPathUpdaterServiceFactory,
13+
IPythonPathUpdaterServiceManager,
14+
} from './types';
1115

1216
@injectable()
1317
export class PythonPathUpdaterService implements IPythonPathUpdaterServiceManager {
1418
constructor(
1519
@inject(IPythonPathUpdaterServiceFactory)
1620
private readonly pythonPathSettingsUpdaterFactory: IPythonPathUpdaterServiceFactory,
1721
@inject(IComponentAdapter) private readonly pyenvs: IComponentAdapter,
22+
@inject(IRecommendedEnvironmentService) private readonly preferredEnvService: IRecommendedEnvironmentService,
1823
) {}
1924

2025
public async updatePythonPath(
@@ -28,6 +33,9 @@ export class PythonPathUpdaterService implements IPythonPathUpdaterServiceManage
2833
let failed = false;
2934
try {
3035
await pythonPathUpdater.updatePythonPath(pythonPath);
36+
if (trigger === 'ui') {
37+
this.preferredEnvService.trackUserSelectedEnvironment(pythonPath, wkspace);
38+
}
3139
} catch (err) {
3240
failed = true;
3341
const reason = err as Error;
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import { inject, injectable } from 'inversify';
5+
import { IRecommendedEnvironmentService } from './types';
6+
import { PythonExtension } from '../../api/types';
7+
import { IExtensionContext, Resource } from '../../common/types';
8+
import { Uri, workspace } from 'vscode';
9+
import { getWorkspaceStateValue, updateWorkspaceStateValue } from '../../common/persistentState';
10+
import { traceError } from '../../logging';
11+
12+
const MEMENTO_KEY = 'userSelectedEnvPath';
13+
14+
@injectable()
15+
export class RecommendedEnvironmentService implements IRecommendedEnvironmentService {
16+
private api?: PythonExtension['environments'];
17+
constructor(@inject(IExtensionContext) private readonly extensionContext: IExtensionContext) {}
18+
19+
registerEnvApi(api: PythonExtension['environments']) {
20+
this.api = api;
21+
}
22+
23+
trackUserSelectedEnvironment(environmentPath: string | undefined, uri: Uri | undefined) {
24+
if (workspace.workspaceFolders?.length) {
25+
try {
26+
void updateWorkspaceStateValue(MEMENTO_KEY, getDataToStore(environmentPath, uri));
27+
} catch (ex) {
28+
traceError('Failed to update workspace state for preferred environment', ex);
29+
}
30+
} else {
31+
void this.extensionContext.globalState.update(MEMENTO_KEY, environmentPath);
32+
}
33+
}
34+
35+
getRecommededEnvironment(
36+
resource: Resource,
37+
):
38+
| { environmentPath: string; reason: 'globalUserSelected' | 'workspaceUserSelected' | 'defaultRecommended' }
39+
| undefined {
40+
let workspaceState: string | undefined = undefined;
41+
try {
42+
workspaceState = getWorkspaceStateValue<string>(MEMENTO_KEY);
43+
} catch (ex) {
44+
traceError('Failed to get workspace state for preferred environment', ex);
45+
}
46+
47+
if (workspace.workspaceFolders?.length && workspaceState) {
48+
const workspaceUri = (
49+
(resource ? workspace.getWorkspaceFolder(resource)?.uri : undefined) ||
50+
workspace.workspaceFolders[0].uri
51+
).toString();
52+
53+
try {
54+
const existingJson: Record<string, string> = JSON.parse(workspaceState);
55+
const selectedEnvPath = existingJson[workspaceUri];
56+
if (selectedEnvPath) {
57+
return { environmentPath: selectedEnvPath, reason: 'workspaceUserSelected' };
58+
}
59+
} catch (ex) {
60+
traceError('Failed to parse existing workspace state value for preferred environment', ex);
61+
}
62+
}
63+
64+
const globalSelectedEnvPath = this.extensionContext.globalState.get<string | undefined>(MEMENTO_KEY);
65+
if (globalSelectedEnvPath) {
66+
return { environmentPath: globalSelectedEnvPath, reason: 'globalUserSelected' };
67+
}
68+
return this.api && workspace.isTrusted
69+
? {
70+
environmentPath: this.api.getActiveEnvironmentPath(resource).path,
71+
reason: 'defaultRecommended',
72+
}
73+
: undefined;
74+
}
75+
}
76+
77+
function getDataToStore(environmentPath: string | undefined, uri: Uri | undefined): string | undefined {
78+
if (!workspace.workspaceFolders?.length) {
79+
return environmentPath;
80+
}
81+
const workspaceUri = (
82+
(uri ? workspace.getWorkspaceFolder(uri)?.uri : undefined) || workspace.workspaceFolders[0].uri
83+
).toString();
84+
const existingData = getWorkspaceStateValue<string>(MEMENTO_KEY);
85+
if (!existingData) {
86+
return JSON.stringify(environmentPath ? { [workspaceUri]: environmentPath } : {});
87+
}
88+
try {
89+
const existingJson: Record<string, string> = JSON.parse(existingData);
90+
if (environmentPath) {
91+
existingJson[workspaceUri] = environmentPath;
92+
} else {
93+
delete existingJson[workspaceUri];
94+
}
95+
return JSON.stringify(existingJson);
96+
} catch (ex) {
97+
traceError('Failed to parse existing workspace state value for preferred environment', ex);
98+
return JSON.stringify({
99+
[workspaceUri]: environmentPath,
100+
});
101+
}
102+
}

src/client/interpreter/configuration/types.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { ConfigurationTarget, Disposable, QuickPickItem, Uri } from 'vscode';
22
import { Resource } from '../../common/types';
33
import { PythonEnvironment } from '../../pythonEnvironments/info';
4+
import { PythonExtension } from '../../api/types';
45

56
export interface IPythonPathUpdaterService {
67
updatePythonPath(pythonPath: string | undefined): Promise<void>;
@@ -96,3 +97,14 @@ export interface IInterpreterQuickPick {
9697
params?: InterpreterQuickPickParams,
9798
): Promise<string | undefined>;
9899
}
100+
101+
export const IRecommendedEnvironmentService = Symbol('IRecommendedEnvironmentService');
102+
export interface IRecommendedEnvironmentService {
103+
registerEnvApi(api: PythonExtension['environments']): void;
104+
trackUserSelectedEnvironment(environmentPath: string | undefined, uri: Uri | undefined): void;
105+
getRecommededEnvironment(
106+
resource: Resource,
107+
):
108+
| { environmentPath: string; reason: 'globalUserSelected' | 'workspaceUserSelected' | 'defaultRecommended' }
109+
| undefined;
110+
}

src/client/interpreter/serviceRegistry.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,14 @@ import { InstallPythonViaTerminal } from './configuration/interpreterSelector/co
1616
import { ResetInterpreterCommand } from './configuration/interpreterSelector/commands/resetInterpreter';
1717
import { SetInterpreterCommand } from './configuration/interpreterSelector/commands/setInterpreter';
1818
import { InterpreterSelector } from './configuration/interpreterSelector/interpreterSelector';
19+
import { RecommendedEnvironmentService } from './configuration/recommededEnvironmentService';
1920
import { PythonPathUpdaterService } from './configuration/pythonPathUpdaterService';
2021
import { PythonPathUpdaterServiceFactory } from './configuration/pythonPathUpdaterServiceFactory';
2122
import {
2223
IInterpreterComparer,
2324
IInterpreterQuickPick,
2425
IInterpreterSelector,
26+
IRecommendedEnvironmentService,
2527
IPythonPathUpdaterServiceFactory,
2628
IPythonPathUpdaterServiceManager,
2729
} from './configuration/types';
@@ -59,6 +61,10 @@ export function registerInterpreterTypes(serviceManager: IServiceManager): void
5961
IExtensionSingleActivationService,
6062
ResetInterpreterCommand,
6163
);
64+
serviceManager.addSingleton<IRecommendedEnvironmentService>(
65+
IRecommendedEnvironmentService,
66+
RecommendedEnvironmentService,
67+
);
6268
serviceManager.addSingleton(IInterpreterQuickPick, SetInterpreterCommand);
6369

6470
serviceManager.addSingleton<IExtensionActivationService>(IExtensionActivationService, VirtualEnvironmentPrompt);

0 commit comments

Comments
 (0)