Skip to content
Open
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
4 changes: 3 additions & 1 deletion src/features/views/projectView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,9 @@ export class ProjectView implements TreeDataProvider<ProjectTreeItem> {
// Store the reference for refreshing packages
this.packageRoots.set(uri ? uri.fsPath : 'global', environmentItem);

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

//return nothing if the element is not a project, environment, or undefined
Expand Down
14 changes: 10 additions & 4 deletions src/features/views/treeViewItems.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ export class PackageTreeItem implements EnvTreeItem {
const defaultIcon = pkg.isTransitive ? new ThemeIcon('list-tree') : new ThemeIcon('package');
item.iconPath = pkg.iconPath ?? defaultIcon;
item.contextValue = pkg.isTransitive ? 'python-package-transitive' : 'python-package';
item.description = (pkg.isTransitive ? l10n.t('(transitive) ') : '') + (pkg.description ?? pkg.version);
item.description = (pkg.isTransitive ? l10n.t('(transitive) ') : '') + (pkg.description ?? pkg.version ?? '');
item.tooltip = pkg.isTransitive
? l10n.t('This package is a dependency of another installed package. It may also have been explicitly installed.')
: pkg.tooltip;
Expand Down Expand Up @@ -433,10 +433,16 @@ export class ProjectPackage implements ProjectTreeItem {
) {
this.id = ProjectPackage.getId(parent, pkg);
const item = new TreeItem(this.pkg.displayName, TreeItemCollapsibleState.None);
item.iconPath = this.pkg.iconPath;
const defaultIcon = this.pkg.isTransitive ? new ThemeIcon('list-tree') : new ThemeIcon('package');
item.iconPath = this.pkg.iconPath ?? defaultIcon;
item.contextValue = this.pkg.isTransitive ? 'python-package-transitive' : 'python-package';
item.description = this.pkg.description ?? this.pkg.version;
item.tooltip = this.pkg.tooltip;
item.description =
(this.pkg.isTransitive ? l10n.t('(transitive) ') : '') + (this.pkg.description ?? this.pkg.version ?? '');
item.tooltip = this.pkg.isTransitive
? l10n.t(
'This package is a dependency of another installed package. It may also have been explicitly installed.',
)
: this.pkg.tooltip;
this.treeItem = item;
}

Expand Down
82 changes: 80 additions & 2 deletions src/test/features/views/treeViewItems.unit.test.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import * as assert from 'assert';
import { Uri } from 'vscode';
import { ThemeIcon, Uri } from 'vscode';
import { Package } from '../../../api';
import { UvInstallStrings, VenvManagerStrings } from '../../../common/localize';
import {
EnvManagerTreeItem,
getEnvironmentParentDirName,
NoPythonEnvTreeItem,
ProjectEnvironment,
ProjectPackage,
PythonEnvTreeItem,
PythonGroupEnvTreeItem,
} from '../../../features/views/treeViewItems';
import { InternalEnvironmentManager, PythonEnvironmentImpl } from '../../../internal.api';
import { InternalEnvironmentManager, InternalPackageManager, PythonEnvironmentImpl } from '../../../internal.api';

/**
* Helper to create a mock PythonEnvironmentImpl with minimal required fields.
Expand Down Expand Up @@ -487,4 +490,79 @@ suite('Test TreeView Items', () => {
assert.equal(item.treeItem.command, undefined, 'Should not have a command');
});
});

suite('ProjectPackage', () => {
// ProjectPackage only reads parent.id and does not call any manager methods,
// so minimal cast mocks are sufficient for exercising the tree item rendering.
const parent = { id: 'project>>>env' } as ProjectEnvironment;
const manager = {} as InternalPackageManager;

function createMockPackage(options: Partial<Package> = {}): Package {
return {
name: options.name ?? 'requests',
displayName: options.displayName ?? options.name ?? 'requests',
version: options.version,
description: options.description,
tooltip: options.tooltip,
iconPath: options.iconPath,
isTransitive: options.isTransitive,
pkgId: { id: options.name ?? 'requests', managerId: 'ms-python.python:pip' },
} as Package;
}

test('Direct package uses package icon and shows no transitive prefix', () => {
// Arrange
const pkg = createMockPackage({ name: 'requests', version: '2.31.0', isTransitive: false });

// Act
const item = new ProjectPackage(parent, pkg, manager);

// Assert
assert.strictEqual(item.treeItem.contextValue, 'python-package');
assert.strictEqual((item.treeItem.iconPath as ThemeIcon).id, 'package');
assert.strictEqual(item.treeItem.description, '2.31.0');
});

test('Transitive package uses list-tree icon and shows transitive prefix', () => {
// Arrange
const pkg = createMockPackage({ name: 'urllib3', version: '2.0.0', isTransitive: true });

// Act
const item = new ProjectPackage(parent, pkg, manager);

// Assert
assert.strictEqual(item.treeItem.contextValue, 'python-package-transitive');
assert.strictEqual((item.treeItem.iconPath as ThemeIcon).id, 'list-tree');
assert.ok(
(item.treeItem.description as string).startsWith('(transitive) '),
'Transitive package description should be prefixed with "(transitive) "',
);
assert.ok(item.treeItem.tooltip, 'Transitive package should have an explanatory tooltip');
});

test('Prefers package-provided iconPath over default icon', () => {
// Arrange
const pkg = createMockPackage({ name: 'numpy', isTransitive: true, iconPath: new ThemeIcon('symbol-numeric') });

// Act
const item = new ProjectPackage(parent, pkg, manager);

// Assert
assert.strictEqual((item.treeItem.iconPath as ThemeIcon).id, 'symbol-numeric');
});

test('Falls back to empty description when version and description are missing', () => {
// Arrange
const directPkg = createMockPackage({ name: 'mypkg', isTransitive: false });
const transitivePkg = createMockPackage({ name: 'mypkg', isTransitive: true });

// Act
const directItem = new ProjectPackage(parent, directPkg, manager);
const transitiveItem = new ProjectPackage(parent, transitivePkg, manager);

// Assert
assert.strictEqual(directItem.treeItem.description, '');
assert.strictEqual(transitiveItem.treeItem.description, '(transitive) ');
});
});
});
Loading