From a0c88bb5117b8062cee4c4cad934a60e72768eb1 Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Mon, 14 Aug 2023 05:46:42 -0500 Subject: [PATCH] Add Suggestions UI & experimental shell completions support (#14938) There's two parts to this PR that should be considered _separately_. 1. The Suggestions UI, a new graphical menu for displaying suggestions / completions to the user in the context of the terminal the user is working in. 2. The VsCode shell completions protocol. This enables the shell to invoke this UI via a VT sequence. These are being introduced at the same time, because they both require one another. However, I need to absolutely emphasize: ### THE FORMAT OF THE COMPLETION PROTOCOL IS EXPERIMENTAL AND SUBJECT TO CHANGE This is what we've prototyped with VsCode, but we're still working on how we want to conclusively define that protocol. However, we can also refine the Suggestions UI independently of how the protocol is actually implemented. This will let us rev the Suggestions UI to support other things like tooltips, recent commands, tasks, INDEPENDENTLY of us rev'ing the completion protocol. So yes, they're both here, but let's not nitpick that protocol for now. ### Checklist * Doesn't actually close anything * Heavily related to #3121, but I'm not gonna say that's closed till we settle on the protocol * See also: * #1595 * #14779 * https://github.com/microsoft/vscode/pull/171648 ### Detailed Description #### Suggestions UI The Suggestions UI is spec'ed over in #14864, so go read that. It's basically a transient Command Palette, that floats by the user's cursor. It's heavily forked from the Command Palette code, with all the business about switching modes removed. The major bit of new code is `SuggestionsControl::Anchor`. It also supports two "modes": * A "palette", which is like the command palette - a list with a text box * A "menu", which is more like the intellisense flyout. No text box. This is the mode that the shell completions use #### Shell Completions Protocol I literally cannot say this enough times - this protocol is experimental and subject to change. Build on it at your own peril. It's disabled in Release builds (but available in preview behind `globals.experimental.enableShellCompletionMenu`), so that when it ships, no one can take a dependency on it accidentally. Right now we're just taking a blob of JSON, passing that up to the App layer, who asks `Command` to parse it and build a list of `sendInput` actions to populate the menu with. It's not a particularly elegant solution, but it's good enough to prototype with. #### How do I test this? I've been testing this in two parts. You'll need a snippet in your powershell profile, and a keybinding in the Terminal settings to trigger it. The work together by binding Ctrl+space to _essentially_ send F12b. Wacky, but it works. ```json { "command": { "action": "sendInput","input": "\u001b[24~b" }, "keys": "ctrl+space" }, ``` ```ps1 function Send-Completions2 { $commandLine = "" $cursorIndex = 0 # TODO: Since fuzzy matching exists, should completions be provided only for character after the # last space and then filter on the client side? That would let you trigger ctrl+space # anywhere on a word and have full completions available [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$commandLine, [ref]$cursorIndex) $completionPrefix = $commandLine # Get completions $result = "`e]633;Completions" if ($completionPrefix.Length -gt 0) { # Get and send completions $completions = TabExpansion2 -inputScript $completionPrefix -cursorColumn $cursorIndex if ($null -ne $completions.CompletionMatches) { $result += ";$($completions.ReplacementIndex);$($completions.ReplacementLength);$($cursorIndex);" $result += $completions.CompletionMatches | ConvertTo-Json -Compress } } $result += "`a" Write-Host -NoNewLine $result } function Set-MappedKeyHandlers { # VS Code send completions request (may override Ctrl+Spacebar) Set-PSReadLineKeyHandler -Chord 'F12,b' -ScriptBlock { Send-Completions2 } } # Register key handlers if PSReadLine is available if (Get-Module -Name PSReadLine) { Set-MappedKeyHandlers } ``` ### TODO * [x] `(prompt | format-hex).`Ctrl+space -> This always throws an exception. Seems like the payload is always clipped to ```{"CompletionText":"Ascii","ListItemText":"Ascii","ResultType":5,"ToolTip":"string Ascii { get``` and that ain't JSON. Investigate on the pwsh side? --- .github/actions/spelling/allow/allow.txt | 1 + .github/actions/spelling/expect/expect.txt | 2 + src/cascadia/LocalTests_TerminalApp/pch.h | 2 + .../TerminalApp/SuggestionsControl.cpp | 1103 +++++++++++++++++ src/cascadia/TerminalApp/SuggestionsControl.h | 132 ++ .../TerminalApp/SuggestionsControl.idl | 48 + .../TerminalApp/SuggestionsControl.xaml | 214 ++++ .../TerminalApp/TerminalAppLib.vcxproj | 13 + src/cascadia/TerminalApp/TerminalPage.cpp | 117 +- src/cascadia/TerminalApp/TerminalPage.h | 11 +- src/cascadia/TerminalApp/TerminalPage.xaml | 8 + src/cascadia/TerminalApp/pch.h | 2 + src/cascadia/TerminalControl/ControlCore.cpp | 13 + src/cascadia/TerminalControl/ControlCore.h | 5 + src/cascadia/TerminalControl/ControlCore.idl | 3 + src/cascadia/TerminalControl/EventArgs.cpp | 1 + src/cascadia/TerminalControl/EventArgs.h | 14 + src/cascadia/TerminalControl/EventArgs.idl | 6 + src/cascadia/TerminalControl/TermControl.cpp | 31 +- src/cascadia/TerminalControl/TermControl.h | 5 + src/cascadia/TerminalControl/TermControl.idl | 6 + src/cascadia/TerminalCore/Terminal.cpp | 5 + src/cascadia/TerminalCore/Terminal.hpp | 5 + src/cascadia/TerminalCore/TerminalApi.cpp | 8 + .../TerminalSettingsModel/Command.cpp | 98 ++ src/cascadia/TerminalSettingsModel/Command.h | 2 + .../TerminalSettingsModel/Command.idl | 2 + .../GlobalAppSettings.idl | 1 + .../TerminalSettingsModel/MTSMSettings.h | 1 + src/features.xml | 11 + src/host/outputStream.cpp | 4 + src/host/outputStream.hpp | 2 + src/terminal/adapter/ITermDispatch.hpp | 2 + src/terminal/adapter/ITerminalApi.hpp | 2 + src/terminal/adapter/adaptDispatch.cpp | 80 ++ src/terminal/adapter/adaptDispatch.hpp | 2 + src/terminal/adapter/termDispatch.hpp | 2 + .../adapter/ut_adapter/adapterTest.cpp | 36 + .../parser/OutputStateMachineEngine.cpp | 5 + .../parser/OutputStateMachineEngine.hpp | 1 + 40 files changed, 2003 insertions(+), 3 deletions(-) create mode 100644 src/cascadia/TerminalApp/SuggestionsControl.cpp create mode 100644 src/cascadia/TerminalApp/SuggestionsControl.h create mode 100644 src/cascadia/TerminalApp/SuggestionsControl.idl create mode 100644 src/cascadia/TerminalApp/SuggestionsControl.xaml diff --git a/.github/actions/spelling/allow/allow.txt b/.github/actions/spelling/allow/allow.txt index e757c327fe2..20999b3a54e 100644 --- a/.github/actions/spelling/allow/allow.txt +++ b/.github/actions/spelling/allow/allow.txt @@ -95,6 +95,7 @@ slnt Sos ssh stakeholders +sxn timeline timelines timestamped diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt index 722bf19093b..626777f6bdb 100644 --- a/.github/actions/spelling/expect/expect.txt +++ b/.github/actions/spelling/expect/expect.txt @@ -315,6 +315,7 @@ CPLINFO cplusplus CPPCORECHECK cppcorecheckrules +cpprest cpprestsdk cppwinrt CProc @@ -1452,6 +1453,7 @@ PPEB ppf ppguid ppidl +pplx PPROC ppropvar ppsi diff --git a/src/cascadia/LocalTests_TerminalApp/pch.h b/src/cascadia/LocalTests_TerminalApp/pch.h index 75aabc570b8..f82561888b1 100644 --- a/src/cascadia/LocalTests_TerminalApp/pch.h +++ b/src/cascadia/LocalTests_TerminalApp/pch.h @@ -74,3 +74,5 @@ Author(s): #include "../../inc/DefaultSettings.h" #include + +#include diff --git a/src/cascadia/TerminalApp/SuggestionsControl.cpp b/src/cascadia/TerminalApp/SuggestionsControl.cpp new file mode 100644 index 00000000000..83694267a6d --- /dev/null +++ b/src/cascadia/TerminalApp/SuggestionsControl.cpp @@ -0,0 +1,1103 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "ActionPaletteItem.h" +#include "CommandLinePaletteItem.h" +#include "SuggestionsControl.h" +#include + +#include "SuggestionsControl.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 +{ + SuggestionsControl::SuggestionsControl() + { + InitializeComponent(); + + _itemTemplateSelector = Resources().Lookup(winrt::box_value(L"PaletteItemTemplateSelector")).try_as(); + _listItemTemplate = Resources().Lookup(winrt::box_value(L"ListItemTemplate")).try_as(); + + _filteredActions = winrt::single_threaded_observable_vector(); + _nestedActionStack = winrt::single_threaded_vector(); + _currentNestedCommands = winrt::single_threaded_vector(); + _allCommands = winrt::single_threaded_vector(); + + _switchToMode(); + + // 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(); + UpdateLayout(); // THIS ONE IN PARTICULAR SEEMS LOAD BEARING. + // Without the UpdateLayout call, our ListView won't have a + // chance to instantiate ListViewItem's. If we don't have those, + // then our call to `SelectedItem()` below is going to return + // null. If it does that, then we won't be able to focus + // ourselves when we're opened. + + // Select the correct element in the list, depending on which + // direction we were opened in. + // + // Make sure to use _scrollToIndex, to move the scrollbar too! + if (_direction == TerminalApp::SuggestionsDirection::TopDown) + { + _scrollToIndex(0); + } + else // BottomUp + { + _scrollToIndex(_filteredActionsView().Items().Size() - 1); + } + + if (_mode == SuggestionsMode::Palette) + { + // Toss focus into the search box in palette mode + _searchBox().Visibility(Visibility::Visible); + _searchBox().Focus(FocusState::Programmatic); + } + else if (_mode == SuggestionsMode::Menu) + { + // Toss focus onto the selected item in menu mode. + // Don't just focus the _filteredActionsView, because that will always select the 0th element. + + _searchBox().Visibility(Visibility::Collapsed); + + if (const auto& dependencyObj = SelectedItem().try_as()) + { + Input::FocusManager::TryFocusAsync(dependencyObj, FocusState::Programmatic); + } + } + + TraceLoggingWrite( + g_hTerminalAppProvider, // handle to TerminalApp tracelogging provider + "SuggestionsControlOpened", + 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*/) { + // 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(); + }); + + _filteredActionsView().SelectionChanged({ this, &SuggestionsControl::_selectedCommandChanged }); + } + + TerminalApp::SuggestionsMode SuggestionsControl::Mode() const + { + return _mode; + } + void SuggestionsControl::Mode(TerminalApp::SuggestionsMode mode) + { + _mode = mode; + + if (_mode == SuggestionsMode::Palette) + { + _searchBox().Visibility(Visibility::Visible); + _searchBox().Focus(FocusState::Programmatic); + } + else if (_mode == SuggestionsMode::Menu) + { + _searchBox().Visibility(Visibility::Collapsed); + _filteredActionsView().Focus(FocusState::Programmatic); + } + } + + // 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: + // - + void SuggestionsControl::SelectNextItem(const bool moveDown) + { + auto selected = _filteredActionsView().SelectedIndex(); + const auto numItems = ::base::saturated_cast(_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: + // - + void SuggestionsControl::_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(index, 0, numItems - 1); + _filteredActionsView().SelectedIndex(clampedIndex); + _filteredActionsView().ScrollIntoView(_filteredActionsView().SelectedItem()); + } + + // Method Description: + // - Computes the number of visible commands + // Arguments: + // - + // Return Value: + // - the approximate number of items visible in the list (in other words the size of the page) + uint32_t SuggestionsControl::_getNumVisibleItems() + { + if (const auto container = _filteredActionsView().ContainerFromIndex(0)) + { + if (const auto item = container.try_as()) + { + const auto itemHeight = ::base::saturated_cast(item.ActualHeight()); + const auto listHeight = ::base::saturated_cast(_filteredActionsView().ActualHeight()); + return listHeight / itemHeight; + } + } + return 0; + } + + // Method Description: + // - Scrolls the focus one page up the list of commands. + // Arguments: + // - + // Return Value: + // - + void SuggestionsControl::ScrollPageUp() + { + auto selected = _filteredActionsView().SelectedIndex(); + auto numVisibleItems = _getNumVisibleItems(); + _scrollToIndex(selected - numVisibleItems); + } + + // Method Description: + // - Scrolls the focus one page down the list of commands. + // Arguments: + // - + // Return Value: + // - + void SuggestionsControl::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: + // - + // Return Value: + // - + void SuggestionsControl::ScrollToTop() + { + _scrollToIndex(0); + } + + // Method Description: + // - Moves the focus to the bottom item in the list of commands. + // Arguments: + // - + // Return Value: + // - + void SuggestionsControl::ScrollToBottom() + { + _scrollToIndex(_filteredActionsView().Items().Size() - 1); + } + + Windows::UI::Xaml::FrameworkElement SuggestionsControl::SelectedItem() + { + auto index = _filteredActionsView().SelectedIndex(); + const auto container = _filteredActionsView().ContainerFromIndex(index); + const auto item = container.try_as(); + return item; + } + + // Method Description: + // - Called when the command selection changes. We'll use this to preview the selected action. + // Arguments: + // - + // Return Value: + // - + void SuggestionsControl::_selectedCommandChanged(const IInspectable& /*sender*/, + const Windows::UI::Xaml::RoutedEventArgs& /*args*/) + { + const auto selectedCommand = _filteredActionsView().SelectedItem(); + const auto filteredCommand{ selectedCommand.try_as() }; + + _PropertyChangedHandlers(*this, Windows::UI::Xaml::Data::PropertyChangedEventArgs{ L"SelectedItem" }); + + // Make sure to not send the preview if we're collapsed. This can + // sometimes fire after we've been closed, which can trigger us to + // preview the action for the empty text (as we've cleared the search + // text as a part of closing). + const bool isVisible{ this->Visibility() == Visibility::Visible }; + + if (filteredCommand != nullptr && + isVisible) + { + if (const auto actionPaletteItem{ filteredCommand.Item().try_as() }) + { + PreviewAction.raise(*this, actionPaletteItem.Command()); + } + } + } + + void SuggestionsControl::_previewKeyDownHandler(const IInspectable& /*sender*/, + const Windows::UI::Xaml::Input::KeyRoutedEventArgs& e) + { + const auto key = e.OriginalKey(); + const auto coreWindow = CoreWindow::GetForCurrentThread(); + const auto ctrlDown = WI_IsFlagSet(coreWindow.GetKeyState(winrt::Windows::System::VirtualKey::Control), CoreVirtualKeyStates::Down); + + if (key == VirtualKey::Home && ctrlDown) + { + ScrollToTop(); + e.Handled(true); + } + else if (key == VirtualKey::End && ctrlDown) + { + ScrollToBottom(); + e.Handled(true); + } + else if (key == VirtualKey::Up) + { + // Move focus to the next item in the list. + SelectNextItem(false); + e.Handled(true); + } + else if (key == VirtualKey::Down) + { + // Move focus to the previous item in the list. + SelectNextItem(true); + e.Handled(true); + } + else if (key == VirtualKey::PageUp) + { + // Move focus to the first visible item in the list. + ScrollPageUp(); + e.Handled(true); + } + else if (key == VirtualKey::PageDown) + { + // Move focus to the last visible item in the list. + ScrollPageDown(); + e.Handled(true); + } + else if (key == VirtualKey::Enter || + key == VirtualKey::Tab || + key == VirtualKey::Right) + { + // If the user pressed enter, tab, or the right arrow key, then + // we'll want to dispatch the command that's selected as they + // accepted the suggestion. + + if (const auto& button = e.OriginalSource().try_as + + + + + + + + + + + + + + + + diff --git a/src/cascadia/TerminalApp/TerminalAppLib.vcxproj b/src/cascadia/TerminalApp/TerminalAppLib.vcxproj index e284d7abe2d..2b933ddfe91 100644 --- a/src/cascadia/TerminalApp/TerminalAppLib.vcxproj +++ b/src/cascadia/TerminalApp/TerminalAppLib.vcxproj @@ -68,6 +68,9 @@ Designer + + Designer + @@ -159,6 +162,9 @@ TerminalWindow.idl + + SuggestionsControl.xaml + @@ -262,6 +268,9 @@ + + SuggestionsControl.xaml + @@ -325,6 +334,10 @@ CommandPalette.xaml Code + + SuggestionsControl.xaml + Code + diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index 8140af22b3e..18515a8afa9 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -1466,6 +1466,11 @@ namespace winrt::TerminalApp::implementation { CommandPaletteElement().Visibility(Visibility::Collapsed); } + if (_suggestionsControlIs(Visibility::Visible) && + cmd.ActionAndArgs().Action() != ShortcutAction::ToggleCommandPalette) + { + SuggestionsElement().Visibility(Visibility::Collapsed); + } // Let's assume the user has bound the dead key "^" to a sendInput command that sends "b". // If the user presses the two keys "^a" it'll produce "bâ", despite us marking the key event as handled. @@ -1654,6 +1659,12 @@ namespace winrt::TerminalApp::implementation term.ShowWindowChanged({ get_weak(), &TerminalPage::_ShowWindowChangedHandler }); + // Don't even register for the event if the feature is compiled off. + if constexpr (Feature_ShellCompletions::IsEnabled()) + { + term.CompletionsChanged({ get_weak(), &TerminalPage::_ControlCompletionsChangedHandler }); + } + term.ContextMenu().Opening({ this, &TerminalPage::_ContextMenuOpened }); term.SelectionContextMenu().Opening({ this, &TerminalPage::_SelectionMenuOpened }); } @@ -1825,6 +1836,37 @@ namespace winrt::TerminalApp::implementation return p; } + SuggestionsControl TerminalPage::LoadSuggestionsUI() + { + if (const auto p = SuggestionsElement()) + { + return p; + } + + return _loadSuggestionsElementSlowPath(); + } + bool TerminalPage::_suggestionsControlIs(WUX::Visibility visibility) + { + const auto p = SuggestionsElement(); + return p && p.Visibility() == visibility; + } + + SuggestionsControl TerminalPage::_loadSuggestionsElementSlowPath() + { + const auto p = FindName(L"SuggestionsElement").as(); + + p.RegisterPropertyChangedCallback(UIElement::VisibilityProperty(), [this](auto&&, auto&&) { + if (SuggestionsElement().Visibility() == Visibility::Collapsed) + { + _FocusActiveControl(nullptr, nullptr); + } + }); + p.DispatchCommandRequested({ this, &TerminalPage::_OnDispatchCommandRequested }); + p.PreviewAction({ this, &TerminalPage::_PreviewActionHandler }); + + return p; + } + // Method Description: // - Warn the user that they are about to close all open windows, then // signal that we want to close everything. @@ -2787,7 +2829,7 @@ namespace winrt::TerminalApp::implementation // Arguments: // - sender (not used) // - args: the arguments specifying how to set the display status to ShowWindow for our window handle - void TerminalPage::_ShowWindowChangedHandler(const IInspectable& /*sender*/, const Microsoft::Terminal::Control::ShowWindowArgs args) + void TerminalPage::_ShowWindowChangedHandler(const IInspectable /*sender*/, const Microsoft::Terminal::Control::ShowWindowArgs args) { _ShowWindowChangedHandlers(*this, args); } @@ -4649,6 +4691,79 @@ namespace winrt::TerminalApp::implementation _updateThemeColors(); } + winrt::fire_and_forget TerminalPage::_ControlCompletionsChangedHandler(const IInspectable sender, + const CompletionsChangedEventArgs args) + { + // This will come in on a background (not-UI, not output) thread. + + // This won't even get hit if the velocity flag is disabled - we gate + // registering for the event based off of + // Feature_ShellCompletions::IsEnabled back in _RegisterTerminalEvents + + // User must explicitly opt-in on Preview builds + if (!_settings.GlobalSettings().EnableShellCompletionMenu()) + { + co_return; + } + + // Parse the json string into a collection of actions + try + { + auto commandsCollection = Command::ParsePowerShellMenuComplete(args.MenuJson(), + args.ReplacementLength()); + + auto weakThis{ get_weak() }; + Dispatcher().RunAsync(CoreDispatcherPriority::Normal, [weakThis, commandsCollection, sender]() { + // On the UI thread... + if (const auto& page{ weakThis.get() }) + { + // Open the Suggestions UI with the commands from the control + page->_OpenSuggestions(sender.try_as(), commandsCollection, SuggestionsMode::Menu); + } + }); + } + CATCH_LOG(); + } + + void TerminalPage::_OpenSuggestions( + const TermControl& sender, + IVector commandsCollection, + winrt::TerminalApp::SuggestionsMode mode) + { + // ON THE UI THREAD + assert(Dispatcher().HasThreadAccess()); + + if (commandsCollection == nullptr) + { + return; + } + if (commandsCollection.Size() == 0) + { + if (const auto p = SuggestionsElement()) + { + p.Visibility(Visibility::Collapsed); + } + return; + } + + const auto& control{ sender ? sender : _GetActiveControl() }; + if (!control) + { + return; + } + + const auto& sxnUi{ LoadSuggestionsUI() }; + + const auto characterSize{ control.CharacterDimensions() }; + // This is in control-relative space. We'll need to convert it to page-relative space. + const auto cursorPos{ control.CursorPositionInDips() }; + const auto controlTransform = control.TransformToVisual(this->Root()); + const auto realCursorPos{ controlTransform.TransformPoint({ cursorPos.X, cursorPos.Y }) }; // == controlTransform + cursorPos + const Windows::Foundation::Size windowDimensions{ gsl::narrow_cast(ActualWidth()), gsl::narrow_cast(ActualHeight()) }; + + sxnUi.Open(mode, commandsCollection, realCursorPos, windowDimensions, characterSize.Height); + } + void TerminalPage::_ContextMenuOpened(const IInspectable& sender, const IInspectable& /*args*/) { diff --git a/src/cascadia/TerminalApp/TerminalPage.h b/src/cascadia/TerminalApp/TerminalPage.h index 7a6866e20ec..e14b46fee72 100644 --- a/src/cascadia/TerminalApp/TerminalPage.h +++ b/src/cascadia/TerminalApp/TerminalPage.h @@ -117,6 +117,8 @@ namespace winrt::TerminalApp::implementation winrt::hstring ApplicationVersion(); CommandPalette LoadCommandPalette(); + SuggestionsControl LoadSuggestionsUI(); + winrt::fire_and_forget RequestQuit(); winrt::fire_and_forget CloseWindow(bool bypassDialog); @@ -280,6 +282,8 @@ namespace winrt::TerminalApp::implementation __declspec(noinline) CommandPalette _loadCommandPaletteSlowPath(); bool _commandPaletteIs(winrt::Windows::UI::Xaml::Visibility visibility); + __declspec(noinline) SuggestionsControl _loadSuggestionsElementSlowPath(); + bool _suggestionsControlIs(winrt::Windows::UI::Xaml::Visibility visibility); winrt::Windows::Foundation::IAsyncOperation _ShowDialogHelper(const std::wstring_view& name); @@ -481,6 +485,7 @@ namespace winrt::TerminalApp::implementation void _RunRestorePreviews(); void _PreviewColorScheme(const Microsoft::Terminal::Settings::Model::SetColorSchemeArgs& args); void _PreviewAdjustOpacity(const Microsoft::Terminal::Settings::Model::AdjustOpacityArgs& args); + winrt::Microsoft::Terminal::Settings::Model::ActionAndArgs _lastPreviewedAction{ nullptr }; std::vector> _restorePreviewFuncs{}; @@ -513,7 +518,11 @@ namespace winrt::TerminalApp::implementation void _updateAllTabCloseButtons(const winrt::TerminalApp::TabBase& focusedTab); void _updatePaneResources(const winrt::Windows::UI::Xaml::ElementTheme& requestedTheme); - void _ShowWindowChangedHandler(const IInspectable& sender, const winrt::Microsoft::Terminal::Control::ShowWindowArgs args); + winrt::fire_and_forget _ControlCompletionsChangedHandler(const winrt::Windows::Foundation::IInspectable sender, const winrt::Microsoft::Terminal::Control::CompletionsChangedEventArgs args); + void _OpenSuggestions(const Microsoft::Terminal::Control::TermControl& sender, Windows::Foundation::Collections::IVector commandsCollection, winrt::TerminalApp::SuggestionsMode mode); + + void _ShowWindowChangedHandler(const IInspectable sender, const winrt::Microsoft::Terminal::Control::ShowWindowArgs args); + winrt::fire_and_forget _windowPropertyChanged(const IInspectable& sender, const winrt::Windows::UI::Xaml::Data::PropertyChangedEventArgs& args); void _onTabDragStarting(const winrt::Microsoft::UI::Xaml::Controls::TabView& sender, const winrt::Microsoft::UI::Xaml::Controls::TabViewTabDragStartingEventArgs& e); diff --git a/src/cascadia/TerminalApp/TerminalPage.xaml b/src/cascadia/TerminalApp/TerminalPage.xaml index 00fb12f86b4..600ec505c67 100644 --- a/src/cascadia/TerminalApp/TerminalPage.xaml +++ b/src/cascadia/TerminalApp/TerminalPage.xaml @@ -175,6 +175,14 @@ PreviewKeyDown="_KeyDownHandler" Visibility="Collapsed" /> + +