Repository: microsoft/terminal (language: cpp)

Issue:
Keyboard navigation of clickable link
# Description of the new feature/enhancement
Keyboard navigation of clickable link in terminal text. [Terminal Roadmap v2.0](https://github.com/microsoft/terminal/blob/master/doc/terminal-v2-roadmap.md) is describing about clickable link

| Hyperlinking any links that appear in the text buffer. When clicking on the link, the link will open in your default browser.

However, this involves pointing device such as mouse or trackball. I believe many of terminal users love keyboard and it is important to keep using keyboard without switching to pointing device as much as possble.

# Proposed technical implementation details (optional)
We may have a command like "NavigateToPreviousClickableLink" or "NavtigateToNextClickableLink" and "InvokeFocusedClickableLink", then we can reassign key binding those command.

bentaudun commented as https://github.com/microsoft/terminal/issues/204#issuecomment-640016016 but wasn't mentioned in https://github.com/microsoft/terminal/issues/574


Discussion / hints:
I agree this would be super useful. I'd think we could probably leave out `InvokeFocusedClickableLink`, and just leave that as <kbd>space</kbd> or <kbd>enter</kbd>, but now as I'm typing this up, maybe people would want to navigate to the previous link, then copy it, then return to the input line and paste it? So maybe another action for that makes sense? Though, we can't have multiple actions bound to the same key, so that wouldn't work with "<kbd>enter</kbd> to copy" people...

I'm maybe rambling a bit. 

This is a good idea for inclusion once #574/#204 lands.
Copying some ideas from the dup I accidentally made...

# Description of the new feature/enhancement
Windows Terminal supports automatic detection of hyperlinks and allows you to open the hyperlink using your mouse (specifically ctrl+clicking the link). I've realized that there's no way to do this using only the keyboard.

Once mark mode lands, the solution is a little better, but still far from perfect and on par. The user would be able to navigate to the text with the hyperlink, create a selection that encompasses it, copy the URL, then manually open it in their preferred web browser.

What if we could make this a nicer experience? Consider this...
1. enter mark mode
2. <kbd>tab</kbd> (or <kbd>shift+tab</kbd>) to navigate to these hyperlinks
3. when you've navigated to the hyperlink, a selection is created that encompasses the relevant hyperlink.
4. <kbd>enter</kbd> can be used to invoke the hyperlink directly

Some corner cases...
- what if <kbd>enter</kbd> is bound to an action (like "copy")? --> prefer the action
- what if we add more bookmark-like things like "prompt position" or user-configured bookmarks --> might as well navigate to them too? I'm thinking these can be tab stops. 


From Mike...
> Oh man do I have thoughts in https://github.com/microsoft/terminal/issues/11000.

Fix patch:
diff --git a/src/cascadia/TerminalControl/ControlCore.cpp b/src/cascadia/TerminalControl/ControlCore.cpp
index d60f90bd380..01669c7d929 100644
--- a/src/cascadia/TerminalControl/ControlCore.cpp
+++ b/src/cascadia/TerminalControl/ControlCore.cpp
@@ -417,13 +417,43 @@ namespace winrt::Microsoft::Terminal::Control::implementation
             vkey != VK_SNAPSHOT &&
             keyDown)
         {
-            const auto isInMarkMode = _terminal->SelectionMode() == ::Microsoft::Terminal::Core::Terminal::SelectionInteractionMode::Mark;
-            if (isInMarkMode && modifiers.IsCtrlPressed() && vkey == 'A')
+            const auto isInMarkMode = _terminal->SelectionMode() == ::Terminal::SelectionInteractionMode::Mark;
+            if (isInMarkMode)
             {
-                auto lock = _terminal->LockForWriting();
-                _terminal->SelectAll();
-                _updateSelectionUI();
-                return true;
+                if (modifiers.IsCtrlPressed() && vkey == 'A')
+                {
+                    // Ctrl + A --> Select all
+                    auto lock = _terminal->LockForWriting();
+                    _terminal->SelectAll();
+                    _updateSelectionUI();
+                    return true;
+                }
+                else if (vkey == VK_TAB && _settings->DetectURLs())
+                {
+                    // [Shift +] Tab --> next/previous hyperlink
+                    auto lock = _terminal->LockForWriting();
+                    const auto direction = modifiers.IsShiftPressed() ? ::Terminal::SearchDirection::Backward : ::Terminal::SearchDirection::Forward;
+                    _terminal->SelectHyperlink(direction);
+                    _updateSelectionUI();
+                    return true;
+                }
+                else if (vkey == VK_RETURN && modifiers.IsCtrlPressed() && _terminal->IsTargetingUrl())
+                {
+                    // Ctrl + Enter --> Open URL
+                    auto lock = _terminal->LockForReading();
+                    const auto uri = _terminal->GetHyperlinkAtBufferPosition(_terminal->GetSelectionAnchor());
+                    _OpenHyperlinkHandlers(*this, winrt::make<OpenHyperlinkEventArgs>(winrt::hstring{ uri }));
+                    return true;
+                }
+                else if (vkey == VK_RETURN)
+                {
+                    // [Shift +] Enter --> copy text
+                    // Don't lock here! CopySelectionToClipboard already locks for you!
+                    CopySelectionToClipboard(modifiers.IsShiftPressed(), nullptr);
+                    _terminal->ClearSelection();
+                    _updateSelectionUI();
+                    return true;
+                }
             }
 
             // try to update the selection
@@ -591,12 +621,12 @@ namespace winrt::Microsoft::Terminal::Control::implementation
         _lastHoveredCell = terminalPosition;
         uint16_t newId{ 0u };
         // we can't use auto here because we're pre-declaring newInterval.
-        decltype(_terminal->GetHyperlinkIntervalFromPosition({})) newInterval{ std::nullopt };
+        decltype(_terminal->GetHyperlinkIntervalFromViewportPosition({})) newInterval{ std::nullopt };
         if (terminalPosition.has_value())
         {
             auto lock = _terminal->LockForReading(); // Lock for the duration of our reads.
-            newId = _terminal->GetHyperlinkIdAtPosition(*terminalPosition);
-            newInterval = _terminal->GetHyperlinkIntervalFromPosition(*terminalPosition);
+            newId = _terminal->GetHyperlinkIdAtViewportPosition(*terminalPosition);
+            newInterval = _terminal->GetHyperlinkIntervalFromViewportPosition(*terminalPosition);
         }
 
         // If the hyperlink ID changed or the interval changed, trigger a redraw all
@@ -626,7 +656,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
     {
         // Lock for the duration of our reads.
         auto lock = _terminal->LockForReading();
-        return winrt::hstring{ _terminal->GetHyperlinkAtPosition(til::point{ pos }) };
+        return winrt::hstring{ _terminal->GetHyperlinkAtViewportPosition(til::point{ pos }) };
     }
 
     winrt::hstring ControlCore::HoveredUriText() const
@@ -634,7 +664,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
         auto lock = _terminal->LockForReading(); // Lock for the duration of our reads.
         if (_lastHoveredCell.has_value())
         {
-            return winrt::hstring{ _terminal->GetHyperlinkAtPosition(*_lastHoveredCell) };
+            return winrt::hstring{ _terminal->GetHyperlinkAtViewportPosition(*_lastHoveredCell) };
         }
         return {};
     }
diff --git a/src/cascadia/TerminalControl/ControlCore.h b/src/cascadia/TerminalControl/ControlCore.h
index 4208798e10d..e4209318e7f 100644
--- a/src/cascadia/TerminalControl/ControlCore.h
+++ b/src/cascadia/TerminalControl/ControlCore.h
@@ -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:
diff --git a/src/cascadia/TerminalControl/ControlCore.idl b/src/cascadia/TerminalControl/ControlCore.idl
index 1d7801bdad4..ec8230fa180 100644
--- a/src/cascadia/TerminalControl/ControlCore.idl
+++ b/src/cascadia/TerminalControl/ControlCore.idl
@@ -153,6 +153,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;
     };
 }
diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp
index 7a1b57df3a6..731ff14e68a 100644
--- a/src/cascadia/TerminalControl/TermControl.cpp
+++ b/src/cascadia/TerminalControl/TermControl.cpp
@@ -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 });
 
diff --git a/src/cascadia/TerminalCore/Terminal.cpp b/src/cascadia/TerminalCore/Terminal.cpp
index d41e4f53b65..f8dce231a3a 100644
--- a/src/cascadia/TerminalCore/Terminal.cpp
+++ b/src/cascadia/TerminalCore/Terminal.cpp
@@ -46,6 +46,7 @@ Terminal::Terminal() :
     _altGrAliasing{ true },
     _blockSelection{ false },
     _selectionMode{ SelectionInteractionMode::None },
+    _isTargetingUrl{ false },
     _selection{ std::nullopt },
     _selectionEndpoint{ static_cast<SelectionEndpoint>(0) },
     _anchorInactiveSelectionEndpoint{ false },
@@ -563,17 +564,24 @@ bool Terminal::ShouldSendAlternateScroll(const unsigned int uiButton,
 // Method Description:
 // - Given a coord, get the URI at that location
 // Arguments:
-// - The position
-std::wstring Terminal::GetHyperlinkAtPosition(const til::point position)
+// - The position relative to the viewport
+std::wstring Terminal::GetHyperlinkAtViewportPosition(const til::point viewportPos)
 {
-    auto attr = _activeBuffer().GetCellDataAt(_ConvertToBufferCell(position))->TextAttr();
+    return GetHyperlinkAtBufferPosition(_ConvertToBufferCell(viewportPos));
+}
+
+std::wstring Terminal::GetHyperlinkAtBufferPosition(const til::point bufferPos)
+{
+    auto attr = _activeBuffer().GetCellDataAt(bufferPos)->TextAttr();
     if (attr.IsHyperlink())
     {
         auto uri = _activeBuffer().GetHyperlinkUriFromId(attr.GetHyperlinkId());
         return uri;
     }
     // also look through our known pattern locations in our pattern interval tree
-    const auto result = GetHyperlinkIntervalFromPosition(position);
+    auto viewportPos = bufferPos;
+    _GetVisibleViewport().ConvertToOrigin(&viewportPos);
+    const auto result = GetHyperlinkIntervalFromViewportPosition(viewportPos);
     if (result.has_value() && result->value == _hyperlinkPatternId)
     {
         const auto start = result->start;
@@ -594,23 +602,23 @@ std::wstring Terminal::GetHyperlinkAtPosition(const til::point position)
 // Method Description:
 // - Gets the hyperlink ID of the text at the given terminal position
 // Arguments:
-// - The position of the text
+// - The position of the text relative to the viewport
 // Return value:
 // - The hyperlink ID
-uint16_t Terminal::GetHyperlinkIdAtPosition(const til::point position)
+uint16_t Terminal::GetHyperlinkIdAtViewportPosition(const til::point viewportPos)
 {
-    return _activeBuffer().GetCellDataAt(_ConvertToBufferCell(position))->TextAttr().GetHyperlinkId();
+    return _activeBuffer().GetCellDataAt(_ConvertToBufferCell(viewportPos))->TextAttr().GetHyperlinkId();
 }
 
 // Method description:
 // - Given a position in a URI pattern, gets the start and end coordinates of the URI
 // Arguments:
-// - The position
+// - The position relative to the viewport
 // Return value:
 // - The interval representing the start and end coordinates
-std::optional<PointTree::interval> Terminal::GetHyperlinkIntervalFromPosition(const til::point position)
+std::optional<PointTree::interval> Terminal::GetHyperlinkIntervalFromViewportPosition(const til::point viewportPos)
 {
-    const auto results = _patternIntervalTree.findOverlapping({ position.X + 1, position.Y }, position);
+    const auto results = _patternIntervalTree.findOverlapping({ viewportPos.X + 1, viewportPos.Y }, viewportPos);
     if (results.size() > 0)
     {
         for (const auto& result : results)
diff --git a/src/cascadia/TerminalCore/Terminal.hpp b/src/cascadia/TerminalCore/Terminal.hpp
index c63eb887b65..5dec2e85124 100644
--- a/src/cascadia/TerminalCore/Terminal.hpp
+++ b/src/cascadia/TerminalCore/Terminal.hpp
@@ -162,9 +162,10 @@ class Microsoft::Terminal::Core::Terminal final :
 
     void FocusChanged(const bool focused) noexcept override;
 
-    std::wstring GetHyperlinkAtPosition(const til::point position);
-    uint16_t GetHyperlinkIdAtPosition(const til::point position);
-    std::optional<interval_tree::IntervalTree<til::point, size_t>::interval> GetHyperlinkIntervalFromPosition(const til::point position);
+    std::wstring GetHyperlinkAtViewportPosition(const til::point viewportPos);
+    std::wstring GetHyperlinkAtBufferPosition(const til::point bufferPos);
+    uint16_t GetHyperlinkIdAtViewportPosition(const til::point viewportPos);
+    std::optional<interval_tree::IntervalTree<til::point, size_t>::interval> GetHyperlinkIntervalFromViewportPosition(const til::point viewportPos);
 #pragma endregion
 
 #pragma region IBaseData(base to IRenderData and IUiaData)
@@ -249,6 +250,12 @@ class Microsoft::Terminal::Core::Terminal final :
         Down
     };
 
+    enum class SearchDirection
+    {
+        Forward,
+        Backward
+    };
+
     enum class SelectionExpansion
     {
         Char,
@@ -273,6 +280,8 @@ class Microsoft::Terminal::Core::Terminal final :
     SelectionInteractionMode SelectionMode() const noexcept;
     void SwitchSelectionEndpoint();
     void ToggleMarkMode();
+    void SelectHyperlink(const SearchDirection dir);
+    bool IsTargetingUrl() const noexcept;
 
     using UpdateSelectionParams = std::optional<std::pair<SelectionDirection, SelectionExpansion>>;
     UpdateSelectionParams ConvertKeyEventToUpdateSelectionParams(const ControlKeyStates mods, const WORD vkey) const;
@@ -349,6 +358,7 @@ class Microsoft::Terminal::Core::Terminal final :
     std::wstring _wordDelimiters;
     SelectionExpansion _multiClickSelectionMode;
     SelectionInteractionMode _selectionMode;
+    bool _isTargetingUrl;
     SelectionEndpoint _selectionEndpoint;
     bool _anchorInactiveSelectionEndpoint;
 #pragma endregion
@@ -424,6 +434,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;
+    void _ScrollToPoint(const til::point pos);
     void _MoveByChar(SelectionDirection direction, til::point& pos);
     void _MoveByWord(SelectionDirection direction, til::point& pos);
     void _MoveByViewport(SelectionDirection direction, til::point& pos);
diff --git a/src/cascadia/TerminalCore/TerminalSelection.cpp b/src/cascadia/TerminalCore/TerminalSelection.cpp
index 007a26b0c3f..dd156b463c4 100644
--- a/src/cascadia/TerminalCore/TerminalSelection.cpp
+++ b/src/cascadia/TerminalCore/TerminalSelection.cpp
@@ -6,6 +6,7 @@
 #include "unicode.hpp"
 
 using namespace Microsoft::Terminal::Core;
+using namespace Microsoft::Console::Types;
 
 DEFINE_ENUM_FLAG_OPERATORS(Terminal::SelectionEndpoint);
 
@@ -303,8 +304,10 @@ void Terminal::ToggleMarkMode()
         _selection->start = cursorPos;
         _selection->end = cursorPos;
         _selection->pivot = cursorPos;
+        _ScrollToPoint(cursorPos);
         _selectionMode = SelectionInteractionMode::Mark;
         _blockSelection = false;
+        _isTargetingUrl = false;
         WI_SetAllFlags(_selectionEndpoint, SelectionEndpoint::Start | SelectionEndpoint::End);
     }
 }
@@ -336,6 +339,157 @@ void Terminal::SwitchSelectionEndpoint()
     }
 }
 
+// Method Description:
+// - selects the next/previous hyperlink, if one is available
+// Arguments:
+// - dir: the direction we're scanning the buffer in to find the hyperlink of interest
+// Return Value:
+// - true if we found a hyperlink to select (and selected it). False otherwise.
+void Terminal::SelectHyperlink(const SearchDirection dir)
+{
+    if (_selectionMode != SelectionInteractionMode::Mark)
+    {
+        // This feature only works in mark mode
+        _isTargetingUrl = false;
+        return;
+    }
+
+    // 0. Useful tools/vars
+    const auto bufferSize = _activeBuffer().GetSize();
+    const auto viewportHeight = _GetMutableViewport().Height();
+
+    // The patterns are stored relative to the "search area". Initially, this search area will be the viewport,
+    // but as we progressively search through more of the buffer, this will change.
+    // Keep track of the search area here, and use the lambda below to convert points to the search area coordinate space.
+    auto searchArea = _GetVisibleViewport();
+    auto convertToSearchArea = [&searchArea](const til::point pt) {
+        auto copy = pt;
+        searchArea.ConvertToOrigin(&copy);
+        return copy;
+    };
+
+    // extracts the next/previous hyperlink from the list of hyperlink ranges provided
+    auto extractResultFromList = [&](std::vector<interval_tree::Interval<til::point, size_t>>& list) {
+        const auto selectionStartInSearchArea = convertToSearchArea(_selection->start);
+
+        std::optional<std::pair<til::point, til::point>> resultFromList;
+        if (!list.empty())
+        {
+            if (dir == SearchDirection::Forward)
+            {
+                // pattern tree includes the currently selected range when going forward,
+                // so we need to check if we're pointing to that one before returning it.
+                auto range = list.front();
+                if (_isTargetingUrl && range.start == selectionStartInSearchArea)
+                {
+                    if (list.size() > 1)
+                    {
+                        // if we're pointing to the currently selected URL,
+                        // pick the next one.
+                        range = til::at(list, 1);
+                        resultFromList = { range.start, range.stop };
+                    }
+                    else
+                    {
+                        // LOAD-BEARING: the only range here is the one that's currently selected.
+                        // Make sure this is set to nullopt so that we keep searching through the buffer.
+                        resultFromList = std::nullopt;
+                    }
+                }
+                else
+                {
+                    // not on currently selected range, return the first one
+                    resultFromList = { range.start, range.stop };
+                }
+            }
+            else if (dir == SearchDirection::Backward)
+            {
+                // moving backwards excludes the currently selected range,
+                // simply return the last one in the list as it's ordered
+                const auto range = list.back();
+                resultFromList = { range.start, range.stop };
+            }
+        }
+
+        // pattern tree stores everything as viewport coords,
+        // so we need to convert them on the way out
+        if (resultFromList)
+        {
+            searchArea.ConvertFromOrigin(&resultFromList->first);
+            searchArea.ConvertFromOrigin(&resultFromList->second);
+        }
+        return resultFromList;
+    };
+
+    // 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;
+
+    // 1.A) Try searching the current viewport (no scrolling required)
+    auto resultList = _patternIntervalTree.findContained(convertToSearchArea(searchStart), convertToSearchArea(searchEnd));
+    std::optional<std::pair<til::point, til::point>> result = extractResultFromList(resultList);
+    if (!result)
+    {
+        // 1.B) Incrementally search through more of the space
+        if (dir == SearchDirection::Forward)
+        {
+            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()) };
+        }
+        searchArea = Viewport::FromDimensions(searchStart, searchEnd.x + 1, searchEnd.y + 1);
+
+        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)
+        {
+            auto patterns = _activeBuffer().GetPatterns(searchStart.y, searchEnd.y);
+            resultList = patterns.findContained(convertToSearchArea(searchStart), convertToSearchArea(searchEnd));
+            result = extractResultFromList(resultList);
+            if (!result)
+            {
+                if (dir == SearchDirection::Forward)
+                {
+                    searchStart.y += 1;
+                    searchEnd.y = std::min(searchStart.y + viewportHeight, ViewEndIndex());
+                }
+                else
+                {
+                    searchEnd.y -= 1;
+                    searchStart.y = std::max(searchEnd.y - viewportHeight, bufferSize.Top());
+                }
+                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
+    _selection->start = result->first;
+    _selection->pivot = result->first;
+    _selection->end = result->second;
+    bufferSize.DecrementInBounds(_selection->end);
+    _isTargetingUrl = true;
+    _selectionEndpoint = SelectionEndpoint::End;
+
+    // 3. Scroll to the selected area (if necessary)
+    _ScrollToPoint(_selection->end);
+}
+
+bool Terminal::IsTargetingUrl() const noexcept
+{
+    return _isTargetingUrl;
+}
+
 Terminal::UpdateSelectionParams Terminal::ConvertKeyEventToUpdateSelectionParams(const ControlKeyStates mods, const WORD vkey) const
 {
     if ((_selectionMode == SelectionInteractionMode::Mark || mods.IsShiftPressed()) && !mods.IsAltPressed())
@@ -437,6 +591,7 @@ void Terminal::UpdateSelection(SelectionDirection direction, SelectionExpansion
     }
 
     // 3. Actually modify the selection state
+    _isTargetingUrl = false;
     _selectionMode = std::max(_selectionMode, SelectionInteractionMode::Keyboard);
     if (shouldMoveBothEndpoints)
     {
@@ -459,22 +614,7 @@ void Terminal::UpdateSelection(SelectionDirection direction, SelectionExpansion
     }
 
     // 4. Scroll (if necessary)
-    if (const auto visibleViewport = _GetVisibleViewport(); !visibleViewport.IsInBounds(targetPos))
-    {
-        if (const auto amtAboveView = visibleViewport.Top() - targetPos.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 = targetPos.Y - visibleViewport.BottomInclusive();
-            _scrollOffset -= amtBelowView;
-        }
-        _NotifyScrollEvent();
-        _activeBuffer().TriggerScroll();
-    }
+    _ScrollToPoint(targetPos);
 }
 
 void Terminal::SelectAll()
@@ -630,6 +770,7 @@ void Terminal::ClearSelection()
 {
     _selection = std::nullopt;
     _selectionMode = SelectionInteractionMode::None;
+    _isTargetingUrl = false;
     _selectionEndpoint = static_cast<SelectionEndpoint>(0);
     _anchorInactiveSelectionEndpoint = false;
 }
@@ -674,6 +815,30 @@ til::point Terminal::_ConvertToBufferCell(const til::point viewportPos) const
     return bufferPos;
 }
 
+// Method Description:
+// - if necessary, scroll the viewport such that the given point is visible
+// Arguments:
+// - pos: a coordinate relative to the buffer (not viewport)
+void Terminal::_ScrollToPoint(const til::point pos)
+{
+    if (const auto visibleViewport = _GetVisibleViewport(); !visibleViewport.IsInBounds(pos))
+    {
+        if (const auto amtAboveView = visibleViewport.Top() - pos.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 = pos.Y - visibleViewport.BottomInclusive();
+            _scrollOffset -= amtBelowView;
+        }
+        _NotifyScrollEvent();
+        _activeBuffer().TriggerScroll();
+    }
+}
+
 // Method Description:
 // - This method won't be used. We just throw and do nothing. For now we
 //   need this method to implement UiaData interface
diff --git a/src/cascadia/TerminalSettingsModel/defaults.json b/src/cascadia/TerminalSettingsModel/defaults.json
index 3527bf125a7..706f21d9ea2 100644
--- a/src/cascadia/TerminalSettingsModel/defaults.json
+++ b/src/cascadia/TerminalSettingsModel/defaults.json
@@ -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" },


Test patch: