Repository: microsoft/terminal (language: cpp)

Issue:
[1.19] Pasting a file from explorer into the Terminal no longer pastes the path (regression from 1.18)
### Windows Terminal version

1.19.240129002

### Windows build number

10.0.19045.3930

### Other Software

no

### Steps to reproduce

copy a file from the File Explorer (a file object, not text), and then paste it into Windows Terminal

### Expected Behavior

In version 1.18.240129001 of Windows Terminal, it can display the path of the pasted file.



### Actual Behavior

In 1.19, and 1.20 of Windows Terminal, nothing is shown.
(copy/paste normal text is OK)


Discussion / hints:
This feature of paste is quite convenient. Could you please bring it back?
Huh, I never knew that worked! We'll look into it 😄 
We could also make it possible to paste multiple file paths in the future by unifying the code that handles drag & drop and the code that does Ctrl+V. Iterating through an `HDROP` with `DragQueryFileW` is quite simple thankfully. We should consider doing that in the future if we need to touch this code again!

Fix patch:
diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp
index 53cd10d0c4e..b350fed479d 100644
--- a/src/cascadia/TerminalApp/TerminalPage.cpp
+++ b/src/cascadia/TerminalApp/TerminalPage.cpp
@@ -2628,6 +2628,75 @@ namespace winrt::TerminalApp::implementation
         CATCH_LOG();
     }
 
+    static wil::unique_close_clipboard_call _openClipboard(HWND hwnd)
+    {
+        bool success = false;
+
+        // OpenClipboard may fail to acquire the internal lock --> retry.
+        for (DWORD sleep = 10;; sleep *= 2)
+        {
+            if (OpenClipboard(hwnd))
+            {
+                success = true;
+                break;
+            }
+            // 10 iterations
+            if (sleep > 10000)
+            {
+                break;
+            }
+            Sleep(sleep);
+        }
+
+        return wil::unique_close_clipboard_call{ success };
+    }
+
+    static winrt::hstring _extractClipboard()
+    {
+        // This handles most cases of pasting text as the OS converts most formats to CF_UNICODETEXT automatically.
+        if (const auto handle = GetClipboardData(CF_UNICODETEXT))
+        {
+            const wil::unique_hglobal_locked lock{ handle };
+            const auto str = static_cast<const wchar_t*>(lock.get());
+            if (!str)
+            {
+                return {};
+            }
+
+            const auto maxLen = GlobalSize(handle) / sizeof(wchar_t);
+            const auto len = wcsnlen(str, maxLen);
+            return winrt::hstring{ str, gsl::narrow_cast<uint32_t>(len) };
+        }
+
+        // We get CF_HDROP when a user copied a file with Ctrl+C in Explorer and pastes that into the terminal (among others).
+        if (const auto handle = GetClipboardData(CF_HDROP))
+        {
+            const wil::unique_hglobal_locked lock{ handle };
+            const auto drop = static_cast<HDROP>(lock.get());
+            if (!drop)
+            {
+                return {};
+            }
+
+            const auto cap = DragQueryFileW(drop, 0, nullptr, 0);
+            if (cap == 0)
+            {
+                return {};
+            }
+
+            auto buffer = winrt::impl::hstring_builder{ cap };
+            const auto len = DragQueryFileW(drop, 0, buffer.data(), cap + 1);
+            if (len == 0)
+            {
+                return {};
+            }
+
+            return buffer.to_hstring();
+        }
+
+        return {};
+    }
+
     // Function Description:
     // - This function is called when the `TermControl` requests that we send
     //   it the clipboard's content.
@@ -2647,53 +2716,14 @@ namespace winrt::TerminalApp::implementation
         const auto weakThis = get_weak();
         const auto dispatcher = Dispatcher();
         const auto globalSettings = _settings.GlobalSettings();
-        winrt::hstring text;
 
         // GetClipboardData might block for up to 30s for delay-rendered contents.
         co_await winrt::resume_background();
 
+        winrt::hstring text;
+        if (const auto clipboard = _openClipboard(nullptr))
         {
-            // According to various reports on the internet, OpenClipboard might
-            // fail to acquire the internal lock, for instance due to rdpclip.exe.
-            for (int attempts = 1;;)
-            {
-                if (OpenClipboard(nullptr))
-                {
-                    break;
-                }
-
-                if (attempts > 5)
-                {
-                    co_return;
-                }
-
-                attempts++;
-                Sleep(10 * attempts);
-            }
-
-            const auto clipboardCleanup = wil::scope_exit([]() {
-                CloseClipboard();
-            });
-
-            const auto data = GetClipboardData(CF_UNICODETEXT);
-            if (!data)
-            {
-                co_return;
-            }
-
-            const auto str = static_cast<const wchar_t*>(GlobalLock(data));
-            if (!str)
-            {
-                co_return;
-            }
-
-            const auto dataCleanup = wil::scope_exit([&]() {
-                GlobalUnlock(data);
-            });
-
-            const auto maxLength = GlobalSize(data) / sizeof(wchar_t);
-            const auto length = wcsnlen(str, maxLength);
-            text = winrt::hstring{ str, gsl::narrow_cast<uint32_t>(length) };
+            text = _extractClipboard();
         }
 
         if (globalSettings.TrimPaste())
diff --git a/src/interactivity/win32/Clipboard.cpp b/src/interactivity/win32/Clipboard.cpp
index 90915616753..f91a1b9e7ac 100644
--- a/src/interactivity/win32/Clipboard.cpp
+++ b/src/interactivity/win32/Clipboard.cpp
@@ -59,29 +59,73 @@ void Clipboard::Paste()
         return;
     }
 
-    const auto handle = GetClipboardData(CF_UNICODETEXT);
-    if (!handle)
+    // This handles most cases of pasting text as the OS converts most formats to CF_UNICODETEXT automatically.
+    if (const auto handle = GetClipboardData(CF_UNICODETEXT))
     {
-        return;
+        const wil::unique_hglobal_locked lock{ handle };
+        const auto str = static_cast<const wchar_t*>(lock.get());
+        if (!str)
+        {
+            return;
+        }
+
+        // As per: https://learn.microsoft.com/en-us/windows/win32/dataxchg/standard-clipboard-formats
+        //   CF_UNICODETEXT: [...] A null character signals the end of the data.
+        // --> Use wcsnlen() to determine the actual length.
+        // NOTE: Some applications don't add a trailing null character. This includes past conhost versions.
+        const auto maxLen = GlobalSize(handle) / sizeof(wchar_t);
+        StringPaste(str, wcsnlen(str, maxLen));
     }
 
-    // Clear any selection or scrolling that may be active.
-    Selection::Instance().ClearSelection();
-    Scrolling::s_ClearScroll();
+    // We get CF_HDROP when a user copied a file with Ctrl+C in Explorer and pastes that into the terminal (among others).
+    if (const auto handle = GetClipboardData(CF_HDROP))
+    {
+        const wil::unique_hglobal_locked lock{ handle };
+        const auto drop = static_cast<HDROP>(lock.get());
+        if (!drop)
+        {
+            return;
+        }
 
-    const wil::unique_hglobal_locked lock{ handle };
-    const auto str = static_cast<const wchar_t*>(lock.get());
-    if (!str)
+        PasteDrop(drop);
+    }
+}
+
+void Clipboard::PasteDrop(HDROP drop)
+{
+    // NOTE: When asking DragQueryFileW for the required capacity it returns a length without trailing \0,
+    // but then expects a capacity that includes it. If you don't make space for a trailing \0
+    // then it will silently (!) cut off the end of the string. A somewhat disappointing API design.
+    const auto expectedLength = DragQueryFileW(drop, 0, nullptr, 0);
+    if (expectedLength == 0)
     {
         return;
     }
 
-    // As per: https://learn.microsoft.com/en-us/windows/win32/dataxchg/standard-clipboard-formats
-    //   CF_UNICODETEXT: [...] A null character signals the end of the data.
-    // --> Use wcsnlen() to determine the actual length.
-    // NOTE: Some applications don't add a trailing null character. This includes past conhost versions.
-    const auto maxLen = GlobalSize(handle) / sizeof(WCHAR);
-    StringPaste(str, wcsnlen(str, maxLen));
+    // If the path contains spaces, we'll wrap it in quotes and so this allocates +2 characters ahead of time.
+    // We'll first make DragQueryFileW copy its contents in the middle and then check if that contains spaces.
+    // If it does, only then we'll add the quotes at the start and end.
+    // This is preferable over calling StringPaste 3x (an alternative, simpler approach),
+    // because the pasted content should be treated as a single atomic unit by the InputBuffer.
+    const auto buffer = std::make_unique_for_overwrite<wchar_t[]>(expectedLength + 2);
+    auto str = buffer.get() + 1;
+    size_t len = expectedLength;
+
+    const auto actualLength = DragQueryFileW(drop, 0, str, expectedLength + 1);
+    if (actualLength != expectedLength)
+    {
+        return;
+    }
+
+    if (wmemchr(str, L' ', len))
+    {
+        str = buffer.get();
+        len += 2;
+        til::at(str, 0) = L'"';
+        til::at(str, len - 1) = L'"';
+    }
+
+    StringPaste(str, len);
 }
 
 Clipboard& Clipboard::Instance()
