terminal/src/host/ut_host/SelectionTests.cpp

624 lines
25 KiB
C++

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "precomp.h"
#include "WexTestClass.h"
#include "../../inc/consoletaeftemplates.hpp"
#include "CommonState.hpp"
#include "globals.h"
#include "selection.hpp"
#include "cmdline.h"
#include "../interactivity/inc/ServiceLocator.hpp"
using namespace WEX::Common;
using namespace WEX::Logging;
using namespace WEX::TestExecution;
using Microsoft::Console::Interactivity::ServiceLocator;
class SelectionTests
{
TEST_CLASS(SelectionTests);
CommonState* m_state;
Selection* m_pSelection;
TEST_CLASS_SETUP(ClassSetup)
{
m_state = new CommonState();
m_state->PrepareGlobalFont();
m_state->PrepareGlobalScreenBuffer();
m_pSelection = &Selection::Instance();
return true;
}
TEST_CLASS_CLEANUP(ClassCleanup)
{
m_pSelection = nullptr;
m_state->CleanupGlobalScreenBuffer();
m_state->CleanupGlobalFont();
delete m_state;
return true;
}
TEST_METHOD_SETUP(MethodSetup)
{
return true;
}
TEST_METHOD_CLEANUP(MethodCleanup)
{
return true;
}
void VerifyGetSelectionRects_BoxMode()
{
const auto selectionRects = m_pSelection->GetSelectionRects();
const UINT cRectanglesExpected = m_pSelection->_srSelectionRect.Bottom - m_pSelection->_srSelectionRect.Top + 1;
if (VERIFY_ARE_EQUAL(cRectanglesExpected, selectionRects.size()))
{
for (auto iRect = 0; iRect < gsl::narrow<int>(selectionRects.size()); iRect++)
{
// ensure each rectangle is exactly the width requested (block selection)
const SMALL_RECT* const psrRect = &selectionRects[iRect];
const short sRectangleLineNumber = (short)iRect + m_pSelection->_srSelectionRect.Top;
VERIFY_ARE_EQUAL(psrRect->Top, sRectangleLineNumber);
VERIFY_ARE_EQUAL(psrRect->Bottom, sRectangleLineNumber);
VERIFY_ARE_EQUAL(psrRect->Left, m_pSelection->_srSelectionRect.Left);
VERIFY_ARE_EQUAL(psrRect->Right, m_pSelection->_srSelectionRect.Right);
}
}
}
TEST_METHOD(TestGetSelectionRects_BoxMode)
{
m_pSelection->_fSelectionVisible = true;
// set selection region
m_pSelection->_srSelectionRect.Top = 0;
m_pSelection->_srSelectionRect.Bottom = 3;
m_pSelection->_srSelectionRect.Left = 1;
m_pSelection->_srSelectionRect.Right = 10;
// #1 top-left to bottom right selection first
m_pSelection->_coordSelectionAnchor.X = m_pSelection->_srSelectionRect.Left;
m_pSelection->_coordSelectionAnchor.Y = m_pSelection->_srSelectionRect.Top;
// A. false/false for the selection modes should mean box selection
m_pSelection->_fLineSelection = false;
m_pSelection->_fUseAlternateSelection = false;
VerifyGetSelectionRects_BoxMode();
// B. true/true for the selection modes should also mean box selection
m_pSelection->_fLineSelection = true;
m_pSelection->_fUseAlternateSelection = true;
VerifyGetSelectionRects_BoxMode();
// now try the other 3 configurations of box region.
// #2 top-right to bottom-left selection
m_pSelection->_coordSelectionAnchor.X = m_pSelection->_srSelectionRect.Right;
m_pSelection->_coordSelectionAnchor.Y = m_pSelection->_srSelectionRect.Top;
VerifyGetSelectionRects_BoxMode();
// #3 bottom-left to top-right selection
m_pSelection->_coordSelectionAnchor.X = m_pSelection->_srSelectionRect.Left;
m_pSelection->_coordSelectionAnchor.Y = m_pSelection->_srSelectionRect.Bottom;
VerifyGetSelectionRects_BoxMode();
// #4 bottom-right to top-left selection
m_pSelection->_coordSelectionAnchor.X = m_pSelection->_srSelectionRect.Right;
m_pSelection->_coordSelectionAnchor.Y = m_pSelection->_srSelectionRect.Bottom;
VerifyGetSelectionRects_BoxMode();
}
void VerifyGetSelectionRects_LineMode()
{
const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
const auto selectionRects = m_pSelection->GetSelectionRects();
const UINT cRectanglesExpected = m_pSelection->_srSelectionRect.Bottom - m_pSelection->_srSelectionRect.Top + 1;
if (VERIFY_ARE_EQUAL(cRectanglesExpected, selectionRects.size()))
{
// RULES:
// 1. If we're only selection one line, select the entire region between the two rectangles.
// Else if we're selecting multiple lines...
// 2. Extend all lines except the last line to the right edge of the screen
// Extend all lines except the first line to the left edge of the screen
// 3. If our anchor is in the top-right or bottom-left corner of the rectangle...
// The inside portion of our rectangle on the first and last lines is invalid.
// Remove from selection (but preserve the anchors themselves).
// RULE #1: If 1 line, entire region selected.
bool fHaveOneLine = selectionRects.size() == 1;
if (fHaveOneLine)
{
SMALL_RECT srSelectionRect = m_pSelection->_srSelectionRect;
VERIFY_ARE_EQUAL(srSelectionRect.Top, srSelectionRect.Bottom);
const SMALL_RECT* const psrRect = &selectionRects[0];
VERIFY_ARE_EQUAL(psrRect->Top, srSelectionRect.Top);
VERIFY_ARE_EQUAL(psrRect->Bottom, srSelectionRect.Bottom);
VERIFY_ARE_EQUAL(psrRect->Left, srSelectionRect.Left);
VERIFY_ARE_EQUAL(psrRect->Right, srSelectionRect.Right);
}
else
{
// RULE #2 : Check extension to edges
for (UINT iRect = 0; iRect < selectionRects.size(); iRect++)
{
// ensure each rectangle is exactly the width requested (block selection)
const SMALL_RECT* const psrRect = &selectionRects[iRect];
const short sRectangleLineNumber = (short)iRect + m_pSelection->_srSelectionRect.Top;
VERIFY_ARE_EQUAL(psrRect->Top, sRectangleLineNumber);
VERIFY_ARE_EQUAL(psrRect->Bottom, sRectangleLineNumber);
bool fIsFirstLine = iRect == 0;
bool fIsLastLine = iRect == selectionRects.size() - 1;
// for all lines except the last, the line should reach the right edge of the buffer
if (!fIsLastLine)
{
// buffer size = 80, then selection goes 0 to 79. Thus X - 1.
VERIFY_ARE_EQUAL(psrRect->Right, gci.GetActiveOutputBuffer().GetTextBuffer().GetSize().RightInclusive());
}
// for all lines except the first, the line should reach the left edge of the buffer
if (!fIsFirstLine)
{
VERIFY_ARE_EQUAL(psrRect->Left, 0);
}
}
// RULE #3: Check first and last line have invalid regions removed, if applicable
UINT iFirst = 0;
UINT iLast = gsl::narrow<UINT>(selectionRects.size() - 1u);
const SMALL_RECT* const psrFirst = &selectionRects[iFirst];
const SMALL_RECT* const psrLast = &selectionRects[iLast];
bool fRemoveRegion = false;
SMALL_RECT srSelectionRect = m_pSelection->_srSelectionRect;
COORD coordAnchor = m_pSelection->_coordSelectionAnchor;
// if the anchor is in the top right or bottom left corner, we must have removed a region. otherwise, it stays as is.
if (coordAnchor.Y == srSelectionRect.Top && coordAnchor.X == srSelectionRect.Right)
{
fRemoveRegion = true;
}
else if (coordAnchor.Y == srSelectionRect.Bottom && coordAnchor.X == srSelectionRect.Left)
{
fRemoveRegion = true;
}
// now check the first row's left based on removal
if (!fRemoveRegion)
{
VERIFY_ARE_EQUAL(psrFirst->Left, srSelectionRect.Left);
}
else
{
VERIFY_ARE_EQUAL(psrFirst->Left, srSelectionRect.Right);
}
// and the last row's right based on removal
if (!fRemoveRegion)
{
VERIFY_ARE_EQUAL(psrLast->Right, srSelectionRect.Right);
}
else
{
VERIFY_ARE_EQUAL(psrLast->Right, srSelectionRect.Left);
}
}
}
}
TEST_METHOD(TestGetSelectionRects_LineMode)
{
m_pSelection->_fSelectionVisible = true;
// Part I: Multiple line selection
// set selection region
m_pSelection->_srSelectionRect.Top = 0;
m_pSelection->_srSelectionRect.Bottom = 3;
m_pSelection->_srSelectionRect.Left = 1;
m_pSelection->_srSelectionRect.Right = 10;
// #1 top-left to bottom right selection first
m_pSelection->_coordSelectionAnchor.X = m_pSelection->_srSelectionRect.Left;
m_pSelection->_coordSelectionAnchor.Y = m_pSelection->_srSelectionRect.Top;
// A. true/false for the selection modes should mean line selection
m_pSelection->_fLineSelection = true;
m_pSelection->_fUseAlternateSelection = false;
VerifyGetSelectionRects_LineMode();
// B. false/true for the selection modes should also mean line selection
m_pSelection->_fLineSelection = false;
m_pSelection->_fUseAlternateSelection = true;
VerifyGetSelectionRects_LineMode();
// now try the other 3 configurations of box region.
// #2 top-right to bottom-left selection
m_pSelection->_coordSelectionAnchor.X = m_pSelection->_srSelectionRect.Right;
m_pSelection->_coordSelectionAnchor.Y = m_pSelection->_srSelectionRect.Top;
VerifyGetSelectionRects_LineMode();
// #3 bottom-left to top-right selection
m_pSelection->_coordSelectionAnchor.X = m_pSelection->_srSelectionRect.Left;
m_pSelection->_coordSelectionAnchor.Y = m_pSelection->_srSelectionRect.Bottom;
VerifyGetSelectionRects_LineMode();
// #4 bottom-right to top-left selection
m_pSelection->_coordSelectionAnchor.X = m_pSelection->_srSelectionRect.Right;
m_pSelection->_coordSelectionAnchor.Y = m_pSelection->_srSelectionRect.Bottom;
VerifyGetSelectionRects_LineMode();
// Part II: Single line selection
m_pSelection->_srSelectionRect.Top = 2;
m_pSelection->_srSelectionRect.Bottom = 2;
m_pSelection->_srSelectionRect.Left = 1;
m_pSelection->_srSelectionRect.Right = 10;
// #1: left to right selection
m_pSelection->_coordSelectionAnchor.X = m_pSelection->_srSelectionRect.Left;
VERIFY_IS_TRUE(m_pSelection->_srSelectionRect.Bottom == m_pSelection->_srSelectionRect.Top);
m_pSelection->_coordSelectionAnchor.Y = m_pSelection->_srSelectionRect.Bottom;
VerifyGetSelectionRects_LineMode();
// #2: right to left selection
m_pSelection->_coordSelectionAnchor.X = m_pSelection->_srSelectionRect.Right;
VERIFY_IS_TRUE(m_pSelection->_srSelectionRect.Bottom == m_pSelection->_srSelectionRect.Top);
m_pSelection->_coordSelectionAnchor.Y = m_pSelection->_srSelectionRect.Top;
VerifyGetSelectionRects_LineMode();
}
void TestBisectSelectionDelta(SHORT sTargetX, SHORT sTargetY, SHORT sLength, SHORT sDeltaLeft, SHORT sDeltaRight)
{
const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
const SCREEN_INFORMATION& screenInfo = gci.GetActiveOutputBuffer();
short sStringLength;
COORD coordTargetPoint;
SMALL_RECT srSelection;
SMALL_RECT srOriginal;
sStringLength = sLength;
coordTargetPoint.X = sTargetX;
coordTargetPoint.Y = sTargetY;
// selection area is always one row at a time so top/bottom = Y = row position
srSelection.Top = srSelection.Bottom = coordTargetPoint.Y;
// selection rectangle starts from the target and goes for the length requested
srSelection.Left = coordTargetPoint.X;
srSelection.Right = coordTargetPoint.X + sStringLength;
// save original for comparison
srOriginal.Top = srSelection.Top;
srOriginal.Bottom = srSelection.Bottom;
srOriginal.Left = srSelection.Left;
srOriginal.Right = srSelection.Right;
COORD startPos{ sTargetX, sTargetY };
COORD endPos{ base::ClampAdd(sTargetX, sLength), sTargetY };
const auto selectionRects = screenInfo.GetTextBuffer().GetTextRects(startPos, endPos);
VERIFY_ARE_EQUAL(static_cast<size_t>(1), selectionRects.size());
srSelection = selectionRects.at(0);
VERIFY_ARE_EQUAL(srOriginal.Top, srSelection.Top);
VERIFY_ARE_EQUAL(srOriginal.Bottom, srSelection.Bottom);
VERIFY_ARE_EQUAL(srOriginal.Left + sDeltaLeft, srSelection.Left);
VERIFY_ARE_EQUAL(srOriginal.Right + sDeltaRight, srSelection.Right);
}
TEST_METHOD(TestBisectSelection)
{
m_state->FillTextBufferBisect();
// From CommonState, this is what rows look like:
// positions of き are at 0, 27-28, 39-40, 67-68, 79
// きABCDEFGHIJKLMNOPQRSTUVWXYZきき0123456789ききABCDEFGHIJKLMNOPQRSTUVWXYZきき0123456789き
// きABCDEFGHIJKLMNOPQRSTUVWXYZきき0123456789ききABCDEFGHIJKLMNOPQRSTUVWXYZきき0123456789き
// きABCDEFGHIJKLMNOPQRSTUVWXYZきき0123456789ききABCDEFGHIJKLMNOPQRSTUVWXYZきき0123456789き
// きABCDEFGHIJKLMNOPQRSTUVWXYZきき0123456789ききABCDEFGHIJKLMNOPQRSTUVWXYZきき0123456789き
// 1a. Start position is trailing half and is at beginning of row
// start from position Column 0, Row 2
// selection is 5 characters long
// the left edge should move one to the right (+1) to not select the trailing byte
// right edge shouldn't move
TestBisectSelectionDelta(0, 2, 5, 1, 0);
// 1b. Start position is trailing half and is elsewhere in the row
// start from position Column 28, Row 2, which is the position of a trailing き in the mid row
// selection is 5 characters long
// the left edge should move one to the left (-1) to select the leading byte also
// right edge shouldn't move
TestBisectSelectionDelta(28, 2, 5, -1, 0);
// 1c. Start position is trailing half and is beginning of buffer
// start from position Column 0, Row 0 which is a trailing byte
// selection is 5 characters long
// the left edge should move one to the right (+1) to not select the trailing byte
// right edge shouldn't move
TestBisectSelectionDelta(0, 0, 5, 1, 0);
// 2a. End position is leading half and is at end of row
// start from position 10 before end of row (80 length row)
// row is 2
// selection is 9 characters long
// the left edge shouldn't move
// the right edge should move one to the left (-1) to not select the leading byte
TestBisectSelectionDelta(70, 2, 9, 0, -1);
// 2b. End position is leading half and is elsewhere in the row
// start from 10 before trailing き in the mid row (pos 68 - 10 = 58)
// row is 2
// selection is 10 characters long
// the left edge shouldn't move
// the right edge should not move, because it is already on the trailing byte
TestBisectSelectionDelta(58, 2, 10, 0, 0);
// 2c. End position is leading half and is at end of buffer
// start from position 10 before end of row (80 length row)
// row is 300 (or 299 for the index)
// selection is 9 characters long
// the left edge shouldn't move
// the right edge should move one to the left (-1) to not select the leading byte
TestBisectSelectionDelta(70, 299, 9, 0, -1);
}
};
class SelectionInputTests
{
TEST_CLASS(SelectionInputTests);
CommonState* m_state;
TEST_CLASS_SETUP(ClassSetup)
{
m_state = new CommonState();
m_state->PrepareGlobalFont();
m_state->PrepareGlobalScreenBuffer();
m_state->PrepareGlobalInputBuffer();
return true;
}
TEST_CLASS_CLEANUP(ClassCleanup)
{
m_state->CleanupGlobalScreenBuffer();
m_state->CleanupGlobalFont();
m_state->CleanupGlobalInputBuffer();
delete m_state;
return true;
}
TEST_METHOD(TestGetInputLineBoundaries)
{
CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
// 80x80 box
const SHORT sRowWidth = 80;
SMALL_RECT srectEdges;
srectEdges.Left = srectEdges.Top = 0;
srectEdges.Right = srectEdges.Bottom = sRowWidth - 1;
// false when no cooked read data exists
VERIFY_IS_FALSE(gci.HasPendingCookedRead());
bool fResult = Selection::s_GetInputLineBoundaries(nullptr, nullptr);
VERIFY_IS_FALSE(fResult);
// prepare some read data
m_state->PrepareReadHandle();
auto cleanupReadHandle = wil::scope_exit([&]() { m_state->CleanupReadHandle(); });
m_state->PrepareCookedReadData();
// set up to clean up read data later
auto cleanupCookedRead = wil::scope_exit([&]() { m_state->CleanupCookedReadData(); });
COOKED_READ_DATA& readData = gci.CookedReadData();
// backup text info position over remainder of text execution duration
TextBuffer& textBuffer = gci.GetActiveOutputBuffer().GetTextBuffer();
COORD coordOldTextInfoPos;
coordOldTextInfoPos.X = textBuffer.GetCursor().GetPosition().X;
coordOldTextInfoPos.Y = textBuffer.GetCursor().GetPosition().Y;
// set various cursor positions
readData.OriginalCursorPosition().X = 15;
readData.OriginalCursorPosition().Y = 3;
readData.VisibleCharCount() = 200;
textBuffer.GetCursor().SetXPosition(35);
textBuffer.GetCursor().SetYPosition(35);
// try getting boundaries with no pointers. parameters should be fully optional.
fResult = Selection::s_GetInputLineBoundaries(nullptr, nullptr);
VERIFY_IS_TRUE(fResult);
// now let's get some actual data
COORD coordStart;
COORD coordEnd;
fResult = Selection::s_GetInputLineBoundaries(&coordStart, &coordEnd);
VERIFY_IS_TRUE(fResult);
// starting position/boundary should always be where the input line started
VERIFY_ARE_EQUAL(coordStart.X, readData.OriginalCursorPosition().X);
VERIFY_ARE_EQUAL(coordStart.Y, readData.OriginalCursorPosition().Y);
// ending position can vary. it's in one of two spots
// 1. If the original cooked cursor was valid (which it was this first time), it's NumberOfVisibleChars ahead.
COORD coordFinalPos;
const short cCharsToAdjust = ((short)readData.VisibleCharCount() - 1); // then -1 to be on the last piece of text, not past it
coordFinalPos.X = (readData.OriginalCursorPosition().X + cCharsToAdjust) % sRowWidth;
coordFinalPos.Y = readData.OriginalCursorPosition().Y + ((readData.OriginalCursorPosition().X + cCharsToAdjust) / sRowWidth);
VERIFY_ARE_EQUAL(coordEnd.X, coordFinalPos.X);
VERIFY_ARE_EQUAL(coordEnd.Y, coordFinalPos.Y);
// 2. if the original cooked cursor is invalid, then it's the text info cursor position
readData.OriginalCursorPosition().X = -1;
readData.OriginalCursorPosition().Y = -1;
fResult = Selection::s_GetInputLineBoundaries(nullptr, &coordEnd);
VERIFY_IS_TRUE(fResult);
VERIFY_ARE_EQUAL(coordEnd.X, textBuffer.GetCursor().GetPosition().X - 1); // -1 to be on the last piece of text, not past it
VERIFY_ARE_EQUAL(coordEnd.Y, textBuffer.GetCursor().GetPosition().Y);
// restore text buffer info position
textBuffer.GetCursor().SetXPosition(coordOldTextInfoPos.X);
textBuffer.GetCursor().SetYPosition(coordOldTextInfoPos.Y);
}
TEST_METHOD(TestWordByWordPrevious)
{
BEGIN_TEST_METHOD_PROPERTIES()
TEST_METHOD_PROPERTY(L"IsolationLevel", L"Method")
END_TEST_METHOD_PROPERTIES()
CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
SCREEN_INFORMATION& screenInfo = gci.GetActiveOutputBuffer();
const std::wstring text(L"this is some test text.");
screenInfo.Write(OutputCellIterator(text));
// Get the left and right side of the text we inserted (right is one past the end)
const COORD left = { 0, 0 };
const COORD right = { gsl::narrow<SHORT>(text.length()), 0 };
// Get the selection instance and buffer size
auto& sel = Selection::Instance();
const auto bufferSize = screenInfo.GetBufferSize();
// The anchor is where the selection started from.
const auto anchor = right;
// The point is the "other end" of the anchor forming the rectangle of what is covered.
// It starts at the same spot as the anchor to represent the initial 1x1 selection.
auto point = anchor;
// Walk through the sequence in reverse extending the sequence by one word each time to the left.
// The anchor is always the end of the line and the selection just gets bigger.
do
{
// We expect the result to be left of where we started.
// It will point at the character just right of the space (or the beginning of the line).
COORD resultExpected = point;
do
{
resultExpected.X--;
} while (resultExpected.X > 0 && text.at(resultExpected.X - 1) != UNICODE_SPACE);
point = sel.WordByWordSelection(true, bufferSize, anchor, point);
VERIFY_ARE_EQUAL(resultExpected, point);
} while (point.X > left.X);
}
TEST_METHOD(TestWordByWordNext)
{
BEGIN_TEST_METHOD_PROPERTIES()
TEST_METHOD_PROPERTY(L"IsolationLevel", L"Method")
END_TEST_METHOD_PROPERTIES()
CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
SCREEN_INFORMATION& screenInfo = gci.GetActiveOutputBuffer();
const std::wstring text(L"this is some test text.");
screenInfo.Write(OutputCellIterator(text));
// Get the left and right side of the text we inserted (right is one past the end)
const COORD left = { 0, 0 };
const COORD right = { gsl::narrow<SHORT>(text.length()), 0 };
// Get the selection instance and buffer size
auto& sel = Selection::Instance();
const auto bufferSize = screenInfo.GetBufferSize();
// The anchor is where the selection started from.
const auto anchor = left;
// The point is the "other end" of the anchor forming the rectangle of what is covered.
// It starts at the same spot as the anchor to represent the initial 1x1 selection.
auto point = anchor;
// Walk through the sequence forward extending the sequence by one word each time to the right.
// The anchor is always the end of the line and the selection just gets bigger.
do
{
// We expect the result to be right of where we started.
COORD resultExpected = point;
do
{
resultExpected.X++;
} while (resultExpected.X + 1 < right.X && text.at(resultExpected.X + 1) != UNICODE_SPACE);
resultExpected.X++;
// when we reach the end, word by word selection will seek forward to the end of the buffer, so update
// the expected to the end in that circumstance
if (resultExpected.X >= right.X)
{
resultExpected.X = bufferSize.RightInclusive();
resultExpected.Y = bufferSize.BottomInclusive();
}
point = sel.WordByWordSelection(false, bufferSize, anchor, point);
VERIFY_ARE_EQUAL(resultExpected, point);
} while (point.Y < bufferSize.BottomInclusive()); // stop once we've advanced to a point on the bottom row of the buffer.
}
};