diff --git a/src/cascadia/TerminalControl/ControlInteractivity.cpp b/src/cascadia/TerminalControl/ControlInteractivity.cpp index 7eaf4275535..8521d6e835c 100644 --- a/src/cascadia/TerminalControl/ControlInteractivity.cpp +++ b/src/cascadia/TerminalControl/ControlInteractivity.cpp @@ -255,10 +255,21 @@ namespace winrt::Microsoft::Terminal::Control::implementation } else if (WI_IsFlagSet(buttonState, MouseButtonState::IsRightButtonDown)) { - // Try to copy the text and clear the selection - const auto successfulCopy = CopySelectionToClipboard(shiftEnabled, nullptr); + const auto copyOnSelect{ _core->CopyOnSelect() }; + bool successfulCopy = false; + + // Don't try to copy if we're in copyOnSelect mode and have already + // copied this selection. GH#14464 demonstrates a scenario where the + // buffer contents might have changed since the selection was made, + // and copying here would cause weirdness. + if (_selectionNeedsToBeCopied || !copyOnSelect) + { + // Try to copy the text and clear the selection + successfulCopy = CopySelectionToClipboard(shiftEnabled, nullptr); + } _core->ClearSelection(); - if (_core->CopyOnSelect() || !successfulCopy) + + if (copyOnSelect || !successfulCopy) { // CopyOnSelect: right click always pastes! // Otherwise: no selection --> paste diff --git a/src/cascadia/UnitTests_Control/ControlInteractivityTests.cpp b/src/cascadia/UnitTests_Control/ControlInteractivityTests.cpp index 5a1225d8335..f9b018f4d62 100644 --- a/src/cascadia/UnitTests_Control/ControlInteractivityTests.cpp +++ b/src/cascadia/UnitTests_Control/ControlInteractivityTests.cpp @@ -43,6 +43,9 @@ namespace ControlUnitTests TEST_METHOD(GetMouseEventsInTest); TEST_METHOD(AltBufferClampMouse); + TEST_METHOD(CopyOnSelectSimple); + TEST_METHOD(CopyOnSelectAltBuffer); + TEST_CLASS_SETUP(ClassSetup) { winrt::init_apartment(winrt::apartment_type::single_threaded); @@ -966,4 +969,183 @@ namespace ControlUnitTests cursorPosition1.to_core_point()); VERIFY_ARE_EQUAL(0u, expectedOutput.size(), L"Validate we drained all the expected output"); } + + void ControlInteractivityTests::CopyOnSelectSimple() + { + auto [settings, conn] = _createSettingsAndConnection(); + settings->CopyOnSelect(true); + auto [core, interactivity] = _createCoreAndInteractivity(*settings, *conn); + _standardInit(core, interactivity); + auto& term{ *core->_terminal }; + + // Output some text + for (auto i = 0; i < core->ViewHeight() / 2; ++i) + { + conn->WriteInput(winrt::hstring{ fmt::format(L"line {}\r\n", i) }); + } + + // A callback for checking Copy events. expectedCopyContents=nullopt + // indicates that we're not expecting any copy events. + std::optional expectedCopyContents{ std::nullopt }; + core->CopyToClipboard([&](auto&&, auto& args) { + VERIFY_IS_TRUE(expectedCopyContents.has_value()); + const std::wstring expected{ expectedCopyContents->c_str() }; + const std::wstring actual{ args.Text().c_str() }; + VERIFY_ARE_EQUAL(expected, actual); + }); + bool expectedPaste = false; + interactivity->PasteFromClipboard([&](auto&&, auto&&) { + VERIFY_IS_TRUE(expectedPaste); + }); + + const auto originalViewport{ term.GetViewport() }; + VERIFY_ARE_EQUAL(originalViewport.Width(), 30); + + // For this test, don't use any modifiers + const auto modifiers = ControlKeyStates(); + const auto leftMouseDown{ Control::MouseButtonState::IsLeftButtonDown }; + const auto rightMouseDown{ Control::MouseButtonState::IsRightButtonDown }; + const Control::MouseButtonState noMouseDown{}; + const til::size fontSize{ 9, 21 }; + const til::point terminalPosition0{ 0, 5 }; + const til::point terminalPosition1{ 5, 5 }; + const auto cursorPosition0{ terminalPosition0 * fontSize }; + const auto cursorPosition1{ terminalPosition1 * fontSize }; + + Log::Comment(L" --- Click on the terminal at 0,5 ---"); + + interactivity->PointerPressed(leftMouseDown, + WM_LBUTTONDOWN, //pointerUpdateKind + 0, // timestamp + modifiers, + cursorPosition0.to_core_point()); + VERIFY_IS_FALSE(core->HasSelection()); + + Log::Comment(L" --- Drag to 5,5 ---"); + + interactivity->PointerMoved(leftMouseDown, + WM_LBUTTONDOWN, //pointerUpdateKind + modifiers, + true, // focused, + cursorPosition1.to_core_point(), + true); // pointerPressedInBounds + VERIFY_IS_TRUE(core->HasSelection()); + // expectedCopyContents being nullopt here will ensure that we don't send the copy event till the pointer released + + Log::Comment(L" --- Release the mouse --- "); + expectedCopyContents = L"line 5"; + interactivity->PointerReleased(noMouseDown, + WM_LBUTTONUP, //pointerUpdateKind + modifiers, + cursorPosition1.to_core_point()); + VERIFY_IS_TRUE(core->HasSelection()); + + Log::Comment(L" --- Right-click to paste --- "); + // Note from GH#14464: we don't want to copy _again_ at this point. The + // copy occured when the selection was made, we shouldn't stealth-update + // the clipboard again. + expectedCopyContents = std::nullopt; + expectedPaste = true; + interactivity->PointerPressed(rightMouseDown, + WM_RBUTTONDOWN, //pointerUpdateKind + 0, // timestamp + modifiers, + cursorPosition1.to_core_point()); + VERIFY_IS_FALSE(core->HasSelection()); + } + + void ControlInteractivityTests::CopyOnSelectAltBuffer() + { + // This test was inspired by GH#14464. Ultimately, it's similar to the + // CopyOnSelectSimple, just with an alt buffer, and outputting text + // after the selection was made. + auto [settings, conn] = _createSettingsAndConnection(); + settings->CopyOnSelect(true); + auto [core, interactivity] = _createCoreAndInteractivity(*settings, *conn); + _standardInit(core, interactivity); + auto& term{ *core->_terminal }; + + Log::Comment(L" --- Switch to alt buffer ---"); + term.Write(L"\x1b[?1049h"); + auto returnToMain = wil::scope_exit([&]() { term.Write(L"\x1b[?1049h"); }); + + // Output some text + auto i = 0; + for (i = 0; i < core->ViewHeight() - 1; ++i) + { + conn->WriteInput(winrt::hstring{ fmt::format(L"line {}\r\n", i) }); + } + + // A callback for checking Copy events. expectedCopyContents=nullopt + // indicates that we're not expecting any copy events. + std::optional expectedCopyContents{ std::nullopt }; + core->CopyToClipboard([&](auto&&, auto& args) { + VERIFY_IS_TRUE(expectedCopyContents.has_value()); + const std::wstring expected{ expectedCopyContents->c_str() }; + const std::wstring actual{ args.Text().c_str() }; + VERIFY_ARE_EQUAL(expected, actual); + }); + bool expectedPaste = false; + interactivity->PasteFromClipboard([&](auto&&, auto&&) { + VERIFY_IS_TRUE(expectedPaste); + }); + + const auto originalViewport{ term.GetViewport() }; + VERIFY_ARE_EQUAL(originalViewport.Width(), 30); + + // For this test, don't use any modifiers + const auto modifiers = ControlKeyStates(); + const auto leftMouseDown{ Control::MouseButtonState::IsLeftButtonDown }; + const auto rightMouseDown{ Control::MouseButtonState::IsRightButtonDown }; + const Control::MouseButtonState noMouseDown{}; + const til::size fontSize{ 9, 21 }; + const til::point terminalPosition0{ 0, 5 }; + const til::point terminalPosition1{ 5, 5 }; + const auto cursorPosition0{ terminalPosition0 * fontSize }; + const auto cursorPosition1{ terminalPosition1 * fontSize }; + + Log::Comment(L" --- Click on the terminal at 0,5 ---"); + + interactivity->PointerPressed(leftMouseDown, + WM_LBUTTONDOWN, //pointerUpdateKind + 0, // timestamp + modifiers, + cursorPosition0.to_core_point()); + VERIFY_IS_FALSE(core->HasSelection()); + + Log::Comment(L" --- Drag to 5,5 ---"); + + interactivity->PointerMoved(leftMouseDown, + WM_LBUTTONDOWN, //pointerUpdateKind + modifiers, + true, // focused, + cursorPosition1.to_core_point(), + true); // pointerPressedInBounds + VERIFY_IS_TRUE(core->HasSelection()); + // expectedCopyContents being nullopt here will ensure that we don't send the copy event till the pointer released + + Log::Comment(L" --- Release the mouse --- "); + expectedCopyContents = L"line 5"; + interactivity->PointerReleased(noMouseDown, + WM_LBUTTONUP, //pointerUpdateKind + modifiers, + cursorPosition1.to_core_point()); + VERIFY_IS_TRUE(core->HasSelection()); + + // Output some text + for (; i < 5; ++i) + { + conn->WriteInput(winrt::hstring{ fmt::format(L"line {}\r\n", i) }); + } + + Log::Comment(L" --- Right-click to paste --- "); + expectedCopyContents = std::nullopt; + expectedPaste = true; + interactivity->PointerPressed(rightMouseDown, + WM_RBUTTONDOWN, //pointerUpdateKind + 0, // timestamp + modifiers, + cursorPosition1.to_core_point()); + VERIFY_IS_FALSE(core->HasSelection()); + } }