-
Notifications
You must be signed in to change notification settings - Fork 8.3k
/
CommandPalette.cpp
1389 lines (1258 loc) · 57 KB
/
CommandPalette.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "pch.h"
#include "ActionPaletteItem.h"
#include "TabPaletteItem.h"
#include "CommandLinePaletteItem.h"
#include "CommandPalette.h"
#include <LibraryResources.h>
#include "CommandPalette.g.cpp"
using namespace winrt;
using namespace winrt::TerminalApp;
using namespace winrt::Windows::UI::Core;
using namespace winrt::Windows::UI::Xaml;
using namespace winrt::Windows::UI::Xaml::Controls;
using namespace winrt::Windows::System;
using namespace winrt::Windows::Foundation;
using namespace winrt::Windows::Foundation::Collections;
using namespace winrt::Microsoft::Terminal::Settings::Model;
namespace winrt::TerminalApp::implementation
{
CommandPalette::CommandPalette() :
_switcherStartIdx{ 0 }
{
InitializeComponent();
_itemTemplateSelector = Resources().Lookup(winrt::box_value(L"PaletteItemTemplateSelector")).try_as<PaletteItemTemplateSelector>();
_listItemTemplate = Resources().Lookup(winrt::box_value(L"ListItemTemplate")).try_as<DataTemplate>();
_filteredActions = winrt::single_threaded_observable_vector<winrt::TerminalApp::FilteredCommand>();
_nestedActionStack = winrt::single_threaded_vector<winrt::TerminalApp::FilteredCommand>();
_currentNestedCommands = winrt::single_threaded_vector<winrt::TerminalApp::FilteredCommand>();
_allCommands = winrt::single_threaded_vector<winrt::TerminalApp::FilteredCommand>();
_tabActions = winrt::single_threaded_vector<winrt::TerminalApp::FilteredCommand>();
_mruTabActions = winrt::single_threaded_vector<winrt::TerminalApp::FilteredCommand>();
_switchToMode(CommandPaletteMode::ActionMode);
// Whatever is hosting us will enable us by setting our visibility to
// "Visible". When that happens, set focus to our search box.
RegisterPropertyChangedCallback(UIElement::VisibilityProperty(), [this](auto&&, auto&&) {
if (Visibility() == Visibility::Visible)
{
// Force immediate binding update so we can select an item
Bindings->Update();
if (_currentMode == CommandPaletteMode::TabSwitchMode)
{
_searchBox().Visibility(Visibility::Collapsed);
_filteredActionsView().SelectedIndex(_switcherStartIdx);
_filteredActionsView().ScrollIntoView(_filteredActionsView().SelectedItem());
_filteredActionsView().Focus(FocusState::Keyboard);
// Do this right after becoming visible so we can quickly catch scenarios where
// modifiers aren't held down (e.g. command palette invocation).
_anchorKeyUpHandler();
}
else
{
_filteredActionsView().SelectedIndex(0);
_searchBox().Focus(FocusState::Programmatic);
}
TraceLoggingWrite(
g_hTerminalAppProvider, // handle to TerminalApp tracelogging provider
"CommandPaletteOpened",
TraceLoggingDescription("Event emitted when the Command Palette is opened"),
TraceLoggingWideString(L"Action", "Mode", "which mode the palette was opened in"),
TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES),
TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage));
}
else
{
// Raise an event to return control to the Terminal.
_dismissPalette();
}
});
// Focusing the ListView when the Command Palette control is set to Visible
// for the first time fails because the ListView hasn't finished loading by
// the time Focus is called. Luckily, We can listen to SizeChanged to know
// when the ListView has been measured out and is ready, and we'll immediately
// revoke the handler because we only needed to handle it once on initialization.
_sizeChangedRevoker = _filteredActionsView().SizeChanged(winrt::auto_revoke, [this](auto /*s*/, auto /*e*/) {
if (_currentMode == CommandPaletteMode::TabSwitchMode)
{
_filteredActionsView().Focus(FocusState::Keyboard);
}
_sizeChangedRevoker.revoke();
});
_filteredActionsView().SelectionChanged({ this, &CommandPalette::_selectedCommandChanged });
_appArgs.DisableHelpInExitMessage();
}
// Method Description:
// - Moves the focus up or down the list of commands. If we're at the top,
// we'll loop around to the bottom, and vice-versa.
// Arguments:
// - moveDown: if true, we're attempting to move to the next item in the
// list. Otherwise, we're attempting to move to the previous.
// Return Value:
// - <none>
void CommandPalette::SelectNextItem(const bool moveDown)
{
auto selected = _filteredActionsView().SelectedIndex();
const auto numItems = ::base::saturated_cast<int>(_filteredActionsView().Items().Size());
// Do not try to select an item if
// - the list is empty
// - if no item is selected and "up" is pressed
if (numItems != 0 && (selected != -1 || moveDown))
{
// Wraparound math. By adding numItems and then calculating modulo numItems,
// we clamp the values to the range [0, numItems) while still supporting moving
// upward from 0 to numItems - 1.
const auto newIndex = ((numItems + selected + (moveDown ? 1 : -1)) % numItems);
_filteredActionsView().SelectedIndex(newIndex);
_filteredActionsView().ScrollIntoView(_filteredActionsView().SelectedItem());
}
}
// Method Description:
// - Scroll the command palette to the specified index
// Arguments:
// - index within a list view of commands
// Return Value:
// - <none>
void CommandPalette::_scrollToIndex(uint32_t index)
{
auto numItems = _filteredActionsView().Items().Size();
if (numItems == 0)
{
// if the list is empty no need to scroll
return;
}
auto clampedIndex = std::clamp<int32_t>(index, 0, numItems - 1);
_filteredActionsView().SelectedIndex(clampedIndex);
_filteredActionsView().ScrollIntoView(_filteredActionsView().SelectedItem());
}
// Method Description:
// - Computes the number of visible commands
// Arguments:
// - <none>
// Return Value:
// - the approximate number of items visible in the list (in other words the size of the page)
uint32_t CommandPalette::_getNumVisibleItems()
{
if (const auto container = _filteredActionsView().ContainerFromIndex(0))
{
if (const auto item = container.try_as<winrt::Windows::UI::Xaml::Controls::ListViewItem>())
{
const auto itemHeight = ::base::saturated_cast<int>(item.ActualHeight());
const auto listHeight = ::base::saturated_cast<int>(_filteredActionsView().ActualHeight());
return listHeight / itemHeight;
}
}
return 0;
}
// Method Description:
// - Scrolls the focus one page up the list of commands.
// Arguments:
// - <none>
// Return Value:
// - <none>
void CommandPalette::ScrollPageUp()
{
auto selected = _filteredActionsView().SelectedIndex();
auto numVisibleItems = _getNumVisibleItems();
_scrollToIndex(selected - numVisibleItems);
}
// Method Description:
// - Scrolls the focus one page down the list of commands.
// Arguments:
// - <none>
// Return Value:
// - <none>
void CommandPalette::ScrollPageDown()
{
auto selected = _filteredActionsView().SelectedIndex();
auto numVisibleItems = _getNumVisibleItems();
_scrollToIndex(selected + numVisibleItems);
}
// Method Description:
// - Moves the focus to the top item in the list of commands.
// Arguments:
// - <none>
// Return Value:
// - <none>
void CommandPalette::ScrollToTop()
{
_scrollToIndex(0);
}
// Method Description:
// - Moves the focus to the bottom item in the list of commands.
// Arguments:
// - <none>
// Return Value:
// - <none>
void CommandPalette::ScrollToBottom()
{
_scrollToIndex(_filteredActionsView().Items().Size() - 1);
}
// Method Description:
// - Called when the command selection changes. We'll use this in the tab
// switcher to "preview" tabs as the user navigates the list of tabs. To
// do that, we'll dispatch the switch to tab command for this tab, but not
// dismiss the switcher.
// Arguments:
// - <unused>
// Return Value:
// - <none>
void CommandPalette::_selectedCommandChanged(const IInspectable& /*sender*/,
const Windows::UI::Xaml::RoutedEventArgs& /*args*/)
{
const auto selectedCommand = _filteredActionsView().SelectedItem();
const auto filteredCommand{ selectedCommand.try_as<winrt::TerminalApp::FilteredCommand>() };
if (_currentMode == CommandPaletteMode::TabSwitchMode)
{
_switchToTab(filteredCommand);
}
else if (_currentMode == CommandPaletteMode::ActionMode && filteredCommand != nullptr)
{
if (const auto actionPaletteItem{ filteredCommand.Item().try_as<winrt::TerminalApp::ActionPaletteItem>() })
{
PreviewAction.raise(*this, actionPaletteItem.Command());
}
}
else if (_currentMode == CommandPaletteMode::CommandlineMode)
{
if (filteredCommand)
{
SearchBoxPlaceholderText(filteredCommand.Item().Name());
}
else
{
SearchBoxPlaceholderText(RS_(L"CmdPalCommandlinePrompt"));
}
}
}
void CommandPalette::_previewKeyDownHandler(const IInspectable& /*sender*/,
const Windows::UI::Xaml::Input::KeyRoutedEventArgs& e)
{
const auto key = e.OriginalKey();
const auto scanCode = e.KeyStatus().ScanCode;
const auto coreWindow = CoreWindow::GetForCurrentThread();
const auto ctrlDown = WI_IsFlagSet(coreWindow.GetKeyState(winrt::Windows::System::VirtualKey::Control), CoreVirtualKeyStates::Down);
const auto altDown = WI_IsFlagSet(coreWindow.GetKeyState(winrt::Windows::System::VirtualKey::Menu), CoreVirtualKeyStates::Down);
const auto shiftDown = WI_IsFlagSet(coreWindow.GetKeyState(winrt::Windows::System::VirtualKey::Shift), CoreVirtualKeyStates::Down);
// Some keypresses such as Tab, Return, Esc, and Arrow Keys are ignored by controls because
// they're not considered input key presses. While they don't raise KeyDown events,
// they do raise PreviewKeyDown events.
//
// Only give anchored tab switcher the ability to cycle through tabs with the tab button.
// For unanchored mode, accessibility becomes an issue when we try to hijack tab since it's
// a really widely used keyboard navigation key.
if (_currentMode == CommandPaletteMode::TabSwitchMode && _actionMap)
{
winrt::Microsoft::Terminal::Control::KeyChord kc{ ctrlDown, altDown, shiftDown, false, static_cast<int32_t>(key), static_cast<int32_t>(scanCode) };
if (const auto cmd{ _actionMap.GetActionByKeyChord(kc) })
{
if (cmd.ActionAndArgs().Action() == ShortcutAction::PrevTab)
{
SelectNextItem(false);
e.Handled(true);
return;
}
else if (cmd.ActionAndArgs().Action() == ShortcutAction::NextTab)
{
SelectNextItem(true);
e.Handled(true);
return;
}
}
}
if (key == VirtualKey::Home && ctrlDown)
{
ScrollToTop();
e.Handled(true);
}
else if (key == VirtualKey::End && ctrlDown)
{
ScrollToBottom();
e.Handled(true);
}
else if (key == VirtualKey::Up)
{
// Action Mode: Move focus to the next item in the list.
SelectNextItem(false);
e.Handled(true);
}
else if (key == VirtualKey::Down)
{
// Action Mode: Move focus to the previous item in the list.
SelectNextItem(true);
e.Handled(true);
}
else if (key == VirtualKey::PageUp)
{
// Action Mode: Move focus to the first visible item in the list.
ScrollPageUp();
e.Handled(true);
}
else if (key == VirtualKey::PageDown)
{
// Action Mode: Move focus to the last visible item in the list.
ScrollPageDown();
e.Handled(true);
}
else if (key == VirtualKey::Enter)
{
if (const auto& button = e.OriginalSource().try_as<Button>())
{
// Let the button handle the Enter key so an eventually attached click handler will be called
e.Handled(false);
return;
}
const auto selectedCommand = _filteredActionsView().SelectedItem();
const auto filteredCommand = selectedCommand.try_as<winrt::TerminalApp::FilteredCommand>();
_dispatchCommand(filteredCommand);
e.Handled(true);
}
else if (key == VirtualKey::Escape)
{
// Dismiss the palette if the text is empty, otherwise clear the
// search string.
if (_searchBox().Text().empty())
{
_dismissPalette();
}
else
{
_searchBox().Text(L"");
}
e.Handled(true);
}
else if (key == VirtualKey::Back && _searchBox().Text().empty() && _lastFilterTextWasEmpty && _currentMode == CommandPaletteMode::ActionMode)
{
// If the last filter text was empty, and we're backspacing from
// that state, then the user "backspaced" the virtual '>' we're
// using as the action mode indicator. Switch into commandline mode.
_switchToMode(CommandPaletteMode::CommandlineMode);
e.Handled(true);
}
else if (key == VirtualKey::C && ctrlDown)
{
_searchBox().CopySelectionToClipboard();
e.Handled(true);
}
else if (key == VirtualKey::V && ctrlDown)
{
_searchBox().PasteFromClipboard();
e.Handled(true);
}
else if (key == VirtualKey::Right && _currentMode == CommandPaletteMode::CommandlineMode)
{
if (const auto command{ _filteredActionsView().SelectedItem().try_as<winrt::TerminalApp::FilteredCommand>() })
{
_searchBox().Text(command.Item().Name());
_searchBox().Select(_searchBox().Text().size(), 0);
_searchBox().Focus(FocusState::Programmatic);
_filteredActionsView().SelectedIndex(-1);
e.Handled(true);
}
}
}
// Method Description:
// - Implements the Alt handler
// Return value:
// - whether the key was handled
bool CommandPalette::OnDirectKeyEvent(const uint32_t vkey, const uint8_t /*scanCode*/, const bool down)
{
auto handled = false;
if (_currentMode == CommandPaletteMode::TabSwitchMode)
{
if (vkey == VK_MENU && !down)
{
_anchorKeyUpHandler();
handled = true;
}
}
return handled;
}
void CommandPalette::_keyUpHandler(const IInspectable& /*sender*/,
const Windows::UI::Xaml::Input::KeyRoutedEventArgs& e)
{
if (_currentMode == CommandPaletteMode::TabSwitchMode)
{
_anchorKeyUpHandler();
e.Handled(true);
}
}
// Method Description:
// - Handles anchor key ups during TabSwitchMode.
// We assume that at least one modifier key should be held down in order to "anchor"
// the ATS UI in place. So this function is called to check if any modifiers are
// still held down, and if not, dispatch the selected tab action and close the ATS.
// Return value:
// - <none>
void CommandPalette::_anchorKeyUpHandler()
{
const auto coreWindow = CoreWindow::GetForCurrentThread();
const auto ctrlDown = WI_IsFlagSet(coreWindow.GetKeyState(winrt::Windows::System::VirtualKey::Control), CoreVirtualKeyStates::Down);
const auto altDown = WI_IsFlagSet(coreWindow.GetKeyState(winrt::Windows::System::VirtualKey::Menu), CoreVirtualKeyStates::Down);
const auto shiftDown = WI_IsFlagSet(coreWindow.GetKeyState(winrt::Windows::System::VirtualKey::Shift), CoreVirtualKeyStates::Down);
if (!ctrlDown && !altDown && !shiftDown)
{
const auto selectedCommand = _filteredActionsView().SelectedItem();
if (const auto filteredCommand = selectedCommand.try_as<winrt::TerminalApp::FilteredCommand>())
{
_dispatchCommand(filteredCommand);
}
}
}
// Method Description:
// - This event is triggered when someone clicks anywhere in the bounds of
// the window that's _not_ the command palette UI. When that happens,
// we'll want to dismiss the palette.
// Arguments:
// - <unused>
// Return Value:
// - <none>
void CommandPalette::_rootPointerPressed(const Windows::Foundation::IInspectable& /*sender*/,
const Windows::UI::Xaml::Input::PointerRoutedEventArgs& /*e*/)
{
if (Visibility() != Visibility::Collapsed)
{
_dismissPalette();
}
}
// Method Description:
// - The purpose of this event handler is to hide the palette if it loses focus.
// We say we lost focus if our root element and all its descendants lost focus.
// This handler is invoked when our root element or some descendant loses focus.
// At this point we need to learn if the newly focused element belongs to this palette.
// To achieve this:
// - We start with the newly focused element and traverse its visual ancestors up to the Xaml root.
// - If one of the ancestors is this CommandPalette, then by our definition the focus is not lost
// - If we reach the Xaml root without meeting this CommandPalette,
// then the focus is not contained in it anymore and it should be dismissed
// Arguments:
// - <unused>
// Return Value:
// - <none>
void CommandPalette::_lostFocusHandler(const Windows::Foundation::IInspectable& /*sender*/,
const Windows::UI::Xaml::RoutedEventArgs& /*args*/)
{
const auto flyout = _searchBox().ContextFlyout();
if (flyout && flyout.IsOpen())
{
return;
}
auto root = this->XamlRoot();
if (!root)
{
return;
}
auto focusedElementOrAncestor = Input::FocusManager::GetFocusedElement(root).try_as<DependencyObject>();
while (focusedElementOrAncestor)
{
if (focusedElementOrAncestor == *this)
{
// This palette is the focused element or an ancestor of the focused element. No need to dismiss.
return;
}
// Go up to the next ancestor
focusedElementOrAncestor = winrt::Windows::UI::Xaml::Media::VisualTreeHelper::GetParent(focusedElementOrAncestor);
}
// We got to the root (the element with no parent) and didn't meet this palette on the path.
// It means that it lost the focus and needs to be dismissed.
_dismissPalette();
}
// Method Description:
// - This event is only triggered when someone clicks in the space right
// next to the text box in the command palette. We _don't_ want that click
// to light dismiss the palette, so we'll mark it handled here.
// Arguments:
// - e: the PointerRoutedEventArgs that we want to mark as handled
// Return Value:
// - <none>
void CommandPalette::_backdropPointerPressed(const Windows::Foundation::IInspectable& /*sender*/,
const Windows::UI::Xaml::Input::PointerRoutedEventArgs& e)
{
e.Handled(true);
}
// Method Description:
// - This event is called when the user clicks on an individual item from
// the list. We'll get the item that was clicked and dispatch the command
// that the user clicked on.
// Arguments:
// - e: an ItemClickEventArgs who's ClickedItem() will be the command that was clicked on.
// Return Value:
// - <none>
void CommandPalette::_listItemClicked(const Windows::Foundation::IInspectable& /*sender*/,
const Windows::UI::Xaml::Controls::ItemClickEventArgs& e)
{
const auto selectedCommand = e.ClickedItem();
if (const auto filteredCommand = selectedCommand.try_as<winrt::TerminalApp::FilteredCommand>())
{
_dispatchCommand(filteredCommand);
}
}
void CommandPalette::_listItemSelectionChanged(const Windows::Foundation::IInspectable& /*sender*/, const Windows::UI::Xaml::Controls::SelectionChangedEventArgs& e)
{
// We don't care about...
// - CommandlineMode: it doesn't have any selectable items in the list view
// - TabSwitchMode: focus and selected item are in sync
if (_currentMode == CommandPaletteMode::ActionMode || _currentMode == CommandPaletteMode::TabSearchMode)
{
if (auto automationPeer{ Automation::Peers::FrameworkElementAutomationPeer::FromElement(_searchBox()) })
{
if (const auto selectedList = e.AddedItems(); selectedList.Size() > 0)
{
const auto selectedCommand = selectedList.GetAt(0);
if (const auto filteredCmd = selectedCommand.try_as<TerminalApp::FilteredCommand>())
{
if (const auto paletteItem = filteredCmd.Item().try_as<TerminalApp::PaletteItem>())
{
automationPeer.RaiseNotificationEvent(
Automation::Peers::AutomationNotificationKind::ItemAdded,
Automation::Peers::AutomationNotificationProcessing::MostRecent,
paletteItem.Name() + L" " + paletteItem.KeyChordText(),
L"CommandPaletteSelectedItemChanged" /* unique name for this notification category */);
}
}
}
}
}
}
// Method Description:
// This event is called when the user clicks on a ChevronLeft button right
// next to the ParentCommandName (e.g. New Tab...) above the subcommands list.
// It'll go up a single level when the user clicks the button.
// Arguments:
// - sender: the button that got clicked
// Return Value:
// - <none>
void CommandPalette::_moveBackButtonClicked(const Windows::Foundation::IInspectable& /*sender*/,
const Windows::UI::Xaml::RoutedEventArgs&)
{
PreviewAction.raise(*this, nullptr);
_searchBox().Focus(FocusState::Programmatic);
const auto previousAction{ _nestedActionStack.GetAt(_nestedActionStack.Size() - 1) };
_nestedActionStack.RemoveAtEnd();
// Repopulate nested commands when the root has not been reached yet
if (_nestedActionStack.Size() > 0)
{
const auto newPreviousAction{ _nestedActionStack.GetAt(_nestedActionStack.Size() - 1) };
const auto actionPaletteItem{ newPreviousAction.Item().try_as<winrt::TerminalApp::ActionPaletteItem>() };
ParentCommandName(actionPaletteItem.Command().Name());
_updateCurrentNestedCommands(actionPaletteItem.Command());
}
else
{
ParentCommandName(L"");
_currentNestedCommands.Clear();
}
_updateFilteredActions();
const auto lastSelectedIt = std::find_if(begin(_filteredActions), end(_filteredActions), [&](const auto& filteredCommand) {
return filteredCommand.Item().Name() == previousAction.Item().Name();
});
const auto lastSelectedIndex = static_cast<int32_t>(std::distance(begin(_filteredActions), lastSelectedIt));
_scrollToIndex(lastSelectedIt != end(_filteredActions) ? lastSelectedIndex : 0);
}
// Method Description:
// - This is called when the user selects a command with subcommands. It
// will update our UI to now display the list of subcommands instead, and
// clear the search text so the user can search from the new list of
// commands.
// Arguments:
// - <none>
// Return Value:
// - <none>
void CommandPalette::_updateUIForStackChange()
{
if (_searchBox().Text().empty())
{
// Manually call _filterTextChanged, because setting the text to the
// empty string won't update it for us (as it won't actually change value.)
_filterTextChanged(nullptr, nullptr);
}
// Changing the value of the search box will trigger _filterTextChanged,
// which will cause us to refresh the list of filterable commands.
_searchBox().Text(L"");
_searchBox().Focus(FocusState::Programmatic);
if (auto automationPeer{ Automation::Peers::FrameworkElementAutomationPeer::FromElement(_searchBox()) })
{
automationPeer.RaiseNotificationEvent(
Automation::Peers::AutomationNotificationKind::ActionCompleted,
Automation::Peers::AutomationNotificationProcessing::CurrentThenMostRecent,
fmt::format(std::wstring_view{ RS_(L"CommandPalette_NestedCommandAnnouncement") }, ParentCommandName()),
L"CommandPaletteNestingLevelChanged" /* unique name for this notification category */);
}
}
// Method Description:
// - Retrieve the list of commands that we should currently be filtering.
// * If the user has command with subcommands, this will return that command's subcommands.
// * If we're in Tab Switcher mode, return the tab actions.
// * Otherwise, just return the list of all the top-level commands.
// Arguments:
// - <none>
// Return Value:
// - A list of Commands to filter.
Collections::IVector<winrt::TerminalApp::FilteredCommand> CommandPalette::_commandsToFilter()
{
switch (_currentMode)
{
case CommandPaletteMode::ActionMode:
if (_nestedActionStack.Size() > 0)
{
return _currentNestedCommands;
}
return _allCommands;
case CommandPaletteMode::TabSearchMode:
return _tabActions;
case CommandPaletteMode::TabSwitchMode:
return _tabSwitcherMode == TabSwitcherMode::MostRecentlyUsed ? _mruTabActions : _tabActions;
case CommandPaletteMode::CommandlineMode:
return _loadRecentCommands();
default:
return _allCommands;
}
}
// Method Description:
// - Helper method for retrieving the action from a command the user
// selected, and dispatching that command. Also fires a tracelogging event
// indicating that the user successfully found the action they were
// looking for.
// Arguments:
// - command: the Command to dispatch. This might be null.
// Return Value:
// - <none>
void CommandPalette::_dispatchCommand(const winrt::TerminalApp::FilteredCommand& filteredCommand)
{
if (_currentMode == CommandPaletteMode::CommandlineMode)
{
_dispatchCommandline(filteredCommand);
}
else if (_currentMode == CommandPaletteMode::TabSwitchMode || _currentMode == CommandPaletteMode::TabSearchMode)
{
_switchToTab(filteredCommand);
_close();
}
else if (filteredCommand)
{
if (const auto actionPaletteItem{ filteredCommand.Item().try_as<winrt::TerminalApp::ActionPaletteItem>() })
{
if (actionPaletteItem.Command().HasNestedCommands())
{
// If this Command had subcommands, then don't dispatch the
// action. Instead, display a new list of commands for the user
// to pick from.
_nestedActionStack.Append(filteredCommand);
ParentCommandName(actionPaletteItem.Command().Name());
_updateCurrentNestedCommands(actionPaletteItem.Command());
_updateUIForStackChange();
}
else
{
// First stash the search text length, because _close will clear this.
const auto searchTextLength = _searchBox().Text().size();
// An action from the root command list has depth=0
const auto nestedCommandDepth = _nestedActionStack.Size();
// Close before we dispatch so that actions that open the command
// palette like the Tab Switcher will be able to have the last laugh.
_close();
// But make an exception for the Toggle Command Palette action: we don't want the dispatch
// make the command palette - that was just closed - visible again.
// All other actions can just be dispatched.
if (actionPaletteItem.Command().ActionAndArgs().Action() != ShortcutAction::ToggleCommandPalette)
{
DispatchCommandRequested.raise(*this, actionPaletteItem.Command());
}
TraceLoggingWrite(
g_hTerminalAppProvider, // handle to TerminalApp tracelogging provider
"CommandPaletteDispatchedAction",
TraceLoggingDescription("Event emitted when the user selects an action in the Command Palette"),
TraceLoggingUInt32(searchTextLength, "SearchTextLength", "Number of characters in the search string"),
TraceLoggingUInt32(nestedCommandDepth, "NestedCommandDepth", "the depth in the tree of commands for the dispatched action"),
TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES),
TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage));
}
}
}
}
// Method Description:
// - Get all the input text in _searchBox that follows any leading spaces.
// Arguments:
// - <none>
// Return Value:
// - the string of input following any number of leading spaces
std::wstring CommandPalette::_getTrimmedInput()
{
const std::wstring input{ _searchBox().Text() };
if (input.empty())
{
return input;
}
// Trim leading whitespace
const auto firstNonSpace = input.find_first_not_of(L" ");
if (firstNonSpace == std::wstring::npos)
{
// All the following characters are whitespace.
return L"";
}
return input.substr(firstNonSpace);
}
// Method Description:
// - Dispatch switch to tab action.
// Arguments:
// - filteredCommand - Selected filtered command - might be null
// Return Value:
// - <none>
void CommandPalette::_switchToTab(const winrt::TerminalApp::FilteredCommand& filteredCommand)
{
if (filteredCommand)
{
if (const auto tabPaletteItem{ filteredCommand.Item().try_as<winrt::TerminalApp::TabPaletteItem>() })
{
if (const auto tab{ tabPaletteItem.Tab() })
{
SwitchToTabRequested.raise(*this, tab);
}
}
}
}
// Method Description:
// - Dispatch the current search text as a ExecuteCommandline action.
// Arguments:
// - filteredCommand - Selected filtered command - might be null
// Return Value:
// - <none>
void CommandPalette::_dispatchCommandline(const winrt::TerminalApp::FilteredCommand& command)
{
const auto filteredCommand = command ? command : _buildCommandLineCommand(winrt::hstring(_getTrimmedInput()));
if (filteredCommand.has_value())
{
_updateRecentCommands(filteredCommand.value().Item().Name());
TraceLoggingWrite(
g_hTerminalAppProvider, // handle to TerminalApp tracelogging provider
"CommandPaletteDispatchedCommandline",
TraceLoggingDescription("Event emitted when the user runs a commandline in the Command Palette"),
TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES),
TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage));
if (const auto commandLinePaletteItem{ filteredCommand.value().Item().try_as<winrt::TerminalApp::CommandLinePaletteItem>() })
{
CommandLineExecutionRequested.raise(*this, commandLinePaletteItem.CommandLine());
_close();
}
}
}
std::optional<TerminalApp::FilteredCommand> CommandPalette::_buildCommandLineCommand(const hstring& commandLine)
{
if (commandLine.empty())
{
return std::nullopt;
}
auto commandLinePaletteItem{ winrt::make<CommandLinePaletteItem>(commandLine) };
return winrt::make<FilteredCommand>(commandLinePaletteItem);
}
// Method Description:
// - Helper method for closing the command palette, when the user has _not_
// selected an action. Also fires a tracelogging event indicating that the
// user closed the palette without running a command.
// Arguments:
// - <none>
// Return Value:
// - <none>
void CommandPalette::_dismissPalette()
{
_close();
TraceLoggingWrite(
g_hTerminalAppProvider, // handle to TerminalApp tracelogging provider
"CommandPaletteDismissed",
TraceLoggingDescription("Event emitted when the user dismisses the Command Palette without selecting an action"),
TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES),
TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage));
}
// Method Description:
// - Event handler for when the text in the input box changes. In Action
// Mode, we'll update the list of displayed commands, and select the first one.
// Arguments:
// - <unused>
// Return Value:
// - <none>
void CommandPalette::_filterTextChanged(const IInspectable& /*sender*/,
const Windows::UI::Xaml::RoutedEventArgs& /*args*/)
{
// When we are executing the _SelectNextTab in the TabManagement.cpp, this method
// is getting triggered because we set up the default value for that CommandPalette
// with an empty string. Therefore, to avoid the reset of the index when executing
// the Next/Prev tab command, we are skipping this execution.
// Check issue https://github.com/microsoft/terminal/issues/11146
if (_currentMode == CommandPaletteMode::TabSwitchMode)
{
return;
}
if (_currentMode == CommandPaletteMode::CommandlineMode)
{
_evaluatePrefix();
}
// We're setting _lastFilterTextWasEmpty here, because if the user tries
// to backspace the last character in the input, the Backspace KeyDown
// event will fire _before_ _filterTextChanged does. Updating the value
// here will ensure that we can check this case appropriately.
_lastFilterTextWasEmpty = _searchBox().Text().empty();
_updateFilteredActions();
// In the command line mode we want the user to explicitly select the command
_filteredActionsView().SelectedIndex(_currentMode == CommandPaletteMode::CommandlineMode ? -1 : 0);
if (_currentMode == CommandPaletteMode::TabSearchMode || _currentMode == CommandPaletteMode::ActionMode)
{
const auto currentNeedleHasResults{ _filteredActions.Size() > 0 };
_noMatchesText().Visibility(currentNeedleHasResults ? Visibility::Collapsed : Visibility::Visible);
if (auto automationPeer{ Automation::Peers::FrameworkElementAutomationPeer::FromElement(_searchBox()) })
{
automationPeer.RaiseNotificationEvent(
Automation::Peers::AutomationNotificationKind::ActionCompleted,
Automation::Peers::AutomationNotificationProcessing::ImportantMostRecent,
currentNeedleHasResults ?
winrt::hstring{ fmt::format(std::wstring_view{ RS_(L"CommandPalette_MatchesAvailable") }, _filteredActions.Size()) } :
NoMatchesText(), // what to announce if results were found
L"CommandPaletteResultAnnouncement" /* unique name for this group of notifications */);
}
}
else
{
_noMatchesText().Visibility(Visibility::Collapsed);
}
if (_currentMode == CommandPaletteMode::CommandlineMode)
{
ParsedCommandLineText(L"");
const auto commandLine = _getTrimmedInput();
if (!commandLine.empty())
{
ExecuteCommandlineArgs args{ commandLine };
_appArgs.FullResetState();
if (_appArgs.ParseArgs(args) == 0)
{
const auto& commands = _appArgs.GetStartupActions();
if (commands.size() > 0)
{
std::wstring commandDescription{ RS_(L"CommandPalette_ParsedCommandLine") };
for (const auto& command : commands)
{
commandDescription += L"\n\t" + command.Args().GenerateName();
}
ParsedCommandLineText(commandDescription.data());
}
}
else
{
ParsedCommandLineText(RS_(L"CommandPalette_FailedParsingCommandLine") + L"\n\t" + til::u8u16(_appArgs.GetExitMessage()));
}
}
}
}
void CommandPalette::_evaluatePrefix()
{
// This will take you from commandline mode, into action mode. The
// backspace handler in _keyDownHandler will handle taking us from
// action mode to commandline mode.
auto newMode = CommandPaletteMode::CommandlineMode;
auto inputText = _getTrimmedInput();
if (inputText.size() > 0)
{
if (inputText[0] == L'>')
{
newMode = CommandPaletteMode::ActionMode;
}
}
if (newMode != _currentMode)
{
//_switchToMode will remove the '>' character from the input.
_switchToMode(newMode);
}
}
Collections::IObservableVector<winrt::TerminalApp::FilteredCommand> CommandPalette::FilteredActions()
{
return _filteredActions;
}
void CommandPalette::SetActionMap(const Microsoft::Terminal::Settings::Model::IActionMapView& actionMap)
{
_actionMap = actionMap;
_populateCommands();
}
void CommandPalette::_populateCommands()
{
_allCommands.Clear();
if (_actionMap)
{
const auto expandedCommands{ _actionMap.ExpandedCommands() };
for (const auto& action : expandedCommands)
{
const auto keyChordText{ KeyChordSerialization::ToString(_actionMap.GetKeyBindingForAction(action.ID())) };
auto actionPaletteItem{ winrt::make<winrt::TerminalApp::implementation::ActionPaletteItem>(action, keyChordText) };
auto filteredCommand{ winrt::make<FilteredCommand>(actionPaletteItem) };
_allCommands.Append(filteredCommand);
}
if (Visibility() == Visibility::Visible && _currentMode == CommandPaletteMode::ActionMode)
{
_updateFilteredActions();
}
}
}
// Method Description:
// - Replaces a list of filtered commands in the target collection with new
// commands based on the tabs in the source collection.
// Although the source observable we still don't register on it,
// so the palette user will need to reset the binding manually every time
// the source collection changes
// Arguments:
// - source: the tabs to use for creation filtered commands
// - target: the collection to store newly created filtered commands
// Return Value:
// - <none>
void CommandPalette::_bindTabs(
const Windows::Foundation::Collections::IObservableVector<winrt::TerminalApp::TabBase>& source,
const Windows::Foundation::Collections::IVector<winrt::TerminalApp::FilteredCommand>& target)
{
target.Clear();
for (const auto& tab : source)
{
auto tabPaletteItem{ winrt::make<winrt::TerminalApp::implementation::TabPaletteItem>(tab) };
auto filteredCommand{ winrt::make<FilteredCommand>(tabPaletteItem) };
target.Append(filteredCommand);
}
}