Implement sort logic for winget list output #6191
Implement sort logic for winget list output
#6191Madhusudhan-MSFT wants to merge 7 commits intomicrosoft:masterfrom
Conversation
… add comprehensive tests
…pp file ## Summary Every other Workflow header in the project follows the declarations-only convention with a matching .cpp file. ListSortHelper.h was the only outlier using inline implementations. This refactor aligns it with the codebase convention and eliminates the duplicated sort loop between ListSortHelper.h and WorkflowBase.cpp. ## Changes - **ListSortHelper.h → declarations only**: Remove inline implementations of CompareByField and SortEntries, keeping only the struct definition and function signatures - **ListSortHelper.cpp**: New compilation unit with the extracted implementations of CompareByField and SortEntries - **WorkflowBase.cpp sort integration**: Replace the duplicated inline sort loop with an index-based permutation sort that projects InstalledPackagesTableLine into SortablePackageEntry and delegates field comparison to the shared CompareByField - Remove the local CompareByField adapter that was creating temporary SortablePackageEntry copies per comparison call - Align include path and grouping with repo conventions (bare sibling name, local before system) - Simplify GetArgs access to match established MultiQueryFlow pattern ## Impact - Sort logic now lives in a single shared location, reusable by future commands (search, upgrade, font list) without duplication - Follows the established Workflows/ header convention (24/24 other headers use .h + .cpp) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add --sort, --ascending, and --descending options to the options table. Add new 'Sorting output' section documenting: - CLI sort arguments with multi-field examples - Settings-based sortOrder configuration - Resolution order (CLI > settings > default) - Relevance protection behavior for queries
Validate --sort argument values in Command::ValidateArguments following the same pattern as --scope and --authentication-mode validation. Invalid values produce a clear error message listing valid options (name, id, version, source, available, relevance). Centralized in Command.cpp so any future command adding --sort gets validation automatically.
This comment has been minimized.
This comment has been minimized.
|
Is this only applicable to |
This seems like it would be counter to what a user would want. If I did |
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The current implementation is scoped to |
This was discussed and agreed upon during the initial design. The intent is to preserve source relevance ordering for queries since a settings-based sort could push the best match down without the user realizing why. Explicit @denelon — I believe this aligns with what we discussed during the design phase, would you mind confirming? |
I think there is, but agree with the approach of limiting the scope for now and letting Demitrius figure out when to bring the rest in |
From the design portion, my feedback was that the default behavior should be to remain unchanged with a query but could have a new ordering without a query. If the user does supply a setting value, my expectation would be that it would be used with or without a query unless overridden with a command line argument. What constitutes a query? Probably only the actual |
…edback - Remove HasRelevanceAffectingArgs function (overly broad filter) - Narrow relevance preservation to only the free-text query argument - Settings output.sortOrder now applies even when query is present (user explicitly configured sort preference takes priority) - Filter args (--id, --name, --tag etc.) no longer block settings sort since they use exact/substring matching with no meaningful relevance - Update list.md documentation to reflect new behavior - Resolution: CLI --sort > settings (always) > default relevance (query only)
Thanks for the clarification, John. Updated in
Removed |
|
|
||
| ## Sorting output | ||
|
|
||
| By default, results are displayed in the order returned by the package source (no sorting applied). You can control sorting through command-line arguments or user settings. |
There was a problem hiding this comment.
This depends on whether there is a query or not, correct?
| Argument{ Execution::Args::Type::IncludePinned, Resource::String::IncludePinnedInListArgumentDescription, ArgumentType::Flag}, | ||
| Argument::ForType(Execution::Args::Type::ListDetails), | ||
| Argument{ Execution::Args::Type::Sort, Resource::String::SortArgumentDescription, ArgumentType::Standard }, | ||
| // 7 = one more than the number of user-facing SortField values (Relevance, Name, Id, Version, Source, Available), |
There was a problem hiding this comment.
If there is going to be a limit, it should be the number of options. A test that provides all of them can detect a failure to update that limit if more are added.
| namespace AppInstaller::CLI::Workflow | ||
| { | ||
| // Lightweight sortable representation of a package line in list output. | ||
| // Decoupled from ICompositePackage/IPackageVersion to enable unit testing. |
There was a problem hiding this comment.
| // Decoupled from ICompositePackage/IPackageVersion to enable unit testing. | |
| // Decoupled from ICompositePackage/IPackageVersion to ease unit testing. |
Claiming that interfaces make it impossible to unit test is a bit untrue.
| switch (field) | ||
| { | ||
| case SortField::Name: | ||
| return Utility::FoldCase(std::string_view{ a.Name.get() }).compare(Utility::FoldCase(std::string_view{ b.Name.get() })); |
There was a problem hiding this comment.
Operations like FoldCase are expensive, the SortablePackageEntry should contain precomputed comparable values for all fields rather than computing them every single time.
| } | ||
| } | ||
|
|
||
| // Returns true if the execution context contains filter arguments that |
There was a problem hiding this comment.
Doesn't appear to return a value.
| // have equivalent relevance. | ||
| // If the user explicitly configured output.sortOrder, respect it even | ||
| // with queries — that is an explicit user preference. | ||
| if (sortFields.empty() && context.Args.Contains(Execution::Args::Type::Query)) |
There was a problem hiding this comment.
When sortFields is empty but no query is provided, shouldn't we be setting the actual default sort fields here?
| sortable.reserve(lines.size()); | ||
| for (const auto& line : lines) | ||
| { | ||
| sortable.push_back({ line.Name, line.Id, line.InstalledVersion, line.AvailableVersion, line.Source }); |
There was a problem hiding this comment.
From another comment, we actually want to construct all of the comparable properties, not just copy. Have a constructor that does so.
Additionally, we could optimize by only constructing the properties that we would actually compare based on the sortFields. If the enum values were converted to flag bits, it could be efficiently checked.
| std::vector<size_t> indices(lines.size()); | ||
| std::iota(indices.begin(), indices.end(), 0); |
There was a problem hiding this comment.
Why not have the entries just contain their related index? Then the separated sort function makes more sense (and can easily be called from this one).
| #include "WorkflowBase.h" | ||
| #include "ExecutionContext.h" | ||
| #include <winget/ManifestComparator.h> | ||
| #include "ListSortHelper.h" |
There was a problem hiding this comment.
| #include "ListSortHelper.h" | |
| #include "PackageTableSortHelper.h" |
Eventually not used exclusively for list.
| // produce relevance-ordered results (query, name, id, moniker, tag, command). | ||
| // Sorts a vector of InstalledPackagesTableLine according to the user's sort preferences. | ||
| // Resolution order: CLI args (--sort) > settings (output.sortOrder) > query-aware default. | ||
| void SortInstalledPackagesTableLines(Execution::Context& context, std::vector<InstalledPackagesTableLine>& lines) |
There was a problem hiding this comment.
This function should absolutely be in the sort helper, templated. And then it should be refactored to extract the sort fields and order determination out from the only part that needs to be templated: conversion from input vector to sortables, then sorting of the input vector via the indices.
Summary
Implements the sort logic for
winget listoutput, completing the feature requested in #4238. This builds on PR #6177 (settings parsing + CLI argument handling) to add the actual sorting behavior.Changes
Sort orchestration (
WorkflowBase.cpp):SortInstalledPackagesTableLines— resolves sort fields from CLI--sortargs > settingsoutput.sortOrder> default (no sort), determines direction, projects rows intoSortablePackageEntry, sorts viastd::stable_sort, and applies the permutation back.output.sortOrder, it is respected even with queries.Sort helper (
ListSortHelper.h/.cpp):SortablePackageEntry— lightweight struct holding the sortable string fields.CompareByField— case-insensitive ordinal comparison viaFoldCasefor a givenSortField.SortEntries— standalonestable_sortwrapper (used by unit tests).CLI integration (
ListCommand.cpp):--sort(repeatable, limit 7),--ascending/--asc,--descending/--descarguments on the list command.Argument validation (
Command.cpp):--sortvalues inCommand::ValidateArguments, following the same pattern as--scopeand--authentication-mode. Invalid values produce a clear error message listing valid options. Centralized so any future command adding--sortgets validation automatically.--ascendingand--descendingmutual exclusivity is enforced viaArgTypeExclusiveSet::SortDirection(from PR [Settings, ListCommand] Add sort types, settings infrastructure, and CLI arguments for output ordering #6177).Documentation (
list.md):--sort,--ascending,--descendingto the options table.Design decisions
-q) is used and nooutput.sortOrderis configured, results keep their relevance ranking. If the user has explicitly configured a sort preference in settings, it is respected even with queries — the user's explicit choice takes priority. Filter arguments (--id,--name,--tagetc.) don't affect relevance since they use exact/substring matching.stable_sort--sort source --sort relevancegroups by source while maintaining relevance order within each group).relevanceas sort fieldCompareByField(no-op), making it a placeholder for future match-score support. Currently serves as a way to preserve source-defined ordering in multi-field sorts.ConvertToSortField(from PR #6177) converts string values toSortFieldenum. Invalid values are silently skipped with a warning logged.How validated
Unit tests — 19 test cases (286 lines) in
ListSort.cppcovering:Manual testing — 34 scenarios on
wingetdev(Debug x64 deployed to AppX):--sort name/id/version/source/available--descending,--ascending(explicit)--sort source --sort name,--sort source --sort id--sortoverrides relevance; without--sortpreserves it--source winget,--source devsource--sort relevance(no-op), source→relevance (groups by source, relevance within), relevance→name (effectively alphabetical)sortOrder: ["name"],sortOrder: ["source","name"]["name"]+ CLI--sort id→ CLI wins["name"]+ query → settings sort applied (user preference respected)["relevance"](no-op),["source","relevance"](Trenly's scenario)["invalid_field"]→ graceful fallback to no sort--sort foobar→ clear error listing valid values--ascending --descending→ mutual exclusivity error["source"]+ CLI--descending→ sort by source descending["name"]+ query +--sort id→ CLI wins over settings--idfilter + settings["source","name"]→ settings applied (not blocked)Key behavior to note: When no sort preference is configured and a free-text query is used, relevance ordering is preserved by default. If the user explicitly configures
output.sortOrderin settings, that preference is respected even with queries. Only the positional query argument (-q) has meaningful relevance — filter args like--id/--namedon't block settings sort. The--ascending/--descendingdirection flags apply to whichever sort fields are active (CLI or settings).Fixes #4238