Repository: microsoft/terminal (language: cpp)

Issue:
Mark Mode: Toggle instead of holding shift
Have you considered following tmux's implementation of this feature?  It has the benefit of being well-known, and it won't step on tmux's own implementation as long as the key combination to activate it doesn't coincide with tmux's

Here's a little guide that explains how that works:
http://www.rushiagr.com/blog/2016/06/16/everything-you-need-to-know-about-tmux-copy-pasting-ubuntu/

_Originally posted by @fpqc in https://github.com/microsoft/terminal/pull/2840_

The main idea here (I think) is rather than holding the shift key to switch anchors, you just toggle which selection anchor you're moving.

Off the top of my head, I'm not sure how to make it not conflict with the existing implementation of mark mode, but I'm interested 😊


Discussion / hints:
This should be a part of the #4993 scenario.

Fix patch:
diff --git a/doc/cascadia/profiles.schema.json b/doc/cascadia/profiles.schema.json
index 2179a4c294f..54ec07c895c 100644
--- a/doc/cascadia/profiles.schema.json
+++ b/doc/cascadia/profiles.schema.json
@@ -354,6 +354,7 @@
         "toggleFocusMode",
         "selectAll",
         "setFocusMode",
+        "switchSelectionEndpoint",
         "toggleFullscreen",
         "setFullScreen",
         "setMaximized",
diff --git a/src/cascadia/TerminalApp/AppActionHandlers.cpp b/src/cascadia/TerminalApp/AppActionHandlers.cpp
index 5b035f38ebe..10a7f8cfa31 100644
--- a/src/cascadia/TerminalApp/AppActionHandlers.cpp
+++ b/src/cascadia/TerminalApp/AppActionHandlers.cpp
@@ -1112,4 +1112,14 @@ namespace winrt::TerminalApp::implementation
             args.Handled(handled);
         }
     }
+
+    void TerminalPage::_HandleSwitchSelectionEndpoint(const IInspectable& /*sender*/,
+                                                      const ActionEventArgs& args)
+    {
+        if (const auto& control{ _GetActiveControl() })
+        {
+            const auto handled = control.SwitchSelectionEndpoint();
+            args.Handled(handled);
+        }
+    }
 }
diff --git a/src/cascadia/TerminalControl/ControlCore.cpp b/src/cascadia/TerminalControl/ControlCore.cpp
index 3472bf1016e..d60f90bd380 100644
--- a/src/cascadia/TerminalControl/ControlCore.cpp
+++ b/src/cascadia/TerminalControl/ControlCore.cpp
@@ -1089,6 +1089,16 @@ namespace winrt::Microsoft::Terminal::Control::implementation
         return static_cast<Control::SelectionInteractionMode>(_terminal->SelectionMode());
     }
 
+    bool ControlCore::SwitchSelectionEndpoint()
+    {
+        if (_terminal->IsSelectionActive())
+        {
+            _terminal->SwitchSelectionEndpoint();
+            return true;
+        }
+        return false;
+    }
+
     // Method Description:
     // - Pre-process text pasted (presumably from the clipboard)
     //   before sending it over the terminal's connection.
diff --git a/src/cascadia/TerminalControl/ControlCore.h b/src/cascadia/TerminalControl/ControlCore.h
index 5f021c10a07..4208798e10d 100644
--- a/src/cascadia/TerminalControl/ControlCore.h
+++ b/src/cascadia/TerminalControl/ControlCore.h
@@ -86,6 +86,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
         bool ToggleBlockSelection();
         void ToggleMarkMode();
         Control::SelectionInteractionMode SelectionMode() const;
+        bool SwitchSelectionEndpoint();
 
         void GotFocus();
         void LostFocus();
diff --git a/src/cascadia/TerminalControl/ControlCore.idl b/src/cascadia/TerminalControl/ControlCore.idl
index a1d348d8a26..1d7801bdad4 100644
--- a/src/cascadia/TerminalControl/ControlCore.idl
+++ b/src/cascadia/TerminalControl/ControlCore.idl
@@ -92,6 +92,7 @@ namespace Microsoft.Terminal.Control
         void ClearSelection();
         Boolean ToggleBlockSelection();
         void ToggleMarkMode();
