1070 lines
40 KiB
C++
1070 lines
40 KiB
C++
// Copyright (c) Microsoft Corporation.
|
|
// Licensed under the MIT license.
|
|
|
|
#include "pch.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::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();
|
|
|
|
_filteredActions = winrt::single_threaded_observable_vector<Command>();
|
|
_nestedActionStack = winrt::single_threaded_vector<Command>();
|
|
_currentNestedCommands = winrt::single_threaded_vector<Command>();
|
|
_allCommands = winrt::single_threaded_vector<Command>();
|
|
_allTabActions = winrt::single_threaded_vector<Command>();
|
|
|
|
_switchToMode(CommandPaletteMode::ActionMode);
|
|
|
|
if (CommandPaletteShadow())
|
|
{
|
|
// Hook up the shadow on the command palette to the backdrop that
|
|
// will actually show it. This needs to be done at runtime, and only
|
|
// if the shadow actually exists. ThemeShadow isn't supported below
|
|
// version 18362.
|
|
CommandPaletteShadow().Receivers().Append(_shadowBackdrop());
|
|
// "raise" the command palette up by 16 units, so it will cast a shadow.
|
|
_backdrop().Translation({ 0, 0, 16 });
|
|
}
|
|
|
|
// 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)
|
|
{
|
|
if (_currentMode == CommandPaletteMode::TabSwitchMode)
|
|
{
|
|
_searchBox().Visibility(Visibility::Collapsed);
|
|
_filteredActionsView().Focus(FocusState::Keyboard);
|
|
_filteredActionsView().SelectedIndex(_switcherStartIdx);
|
|
_filteredActionsView().ScrollIntoView(_filteredActionsView().SelectedItem());
|
|
|
|
// 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
|
|
{
|
|
_searchBox().Focus(FocusState::Programmatic);
|
|
_updateFilteredActions();
|
|
_filteredActionsView().SelectedIndex(0);
|
|
}
|
|
|
|
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_ProductAndServicePerformance));
|
|
}
|
|
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 });
|
|
}
|
|
|
|
// 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)
|
|
{
|
|
const auto selected = _filteredActionsView().SelectedIndex();
|
|
const int numItems = ::base::saturated_cast<int>(_filteredActionsView().Items().Size());
|
|
// 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:
|
|
// - Scrolls the focus up or down the list of commands.
|
|
// Arguments:
|
|
// - pageDown: if true, we're attempting to move to the last visible item in the
|
|
// list. Otherwise, we're attempting to move to the first visible item.
|
|
// Return Value:
|
|
// - <none>
|
|
void CommandPalette::ScrollDown(const bool pageDown)
|
|
{
|
|
const auto container = _filteredActionsView().ContainerFromIndex(0);
|
|
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());
|
|
const int numVisibleItems = listHeight / itemHeight;
|
|
|
|
auto selected = _filteredActionsView().SelectedIndex();
|
|
const int numItems = ::base::saturated_cast<int>(_filteredActionsView().Items().Size());
|
|
|
|
const auto newIndex = ((numItems + selected + (pageDown ? numVisibleItems : -numVisibleItems)) % numItems);
|
|
_filteredActionsView().SelectedIndex(newIndex);
|
|
_filteredActionsView().ScrollIntoView(_filteredActionsView().SelectedItem());
|
|
}
|
|
|
|
// Method Description:
|
|
// - Moves the focus either to the top item or to the end item in the list of commands.
|
|
// Arguments:
|
|
// - end: if true, we're attempting to move to the last item in the
|
|
// list. Otherwise, we're attempting to move to the first item.
|
|
// Depends on the pageUpDown argument.
|
|
// Return Value:
|
|
// - <none>
|
|
void CommandPalette::GoEnd(const bool end)
|
|
{
|
|
const auto lastIndex = ::base::saturated_cast<int>(_filteredActionsView().Items().Size() - 1);
|
|
if (end)
|
|
{
|
|
_filteredActionsView().SelectedIndex(lastIndex);
|
|
}
|
|
else
|
|
{
|
|
_filteredActionsView().SelectedIndex(0);
|
|
}
|
|
_filteredActionsView().ScrollIntoView(_filteredActionsView().SelectedItem());
|
|
}
|
|
|
|
// 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*/)
|
|
{
|
|
if (_currentMode == CommandPaletteMode::TabSwitchMode)
|
|
{
|
|
const auto& selectedCommand = _filteredActionsView().SelectedItem();
|
|
if (const auto& command = selectedCommand.try_as<Command>())
|
|
{
|
|
const auto& actionAndArgs = command.Action();
|
|
_dispatch.DoAction(actionAndArgs);
|
|
}
|
|
}
|
|
}
|
|
|
|
void CommandPalette::_previewKeyDownHandler(IInspectable const& /*sender*/,
|
|
Windows::UI::Xaml::Input::KeyRoutedEventArgs const& e)
|
|
{
|
|
auto key = e.OriginalKey();
|
|
|
|
// 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 && key == VirtualKey::Tab)
|
|
{
|
|
auto const state = CoreWindow::GetForCurrentThread().GetKeyState(winrt::Windows::System::VirtualKey::Shift);
|
|
if (WI_IsFlagSet(state, CoreVirtualKeyStates::Down))
|
|
{
|
|
SelectNextItem(false);
|
|
e.Handled(true);
|
|
}
|
|
else
|
|
{
|
|
SelectNextItem(true);
|
|
e.Handled(true);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Method Description:
|
|
// - Process keystrokes in the input box. This is used for moving focus up
|
|
// and down the list of commands in Action mode, and for executing
|
|
// commands in both Action mode and Commandline mode.
|
|
// Arguments:
|
|
// - e: the KeyRoutedEventArgs containing info about the keystroke.
|
|
// Return Value:
|
|
// - <none>
|
|
void CommandPalette::_keyDownHandler(IInspectable const& /*sender*/,
|
|
Windows::UI::Xaml::Input::KeyRoutedEventArgs const& e)
|
|
{
|
|
auto key = e.OriginalKey();
|
|
|
|
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.
|
|
ScrollDown(false);
|
|
e.Handled(true);
|
|
}
|
|
else if (key == VirtualKey::PageDown)
|
|
{
|
|
// Action Mode: Move focus to the last visible item in the list.
|
|
ScrollDown(true);
|
|
e.Handled(true);
|
|
}
|
|
else if (key == VirtualKey::Home)
|
|
{
|
|
// Action Mode: Move focus to the first item in the list.
|
|
GoEnd(false);
|
|
e.Handled(true);
|
|
}
|
|
else if (key == VirtualKey::End)
|
|
{
|
|
// Action Mode: Move focus to the last item in the list.
|
|
GoEnd(true);
|
|
e.Handled(true);
|
|
}
|
|
else if (key == VirtualKey::Enter)
|
|
{
|
|
// Action, TabSwitch or TabSearchMode Mode: Dispatch the action of the selected command.
|
|
if (_currentMode != CommandPaletteMode::CommandlineMode)
|
|
{
|
|
if (const auto selectedItem = _filteredActionsView().SelectedItem())
|
|
{
|
|
_dispatchCommand(selectedItem.try_as<Command>());
|
|
}
|
|
}
|
|
// Commandline Mode: Use the input to synthesize an ExecuteCommandline action
|
|
else if (_currentMode == CommandPaletteMode::CommandlineMode)
|
|
{
|
|
_dispatchCommandline();
|
|
}
|
|
|
|
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)
|
|
{
|
|
// 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.
|
|
if (_searchBox().Text().empty() && _lastFilterTextWasEmpty && _currentMode == CommandPaletteMode::ActionMode)
|
|
{
|
|
_switchToMode(CommandPaletteMode::CommandlineMode);
|
|
}
|
|
|
|
e.Handled(true);
|
|
}
|
|
else
|
|
{
|
|
const auto vkey = ::gsl::narrow_cast<WORD>(e.OriginalKey());
|
|
|
|
// In the interest of not telling all modes to check for keybindings, limit to TabSwitch mode for now.
|
|
if (_currentMode == CommandPaletteMode::TabSwitchMode)
|
|
{
|
|
auto const ctrlDown = WI_IsFlagSet(CoreWindow::GetForCurrentThread().GetKeyState(winrt::Windows::System::VirtualKey::Control), CoreVirtualKeyStates::Down);
|
|
auto const altDown = WI_IsFlagSet(CoreWindow::GetForCurrentThread().GetKeyState(winrt::Windows::System::VirtualKey::Menu), CoreVirtualKeyStates::Down);
|
|
auto const shiftDown = WI_IsFlagSet(CoreWindow::GetForCurrentThread().GetKeyState(winrt::Windows::System::VirtualKey::Shift), CoreVirtualKeyStates::Down);
|
|
|
|
auto success = _bindings.TryKeyChord({
|
|
ctrlDown,
|
|
altDown,
|
|
shiftDown,
|
|
vkey,
|
|
});
|
|
|
|
if (success)
|
|
{
|
|
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(IInspectable const& /*sender*/,
|
|
Windows::UI::Xaml::Input::KeyRoutedEventArgs const& 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()
|
|
{
|
|
auto const ctrlDown = WI_IsFlagSet(CoreWindow::GetForCurrentThread().GetKeyState(winrt::Windows::System::VirtualKey::Control), CoreVirtualKeyStates::Down);
|
|
auto const altDown = WI_IsFlagSet(CoreWindow::GetForCurrentThread().GetKeyState(winrt::Windows::System::VirtualKey::Menu), CoreVirtualKeyStates::Down);
|
|
auto const shiftDown = WI_IsFlagSet(CoreWindow::GetForCurrentThread().GetKeyState(winrt::Windows::System::VirtualKey::Shift), CoreVirtualKeyStates::Down);
|
|
|
|
if (!ctrlDown && !altDown && !shiftDown)
|
|
{
|
|
if (const auto selectedItem = _filteredActionsView().SelectedItem())
|
|
{
|
|
if (const auto data = selectedItem.try_as<Command>())
|
|
{
|
|
_dispatchCommand(data);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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(Windows::Foundation::IInspectable const& /*sender*/,
|
|
Windows::UI::Xaml::Input::PointerRoutedEventArgs const& /*e*/)
|
|
{
|
|
_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(Windows::Foundation::IInspectable const& /*sender*/,
|
|
Windows::UI::Xaml::Input::PointerRoutedEventArgs const& 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(Windows::Foundation::IInspectable const& /*sender*/,
|
|
Windows::UI::Xaml::Controls::ItemClickEventArgs const& e)
|
|
{
|
|
_dispatchCommand(e.ClickedItem().try_as<Command>());
|
|
}
|
|
|
|
// 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"");
|
|
}
|
|
|
|
// 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<Command> CommandPalette::_commandsToFilter()
|
|
{
|
|
switch (_currentMode)
|
|
{
|
|
case CommandPaletteMode::ActionMode:
|
|
if (_nestedActionStack.Size() > 0)
|
|
{
|
|
return _currentNestedCommands;
|
|
}
|
|
|
|
return _allCommands;
|
|
case CommandPaletteMode::TabSearchMode:
|
|
case CommandPaletteMode::TabSwitchMode:
|
|
return _allTabActions;
|
|
case CommandPaletteMode::CommandlineMode:
|
|
return winrt::single_threaded_vector<Command>();
|
|
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 Command& command)
|
|
{
|
|
if (command)
|
|
{
|
|
if (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(command);
|
|
ParentCommandName(command.Name());
|
|
_currentNestedCommands.Clear();
|
|
for (const auto& nameAndCommand : command.NestedCommands())
|
|
{
|
|
_currentNestedCommands.Append(nameAndCommand.Value());
|
|
}
|
|
|
|
_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();
|
|
|
|
const auto actionAndArgs = command.Action();
|
|
_dispatch.DoAction(actionAndArgs);
|
|
|
|
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_ProductAndServicePerformance));
|
|
}
|
|
}
|
|
}
|
|
// 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 the current search text as a ExecuteCommandline action.
|
|
// Arguments:
|
|
// - <none>
|
|
// Return Value:
|
|
// - <none>
|
|
void CommandPalette::_dispatchCommandline()
|
|
{
|
|
auto cmdline{ _getTrimmedInput() };
|
|
if (cmdline.empty())
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Build the ExecuteCommandline action from the values we've parsed on the commandline.
|
|
ExecuteCommandlineArgs args{ cmdline };
|
|
ActionAndArgs executeActionAndArgs{ ShortcutAction::ExecuteCommandline, args };
|
|
|
|
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_ProductAndServicePerformance));
|
|
|
|
if (_dispatch.DoAction(executeActionAndArgs))
|
|
{
|
|
_close();
|
|
}
|
|
}
|
|
|
|
// 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_ProductAndServicePerformance));
|
|
}
|
|
|
|
// 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(IInspectable const& /*sender*/,
|
|
Windows::UI::Xaml::RoutedEventArgs const& /*args*/)
|
|
{
|
|
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();
|
|
_filteredActionsView().SelectedIndex(0);
|
|
|
|
if (_currentMode == CommandPaletteMode::TabSearchMode || _currentMode == CommandPaletteMode::ActionMode)
|
|
{
|
|
_noMatchesText().Visibility(_filteredActions.Size() > 0 ? Visibility::Collapsed : Visibility::Visible);
|
|
}
|
|
else
|
|
{
|
|
_noMatchesText().Visibility(Visibility::Collapsed);
|
|
}
|
|
}
|
|
|
|
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<Command> CommandPalette::FilteredActions()
|
|
{
|
|
return _filteredActions;
|
|
}
|
|
|
|
void CommandPalette::SetKeyBindings(Microsoft::Terminal::TerminalControl::IKeyBindings bindings)
|
|
{
|
|
_bindings = bindings;
|
|
}
|
|
|
|
void CommandPalette::SetCommands(Collections::IVector<Command> const& actions)
|
|
{
|
|
_allCommands = actions;
|
|
_updateFilteredActions();
|
|
}
|
|
|
|
void CommandPalette::SetTabActions(Collections::IVector<Command> const& tabs)
|
|
{
|
|
_allTabActions = tabs;
|
|
_updateFilteredActions();
|
|
}
|
|
|
|
void CommandPalette::EnableCommandPaletteMode()
|
|
{
|
|
_switchToMode(CommandPaletteMode::ActionMode);
|
|
_updateFilteredActions();
|
|
}
|
|
|
|
void CommandPalette::_switchToMode(CommandPaletteMode mode)
|
|
{
|
|
// The smooth remove/add animations that happen during
|
|
// UpdateFilteredActions don't work very well when switching between
|
|
// modes because of the sheer amount of remove/adds. So, let's just
|
|
// clear + append when switching between modes.
|
|
if (mode != _currentMode)
|
|
{
|
|
_currentMode = mode;
|
|
_filteredActions.Clear();
|
|
auto commandsToFilter = _commandsToFilter();
|
|
|
|
for (auto action : commandsToFilter)
|
|
{
|
|
_filteredActions.Append(action);
|
|
}
|
|
}
|
|
|
|
_searchBox().Text(L"");
|
|
_searchBox().Select(_searchBox().Text().size(), 0);
|
|
// Leaving this block of code outside the above if-statement
|
|
// guarantees that the correct text is shown for the mode
|
|
// whenever _switchToMode is called.
|
|
switch (_currentMode)
|
|
{
|
|
case CommandPaletteMode::TabSearchMode:
|
|
case CommandPaletteMode::TabSwitchMode:
|
|
{
|
|
SearchBoxPlaceholderText(RS_(L"TabSwitcher_SearchBoxText"));
|
|
NoMatchesText(RS_(L"TabSwitcher_NoMatchesText"));
|
|
ControlName(RS_(L"TabSwitcherControlName"));
|
|
PrefixCharacter(L"");
|
|
break;
|
|
}
|
|
case CommandPaletteMode::CommandlineMode:
|
|
SearchBoxPlaceholderText(RS_(L"CmdPalCommandlinePrompt"));
|
|
NoMatchesText(L"");
|
|
ControlName(RS_(L"CommandPaletteControlName"));
|
|
PrefixCharacter(L"");
|
|
break;
|
|
case CommandPaletteMode::ActionMode:
|
|
default:
|
|
SearchBoxPlaceholderText(RS_(L"CommandPalette_SearchBox/PlaceholderText"));
|
|
NoMatchesText(RS_(L"CommandPalette_NoMatchesText/Text"));
|
|
ControlName(RS_(L"CommandPaletteControlName"));
|
|
PrefixCharacter(L">");
|
|
break;
|
|
}
|
|
}
|
|
|
|
// This is a helper to aid in sorting commands by their `Name`s, alphabetically.
|
|
static bool _compareCommandNames(const Command& lhs, const Command& rhs)
|
|
{
|
|
std::wstring_view leftName{ lhs.Name() };
|
|
std::wstring_view rightName{ rhs.Name() };
|
|
return leftName.compare(rightName) < 0;
|
|
}
|
|
|
|
// This is a helper struct to aid in sorting Commands by a given weighting.
|
|
struct WeightedCommand
|
|
{
|
|
Command command;
|
|
int weight;
|
|
int inOrderCounter;
|
|
|
|
bool operator<(const WeightedCommand& other) const
|
|
{
|
|
if (weight == other.weight)
|
|
{
|
|
// If two commands have the same weight, then we'll sort them alphabetically.
|
|
// If they both have the same name, fall back to the order in which they were
|
|
// pushed into the heap.
|
|
if (command.Name() == other.command.Name())
|
|
{
|
|
return inOrderCounter > other.inOrderCounter;
|
|
}
|
|
else
|
|
{
|
|
return !_compareCommandNames(command, other.command);
|
|
}
|
|
}
|
|
return weight < other.weight;
|
|
}
|
|
};
|
|
|
|
// Method Description:
|
|
// - Produce a list of filtered actions to reflect the current contents of
|
|
// the input box. For more details on which commands will be displayed,
|
|
// see `_getWeight`.
|
|
// Arguments:
|
|
// - A collection that will receive the filtered actions
|
|
// Return Value:
|
|
// - <none>
|
|
std::vector<Command> CommandPalette::_collectFilteredActions()
|
|
{
|
|
std::vector<Command> actions;
|
|
|
|
winrt::hstring searchText{ _getTrimmedInput() };
|
|
const bool addAll = searchText.empty();
|
|
|
|
auto commandsToFilter = _commandsToFilter();
|
|
|
|
// If there's no filter text, then just add all the commands in order to the list.
|
|
// - TODO GH#6647:Possibly add the MRU commands first in order, followed
|
|
// by the rest of the commands.
|
|
if (addAll)
|
|
{
|
|
// If TabSwitcherMode, just add all as is. We don't want
|
|
// them to be sorted alphabetically.
|
|
if (_currentMode == CommandPaletteMode::TabSearchMode || _currentMode == CommandPaletteMode::TabSwitchMode)
|
|
{
|
|
for (auto action : commandsToFilter)
|
|
{
|
|
actions.push_back(action);
|
|
}
|
|
|
|
return actions;
|
|
}
|
|
|
|
// Add all the commands, but make sure they're sorted alphabetically.
|
|
std::vector<Command> sortedCommands;
|
|
sortedCommands.reserve(commandsToFilter.Size());
|
|
|
|
for (auto action : commandsToFilter)
|
|
{
|
|
sortedCommands.push_back(action);
|
|
}
|
|
std::sort(sortedCommands.begin(),
|
|
sortedCommands.end(),
|
|
_compareCommandNames);
|
|
|
|
for (auto action : sortedCommands)
|
|
{
|
|
actions.push_back(action);
|
|
}
|
|
|
|
return actions;
|
|
}
|
|
|
|
// Here, there was some filter text.
|
|
// Show these actions in a weighted order.
|
|
// - Matching the first character of a word, then the first char of a
|
|
// subsequent word seems better than just "the order they appear in
|
|
// the list".
|
|
// - TODO GH#6647:"Recently used commands" ordering also seems valuable.
|
|
// * This could be done by weighting the recently used commands
|
|
// higher the more recently they were used, then weighting all
|
|
// the unused commands as 1
|
|
|
|
// Use a priority queue to order commands so that "better" matches
|
|
// appear first in the list. The ordering will be determined by the
|
|
// match weight produced by _getWeight.
|
|
std::priority_queue<WeightedCommand> heap;
|
|
|
|
// TODO GH#7205: Find a better way to ensure that WCs of the same
|
|
// weight and name stay in the order in which they were pushed onto
|
|
// the PQ.
|
|
uint32_t counter = 0;
|
|
for (auto action : commandsToFilter)
|
|
{
|
|
const auto weight = CommandPalette::_getWeight(searchText, action.Name());
|
|
if (weight > 0)
|
|
{
|
|
WeightedCommand wc;
|
|
wc.command = action;
|
|
wc.weight = weight;
|
|
wc.inOrderCounter = counter++;
|
|
|
|
heap.push(wc);
|
|
}
|
|
}
|
|
|
|
// At this point, all the commands in heap are matches. We've also
|
|
// sorted commands with the same weight alphabetically.
|
|
// Remove everything in-order from the queue, and add to the list of
|
|
// filtered actions.
|
|
while (!heap.empty())
|
|
{
|
|
auto top = heap.top();
|
|
heap.pop();
|
|
actions.push_back(top.command);
|
|
}
|
|
|
|
return actions;
|
|
}
|
|
|
|
// Method Description:
|
|
// - Update our list of filtered actions to reflect the current contents of
|
|
// the input box. For more details on which commands will be displayed,
|
|
// see `_getWeight`.
|
|
// Arguments:
|
|
// - <none>
|
|
// Return Value:
|
|
// - <none>
|
|
void CommandPalette::_updateFilteredActions()
|
|
{
|
|
if (_currentMode == CommandPaletteMode::CommandlineMode)
|
|
{
|
|
_filteredActions.Clear();
|
|
return;
|
|
}
|
|
|
|
auto actions = _collectFilteredActions();
|
|
|
|
// Make _filteredActions look identical to actions, using only Insert and Remove.
|
|
// This allows WinUI to nicely animate the ListView as it changes.
|
|
for (uint32_t i = 0; i < _filteredActions.Size() && i < actions.size(); i++)
|
|
{
|
|
for (uint32_t j = i; j < _filteredActions.Size(); j++)
|
|
{
|
|
if (_filteredActions.GetAt(j) == actions[i])
|
|
{
|
|
for (uint32_t k = i; k < j; k++)
|
|
{
|
|
_filteredActions.RemoveAt(i);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (_filteredActions.GetAt(i) != actions[i])
|
|
{
|
|
_filteredActions.InsertAt(i, actions[i]);
|
|
}
|
|
}
|
|
|
|
// Remove any extra trailing items from the destination
|
|
while (_filteredActions.Size() > actions.size())
|
|
{
|
|
_filteredActions.RemoveAtEnd();
|
|
}
|
|
|
|
// Add any extra trailing items from the source
|
|
while (_filteredActions.Size() < actions.size())
|
|
{
|
|
_filteredActions.Append(actions[_filteredActions.Size()]);
|
|
}
|
|
}
|
|
|
|
// Function Description:
|
|
// - Calculates a "weighting" by which should be used to order a command
|
|
// name relative to other names, given a specific search string.
|
|
// Currently, this is based off of two factors:
|
|
// * The weight is incremented once for each matched character of the
|
|
// search text.
|
|
// * If a matching character from the search text was found at the start
|
|
// of a word in the name, then we increment the weight again.
|
|
// * For example, for a search string "sp", we want "Split Pane" to
|
|
// appear in the list before "Close Pane"
|
|
// * Consecutive matches will be weighted higher than matches with
|
|
// characters in between the search characters.
|
|
// - This will return 0 if the command should not be shown. If all the
|
|
// characters of search text appear in order in `name`, then this function
|
|
// will return a positive number. There can be any number of characters
|
|
// separating consecutive characters in searchText.
|
|
// * For example:
|
|
// "name": "New Tab"
|
|
// "name": "Close Tab"
|
|
// "name": "Close Pane"
|
|
// "name": "[-] Split Horizontal"
|
|
// "name": "[ | ] Split Vertical"
|
|
// "name": "Next Tab"
|
|
// "name": "Prev Tab"
|
|
// "name": "Open Settings"
|
|
// "name": "Open Media Controls"
|
|
// * "open" should return both "**Open** Settings" and "**Open** Media Controls".
|
|
// * "Tab" would return "New **Tab**", "Close **Tab**", "Next **Tab**" and "Prev
|
|
// **Tab**".
|
|
// * "P" would return "Close **P**ane", "[-] S**p**lit Horizontal", "[ | ]
|
|
// S**p**lit Vertical", "**P**rev Tab", "O**p**en Settings" and "O**p**en Media
|
|
// Controls".
|
|
// * "sv" would return "[ | ] Split Vertical" (by matching the **S** in
|
|
// "Split", then the **V** in "Vertical").
|
|
// Arguments:
|
|
// - searchText: the string of text to search for in `name`
|
|
// - name: the name to check
|
|
// Return Value:
|
|
// - the relative weight of this match
|
|
int CommandPalette::_getWeight(const winrt::hstring& searchText,
|
|
const winrt::hstring& name)
|
|
{
|
|
int totalWeight = 0;
|
|
bool lastWasSpace = true;
|
|
|
|
auto it = name.cbegin();
|
|
|
|
for (auto searchChar : searchText)
|
|
{
|
|
searchChar = std::towlower(searchChar);
|
|
// Advance the iterator to the next character that we're looking
|
|
// for.
|
|
|
|
bool lastWasMatch = true;
|
|
while (true)
|
|
{
|
|
// If we are at the end of the name string, we haven't found
|
|
// it.
|
|
if (it == name.cend())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// found it
|
|
if (std::towlower(*it) == searchChar)
|
|
{
|
|
break;
|
|
}
|
|
|
|
lastWasSpace = *it == L' ';
|
|
++it;
|
|
lastWasMatch = false;
|
|
}
|
|
|
|
// Advance the iterator by one character so that we don't
|
|
// end up on the same character in the next iteration.
|
|
++it;
|
|
|
|
totalWeight += 1;
|
|
totalWeight += lastWasSpace ? 1 : 0;
|
|
totalWeight += (lastWasMatch) ? 1 : 0;
|
|
}
|
|
|
|
return totalWeight;
|
|
}
|
|
|
|
void CommandPalette::SetDispatch(const winrt::TerminalApp::ShortcutActionDispatch& dispatch)
|
|
{
|
|
_dispatch = dispatch;
|
|
}
|
|
|
|
// Method Description:
|
|
// - Dismiss the command palette. This will:
|
|
// * select all the current text in the input box
|
|
// * set our visibility to Hidden
|
|
// * raise our Closed event, so the page can return focus to the active Terminal
|
|
// Arguments:
|
|
// - <none>
|
|
// Return Value:
|
|
// - <none>
|
|
void CommandPalette::_close()
|
|
{
|
|
Visibility(Visibility::Collapsed);
|
|
|
|
// Reset visibility in case anchor mode tab switcher just finished.
|
|
_searchBox().Visibility(Visibility::Visible);
|
|
|
|
// Clear the text box each time we close the dialog. This is consistent with VsCode.
|
|
_searchBox().Text(L"");
|
|
|
|
_nestedActionStack.Clear();
|
|
|
|
ParentCommandName(L"");
|
|
_currentNestedCommands.Clear();
|
|
}
|
|
|
|
void CommandPalette::EnableTabSwitcherMode(const bool searchMode, const uint32_t startIdx)
|
|
{
|
|
_switcherStartIdx = startIdx;
|
|
|
|
if (searchMode)
|
|
{
|
|
_switchToMode(CommandPaletteMode::TabSearchMode);
|
|
}
|
|
else
|
|
{
|
|
_switchToMode(CommandPaletteMode::TabSwitchMode);
|
|
}
|
|
|
|
_updateFilteredActions();
|
|
}
|
|
}
|