Repository: microsoft/terminal (language: cpp)

Issue:
Unable to select text correctly while new lines are printed and history buffer is full
### Windows Terminal version

1.15.2875.0

### Windows build number

10.0.22621.819

### Other Software

_No response_

### Steps to reproduce

See #10319 for the previous report, it was supposedly fixed in v1.11, but I still see this behaviour.

1. Set `"historySize": 100,` for the profile.
2. Run a script that echoes new lines
3. Try to select text

Example script to test the issue:
```powershell
Do {
	echo $(Get-Random)
	sleep 0.1
}
while ($true)
```


https://user-images.githubusercontent.com/613331/204636943-b8f8f4b5-e6da-4fdb-88cd-b59bc8e8ad17.mp4



### Expected Behavior

Correctly select text. It works when the history buffer is full.



### Actual Behavior

The selection scrolls away from you, selecting different text.


Discussion / hints:
It's like, the viewport is staying at the same place relative to the first-in-order of the selection anchors, but the second anchor is moving... down? Like, it's in the same buffer-relative position.

Almost as if the first one is being adjusted as lines scroll off, but not the second. 

Fix patch:
diff --git a/src/cascadia/TerminalCore/Terminal.cpp b/src/cascadia/TerminalCore/Terminal.cpp
index a8331a881ba..58443c863c5 100644
--- a/src/cascadia/TerminalCore/Terminal.cpp
+++ b/src/cascadia/TerminalCore/Terminal.cpp
@@ -1177,6 +1177,8 @@ void Terminal::_AdjustCursorPosition(const til::point proposedPosition)
             // Update our selection too, so it doesn't move as the buffer is cycled
             if (_selection)
             {
+                // Stash this, so we can make sure to update the pivot to match later
+                const auto pivotWasStart = _selection->start == _selection->pivot;
                 // If the start of the selection is above 0, we can reduce both the start and end by 1
                 if (_selection->start.y > 0)
                 {
@@ -1197,6 +1199,15 @@ void Terminal::_AdjustCursorPosition(const til::point proposedPosition)
                         _selection.reset();
                     }
                 }
+
+                // If we still have a selection, make sure to sync the pivot
+                // with whichever value is the right one.
+                //
+                // Failure to do this might lead to GH #14462
+                if (_selection.has_value())
+                {
+                    _selection->pivot = pivotWasStart ? _selection->start : _selection->end;
+                }
             }
         }
 
diff --git a/src/cascadia/UnitTests_Control/ControlInteractivityTests.cpp b/src/cascadia/UnitTests_Control/ControlInteractivityTests.cpp
index 5a1225d8335..94dfb227a52 100644
--- a/src/cascadia/UnitTests_Control/ControlInteractivityTests.cpp
+++ b/src/cascadia/UnitTests_Control/ControlInteractivityTests.cpp
@@ -757,12 +757,14 @@ namespace ControlUnitTests
         _standardInit(core, interactivity);
 
         Log::Comment(L"Fill up the history buffer");
+        const auto scrollbackLength = settings->HistorySize();
         // Output lines equal to history size + viewport height to make sure we're
         // at the point where outputting more lines causes circular incrementing
         for (auto i = 0; i < settings->HistorySize() + core->ViewHeight(); ++i)
         {
             conn->WriteInput(L"Foo\r\n");
         }
+        VERIFY_ARE_EQUAL(scrollbackLength, core->_terminal->GetScrollOffset());
 
         // For this test, don't use any modifiers
         const auto modifiers = ControlKeyStates();
@@ -811,8 +813,86 @@ namespace ControlUnitTests
         Log::Comment(L"Verify the location of the selection");
         // The selection should now be 1 row lower
         expectedAnchor.y -= 1;