+        Boolean SwitchSelectionEndpoint();
         void ClearBuffer(ClearBufferType clearType);
 
         void SetHoveredCell(Microsoft.Terminal.Core.Point terminalPosition);
diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp
index 1e5dcdc9f1a..7a1b57df3a6 100644
--- a/src/cascadia/TerminalControl/TermControl.cpp
+++ b/src/cascadia/TerminalControl/TermControl.cpp
@@ -1929,6 +1929,11 @@ namespace winrt::Microsoft::Terminal::Control::implementation
         _core.ToggleMarkMode();
     }
 
+    bool TermControl::SwitchSelectionEndpoint()
+    {
+        return _core.SwitchSelectionEndpoint();
+    }
+
     void TermControl::Close()
     {
         if (!_IsClosing())
diff --git a/src/cascadia/TerminalControl/TermControl.h b/src/cascadia/TerminalControl/TermControl.h
index 0bde6b53fb0..e67f7c773ce 100644
--- a/src/cascadia/TerminalControl/TermControl.h
+++ b/src/cascadia/TerminalControl/TermControl.h
@@ -40,6 +40,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
         void SelectAll();
         bool ToggleBlockSelection();
         void ToggleMarkMode();
+        bool SwitchSelectionEndpoint();
         void Close();
         Windows::Foundation::Size CharacterDimensions() const;
         Windows::Foundation::Size MinimumSize();
diff --git a/src/cascadia/TerminalControl/TermControl.idl b/src/cascadia/TerminalControl/TermControl.idl
index f2aadf16934..3d8cde2714e 100644
--- a/src/cascadia/TerminalControl/TermControl.idl
+++ b/src/cascadia/TerminalControl/TermControl.idl
@@ -53,6 +53,7 @@ namespace Microsoft.Terminal.Control
         void SelectAll();
         Boolean ToggleBlockSelection();
         void ToggleMarkMode();
+        Boolean SwitchSelectionEndpoint();
         void ClearBuffer(ClearBufferType clearType);
         void Close();
         Windows.Foundation.Size CharacterDimensions { get; };
diff --git a/src/cascadia/TerminalCore/Terminal.cpp b/src/cascadia/TerminalCore/Terminal.cpp
index 2e78808f8e2..d41e4f53b65 100644
--- a/src/cascadia/TerminalCore/Terminal.cpp
+++ b/src/cascadia/TerminalCore/Terminal.cpp
@@ -48,6 +48,7 @@ Terminal::Terminal() :
     _selectionMode{ SelectionInteractionMode::None },
     _selection{ std::nullopt },
     _selectionEndpoint{ static_cast<SelectionEndpoint>(0) },
+    _anchorInactiveSelectionEndpoint{ false },
     _taskbarState{ 0 },
     _taskbarProgress{ 0 },
     _trimBlockSelection{ false },
diff --git a/src/cascadia/TerminalCore/Terminal.hpp b/src/cascadia/TerminalCore/Terminal.hpp
index 7f08390cbd2..c63eb887b65 100644
--- a/src/cascadia/TerminalCore/Terminal.hpp
+++ b/src/cascadia/TerminalCore/Terminal.hpp
@@ -271,6 +271,7 @@ class Microsoft::Terminal::Core::Terminal final :
     void UpdateSelection(SelectionDirection direction, SelectionExpansion mode, ControlKeyStates mods);
     void SelectAll();
     SelectionInteractionMode SelectionMode() const noexcept;
+    void SwitchSelectionEndpoint();
     void ToggleMarkMode();
 
     using UpdateSelectionParams = std::optional<std::pair<SelectionDirection, SelectionExpansion>>;
@@ -349,6 +350,7 @@ class Microsoft::Terminal::Core::Terminal final :
     SelectionExpansion _multiClickSelectionMode;
     SelectionInteractionMode _selectionMode;
     SelectionEndpoint _selectionEndpoint;
+    bool _anchorInactiveSelectionEndpoint;
 #pragma endregion
 
     std::unique_ptr<TextBuffer> _mainBuffer;
diff --git a/src/cascadia/TerminalCore/TerminalSelection.cpp b/src/cascadia/TerminalCore/TerminalSelection.cpp
index b2626ec0245..007a26b0c3f 100644
--- a/src/cascadia/TerminalCore/TerminalSelection.cpp
+++ b/src/cascadia/TerminalCore/TerminalSelection.cpp
@@ -309,6 +309,33 @@ void Terminal::ToggleMarkMode()
     }
 }
 
+// Method Description:
+// - switch the targeted selection endpoint with the other one (i.e. start <--> end)
+void Terminal::SwitchSelectionEndpoint()
+{
+    if (IsSelectionActive())
+    {
+        if (WI_IsFlagSet(_selectionEndpoint, SelectionEndpoint::Start) && WI_IsFlagSet(_selectionEndpoint, SelectionEndpoint::End))
+        {
+            // moving cursor --> anchor start, move end
+            _selectionEndpoint = SelectionEndpoint::End;
+            _anchorInactiveSelectionEndpoint = true;
+        }
+        else if (WI_IsFlagSet(_selectionEndpoint, SelectionEndpoint::End))
+        {
+            // moving end --> now we're moving start
+            _selectionEndpoint = SelectionEndpoint::Start;
+            _selection->pivot = _selection->end;
+        }
+        else if (WI_IsFlagSet(_selectionEndpoint, SelectionEndpoint::Start))
+        {
+            // moving start --> now we're moving end
+            _selectionEndpoint = SelectionEndpoint::End;
+            _selection->pivot = _selection->start;
+        }
+    }
+}
+
 Terminal::UpdateSelectionParams Terminal::ConvertKeyEventToUpdateSelectionParams(const ControlKeyStates mods, const WORD vkey) const
 {
     if ((_selectionMode == SelectionInteractionMode::Mark || mods.IsShiftPressed()) && !mods.IsAltPressed())
@@ -365,6 +392,12 @@ Terminal::UpdateSelectionParams Terminal::ConvertKeyEventToUpdateSelectionParams
 // - mods: the key modifiers pressed when performing this update
 void Terminal::UpdateSelection(SelectionDirection direction, SelectionExpansion mode, ControlKeyStates mods)
 {
+    // This is a special variable used to track if we should move the cursor when in mark mode.
+    //   We have special functionality where if you use the "switchSelectionEndpoint" action
+    //   when in mark mode (moving the cursor), we anchor an endpoint and you can use the
+    //   plain arrow keys to move the endpoint. This way, you don't have to hold shift anymore!
+    const bool shouldMoveBothEndpoints = _selectionMode == SelectionInteractionMode::Mark && !_anchorInactiveSelectionEndpoint && !mods.IsShiftPressed();
+
     // 1. Figure out which endpoint to update
     // [Mark Mode]
     // - shift pressed --> only move "end" (or "start" if "pivot" == "end")
@@ -372,7 +405,7 @@ void Terminal::UpdateSelection(SelectionDirection direction, SelectionExpansion
     // [Quick Edit]
     // - just move "end" (or "start" if "pivot" == "end")
     _selectionEndpoint = static_cast<SelectionEndpoint>(0);
-    if (_selectionMode == SelectionInteractionMode::Mark && !mods.IsShiftPressed())
+    if (shouldMoveBothEndpoints)
     {
         WI_SetAllFlags(_selectionEndpoint, SelectionEndpoint::Start | SelectionEndpoint::End);
     }
@@ -405,7 +438,7 @@ void Terminal::UpdateSelection(SelectionDirection direction, SelectionExpansion
 
     // 3. Actually modify the selection state
     _selectionMode = std::max(_selectionMode, SelectionInteractionMode::Keyboard);
-    if (_selectionMode == SelectionInteractionMode::Mark && !mods.IsShiftPressed())
+    if (shouldMoveBothEndpoints)
     {
         // [Mark Mode] + shift unpressed --> move all three (i.e. just use arrow keys)
         _selection->start = targetPos;
@@ -598,6 +631,7 @@ void Terminal::ClearSelection()
     _selection = std::nullopt;
     _selectionMode = SelectionInteractionMode::None;
     _selectionEndpoint = static_cast<SelectionEndpoint>(0);
+    _anchorInactiveSelectionEndpoint = false;
 }
 
 // Method Description:
diff --git a/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp b/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp
index 1b49cc3dd4c..8591457e611 100644
--- a/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp
+++ b/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp
@@ -82,6 +82,7 @@ static constexpr std::string_view RestoreLastClosedKey{ "restoreLastClosed" };
 static constexpr std::string_view SelectAllKey{ "selectAll" };
 static constexpr std::string_view MarkModeKey{ "markMode" };
 static constexpr std::string_view ToggleBlockSelectionKey{ "toggleBlockSelection" };
+static constexpr std::string_view SwitchSelectionEndpointKey{ "switchSelectionEndpoint" };
 
 static constexpr std::string_view ActionKey{ "action" };
 
@@ -400,6 +401,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
                 { ShortcutAction::SelectAll, RS_(L"SelectAllCommandKey") },
                 { ShortcutAction::MarkMode, RS_(L"MarkModeCommandKey") },
                 { ShortcutAction::ToggleBlockSelection, RS_(L"ToggleBlockSelectionCommandKey") },
+                { ShortcutAction::SwitchSelectionEndpoint, RS_(L"SwitchSelectionEndpointCommandKey") },
             };
         }();
 
diff --git a/src/cascadia/TerminalSettingsModel/AllShortcutActions.h b/src/cascadia/TerminalSettingsModel/AllShortcutActions.h
index c9bd6bd2076..23d4cf2110c 100644
--- a/src/cascadia/TerminalSettingsModel/AllShortcutActions.h
+++ b/src/cascadia/TerminalSettingsModel/AllShortcutActions.h
@@ -95,7 +95,8 @@
     ON_ALL_ACTIONS(RestoreLastClosed)      \
     ON_ALL_ACTIONS(SelectAll)              \
     ON_ALL_ACTIONS(MarkMode)               \
-    ON_ALL_ACTIONS(ToggleBlockSelection)
+    ON_ALL_ACTIONS(ToggleBlockSelection)   \
+    ON_ALL_ACTIONS(SwitchSelectionEndpoint)
 
 #define ALL_SHORTCUT_ACTIONS_WITH_ARGS             \
     ON_ALL_ACTIONS_WITH_ARGS(AdjustFontSize)       \
diff --git a/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw b/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw
index e71dcf94a0e..ab0befb1533 100644
--- a/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw
+++ b/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw
@@ -564,4 +564,7 @@
   <data name="ToggleBlockSelectionCommandKey" xml:space="preserve">
     <value>Toggle block selection</value>
   </data>
+  <data name="SwitchSelectionEndpointCommandKey" xml:space="preserve">
+    <value>Switch selection endpoint</value>
+  </data>
 </root>
diff --git a/src/cascadia/TerminalSettingsModel/defaults.json b/src/cascadia/TerminalSettingsModel/defaults.json
index 6067aa6130e..3527bf125a7 100644
--- a/src/cascadia/TerminalSettingsModel/defaults.json
+++ b/src/cascadia/TerminalSettingsModel/defaults.json
@@ -388,6 +388,7 @@
         { "command": "selectAll", "keys": "ctrl+shift+a" },
         { "command": "markMode", "keys": "ctrl+shift+m" },
         { "command": "toggleBlockSelection" },
+        { "command": "switchSelectionEndpoint" },
 
         // Scrollback
         { "command": "scrollDown", "keys": "ctrl+shift+down" },


Test patch: