/ src / modules / cmdpal / ext / SamplePagesExtension / EvilSamplesPage.cs
EvilSamplesPage.cs
  1  // Copyright (c) Microsoft Corporation
  2  // The Microsoft Corporation licenses this file to you under the MIT license.
  3  // See the LICENSE file in the project root for more information.
  4  
  5  using System.Linq;
  6  using System.Threading;
  7  using System.Threading.Tasks;
  8  using Microsoft.CommandPalette.Extensions;
  9  using Microsoft.CommandPalette.Extensions.Toolkit;
 10  using Windows.System;
 11  
 12  namespace SamplePagesExtension;
 13  
 14  public partial class EvilSamplesPage : ListPage
 15  {
 16      private readonly IListItem[] _commands = [
 17          new ListItem(new EvilSampleListPage())
 18          {
 19              Title = "List Page without items",
 20              Subtitle = "Throws exception on GetItems",
 21          },
 22          new ListItem(new ExplodeInFiveSeconds(false))
 23          {
 24              Title = "Page that will throw an exception after loading it",
 25              Subtitle = "Throws exception on GetItems _after_ a ItemsChanged",
 26          },
 27          new ListItem(new ExplodeInFiveSeconds(true))
 28          {
 29              Title = "Page that keeps throwing exceptions",
 30              Subtitle = "Will throw every 5 seconds once you open it",
 31          },
 32          new ListItem(new ExplodeOnPropChange())
 33          {
 34              Title = "Throw in the middle of a PropChanged",
 35              Subtitle = "Will throw every 5 seconds once you open it",
 36          },
 37          new ListItem(new SelfImmolateCommand())
 38          {
 39              Title = "Terminate this extension",
 40              Subtitle = "Will exit this extension (while it's loaded!)",
 41          },
 42          new ListItem(new EvilSlowDynamicPage())
 43          {
 44              Title = "Slow loading Dynamic Page",
 45              Subtitle = "Takes 5 seconds to load each time you type",
 46              Tags = [new Tag("GH #38190")],
 47          },
 48          new ListItem(new EvilFastUpdatesPage())
 49          {
 50              Title = "Fast updating Dynamic Page",
 51              Subtitle = "Updates in the middle of a GetItems call",
 52              Tags = [new Tag("GH #41149")],
 53          },
 54          new ListItem(new NoOpCommand())
 55          {
 56             Title = "I have lots of nulls",
 57             Subtitle = null,
 58             MoreCommands = null,
 59             Tags = null,
 60             Details = new Details()
 61             {
 62                 Title = null,
 63                 HeroImage = null,
 64                 Metadata = null,
 65             },
 66          },
 67          new ListItem(new NoOpCommand())
 68          {
 69             Title = "I also have nulls",
 70             Subtitle = null,
 71             MoreCommands = null,
 72             Details = new Details()
 73             {
 74                 Title = null,
 75                 HeroImage = null,
 76                 Metadata = [new DetailsElement() { Key = "Oops all nulls", Data = new DetailsTags() { Tags = null } }],
 77             },
 78          },
 79          new ListItem(new AnonymousCommand(action: () =>
 80          {
 81              ToastStatusMessage toast = new("I should appear immediately");
 82              toast.Show();
 83              Thread.Sleep(5000);
 84          }) { Result = CommandResult.KeepOpen() })
 85          {
 86             Title = "I take just forever to return something",
 87             Subtitle = "The toast should appear immediately.",
 88             MoreCommands = null,
 89             Details = new Details()
 90             {
 91                 Body = "This is a test for GH#512. If it doesn't appear immediately, it's likely InvokeCommand is happening on the UI thread.",
 92             },
 93          },
 94  
 95          // More edge cases than truly evil
 96          new ListItem(
 97              new ToastCommand("Primary command invoked", MessageState.Info) { Name = "Primary command", Icon = new IconInfo("\uF146") }) // dial 1
 98          {
 99              Title = "anonymous command test",
100              Subtitle = "Try pressing Ctrl+1 with me selected",
101              Icon = new IconInfo("\uE712"),  // "More" dots
102              MoreCommands = [
103                  new CommandContextItem(
104                      new ToastCommand("Secondary command invoked", MessageState.Warning) { Name = "Secondary command", Icon = new IconInfo("\uF147") }) // dial 2
105                  {
106                      Title = "I'm a second command",
107                      RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.Number1),
108                  },
109                  new CommandContextItem("nested...")
110                  {
111                      Title = "We can go deeper...",
112                      Icon = new IconInfo("\uF148"),
113                      RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.Number2),
114                      MoreCommands = [
115                          new CommandContextItem(
116                              new ToastCommand("Nested A invoked") { Name = "Do it", Icon = new IconInfo("A") })
117                          {
118                              Title = "Nested A",
119                              RequestedShortcut = KeyChordHelpers.FromModifiers(alt: true, vkey: VirtualKey.A),
120                          },
121  
122                          new CommandContextItem(
123                              new ToastCommand("Nested B invoked") { Name = "Do it", Icon = new IconInfo("B") })
124                          {
125                              Title = "Nested B...",
126                              RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.B),
127                              MoreCommands = [
128                                  new CommandContextItem(
129                                      new ToastCommand("Nested C invoked") { Name = "Do it" })
130                                  {
131                                      Title = "You get it",
132                                      RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.B),
133                                  }
134                              ],
135                          },
136                      ],
137                  }
138              ],
139          },
140          new ListItem(
141              new ToastCommand("Primary command invoked", MessageState.Info) { Name = "Primary command", Icon = new IconInfo("\uF146") }) // dial 1
142          {
143              Title = "noop command test",
144              Subtitle = "Try pressing Ctrl+1 with me selected",
145              Icon = new IconInfo("\uE712"),  // "More" dots
146              MoreCommands = [
147                  new CommandContextItem(
148                      new ToastCommand("Secondary command invoked", MessageState.Warning) { Name = "Secondary command", Icon = new IconInfo("\uF147") }) // dial 2
149                  {
150                      Title = "I'm a second command",
151                      RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.Number1),
152                  },
153                  new CommandContextItem(new NoOpCommand())
154                  {
155                      Title = "We can go deeper...",
156                      Icon = new IconInfo("\uF148"),
157                      RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.Number2),
158                      MoreCommands = [
159                          new CommandContextItem(
160                              new ToastCommand("Nested A invoked") { Name = "Do it", Icon = new IconInfo("A") })
161                          {
162                              Title = "Nested A",
163                              RequestedShortcut = KeyChordHelpers.FromModifiers(alt: true, vkey: VirtualKey.A),
164                          },
165  
166                          new CommandContextItem(
167                              new ToastCommand("Nested B invoked") { Name = "Do it", Icon = new IconInfo("B") })
168                          {
169                              Title = "Nested B...",
170                              RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.B),
171                              MoreCommands = [
172                                  new CommandContextItem(
173                                      new ToastCommand("Nested C invoked") { Name = "Do it" })
174                                  {
175                                      Title = "You get it",
176                                      RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.B),
177                                  }
178                              ],
179                          },
180                      ],
181                  }
182              ],
183          },
184          new ListItem(
185              new ToastCommand("Primary command invoked", MessageState.Info) { Name = "Primary command", Icon = new IconInfo("\uF146") }) // dial 1
186          {
187              Title = "noop secondary command test",
188              Subtitle = "Try pressing Ctrl+1 with me selected",
189              Icon = new IconInfo("\uE712"),  // "More" dots
190              MoreCommands = [
191                  new CommandContextItem(new NoOpCommand())
192                  {
193                      Title = "We can go deeper...",
194                      Icon = new IconInfo("\uF148"),
195                      RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.Number2),
196                      MoreCommands = [
197                          new CommandContextItem(
198                              new ToastCommand("Nested A invoked") { Name = "Do it", Icon = new IconInfo("A") })
199                          {
200                              Title = "Nested A",
201                              RequestedShortcut = KeyChordHelpers.FromModifiers(alt: true, vkey: VirtualKey.A),
202                          },
203  
204                          new CommandContextItem(
205                              new ToastCommand("Nested B invoked") { Name = "Do it", Icon = new IconInfo("B") })
206                          {
207                              Title = "Nested B...",
208                              RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.B),
209                              MoreCommands = [
210                                  new CommandContextItem(
211                                      new ToastCommand("Nested C invoked") { Name = "Do it" })
212                                  {
213                                      Title = "You get it",
214                                      RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.B),
215                                  }
216                              ],
217                          },
218                      ],
219                  }
220              ],
221          },
222          new ListItem(
223              new ToastCommand("Primary command invoked", MessageState.Info) { Name = "H W\r\nE O\r\nL R\r\nL L\r\nO D", Icon = new IconInfo("\uF146") })
224          {
225              Title = "noop third command test",
226              Icon = new IconInfo("\uE712"),  // "More" dots
227          },
228          new ListItem(new EvilDuplicateRequestedShortcut())
229          {
230              Title = "Evil keyboard shortcuts",
231              Subtitle = "Two commands with the same shortcut and more...",
232              Icon = new IconInfo("\uE765"),
233          },
234      ];
235  
236      public EvilSamplesPage()
237      {
238          Name = "Evil Samples";
239          Icon = new IconInfo("👿"); // Info
240      }
241  
242      public override IListItem[] GetItems() => _commands;
243  }
244  
245  [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Sample code")]
246  internal sealed partial class ExplodeOnPropChange : ListPage
247  {
248      private bool _explode;
249  
250      public override string Title
251      {
252          get => _explode ? Commands[9001].Title : base.Title;
253          set => base.Title = value;
254      }
255  
256      private IListItem[] Commands => [
257        new ListItem(new NoOpCommand())
258             {
259                 Title = "This page will explode in five seconds!",
260                 Subtitle = "I'll change my Name, then explode",
261             },
262          ];
263  
264      public ExplodeOnPropChange()
265      {
266          Icon = new IconInfo(string.Empty);
267          Name = "Open";
268      }
269  
270      public override IListItem[] GetItems()
271      {
272          _ = Task.Run(() =>
273          {
274              Thread.Sleep(1000);
275              Title = "Ready? 3...";
276              Thread.Sleep(1000);
277              Title = "Ready? 2...";
278              Thread.Sleep(1000);
279              Title = "Ready? 1...";
280              Thread.Sleep(1000);
281              _explode = true;
282              Title = "boom";
283          });
284          return Commands;
285      }
286  }
287  
288  /// <summary>
289  /// This sample simulates a long delay in handling UpdateSearchText. I've found
290  /// that if I type "124356781234", then somewhere around the second "1234",
291  /// we'll get into a state where the character is typed, but then CmdPal snaps
292  /// back to a previous query.
293  ///
294  /// We can use this to validate that we're always sticking with the last
295  /// SearchText. My guess is that it's a bug in
296  /// Toolkit.DynamicListPage.SearchText.set
297  ///
298  /// see GH #38190
299  /// </summary>
300  [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Sample code")]
301  internal sealed partial class EvilSlowDynamicPage : DynamicListPage
302  {
303      private IListItem[] _items = [];
304  
305      public EvilSlowDynamicPage()
306      {
307          Icon = new IconInfo(string.Empty);
308          Name = "Open";
309          Title = "Evil Slow Dynamic Page";
310          PlaceholderText = "Type to see items appear after a delay";
311      }
312  
313      public override void UpdateSearchText(string oldSearch, string newSearch)
314      {
315          DoQuery(newSearch);
316          RaiseItemsChanged(newSearch.Length);
317      }
318  
319      public override IListItem[] GetItems()
320      {
321          return _items.Length > 0 ? _items : DoQuery(SearchText);
322      }
323  
324      private IListItem[] DoQuery(string newSearch)
325      {
326          IsLoading = true;
327  
328          // Sleep for longer for shorter search terms
329          var delay = 10000 - (newSearch.Length * 2000);
330          delay = delay < 0 ? 0 : delay;
331          if (newSearch.Length == 0)
332          {
333              delay = 0;
334          }
335  
336          delay += 50;
337  
338          Thread.Sleep(delay); // Simulate a long load time
339  
340          var items = newSearch.ToCharArray().Select(ch => new ListItem(new NoOpCommand()) { Title = ch.ToString() }).ToArray();
341          if (items.Length == 0)
342          {
343              items = [new ListItem(new NoOpCommand()) { Title = "Start typing in the search box" }];
344          }
345  
346          if (items.Length > 0)
347          {
348              items[0].Subtitle = "Notice how the number of items changes for this page when you type in the filter box";
349          }
350  
351          IsLoading = false;
352  
353          return items;
354      }
355  }
356  
357  /// <summary>
358  /// A sample for a page that updates its items in the middle of a GetItems call.
359  /// In this sample, we're returning 10000 items, which genuinely marshal slowly
360  /// (even before we start retrieving properties from them).
361  ///
362  ///  While we're in the middle of the marshalling of that GetItems call, the
363  ///  background thread we started will kick off another GetItems (via the
364  ///  RaiseItemsChanged).
365  ///
366  /// That second GetItems will return a single item, which marshals quickly.
367  /// CmdPal _should_ only display that single green item. However, as of v0.4,
368  /// we'll display that green item, then "snap back" to the red items, when they
369  /// finish marshalling.
370  ///
371  /// See GH #41149
372  /// </summary>
373  [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Sample code")]
374  internal sealed partial class EvilFastUpdatesPage : DynamicListPage
375  {
376      private static readonly IconInfo _red = new("🔴"); // "Red" icon
377      private static readonly IconInfo _green = new("🟢"); // "Green" icon
378  
379      private IListItem[] _redItems = [];
380      private IListItem[] _greenItems = [];
381      private bool _sentRed;
382  
383      public EvilFastUpdatesPage()
384      {
385          Icon = new IconInfo(string.Empty);
386          Name = "Open";
387          Title = "Evil Fast Updates Page";
388          PlaceholderText = "Type to trigger an update";
389  
390          _redItems = Enumerable.Range(0, 10000).Select(i => new ListItem(new NoOpCommand())
391          {
392              Icon = _red,
393              Title = $"Item {i + 1}",
394              Subtitle = "CmdPal is doing it wrong",
395          }).ToArray();
396          _greenItems = [new ListItem(new NoOpCommand()) { Icon = _green, Title = "It works" }];
397      }
398  
399      public override void UpdateSearchText(string oldSearch, string newSearch)
400      {
401          _sentRed = false;
402          RaiseItemsChanged();
403      }
404  
405      public override IListItem[] GetItems()
406      {
407          if (!_sentRed)
408          {
409              IsLoading = true;
410              _sentRed = true;
411  
412              // kick off a task to update the items after a delay
413              _ = Task.Run(() =>
414              {
415                  Thread.Sleep(5);
416                  RaiseItemsChanged();
417              });
418  
419              return _redItems;
420          }
421          else
422          {
423              IsLoading = false;
424              return _greenItems;
425          }
426      }
427  }
428  
429  [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Sample code")]
430  internal sealed partial class EvilDuplicateRequestedShortcut : ListPage
431  {
432      private readonly IListItem[] _items =
433      [
434          new ListItem(new NoOpCommand())
435          {
436              Title = "I'm evil!",
437              Subtitle = "I have multiple commands sharing the same keyboard shortcut",
438              MoreCommands = [
439                  new CommandContextItem(new AnonymousCommand(() => new ToastStatusMessage("Me too executed").Show())
440                  {
441                      Result = CommandResult.KeepOpen(),
442                  })
443                  {
444                      Title = "Me too",
445                      RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.Number1),
446                  },
447                  new CommandContextItem(new AnonymousCommand(() => new ToastStatusMessage("Me three executed").Show())
448                  {
449                      Result = CommandResult.KeepOpen(),
450                  })
451                  {
452                      Title = "Me three",
453                      RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.Number1),
454                  },
455              ],
456          },
457      ];
458  
459      public override IListItem[] GetItems() => _items;
460  
461      public EvilDuplicateRequestedShortcut()
462      {
463          Icon = new IconInfo(string.Empty);
464          Name = "Open";
465      }
466  }