Repository: microsoft/terminal (language: cpp)

Issue:
`sendInput` should be previewable
See #9818, 60f63b09cc59f1a46d142952f49dbf7599bc4cf8, #3121. I literally have this prototyped, just need to check it in. It's important for future issues...


Discussion / hints:
More cleanly pulled to its own branch: https://github.com/microsoft/terminal/pull/new/dev/migrie/f/12861-preview-input


Note: You'll also want the backspace trimming thing. That merged into TSFControl in 0f32692 but I don't know where its origin was before that. 


```c++

    void TSFInputControl::ManuallyDisplayText(const winrt::hstring& text)
    {
        _focused = !text.empty();
        Canvas().Visibility(text.empty() ? Visibility::Collapsed : Visibility::Visible);

        _inputBuffer.clear();
        // _editContext.NotifyFocusLeave();
        _activeTextStart = 0;
        _inComposition = false;

        // HACK trim off leading DEL chars.
        std::wstring_view view{ text.c_str() };
        const auto strBegin = view.find_first_not_of(L"\x7f");
        if (strBegin != std::wstring::npos)
        {
            view = view.substr(strBegin * 2);
        }

        TextBlock().Text(winrt::hstring{ view });
        TextBlock().UpdateLayout();
        TryRedrawCanvas();
    }
```
Curious how this will handle `sendInput`d escape sequences!
Protip, revert 7c3fd2aeecf57d0320ff915d7c8c68e10acb1763

I backed it out because it:
* worked wacky with wide glyphs / unicode surrogates and the backspacing code
* the backspacing code was hacky kinda always
* it didn't always scale and offset to the correct position
* didn't really handle line wrapping well
* with the pwsh ghost text, was very confusing

---

you may also need 8f0b6f8a2
Punting out of 1.19. This was wacky as-is. We may want to come back to re-try this with more runway. 

Fix patch:
diff --git a/src/cascadia/TerminalApp/ActionPreviewHandlers.cpp b/src/cascadia/TerminalApp/ActionPreviewHandlers.cpp
index 03e45ea3f91..5958e6634b7 100644
--- a/src/cascadia/TerminalApp/ActionPreviewHandlers.cpp
+++ b/src/cascadia/TerminalApp/ActionPreviewHandlers.cpp
@@ -50,6 +50,7 @@ namespace winrt::TerminalApp::implementation
         {
         case ShortcutAction::SetColorScheme:
         case ShortcutAction::AdjustOpacity:
+        case ShortcutAction::SendInput:
         {
             _RunRestorePreviews();
             break;
@@ -140,6 +141,24 @@ namespace winrt::TerminalApp::implementation
         });
     }
 
+    void TerminalPage::_PreviewSendInput(const Settings::Model::SendInputArgs& args)
+    {
+        const auto backup = _restorePreviewFuncs.empty();
+
+        _ApplyToActiveControls([&](const auto& control) {
+            const auto& str{ args.Input() };
+            control.PreviewInput(str);
+
+            if (backup)
+            {
+                _restorePreviewFuncs.emplace_back([=]() {
+                    // On dismiss:
+                    control.PreviewInput(hstring{});
+                });
+            }
+        });
+    }
+
     void TerminalPage::_PreviewAction(const Settings::Model::ActionAndArgs& args)
     {
         switch (args.Action())
@@ -150,6 +169,9 @@ namespace winrt::TerminalApp::implementation
         case ShortcutAction::AdjustOpacity:
             _PreviewAdjustOpacity(args.Args().try_as<AdjustOpacityArgs>());
             break;
+        case ShortcutAction::SendInput:
+            _PreviewSendInput(args.Args().try_as<SendInputArgs>());
+            break;
         }
 
         // GH#9818 Other ideas for actions that could be preview-able:
diff --git a/src/cascadia/TerminalApp/TerminalPage.h b/src/cascadia/TerminalApp/TerminalPage.h
index 7095dbaf290..70982c153b6 100644
--- a/src/cascadia/TerminalApp/TerminalPage.h
+++ b/src/cascadia/TerminalApp/TerminalPage.h
@@ -485,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);
+        void _PreviewSendInput(const Microsoft::Terminal::Settings::Model::SendInputArgs& args);
 
         winrt::Microsoft::Terminal::Settings::Model::ActionAndArgs _lastPreviewedAction{ nullptr };
         std::vector<std::function<void()>> _restorePreviewFuncs{};
diff --git a/src/cascadia/TerminalControl/ControlCore.cpp b/src/cascadia/TerminalControl/ControlCore.cpp
index 6b4b86a194d..ceec02ed52a 100644
--- a/src/cascadia/TerminalControl/ControlCore.cpp
+++ b/src/cascadia/TerminalControl/ControlCore.cpp
@@ -2893,4 +2893,10 @@ namespace winrt::Microsoft::Terminal::Control::implementation
         return _clickedOnMark(_contextMenuBufferPosition,
                               [](const ::MarkExtents& m) -> bool { return !m.HasOutput(); });
     }
+
+    void ControlCore::PreviewInput(std::wstring_view input)
+    {
+        _terminal->PreviewText(input);
+    }
+
 }
diff --git a/src/cascadia/TerminalControl/ControlCore.h b/src/cascadia/TerminalControl/ControlCore.h
index 0798cc6c1fe..7b43bc2d1dd 100644
--- a/src/cascadia/TerminalControl/ControlCore.h
+++ b/src/cascadia/TerminalControl/ControlCore.h
@@ -257,6 +257,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation
         bool ShouldShowSelectCommand();
         bool ShouldShowSelectOutput();
 
+        void PreviewInput(std::wstring_view input);
+
         RUNTIME_SETTING(float, Opacity, _settings->Opacity());
         RUNTIME_SETTING(float, FocusedOpacity, FocusedAppearance().Opacity());
         RUNTIME_SETTING(bool, UseAcrylic, _settings->UseAcrylic());
diff --git a/src/cascadia/TerminalControl/Resources/en-US/Resources.resw b/src/cascadia/TerminalControl/Resources/en-US/Resources.resw
index 2d0efed81fc..54c6a4643b5 100644
--- a/src/cascadia/TerminalControl/Resources/en-US/Resources.resw
+++ b/src/cascadia/TerminalControl/Resources/en-US/Resources.resw
@@ -316,4 +316,8 @@ Please either install the missing font or choose another one.</value>
     <value>invalid</value>
     <comment>This brief message is displayed when a regular expression is invalid.</comment>
   </data>
+  <data name="PreviewTextAnnouncement" xml:space="preserve">
+    <value>Suggested input: {0}</value>
+    <comment>{Locked="{0}"} {0} will be replaced with a string of input that is suggested for the user to input</comment>
+  </data>
 </root>
diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp
index cc97cf6aa57..bdc6f42061f 100644
--- a/src/cascadia/TerminalControl/TermControl.cpp
+++ b/src/cascadia/TerminalControl/TermControl.cpp
@@ -731,6 +731,9 @@ namespace winrt::Microsoft::Terminal::Control::implementation
     // - <none>
     void TermControl::SendInput(const winrt::hstring& wstr)
     {
+        // Dismiss any previewed input.
+        PreviewInput(hstring{});
+
         // only broadcast if there's an actual listener. Saves the overhead of some object creation.
         if (StringSent)
         {
@@ -3664,6 +3667,23 @@ namespace winrt::Microsoft::Terminal::Control::implementation
         return _core.OwningHwnd();
     }
 
+    void TermControl::PreviewInput(const winrt::hstring& text)
+    {
+        get_self<ControlCore>(_core)->PreviewInput(text);
+
+        if (!text.empty())
+        {
+            if (auto automationPeer{ FrameworkElementAutomationPeer::FromElement(*this) })
+            {
+                automationPeer.RaiseNotificationEvent(
+                    AutomationNotificationKind::ItemAdded,
+                    AutomationNotificationProcessing::All,
+                    winrt::hstring{ fmt::format(std::wstring_view{ RS_(L"PreviewTextAnnouncement") }, text) },
+                    L"PreviewTextAnnouncement" /* unique name for this group of notifications */);
+            }
+        }
+    }
+
     void TermControl::AddMark(const Control::ScrollMark& mark)
     {
         _core.AddMark(mark);
diff --git a/src/cascadia/TerminalControl/TermControl.h b/src/cascadia/TerminalControl/TermControl.h
index 75d49c3b317..e9494ceffa1 100644
--- a/src/cascadia/TerminalControl/TermControl.h
+++ b/src/cascadia/TerminalControl/TermControl.h
@@ -73,6 +73,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
         Windows::Foundation::Size CharacterDimensions() const;
         Windows::Foundation::Size MinimumSize();
         float SnapDimensionToGrid(const bool widthOrHeight, const float dimension);
+        void PreviewInput(const winrt::hstring& text);
 
         Windows::Foundation::Point CursorPositionInDips();
 
diff --git a/src/cascadia/TerminalControl/TermControl.idl b/src/cascadia/TerminalControl/TermControl.idl
index ab17ab64d9f..5b444b5d9a3 100644
--- a/src/cascadia/TerminalControl/TermControl.idl
+++ b/src/cascadia/TerminalControl/TermControl.idl
@@ -127,6 +127,7 @@ namespace Microsoft.Terminal.Control
         CommandHistoryContext CommandHistory();
 
         void AdjustOpacity(Single Opacity, Boolean relative);
+        void PreviewInput(String text);
 
         // You'd think this should just be "Opacity", but UIElement already
         // defines an "Opacity", which we're actually not setting at all. We're
diff --git a/src/cascadia/TerminalCore/Terminal.cpp b/src/cascadia/TerminalCore/Terminal.cpp
index 9f72cccb5be..05f9a681d84 100644
--- a/src/cascadia/TerminalCore/Terminal.cpp
+++ b/src/cascadia/TerminalCore/Terminal.cpp
@@ -1584,6 +1584,57 @@ til::point Terminal::GetViewportRelativeCursorPosition() const noexcept
     return absoluteCursorPosition - viewport.Origin();
 }
 
+void Terminal::PreviewText(std::wstring_view input)
+{
+    // Our suggestion text is default-on-default, in italics.
+    static constexpr TextAttribute previewAttrs{ CharacterAttributes::Italics, TextColor{}, TextColor{}, 0u, TextColor{} };
+
+    auto lock = LockForWriting();
+    if (input.empty())
+    {
+        snippetPreview.text = L"";
+        snippetPreview.cursorPos = 0;
+        snippetPreview.attributes.clear();
+        _activeBuffer().NotifyPaintFrame();
+        return;
+    }
+
+    // When we're previewing suggestions, they might be preceded with DEL
+    // characters to backspace off the old command.
+    //
+    // But also, in the case of something like pwsh, there might be MORE "ghost"
+    // text in the buffer _after_ the commandline.
+    //
+    // We need to trim off the leading DELs, then pad out the rest of the line
+    // to cover any other ghost text.
+    // Where do the DELs end?
+    const auto strBegin = input.find_first_not_of(L"\x7f");
+    if (strBegin != std::wstring::npos)
+    {
+        // Trim them off.
+        input = input.substr(strBegin);
+    }
+    // How many spaces do we need, so that the preview exactly covers the entire
+    // prompt, all the way to the end of the viewport?
+    const auto bufferWidth = _GetMutableViewport().Width();
+    const auto cursorX = _activeBuffer().GetCursor().GetPosition().x;
+    const auto expectedLenTillEnd = strBegin + (static_cast<size_t>(bufferWidth) - static_cast<size_t>(cursorX));
+    std::wstring preview{ input };
+    const auto originalSize{ preview.size() };
+    if (expectedLenTillEnd > originalSize)
+    {
+        // pad it out
+        preview.insert(originalSize, expectedLenTillEnd - originalSize, L' ');
+    }
+    snippetPreview.text = til::visualize_nonspace_control_codes(preview);
+    // Build our composition data
+    const auto len = snippetPreview.text.size();
+    snippetPreview.attributes.clear();
+    snippetPreview.attributes.emplace_back(len, previewAttrs);
+    snippetPreview.cursorPos = len;
+    _activeBuffer().NotifyPaintFrame();
+}
+
 // These functions are used by TerminalInput, which must build in conhost
 // against OneCore compatible signatures. See the definitions in
 // VtApiRedirection.hpp (which we cannot include cross-project.)
diff --git a/src/cascadia/TerminalCore/Terminal.hpp b/src/cascadia/TerminalCore/Terminal.hpp
index bf1120f421a..a52a5722da5 100644
--- a/src/cascadia/TerminalCore/Terminal.hpp
+++ b/src/cascadia/TerminalCore/Terminal.hpp
@@ -246,6 +246,7 @@ class Microsoft::Terminal::Core::Terminal final :
     const size_t GetTaskbarProgress() const noexcept;
 
     void ColorSelection(const TextAttribute& attr, winrt::Microsoft::Terminal::Core::MatchMode matchMode);
+    void PreviewText(std::wstring_view input);
 
 #pragma region TextSelection
     // These methods are defined in TerminalSelection.cpp
diff --git a/src/renderer/base/renderer.cpp b/src/renderer/base/renderer.cpp
index 261b7f88cbf..7d7a4b356eb 100644
--- a/src/renderer/base/renderer.cpp
+++ b/src/renderer/base/renderer.cpp
@@ -722,6 +722,7 @@ void Renderer::_PaintBufferOutput(_In_ IRenderEngine* const pEngine)
     // relative to the entire buffer.
     const auto view = _pData->GetViewport();
     const auto compositionRow = _compositionCache ? _compositionCache->absoluteOrigin.y : -1;
+    const auto& activeComposition = _pData->GetActiveComposition();
 
     // This is effectively the number of cells on the visible screen that need to be redrawn.
     // The origin is always 0, 0 because it represents the screen itself, not the underlying buffer.
@@ -753,7 +754,6 @@ void Renderer::_PaintBufferOutput(_In_ IRenderEngine* const pEngine)
 
         // Retrieve the text buffer so we can read information out of it.
         auto& buffer = _pData->GetTextBuffer();
-
         // Now walk through each row of text that we need to redraw.
         for (auto row = redraw.Top(); row < redraw.BottomExclusive(); row++)
         {
@@ -772,14 +772,14 @@ void Renderer::_PaintBufferOutput(_In_ IRenderEngine* const pEngine)
                 scratch.CopyFrom(r);
                 rowBackup = &scratch;
 
-                std::wstring_view text{ _pData->activeComposition.text };
+                std::wstring_view text{ activeComposition.text };
                 RowWriteState state{
                     .columnLimit = r.GetReadableColumnCount(),
                     .columnEnd = _compositionCache->absoluteOrigin.x,
                 };
 
                 size_t off = 0;
-                for (const auto& range : _pData->activeComposition.attributes)
+                for (const auto& range : activeComposition.attributes)
                 {
                     const auto len = range.len;
                     auto attr = range.attr;
@@ -1225,7 +1225,7 @@ void Renderer::_invalidateOldComposition() const
 // so that _PaintBufferOutput() actually gets a chance to draw it.
 void Renderer::_prepareNewComposition()
 {
-    if (_pData->activeComposition.text.empty())
+    if (_pData->GetActiveComposition().text.empty())
     {
         return;
     }
@@ -1245,17 +1245,18 @@ void Renderer::_prepareNewComposition()
 
         auto& buffer = _pData->GetTextBuffer();
         auto& scratch = buffer.GetScratchpadRow();
+        const auto& activeComposition = _pData->GetActiveComposition();
 
-        std::wstring_view text{ _pData->activeComposition.text };
+        std::wstring_view text{ activeComposition.text };
         RowWriteState state{
             .columnLimit = buffer.GetRowByOffset(line.top).GetReadableColumnCount(),
         };
 
-        state.text = text.substr(0, _pData->activeComposition.cursorPos);
+        state.text = text.substr(0, activeComposition.cursorPos);
         scratch.ReplaceText(state);
         const auto cursorOffset = state.columnEnd;
 
-        state.text = text.substr(_pData->activeComposition.cursorPos);
+        state.text = text.substr(activeComposition.cursorPos);
         state.columnBegin = state.columnEnd;
         scratch.ReplaceText(state);
 
diff --git a/src/renderer/inc/IRenderData.hpp b/src/renderer/inc/IRenderData.hpp
index e55bdcbe87c..cb291f8f3c2 100644
--- a/src/renderer/inc/IRenderData.hpp
+++ b/src/renderer/inc/IRenderData.hpp
@@ -79,6 +79,12 @@ namespace Microsoft::Console::Render
         // Ideally this would not be stored on an interface, however ideally IRenderData should not be an interface in the first place.
         // This is because we should have only 1 way how to represent render data across the codebase anyway, and it should
         // be by-value in a struct so that we can snapshot it and release the terminal lock as quickly as possible.
-        Composition activeComposition;
+        const Composition& GetActiveComposition() const noexcept
+        {
+            return !snippetPreview.text.empty() ? snippetPreview : tsfPreview;
+        }
+
+        Composition tsfPreview;
+        Composition snippetPreview;
     };
 }
diff --git a/src/tsf/Implementation.cpp b/src/tsf/Implementation.cpp
index aa7ace81fca..856d124ec60 100644
--- a/src/tsf/Implementation.cpp
+++ b/src/tsf/Implementation.cpp
@@ -144,9 +144,9 @@ void Implementation::Unfocus(IDataProvider* provider)
             renderData->UnlockConsole();
         });
 
-        if (!renderData->activeComposition.text.empty())
+        if (!renderData->tsfPreview.text.empty())
         {
-            auto& comp = renderData->activeComposition;
+            auto& comp = renderData->tsfPreview;
             comp.text.clear();
             comp.attributes.clear();
             renderer->NotifyPaintFrame();
@@ -570,7 +570,7 @@ void Implementation::_doCompositionUpdate(TfEditCookie ec)
                 renderData->UnlockConsole();
             });
 
-            auto& comp = renderData->activeComposition;
+            auto& comp = renderData->tsfPreview;
             comp.text = std::move(activeComposition);
             comp.attributes = std::move(activeCompositionRanges);
             // The code block above that calculates the `cursorPos` will clamp it to a positive number.


Test patch: