Skip to content

Commit 7e3f696

Browse files
authored
Distinguish and sort transitive packages in the Project View (microsoft#1606)
The Project View rendered direct and transitive packages identically, while the Environment View distinguishes them (icon, label, tooltip) and sorts direct-first. This made the two views inconsistent. ### Changes - **`treeViewItems.ts` (`ProjectPackage`)** — mirror `PackageTreeItem`: `list-tree` icon for transitive (vs `package`), `(transitive) ` description prefix, and the explanatory dependency tooltip. Package-provided `iconPath` still takes precedence. - **`projectView.ts`** — sort packages direct-first using the same comparator as the Environment View before mapping to tree items. - **Robustness** — fall back to `''` when both `version` and `description` are absent, avoiding a stray `undefined` in the label; applied to both views. - **Tests** — add a `ProjectPackage` suite covering direct vs. transitive rendering, icon override, and the empty-description case. ```ts // projectView.ts — direct packages first, then transitive return packages .sort((a, b) => (a.isTransitive === b.isTransitive ? 0 : a.isTransitive ? 1 : -1)) .map((p) => new ProjectPackage(environmentItem, p, pkgManager)); ``` Note: the description/tooltip/icon logic is intentionally duplicated across `ProjectPackage` and `PackageTreeItem` to keep the two views in lockstep; a shared helper is a reasonable follow-up if further divergence is a concern. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
1 parent 707f2a8 commit 7e3f696

3 files changed

Lines changed: 93 additions & 7 deletions

File tree

src/features/views/projectView.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,9 @@ export class ProjectView implements TreeDataProvider<ProjectTreeItem> {
252252
// Store the reference for refreshing packages
253253
this.packageRoots.set(uri ? uri.fsPath : 'global', environmentItem);
254254

255-
return packages.map((p) => new ProjectPackage(environmentItem, p, pkgManager));
255+
return packages
256+
.sort((a, b) => (a.isTransitive === b.isTransitive ? 0 : a.isTransitive ? 1 : -1))
257+
.map((p) => new ProjectPackage(environmentItem, p, pkgManager));
256258
}
257259

258260
//return nothing if the element is not a project, environment, or undefined

src/features/views/treeViewItems.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ export class PackageTreeItem implements EnvTreeItem {
213213
const defaultIcon = pkg.isTransitive ? new ThemeIcon('list-tree') : new ThemeIcon('package');
214214
item.iconPath = pkg.iconPath ?? defaultIcon;
215215
item.contextValue = pkg.isTransitive ? 'python-package-transitive' : 'python-package';
216-
item.description = (pkg.isTransitive ? l10n.t('(transitive) ') : '') + (pkg.description ?? pkg.version);
216+
item.description = (pkg.isTransitive ? l10n.t('(transitive) ') : '') + (pkg.description ?? pkg.version ?? '');
217217
item.tooltip = pkg.isTransitive
218218
? l10n.t('This package is a dependency of another installed package. It may also have been explicitly installed.')
219219
: pkg.tooltip;
@@ -433,10 +433,16 @@ export class ProjectPackage implements ProjectTreeItem {
433433
) {
434434
this.id = ProjectPackage.getId(parent, pkg);
435435
const item = new TreeItem(this.pkg.displayName, TreeItemCollapsibleState.None);
436-
item.iconPath = this.pkg.iconPath;
436+
const defaultIcon = this.pkg.isTransitive ? new ThemeIcon('list-tree') : new ThemeIcon('package');
437+
item.iconPath = this.pkg.iconPath ?? defaultIcon;
437438
item.contextValue = this.pkg.isTransitive ? 'python-package-transitive' : 'python-package';
438-
item.description = this.pkg.description ?? this.pkg.version;
439-
item.tooltip = this.pkg.tooltip;
439+
item.description =
440+
(this.pkg.isTransitive ? l10n.t('(transitive) ') : '') + (this.pkg.description ?? this.pkg.version ?? '');
441+
item.tooltip = this.pkg.isTransitive
442+
? l10n.t(
443+
'This package is a dependency of another installed package. It may also have been explicitly installed.',
444+
)
445+
: this.pkg.tooltip;
440446
this.treeItem = item;
441447
}
442448

src/test/features/views/treeViewItems.unit.test.ts

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
import * as assert from 'assert';
2-
import { Uri } from 'vscode';
2+
import { ThemeIcon, Uri } from 'vscode';
3+
import { Package } from '../../../api';
34
import { UvInstallStrings, VenvManagerStrings } from '../../../common/localize';
45
import {
56
EnvManagerTreeItem,
67
getEnvironmentParentDirName,
78
NoPythonEnvTreeItem,
9+
ProjectEnvironment,
10+
ProjectPackage,
811
PythonEnvTreeItem,
912
PythonGroupEnvTreeItem,
1013
} from '../../../features/views/treeViewItems';
11-
import { InternalEnvironmentManager, PythonEnvironmentImpl } from '../../../internal.api';
14+
import { InternalEnvironmentManager, InternalPackageManager, PythonEnvironmentImpl } from '../../../internal.api';
1215

1316
/**
1417
* Helper to create a mock PythonEnvironmentImpl with minimal required fields.
@@ -487,4 +490,79 @@ suite('Test TreeView Items', () => {
487490
assert.equal(item.treeItem.command, undefined, 'Should not have a command');
488491
});
489492
});
493+
494+
suite('ProjectPackage', () => {
495+
// ProjectPackage only reads parent.id and does not call any manager methods,
496+
// so minimal cast mocks are sufficient for exercising the tree item rendering.
497+
const parent = { id: 'project>>>env' } as ProjectEnvironment;
498+
const manager = {} as InternalPackageManager;
499+
500+
function createMockPackage(options: Partial<Package> = {}): Package {
501+
return {
502+
name: options.name ?? 'requests',
503+
displayName: options.displayName ?? options.name ?? 'requests',
504+
version: options.version,
505+
description: options.description,
506+
tooltip: options.tooltip,
507+
iconPath: options.iconPath,
508+
isTransitive: options.isTransitive,
509+
pkgId: { id: options.name ?? 'requests', managerId: 'ms-python.python:pip' },
510+
} as Package;
511+
}
512+
513+
test('Direct package uses package icon and shows no transitive prefix', () => {
514+
// Arrange
515+
const pkg = createMockPackage({ name: 'requests', version: '2.31.0', isTransitive: false });
516+
517+
// Act
518+
const item = new ProjectPackage(parent, pkg, manager);
519+
520+
// Assert
521+
assert.strictEqual(item.treeItem.contextValue, 'python-package');
522+
assert.strictEqual((item.treeItem.iconPath as ThemeIcon).id, 'package');
523+
assert.strictEqual(item.treeItem.description, '2.31.0');
524+
});
525+
526+
test('Transitive package uses list-tree icon and shows transitive prefix', () => {
527+
// Arrange
528+
const pkg = createMockPackage({ name: 'urllib3', version: '2.0.0', isTransitive: true });
529+
530+
// Act
531+
const item = new ProjectPackage(parent, pkg, manager);
532+
533+
// Assert
534+
assert.strictEqual(item.treeItem.contextValue, 'python-package-transitive');
535+
assert.strictEqual((item.treeItem.iconPath as ThemeIcon).id, 'list-tree');
536+
assert.ok(
537+
(item.treeItem.description as string).startsWith('(transitive) '),
538+
'Transitive package description should be prefixed with "(transitive) "',
539+
);
540+
assert.ok(item.treeItem.tooltip, 'Transitive package should have an explanatory tooltip');
541+
});
542+
543+
test('Prefers package-provided iconPath over default icon', () => {
544+
// Arrange
545+
const pkg = createMockPackage({ name: 'numpy', isTransitive: true, iconPath: new ThemeIcon('symbol-numeric') });
546+
547+
// Act
548+
const item = new ProjectPackage(parent, pkg, manager);
549+
550+
// Assert
551+
assert.strictEqual((item.treeItem.iconPath as ThemeIcon).id, 'symbol-numeric');
552+
});
553+
554+
test('Falls back to empty description when version and description are missing', () => {
555+
// Arrange
556+
const directPkg = createMockPackage({ name: 'mypkg', isTransitive: false });
557+
const transitivePkg = createMockPackage({ name: 'mypkg', isTransitive: true });
558+
559+
// Act
560+
const directItem = new ProjectPackage(parent, directPkg, manager);
561+
const transitiveItem = new ProjectPackage(parent, transitivePkg, manager);
562+
563+
// Assert
564+
assert.strictEqual(directItem.treeItem.description, '');
565+
assert.strictEqual(transitiveItem.treeItem.description, '(transitive) ');
566+
});
567+
});
490568
});

0 commit comments

Comments
 (0)