-        VERIFY_ARE_EQUAL(expectedAnchor, core->_terminal->GetSelectionAnchor());
-        VERIFY_ARE_EQUAL(expectedAnchor, core->_terminal->GetSelectionEnd());
+        {
+            const auto anchor{ core->_terminal->GetSelectionAnchor() };
+            const auto end{ core->_terminal->GetSelectionEnd() };
+            Log::Comment(fmt::format(L"expectedAnchor:({},{})", expectedAnchor.x, expectedAnchor.y).c_str());
+            Log::Comment(fmt::format(L"anchor:({},{})", anchor.x, anchor.y).c_str());
+            Log::Comment(fmt::format(L"end:({},{})", end.x, end.y).c_str());
+
+            VERIFY_ARE_EQUAL(expectedAnchor, anchor);
+            VERIFY_ARE_EQUAL(expectedAnchor, end);
+        }
+        VERIFY_ARE_EQUAL(scrollbackLength - 1, core->_terminal->GetScrollOffset());
+
+        Log::Comment(L"Output a line of text");
+        conn->WriteInput(L"Foo\r\n");
+
+        Log::Comment(L"Verify the location of the selection");
+        // The selection should now be 1 row lower
+        expectedAnchor.y -= 1;
+        {
+            const auto anchor{ core->_terminal->GetSelectionAnchor() };
+            const auto end{ core->_terminal->GetSelectionEnd() };
+            Log::Comment(fmt::format(L"expectedAnchor:({},{})", expectedAnchor.x, expectedAnchor.y).c_str());
+            Log::Comment(fmt::format(L"anchor:({},{})", anchor.x, anchor.y).c_str());
+            Log::Comment(fmt::format(L"end:({},{})", end.x, end.y).c_str());
+
+            VERIFY_ARE_EQUAL(expectedAnchor, anchor);
+            VERIFY_ARE_EQUAL(expectedAnchor, end);
+        }
+        VERIFY_ARE_EQUAL(scrollbackLength - 2, core->_terminal->GetScrollOffset());
+
+        Log::Comment(L"Move the mouse a little, to update the selection");
+        // At this point, there should only be one selection region! The
+        // viewport moved up to keep the selection at the same relative spot. So
+        // wiggling the cursor should continue to select only the same
+        // character in the buffer (if, albeit in a new location).
+        //
+        // This helps test GH #14462, a regression from #10749.
+        interactivity->PointerMoved(leftMouseDown,
+                                    WM_LBUTTONDOWN, //pointerUpdateKind
+                                    modifiers,
+                                    true, // focused,
+                                    cursorPosition0.to_core_point(),
+                                    true);
+        VERIFY_IS_TRUE(core->HasSelection());
+        VERIFY_ARE_EQUAL(1u, core->_terminal->GetSelectionRects().size());
+        {
+            const auto anchor{ core->_terminal->GetSelectionAnchor() };
+            const auto end{ core->_terminal->GetSelectionEnd() };
+            Log::Comment(fmt::format(L"expectedAnchor:({},{})", expectedAnchor.x, expectedAnchor.y).c_str());
+            Log::Comment(fmt::format(L"anchor:({},{})", anchor.x, anchor.y).c_str());
+            Log::Comment(fmt::format(L"end:({},{})", end.x, end.y).c_str());
+
+            VERIFY_ARE_EQUAL(expectedAnchor, anchor);
+            VERIFY_ARE_EQUAL(expectedAnchor, end);
+        }
+
+        Log::Comment(L"Output a line ant move the mouse a little to update the selection, all at once");
+        // Same as above. The viewport has moved, so the mouse is still over the
+        // same character, even though it's at a new offset.
+        conn->WriteInput(L"Foo\r\n");
+        expectedAnchor.y -= 1;
+        VERIFY_ARE_EQUAL(scrollbackLength - 3, core->_terminal->GetScrollOffset());
+        interactivity->PointerMoved(leftMouseDown,
+                                    WM_LBUTTONDOWN, //pointerUpdateKind
+                                    modifiers,
+                                    true, // focused,
+                                    cursorPosition1.to_core_point(),
+                                    true);
+        VERIFY_IS_TRUE(core->HasSelection());
+        VERIFY_ARE_EQUAL(1u, core->_terminal->GetSelectionRects().size());
+        {
+            const auto anchor{ core->_terminal->GetSelectionAnchor() };
+            const auto end{ core->_terminal->GetSelectionEnd() };
+            Log::Comment(fmt::format(L"expectedAnchor:({},{})", expectedAnchor.x, expectedAnchor.y).c_str());
+            Log::Comment(fmt::format(L"anchor:({},{})", anchor.x, anchor.y).c_str());
+            Log::Comment(fmt::format(L"end:({},{})", end.x, end.y).c_str());
+
+            VERIFY_ARE_EQUAL(expectedAnchor, anchor);
+            VERIFY_ARE_EQUAL(expectedAnchor, end);
+        }
 
         // Output enough text for the selection to get pushed off the buffer
         for (auto i = 0; i < settings->HistorySize() + core->ViewHeight(); ++i)


Test patch: