Skip to content

Commit

Permalink
Add keyboard navigation to hyperlinks
Browse files Browse the repository at this point in the history
  • Loading branch information
carlos-zamora committed Jun 30, 2022
1 parent 04a6d0b commit 6f4cc16
Show file tree
Hide file tree
Showing 8 changed files with 197 additions and 7 deletions.
27 changes: 22 additions & 5 deletions src/cascadia/TerminalControl/ControlCore.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -417,12 +417,29 @@ namespace winrt::Microsoft::Terminal::Control::implementation
vkey != VK_SNAPSHOT &&
keyDown)
{
if (_terminal->IsInMarkMode() && modifiers.IsCtrlPressed() && vkey == 'A')
if (_terminal->IsInMarkMode())
{
auto lock = _terminal->LockForWriting();
_terminal->SelectAll();
_updateSelection();
return true;
if (modifiers.IsCtrlPressed() && vkey == 'A')
{
auto lock = _terminal->LockForWriting();
_terminal->SelectAll();
_updateSelection();
return true;
}
else if (_settings->DetectURLs() && vkey == VK_TAB)
{
auto lock = _terminal->LockForWriting();
_terminal->SelectHyperlink(!modifiers.IsShiftPressed());
_updateSelection();
return true;
}
else if (_terminal->IsTargetingUrl() && vkey == VK_RETURN)
{
auto lock = _terminal->LockForReading();
const auto uri = _terminal->GetHyperlinkAtPosition(_terminal->GetSelectionAnchor());
_OpenHyperlinkHandlers(*this, winrt::make<OpenHyperlinkEventArgs>(winrt::hstring{ uri }));
return true;
}
}

// try to update the selection
Expand Down
1 change: 1 addition & 0 deletions src/cascadia/TerminalControl/ControlCore.h
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
TYPED_EVENT(FoundMatch, IInspectable, Control::FoundResultsArgs);
TYPED_EVENT(ShowWindowChanged, IInspectable, Control::ShowWindowArgs);
TYPED_EVENT(UpdateSelectionMarkers, IInspectable, Control::UpdateSelectionMarkersEventArgs);
TYPED_EVENT(OpenHyperlink, IInspectable, Control::OpenHyperlinkEventArgs);
// clang-format on

private:
Expand Down
2 changes: 1 addition & 1 deletion src/cascadia/TerminalControl/ControlCore.idl
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,6 @@ namespace Microsoft.Terminal.Control
event Windows.Foundation.TypedEventHandler<Object, FoundResultsArgs> FoundMatch;
event Windows.Foundation.TypedEventHandler<Object, ShowWindowArgs> ShowWindowChanged;
event Windows.Foundation.TypedEventHandler<Object, UpdateSelectionMarkersEventArgs> UpdateSelectionMarkers;

event Windows.Foundation.TypedEventHandler<Object, OpenHyperlinkEventArgs> OpenHyperlink;
};
}
1 change: 1 addition & 0 deletions src/cascadia/TerminalControl/TermControl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
_core.HoveredHyperlinkChanged({ this, &TermControl::_hoveredHyperlinkChanged });
_core.FoundMatch({ this, &TermControl::_coreFoundMatch });
_core.UpdateSelectionMarkers({ this, &TermControl::_updateSelectionMarkers });
_core.OpenHyperlink({ this, &TermControl::_HyperlinkHandler });
_interactivity.OpenHyperlink({ this, &TermControl::_HyperlinkHandler });
_interactivity.ScrollPositionChanged({ this, &TermControl::_ScrollPositionChanged });

Expand Down
1 change: 1 addition & 0 deletions src/cascadia/TerminalCore/Terminal.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ Terminal::Terminal() :
_snapOnInput{ true },
_altGrAliasing{ true },
_blockSelection{ false },
_isTargetingUrl{ false },
_markMode{ false },
_quickEditMode{ false },
_selection{ std::nullopt },
Expand Down
4 changes: 4 additions & 0 deletions src/cascadia/TerminalCore/Terminal.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,8 @@ class Microsoft::Terminal::Core::Terminal final :
void SwitchSelectionEndpoint();
bool IsInMarkMode() const;
void ToggleMarkMode();
void SelectHyperlink(const bool movingForward);
bool IsTargetingUrl() const noexcept;

using UpdateSelectionParams = std::optional<std::pair<SelectionDirection, SelectionExpansion>>;
UpdateSelectionParams ConvertKeyEventToUpdateSelectionParams(const ControlKeyStates mods, const WORD vkey) const;
Expand Down Expand Up @@ -341,6 +343,7 @@ class Microsoft::Terminal::Core::Terminal final :
bool _blockSelection;
std::wstring _wordDelimiters;
SelectionExpansion _multiClickSelectionMode;
bool _isTargetingUrl;
bool _markMode;
bool _quickEditMode;
SelectionEndpoint _selectionEndpoint;
Expand Down Expand Up @@ -418,6 +421,7 @@ class Microsoft::Terminal::Core::Terminal final :
std::pair<til::point, til::point> _PivotSelection(const til::point targetPos, bool& targetStart) const;
std::pair<til::point, til::point> _ExpandSelectionAnchors(std::pair<til::point, til::point> anchors) const;
til::point _ConvertToBufferCell(const til::point viewportPos) const;
til::point _ConvertToViewportCell(const til::point viewportPos) const;
void _MoveByChar(SelectionDirection direction, til::point& pos);
void _MoveByWord(SelectionDirection direction, til::point& pos);
void _MoveByViewport(SelectionDirection direction, til::point& pos);
Expand Down
167 changes: 167 additions & 0 deletions src/cascadia/TerminalCore/TerminalSelection.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,7 @@ void Terminal::ToggleMarkMode()
_markMode = true;
_quickEditMode = false;
_blockSelection = false;
_isTargetingUrl = false;
WI_SetAllFlags(_selectionEndpoint, SelectionEndpoint::Start | SelectionEndpoint::End);
}
}
Expand Down Expand Up @@ -341,6 +342,157 @@ void Terminal::SwitchSelectionEndpoint()
}
}

// Method Description:
// - selects the next/previous hyperlink, if one is available
// Arguments:
// - searchForward: if true, we're scanning forward towards the end of the buffer. Otherwise, we're scanning backwards towards the top.
// Return Value:
// - true if we found a hyperlink to select (and selected it). False otherwise.
void Terminal::SelectHyperlink(const bool searchForward)
{
if (!_markMode)
{
// This feature only works in mark mode
_isTargetingUrl = false;
return;
}

// 0. Useful tools/vars
const auto bufferSize = _activeBuffer().GetSize();
const auto viewportHeight = _GetMutableViewport().Height();

// extracts the next/previous hyperlink from the list of hyperlink ranges provided
auto extractResultFromList = [&, onUrl = _isTargetingUrl, selectionStart = _selection->start](const std::vector<interval_tree::Interval<til::point, size_t>>& list, bool forward, bool useViewportCoords) {
// checks if we're already on the hyperlink and if we're trying to set it to the same one
auto isAlreadySelected = [onUrl, selectionStartForValidation = useViewportCoords ? _ConvertToViewportCell(selectionStart) : selectionStart](const interval_tree::Interval<til::point, size_t> range) {
return onUrl && selectionStartForValidation == range.start;
};

std::optional<std::pair<til::point, til::point>> resultFromList;
if (list.size() == 1)
{
if (!isAlreadySelected(list.front()))
{
resultFromList = std::make_pair(list.front().start, list.front().stop);
}
}
else if (list.size() > 1)
{
// multiple results, find the first one in the direction we're going
if (forward)
{
for (const auto& range : list)
{
if (isAlreadySelected(range))
{
continue;
}
else if (!resultFromList || range.start < resultFromList->first)
{
resultFromList = std::make_pair(range.start, range.stop);
}
}
}
else
{
for (const auto& range : list)
{
if (isAlreadySelected(range))
{
continue;
}
else if (!resultFromList || range.start > resultFromList->first)
{
resultFromList = std::make_pair(range.start, range.stop);
}
}
}
}

// useViewportCoords --> pattern tree stores everything as viewport coords,
// so we need to convert them on the way out
if (useViewportCoords && resultFromList)
{
resultFromList->first = _ConvertToBufferCell(resultFromList->first);
resultFromList->second = _ConvertToBufferCell(resultFromList->second);
}
return resultFromList;
};

// 1. Look for the hyperlink
til::point searchStart = searchForward ? _selection->start : til::point{ bufferSize.Left(), _VisibleStartIndex() };
til::point searchEnd = searchForward ? til::point{ bufferSize.RightInclusive(), _VisibleEndIndex() } : _selection->start;

// 1.A) Try searching the current viewport (no scrolling required)
auto patterns = _patternIntervalTree;
auto resultList = patterns.findContained(_ConvertToViewportCell(searchStart), _ConvertToViewportCell(searchEnd));
std::optional<std::pair<til::point, til::point>> result = extractResultFromList(resultList, searchForward, true);
if (!result)
{
// 1.B) Incrementally search through more of the space
if (searchForward)
{
searchStart = { bufferSize.Left(), searchEnd.y + 1 };
searchEnd = { bufferSize.RightInclusive(), std::min(searchStart.y + viewportHeight, ViewEndIndex()) };
}
else
{
searchEnd = { bufferSize.RightInclusive(), searchStart.y - 1 };
searchStart = { bufferSize.Left(), std::max(searchStart.y - viewportHeight, bufferSize.Top()) };
}
const til::point bufferStart{ bufferSize.Origin() };
const til::point bufferEnd{ bufferSize.RightInclusive(), ViewEndIndex() };
while (!result && bufferSize.IsInBounds(searchStart) && bufferSize.IsInBounds(searchEnd) && searchStart <= searchEnd && bufferStart <= searchStart && searchEnd <= bufferEnd)
{
patterns = _activeBuffer().GetPatterns(searchStart.y, searchEnd.y);
resultList = patterns.findContained(searchStart, searchEnd);
result = extractResultFromList(resultList, searchForward, false);
if (!result)
{
searchStart.y += 1;
searchEnd.y = std::min(searchStart.y + viewportHeight, ViewEndIndex());
}
}

// 1.C) Nothing was found. Bail!
if (!result.has_value())
{
return;
}
}

// 2. Select the hyperlink
_selection->start = result->first;
_selection->pivot = result->first;
_selection->end = result->second;
bufferSize.DecrementInBounds(_selection->end);
_isTargetingUrl = true;

// 3. Scroll to the selected area (if necessary)
// TODO CARLOS: I stole this from UpdateSelection(). I should probably de-duplicate it.
if (const auto visibleViewport = _GetVisibleViewport(); !visibleViewport.IsInBounds(_selection->end))
{
if (const auto amtAboveView = visibleViewport.Top() - _selection->end.Y; amtAboveView > 0)
{
// anchor is above visible viewport, scroll by that amount
_scrollOffset += amtAboveView;
}
else
{
// anchor is below visible viewport, scroll by that amount
const auto amtBelowView = _selection->end.Y - visibleViewport.BottomInclusive();
_scrollOffset -= amtBelowView;
}
_NotifyScrollEvent();
_activeBuffer().TriggerScroll();
}
}

bool Terminal::IsTargetingUrl() const noexcept
{
return _isTargetingUrl;
}

Terminal::UpdateSelectionParams Terminal::ConvertKeyEventToUpdateSelectionParams(const ControlKeyStates mods, const WORD vkey) const
{
if ((_markMode || mods.IsShiftPressed()) && !mods.IsAltPressed())
Expand Down Expand Up @@ -445,6 +597,7 @@ void Terminal::UpdateSelection(SelectionDirection direction, SelectionExpansion
targetPos = std::min(targetPos, _GetMutableViewport().BottomRightInclusive());

// 3. Actually modify the selection state
_isTargetingUrl = false;
_quickEditMode = !_markMode;
if (shouldMoveCursor)
{
Expand Down Expand Up @@ -636,6 +789,7 @@ void Terminal::ClearSelection()
_selection = std::nullopt;
_markMode = false;
_quickEditMode = false;
_isTargetingUrl = false;
_selectionEndpoint = static_cast<SelectionEndpoint>(0);
_anchorSelectionEndpoint = false;
}
Expand Down Expand Up @@ -680,6 +834,19 @@ til::point Terminal::_ConvertToBufferCell(const til::point viewportPos) const
return bufferPos;
}

// Method Description:
// - convert buffer position to the corresponding location on the viewport
// Arguments:
// - bufferPos: a coordinate on the buffer
// Return Value:
// - the corresponding location on the viewport
til::point Terminal::_ConvertToViewportCell(const til::point bufferPos) const
{
const auto yPos = bufferPos.Y - _VisibleStartIndex();
til::point viewportPos = { bufferPos.X, yPos };
return viewportPos;
}

// Method Description:
// - This method won't be used. We just throw and do nothing. For now we
// need this method to implement UiaData interface
Expand Down
1 change: 0 additions & 1 deletion src/cascadia/TerminalSettingsModel/defaults.json
Original file line number Diff line number Diff line change
Expand Up @@ -382,7 +382,6 @@
// Clipboard Integration
{ "command": { "action": "copy", "singleLine": false }, "keys": "ctrl+shift+c" },
{ "command": { "action": "copy", "singleLine": false }, "keys": "ctrl+insert" },
{ "command": { "action": "copy", "singleLine": false }, "keys": "enter" },
{ "command": "paste", "keys": "ctrl+shift+v" },
{ "command": "paste", "keys": "shift+insert" },
{ "command": "selectAll", "keys": "ctrl+shift+a" },
Expand Down

0 comments on commit 6f4cc16

Please sign in to comment.