Repository: microsoft/terminal (language: cpp)

Issue:
Move to OSC 8 hyperlinks with keyboard in mark mode
# Description of the new feature/enhancement

Currently you can cycle through URLs in the scrollback buffer with tab and shift+tab in mark mode. It would be nice if this also worked with OSC 8 hyperlinks.

Right now this:
```bash
printf '\e]8;;http://example.com\e\\This is a link\e]8;;\e\\\nhttp://example.com\n'
```
only lets you shift+tab back to the plain text link, not the OSC 8 one, which you would have to navigate to using the arrow keys.


Discussion / hints:

Fix patch:
diff --git a/src/cascadia/TerminalCore/TerminalSelection.cpp b/src/cascadia/TerminalCore/TerminalSelection.cpp
index 9501f64a85e..707415015a6 100644
--- a/src/cascadia/TerminalCore/TerminalSelection.cpp
+++ b/src/cascadia/TerminalCore/TerminalSelection.cpp
@@ -437,7 +437,8 @@ void Terminal::SelectHyperlink(const SearchDirection dir)
     }
 
     // 0. Useful tools/vars
-    const auto bufferSize = _activeBuffer().GetSize();
+    const auto& buffer = _activeBuffer();
+    const auto bufferSize = buffer.GetSize();
     const auto viewportHeight = _GetMutableViewport().Height();
 
     // The patterns are stored relative to the "search area". Initially, this search area will be the viewport,
@@ -504,8 +505,18 @@ void Terminal::SelectHyperlink(const SearchDirection dir)
     };
 
     // 1. Look for the hyperlink
-    til::point searchStart = dir == SearchDirection::Forward ? _selection->start : til::point{ bufferSize.Left(), _VisibleStartIndex() };
-    til::point searchEnd = dir == SearchDirection::Forward ? til::point{ bufferSize.RightInclusive(), _VisibleEndIndex() } : _selection->start;
+    til::point searchStart;
+    til::point searchEnd;
+    if (dir == SearchDirection::Forward)
+    {
+        searchStart = _selection->start;
+        searchEnd = til::point{ bufferSize.RightInclusive(), _VisibleEndIndex() };
+    }
+    else
+    {
+        searchStart = til::point{ bufferSize.Left(), _VisibleStartIndex() };
+        searchEnd = _selection->start;
+    }
 
     // 1.A) Try searching the current viewport (no scrolling required)
     auto resultList = _patternIntervalTree.findContained(convertToSearchArea(searchStart), convertToSearchArea(searchEnd));
@@ -547,27 +558,81 @@ void Terminal::SelectHyperlink(const SearchDirection dir)
                 searchArea = Viewport::FromDimensions(searchStart, { searchEnd.x + 1, searchEnd.y + 1 });
             }
         }
+    }
 
-        // 1.C) Nothing was found. Bail!
-        if (!result.has_value())
+    // 2. We found a hyperlink from the pattern tree. Look for embedded hyperlinks too!
+    // Use the result (if one was found) to narrow down the search.
+    if (dir == SearchDirection::Forward)
+    {
+        searchStart = _selection->start;
+        searchEnd = (result ? result->first : buffer.GetLastNonSpaceCharacter());
+    }
+    else
+    {
+        searchStart = (result ? result->second : bufferSize.Origin());
+        searchEnd = _selection->start;
+    }
+
+    // Careful! Selection can point to RightExclusive(), which doesn't contain data!
+    // Clamp to be safe.
+    auto initialPos = dir == SearchDirection::Forward ? searchStart : searchEnd;
+    bufferSize.Clamp(initialPos);
+    auto iter = buffer.GetCellDataAt(initialPos);
+    while (dir == SearchDirection::Forward ? iter.Pos() < searchEnd : iter.Pos() > searchStart)
+    {
+        // Don't let us select the same hyperlink again
+        if (iter.Pos() < _selection->start || iter.Pos() > _selection->end)
         {
-            return;
+            if (auto attr = iter->TextAttr(); attr.IsHyperlink())
+            {
+                // Found an embedded hyperlink!
+                const auto hyperlinkId = attr.GetHyperlinkId();
+
+                // Expand the start to include the entire hyperlink
+                TextBufferCellIterator hyperlinkStartIter{ buffer, iter.Pos() };
+                while (hyperlinkStartIter.Pos() > searchStart && attr.IsHyperlink() && attr.GetHyperlinkId() == hyperlinkId)
+                {
+                    --hyperlinkStartIter;
+                    attr = hyperlinkStartIter->TextAttr();
+                }
+                if (hyperlinkStartIter.Pos() != bufferSize.Origin())
+                {
+                    // undo a move to be inclusive
+                    ++hyperlinkStartIter;
+                }
+
+                // Expand the end to include the entire hyperlink
+                // No need to undo a move! We'll decrement in the next step anyways.
+                TextBufferCellIterator hyperlinkEndIter{ buffer, iter.Pos() };
+                attr = hyperlinkEndIter->TextAttr();
+                while (hyperlinkEndIter.Pos() < searchEnd && attr.IsHyperlink() && attr.GetHyperlinkId() == hyperlinkId)
+                {
+                    ++hyperlinkEndIter;
+                    attr = hyperlinkEndIter->TextAttr();
+                }
+
+                result = { hyperlinkStartIter.Pos(), hyperlinkEndIter.Pos() };
+                break;
+            }
         }
+        iter += dir == SearchDirection::Forward ? 1 : -1;
     }
 
-    // 2. Select the hyperlink
+    // 3. Select the hyperlink, if one exists
+    if (!result.has_value())
     {
-        auto selection{ _selection.write() };
-        wil::hide_name _selection;
-        selection->start = result->first;
-        selection->pivot = result->first;
-        selection->end = result->second;
-        _selectionIsTargetingUrl = true;
-        _selectionEndpoint = SelectionEndpoint::End;
+        return;
     }
-
-    // 3. Scroll to the selected area (if necessary)
-    _ScrollToPoint(_selection->end);
+    auto selection{ _selection.write() };
+    wil::hide_name _selection;
+    selection->start = result->first;
+    selection->pivot = result->first;
+    selection->end = result->second;
+    _selectionIsTargetingUrl = true;
+    _selectionEndpoint = SelectionEndpoint::End;
+
+    // 4. Scroll to the selected area (if necessary)
+    _ScrollToPoint(selection->end);
 }
 
 Terminal::UpdateSelectionParams Terminal::ConvertKeyEventToUpdateSelectionParams(const ControlKeyStates mods, const WORD vkey) const noexcept


Test patch: