diff --git a/src/cascadia/TerminalApp/SuggestionsControl.cpp b/src/cascadia/TerminalApp/SuggestionsControl.cpp index d8d11a71ef9..366205fc85a 100644 --- a/src/cascadia/TerminalApp/SuggestionsControl.cpp +++ b/src/cascadia/TerminalApp/SuggestionsControl.cpp @@ -8,6 +8,7 @@ #include #include "SuggestionsControl.g.cpp" +#include "../../types/inc/utils.hpp" using namespace winrt; using namespace winrt::TerminalApp; @@ -19,6 +20,8 @@ using namespace winrt::Windows::Foundation; using namespace winrt::Windows::Foundation::Collections; using namespace winrt::Microsoft::Terminal::Settings::Model; +using namespace std::chrono_literals; + namespace winrt::TerminalApp::implementation { SuggestionsControl::SuggestionsControl() @@ -103,9 +106,7 @@ namespace winrt::TerminalApp::implementation // 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); + this->_recalculateTopMargin(); } }); @@ -281,9 +282,78 @@ namespace winrt::TerminalApp::implementation { if (const auto actionPaletteItem{ filteredCommand.Item().try_as() }) { - PreviewAction.raise(*this, actionPaletteItem.Command()); + const auto& cmd = actionPaletteItem.Command(); + PreviewAction.raise(*this, cmd); + + const auto description{ cmd.Description() }; + + if (const auto& selected{ SelectedItem() }) + { + selected.SetValue(Automation::AutomationProperties::FullDescriptionProperty(), winrt::box_value(description)); + } + + if (!description.empty()) + { + _openTooltip(cmd); + } + else + { + // If there's no description, then just close the tooltip. + _descriptionsView().Visibility(Visibility::Collapsed); + _descriptionsBackdrop().Visibility(Visibility::Collapsed); + _recalculateTopMargin(); + } + } + } + } + + void SuggestionsControl::_openTooltip(Command cmd) + { + const auto description{ cmd.Description() }; + if (description.empty()) + { + return; + } + + // Build the contents of the "tooltip" based on the description + // + // First, the title. This is just the name of the command. + _descriptionTitle().Inlines().Clear(); + Documents::Run titleRun; + titleRun.Text(cmd.Name()); + _descriptionTitle().Inlines().Append(titleRun); + + // Now fill up the "subtitle" part of the "tooltip" with the actual + // description itself. + const auto& inlines{ _descriptionComment().Inlines() }; + inlines.Clear(); + + // Split the filtered description on '\n` + const auto lines = ::Microsoft::Console::Utils::SplitString(description, L'\n'); + // build a Run + LineBreak, and add them to the text block + for (const auto& line : lines) + { + // Trim off any `\r`'s in the string. Pwsh completions will + // frequently have these embedded. + std::wstring trimmed{ line }; + trimmed.erase(std::remove(trimmed.begin(), trimmed.end(), L'\r'), trimmed.end()); + if (trimmed.empty()) + { + continue; } + + Documents::Run textRun; + textRun.Text(trimmed); + inlines.Append(textRun); + inlines.Append(Documents::LineBreak{}); } + + // Now, make ourselves visible. + _descriptionsView().Visibility(Visibility::Visible); + _descriptionsBackdrop().Visibility(Visibility::Visible); + // and update the padding to account for our new contents. + _recalculateTopMargin(); + return; } void SuggestionsControl::_previewKeyDownHandler(const IInspectable& /*sender*/, @@ -1020,16 +1090,71 @@ namespace winrt::TerminalApp::implementation void SuggestionsControl::_setDirection(TerminalApp::SuggestionsDirection direction) { _direction = direction; + + // We need to move either the list of suggestions, or the tooltip, to + // the top of the stack panel (depending on the layout). + Grid controlToMoveToTop = nullptr; + if (_direction == TerminalApp::SuggestionsDirection::TopDown) { Controls::Grid::SetRow(_searchBox(), 0); + controlToMoveToTop = _backdrop(); } else // BottomUp { Controls::Grid::SetRow(_searchBox(), 4); + controlToMoveToTop = _descriptionsBackdrop(); + } + + assert(controlToMoveToTop); + const auto& children{ _listAndDescriptionStack().Children() }; + uint32_t index; + if (children.IndexOf(controlToMoveToTop, index)) + { + children.Move(index, 0); } } + void SuggestionsControl::_recalculateTopMargin() + { + auto currentMargin = Margin(); + // Call Measure() on the descriptions backdrop, so that it gets it's new + // DesiredSize for this new description text. + // + // If you forget this, then we _probably_ weren't laid out since + // updating that text, and the ActualHeight will be the _last_ + // description's height. + _descriptionsBackdrop().Measure({ + static_cast(ActualWidth()), + static_cast(ActualHeight()), + }); + + // Now, position vertically. + if (_direction == TerminalApp::SuggestionsDirection::TopDown) + { + // The control should open right below the cursor, with the list + // extending below. This is easy, we can just use the cursor as the + // origin (more or less) + currentMargin.Top = (_anchor.Y); + } + else + { + // Bottom Up. + + // This is wackier, because we need to calculate the offset upwards + // from our anchor. So we need to get the size of our elements: + const auto backdropHeight = _backdrop().ActualHeight(); + const auto descriptionDesiredHeight = _descriptionsBackdrop().Visibility() == Visibility::Visible ? + _descriptionsBackdrop().DesiredSize().Height : + 0; + + const auto marginTop = (_anchor.Y - backdropHeight - descriptionDesiredHeight); + + currentMargin.Top = marginTop; + } + Margin(currentMargin); + } + void SuggestionsControl::Open(TerminalApp::SuggestionsMode mode, const Windows::Foundation::Collections::IVector& commands, winrt::hstring filter, @@ -1047,9 +1172,8 @@ namespace winrt::TerminalApp::implementation _anchor = anchor; _space = space; - const til::size actualSize{ til::math::rounding, ActualWidth(), ActualHeight() }; // Is there space in the window below the cursor to open the menu downwards? - const bool canOpenDownwards = (_anchor.Y + characterHeight + actualSize.height) < space.Height; + const bool canOpenDownwards = (_anchor.Y + characterHeight + ActualHeight()) < space.Height; _setDirection(canOpenDownwards ? TerminalApp::SuggestionsDirection::TopDown : TerminalApp::SuggestionsDirection::BottomUp); // Set the anchor below by a character height @@ -1063,26 +1187,13 @@ namespace winrt::TerminalApp::implementation const auto proposedX = gsl::narrow_cast(_anchor.X - 40); // If the control is too wide to fit in the window, clamp it fit inside // the window. - const auto maxX = gsl::narrow_cast(space.Width - actualSize.width); + const auto maxX = gsl::narrow_cast(space.Width - ActualWidth()); const auto clampedX = std::clamp(proposedX, 0, maxX); - // Create a thickness for the new margins - auto newMargin = Windows::UI::Xaml::ThicknessHelper::FromLengths(clampedX, 0, 0, 0); - // Now, position vertically. - if (_direction == TerminalApp::SuggestionsDirection::TopDown) - { - // The control should open right below the cursor, with the list - // extending below. This is easy, we can just use the cursor as the - // origin (more or less) - newMargin.Top = (_anchor.Y); - } - else - { - // Position at the cursor. The suggestions UI itself will maintain - // its own offset such that it's always above its origin - newMargin.Top = (_anchor.Y - actualSize.height); - } - Margin(newMargin); + // Create a thickness for the new margins. This will set the left, then + // we'll go update the top separately + Margin(Windows::UI::Xaml::ThicknessHelper::FromLengths(clampedX, 0, 0, 0)); + _recalculateTopMargin(); _searchBox().Text(filter); @@ -1099,5 +1210,4 @@ namespace winrt::TerminalApp::implementation // selection starting at the end of the string. _searchBox().Select(filter.size(), 0); } - } diff --git a/src/cascadia/TerminalApp/SuggestionsControl.h b/src/cascadia/TerminalApp/SuggestionsControl.h index 356251ebc61..9ec46e2f3e7 100644 --- a/src/cascadia/TerminalApp/SuggestionsControl.h +++ b/src/cascadia/TerminalApp/SuggestionsControl.h @@ -98,6 +98,8 @@ namespace winrt::TerminalApp::implementation void _close(); void _dismissPalette(); + void _recalculateTopMargin(); + void _filterTextChanged(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& args); void _previewKeyDownHandler(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::Input::KeyRoutedEventArgs& e); void _keyUpHandler(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::Input::KeyRoutedEventArgs& e); @@ -110,6 +112,7 @@ namespace winrt::TerminalApp::implementation void _listItemClicked(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::Controls::ItemClickEventArgs& e); void _listItemSelectionChanged(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::Controls::SelectionChangedEventArgs& e); void _selectedCommandChanged(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& args); + void _openTooltip(Microsoft::Terminal::Settings::Model::Command cmd); void _moveBackButtonClicked(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs&); void _updateCurrentNestedCommands(const winrt::Microsoft::Terminal::Settings::Model::Command& parentCommand); @@ -121,7 +124,6 @@ namespace winrt::TerminalApp::implementation Windows::Foundation::Collections::IVector _commandsToFilter(); std::wstring _getTrimmedInput(); uint32_t _getNumVisibleItems(); - friend class TerminalAppLocalTests::TabTests; }; } diff --git a/src/cascadia/TerminalApp/SuggestionsControl.xaml b/src/cascadia/TerminalApp/SuggestionsControl.xaml index c1cbdbab97a..47bcef82ccd 100644 --- a/src/cascadia/TerminalApp/SuggestionsControl.xaml +++ b/src/cascadia/TerminalApp/SuggestionsControl.xaml @@ -102,23 +102,10 @@ - - - - - - - - - - - - + + - + - + - + - + - + + + + + + + + + + + + + + + + - + diff --git a/src/cascadia/TerminalSettingsModel/Command.cpp b/src/cascadia/TerminalSettingsModel/Command.cpp index 519ffc5e980..b79c398351c 100644 --- a/src/cascadia/TerminalSettingsModel/Command.cpp +++ b/src/cascadia/TerminalSettingsModel/Command.cpp @@ -37,6 +37,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation command->_ActionAndArgs = *get_self(_ActionAndArgs)->Copy(); command->_iconPath = _iconPath; command->_IterateOn = _IterateOn; + command->_Description = _Description; command->_originalJson = _originalJson; command->_nestedCommand = _nestedCommand; @@ -219,6 +220,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation auto nested = false; JsonUtils::GetValueForKey(json, IterateOnKey, result->_IterateOn); + JsonUtils::GetValueForKey(json, DescriptionKey, result->_Description); // For iterable commands, we'll make another pass at parsing them once // the json is patched. So ignore parsing sub-commands for now. Commands @@ -353,6 +355,10 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation { JsonUtils::SetValueForKey(cmdJson, IconKey, _iconPath); JsonUtils::SetValueForKey(cmdJson, NameKey, _name); + if (!_Description.empty()) + { + JsonUtils::SetValueForKey(cmdJson, DescriptionKey, _Description); + } if (!_ID.empty()) { JsonUtils::SetValueForKey(cmdJson, IDKey, _ID); @@ -580,8 +586,10 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation const auto parseElement = [&](const auto& element) { winrt::hstring completionText; winrt::hstring listText; + winrt::hstring tooltipText; JsonUtils::GetValueForKey(element, "CompletionText", completionText); JsonUtils::GetValueForKey(element, "ListItemText", listText); + JsonUtils::GetValueForKey(element, "ToolTip", tooltipText); auto args = winrt::make_self( winrt::hstring{ fmt::format(FMT_COMPILE(L"{:\x7f^{}}{}"), @@ -593,8 +601,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation auto c = winrt::make_self(); c->_name = listText; + c->_Description = tooltipText; c->_ActionAndArgs = actionAndArgs; - // Try to assign a sensible icon based on the result type. These are // roughly chosen to align with the icons in // https://github.com/PowerShell/PowerShellEditorServices/pull/1738 diff --git a/src/cascadia/TerminalSettingsModel/Command.h b/src/cascadia/TerminalSettingsModel/Command.h index 7f19eb56944..e67b593e561 100644 --- a/src/cascadia/TerminalSettingsModel/Command.h +++ b/src/cascadia/TerminalSettingsModel/Command.h @@ -38,6 +38,7 @@ static constexpr std::string_view ActionKey{ "command" }; static constexpr std::string_view IterateOnKey{ "iterateOn" }; static constexpr std::string_view CommandsKey{ "commands" }; static constexpr std::string_view KeysKey{ "keys" }; +static constexpr std::string_view DescriptionKey{ "description" }; namespace winrt::Microsoft::Terminal::Settings::Model::implementation { @@ -83,6 +84,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation WINRT_PROPERTY(ExpandCommandType, IterateOn, ExpandCommandType::None); WINRT_PROPERTY(Model::ActionAndArgs, ActionAndArgs); WINRT_PROPERTY(OriginTag, Origin); + WINRT_PROPERTY(winrt::hstring, Description, L""); private: Json::Value _originalJson; diff --git a/src/cascadia/TerminalSettingsModel/Command.idl b/src/cascadia/TerminalSettingsModel/Command.idl index 78d09d78092..aacc2a1bd74 100644 --- a/src/cascadia/TerminalSettingsModel/Command.idl +++ b/src/cascadia/TerminalSettingsModel/Command.idl @@ -37,6 +37,9 @@ namespace Microsoft.Terminal.Settings.Model String Name { get; }; String ID { get; }; + + String Description { get; }; + ActionAndArgs ActionAndArgs { get; }; String IconPath;