Repository: microsoft/terminal (language: cpp)

Issue:
Double-click word selection picks up the RPROMPT from the previous line because word wrapping is hard
Discussed this with Leonard a while ago


Discussion / hints:

Fix patch:
diff --git a/.github/actions/spelling/allow/allow.txt b/.github/actions/spelling/allow/allow.txt
index f6c03606a58..d5f5f9c2a73 100644
--- a/.github/actions/spelling/allow/allow.txt
+++ b/.github/actions/spelling/allow/allow.txt
@@ -80,6 +80,7 @@ mnt
 mru
 nje
 noreply
+notwrapped
 ogonek
 ok'd
 overlined
diff --git a/src/buffer/out/textBuffer.cpp b/src/buffer/out/textBuffer.cpp
index 10c70646e9d..2aa9176a8ca 100644
--- a/src/buffer/out/textBuffer.cpp
+++ b/src/buffer/out/textBuffer.cpp
@@ -1440,10 +1440,23 @@ til::point TextBuffer::_GetWordStartForSelection(const til::point target, const
     // expand left until we hit the left boundary or a different delimiter class
     while (result != bufferSize.Origin() && _GetDelimiterClassAt(result, wordDelimiters) == initialDelimiter)
     {
-        //prevent selection wrapping on whitespace selection
-        if (isControlChar && result.x == bufferSize.Left())
+        if (result.x == bufferSize.Left())
         {
-            break;
+            // Prevent wrapping to the previous line if the selection begins on whitespace
+            if (isControlChar)
+            {
+                break;
+            }
+
+            if (result.y > 0)
+            {
+                // Prevent wrapping to the previous line if it was hard-wrapped (e.g. not forced by us to wrap)
+                const auto& priorRow = GetRowByOffset(result.y - 1);
+                if (!priorRow.WasWrapForced())
+                {
+                    break;
+                }
+            }
         }
         bufferSize.DecrementInBounds(result);
     }
@@ -1563,10 +1576,22 @@ til::point TextBuffer::_GetWordEndForSelection(const til::point target, const st
     // expand right until we hit the right boundary as a ControlChar or a different delimiter class
     while (result != bufferSize.BottomRightInclusive() && _GetDelimiterClassAt(result, wordDelimiters) == initialDelimiter)
     {
-        if (isControlChar && result.x == bufferSize.RightInclusive())
+        if (result.x == bufferSize.RightInclusive())
         {
-            break;
+            // Prevent wrapping to the next line if the selection begins on whitespace
+            if (isControlChar)
+            {
+                break;
+            }
+
+            // Prevent wrapping to the next line if this one was hard-wrapped (e.g. not forced by us to wrap)
+            const auto& row = GetRowByOffset(result.y);
+            if (!row.WasWrapForced())
+            {
+                break;
+            }
         }
+
         bufferSize.IncrementInBoundsCircular(result);
     }
 
diff --git a/src/host/ut_host/TextBufferTests.cpp b/src/host/ut_host/TextBufferTests.cpp
index 8f837985b70..f70f5c6b8ec 100644
--- a/src/host/ut_host/TextBufferTests.cpp
+++ b/src/host/ut_host/TextBufferTests.cpp
@@ -2336,16 +2336,32 @@ void TextBufferTests::GetWordBoundaries()
     }
 
     _buffer->Reset();
-    _buffer->ResizeTraditional({ 10, 5 });
+    _buffer->ResizeTraditional({ 10, 6 });
     const std::vector<std::wstring> secondText = { L"this wordiswrapped",
+                                                   L"notwrapped"
                                                    L"spaces        wrapped reachEOB" };
-    //Buffer looks like:
-    //  this wordi
-    //  swrapped
-    //  spaces
-    //      wrappe
-    //  d reachEOB
+
     WriteLinesToBuffer(secondText, *_buffer);
+
+    //Buffer looks like:
+    //   0123456789
+    // 0|this wordi| < wrapped
+    // 1|swrapped  | < not wrapped
+    // 2|notwrapped| < not wrapped
+    // 3|spaces    | < wrapped
+    // 4|    wrappe| < wrapped
+    // 5|d reachEOB| < wrapped
+
+    VERIFY_IS_TRUE(_buffer->GetRowByOffset(0).WasWrapForced());
+    VERIFY_IS_FALSE(_buffer->GetRowByOffset(1).WasWrapForced());
+    // GH#780 See the comment in WriteLinesToBuffer
+    // VERIFY_IS_FALSE(_buffer->GetRowByOffset(2).WasWrapForced());
+    _buffer->GetMutableRowByOffset(2).SetWrapForced(false); // Ugh
+    VERIFY_IS_TRUE(_buffer->GetRowByOffset(3).WasWrapForced());
+    VERIFY_IS_TRUE(_buffer->GetRowByOffset(4).WasWrapForced());
+    VERIFY_IS_TRUE(_buffer->GetRowByOffset(5).WasWrapForced());
+
+    // clang-format off
     testData = {
         { { 0, 0 }, { { 0, 0 }, { 0, 0 } } },
         { { 1, 0 }, { { 0, 0 }, { 0, 0 } } },
@@ -2358,15 +2374,18 @@ void TextBufferTests::GetWordBoundaries()
         { { 9, 1 }, { { 8, 1 }, { 5, 0 } } },
 
         { { 0, 2 }, { { 0, 2 }, { 0, 2 } } },
-        { { 7, 2 }, { { 6, 2 }, { 0, 2 } } },
-
-        { { 1, 3 }, { { 0, 3 }, { 0, 2 } } },
-        { { 4, 3 }, { { 4, 3 }, { 4, 3 } } },
-        { { 8, 3 }, { { 4, 3 }, { 4, 3 } } },
-
-        { { 0, 4 }, { { 4, 3 }, { 4, 3 } } },
-        { { 1, 4 }, { { 1, 4 }, { 4, 3 } } },
-        { { 9, 4 }, { { 2, 4 }, { 2, 4 } } },
+        { { 9, 2 }, { { 0, 2 }, { 0, 2 } } },
+                                  // v accessibility does not consider wrapping
+        { { 0, 3 }, { { 0, 3 }, { 0, 2 } } },
+        { { 7, 3 }, { { 6, 3 }, { 0, 2 } } },
+                                  // v accessibility does not consider wrapping
+        { { 1, 4 }, { { 0, 4 }, { 0, 2 } } },
+        { { 4, 4 }, { { 4, 4 }, { 4, 4 } } },
+        { { 8, 4 }, { { 4, 4 }, { 4, 4 } } },
+
+        { { 0, 5 }, { { 4, 4 }, { 4, 4 } } },
+        { { 1, 5 }, { { 1, 5 }, { 4, 4 } } },
+        { { 9, 5 }, { { 2, 5 }, { 2, 5 } } },
     };
     for (const auto& test : testData)
     {
@@ -2377,12 +2396,15 @@ void TextBufferTests::GetWordBoundaries()
     }
 
     //GetWordEnd for Wrapping Text
-    //Buffer looks like:
-    //  this wordi
-    //  swrapped
-    //  spaces
-    //      wrappe
-    //  d reachEOB
+    // Buffer:
+    //   0123456789
+    // 0|this wordi| < wrapped
+    // 1|swrapped  | < not wrapped
+    // 2|notwrapped| < not wrapped
+    // 3|spaces    | < wrapped
+    // 4|    wrappe| < wrapped
+    // 5|d reachEOB| < wrapped
+    // clang-format off
     testData = {
         // tests for first line of text
         { { 0, 0 }, { { 3, 0 }, { 5, 0 } } },
@@ -2395,17 +2417,20 @@ void TextBufferTests::GetWordBoundaries()
         { { 7, 1 }, { { 7, 1 }, { 0, 2 } } },
         { { 9, 1 }, { { 9, 1 }, { 0, 2 } } },
 
-        { { 0, 2 }, { { 5, 2 }, { 4, 3 } } },
-        { { 7, 2 }, { { 9, 2 }, { 4, 3 } } },
+        { { 0, 2 }, { { 9, 2 }, { 4, 4 } } },
+        { { 9, 2 }, { { 9, 2 }, { 4, 4 } } },
+
+        { { 0, 3 }, { { 5, 3 }, { 4, 4 } } },
+        { { 7, 3 }, { { 9, 3 }, { 4, 4 } } },
 
-        { { 1, 3 }, { { 3, 3 }, { 4, 3 } } },
-        { { 4, 3 }, { { 0, 4 }, { 2, 4 } } },
-        { { 8, 3 }, { { 0, 4 }, { 2, 4 } } },
+        { { 1, 4 }, { { 3, 4 }, { 4, 4 } } },
+        { { 4, 4 }, { { 0, 5 }, { 2, 5 } } },
+        { { 8, 4 }, { { 0, 5 }, { 2, 5 } } },
 
-        { { 0, 4 }, { { 0, 4 }, { 2, 4 } } },
-        { { 1, 4 }, { { 1, 4 }, { 2, 4 } } },
-        { { 4, 4 }, { { 9, 4 }, { 0, 5 } } },
-        { { 9, 4 }, { { 9, 4 }, { 0, 5 } } },
+        { { 0, 5 }, { { 0, 5 }, { 2, 5 } } },
+        { { 1, 5 }, { { 1, 5 }, { 2, 5 } } },
+        { { 4, 5 }, { { 9, 5 }, { 0, 6 } } },
+        { { 9, 5 }, { { 9, 5 }, { 0, 6 } } },
     };
     // clang-format on
 


Test patch: