Skip to content

Adds group feature environment manager UI #101

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jan 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,33 @@ export interface PythonEnvironmentId {
managerId: string;
}

/**
* Display information for an environment group.
*/
export interface EnvironmentGroupInfo {
/**
* The name of the environment group. This is used as an identifier for the group.
*
* Note: The first instance of the group with the given name will be used in the UI.
*/
readonly name: string;

/**
* The description of the environment group.
*/
readonly description?: string;

/**
* The tooltip for the environment group, which can be a string or a Markdown string.
*/
readonly tooltip?: string | MarkdownString;

/**
* The icon path for the environment group, which can be a string, Uri, or an object with light and dark theme paths.
*/
readonly iconPath?: IconPath;
}

/**
* Interface representing information about a Python environment.
*/
Expand Down Expand Up @@ -202,6 +229,11 @@ export interface PythonEnvironmentInfo {
* This is required by extension like Jupyter, Pylance, and other extensions to provide better experience with python.
*/
readonly sysPrefix: string;

/**
* Optional `group` for this environment. This is used to group environments in the Environment Manager UI.
*/
readonly group?: string | EnvironmentGroupInfo;
}

/**
Expand Down
4 changes: 3 additions & 1 deletion src/features/envCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
ProjectEnvironment,
ProjectPackageRootTreeItem,
GlobalProjectItem,
EnvTreeItemKind,
} from './views/treeViewItems';
import { Common } from '../common/localize';
import { pickEnvironment } from '../common/pickers/environments';
Expand Down Expand Up @@ -156,7 +157,8 @@ export async function createAnyEnvironmentCommand(
export async function removeEnvironmentCommand(context: unknown, managers: EnvironmentManagers): Promise<void> {
if (context instanceof PythonEnvTreeItem) {
const view = context as PythonEnvTreeItem;
const manager = view.parent.manager;
const manager =
view.parent.kind === EnvTreeItemKind.environmentGroup ? view.parent.parent.manager : view.parent.manager;
await manager.remove(view.environment);
} else if (context instanceof Uri) {
const manager = managers.getEnvironmentManager(context as Uri);
Expand Down
54 changes: 50 additions & 4 deletions src/features/views/envManagersView.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Disposable, Event, EventEmitter, ProviderResult, TreeDataProvider, TreeItem, TreeView, window } from 'vscode';
import { PythonEnvironment } from '../../api';
import { EnvironmentGroupInfo, PythonEnvironment } from '../../api';
import {
DidChangeEnvironmentManagerEventArgs,
DidChangePackageManagerEventArgs,
Expand All @@ -19,6 +19,7 @@ import {
NoPythonEnvTreeItem,
EnvInfoTreeItem,
PackageRootInfoTreeItem,
PythonGroupEnvTreeItem,
} from './treeViewItems';
import { createSimpleDebounce } from '../../common/utils/debounce';
import { ProjectViews } from '../../common/localize';
Expand Down Expand Up @@ -97,21 +98,66 @@ export class EnvManagerView implements TreeDataProvider<EnvTreeItem>, Disposable
const manager = (element as EnvManagerTreeItem).manager;
const views: EnvTreeItem[] = [];
const envs = await manager.getEnvironments('all');
envs.forEach((env) => {
envs.filter((e) => !e.group).forEach((env) => {
const view = new PythonEnvTreeItem(env, element as EnvManagerTreeItem);
views.push(view);
this.revealMap.set(env.envId.id, view);
});

const groups: string[] = [];
const groupObjects: (string | EnvironmentGroupInfo)[] = [];
envs.filter((e) => e.group).forEach((env) => {
const name =
env.group && typeof env.group === 'string' ? env.group : (env.group as EnvironmentGroupInfo).name;
if (name && !groups.includes(name)) {
groups.push(name);
groupObjects.push(env.group as EnvironmentGroupInfo);
}
});

groupObjects.forEach((group) => {
views.push(new PythonGroupEnvTreeItem(element as EnvManagerTreeItem, group));
});

if (views.length === 0) {
views.push(new NoPythonEnvTreeItem(element as EnvManagerTreeItem));
}
return views;
}

if (element.kind === EnvTreeItemKind.environmentGroup) {
const groupItem = element as PythonGroupEnvTreeItem;
const manager = groupItem.parent.manager;
const views: EnvTreeItem[] = [];
const envs = await manager.getEnvironments('all');
const groupName =
typeof groupItem.group === 'string' ? groupItem.group : (groupItem.group as EnvironmentGroupInfo).name;
const grouped = envs.filter((e) => {
if (e.group) {
const name =
e.group && typeof e.group === 'string' ? e.group : (e.group as EnvironmentGroupInfo).name;
return name === groupName;
}
return false;
});

grouped.forEach((env) => {
const view = new PythonEnvTreeItem(env, groupItem);
views.push(view);
this.revealMap.set(env.envId.id, view);
});

return views;
}

if (element.kind === EnvTreeItemKind.environment) {
const environment = (element as PythonEnvTreeItem).environment;
const envManager = (element as PythonEnvTreeItem).parent.manager;
const pythonEnvItem = element as PythonEnvTreeItem;
const environment = pythonEnvItem.environment;
const envManager =
pythonEnvItem.parent.kind === EnvTreeItemKind.environmentGroup
? pythonEnvItem.parent.parent.manager
: pythonEnvItem.parent.manager;

const pkgManager = this.getSupportedPackageManager(envManager);
const parent = element as PythonEnvTreeItem;
const views: EnvTreeItem[] = [];
Expand Down
34 changes: 30 additions & 4 deletions src/features/views/treeViewItems.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { TreeItem, TreeItemCollapsibleState, MarkdownString, Command, ThemeIcon } from 'vscode';
import { InternalEnvironmentManager, InternalPackageManager } from '../../internal.api';
import { PythonEnvironment, IconPath, Package, PythonProject } from '../../api';
import { PythonEnvironment, IconPath, Package, PythonProject, EnvironmentGroupInfo } from '../../api';
import { removable } from './utils';
import { isActivatableEnvironment } from '../common/activation';

export enum EnvTreeItemKind {
manager = 'python-env-manager',
environment = 'python-env',
environmentGroup = 'python-env-group',
noEnvironment = 'python-no-env',
package = 'python-package',
packageRoot = 'python-package-root',
Expand Down Expand Up @@ -42,11 +43,31 @@ export class EnvManagerTreeItem implements EnvTreeItem {
}
}

export class PythonGroupEnvTreeItem implements EnvTreeItem {
public readonly kind = EnvTreeItemKind.environmentGroup;
public readonly treeItem: TreeItem;
constructor(public readonly parent: EnvManagerTreeItem, public readonly group: string | EnvironmentGroupInfo) {
const label = typeof group === 'string' ? group : group.name;
const item = new TreeItem(label, TreeItemCollapsibleState.Collapsed);
item.contextValue = `pythonEnvGroup;${this.parent.manager.id}:${label};`;
this.treeItem = item;

if (typeof group !== 'string') {
item.description = group.description;
item.tooltip = group.tooltip;
item.iconPath = group.iconPath;
}
}
}

export class PythonEnvTreeItem implements EnvTreeItem {
public readonly kind = EnvTreeItemKind.environment;
public readonly treeItem: TreeItem;
constructor(public readonly environment: PythonEnvironment, public readonly parent: EnvManagerTreeItem) {
const item = new TreeItem(environment.displayName, TreeItemCollapsibleState.Collapsed);
constructor(
public readonly environment: PythonEnvironment,
public readonly parent: EnvManagerTreeItem | PythonGroupEnvTreeItem,
) {
const item = new TreeItem(environment.displayName ?? environment.name, TreeItemCollapsibleState.Collapsed);
item.contextValue = this.getContextValue();
item.description = environment.description;
item.tooltip = environment.tooltip;
Expand All @@ -56,7 +77,12 @@ export class PythonEnvTreeItem implements EnvTreeItem {

private getContextValue() {
const activatable = isActivatableEnvironment(this.environment) ? 'activatable' : '';
const remove = this.parent.manager.supportsRemove ? 'remove' : '';
let remove = '';
if (this.parent.kind === EnvTreeItemKind.environmentGroup) {
remove = this.parent.parent.manager.supportsRemove ? 'remove' : '';
} else if (this.parent.kind === EnvTreeItemKind.manager) {
remove = this.parent.manager.supportsRemove ? 'remove' : '';
}
const parts = ['pythonEnvironment', remove, activatable].filter(Boolean);
return parts.join(';') + ';';
}
Expand Down
3 changes: 3 additions & 0 deletions src/internal.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
ResolveEnvironmentContext,
PackageInstallOptions,
Installable,
EnvironmentGroupInfo,
} from './api';
import { CreateEnvironmentNotSupported, RemoveEnvironmentNotSupported } from './common/errors/NotSupportedError';

Expand Down Expand Up @@ -268,6 +269,7 @@ export class PythonEnvironmentImpl implements PythonEnvironment {
public readonly iconPath?: IconPath;
public readonly execInfo: PythonEnvironmentExecutionInfo;
public readonly sysPrefix: string;
public readonly group?: string | EnvironmentGroupInfo;

constructor(public readonly envId: PythonEnvironmentId, info: PythonEnvironmentInfo) {
this.name = info.name;
Expand All @@ -281,6 +283,7 @@ export class PythonEnvironmentImpl implements PythonEnvironment {
this.iconPath = info.iconPath;
this.execInfo = info.execInfo;
this.sysPrefix = info.sysPrefix;
this.group = info.group;
}
}

Expand Down
4 changes: 4 additions & 0 deletions src/managers/conda/condaUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,7 @@ function nativeToPythonEnv(
activation: [{ executable: conda, args: ['activate', e.prefix] }],
deactivation: [{ executable: conda, args: ['deactivate'] }],
},
group: 'Prefix',
},
manager,
);
Expand Down Expand Up @@ -297,6 +298,7 @@ function nativeToPythonEnv(
activation: [{ executable: conda, args: ['activate', name] }],
deactivation: [{ executable: conda, args: ['deactivate'] }],
},
group: 'Named',
},
manager,
);
Expand Down Expand Up @@ -490,6 +492,7 @@ async function createNamedCondaEnvironment(
run: { executable: path.join(envPath, bin) },
},
sysPrefix: envPath,
group: 'Named',
},
manager,
);
Expand Down Expand Up @@ -565,6 +568,7 @@ async function createPrefixCondaEnvironment(
deactivation: [{ executable: 'conda', args: ['deactivate'] }],
},
sysPrefix: prefix,
group: 'Prefix',
},
manager,
);
Expand Down
Loading