Implement sort logic for winget list output #6191
Implement sort logic for winget list output
#6191Madhusudhan-MSFT wants to merge 15 commits intomicrosoft:masterfrom
Conversation
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 |
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 |
Thanks for the clarification, John. Updated in
Removed |
d92410d to
5aae88f
Compare
… 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.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…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)
- Fix comment: 'enable' -> 'ease' unit testing (not impossible, just decoupled) - Remove stale comment from deleted HasRelevanceAffectingArgs function - Add THROW_HR(E_UNEXPECTED) assertion for invalid sort values that bypass validation - Change --sort count limit from 7 to 6 (matches actual SortField enum count)
When no sort preferences are configured (no CLI --sort, no settings) and no positional query is present, default to sorting by name ascending for deterministic, user-friendly output. With a positional query, relevance ordering is preserved unless the user explicitly configures sort preferences. Update list.md to document this conditional default behavior.
Restructure ListSortHelper to own the production sort path: - SortablePackageEntry now precomputes case-folded strings and parsed versions at construction time, avoiding repeated FoldCase/Version parsing during comparisons. - Add OriginalIndex field so entries track their source position. - Add SortBy<T> template that converts arbitrary item vectors into SortablePackageEntry projections, sorts, then reorders the source vector to match. This is the production code path. - WorkflowBase calls SortBy with a conversion lambda, replacing the inline permutation sort. - SortEntries is no longer test-only — it is the same code path that production uses via SortBy. - Add SortBy template tests to validate the end-to-end pipeline with custom source types.
Rename to reflect broader scope — the helper sorts package table rows for any table-based command (list, search, upgrade), not just list.
Change SortField to flag-bit enum (uint32_t) and add DEFINE_ENUM_FLAG_OPERATORS so sort fields compose into a bitmask. SortBy computes the mask once and passes it through the converter to SortablePackageEntry's constructor, which now only precomputes fields that are actually needed for the requested sort.
- Add static_assert on SortField::Available at enum definition site to catch new values that bypass constructor handling. - Replace invalid-sort assertion with FAIL_FAST_MSG for clearer diagnostics. - Collapse two consecutive sortFields.empty() checks into a single branch. - Format SortablePackageEntry constructor arguments one-per-line. - Add --query example to list.md Example queries section.
- `WI_IsAnyFlagSet` was used for single-flag checks where `WI_IsFlagSet` is the correct API - `ComputeSortFieldMask` was defined inline in the header instead of the .cpp file - `SortInstalledPackagesTableLines` was called at three separate call sites instead of being encapsulated inside `OutputInstalledPackages` - Default-sort bypass only checked `Query` argument, missing the `MultiQuery` path - Test assertions validated only a single field (name or ID), providing no cross-field verification of sort correctness - Replace all five `WI_IsAnyFlagSet` calls with `WI_IsFlagSet` for single-flag conditionals - Move `ComputeSortFieldMask` body to PackageTableSortHelper.cpp, leaving only the declaration in the header - Consolidate sort invocation inside `OutputInstalledPackages`, reducing three call sites to one - Add `MultiQuery` argument check alongside `Query` for relevance-preserving default - Replace `GetNames`/`GetIds` test helpers with `ValidateSortResult` that asserts all six precomputed fields (FoldedName, FoldedId, FoldedSource, HasAvailableVersion, ParsedInstalledVersion, ParsedAvailableVersion) per entry - Add release notes entry for sortable list output - No behavioral change to end users — fixes are code quality improvements and correctness for the `--query` multi-value path - Test suite now validates complete sort state rather than a single projection, catching regressions in any field computation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
e8ba2d5 to
c19ce84
Compare
There was a problem hiding this comment.
nit: Eventually all of this "determine the sort fields and order" will end up in the helper file as well.
There was a problem hiding this comment.
I meant all of it. I don't see a reason to pessimistically split out the context/argument extraction from the resolution portion. If we expand to other commands, the arguments are likely to stay the same. Maybe certain fields are not valid and can be checked by those commands, but this shared portion should be able to stay unchanged.
## Problem - Sort argument count limit was hardcoded to a magic number (6), creating drift risk when new SortField values are added - SortField::Relevance occupied 0x0, conflicting with bitmask zero-initialization semantics - Sort resolution logic was inlined in WorkflowBase.cpp, making it untestable in isolation - Available version comparison used a separate bool flag instead of leveraging std::optional semantics - Missing WI_ASSERT guards on default switch cases in CompareByField ## Solution - Replace hardcoded SetCountLimit(6) with dynamic computation via GetAllExponentialEnumValues - Add SortField::None = 0x0 for bitmask initialization, move Relevance to 0x20, add Max = 0x40 sentinel - Extract ResolveSortParameters() into PackageTableSortHelper as a self-contained testable function - Replace HasAvailableVersion bool with std::optional<Version> throughout sort pipeline - Add WI_ASSERT(false) to default cases in both GetSortKey and CompareByField - Simplify EnsureListSortFieldCountMatchesLimit test to assert (1u << limit) == Max - Remove redundant ValidateArguments_StandardArgExceedsCountLimit test (covered by framework) - Add using-declaration for Settings namespace to reduce qualification noise ## How Validated - Full solution build (AppInstallerCLICore + AppInstallerCLITests): clean, zero warnings - All 19 sort-related test cases pass (199 assertions) - Spell check: no CI violations - End-to-end wingetdev deployment verified across 12 scenarios including settings, query, multi-sort, order direction, and invalid input edge cases
| { | ||
| using Settings::SortField; | ||
| SortParameters result; | ||
| std::vector<SortField> sortFields; |
There was a problem hiding this comment.
nit: why not just use result.Fields directly?
If the answer is allowing [relevance] to be returned, I don't see a problem. It is true, and ShouldSort is what you expect anyone to look at first.
| } | ||
|
|
||
| // Resolve direction | ||
| SortDirection direction = SortDirection::Ascending; |
There was a problem hiding this comment.
nit: same here
For both of these it is more a style/preference thing as performance should be equivalent. I just personally like removing the 4 lines of code but I'm open to hearing about reasons not to. I can imagine a reason that is about first evaluating and then collecting the data independently for clarity. I can also imagine a NRVO type argument (although maybe this should be the constructor of SortParameters if that is a concern).
| const std::vector<SortField>& sortFields, | ||
| SortDirection direction) |
|
|
||
| // Relevance-only means no sorting | ||
| if (sortFields.size() == 1 && sortFields[0] == Settings::SortField::Relevance) | ||
| if (sortFields.size() == 1 && sortFields[0] == SortField::Relevance) |
| // hasQuery - whether a free-text query argument is present | ||
| // hasExplicitAscending - whether --ascending was passed | ||
| // hasExplicitDescending - whether --descending was passed | ||
| SortParameters ResolveSortParameters( |
There was a problem hiding this comment.
Why not be a constructor of SortParameters? You can leave the default constructor included as well if needed.
| 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 }.SetCountLimit(6), | ||
| Argument{ Execution::Args::Type::Sort, Resource::String::SortArgumentDescription, ArgumentType::Standard }.SetCountLimit(AppInstaller::GetAllExponentialEnumValues<Settings::SortField>(Settings::SortField::None).size()), |
There was a problem hiding this comment.
nit: There is probably a more efficient way that we could do this at compile time, like consteval size_t GetExponentialEnumValuesCount<Enum> that does the same algorithm just counting the number of items until we hit Max.
There was a problem hiding this comment.
I meant all of it. I don't see a reason to pessimistically split out the context/argument extraction from the resolution portion. If we expand to other commands, the arguments are likely to stay the same. Maybe certain fields are not valid and can be checked by those commands, but this shared portion should be able to stay unchanged.
| REQUIRE(actual[i].ParsedAvailableVersion.has_value() == expected[i].ParsedAvailableVersion.has_value()); | ||
| REQUIRE(actual[i].ParsedInstalledVersion == expected[i].ParsedInstalledVersion); | ||
| REQUIRE(actual[i].ParsedAvailableVersion == expected[i].ParsedAvailableVersion); | ||
| if (actual[i].ParsedAvailableVersion.has_value() && expected[i].ParsedAvailableVersion.has_value()) |
There was a problem hiding this comment.
optional gives you all of these ParsedAvailableVersion checks at once through its equality operator.
Summary
Implements the sort logic for
winget listoutput, completing the feature requested in #4238. Builds on PR #6177 (settings parsing + CLI handling).Changes
Sort orchestration (
WorkflowBase.cpp):SortInstalledPackagesTableLines— resolves sort fields from CLI--sort> settingsoutput.sortOrder> query-aware default, determines direction, sorts viastd::stable_sort, applies permutation back.-q) is used and no sort preference is configured, relevance ordering is preserved. Ifoutput.sortOrderis configured, it is respected even with queries.FAIL_FAST_MSGfor unrecognized sort field values — ensures invalid enum values crash loudly in debug builds.Sort helper (
PackageTableSortHelper.h/.cpp):SortablePackageEntry— holds pre-computed case-folded sortable fields, computed only for fields needed viaComputeSortFieldMaskbitmask optimization.CompareByField— case-insensitive ordinal comparison using pre-computedFoldCasevalues.SortBy— template sort executor used by the workflow; standaloneSortEntrieswrapper available for unit tests.SortFieldenum uses flag-bit values (uint32_t) withDEFINE_ENUM_FLAG_OPERATORSfor efficient bitmask field tracking.CLI integration (
ListCommand.cpp):--sort(repeatable, limit 6),--ascending/--asc,--descending/--descarguments.Argument validation (
Command.cpp):--sortvalues inValidateArguments, following the--scope/--authentication-modepattern.Documentation (
list.md):--sort,--ascending,--descendingin options table. New "Sorting output" section.Previously,
winget listoutput preserved the source's natural ordering (source-determined relevance ranking). This PR changes the default to sort alphabetically by name (ascending) when no--sortargument, no-qquery, and nooutput.sortOrdersetting is configured.--sort-qquery with no sort preference--sort relevanceor"sortOrder": ["relevance"]"sortOrder": [](empty array)output.sortOrderconfigured +-qqueryTo restore previous (source-determined) ordering:
winget list --sort relevance{ "output": { "sortOrder": ["relevance"] } }Design decisions
-q) without configured sort preserves source relevance ranking. Settings override this if configured — explicit user preference takes priority.stable_sort--sort source --sort namegroups by source, alphabetical within).SortFieldenumComputeSortFieldMaskto skip unused field extraction — measurable win for large package lists.FoldCaseFAIL_FAST_MSGon invalid sortHow validated
Unit tests — 20 test cases:
PackageTableSortHelper.cpp: single/multi-field sorting, both directions, edge cases, case-insensitive comparison, relevance no-opCommand.cpp: readsListCommand::GetArguments()limit and validates against knownConvertToSortFieldstrings — breaks if enum changes without updating limitCommand.cpp: verifiesSetCountLimit(6)rejects 7--sortvalues withTooManyArgErrorManual testing — 21 scenarios on
wingetdev(Debug x64): defaults, single/multi-field sorts, direction flags, query + sort interaction, settings precedence, edge cases, invalid input. All passing.Fixes #4238