diff --git a/src/modules/cmdpal/ext/SamplePagesExtension/EvilSamplesPage.cs b/src/modules/cmdpal/ext/SamplePagesExtension/EvilSamplesPage.cs
index 2fc1218bd7..21e033f8da 100644
--- a/src/modules/cmdpal/ext/SamplePagesExtension/EvilSamplesPage.cs
+++ b/src/modules/cmdpal/ext/SamplePagesExtension/EvilSamplesPage.cs
@@ -2,6 +2,7 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
+using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CommandPalette.Extensions;
@@ -13,31 +14,43 @@ namespace SamplePagesExtension;
public partial class EvilSamplesPage : ListPage
{
private readonly IListItem[] _commands = [
- new ListItem(new EvilSampleListPage())
- {
- Title = "List Page without items",
- Subtitle = "Throws exception on GetItems",
- },
- new ListItem(new ExplodeInFiveSeconds(false))
- {
- Title = "Page that will throw an exception after loading it",
- Subtitle = "Throws exception on GetItems _after_ a ItemsChanged",
- },
- new ListItem(new ExplodeInFiveSeconds(true))
- {
- Title = "Page that keeps throwing exceptions",
- Subtitle = "Will throw every 5 seconds once you open it",
- },
- new ListItem(new ExplodeOnPropChange())
- {
- Title = "Throw in the middle of a PropChanged",
- Subtitle = "Will throw every 5 seconds once you open it",
- },
- new ListItem(new SelfImmolateCommand())
- {
- Title = "Terminate this extension",
- Subtitle = "Will exit this extension (while it's loaded!)",
- },
+ new ListItem(new EvilSampleListPage())
+ {
+ Title = "List Page without items",
+ Subtitle = "Throws exception on GetItems",
+ },
+ new ListItem(new ExplodeInFiveSeconds(false))
+ {
+ Title = "Page that will throw an exception after loading it",
+ Subtitle = "Throws exception on GetItems _after_ a ItemsChanged",
+ },
+ new ListItem(new ExplodeInFiveSeconds(true))
+ {
+ Title = "Page that keeps throwing exceptions",
+ Subtitle = "Will throw every 5 seconds once you open it",
+ },
+ new ListItem(new ExplodeOnPropChange())
+ {
+ Title = "Throw in the middle of a PropChanged",
+ Subtitle = "Will throw every 5 seconds once you open it",
+ },
+ new ListItem(new SelfImmolateCommand())
+ {
+ Title = "Terminate this extension",
+ Subtitle = "Will exit this extension (while it's loaded!)",
+ },
+ new ListItem(new EvilSlowDynamicPage())
+ {
+ Title = "Slow loading Dynamic Page",
+ Subtitle = "Takes 5 seconds to load each time you type",
+ Tags = [new Tag("GH #38190")],
+ },
+ new ListItem(new EvilFastUpdatesPage())
+ {
+ Title = "Fast updating Dynamic Page",
+ Subtitle = "Updates in the middle of a GetItems call",
+ Tags = [new Tag("GH #41149")],
+ },
new ListItem(new NoOpCommand())
{
Title = "I have lots of nulls",
@@ -260,3 +273,144 @@ internal sealed partial class ExplodeOnPropChange : ListPage
return Commands;
}
}
+
+///
+/// This sample simulates a long delay in handling UpdateSearchText. I've found
+/// that if I type "124356781234", then somewhere around the second "1234",
+/// we'll get into a state where the character is typed, but then CmdPal snaps
+/// back to a previous query.
+///
+/// We can use this to validate that we're always sticking with the last
+/// SearchText. My guess is that it's a bug in
+/// Toolkit.DynamicListPage.SearchText.set
+///
+/// see GH #38190
+///
+[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Sample code")]
+internal sealed partial class EvilSlowDynamicPage : DynamicListPage
+{
+ private IListItem[] _items = [];
+
+ public EvilSlowDynamicPage()
+ {
+ Icon = new IconInfo(string.Empty);
+ Name = "Open";
+ Title = "Evil Slow Dynamic Page";
+ PlaceholderText = "Type to see items appear after a delay";
+ }
+
+ public override void UpdateSearchText(string oldSearch, string newSearch)
+ {
+ DoQuery(newSearch);
+ RaiseItemsChanged(newSearch.Length);
+ }
+
+ public override IListItem[] GetItems()
+ {
+ return _items.Length > 0 ? _items : DoQuery(SearchText);
+ }
+
+ private IListItem[] DoQuery(string newSearch)
+ {
+ IsLoading = true;
+
+ // Sleep for longer for shorter search terms
+ var delay = 10000 - (newSearch.Length * 2000);
+ delay = delay < 0 ? 0 : delay;
+ if (newSearch.Length == 0)
+ {
+ delay = 0;
+ }
+
+ delay += 50;
+
+ Thread.Sleep(delay); // Simulate a long load time
+
+ var items = newSearch.ToCharArray().Select(ch => new ListItem(new NoOpCommand()) { Title = ch.ToString() }).ToArray();
+ if (items.Length == 0)
+ {
+ items = [new ListItem(new NoOpCommand()) { Title = "Start typing in the search box" }];
+ }
+
+ if (items.Length > 0)
+ {
+ items[0].Subtitle = "Notice how the number of items changes for this page when you type in the filter box";
+ }
+
+ IsLoading = false;
+
+ return items;
+ }
+}
+
+///
+/// A sample for a page that updates its items in the middle of a GetItems call.
+/// In this sample, we're returning 10000 items, which genuinely marshal slowly
+/// (even before we start retrieving properties from them).
+///
+/// While we're in the middle of the marshalling of that GetItems call, the
+/// background thread we started will kick off another GetItems (via the
+/// RaiseItemsChanged).
+///
+/// That second GetItems will return a single item, which marshals quickly.
+/// CmdPal _should_ only display that single green item. However, as of v0.4,
+/// we'll display that green item, then "snap back" to the red items, when they
+/// finish marshalling.
+///
+/// See GH #41149
+///
+[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Sample code")]
+internal sealed partial class EvilFastUpdatesPage : DynamicListPage
+{
+ private static readonly IconInfo _red = new("🔴"); // "Red" icon
+ private static readonly IconInfo _green = new("🟢"); // "Green" icon
+
+ private IListItem[] _redItems = [];
+ private IListItem[] _greenItems = [];
+ private bool _sentRed;
+
+ public EvilFastUpdatesPage()
+ {
+ Icon = new IconInfo(string.Empty);
+ Name = "Open";
+ Title = "Evil Fast Updates Page";
+ PlaceholderText = "Type to trigger an update";
+
+ _redItems = Enumerable.Range(0, 10000).Select(i => new ListItem(new NoOpCommand())
+ {
+ Icon = _red,
+ Title = $"Item {i + 1}",
+ Subtitle = "CmdPal is doing it wrong",
+ }).ToArray();
+ _greenItems = [new ListItem(new NoOpCommand()) { Icon = _green, Title = "It works" }];
+ }
+
+ public override void UpdateSearchText(string oldSearch, string newSearch)
+ {
+ _sentRed = false;
+ RaiseItemsChanged();
+ }
+
+ public override IListItem[] GetItems()
+ {
+ if (!_sentRed)
+ {
+ IsLoading = true;
+ _sentRed = true;
+
+ // kick off a task to update the items after a delay
+ _ = Task.Run(() =>
+ {
+ Thread.Sleep(5);
+ RaiseItemsChanged();
+ });
+
+ return _redItems;
+ }
+ else
+ {
+ IsLoading = false;
+ return _greenItems;
+ }
+ }
+}