diff --git a/src/cascadia/TerminalApp/AppActionHandlers.cpp b/src/cascadia/TerminalApp/AppActionHandlers.cpp index cad581983f3..2b953dd24a2 100644 --- a/src/cascadia/TerminalApp/AppActionHandlers.cpp +++ b/src/cascadia/TerminalApp/AppActionHandlers.cpp @@ -1257,6 +1257,34 @@ namespace winrt::TerminalApp::implementation } } + void TerminalPage::_HandleSuggestions(const IInspectable& /*sender*/, + const ActionEventArgs& args) + { + if (args) + { + if (const auto& realArgs = args.ActionArgs().try_as()) + { + auto source = realArgs.Source(); + + switch (source) + { + case SuggestionsSource::CommandHistory: + { + if (const auto& control{ _GetActiveControl() }) + { + const auto context = control.CommandHistory(); + _OpenSuggestions(control, + Command::HistoryToCommands(context.History(), context.CurrentCommandline(), false), + SuggestionsMode::Palette); + } + args.Handled(true); + } + break; + } + } + } + } + void TerminalPage::_HandleColorSelection(const IInspectable& /*sender*/, const ActionEventArgs& args) { diff --git a/src/cascadia/TerminalApp/SuggestionsControl.cpp b/src/cascadia/TerminalApp/SuggestionsControl.cpp index 83694267a6d..528fe9f4b71 100644 --- a/src/cascadia/TerminalApp/SuggestionsControl.cpp +++ b/src/cascadia/TerminalApp/SuggestionsControl.cpp @@ -96,17 +96,17 @@ namespace winrt::TerminalApp::implementation } }); - // 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*/) { - // This does only fire once, when the size changes, which is the - // very first time it's opened. It does not fire for subsequent - // openings. - - _sizeChangedRevoker.revoke(); + // When we're in BottomUp mode, we need to adjust our own position + // so that our bottom is aligned with our origin. This will ensure + // that as the menu changes in size (as we filter results), the menu + // stays "attached" to the cursor. + if (Visibility() == Visibility::Visible && _direction == TerminalApp::SuggestionsDirection::BottomUp) + { + auto m = this->Margin(); + m.Top = (_anchor.Y - ActualHeight()); + this->Margin(m); + } }); _filteredActionsView().SelectionChanged({ this, &SuggestionsControl::_selectedCommandChanged }); @@ -851,11 +851,13 @@ namespace winrt::TerminalApp::implementation } } } - if (_mode == SuggestionsMode::Palette) - { - // We want to present the commands sorted - std::sort(actions.begin(), actions.end(), FilteredCommand::Compare); - } + + // No sorting in palette mode, so results are still filtered, but in the + // original order. This feels more right for something like + // recentCommands. + // + // This is in contrast to the Command Palette, which always sorts its + // actions. // Adjust the order of the results depending on if we're top-down or // bottom up. This way, the "first" / "best" match is always closest to diff --git a/src/cascadia/TerminalControl/ControlCore.cpp b/src/cascadia/TerminalControl/ControlCore.cpp index c33d33d2cc5..eefbdd54856 100644 --- a/src/cascadia/TerminalControl/ControlCore.cpp +++ b/src/cascadia/TerminalControl/ControlCore.cpp @@ -1951,6 +1951,42 @@ namespace winrt::Microsoft::Terminal::Control::implementation return hstring{ str }; } + // Get all of our recent commands. This will only really work if the user has enabled shell integration. + Control::CommandHistoryContext ControlCore::CommandHistory() const + { + auto terminalLock = _terminal->LockForWriting(); + const auto& textBuffer = _terminal->GetTextBuffer(); + + std::vector commands; + for (const auto& mark : _terminal->GetScrollMarks()) + { + // The command text is between the `end` (which denotes the end of + // the prompt) and the `commandEnd`. + bool markHasCommand = mark.commandEnd.has_value() && + mark.commandEnd != mark.end; + if (!markHasCommand) + { + continue; + } + + // Get the text of the command + const auto line = mark.end.y; + const auto& row = textBuffer.GetRowByOffset(line); + const auto commandText = row.GetText(mark.end.x, mark.commandEnd->x); + + // Trim off trailing spaces. + const auto strEnd = commandText.find_last_not_of(UNICODE_SPACE); + if (strEnd != std::string::npos) + { + const auto trimmed = commandText.substr(0, strEnd + 1); + commands.push_back(winrt::hstring{ trimmed }); + } + } + auto context = winrt::make_self(std::move(commands)); + + return *context; + } + Core::Scheme ControlCore::ColorScheme() const noexcept { Core::Scheme s; diff --git a/src/cascadia/TerminalControl/ControlCore.h b/src/cascadia/TerminalControl/ControlCore.h index eea115952f9..5f9f23a3e89 100644 --- a/src/cascadia/TerminalControl/ControlCore.h +++ b/src/cascadia/TerminalControl/ControlCore.h @@ -17,6 +17,7 @@ #include "ControlCore.g.h" #include "SelectionColor.g.h" +#include "CommandHistoryContext.g.h" #include "ControlSettings.h" #include "../../audio/midi/MidiAudio.hpp" #include "../../renderer/base/Renderer.hpp" @@ -53,6 +54,16 @@ namespace winrt::Microsoft::Terminal::Control::implementation til::property Color; til::property IsIndex16; }; + struct CommandHistoryContext : CommandHistoryContextT + { + til::property> History; + til::property CurrentCommandline; + + CommandHistoryContext(std::vector&& history) + { + History(winrt::single_threaded_vector(std::move(history))); + } + }; struct ControlCore : ControlCoreT { @@ -213,6 +224,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation void SetReadOnlyMode(const bool readOnlyState); hstring ReadEntireBuffer() const; + Control::CommandHistoryContext CommandHistory() const; static bool IsVintageOpacityAvailable() noexcept; diff --git a/src/cascadia/TerminalControl/ControlCore.idl b/src/cascadia/TerminalControl/ControlCore.idl index 8f8f87f2e4b..8157346dcd1 100644 --- a/src/cascadia/TerminalControl/ControlCore.idl +++ b/src/cascadia/TerminalControl/ControlCore.idl @@ -58,6 +58,12 @@ namespace Microsoft.Terminal.Control Boolean IsIndex16; }; + [default_interface] runtimeclass CommandHistoryContext + { + IVector History { get; }; + String CurrentCommandline { get; }; + }; + [default_interface] runtimeclass ControlCore : ICoreState { ControlCore(IControlSettings settings, @@ -136,6 +142,7 @@ namespace Microsoft.Terminal.Control void EnablePainting(); String ReadEntireBuffer(); + CommandHistoryContext CommandHistory(); void AdjustOpacity(Double Opacity, Boolean relative); void WindowVisibilityChanged(Boolean showOrHide); diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index c82de3b3acd..2bce8ecc0d0 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -3377,6 +3377,10 @@ namespace winrt::Microsoft::Terminal::Control::implementation { return _core.ReadEntireBuffer(); } + Control::CommandHistoryContext TermControl::CommandHistory() const + { + return _core.CommandHistory(); + } Core::Scheme TermControl::ColorScheme() const noexcept { diff --git a/src/cascadia/TerminalControl/TermControl.h b/src/cascadia/TerminalControl/TermControl.h index a11c3f7c469..6b05e7e9a83 100644 --- a/src/cascadia/TerminalControl/TermControl.h +++ b/src/cascadia/TerminalControl/TermControl.h @@ -142,6 +142,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation static Windows::UI::Xaml::Thickness ParseThicknessFromPadding(const hstring padding); hstring ReadEntireBuffer() const; + Control::CommandHistoryContext CommandHistory() const; winrt::Microsoft::Terminal::Core::Scheme ColorScheme() const noexcept; void ColorScheme(const winrt::Microsoft::Terminal::Core::Scheme& scheme) const noexcept; diff --git a/src/cascadia/TerminalControl/TermControl.idl b/src/cascadia/TerminalControl/TermControl.idl index 11b47d8de35..63495109325 100644 --- a/src/cascadia/TerminalControl/TermControl.idl +++ b/src/cascadia/TerminalControl/TermControl.idl @@ -112,6 +112,7 @@ namespace Microsoft.Terminal.Control void SetReadOnly(Boolean readOnlyState); String ReadEntireBuffer(); + CommandHistoryContext CommandHistory(); void AdjustOpacity(Double Opacity, Boolean relative); diff --git a/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp b/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp index 7c9802c07cb..ae81d5b498f 100644 --- a/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp +++ b/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp @@ -52,6 +52,7 @@ static constexpr std::string_view SwitchToTabKey{ "switchToTab" }; static constexpr std::string_view TabSearchKey{ "tabSearch" }; static constexpr std::string_view ToggleAlwaysOnTopKey{ "toggleAlwaysOnTop" }; static constexpr std::string_view ToggleCommandPaletteKey{ "commandPalette" }; +static constexpr std::string_view SuggestionsKey{ "showSuggestions" }; static constexpr std::string_view ToggleFocusModeKey{ "toggleFocusMode" }; static constexpr std::string_view SetFocusModeKey{ "setFocusMode" }; static constexpr std::string_view ToggleFullscreenKey{ "toggleFullscreen" }; @@ -386,6 +387,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation { ShortcutAction::TabSearch, RS_(L"TabSearchCommandKey") }, { ShortcutAction::ToggleAlwaysOnTop, RS_(L"ToggleAlwaysOnTopCommandKey") }, { ShortcutAction::ToggleCommandPalette, MustGenerate }, + { ShortcutAction::Suggestions, MustGenerate }, { ShortcutAction::ToggleFocusMode, RS_(L"ToggleFocusModeCommandKey") }, { ShortcutAction::SetFocusMode, MustGenerate }, { ShortcutAction::ToggleFullscreen, RS_(L"ToggleFullscreenCommandKey") }, diff --git a/src/cascadia/TerminalSettingsModel/ActionArgs.cpp b/src/cascadia/TerminalSettingsModel/ActionArgs.cpp index 64f7bd13b0c..6dc778df909 100644 --- a/src/cascadia/TerminalSettingsModel/ActionArgs.cpp +++ b/src/cascadia/TerminalSettingsModel/ActionArgs.cpp @@ -33,6 +33,7 @@ #include "AddMarkArgs.g.cpp" #include "FindMatchArgs.g.cpp" #include "ToggleCommandPaletteArgs.g.cpp" +#include "SuggestionsArgs.g.cpp" #include "NewWindowArgs.g.cpp" #include "PrevTabArgs.g.cpp" #include "NextTabArgs.g.cpp" @@ -706,6 +707,16 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation return RS_(L"ToggleCommandPaletteCommandKey"); } + winrt::hstring SuggestionsArgs::GenerateName() const + { + switch (Source()) + { + case SuggestionsSource::CommandHistory: + return RS_(L"SuggestionsCommandHistoryCommandKey"); + } + return RS_(L"SuggestionsCommandKey"); + } + winrt::hstring FindMatchArgs::GenerateName() const { switch (Direction()) diff --git a/src/cascadia/TerminalSettingsModel/ActionArgs.h b/src/cascadia/TerminalSettingsModel/ActionArgs.h index 99f52ad991d..8e278e16a3a 100644 --- a/src/cascadia/TerminalSettingsModel/ActionArgs.h +++ b/src/cascadia/TerminalSettingsModel/ActionArgs.h @@ -34,6 +34,7 @@ #include "AddMarkArgs.g.h" #include "MoveTabArgs.g.h" #include "ToggleCommandPaletteArgs.g.h" +#include "SuggestionsArgs.g.h" #include "FindMatchArgs.g.h" #include "NewWindowArgs.g.h" #include "PrevTabArgs.g.h" @@ -213,6 +214,10 @@ private: \ #define TOGGLE_COMMAND_PALETTE_ARGS(X) \ X(CommandPaletteLaunchMode, LaunchMode, "launchMode", false, CommandPaletteLaunchMode::Action) +//////////////////////////////////////////////////////////////////////////////// +#define SUGGESTIONS_ARGS(X) \ + X(SuggestionsSource, Source, "source", false, SuggestionsSource::Tasks) + //////////////////////////////////////////////////////////////////////////////// #define FIND_MATCH_ARGS(X) \ X(FindMatchDirection, Direction, "direction", args->Direction() == FindMatchDirection::None, FindMatchDirection::None) @@ -709,6 +714,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation ACTION_ARGS_STRUCT(ToggleCommandPaletteArgs, TOGGLE_COMMAND_PALETTE_ARGS); + ACTION_ARGS_STRUCT(SuggestionsArgs, SUGGESTIONS_ARGS); + ACTION_ARGS_STRUCT(FindMatchArgs, FIND_MATCH_ARGS); ACTION_ARGS_STRUCT(PrevTabArgs, PREV_TAB_ARGS); @@ -836,6 +843,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::factory_implementation BASIC_FACTORY(ClearBufferArgs); BASIC_FACTORY(MultipleActionsArgs); BASIC_FACTORY(AdjustOpacityArgs); + BASIC_FACTORY(SuggestionsArgs); BASIC_FACTORY(SelectCommandArgs); BASIC_FACTORY(SelectOutputArgs); } diff --git a/src/cascadia/TerminalSettingsModel/ActionArgs.idl b/src/cascadia/TerminalSettingsModel/ActionArgs.idl index a5daa935ece..b061eaae2fe 100644 --- a/src/cascadia/TerminalSettingsModel/ActionArgs.idl +++ b/src/cascadia/TerminalSettingsModel/ActionArgs.idl @@ -112,6 +112,12 @@ namespace Microsoft.Terminal.Settings.Model ToCurrent, ToMouse, }; + enum SuggestionsSource + { + Tasks, + CommandHistory, + DirectoryHistory, + }; [default_interface] runtimeclass NewTerminalArgs { NewTerminalArgs(); @@ -318,6 +324,13 @@ namespace Microsoft.Terminal.Settings.Model CommandPaletteLaunchMode LaunchMode { get; }; }; + [default_interface] runtimeclass SuggestionsArgs : IActionArgs + { + SuggestionsArgs(); + SuggestionsArgs(SuggestionsSource source); + SuggestionsSource Source { get; }; + }; + [default_interface] runtimeclass FindMatchArgs : IActionArgs { FindMatchArgs(FindMatchDirection direction); diff --git a/src/cascadia/TerminalSettingsModel/AllShortcutActions.h b/src/cascadia/TerminalSettingsModel/AllShortcutActions.h index 53a260f46ea..df008b58220 100644 --- a/src/cascadia/TerminalSettingsModel/AllShortcutActions.h +++ b/src/cascadia/TerminalSettingsModel/AllShortcutActions.h @@ -103,6 +103,7 @@ ON_ALL_ACTIONS(MarkMode) \ ON_ALL_ACTIONS(ToggleBlockSelection) \ ON_ALL_ACTIONS(SwitchSelectionEndpoint) \ + ON_ALL_ACTIONS(Suggestions) \ ON_ALL_ACTIONS(ColorSelection) \ ON_ALL_ACTIONS(ShowContextMenu) \ ON_ALL_ACTIONS(ExpandSelectionToWord) \ @@ -150,6 +151,7 @@ ON_ALL_ACTIONS_WITH_ARGS(ClearBuffer) \ ON_ALL_ACTIONS_WITH_ARGS(MultipleActions) \ ON_ALL_ACTIONS_WITH_ARGS(AdjustOpacity) \ + ON_ALL_ACTIONS_WITH_ARGS(Suggestions) \ ON_ALL_ACTIONS_WITH_ARGS(SelectCommand) \ ON_ALL_ACTIONS_WITH_ARGS(SelectOutput) \ ON_ALL_ACTIONS_WITH_ARGS(ColorSelection) diff --git a/src/cascadia/TerminalSettingsModel/Command.cpp b/src/cascadia/TerminalSettingsModel/Command.cpp index e2eec5c95b0..b34e5a02c2e 100644 --- a/src/cascadia/TerminalSettingsModel/Command.cpp +++ b/src/cascadia/TerminalSettingsModel/Command.cpp @@ -730,4 +730,53 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation return winrt::single_threaded_vector(std::move(result)); } + + // Method description: + // * Convert the list of recent commands into a list of sendInput actions to + // send those commands. + // * We'll give each command a "history" icon. + // * If directories is true, we'll prepend "cd " to each command, so that + // the command will be run as a directory change instead. + IVector Command::HistoryToCommands(IVector history, + winrt::hstring /*currentCommandline*/, + bool directories) + { + std::wstring cdText = directories ? L"cd " : L""; + auto result = std::vector(); + + // Use this map to discard duplicates. + std::unordered_map foundCommands{}; + + auto backspaces = std::wstring(::base::saturated_cast(0), L'\x7f'); + + // Iterate in reverse over the history, so that most recent commands are first + for (auto i = history.Size(); i > 0; i--) + { + std::wstring_view line{ history.GetAt(i - 1) }; + + if (line.empty()) + { + continue; + } + if (foundCommands.contains(line)) + { + continue; + } + auto args = winrt::make_self( + winrt::hstring{ fmt::format(L"{}{}{}", cdText, backspaces, line) }); + + Model::ActionAndArgs actionAndArgs{ ShortcutAction::SendInput, *args }; + + auto command = winrt::make_self(); + command->_ActionAndArgs = actionAndArgs; + command->_name = winrt::hstring{ line }; + command->_iconPath = directories ? + L"\ue8da" : // OpenLocal (a folder with an arrow pointing up) + L"\ue81c"; // History icon + result.push_back(*command); + foundCommands[line] = true; + } + + return winrt::single_threaded_vector(std::move(result)); + } } diff --git a/src/cascadia/TerminalSettingsModel/Command.h b/src/cascadia/TerminalSettingsModel/Command.h index b001c795671..59a080c4c0a 100644 --- a/src/cascadia/TerminalSettingsModel/Command.h +++ b/src/cascadia/TerminalSettingsModel/Command.h @@ -67,6 +67,9 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation void IconPath(const hstring& val); static Windows::Foundation::Collections::IVector ParsePowerShellMenuComplete(winrt::hstring json, int32_t replaceLength); + static Windows::Foundation::Collections::IVector HistoryToCommands(Windows::Foundation::Collections::IVector history, + winrt::hstring currentCommandline, + bool directories); WINRT_PROPERTY(ExpandCommandType, IterateOn, ExpandCommandType::None); WINRT_PROPERTY(Model::ActionAndArgs, ActionAndArgs); diff --git a/src/cascadia/TerminalSettingsModel/Command.idl b/src/cascadia/TerminalSettingsModel/Command.idl index 934df64d6cb..e92f459e69d 100644 --- a/src/cascadia/TerminalSettingsModel/Command.idl +++ b/src/cascadia/TerminalSettingsModel/Command.idl @@ -47,5 +47,7 @@ namespace Microsoft.Terminal.Settings.Model Windows.Foundation.Collections.IMapView NestedCommands { get; }; static IVector ParsePowerShellMenuComplete(String json, Int32 replaceLength); + static IVector HistoryToCommands(IVector commandHistory, String commandline, Boolean directories); + } } diff --git a/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw b/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw index 8de5d841734..74035df755d 100644 --- a/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw @@ -449,6 +449,12 @@ Toggle command palette + + Recent commands... + + + Open suggestions... + Toggle command palette in command line mode @@ -714,4 +720,4 @@ Search the web for selected text This will open a web browser to search for some user-selected text - + \ No newline at end of file diff --git a/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h b/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h index 4685f6f3581..73af3411e2d 100644 --- a/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h +++ b/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h @@ -499,6 +499,15 @@ JSON_ENUM_MAPPER(::winrt::Microsoft::Terminal::Settings::Model::FindMatchDirecti }; }; +JSON_ENUM_MAPPER(::winrt::Microsoft::Terminal::Settings::Model::SuggestionsSource) +{ + JSON_MAPPINGS(3) = { + pair_type{ "tasks", ValueType::Tasks }, + pair_type{ "commandHistory", ValueType::CommandHistory }, + pair_type{ "directoryHistory", ValueType::DirectoryHistory } + }; +}; + JSON_ENUM_MAPPER(::winrt::Microsoft::Terminal::Settings::Model::WindowingMode) { JSON_MAPPINGS(3) = {