Skip to content

Commit 48fdb1c

Browse files
committed
Adds group feature environment manager UI
1 parent a2bfe2b commit 48fdb1c

File tree

6 files changed

+121
-8
lines changed

6 files changed

+121
-8
lines changed

src/api.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,33 @@ export interface PythonEnvironmentId {
143143
managerId: string;
144144
}
145145

146+
/**
147+
* Display information for an environment group.
148+
*/
149+
export interface EnvironmentGroupInfo {
150+
/**
151+
* The name of the environment group. This is used as an identifier for the group.
152+
*
153+
* Note: The first instance of the group with the given name will be used in the UI.
154+
*/
155+
readonly name: string;
156+
157+
/**
158+
* The description of the environment group.
159+
*/
160+
readonly description?: string;
161+
162+
/**
163+
* The tooltip for the environment group, which can be a string or a Markdown string.
164+
*/
165+
readonly tooltip?: string | MarkdownString;
166+
167+
/**
168+
* The icon path for the environment group, which can be a string, Uri, or an object with light and dark theme paths.
169+
*/
170+
readonly iconPath?: IconPath;
171+
}
172+
146173
/**
147174
* Interface representing information about a Python environment.
148175
*/
@@ -202,6 +229,11 @@ export interface PythonEnvironmentInfo {
202229
* This is required by extension like Jupyter, Pylance, and other extensions to provide better experience with python.
203230
*/
204231
readonly sysPrefix: string;
232+
233+
/**
234+
* Optional `group` for this environment. This is used to group environments in the Environment Manager UI.
235+
*/
236+
readonly group?: string | EnvironmentGroupInfo;
205237
}
206238

