Skip to content

Commit e42792d

Browse files
Copiloteleanorjboydkarthiknadig
authored
Move packages to top level under each environment (#435)
## Problem Currently, in the Python Projects sidebar view, packages are nested under an extra "Packages" dropdown under each environment, creating an unnecessary level of nesting: ![Before](https://github.com/user-attachments/assets/3a533a5a-e503-4499-9966-cfc0bebd5837) ## Changes This PR removes the intermediate "Packages" dropdown and shows packages directly under each environment: - Modified `ProjectView.getChildren()` to skip the PackageRoot level - Updated `ProjectPackage` constructor to accept a `ProjectEnvironment` parent - Updated package refresh logic to work with the new structure - Updated command handlers in `envCommands.ts` to handle the modified package structure - Updated menu condition in `package.json` to attach refresh command to environment nodes After this change, users can access installed packages with just one expansion from the environment level, which improves the user experience by reducing clicks and making the structure more intuitive. Fixes #330. --- 💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more [Copilot coding agent tips](https://gh.io/copilot-coding-agent-tips) in the docs. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> Co-authored-by: karthiknadig <3840081+karthiknadig@users.noreply.github.com>
1 parent f26451b commit e42792d

File tree

6 files changed

+71
-121
lines changed

6 files changed

+71
-121
lines changed

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -361,7 +361,7 @@
361361
{
362362
"command": "python-envs.refreshPackages",
363363
"group": "inline",
364-
"when": "view == env-managers && viewItem == python-package-root"
364+
"when": "view == env-managers && viewItem =~ /.*pythonEnvironment.*/"
365365
},
366366
{
367367
"command": "python-envs.packages",
@@ -395,7 +395,7 @@
395395
{
396396
"command": "python-envs.refreshPackages",
397397
"group": "inline",
398-
"when": "view == python-projects && viewItem == python-package-root"
398+
"when": "view == python-projects && viewItem == python-env"
399399
},
400400
{
401401
"command": "python-envs.removePythonProject",

src/extension.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
155155
await Promise.all(envManagers.managers.map((m) => m.refresh(undefined)));
156156
}),
157157
commands.registerCommand('python-envs.refreshPackages', async (item) => {
158-
await refreshPackagesCommand(item);
158+
await refreshPackagesCommand(item, envManagers);
159159
}),
160160
commands.registerCommand('python-envs.create', async (item) => {
161161
return await createEnvironmentCommand(item, envManagers, projectManager);

src/features/envCommands.ts

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,10 @@ import {
3131
EnvManagerTreeItem,
3232
EnvTreeItemKind,
3333
GlobalProjectItem,
34-
PackageRootTreeItem,
3534
PackageTreeItem,
3635
ProjectEnvironment,
3736
ProjectItem,
3837
ProjectPackage,
39-
ProjectPackageRootTreeItem,
4038
PythonEnvTreeItem,
4139
} from './views/treeViewItems';
4240

@@ -49,15 +47,25 @@ export async function refreshManagerCommand(context: unknown): Promise<void> {
4947
}
5048
}
5149

52-
export async function refreshPackagesCommand(context: unknown) {
53-
if (context instanceof ProjectPackageRootTreeItem) {
54-
const view = context as ProjectPackageRootTreeItem;
55-
const manager = view.manager;
56-
await manager.refresh(view.environment);
57-
} else if (context instanceof PackageRootTreeItem) {
58-
const view = context as PackageRootTreeItem;
59-
const manager = view.manager;
60-
await manager.refresh(view.environment);
50+
export async function refreshPackagesCommand(context: unknown, managers?: EnvironmentManagers): Promise<void> {
51+
if (context instanceof ProjectEnvironment) {
52+
const view = context as ProjectEnvironment;
53+
if (managers) {
54+
const pkgManager = managers.getPackageManager(view.parent.project.uri);
55+
if (pkgManager) {
56+
await pkgManager.refresh(view.environment);
57+
}
58+
}
59+
} else if (context instanceof PythonEnvTreeItem) {
60+
const view = context as PythonEnvTreeItem;
61+
const envManager =
62+
view.parent.kind === EnvTreeItemKind.environmentGroup
63+
? view.parent.parent.manager
64+
: view.parent.manager;
65+
const pkgManager = managers?.getPackageManager(envManager.preferredPackageManagerId);
66+
if (pkgManager) {
67+
await pkgManager.refresh(view.environment);
68+
}
6169
} else {
6270
traceVerbose(`Invalid context for refresh command: ${context}`);
6371
}
@@ -193,7 +201,8 @@ export async function removeEnvironmentCommand(context: unknown, managers: Envir
193201
export async function handlePackageUninstall(context: unknown, em: EnvironmentManagers) {
194202
if (context instanceof PackageTreeItem || context instanceof ProjectPackage) {
195203
const moduleName = context.pkg.name;
196-
const environment = context.parent.environment;
204+
const environment =
205+
context instanceof ProjectPackage ? context.parent.environment : context.parent.environment;
197206
const packageManager = em.getPackageManager(environment);
198207
await packageManager?.manage(environment, { uninstall: [moduleName], install: [] });
199208
return;

src/features/views/envManagersView.ts

Lines changed: 17 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,10 @@ import {
1313
EnvTreeItem,
1414
EnvManagerTreeItem,
1515
PythonEnvTreeItem,
16-
PackageRootTreeItem,
1716
PackageTreeItem,
1817
EnvTreeItemKind,
1918
NoPythonEnvTreeItem,
2019
EnvInfoTreeItem,
21-
PackageRootInfoTreeItem,
2220
PythonGroupEnvTreeItem,
2321
} from './treeViewItems';
2422
import { createSimpleDebounce } from '../../common/utils/debounce';
@@ -31,7 +29,6 @@ export class EnvManagerView implements TreeDataProvider<EnvTreeItem>, Disposable
3129
>();
3230
private revealMap = new Map<string, PythonEnvTreeItem>();
3331
private managerViews = new Map<string, EnvManagerTreeItem>();
34-
private packageRoots = new Map<string, PackageRootTreeItem>();
3532
private selected: Map<string, string> = new Map();
3633
private disposables: Disposable[] = [];
3734

@@ -42,7 +39,6 @@ export class EnvManagerView implements TreeDataProvider<EnvTreeItem>, Disposable
4239

4340
this.disposables.push(
4441
new Disposable(() => {
45-
this.packageRoots.clear();
4642
this.revealMap.clear();
4743
this.managerViews.clear();
4844
this.selected.clear();
@@ -165,32 +161,18 @@ export class EnvManagerView implements TreeDataProvider<EnvTreeItem>, Disposable
165161
const views: EnvTreeItem[] = [];
166162

167163
if (pkgManager) {
168-
const item = new PackageRootTreeItem(parent, pkgManager, environment);
169-
this.packageRoots.set(environment.envId.id, item);
170-
views.push(item);
164+
const packages = await pkgManager.getPackages(environment);
165+
if (packages && packages.length > 0) {
166+
views.push(...packages.map((p) => new PackageTreeItem(p, parent, pkgManager)));
167+
} else {
168+
views.push(new EnvInfoTreeItem(parent, ProjectViews.noPackages));
169+
}
171170
} else {
172171
views.push(new EnvInfoTreeItem(parent, ProjectViews.noPackageManager));
173172
}
174173

175174
return views;
176175
}
177-
178-
if (element.kind === EnvTreeItemKind.packageRoot) {
179-
const root = element as PackageRootTreeItem;
180-
const manager = root.manager;
181-
const environment = root.environment;
182-
183-
let packages = await manager.getPackages(environment);
184-
const views: EnvTreeItem[] = [];
185-
186-
if (packages) {
187-
views.push(...packages.map((p) => new PackageTreeItem(p, root, manager)));
188-
} else {
189-
views.push(new PackageRootInfoTreeItem(root, ProjectViews.noPackages));
190-
}
191-
192-
return views;
193-
}
194176
}
195177

196178
getParent(element: EnvTreeItem): ProviderResult<EnvTreeItem> {
@@ -219,35 +201,32 @@ export class EnvManagerView implements TreeDataProvider<EnvTreeItem>, Disposable
219201
}
220202

221203
private onDidChangePackages(args: InternalDidChangePackagesEventArgs) {
222-
const pkgRoot = this.packageRoots.get(args.environment.envId.id);
223-
if (pkgRoot) {
224-
this.fireDataChanged(pkgRoot);
204+
const view = Array.from(this.revealMap.values()).find(
205+
(v) => v.environment.envId.id === args.environment.envId.id
206+
);
207+
if (view) {
208+
this.fireDataChanged(view);
225209
}
226210
}
227211

228-
private onDidChangePackageManager(args: DidChangePackageManagerEventArgs) {
229-
const roots = Array.from(this.packageRoots.values()).filter((r) => r.manager.id === args.manager.id);
230-
this.fireDataChanged(roots);
212+
private onDidChangePackageManager(_args: DidChangePackageManagerEventArgs) {
213+
// Since we removed the packageRoots level, just refresh all environments
214+
// This is a simplified approach that isn't as targeted but ensures packages get refreshed
215+
this.fireDataChanged(undefined);
231216
}
232217

233218
public environmentChanged(e: DidChangeEnvironmentEventArgs) {
234219
const views = [];
235220
if (e.old) {
236221
this.selected.delete(e.old.envId.id);
237-
let view: EnvTreeItem | undefined = this.packageRoots.get(e.old.envId.id);
238-
if (!view) {
239-
view = this.managerViews.get(e.old.envId.managerId);
240-
}
222+
const view: EnvTreeItem | undefined = this.revealMap.get(e.old.envId.id);
241223
if (view) {
242224
views.push(view);
243225
}
244226
}
245227
if (e.new) {
246228
this.selected.set(e.new.envId.id, e.uri === undefined ? 'global' : e.uri.fsPath);
247-
let view: EnvTreeItem | undefined = this.packageRoots.get(e.new.envId.id);
248-
if (!view) {
249-
view = this.managerViews.get(e.new.envId.managerId);
250-
}
229+
const view: EnvTreeItem | undefined = this.revealMap.get(e.new.envId.id);
251230
if (view && !views.includes(view)) {
252231
views.push(view);
253232
}

src/features/views/projectView.ts

Lines changed: 23 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,6 @@ import {
2121
ProjectEnvironmentInfo,
2222
ProjectItem,
2323
ProjectPackage,
24-
ProjectPackageRootInfoTreeItem,
25-
ProjectPackageRootTreeItem,
2624
ProjectTreeItem,
2725
ProjectTreeItemKind,
2826
} from './treeViewItems';
@@ -34,7 +32,7 @@ export class ProjectView implements TreeDataProvider<ProjectTreeItem> {
3432
>();
3533
private projectViews: Map<string, ProjectItem> = new Map();
3634
private revealMap: Map<string, ProjectEnvironment> = new Map();
37-
private packageRoots: Map<string, ProjectPackageRootTreeItem> = new Map();
35+
private packageRoots: Map<string, ProjectEnvironment> = new Map();
3836
private disposables: Disposable[] = [];
3937
private debouncedUpdateProject = createSimpleDebounce(500, () => this.updateProject());
4038
public constructor(private envManagers: EnvironmentManagers, private projectManager: PythonProjectManager) {
@@ -83,7 +81,8 @@ export class ProjectView implements TreeDataProvider<ProjectTreeItem> {
8381

8482
private updatePackagesForEnvironment(e: PythonEnvironment): void {
8583
const views: ProjectTreeItem[] = [];
86-
this.packageRoots.forEach((v) => {
84+
// Look for environments matching this environment ID and refresh them
85+
this.revealMap.forEach((v) => {
8786
if (v.environment.envId.id === e.envId.id) {
8887
views.push(v);
8988
}
@@ -125,8 +124,16 @@ export class ProjectView implements TreeDataProvider<ProjectTreeItem> {
125124
return element.treeItem;
126125
}
127126

127+
/**
128+
* Returns the children of a given element in the project tree view:
129+
* If param is undefined, return root project items
130+
* If param is a project, returns its environments.
131+
* If param is an environment, returns its packages.
132+
* @param element The tree item for which to get children.
133+
*/
128134
async getChildren(element?: ProjectTreeItem | undefined): Promise<ProjectTreeItem[] | undefined> {
129135
if (element === undefined) {
136+
// Return the root items
130137
this.projectViews.clear();
131138
const views: ProjectTreeItem[] = [];
132139
const projects = this.projectManager.getProjects();
@@ -187,38 +194,30 @@ export class ProjectView implements TreeDataProvider<ProjectTreeItem> {
187194
}
188195

189196
if (element.kind === ProjectTreeItemKind.environment) {
197+
// Return packages directly under the environment
198+
190199
const environmentItem = element as ProjectEnvironment;
191200
const parent = environmentItem.parent;
192201
const uri = parent.id === 'global' ? undefined : parent.project.uri;
193202
const pkgManager = this.envManagers.getPackageManager(uri);
194203
const environment = environmentItem.environment;
195204

196-
const views: ProjectTreeItem[] = [];
205+
if (!pkgManager) {
206+
return [new ProjectEnvironmentInfo(environmentItem, ProjectViews.noPackageManager)];
207+
}
197208

198-
if (pkgManager) {
199-
const item = new ProjectPackageRootTreeItem(environmentItem, pkgManager, environment);
200-
this.packageRoots.set(uri ? uri.fsPath : 'global', item);
201-
views.push(item);
202-
} else {
203-
views.push(new ProjectEnvironmentInfo(environmentItem, ProjectViews.noPackageManager));
209+
let packages = await pkgManager.getPackages(environment);
210+
if (!packages) {
211+
return [new ProjectEnvironmentInfo(environmentItem, ProjectViews.noPackages)];
204212
}
205-
return views;
206-
}
207213

208-
if (element.kind === ProjectTreeItemKind.packageRoot) {
209-
const root = element as ProjectPackageRootTreeItem;
210-
const manager = root.manager;
211-
const environment = root.environment;
212-
let packages = await manager.getPackages(environment);
213-
const views: ProjectTreeItem[] = [];
214+
// Store the reference for refreshing packages
215+
this.packageRoots.set(uri ? uri.fsPath : 'global', environmentItem);
214216

215-
if (packages) {
216-
return packages.map((p) => new ProjectPackage(root, p, manager));
217-
} else {
218-
views.push(new ProjectPackageRootInfoTreeItem(root, ProjectViews.noPackages));
219-
}
217+
return packages.map((p) => new ProjectPackage(environmentItem, p, pkgManager));
220218
}
221219

220+
//return nothing if the element is not a project, environment, or undefined
222221
return undefined;
223222
}
224223
getParent(element: ProjectTreeItem): ProviderResult<ProjectTreeItem> {

0 commit comments

Comments
 (0)