Skip to content

Commit

Permalink
Add support for tabbing to embedded hyperlinks (#18347)
Browse files Browse the repository at this point in the history
## 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
  • Loading branch information
carlos-zamora authored Feb 24, 2025
1 parent 62e7f4b commit 35bd607
Showing 1 changed file with 82 additions and 17 deletions.
99 changes: 82 additions & 17 deletions src/cascadia/TerminalCore/TerminalSelection.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 35bd607

Please # to comment.