Skip to content

Commit 36a789c

Browse files
committed
feat: add disambiguation suffixes for Python environments in env manager
1 parent 7cea36b commit 36a789c

File tree

3 files changed

+440
-233
lines changed

3 files changed

+440
-233
lines changed

src/features/views/envManagersView.ts

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
EnvManagerTreeItem,
1818
EnvTreeItem,
1919
EnvTreeItemKind,
20+
getEnvironmentParentDirName,
2021
NoPythonEnvTreeItem,
2122
PackageTreeItem,
2223
PythonEnvTreeItem,
@@ -27,6 +28,53 @@ const COPIED_STATE = 'copied';
2728
const SELECTED_STATE = 'selected';
2829
const ENV_STATE_KEYS = [COPIED_STATE, SELECTED_STATE];
2930

31+
/**
32+
* Extracts the base name from a display name by removing version info.
33+
* @example getBaseName('.venv (3.12)') returns '.venv'
34+
* @example getBaseName('myenv (3.14.1)') returns 'myenv'
35+
*/
36+
function getBaseName(displayName: string): string {
37+
return displayName.replace(/\s*\([0-9.]+\)\s*$/, '').trim();
38+
}
39+
40+
/**
41+
* Computes disambiguation suffixes for environments with similar base names.
42+
*
43+
* When multiple environments share the same base name (ignoring version numbers),
44+
* this function returns a map from environment ID to the parent folder name,
45+
* which can be displayed to help users distinguish between them.
46+
*
47+
* @example Two environments '.venv (3.12)' in folders 'alice' and 'bob' would
48+
* return suffixes 'alice' and 'bob' respectively.
49+
*
50+
* @param envs List of environments to analyze
51+
* @returns Map from environment ID to disambiguation suffix (parent folder name)
52+
*/
53+
function computeDisambiguationSuffixes(envs: PythonEnvironment[]): Map<string, string> {
54+
const suffixes = new Map<string, string>();
55+
56+
// Group environments by their base name (ignoring version)
57+
const baseNameToEnvs = new Map<string, PythonEnvironment[]>();
58+
for (const env of envs) {
59+
const displayName = env.displayName ?? env.name;
60+
const baseName = getBaseName(displayName);
61+
const existing = baseNameToEnvs.get(baseName) ?? [];
62+
existing.push(env);
63+
baseNameToEnvs.set(baseName, existing);
64+
}
65+
66+
// For base names with multiple environments, compute suffixes
67+
for (const [, similarEnvs] of baseNameToEnvs) {
68+
if (similarEnvs.length > 1) {
69+
for (const env of similarEnvs) {
70+
suffixes.set(env.envId.id, getEnvironmentParentDirName(env));
71+
}
72+
}
73+
}
74+
75+
return suffixes;
76+
}
77+
3078
export class EnvManagerView implements TreeDataProvider<EnvTreeItem>, Disposable {
3179
private treeView: TreeView<EnvTreeItem>;
3280
private treeDataChanged: EventEmitter<EnvTreeItem | EnvTreeItem[] | null | undefined> = new EventEmitter<
@@ -37,7 +85,10 @@ export class EnvManagerView implements TreeDataProvider<EnvTreeItem>, Disposable
3785
private selected: Map<string, string> = new Map();
3886
private disposables: Disposable[] = [];
3987

40-
public constructor(public providers: EnvironmentManagers, private stateManager: ITemporaryStateManager) {
88+
public constructor(
89+
public providers: EnvironmentManagers,
90+
private stateManager: ITemporaryStateManager,
91+
) {
4192
this.treeView = window.createTreeView<EnvTreeItem>('env-managers', {
4293
treeDataProvider: this,
4394
});
@@ -119,8 +170,16 @@ export class EnvManagerView implements TreeDataProvider<EnvTreeItem>, Disposable
119170
const manager = (element as EnvManagerTreeItem).manager;
120171
const views: EnvTreeItem[] = [];
121172
const envs = await manager.getEnvironments('all');
122-
envs.filter((e) => !e.group).forEach((env) => {
123-
const view = new PythonEnvTreeItem(env, element as EnvManagerTreeItem, this.selected.get(env.envId.id));
173+
const nonGroupedEnvs = envs.filter((e) => !e.group);
174+
const disambiguationSuffixes = computeDisambiguationSuffixes(nonGroupedEnvs);
175+
nonGroupedEnvs.forEach((env) => {
176+
const suffix = disambiguationSuffixes.get(env.envId.id);
177+
const view = new PythonEnvTreeItem(
178+
env,
179+
element as EnvManagerTreeItem,
180+
this.selected.get(env.envId.id),
181+
suffix,
182+
);
124183
views.push(view);
125184
this.revealMap.set(env.envId.id, view);
126185
});
@@ -162,8 +221,10 @@ export class EnvManagerView implements TreeDataProvider<EnvTreeItem>, Disposable
162221
return false;
163222
});
164223

224+
const groupDisambiguationSuffixes = computeDisambiguationSuffixes(grouped);
165225
grouped.forEach((env) => {
166-
const view = new PythonEnvTreeItem(env, groupItem, this.selected.get(env.envId.id));
226+
const suffix = groupDisambiguationSuffixes.get(env.envId.id);
227+
const view = new PythonEnvTreeItem(env, groupItem, this.selected.get(env.envId.id), suffix);
167228
views.push(view);
168229
this.revealMap.set(env.envId.id, view);
169230
});

src/features/views/treeViewItems.ts

Lines changed: 67 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,43 @@ import { InternalEnvironmentManager, InternalPackageManager } from '../../intern
55
import { isActivatableEnvironment } from '../common/activation';
66
import { removable } from './utils';
77

8+
/**
9+
* Extracts the parent folder name from an environment path for disambiguation.
10+
*
11+
* This function handles various path formats including:
12+
* - Unix paths with bin folder: /home/user/my-project/.venv/bin/python → my-project
13+
* - Windows paths with Scripts folder: C:\Users\bob\project\.venv\Scripts\python.exe → project
14+
* - Direct venv folder paths: /home/user/project/.venv → project
15+
*
16+
* @param environment The Python environment to extract the parent folder from
17+
* @returns The name of the parent folder containing the virtual environment
18+
*/
19+
export function getEnvironmentParentDirName(environment: PythonEnvironment): string {
20+
const envPath = environment.environmentPath.fsPath.replace(/\\/g, '/');
21+
const parts = envPath.split('/').filter((p) => p.length > 0);
22+
23+
let venvFolderIndex = -1;
24+
25+
for (let i = parts.length - 1; i >= 0; i--) {
26+
const part = parts[i].toLowerCase();
27+
if (part === 'bin' || part === 'scripts') {
28+
venvFolderIndex = i - 1;
29+
break;
30+
}
31+
if (part.startsWith('python')) {
32+
continue;
33+
}
34+
venvFolderIndex = i;
35+
break;
36+
}
37+
38+
if (venvFolderIndex > 0) {
39+
return parts[venvFolderIndex - 1];
40+
}
41+
42+
return parts.length >= 2 ? parts[parts.length - 2] : parts[0] || '';
43+
}
44+
845
export enum EnvTreeItemKind {
946
manager = 'python-env-manager',
1047
environment = 'python-env',
@@ -45,7 +82,10 @@ export class EnvManagerTreeItem implements EnvTreeItem {
4582
export class PythonGroupEnvTreeItem implements EnvTreeItem {
4683
public readonly kind = EnvTreeItemKind.environmentGroup;
4784
public readonly treeItem: TreeItem;
48-
constructor(public readonly parent: EnvManagerTreeItem, public readonly group: string | EnvironmentGroupInfo) {
85+
constructor(
86+
public readonly parent: EnvManagerTreeItem,
87+
public readonly group: string | EnvironmentGroupInfo,
88+
) {
4989
const label = typeof group === 'string' ? group : group.name;
5090
const item = new TreeItem(label, TreeItemCollapsibleState.Collapsed);
5191
item.contextValue = `pythonEnvGroup;${this.parent.manager.id}:${label};`;
@@ -62,12 +102,21 @@ export class PythonGroupEnvTreeItem implements EnvTreeItem {
62102
export class PythonEnvTreeItem implements EnvTreeItem {
63103
public readonly kind = EnvTreeItemKind.environment;
64104
public readonly treeItem: TreeItem;
105+
/**
106+
* Creates a tree item for a Python environment.
107+
* @param environment The Python environment to display
108+
* @param parent The parent tree item (manager or group)
109+
* @param selected If set, indicates this environment is selected ('global' or workspace path)
110+
* @param disambiguationSuffix If set, shown in description to distinguish similarly-named environments
111+
*/
65112
constructor(
66113
public readonly environment: PythonEnvironment,
67114
public readonly parent: EnvManagerTreeItem | PythonGroupEnvTreeItem,
68115
public readonly selected?: string,
116+
public readonly disambiguationSuffix?: string,
69117
) {
70-
let name = environment.displayName ?? environment.name;
118+
const name = environment.displayName ?? environment.name;
119+
71120
let tooltip = environment.tooltip ?? environment.description;
72121
if (selected) {
73122
const selectedTooltip =
@@ -77,7 +126,18 @@ export class PythonEnvTreeItem implements EnvTreeItem {
77126

78127
const item = new TreeItem(name, TreeItemCollapsibleState.Collapsed);
79128
item.contextValue = this.getContextValue();
80-
item.description = environment.description;
129+
130+
// Build description with optional [uv] indicator and disambiguation suffix
131+
const uvIndicator = environment.description?.toLowerCase().includes('uv') ? '[uv]' : '';
132+
const descriptionParts: string[] = [];
133+
if (uvIndicator) {
134+
descriptionParts.push(uvIndicator);
135+
}
136+
if (disambiguationSuffix) {
137+
descriptionParts.push(disambiguationSuffix);
138+
}
139+
item.description = descriptionParts.length > 0 ? descriptionParts.join(' ') : undefined;
140+
81141
item.tooltip = tooltip;
82142
item.iconPath = environment.iconPath;
83143
this.treeItem = item;
@@ -240,7 +300,10 @@ export class ProjectEnvironment implements ProjectTreeItem {
240300
public readonly kind = ProjectTreeItemKind.environment;
241301
public readonly id: string;
242302
public readonly treeItem: TreeItem;
243-
constructor(public readonly parent: ProjectItem, public readonly environment: PythonEnvironment) {
303+
constructor(
304+
public readonly parent: ProjectItem,
305+
public readonly environment: PythonEnvironment,
306+
) {
244307
this.id = this.getId(parent, environment);
245308
const item = new TreeItem(
246309
this.environment.displayName ?? this.environment.name,

0 commit comments

Comments
 (0)