Skip to content

Commit

Permalink
Chunk Selection Expansion for Double/Triple Click Selection (#2184)
Browse files Browse the repository at this point in the history
Double/Triple click create a selection expanding beyond one cell. This PR makes it so that when you're dragging your mouse to expand the selection, you expand to the next delimiter defined by double/triple click.

So, double click expands by doubleClickDelimiter ranges. Triple click expands by line.

When you double/triple click, a word/line is selected. When you drag, that word/line will remain selected after the expansion occurs.

Closes #1933 

## Details
Rather than resizing the selection when the mouse event occurs, I figured I'd do what I did with wide glyph selection: expand at render time.

We needed an enum `multiClickSelectionMode` to keep track of which expansion mode we're in.

Minor modifications to `_ExpandDoubleClickSelection*(COORD)` had to be made so that we can re-use them. 

Actual expansion occurs in `_GetSelectionRects()`

## Validation Steps Performed
- generic double click test
  - `dir` or `ls`
  - double click a word
  - drag up
  - Works! ✔
- double click on delimiter test
  - `dir` or `ls`
  - double click a word delimiter (i.e.: space between words)
  - drag up
  - Works! ✔
- generic triple click test
  - `dir` or `ls`
  - triple click a line
  - drag up
  - Works! ✔
- ALT + double click test
  - `dir` or `ls`
  - hold ALT
  - double click a word
  - drag up
  - Works! ✔

repeat above tests in following scenarios:
- when at top of scrollback
- drag down instead of up
  • Loading branch information
carlos-zamora authored and DHowett committed Aug 14, 2019
1 parent 82de43b commit 1f41fd3
Show file tree
Hide file tree
Showing 6 changed files with 320 additions and 85 deletions.
14 changes: 11 additions & 3 deletions src/cascadia/TerminalCore/Terminal.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -164,14 +164,22 @@ class Microsoft::Terminal::Core::Terminal final :

bool _snapOnInput;

// Text Selection
#pragma region Text Selection
enum SelectionExpansionMode
{
Cell,
Word,
Line
};
COORD _selectionAnchor;
COORD _endSelectionPosition;
bool _boxSelection;
bool _selectionActive;
SHORT _selectionAnchor_YOffset;
SHORT _endSelectionPosition_YOffset;
std::wstring _wordDelimiters;
SelectionExpansionMode _multiClickSelectionMode;
#pragma endregion

std::shared_mutex _readWriteLock;

Expand Down Expand Up @@ -214,8 +222,8 @@ class Microsoft::Terminal::Core::Terminal final :
std::vector<SMALL_RECT> _GetSelectionRects() const;
const SHORT _ExpandWideGlyphSelectionLeft(const SHORT xPos, const SHORT yPos) const;
const SHORT _ExpandWideGlyphSelectionRight(const SHORT xPos, const SHORT yPos) const;
void _ExpandDoubleClickSelectionLeft(const COORD position);
void _ExpandDoubleClickSelectionRight(const COORD position);
COORD _ExpandDoubleClickSelectionLeft(const COORD position) const;
COORD _ExpandDoubleClickSelectionRight(const COORD position) const;
const bool _isWordDelimiter(std::wstring_view cellChar) const;
const COORD _ConvertToBufferCell(const COORD viewportPos) const;
#pragma endregion
Expand Down
78 changes: 49 additions & 29 deletions src/cascadia/TerminalCore/TerminalSelection.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,27 @@ std::vector<SMALL_RECT> Terminal::_GetSelectionRects() const
selectionRow.Right = (row == lowerCoord.Y) ? lowerCoord.X : bufferSize.RightInclusive();
}

// expand selection for Double/Triple Click
if (_multiClickSelectionMode == SelectionExpansionMode::Word)
{
const auto cellChar = _buffer->GetCellDataAt(selectionAnchorWithOffset)->Chars();
if (_selectionAnchor == _endSelectionPosition && _isWordDelimiter(cellChar))
{
// only highlight the cell if you double click a delimiter
}
else
{
selectionRow.Left = _ExpandDoubleClickSelectionLeft({ selectionRow.Left, row }).X;
selectionRow.Right = _ExpandDoubleClickSelectionRight({ selectionRow.Right, row }).X;
}
}
else if (_multiClickSelectionMode == SelectionExpansionMode::Line)
{
selectionRow.Left = 0;
selectionRow.Right = bufferSize.RightInclusive();
}

// expand selection for Wide Glyphs
selectionRow.Left = _ExpandWideGlyphSelectionLeft(selectionRow.Left, row);
selectionRow.Right = _ExpandWideGlyphSelectionRight(selectionRow.Right, row);

Expand Down Expand Up @@ -142,16 +163,24 @@ void Terminal::DoubleClickSelection(const COORD position)
if (_isWordDelimiter(cellChar))
{
SetSelectionAnchor(position);
_multiClickSelectionMode = SelectionExpansionMode::Word;
return;
}

// scan leftwards until delimiter is found and
// set selection anchor to one right of that spot
_ExpandDoubleClickSelectionLeft(position);
_selectionAnchor = _ExpandDoubleClickSelectionLeft(positionWithOffsets);
THROW_IF_FAILED(ShortSub(_selectionAnchor.Y, gsl::narrow<SHORT>(_ViewStartIndex()), &_selectionAnchor.Y));
_selectionAnchor_YOffset = gsl::narrow<SHORT>(_ViewStartIndex());

// scan rightwards until delimiter is found and
// set endSelectionPosition to one left of that spot
_ExpandDoubleClickSelectionRight(position);
_endSelectionPosition = _ExpandDoubleClickSelectionRight(positionWithOffsets);
THROW_IF_FAILED(ShortSub(_endSelectionPosition.Y, gsl::narrow<SHORT>(_ViewStartIndex()), &_endSelectionPosition.Y));
_endSelectionPosition_YOffset = gsl::narrow<SHORT>(_ViewStartIndex());

_selectionActive = true;
_multiClickSelectionMode = SelectionExpansionMode::Word;
}

// Method Description:
Expand All @@ -162,6 +191,8 @@ void Terminal::TripleClickSelection(const COORD position)
{
SetSelectionAnchor({ 0, position.Y });
SetEndSelectionPosition({ _buffer->GetSize().RightInclusive(), position.Y });

_multiClickSelectionMode = SelectionExpansionMode::Line;
}

// Method Description:
Expand All @@ -181,6 +212,8 @@ void Terminal::SetSelectionAnchor(const COORD position)

_selectionActive = true;
SetEndSelectionPosition(position);

_multiClickSelectionMode = SelectionExpansionMode::Cell;
}

// Method Description:
Expand Down Expand Up @@ -253,16 +286,10 @@ const std::wstring Terminal::RetrieveSelectedTextFromBuffer(bool trimTrailingWhi
// Arguments:
// - position: viewport coordinate for selection
// Return Value:
// - update _selectionAnchor to new expanded location
void Terminal::_ExpandDoubleClickSelectionLeft(const COORD position)
// - updated copy of "position" to new expanded location (with vertical offset)
COORD Terminal::_ExpandDoubleClickSelectionLeft(const COORD position) const
{
// don't change the value if at/outside the boundary
if (position.X <= 0 || position.X >= _buffer->GetSize().RightInclusive())
{
return;
}

COORD positionWithOffsets = _ConvertToBufferCell(position);
COORD positionWithOffsets = position;
const auto bufferViewport = _buffer->GetSize();
auto cellChar = _buffer->GetCellDataAt(positionWithOffsets)->Chars();
while (positionWithOffsets.X != 0 && !_isWordDelimiter(cellChar))
Expand All @@ -271,33 +298,24 @@ void Terminal::_ExpandDoubleClickSelectionLeft(const COORD position)
cellChar = _buffer->GetCellDataAt(positionWithOffsets)->Chars();
}

if (positionWithOffsets.X != 0 || _isWordDelimiter(cellChar))
if (positionWithOffsets.X != 0 && _isWordDelimiter(cellChar))
{
// move off of delimiter to highlight properly
bufferViewport.IncrementInBounds(positionWithOffsets);
}

THROW_IF_FAILED(ShortSub(positionWithOffsets.Y, gsl::narrow<SHORT>(_ViewStartIndex()), &positionWithOffsets.Y));
_selectionAnchor = positionWithOffsets;
_selectionAnchor_YOffset = gsl::narrow<SHORT>(_ViewStartIndex());
_selectionActive = true;
return positionWithOffsets;
}

// Method Description:
// - expand the double click selection to the right (stopped by delimiter)
// Arguments:
// - position: viewport coordinate for selection
// Return Value:
// - update _endSelectionPosition to new expanded location
void Terminal::_ExpandDoubleClickSelectionRight(const COORD position)
// - updated copy of "position" to new expanded location (with vertical offset)
COORD Terminal::_ExpandDoubleClickSelectionRight(const COORD position) const
{
// don't change the value if at/outside the boundary
if (position.X <= 0 || position.X >= _buffer->GetSize().RightInclusive())
{
return;
}

COORD positionWithOffsets = _ConvertToBufferCell(position);
COORD positionWithOffsets = position;
const auto bufferViewport = _buffer->GetSize();
auto cellChar = _buffer->GetCellDataAt(positionWithOffsets)->Chars();
while (positionWithOffsets.X != _buffer->GetSize().RightInclusive() && !_isWordDelimiter(cellChar))
Expand All @@ -306,15 +324,13 @@ void Terminal::_ExpandDoubleClickSelectionRight(const COORD position)
cellChar = _buffer->GetCellDataAt(positionWithOffsets)->Chars();
}

if (positionWithOffsets.X != bufferViewport.RightInclusive() || _isWordDelimiter(cellChar))
if (positionWithOffsets.X != bufferViewport.RightInclusive() && _isWordDelimiter(cellChar))
{
// move off of delimiter to highlight properly
bufferViewport.DecrementInBounds(positionWithOffsets);
}

THROW_IF_FAILED(ShortSub(positionWithOffsets.Y, gsl::narrow<SHORT>(_ViewStartIndex()), &positionWithOffsets.Y));
_endSelectionPosition = positionWithOffsets;
_endSelectionPosition_YOffset = gsl::narrow<SHORT>(_ViewStartIndex());
return positionWithOffsets;
}

// Method Description:
Expand All @@ -336,7 +352,11 @@ const bool Terminal::_isWordDelimiter(std::wstring_view cellChar) const
// - the corresponding location on the buffer
const COORD Terminal::_ConvertToBufferCell(const COORD viewportPos) const
{
// Force position to be valid
COORD positionWithOffsets = viewportPos;
positionWithOffsets.X = std::clamp(viewportPos.X, static_cast<SHORT>(0), _buffer->GetSize().RightInclusive());
positionWithOffsets.Y = std::clamp(viewportPos.Y, static_cast<SHORT>(0), _buffer->GetSize().BottomInclusive());

THROW_IF_FAILED(ShortSub(viewportPos.Y, gsl::narrow<SHORT>(_scrollOffset), &positionWithOffsets.Y));
THROW_IF_FAILED(ShortAdd(positionWithOffsets.Y, gsl::narrow<SHORT>(_ViewStartIndex()), &positionWithOffsets.Y));
return positionWithOffsets;
Expand Down
59 changes: 59 additions & 0 deletions src/cascadia/UnitTests_TerminalCore/MockTermSettings.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#pragma once

#include "precomp.h"
#include <WexTestClass.h>

#include "DefaultSettings.h"

#include "winrt/Microsoft.Terminal.Settings.h"

using namespace winrt::Microsoft::Terminal::Settings;

namespace TerminalCoreUnitTests
{
class MockTermSettings : public winrt::implements<MockTermSettings, ICoreSettings>
{
public:
MockTermSettings(int32_t historySize, int32_t initialRows, int32_t initialCols) :
_historySize(historySize),
_initialRows(initialRows),
_initialCols(initialCols)
{
}

// property getters - all implemented
int32_t HistorySize() { return _historySize; }
int32_t InitialRows() { return _initialRows; }
int32_t InitialCols() { return _initialCols; }
uint32_t DefaultForeground() { return COLOR_WHITE; }
uint32_t DefaultBackground() { return COLOR_BLACK; }
bool SnapOnInput() { return false; }
uint32_t CursorColor() { return COLOR_WHITE; }
CursorStyle CursorShape() const noexcept { return CursorStyle::Vintage; }
uint32_t CursorHeight() { return 42UL; }
winrt::hstring WordDelimiters() { return winrt::to_hstring(DEFAULT_WORD_DELIMITERS.c_str()); }

// other implemented methods
uint32_t GetColorTableEntry(int32_t) const { return 123; }

// property setters - all unimplemented
void HistorySize(int32_t) {}
void InitialRows(int32_t) {}
void InitialCols(int32_t) {}
void DefaultForeground(uint32_t) {}
void DefaultBackground(uint32_t) {}
void SnapOnInput(bool) {}
void CursorColor(uint32_t) {}
void CursorShape(CursorStyle const&) noexcept {}
void CursorHeight(uint32_t) {}
void WordDelimiters(winrt::hstring) {}

// other unimplemented methods
void SetColorTableEntry(int32_t /* index */, uint32_t /* value */) {}

private:
int32_t _historySize;
int32_t _initialRows;
int32_t _initialCols;
};
}
50 changes: 1 addition & 49 deletions src/cascadia/UnitTests_TerminalCore/ScreenSizeLimitsTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,64 +4,16 @@
#include "precomp.h"
#include <WexTestClass.h>

#include "DefaultSettings.h"
#include "../cascadia/TerminalCore/Terminal.hpp"
#include "MockTermSettings.h"
#include "../renderer/inc/DummyRenderTarget.hpp"
#include "consoletaeftemplates.hpp"

#include "winrt/Microsoft.Terminal.Settings.h"

using namespace winrt::Microsoft::Terminal::Settings;
using namespace Microsoft::Terminal::Core;

namespace TerminalCoreUnitTests
{
class MockTermSettings : public winrt::implements<MockTermSettings, ICoreSettings>
{
public:
MockTermSettings(int32_t historySize, int32_t initialRows, int32_t initialCols) :
_historySize(historySize),
_initialRows(initialRows),
_initialCols(initialCols)
{
}

// property getters - all implemented
int32_t HistorySize() { return _historySize; }
int32_t InitialRows() { return _initialRows; }
int32_t InitialCols() { return _initialCols; }
uint32_t DefaultForeground() { return COLOR_WHITE; }
uint32_t DefaultBackground() { return COLOR_BLACK; }
bool SnapOnInput() { return false; }
uint32_t CursorColor() { return COLOR_WHITE; }
CursorStyle CursorShape() const noexcept { return CursorStyle::Vintage; }
uint32_t CursorHeight() { return 42UL; }
winrt::hstring WordDelimiters() { return winrt::to_hstring(DEFAULT_WORD_DELIMITERS.c_str()); }

// other implemented methods
uint32_t GetColorTableEntry(int32_t) const { return 123; }

// property setters - all unimplemented
void HistorySize(int32_t) {}
void InitialRows(int32_t) {}
void InitialCols(int32_t) {}
void DefaultForeground(uint32_t) {}
void DefaultBackground(uint32_t) {}
void SnapOnInput(bool) {}
void CursorColor(uint32_t) {}
void CursorShape(CursorStyle const&) noexcept {}
void CursorHeight(uint32_t) {}
void WordDelimiters(winrt::hstring) {}

// other unimplemented methods
void SetColorTableEntry(int32_t /* index */, uint32_t /* value */) {}

private:
int32_t _historySize;
int32_t _initialRows;
int32_t _initialCols;
};

#define WCS(x) WCSHELPER(x)
#define WCSHELPER(x) L#x

Expand Down
Loading

0 comments on commit 1f41fd3

Please sign in to comment.