207239
/**

src/features/envCommands.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
ProjectEnvironment,
3030
ProjectPackageRootTreeItem,
3131
GlobalProjectItem,
32+
EnvTreeItemKind,
3233
} from './views/treeViewItems';
3334
import { Common } from '../common/localize';
3435
import { pickEnvironment } from '../common/pickers/environments';
@@ -156,7 +157,8 @@ export async function createAnyEnvironmentCommand(
156157
export async function removeEnvironmentCommand(context: unknown, managers: EnvironmentManagers): Promise<void> {
157158
if (context instanceof PythonEnvTreeItem) {
158159
const view = context as PythonEnvTreeItem;
159-
const manager = view.parent.manager;
160+
const manager =
161+
view.parent.kind === EnvTreeItemKind.environmentGroup ? view.parent.parent.manager : view.parent.manager;
160162
await manager.remove(view.environment);
161163
} else if (context instanceof Uri) {
162164
const manager = managers.getEnvironmentManager(context as Uri);

src/features/views/envManagersView.ts

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Disposable, Event, EventEmitter, ProviderResult, TreeDataProvider, TreeItem, TreeView, window } from 'vscode';
2-
import { PythonEnvironment } from '../../api';
2+
import { EnvironmentGroupInfo, PythonEnvironment } from '../../api';
33
import {
44
DidChangeEnvironmentManagerEventArgs,
55
DidChangePackageManagerEventArgs,
@@ -19,6 +19,7 @@ import {
1919
NoPythonEnvTreeItem,
2020
EnvInfoTreeItem,
2121
PackageRootInfoTreeItem,
22+
PythonGroupEnvTreeItem,
2223
} from './treeViewItems';
2324
import { createSimpleDebounce } from '../../common/utils/debounce';
2425
import { ProjectViews } from '../../common/localize';
@@ -97,21 +98,66 @@ export class EnvManagerView implements TreeDataProvider<EnvTreeItem>, Disposable
9798
const manager = (element as EnvManagerTreeItem).manager;
9899
const views: EnvTreeItem[] = [];
99100
const envs = await manager.getEnvironments('all');
100-
envs.forEach((env) => {
101+
envs.filter((e) => !e.group).forEach((env) => {
101102
const view = new PythonEnvTreeItem(env, element as EnvManagerTreeItem);
102103
views.push(view);
103104
this.revealMap.set(env.envId.id, view);
104105
});
105106

107+
const groups: string[] = [];
108+
const groupObjects: (string | EnvironmentGroupInfo)[] = [];
109+
envs.filter((e) => e.group).forEach((env) => {
110+
const name =
111+
env.group && typeof env.group === 'string' ? env.group : (env.group as EnvironmentGroupInfo).name;
112+
if (name && !groups.includes(name)) {
113+
groups.push(name);
114+
groupObjects.push(env.group as EnvironmentGroupInfo);
115+
}
116+
});
117+
118+
groupObjects.forEach((group) => {
119+
views.push(new PythonGroupEnvTreeItem(element as EnvManagerTreeItem, group));
120+
});
121+
106122
if (views.length === 0) {
107123
views.push(new NoPythonEnvTreeItem(element as EnvManagerTreeItem));
108124
}
109125
return views;
110126
}
111127

128+
if (element.kind === EnvTreeItemKind.environmentGroup) {
129+
const groupItem = element as PythonGroupEnvTreeItem;
130+
const manager = groupItem.parent.manager;
131+
const views: EnvTreeItem[] = [];
132+
const envs = await manager.getEnvironments('all');
133+
const groupName =
134+
typeof groupItem.group === 'string' ? groupItem.group : (groupItem.group as EnvironmentGroupInfo).name;
135+
const grouped = envs.filter((e) => {
136+
if (e.group) {
137+
const name =
138+
e.group && typeof e.group === 'string' ? e.group : (e.group as EnvironmentGroupInfo).name;
139+
return name === groupName;
140+
}
141+
return false;
142+
});
143+
144+
grouped.forEach((env) => {
145+
const view = new PythonEnvTreeItem(env, groupItem);
146+
views.push(view);
147+
this.revealMap.set(env.envId.id, view);
148+
});
149+
150+
return views;
151+
}
152+
112153
if (element.kind === EnvTreeItemKind.environment) {
113-
const environment = (element as PythonEnvTreeItem).environment;
114-
const envManager = (element as PythonEnvTreeItem).parent.manager;
154+
const pythonEnvItem = element as PythonEnvTreeItem;
155+
const environment = pythonEnvItem.environment;
156+
const envManager =
157+
pythonEnvItem.parent.kind === EnvTreeItemKind.environmentGroup
158+
? pythonEnvItem.parent.parent.manager
159+
: pythonEnvItem.parent.manager;
160+
115161
const pkgManager = this.getSupportedPackageManager(envManager);
116162
const parent = element as PythonEnvTreeItem;
117163
const views: EnvTreeItem[] = [];

src/features/views/treeViewItems.ts

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import { TreeItem, TreeItemCollapsibleState, MarkdownString, Command, ThemeIcon, Uri } from 'vscode';
22
import { InternalEnvironmentManager, InternalPackageManager } from '../../internal.api';
3-
import { PythonEnvironment, IconPath, Package, PythonProject } from '../../api';
3+
import { PythonEnvironment, IconPath, Package, PythonProject, EnvironmentGroupInfo } from '../../api';
44
import { removable } from './utils';
55
import { isActivatableEnvironment } from '../common/activation';
66

77
export enum EnvTreeItemKind {
88
manager = 'python-env-manager',
99
environment = 'python-env',
10+
environmentGroup = 'python-env-group',
1011
noEnvironment = 'python-no-env',
1112
package = 'python-package',
1213
packageRoot = 'python-package-root',
@@ -51,10 +52,30 @@ export class EnvManagerTreeItem implements EnvTreeItem {
5152
}
5253
}
5354

55+
export class PythonGroupEnvTreeItem implements EnvTreeItem {
56+
public readonly kind = EnvTreeItemKind.environmentGroup;
57+
public readonly treeItem: TreeItem;
58+
constructor(public readonly parent: EnvManagerTreeItem, public readonly group: string | EnvironmentGroupInfo) {
59+
const label = typeof group === 'string' ? group : group.name;
60+
const item = new TreeItem(label, TreeItemCollapsibleState.Collapsed);
61+
item.contextValue = 'pythonEnvGroup';
62+
this.treeItem = item;
63+
64+
if (typeof group !== 'string') {
65+
item.description = group.description;
66+
item.tooltip = group.tooltip;
67+
item.iconPath = group.iconPath;
68+
}
69+
}
70+
}
71+
5472
export class PythonEnvTreeItem implements EnvTreeItem {
5573
public readonly kind = EnvTreeItemKind.environment;
5674
public readonly treeItem: TreeItem;
57-
constructor(public readonly environment: PythonEnvironment, public readonly parent: EnvManagerTreeItem) {
75+
constructor(
76+
public readonly environment: PythonEnvironment,
77+
public readonly parent: EnvManagerTreeItem | PythonGroupEnvTreeItem,
78+
) {
5879
const item = new TreeItem(environment.displayName ?? environment.name, TreeItemCollapsibleState.Collapsed);
5980
item.contextValue = this.getContextValue();
6081
item.description = environment.description;
@@ -65,7 +86,12 @@ export class PythonEnvTreeItem implements EnvTreeItem {
6586

6687
private getContextValue() {
6788
const activatable = isActivatableEnvironment(this.environment) ? '-activatable' : '';
68-
const remove = this.parent.manager.supportsRemove ? '-remove' : '';
89+
let remove = '';
90+
if (this.parent.kind === EnvTreeItemKind.environmentGroup) {
91+
remove = this.parent.parent.manager.supportsRemove ? '-remove' : '';
92+
} else if (this.parent.kind === EnvTreeItemKind.manager) {
93+
remove = this.parent.manager.supportsRemove ? '-remove' : '';
94+
}
6995
return `pythonEnvironment${remove}${activatable}`;
7096
}
7197

src/internal.api.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
ResolveEnvironmentContext,
2525
PackageInstallOptions,
2626
Installable,
27+
EnvironmentGroupInfo,
2728
} from './api';
2829
import { CreateEnvironmentNotSupported, RemoveEnvironmentNotSupported } from './common/errors/NotSupportedError';
2930

@@ -268,6 +269,7 @@ export class PythonEnvironmentImpl implements PythonEnvironment {
268269
public readonly iconPath?: IconPath;
269270
public readonly execInfo: PythonEnvironmentExecutionInfo;
270271
public readonly sysPrefix: string;
272+
public readonly group?: string | EnvironmentGroupInfo;
271273

272274
constructor(public readonly envId: PythonEnvironmentId, info: PythonEnvironmentInfo) {
273275
this.name = info.name;
@@ -281,6 +283,7 @@ export class PythonEnvironmentImpl implements PythonEnvironment {
281283
this.iconPath = info.iconPath;
282284
this.execInfo = info.execInfo;
283285
this.sysPrefix = info.sysPrefix;
286+
this.group = info.group;
284287
}
285288
}
286289

src/managers/conda/condaUtils.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,7 @@ function nativeToPythonEnv(
270270
activation: [{ executable: conda, args: ['activate', e.prefix] }],
271271
deactivation: [{ executable: conda, args: ['deactivate'] }],
272272
},
273+
group: 'Prefix',
273274
},
274275
manager,
275276
);
@@ -297,6 +298,7 @@ function nativeToPythonEnv(
297298
activation: [{ executable: conda, args: ['activate', name] }],
298299
deactivation: [{ executable: conda, args: ['deactivate'] }],
299300
},
301+
group: 'Named',
300302
},
301303
manager,
302304
);
@@ -490,6 +492,7 @@ async function createNamedCondaEnvironment(
490492
run: { executable: path.join(envPath, bin) },
491493
},
492494
sysPrefix: envPath,
495+
group: 'Named',
493496
},
494497
manager,
495498
);
@@ -565,6 +568,7 @@ async function createPrefixCondaEnvironment(
565568
deactivation: [{ executable: 'conda', args: ['deactivate'] }],
566569
},
567570
sysPrefix: prefix,
571+
group: 'Prefix',
568572
},
569573
manager,
570574
);

0 commit comments

Comments
 (0)