Skip to content

Commit

Permalink
Add an experimental setting for moving the cursor with the mouse (#15758
Browse files Browse the repository at this point in the history
)

## Summary of the Pull Request

This adds a new experimental per-setting to the terminal. 

```ts
"experimental.repositionCursorWithMouse": bool
```

When:
* the setting is on 
* AND you turn on shell integration (at least `133;B`)
* AND you click is somewhere _after_ the "active command" mark

we'll send a number of simulated keystrokes to the terminal based off
the number of cells between the place clicked and where the current
mouse cursor is.


## PR Checklist
- [ ] Related to #8573. I'm not marking as _closed_, because we should
probably polish this before we close that out. This is more a place to
start.

## Detailed Description of the Pull Request / Additional comments

There was a LOT of discussion in #8573. This is kinda a best effort
feature - it won't always work, but it should improve the experience
_most of the time_. We all kinda agreed that as much as the shell
probably should be responsible for doing this, there's myriad reasons
that won't work in practicality:
* That would also disable selection made by the terminal. That's a hard
sell.
* We'd need to invent some new mouse mode to support
click-to-reposition-but-drags-to-select-I-don't-want
* We'd then need shells to adopt that functionality.

And eventually settled that this was the least horrifying comprimise.

This has _e d g e  c a s e s_: 
* Does it work for wrapped lines? Well, kinda okay actually.
* Does it work for `vim`/`emacs`? Nope. 
* Does it work for emoji/wide glyphs? I wouldn't expect it to! I mean,
emoji input is messed up anyways, right?
* Other characters like `ESC` (which are rendered by the shell as two
cells "^[")? Nope.
* Does it do selections? Nope.
* Clicking across lines with continuation prompts? Nope.
* Tabs? Nope.
* Wraps within tmux/screen? Nope.


https://github.com/xtermjs/xterm.js/blob/master/src/browser/input/MoveToCell.ts
has probably a more complete implementation of how we'd want to generate
the keypresses and such.
  • Loading branch information
zadjii-msft committed Aug 14, 2023
1 parent a0c88bb commit b556594
Show file tree
Hide file tree
Showing 10 changed files with 171 additions and 2 deletions.
21 changes: 21 additions & 0 deletions src/buffer/out/textBuffer.cpp
Expand Up @@ -246,6 +246,27 @@ til::CoordType TextBuffer::TotalRowCount() const noexcept
return _height;
}

// Method Description:
// - Gets the number of glyphs in the buffer between two points.
// - IMPORTANT: Make sure that start is before end, or this will never return!
// Arguments:
// - start - The starting point of the range to get the glyph count for.
// - end - The ending point of the range to get the glyph count for.
// Return Value:
// - The number of glyphs in the buffer between the two points.
size_t TextBuffer::GetCellDistance(const til::point from, const til::point to) const
{
auto startCell = GetCellDataAt(from);
const auto endCell = GetCellDataAt(to);
auto delta = 0;
while (startCell != endCell)
{
++startCell;
++delta;
}
return delta;
}

// Routine Description:
// - Retrieves read-only text iterator at the given buffer location
// Arguments:
Expand Down
2 changes: 2 additions & 0 deletions src/buffer/out/textBuffer.hpp
Expand Up @@ -131,6 +131,8 @@ class TextBuffer final
TextBufferTextIterator GetTextLineDataAt(const til::point at) const;
TextBufferTextIterator GetTextDataAt(const til::point at, const Microsoft::Console::Types::Viewport limit) const;

size_t GetCellDistance(const til::point from, const til::point to) const;

static size_t GraphemeNext(const std::wstring_view& chars, size_t position) noexcept;
static size_t GraphemePrev(const std::wstring_view& chars, size_t position) noexcept;

Expand Down
67 changes: 67 additions & 0 deletions src/cascadia/TerminalControl/ControlCore.cpp
Expand Up @@ -1754,6 +1754,73 @@ namespace winrt::Microsoft::Terminal::Control::implementation
_terminal->MultiClickSelection(terminalPosition, mode);
selectionNeedsToBeCopied = true;
}
else if (_settings->RepositionCursorWithMouse()) // This is also mode==Char && !shiftEnabled
{
// If we're handling a single left click, without shift pressed, and
// outside mouse mode, AND the user has RepositionCursorWithMouse turned
// on, let's try to move the cursor.
//
// We'll only move the cursor if the user has clicked after the last
// mark, if there is one. That means the user also needs to set up
// shell integration to enable this feature.
//
// As noted in GH #8573, there's plenty of edge cases with this
// approach, but it's good enough to bring value to 90% of use cases.
const auto cursorPos{ _terminal->GetCursorPosition() };

// Does the current buffer line have a mark on it?
const auto& marks{ _terminal->GetScrollMarks() };
if (!marks.empty())
{
const auto& last{ marks.back() };
const auto [start, end] = last.GetExtent();
const auto lastNonSpace = _terminal->GetTextBuffer().GetLastNonSpaceCharacter();

// If the user clicked off to the right side of the prompt, we
// want to send keystrokes to the last character in the prompt +1.
//
// We don't want to send too many here. In CMD, if the user's
// last command is longer than what they've currently typed, and
// they press right arrow at the end of the prompt, COOKED_READ
// will fill in characters from the previous command.
//
// By only sending keypresses to the end of the command + 1, we
// should leave the cursor at the very end of the prompt,
// without adding any characters from a previous command.
auto clampedClick = terminalPosition;
if (terminalPosition > lastNonSpace)
{
clampedClick = lastNonSpace + til::point{ 1, 0 };
_terminal->GetTextBuffer().GetSize().Clamp(clampedClick);
}

if (clampedClick >= end)
{
// Get the distance between the cursor and the click, in cells.
const auto bufferSize = _terminal->GetTextBuffer().GetSize();

// First, make sure to iterate from the first point to the
// second. The user may have clicked _earlier_ in the
// buffer!
auto goRight = clampedClick > cursorPos;
const auto startPoint = goRight ? cursorPos : clampedClick;
const auto endPoint = goRight ? clampedClick : cursorPos;

const auto delta = _terminal->GetTextBuffer().GetCellDistance(startPoint, endPoint);

const WORD key = goRight ? VK_RIGHT : VK_LEFT;
// Send an up and a down once per cell. This won't
// accurately handle wide characters, or continuation
// prompts, or cases where a single escape character in the
// command (e.g. ^[) takes up two cells.
for (size_t i = 0u; i < delta; i++)
{
_terminal->SendKeyEvent(key, 0, {}, true);
_terminal->SendKeyEvent(key, 0, {}, false);
}
}
}
}
_updateSelectionUI();
}

Expand Down
1 change: 1 addition & 0 deletions src/cascadia/TerminalControl/IControlSettings.idl
Expand Up @@ -61,5 +61,6 @@ namespace Microsoft.Terminal.Control
Boolean ShowMarks { get; };
Boolean UseBackgroundImageForWindow { get; };
Boolean RightClickContextMenu { get; };
Boolean RepositionCursorWithMouse { get; };
};
}
9 changes: 8 additions & 1 deletion src/cascadia/TerminalSettingsModel/MTSMSettings.h
Expand Up @@ -69,6 +69,12 @@ Author(s):
X(bool, IsolatedMode, "compatibility.isolatedMode", false) \
X(hstring, SearchWebDefaultQueryUrl, "searchWebDefaultQueryUrl", L"https://www.bing.com/search?q=%22%s%22")

// Also add these settings to:
// * Profile.idl
// * TerminalSettings.h
// * TerminalSettings.cpp: TerminalSettings::_ApplyProfileSettings
// * IControlSettings.idl or ICoreSettings.idl
// * ControlProperties.h
#define MTSM_PROFILE_SETTINGS(X) \
X(int32_t, HistorySize, "historySize", DEFAULT_HISTORY_SIZE) \
X(bool, SnapOnInput, "snapOnInput", true) \
Expand All @@ -91,7 +97,8 @@ Author(s):
X(bool, Elevate, "elevate", false) \
X(bool, VtPassthrough, "experimental.connection.passthroughMode", false) \
X(bool, AutoMarkPrompts, "experimental.autoMarkPrompts", false) \
X(bool, ShowMarks, "experimental.showMarksOnScrollbar", false)
X(bool, ShowMarks, "experimental.showMarksOnScrollbar", false) \
X(bool, RepositionCursorWithMouse, "experimental.repositionCursorWithMouse", false)

// Intentionally omitted Profile settings:
// * Name
Expand Down
1 change: 1 addition & 0 deletions src/cascadia/TerminalSettingsModel/Profile.idl
Expand Up @@ -94,5 +94,6 @@ namespace Microsoft.Terminal.Settings.Model
INHERITABLE_PROFILE_SETTING(Boolean, ShowMarks);

INHERITABLE_PROFILE_SETTING(Boolean, RightClickContextMenu);
INHERITABLE_PROFILE_SETTING(Boolean, RepositionCursorWithMouse);
}
}
2 changes: 2 additions & 0 deletions src/cascadia/TerminalSettingsModel/TerminalSettings.cpp
Expand Up @@ -325,6 +325,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
_ShowMarks = Feature_ScrollbarMarks::IsEnabled() && profile.ShowMarks();

_RightClickContextMenu = profile.RightClickContextMenu();

_RepositionCursorWithMouse = profile.RepositionCursorWithMouse();
}

// Method Description:
Expand Down
1 change: 1 addition & 0 deletions src/cascadia/TerminalSettingsModel/TerminalSettings.h
Expand Up @@ -164,6 +164,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
INHERITABLE_SETTING(Model::TerminalSettings, bool, AutoMarkPrompts, false);
INHERITABLE_SETTING(Model::TerminalSettings, bool, ShowMarks, false);
INHERITABLE_SETTING(Model::TerminalSettings, bool, RightClickContextMenu, false);
INHERITABLE_SETTING(Model::TerminalSettings, bool, RepositionCursorWithMouse, false);

private:
std::optional<std::array<Microsoft::Terminal::Core::Color, COLOR_TABLE_SIZE>> _ColorTable;
Expand Down
66 changes: 66 additions & 0 deletions src/cascadia/UnitTests_Control/ControlCoreTests.cpp
Expand Up @@ -41,6 +41,8 @@ namespace ControlUnitTests
TEST_METHOD(TestSelectCommandSimple);
TEST_METHOD(TestSelectOutputSimple);

TEST_METHOD(TestSimpleClickSelection);

TEST_CLASS_SETUP(ModuleSetup)
{
winrt::init_apartment(winrt::apartment_type::single_threaded);
Expand Down Expand Up @@ -499,4 +501,68 @@ namespace ControlUnitTests
VERIFY_ARE_EQUAL(expectedEnd, end);
}
}

void ControlCoreTests::TestSimpleClickSelection()
{
// Create a simple selection with the mouse, then click somewhere else,
// and confirm the selection got updated.

auto [settings, conn] = _createSettingsAndConnection();
Log::Comment(L"Create ControlCore object");
auto core = createCore(*settings, *conn);
VERIFY_IS_NOT_NULL(core);
_standardInit(core);

// Here, we're using the UpdateSelectionMarkers as a stand-in to check
// if the selection got updated with the renderer. Standing up a whole
// dummy renderer for this test would be not very ergonomic. Instead, we
// are relying on ControlCore::_updateSelectionUI both
// TriggerSelection()'ing and also rasing this event
bool expectedSelectionUpdate = false;
bool gotSelectionUpdate = false;
core->UpdateSelectionMarkers([&](auto&& /*sender*/, auto&& /*args*/) {
VERIFY_IS_TRUE(expectedSelectionUpdate);
expectedSelectionUpdate = false;
gotSelectionUpdate = true;
});

auto needToCopy = false;
expectedSelectionUpdate = true;
core->LeftClickOnTerminal(til::point{ 1, 1 },
1,
false,
true,
false,
needToCopy);

VERIFY_IS_TRUE(core->HasSelection());
{
const auto& start = core->_terminal->GetSelectionAnchor();
const auto& end = core->_terminal->GetSelectionEnd();
const til::point expectedStart{ 1, 1 };
const til::point expectedEnd{ 1, 1 };
VERIFY_ARE_EQUAL(expectedStart, start);
VERIFY_ARE_EQUAL(expectedEnd, end);
}
VERIFY_IS_TRUE(gotSelectionUpdate);

expectedSelectionUpdate = true;
core->LeftClickOnTerminal(til::point{ 1, 2 },
1,
false,
true,
false,
needToCopy);

VERIFY_IS_TRUE(core->HasSelection());
{
const auto& start = core->_terminal->GetSelectionAnchor();
const auto& end = core->_terminal->GetSelectionEnd();
const til::point expectedStart{ 1, 1 };
const til::point expectedEnd{ 1, 2 };
VERIFY_ARE_EQUAL(expectedStart, start);
VERIFY_ARE_EQUAL(expectedEnd, end);
}
VERIFY_IS_TRUE(gotSelectionUpdate);
}
}
3 changes: 2 additions & 1 deletion src/cascadia/inc/ControlProperties.h
Expand Up @@ -47,7 +47,8 @@
X(winrt::hstring, StartingTitle) \
X(bool, DetectURLs, true) \
X(bool, VtPassthrough, false) \
X(bool, AutoMarkPrompts)
X(bool, AutoMarkPrompts) \
X(bool, RepositionCursorWithMouse, false)

// --------------------------- Control Settings ---------------------------
// All of these settings are defined in IControlSettings.
Expand Down

0 comments on commit b556594

Please sign in to comment.