diff --git a/extensions/positron-python/python_files/posit/positron/ui.py b/extensions/positron-python/python_files/posit/positron/ui.py index 7b6db130e1df..f8d7555a2b05 100644 --- a/extensions/positron-python/python_files/posit/positron/ui.py +++ b/extensions/positron-python/python_files/posit/positron/ui.py @@ -8,10 +8,11 @@ import inspect import logging import os +import re import sys import webbrowser from pathlib import Path -from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Union +from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Union from urllib.parse import urlparse from comm.base_comm import BaseComm @@ -126,11 +127,28 @@ def _get_packages_installed(_kernel: "PositronIPyKernel", _params: List[JsonData canonical = canonicalize_name(name) # Dedupe by canonical name - keeps first occurrence (the one that would be imported) if canonical not in packages_dict: + # PackageMetadata (the 3.14 protocol) doesn't expose .get(), but the + # runtime object (email.message.Message) always has it. + metadata: Any = dist.metadata + summary = metadata.get("Summary") + # Fall back through Author → Author-email → Maintainer → Maintainer-email so + # packages that set only a maintainer (e.g. click's `Pallets`) still show one. + author = "" + for field in ("Author", "Author-email", "Maintainer", "Maintainer-email"): + value = metadata.get(field) + if value and value != "UNKNOWN": + # Strip trailing email addresses in angle brackets to match the R side. + stripped = re.sub(r"\s*<[^>]+>", "", value).strip() + if stripped: + author = stripped + break packages_dict[canonical] = { "id": f"{canonical}-{dist.version}", "name": name, "displayName": canonical, "version": dist.version, + "description": summary if summary and summary != "UNKNOWN" else "", + "author": author, } return sorted(packages_dict.values(), key=lambda p: p["displayName"]) diff --git a/src/positron-dts/positron.d.ts b/src/positron-dts/positron.d.ts index 3c68289bd4f1..ab2733461f4d 100644 --- a/src/positron-dts/positron.d.ts +++ b/src/positron-dts/positron.d.ts @@ -1156,6 +1156,11 @@ declare module 'positron' { /** Publication/release date */ publishedDate?: string; + + /** Optional short description or summary shown in the Packages pane card view. */ + description?: string; + /** Optional author shown in the Packages pane card view. */ + author?: string; } /** diff --git a/src/vs/workbench/contrib/positronPackages/browser/components/listPackages.css b/src/vs/workbench/contrib/positronPackages/browser/components/listPackages.css index 8e89b1afd203..ed6ee435db94 100644 --- a/src/vs/workbench/contrib/positronPackages/browser/components/listPackages.css +++ b/src/vs/workbench/contrib/positronPackages/browser/components/listPackages.css @@ -35,18 +35,52 @@ } .packages-list-item { - margin-top: 5px; padding-left: 8px; padding-right: 8px; + cursor: pointer; + overflow: hidden; display: flex; + box-sizing: border-box; +} + +.packages-list-item.item-size-row { + flex-direction: row; align-items: center; - cursor: pointer; + margin-top: 5px; +} + +.packages-list-item.item-size-card { + flex-direction: column; + justify-content: center; + /* Align with the search input's outer-left edge (filter-container 8px + input border 1px), plus the 0.5px nudge from .packages-list-container. */ + padding-left: 10px; + padding-right: 11px; + padding-top: 6px; + padding-bottom: 6px; +} + +.packages-list-item-header { + display: flex; + flex-direction: row; + align-items: baseline; overflow: hidden; + min-width: 0; +} + +.packages-list-item.item-size-row .packages-list-item-header { + flex: 1 1 auto; } .packages-list-item-name { - flex: 0 0 auto; + flex: 0 1 auto; + min-width: 0; white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.packages-list-item.item-size-card .packages-list-item-name { + font-weight: bold; } .packages-list-item-version { @@ -67,6 +101,24 @@ font-size: 0.9em; } +.packages-list-item-description { + color: var(--vscode-descriptionForeground); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + min-width: 0; +} + +.packages-list-item-author { + font-size: 90%; + color: var(--vscode-descriptionForeground); + opacity: 0.85; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + min-width: 0; +} + .packages-list-item:hover { background: var(--vscode-list-hoverBackground); } diff --git a/src/vs/workbench/contrib/positronPackages/browser/components/listPackages.tsx b/src/vs/workbench/contrib/positronPackages/browser/components/listPackages.tsx index f8475943b5fc..b95018e5e6c6 100644 --- a/src/vs/workbench/contrib/positronPackages/browser/components/listPackages.tsx +++ b/src/vs/workbench/contrib/positronPackages/browser/components/listPackages.tsx @@ -48,6 +48,10 @@ const positronUpdatePackage = localize( // Height of the filter container in pixels const FILTER_HEIGHT = 34; +// Row heights for each item size mode. +const ROW_ITEM_HEIGHT = 26; +const CARD_ITEM_HEIGHT = 72; + export const ListPackages = (props: React.PropsWithChildren) => { const { activeInstance, @@ -57,6 +61,15 @@ export const ListPackages = (props: React.PropsWithChildren) => { const [packages, setPackages] = useState([]); + // Item size mode ('card' or 'row'), driven by the packages service. + const [itemSize, setItemSize] = useState(() => services.positronPackagesService.itemSize); + useEffect(() => { + const disposable = services.positronPackagesService.onDidChangeItemSize((size) => { + setItemSize(size); + }); + return () => disposable.dispose(); + }, [services.positronPackagesService]); + // Progress Bar const progressRef = useRef(null); @@ -251,7 +264,7 @@ export const ListPackages = (props: React.PropsWithChildren) => { // Item renderer const ItemEntry = (props: { index: number; style: CSSProperties }) => { const itemProps = filteredPackages[props.index]; - const { id, name, displayName, version, latestVersion } = itemProps; + const { id, name, displayName, version, latestVersion, description, author } = itemProps; // Check if package has an update available const hasUpdate = latestVersion && latestVersion !== version; @@ -259,7 +272,7 @@ export const ListPackages = (props: React.PropsWithChildren) => { return ( // eslint-disable-next-line jsx-a11y/no-static-element-interactions
) => { } }} > -
{displayName}
-
{version}
- {hasUpdate && ( -
- ↑ -
+
+
{displayName}
+
{version}
+ {hasUpdate && ( +
+ ↑ +
+ )} +
+ {itemSize === 'card' && ( + <> +
+ {description ?? ''} +
+
+ {author ?? ''} +
+ )}
); @@ -410,7 +435,8 @@ export const ListPackages = (props: React.PropsWithChildren) => { innerRef={innerRef} itemCount={filteredPackages.length} itemKey={(index) => filteredPackages[index].id} - itemSize={26} + itemSize={itemSize === 'card' ? CARD_ITEM_HEIGHT : ROW_ITEM_HEIGHT} + style={{ overflowX: 'hidden' }} width={'calc(100% - 2px)'} > {ItemEntry} diff --git a/src/vs/workbench/contrib/positronPackages/browser/interfaces/positronPackagesService.ts b/src/vs/workbench/contrib/positronPackages/browser/interfaces/positronPackagesService.ts index 834fd263b250..2bdc54b5edc9 100644 --- a/src/vs/workbench/contrib/positronPackages/browser/interfaces/positronPackagesService.ts +++ b/src/vs/workbench/contrib/positronPackages/browser/interfaces/positronPackagesService.ts @@ -8,6 +8,7 @@ import { Event } from '../../../../../base/common/event.js'; import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js'; import { ILanguageRuntimePackage, ILanguageRuntimeSession, IPackageSpec } from '../../../../services/runtimeSession/common/runtimeSessionService.js'; import { IPositronPackagesInstance } from '../positronPackagesInstance.js'; +import { PackagesItemSize } from '../positronPackagesContextKeys.js'; // Create the decorator for the Positron packages service (used in dependency injection). export const IPositronPackagesService = createDecorator('positronPackagesService'); @@ -36,6 +37,21 @@ export interface IPositronPackagesService { */ setSelectedPackage(packageName: string | undefined): void; + /** + * The current item size mode for the packages list. + */ + readonly itemSize: PackagesItemSize; + + /** + * Sets the item size mode for the packages list. + */ + setItemSize(itemSize: PackagesItemSize): void; + + /** + * Fired when the item size mode changes. + */ + readonly onDidChangeItemSize: Event; + /** * The onDidRefreshPackagesInstance event. */ diff --git a/src/vs/workbench/contrib/positronPackages/browser/positronPackages.contribution.ts b/src/vs/workbench/contrib/positronPackages/browser/positronPackages.contribution.ts index cb42c030084d..bde735a39b23 100644 --- a/src/vs/workbench/contrib/positronPackages/browser/positronPackages.contribution.ts +++ b/src/vs/workbench/contrib/positronPackages/browser/positronPackages.contribution.ts @@ -23,7 +23,7 @@ import { IViewContainersRegistry, IViewsRegistry, Extensions as ViewContainerExt import { ILanguageRuntimePackage, IRuntimeSessionService } from '../../../services/runtimeSession/common/runtimeSessionService.js'; import { positronSessionViewIcon } from '../../positronSession/browser/positronSessionContainer.js'; import { IPositronPackagesService } from './interfaces/positronPackagesService.js'; -import { PACKAGES_CAN_RUN_ACTION, PACKAGES_HAS_SELECTION, PACKAGES_VIEW_VISIBLE, POSITRON_PACKAGES_VIEW_ID } from './positronPackagesContextKeys.js'; +import { PACKAGES_CAN_RUN_ACTION, PACKAGES_HAS_SELECTION, PACKAGES_VIEW_VISIBLE, POSITRON_PACKAGES_ITEM_SIZE, POSITRON_PACKAGES_VIEW_ID } from './positronPackagesContextKeys.js'; import { installPackage, uninstallPackage, updatePackage } from './positronPackagesQuickPick.js'; import { PositronPackagesService } from './positronPackagesService.js'; import { PositronPackagesView } from './positronPackagesView.js'; @@ -580,6 +580,75 @@ class RefreshMetadataAction extends Action2 { } } +/** + * Switches the Packages view to the expanded card layout. + * Only visible in the view title when the view is currently showing compact rows. + */ +class SetPackagesCardViewAction extends Action2 { + constructor() { + super({ + id: 'positronPackages.setCardView', + title: nls.localize2('positronPackages.showAsCards', 'Show as Cards'), + category: PACKAGES_CATEGORY, + icon: Codicon.listSelection, + precondition: POSITRON_PACKAGES_ENABLED, + menu: { + id: MenuId.ViewTitle, + when: ContextKeyExpr.and(PACKAGES_VIEW_VISIBLE, POSITRON_PACKAGES_ITEM_SIZE.isEqualTo('row')), + group: 'navigation', + order: 2, + }, + }); + } + override async run(accessor: ServicesAccessor): Promise { + accessor.get(IPositronPackagesService).setItemSize('card'); + } +} + +/** + * Switches the Packages view to the compact row layout. + * Only visible in the view title when the view is currently showing cards. + */ +class SetPackagesRowViewAction extends Action2 { + constructor() { + super({ + id: 'positronPackages.setRowView', + title: nls.localize2('positronPackages.showAsRows', 'Show as Rows'), + category: PACKAGES_CATEGORY, + icon: Codicon.listFlat, + precondition: POSITRON_PACKAGES_ENABLED, + menu: { + id: MenuId.ViewTitle, + when: ContextKeyExpr.and(PACKAGES_VIEW_VISIBLE, POSITRON_PACKAGES_ITEM_SIZE.isEqualTo('card')), + group: 'navigation', + order: 2, + }, + }); + } + override async run(accessor: ServicesAccessor): Promise { + accessor.get(IPositronPackagesService).setItemSize('row'); + } +} + +/** + * Toggles between card and row layouts. Exposed via the command palette. + */ +class TogglePackagesItemSizeAction extends Action2 { + constructor() { + super({ + id: 'positronPackages.toggleItemSize', + title: nls.localize2('positronPackages.toggleItemSize', 'Toggle Packages List Layout'), + category: PACKAGES_CATEGORY, + f1: true, + precondition: POSITRON_PACKAGES_ENABLED, + }); + } + override async run(accessor: ServicesAccessor): Promise { + const service = accessor.get(IPositronPackagesService); + service.setItemSize(service.itemSize === 'card' ? 'row' : 'card'); + } +} + registerAction2(InstallPackageAction); registerAction2(RefreshPackagesAction); registerAction2(RefreshMetadataAction); @@ -588,4 +657,7 @@ registerAction2(UpdatePackageAction); registerAction2(UpdateAllPackagesAction); registerAction2(UpdateSelectedPackageAction); registerAction2(UninstallSelectedPackageAction); +registerAction2(SetPackagesCardViewAction); +registerAction2(SetPackagesRowViewAction); +registerAction2(TogglePackagesItemSizeAction); registerSingleton(IPositronPackagesService, PositronPackagesService, InstantiationType.Delayed); diff --git a/src/vs/workbench/contrib/positronPackages/browser/positronPackagesContextKeys.ts b/src/vs/workbench/contrib/positronPackages/browser/positronPackagesContextKeys.ts index a3333686bc6f..fca7c16c4f8d 100644 --- a/src/vs/workbench/contrib/positronPackages/browser/positronPackagesContextKeys.ts +++ b/src/vs/workbench/contrib/positronPackages/browser/positronPackagesContextKeys.ts @@ -13,6 +13,10 @@ export const POSITRON_PACKAGES_HAS_ACTIVE_SESSION = new RawContextKey(' export const POSITRON_PACKAGES_IS_BUSY = new RawContextKey('positronPackages.isBusy', false); export const POSITRON_PACKAGES_SELECTED_PACKAGE = new RawContextKey('positronPackages.selectedPackage', ''); +// Item size mode for the packages list: 'card' or 'row'. +export type PackagesItemSize = 'card' | 'row'; +export const POSITRON_PACKAGES_ITEM_SIZE = new RawContextKey('positronPackages.itemSize', 'row'); + // Context key expressions for menu enablement export const PACKAGES_VIEW_VISIBLE = ContextKeyExpr.equals('view', POSITRON_PACKAGES_VIEW_ID); export const PACKAGES_CAN_RUN_ACTION = ContextKeyExpr.and( diff --git a/src/vs/workbench/contrib/positronPackages/browser/positronPackagesService.ts b/src/vs/workbench/contrib/positronPackages/browser/positronPackagesService.ts index 8f679bdf9bcd..a422f5674d9c 100644 --- a/src/vs/workbench/contrib/positronPackages/browser/positronPackagesService.ts +++ b/src/vs/workbench/contrib/positronPackages/browser/positronPackagesService.ts @@ -12,7 +12,7 @@ import { ILogService } from '../../../../platform/log/common/log.js'; import { LanguageRuntimeSessionMode } from '../../../services/languageRuntime/common/languageRuntimeService.js'; import { ILanguageRuntimePackage, ILanguageRuntimeSession, IPackageSpec, IRuntimeSessionService } from '../../../services/runtimeSession/common/runtimeSessionService.js'; import { IPositronPackagesService } from './interfaces/positronPackagesService.js'; -import { POSITRON_PACKAGES_HAS_ACTIVE_SESSION, POSITRON_PACKAGES_IS_BUSY, POSITRON_PACKAGES_SELECTED_PACKAGE } from './positronPackagesContextKeys.js'; +import { PackagesItemSize, POSITRON_PACKAGES_HAS_ACTIVE_SESSION, POSITRON_PACKAGES_IS_BUSY, POSITRON_PACKAGES_ITEM_SIZE, POSITRON_PACKAGES_SELECTED_PACKAGE } from './positronPackagesContextKeys.js'; import { IPositronPackagesInstance, PositronPackagesInstance } from './positronPackagesInstance.js'; const TIMEOUT_REFRESH_MS = 5_000; // 5 seconds @@ -27,6 +27,8 @@ export class PositronPackagesService extends Disposable implements IPositronPack private readonly _onDidStopPositronPackagesInstanceEmitter = this._register(new Emitter()); + private readonly _onDidChangeItemSize = this._register(new Emitter()); + private readonly _instancesBySessionId = this._register(new DisposableMap()); private _activeInstance: PositronPackagesInstance | undefined; @@ -35,6 +37,7 @@ export class PositronPackagesService extends Disposable implements IPositronPack private readonly _hasActiveSessionContextKey: IContextKey; private readonly _isBusyContextKey: IContextKey; private readonly _selectedPackageContextKey: IContextKey; + private readonly _itemSizeContextKey: IContextKey; // Disposables for tracking busy state of the active instance private readonly _activeInstanceDisposables = this._register(new DisposableStore()); @@ -61,6 +64,7 @@ export class PositronPackagesService extends Disposable implements IPositronPack this._hasActiveSessionContextKey = POSITRON_PACKAGES_HAS_ACTIVE_SESSION.bindTo(this._contextKeyService); this._isBusyContextKey = POSITRON_PACKAGES_IS_BUSY.bindTo(this._contextKeyService); this._selectedPackageContextKey = POSITRON_PACKAGES_SELECTED_PACKAGE.bindTo(this._contextKeyService); + this._itemSizeContextKey = POSITRON_PACKAGES_ITEM_SIZE.bindTo(this._contextKeyService); // Create new instances this._register(this._runtimeSessionService.onWillStartSession((e) => { @@ -178,6 +182,20 @@ export class PositronPackagesService extends Disposable implements IPositronPack this._selectedPackageContextKey.set(packageName ?? ''); } + get itemSize(): PackagesItemSize { + return this._itemSizeContextKey.get() ?? 'row'; + } + + setItemSize(itemSize: PackagesItemSize): void { + if (this.itemSize === itemSize) { + return; + } + this._itemSizeContextKey.set(itemSize); + this._onDidChangeItemSize.fire(itemSize); + } + + readonly onDidChangeItemSize = this._onDidChangeItemSize.event; + async refreshPackages(token?: CancellationToken): Promise { const instance = this._activeInstance; if (instance) { diff --git a/src/vs/workbench/services/runtimeSession/common/runtimeSessionService.ts b/src/vs/workbench/services/runtimeSession/common/runtimeSessionService.ts index 70541a49a8d6..c44193c3547d 100644 --- a/src/vs/workbench/services/runtimeSession/common/runtimeSessionService.ts +++ b/src/vs/workbench/services/runtimeSession/common/runtimeSessionService.ts @@ -298,6 +298,12 @@ export interface ILanguageRuntimePackage { /** Publication/release date */ publishedDate?: string; + + /** Optional short description or summary. */ + description?: string; + + /** Optional package author. */ + author?: string; } /**