Add support for tabbing to embedded hyperlinks (#18347)

## Summary of the Pull Request
There's already logic to tab to a hyperlink when we're in mark mode. We
do this by looking at the automatically detected hyperlinks and finding
the next one of interest. This adds an extra step afterwards to find any
embedded hyperlinks and tab to them too.

Since embedded hyperlinks are stored as text attributes, we need to
iterate through the buffer to find the hyperlink and it's buffer
boundaries. This PR tries to reduce the workload of that by first
finding the automatically detected hyperlinks (since that's a fairly
quick process), then using the reduced search area to find the embedded
hyperlink (if one exists).

## Validation Steps Performed
In PowerShell, add an embedded hyperlink as such:
```powershell
${ESC}=[char]27
Write-Host "${ESC}]8;;https://github.com/microsoft/terminal${ESC}\This is a link!${ESC}]8;;${ESC}\"
```
Enter mark mode (ctrl+shift+m) then shift+tab to it.
 The "This is a link!" is selected
 Verified that this works when searching forwards and backwards

Closes #18310
Closes #15194 
Follow-up from #13405
OSC 8 support added in #7251
This commit is contained in:
Carlos Zamora 2025-02-24 15:55:45 -08:00 committed by GitHub
parent 62e7f4bfad
commit 35bd60782f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -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())
{
return;
}
}
// 2. Select the hyperlink
// 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)
{
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;
searchStart = _selection->start;
searchEnd = (result ? result->first : buffer.GetLastNonSpaceCharacter());
}
else
{
searchStart = (result ? result->second : bufferSize.Origin());
searchEnd = _selection->start;
}
// 3. Scroll to the selected area (if necessary)
_ScrollToPoint(_selection->end);
// 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)
{
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;
}
// 3. Select the hyperlink, if one exists
if (!result.has_value())
{
return;
}
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