@@ -109,6 +153,10 @@ void Clipboard::StringPaste(_In_reads_(cchData) const wchar_t* const pData,
 
     try
     {
+        // Clear any selection or scrolling that may be active.
+        Selection::Instance().ClearSelection();
+        Scrolling::s_ClearScroll();
+
         const auto vtInputMode = gci.pInputBuffer->IsInVirtualTerminalInputMode();
         const auto bracketedPasteMode = gci.GetBracketedPasteMode();
         auto inEvents = TextToKeyEvents(pData, cchData, vtInputMode && bracketedPasteMode);
diff --git a/src/interactivity/win32/clipboard.hpp b/src/interactivity/win32/clipboard.hpp
index 169b6531039..09990ab5146 100644
--- a/src/interactivity/win32/clipboard.hpp
+++ b/src/interactivity/win32/clipboard.hpp
@@ -30,15 +30,15 @@ namespace Microsoft::Console::Interactivity::Win32
         static Clipboard& Instance();
 
         void Copy(_In_ const bool fAlsoCopyFormatting = false);
-        void StringPaste(_In_reads_(cchData) PCWCHAR pwchData,
-                         const size_t cchData);
         void Paste();
+        void PasteDrop(HDROP drop);
 
     private:
         static wil::unique_close_clipboard_call _openClipboard(HWND hwnd);
         static void _copyToClipboard(UINT format, const void* src, size_t bytes);
         static void _copyToClipboardRegisteredFormat(const wchar_t* format, const void* src, size_t bytes);
 
+        void StringPaste(_In_reads_(cchData) PCWCHAR pwchData, const size_t cchData);
         InputEventQueue TextToKeyEvents(_In_reads_(cchData) const wchar_t* const pData,
                                         const size_t cchData,
                                         const bool bracketedPaste = false);
diff --git a/src/interactivity/win32/windowproc.cpp b/src/interactivity/win32/windowproc.cpp
index 37def3100da..3e2225e182f 100644
--- a/src/interactivity/win32/windowproc.cpp
+++ b/src/interactivity/win32/windowproc.cpp
@@ -857,24 +857,7 @@ void Window::_HandleWindowPosChanged(const LPARAM lParam)
 // - <none>
 void Window::_HandleDrop(const WPARAM wParam) const
 {
-    WCHAR szPath[MAX_PATH];
-    BOOL fAddQuotes;
-
-    if (DragQueryFile((HDROP)wParam, 0, szPath, ARRAYSIZE(szPath)) != 0)
-    {
-        fAddQuotes = (wcschr(szPath, L' ') != nullptr);
-        if (fAddQuotes)
-        {
-            Clipboard::Instance().StringPaste(L"\"", 1);
-        }
-
-        Clipboard::Instance().StringPaste(szPath, wcslen(szPath));
-
-        if (fAddQuotes)
-        {
-            Clipboard::Instance().StringPaste(L"\"", 1);
-        }
-    }
+    Clipboard::Instance().PasteDrop((HDROP)wParam);
 }
 
 [[nodiscard]] LRESULT Window::_HandleGetObject(const HWND hwnd, const WPARAM wParam, const LPARAM lParam)


Test patch: