Skip to content

Commit 10f3105

Browse files
authored
feat: add disambiguation suffixes for Python environments in env manager (#1197)
fixes #1196
1 parent 451851c commit 10f3105

File tree

3 files changed

+430
-231
lines changed

3 files changed

+430
-231
lines changed

src/features/views/envManagersView.ts

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
EnvManagerTreeItem,
1919
EnvTreeItem,
2020
EnvTreeItemKind,
21+
getEnvironmentParentDirName,
2122
NoPythonEnvTreeItem,
2223
PackageTreeItem,
2324
PythonEnvTreeItem,
@@ -28,6 +29,53 @@ const COPIED_STATE = 'copied';
2829
const SELECTED_STATE = 'selected';
2930
const ENV_STATE_KEYS = [COPIED_STATE, SELECTED_STATE];
3031

32+
/**
33+
* Extracts the base name from a display name by removing version info.
34+
* @example getBaseName('.venv (3.12)') returns '.venv'
35+
* @example getBaseName('myenv (3.14.1)') returns 'myenv'
36+
*/
37+
function getBaseName(displayName: string): string {
38+
return displayName.replace(/\s*\([0-9.]+\)\s*$/, '').trim();
39+
}
40+
41+
/**
42+
* Computes disambiguation suffixes for environments with similar base names.
43+
*
44+
* When multiple environments share the same base name (ignoring version numbers),
45+
* this function returns a map from environment ID to the parent folder name,
46+
* which can be displayed to help users distinguish between them.
47+
*
48+
* @example Two environments '.venv (3.12)' in folders 'alice' and 'bob' would
49+
* return suffixes 'alice' and 'bob' respectively.
50+
*
51+
* @param envs List of environments to analyze
52+
* @returns Map from environment ID to disambiguation suffix (parent folder name)
53+
*/
54+
function computeDisambiguationSuffixes(envs: PythonEnvironment[]): Map<string, string> {
55+
const suffixes = new Map<string, string>();
56+
57+
// Group environments by their base name (ignoring version)
58+
const baseNameToEnvs = new Map<string, PythonEnvironment[]>();
59+
for (const env of envs) {
60+
const displayName = env.displayName ?? env.name;
61+
const baseName = getBaseName(displayName);
62+
const existing = baseNameToEnvs.get(baseName) ?? [];
63+
existing.push(env);
64+
baseNameToEnvs.set(baseName, existing);
65+
}
66+
67+
// For base names with multiple environments, compute suffixes
68+
for (const [, similarEnvs] of baseNameToEnvs) {
69+
if (similarEnvs.length > 1) {
70+
for (const env of similarEnvs) {
71+
suffixes.set(env.envId.id, getEnvironmentParentDirName(env));
72+
}
73+
}
74+
}
75+
76+
return suffixes;
77+
}
78+
3179
export class EnvManagerView implements TreeDataProvider<EnvTreeItem>, Disposable {
3280
private treeView: TreeView<EnvTreeItem>;
3381
private treeDataChanged: EventEmitter<EnvTreeItem | EnvTreeItem[] | null | undefined> = new EventEmitter<
@@ -126,8 +174,16 @@ export class EnvManagerView implements TreeDataProvider<EnvTreeItem>, Disposable
126174
const manager = (element as EnvManagerTreeItem).manager;
127175
const views: EnvTreeItem[] = [];
128176
const envs = await manager.getEnvironments('all');
129-
envs.filter((e) => !e.group).forEach((env) => {
130-
const view = new PythonEnvTreeItem(env, element as EnvManagerTreeItem, this.selected.get(env.envId.id));
177+
const nonGroupedEnvs = envs.filter((e) => !e.group);
178+
const disambiguationSuffixes = computeDisambiguationSuffixes(nonGroupedEnvs);
179+
nonGroupedEnvs.forEach((env) => {
180+
const suffix = disambiguationSuffixes.get(env.envId.id);
181+
const view = new PythonEnvTreeItem(
182+
env,
183+
element as EnvManagerTreeItem,
184+
this.selected.get(env.envId.id),
185+
suffix,
186+
);
131187
views.push(view);
132188
this.revealMap.set(env.envId.id, view);
133189
});
@@ -172,8 +228,10 @@ export class EnvManagerView implements TreeDataProvider<EnvTreeItem>, Disposable
172228
return false;
173229
});
174230

231+
const groupDisambiguationSuffixes = computeDisambiguationSuffixes(grouped);
175232
grouped.forEach((env) => {
176-
const view = new PythonEnvTreeItem(env, groupItem, this.selected.get(env.envId.id));
233+
const suffix = groupDisambiguationSuffixes.get(env.envId.id);
234+
const view = new PythonEnvTreeItem(env, groupItem, this.selected.get(env.envId.id), suffix);
177235
views.push(view);
178236
this.revealMap.set(env.envId.id, view);
179237
});

src/features/views/treeViewItems.ts

Lines changed: 61 additions & 3 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',
@@ -65,12 +102,21 @@ export class PythonGroupEnvTreeItem implements EnvTreeItem {
65102
export class PythonEnvTreeItem implements EnvTreeItem {
66103
public readonly kind = EnvTreeItemKind.environment;
67104
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+
*/
68112
constructor(
69113
public readonly environment: PythonEnvironment,
70114
public readonly parent: EnvManagerTreeItem | PythonGroupEnvTreeItem,
71115
public readonly selected?: string,
116+
public readonly disambiguationSuffix?: string,
72117
) {
73-
let name = environment.displayName ?? environment.name;
118+
const name = environment.displayName ?? environment.name;
119+
74120
let tooltip = environment.tooltip ?? environment.description;
75121
const isBroken = !!environment.error;
76122

@@ -82,8 +128,20 @@ export class PythonEnvTreeItem implements EnvTreeItem {
82128

83129
const item = new TreeItem(name, TreeItemCollapsibleState.Collapsed);
84130
item.contextValue = this.getContextValue();
85-
// Show error message for broken environments
86-
item.description = isBroken ? environment.error : environment.description;
131+
132+
// Build description with optional [uv] indicator and disambiguation suffix
133+
const uvIndicator = environment.description?.toLowerCase().includes('uv') ? '[uv]' : '';
134+
const descriptionParts: string[] = [];
135+
if (uvIndicator) {
136+
descriptionParts.push(uvIndicator);
137+
}
138+
if (disambiguationSuffix) {
139+
descriptionParts.push(disambiguationSuffix);
140+
}
141+
const computedDescription = descriptionParts.length > 0 ? descriptionParts.join(' ') : undefined;
142+
143+
// Use error message for broken environments, otherwise use computed description
144+
item.description = isBroken ? environment.error : computedDescription;
87145
item.tooltip = isBroken ? environment.error : tooltip;
88146
// Show warning icon for broken environments
89147
item.iconPath = isBroken ? new ThemeIcon('warning') : environment.iconPath;

0 commit comments

Comments
 (0)