CmdPal: Lightning-fast mode (#45764)

## Summary of the Pull Request

This PR unlocks lightning-fast mode for Command Palette:

- Hides visual and motion distractions when updating the result list:
- Ensures the first interactable result item is selected as early as
possible after the result list is updated, reducing flashing and
blinking caused by the selection highlight moving around.
- Removes the list item selection indicator animation (unfortunately by
removing the pill altogether for now) and prevents it from temporarily
appearing on other items as the selection moves.
- Adds a new "Results" section header above the home page results when
no other section is present.
- This ensures the first item on the home page has consistent visuals
and styling, preventing offsets and excessive visual changes when
elements are replaced in place.

- Improves update performance and container reuse:
- Fixes the `removed` output parameter in `ListHelper.UpdateInPlace` to
only include items that were actually removed (items that were merely
moved to a different position should not be reported as removed).
    - Adds unit tests to prevent regression.
- Updates `ListHelper.UpdateInPlace` for `ObservableCollection` to use
`Move` instead of `Remove`/`Add`, and avoids `Clear` to prevent
`ListView` resets (which force recreation of all item containers and are
expensive).
- Adds a simple cache for list page item view models to reduce
unnecessary recreation during forward incremental search.
- `ListViewModel` and `FetchItems` have no notion of item lifetime or
incremental search phase, so the cache intentionally remains simple
rather than clever.
  - Updates ListPage templates to make them a little lighter:
- Tag template uses OneTime, instead of OneWay - since Tag is immutable
- Replaces ItemsControl with ItemsRepeater for Tag list on list items
- Increases the debounce for showing the details pane and adds a
debounce for hiding it. This improves performance when browsing the list
and prevents the details pane animation from bouncing left and right

## Pictures? Moving!



https://github.com/user-attachments/assets/36428d20-cf46-4321-83c0-d94d6d4a2299



<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [x] Closes: #44407
- [x] Closes: #45691
This commit is contained in:
Jiří Polášek
2026-02-26 13:17:34 +01:00
committed by GitHub
parent 1b4641a158
commit 169bfe3f04
12 changed files with 1714 additions and 326 deletions

View File

@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
@@ -17,6 +17,9 @@ namespace Microsoft.CmdPal.UI.ViewModels.UnitTests;
[TestClass]
public partial class MainListPageResultFactoryTests
{
private static readonly Separator _resultsSeparator = new("Results");
private static readonly Separator _fallbacksSeparator = new("Fallbacks");
private sealed partial class MockListItem : IListItem
{
public string Title { get; set; } = string.Empty;
@@ -82,18 +85,22 @@ public partial class MainListPageResultFactoryTests
scoredFallback,
apps,
fallbacks,
_resultsSeparator,
_fallbacksSeparator,
appResultLimit: 10);
// Expected order:
// "Results" section header
// 100: F1, SF1, A1
// 60: SF2
// 55: A2
// 50: F2
// "Fallbacks" section header
// Then fallbacks in original order: FB1, FB2
var titles = result.Select(r => r.Title).ToArray();
#pragma warning disable CA1861 // Avoid constant arrays as arguments
CollectionAssert.AreEqual(
new[] { "F1", "SF1", "A1", "SF2", "A2", "F2", "Fallbacks", "FB1", "FB2" },
new[] { "Results", "F1", "SF1", "A1", "SF2", "A2", "F2", "Fallbacks", "FB1", "FB2" },
titles);
#pragma warning restore CA1861 // Avoid constant arrays as arguments
}
@@ -113,11 +120,14 @@ public partial class MainListPageResultFactoryTests
null,
apps,
null,
_resultsSeparator,
_fallbacksSeparator,
2);
Assert.AreEqual(2, result.Length);
Assert.AreEqual("A1", result[0].Title);
Assert.AreEqual("A2", result[1].Title);
Assert.AreEqual(3, result.Length);
Assert.AreEqual("Results", result[0].Title);
Assert.AreEqual("A1", result[1].Title);
Assert.AreEqual("A2", result[2].Title);
}
[TestMethod]
@@ -135,10 +145,13 @@ public partial class MainListPageResultFactoryTests
null,
apps,
null,
_resultsSeparator,
_fallbacksSeparator,
appResultLimit: 1);
Assert.AreEqual(1, result.Length);
Assert.AreEqual("A1", result[0].Title);
Assert.AreEqual(2, result.Length);
Assert.AreEqual("Results", result[0].Title);
Assert.AreEqual("A1", result[1].Title);
}
[TestMethod]
@@ -155,6 +168,8 @@ public partial class MainListPageResultFactoryTests
null,
apps,
null,
_resultsSeparator,
_fallbacksSeparator,
appResultLimit: 0);
Assert.AreEqual(0, result.Length);
@@ -181,12 +196,15 @@ public partial class MainListPageResultFactoryTests
null,
apps,
null,
_resultsSeparator,
_fallbacksSeparator,
appResultLimit: 1);
Assert.AreEqual(3, result.Length);
Assert.AreEqual("F1", result[0].Title);
Assert.AreEqual("A1", result[1].Title);
Assert.AreEqual("F2", result[2].Title);
Assert.AreEqual(4, result.Length);
Assert.AreEqual("Results", result[0].Title);
Assert.AreEqual("F1", result[1].Title);
Assert.AreEqual("A1", result[2].Title);
Assert.AreEqual("F2", result[3].Title);
}
[TestMethod]
@@ -203,6 +221,8 @@ public partial class MainListPageResultFactoryTests
null,
null,
fallbacks,
_resultsSeparator,
_fallbacksSeparator,
appResultLimit: 10);
Assert.AreEqual(3, result.Length);
@@ -219,6 +239,8 @@ public partial class MainListPageResultFactoryTests
null,
null,
null,
_resultsSeparator,
_fallbacksSeparator,
appResultLimit: 10);
Assert.IsNotNull(result);