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; + } + } +}