terminal/src/buffer/out/search.cpp
James Holderness 4c53c595e7
Add support for double-width/double-height lines in conhost (#8664)
This PR adds support for the VT line rendition attributes, which allow
for double-width and double-height line renditions. These renditions are
enabled with the `DECDWL` (double-width line) and `DECDHL`
(double-height line) escape sequences. Both reset to the default
rendition with the `DECSWL` (single-width line) escape sequence. For now
this functionality is only supported by the GDI renderer in conhost.

There are a lot of changes, so this is just a general overview of the
main areas affected.

Previously it was safe to assume that the screen had a fixed width, at
least for a given point in time. But now we need to deal with the
possibility of different lines have different widths, so all the
functions that are constrained by the right border (text wrapping,
cursor movement operations, and sequences like `EL` and `ICH`) now need
to lookup the width of the active line in order to behave correctly.

Similarly it used to be safe to assume that buffer and screen
coordinates were the same thing, but that is no longer true. Lots of
places now need to translate back and forth between coordinate systems
dependent on the line rendition. This includes clipboard handling, the
conhost color selection and search, accessibility location tracking and
screen reading, IME editor positioning, "snapping" the viewport, and of
course all the rendering calculations.

For the rendering itself, I've had to introduce a new
`PrepareLineTransform` method that the render engines can use to setup
the necessary transform matrix for a given line rendition. This is also
now used to handle the horizontal viewport offset, since that could no
longer be achieved just by changing the target coordinates (on a double
width line, the viewport offset may be halfway through a character).

I've also had to change the renderer's existing `InvalidateCursor`
method to take a `SMALL_RECT` rather than a `COORD`, to allow for the
cursor being a variable width. Technically this was already a problem,
because the cursor could occupy two screen cells when over a
double-width character, but now it can be anything between one and four
screen cells (e.g. a double-width character on the double-width line).

In terms of architectural changes, there is now a new `lineRendition`
field in the `ROW` class that keeps track of the line rendition for each
row, and several new methods in the `ROW` and `TextBuffer` classes for
manipulating that state. This includes a few helper methods for handling
the various issues discussed above, e.g. position clamping and
translating between coordinate systems.

## Validation Steps Performed

I've manually confirmed all the double-width and double-height tests in
_Vttest_ are now working as expected, and the _VT100 Torture Test_ now
renders correctly (at least the line rendition aspects). I've also got
my own test scripts that check many of the line rendition boundary cases
and have confirmed that those are now passing.

I've manually tested as many areas of the conhost UI that I could think
of, that might be affected by line rendition, including things like
searching, selection, copying, and color highlighting. For
accessibility, I've confirmed that the _Magnifier_ and _Narrator_
correctly handle double-width lines. And I've also tested the Japanese
IME, which while not perfect, is at least useable.

Closes #7865
2021-02-18 05:44:50 +00:00

347 lines
12 KiB
C++

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "precomp.h"
#include "search.h"
#include "CharRow.hpp"
#include "textBuffer.hpp"
#include "../types/inc/Utf16Parser.hpp"
#include "../types/inc/GlyphWidth.hpp"
using namespace Microsoft::Console::Types;
// Routine Description:
// - Constructs a Search object.
// - Make a Search object then call .FindNext() to locate items.
// - Once you've found something, you can perform actions like .Select() or .Color()
// Arguments:
// - textBuffer - The screen text buffer to search through (the "haystack")
// - uiaData - The IUiaData type reference, it is for providing selection methods
// - str - The search term you want to find (the "needle")
// - direction - The direction to search (upward or downward)
// - sensitivity - Whether or not you care about case
Search::Search(IUiaData& uiaData,
const std::wstring& str,
const Direction direction,
const Sensitivity sensitivity) :
_direction(direction),
_sensitivity(sensitivity),
_needle(s_CreateNeedleFromString(str)),
_uiaData(uiaData),
_coordAnchor(s_GetInitialAnchor(uiaData, direction))
{
_coordNext = _coordAnchor;
}
// Routine Description:
// - Constructs a Search object.
// - Make a Search object then call .FindNext() to locate items.
// - Once you've found something, you can perform actions like .Select() or .Color()
// Arguments:
// - textBuffer - The screen text buffer to search through (the "haystack")
// - uiaData - The IUiaData type reference, it is for providing selection methods
// - str - The search term you want to find (the "needle")
// - direction - The direction to search (upward or downward)
// - sensitivity - Whether or not you care about case
// - anchor - starting search location in screenInfo
Search::Search(IUiaData& uiaData,
const std::wstring& str,
const Direction direction,
const Sensitivity sensitivity,
const COORD anchor) :
_direction(direction),
_sensitivity(sensitivity),
_needle(s_CreateNeedleFromString(str)),
_coordAnchor(anchor),
_uiaData(uiaData)
{
_coordNext = _coordAnchor;
}
// Routine Description
// - Locates the next instance of the search term within the screen buffer.
// Arguments:
// - <none> - Uses internal state from constructor
// Return Value:
// - True if we found another item. False if we've reached the end of the buffer.
// - NOTE: You can FindNext() again after False to go around the buffer again.
bool Search::FindNext()
{
if (_reachedEnd)
{
_reachedEnd = false;
return false;
}
do
{
if (_FindNeedleInHaystackAt(_coordNext, _coordSelStart, _coordSelEnd))
{
_UpdateNextPosition();
_reachedEnd = _coordNext == _coordAnchor;
return true;
}
else
{
_UpdateNextPosition();
}
} while (_coordNext != _coordAnchor);
return false;
}
// Routine Description:
// - Takes the found word and selects it in the screen buffer
void Search::Select() const
{
// Convert buffer selection offsets into the equivalent screen coordinates
// required by SelectNewRegion, taking line renditions into account.
const auto& textBuffer = _uiaData.GetTextBuffer();
const auto selStart = textBuffer.BufferToScreenPosition(_coordSelStart);
const auto selEnd = textBuffer.BufferToScreenPosition(_coordSelEnd);
_uiaData.SelectNewRegion(selStart, selEnd);
}
// Routine Description:
// - In console host, we take the found word and apply the given color to it in the screen buffer
// - In Windows Terminal, we just select the found word, but we do not modify the buffer
// Arguments:
// - ulAttr - The legacy color attribute to apply to the word
void Search::Color(const TextAttribute attr) const
{
// Only select if we've found something.
if (_coordSelStart != _coordSelEnd)
{
_uiaData.ColorSelection(_coordSelStart, _coordSelEnd, attr);
}
}
// Routine Description:
// - gets start and end position of text sound by search. only guaranteed to have valid data if FindNext has
// been called and returned true.
// Return Value:
// - pair containing [start, end] coord positions of text found by search
std::pair<COORD, COORD> Search::GetFoundLocation() const noexcept
{
return { _coordSelStart, _coordSelEnd };
}
// Routine Description:
// - Finds the anchor position where we will start searches from.
// - This position will represent the "wrap around" point in the buffer or where
// we reach the end of our search.
// - If the screen buffer given already has a selection in it, it will be used to determine the anchor.
// - Otherwise, we will choose one of the ends of the screen buffer depending on direction.
// Arguments:
// - uiaData - The reference to the IUiaData interface type object
// - direction - The intended direction of the search
// Return Value:
// - Coordinate to start the search from.
COORD Search::s_GetInitialAnchor(IUiaData& uiaData, const Direction direction)
{
const auto& textBuffer = uiaData.GetTextBuffer();
const COORD textBufferEndPosition = uiaData.GetTextBufferEndPosition();
if (uiaData.IsSelectionActive())
{
// Convert the screen position of the selection anchor into an equivalent
// buffer position to start searching, taking line rendition into account.
auto anchor = textBuffer.ScreenToBufferPosition(uiaData.GetSelectionAnchor());
if (direction == Direction::Forward)
{
textBuffer.GetSize().IncrementInBoundsCircular(anchor);
}
else
{
textBuffer.GetSize().DecrementInBoundsCircular(anchor);
// If the selection starts at (0, 0), we need to make sure
// it does not exceed the text buffer end position
anchor.X = std::min(textBufferEndPosition.X, anchor.X);
anchor.Y = std::min(textBufferEndPosition.Y, anchor.Y);
}
return anchor;
}
else
{
if (direction == Direction::Forward)
{
return { 0, 0 };
}
else
{
return textBufferEndPosition;
}
}
}
// Routine Description:
// - Attempts to compare the search term (the needle) to the screen buffer (the haystack)
// at the given coordinate position of the screen buffer.
// - Performs one comparison. Call again with new positions to check other spots.
// Arguments:
// - pos - The position in the haystack (screen buffer) to compare
// - start - If we found it, this is filled with the coordinate of the first character of the needle.
// - end - If we found it, this is filled with the coordinate of the last character of the needle.
// Return Value:
// - True if we found it. False if not.
bool Search::_FindNeedleInHaystackAt(const COORD pos, COORD& start, COORD& end) const
{
start = { 0 };
end = { 0 };
COORD bufferPos = pos;
for (const auto& needleCell : _needle)
{
// Haystack is the buffer. Needle is the string we were given.
const auto hayIter = _uiaData.GetTextBuffer().GetTextDataAt(bufferPos);
const auto hayChars = *hayIter;
const auto needleChars = std::wstring_view(needleCell.data(), needleCell.size());
// If we didn't match at any point of the needle, return false.
if (!_CompareChars(hayChars, needleChars))
{
return false;
}
_IncrementCoord(bufferPos);
}
_DecrementCoord(bufferPos);
// If we made it the whole way through the needle, then it was in the haystack.
// Fill out the span that we found the result at and return true.
start = pos;
end = bufferPos;
return true;
}
// Routine Description:
// - Provides an abstraction for comparing two spans of text.
// - Internally handles case sensitivity based on object construction.
// Arguments:
// - one - String view representing the first string of text
// - two - String view representing the second string of text
// Return Value:
// - True if they are the same. False otherwise.
bool Search::_CompareChars(const std::wstring_view one, const std::wstring_view two) const noexcept
{
if (one.size() != two.size())
{
return false;
}
for (size_t i = 0; i < one.size(); i++)
{
if (_ApplySensitivity(one.at(i)) != _ApplySensitivity(two.at(i)))
{
return false;
}
}
return true;
}
// Routine Description:
// - Provides an abstraction for conditionally applying case sensitivity
// based on object construction
// Arguments:
// - wch - Character to adjust if necessary
// Return Value:
// - Adjusted value (or not).
wchar_t Search::_ApplySensitivity(const wchar_t wch) const noexcept
{
if (_sensitivity == Sensitivity::CaseInsensitive)
{
return ::towlower(wch);
}
else
{
return wch;
}
}
// Routine Description:
// - Helper to increment a coordinate in respect to the associated screen buffer
// Arguments
// - coord - Updated by function to increment one position (will wrap X and Y direction)
void Search::_IncrementCoord(COORD& coord) const noexcept
{
_uiaData.GetTextBuffer().GetSize().IncrementInBoundsCircular(coord);
}
// Routine Description:
// - Helper to decrement a coordinate in respect to the associated screen buffer
// Arguments
// - coord - Updated by function to decrement one position (will wrap X and Y direction)
void Search::_DecrementCoord(COORD& coord) const noexcept
{
_uiaData.GetTextBuffer().GetSize().DecrementInBoundsCircular(coord);
}
// Routine Description:
// - Helper to update the coordinate position to the next point to be searched
// Return Value:
// - True if we haven't reached the end of the buffer. False otherwise.
void Search::_UpdateNextPosition()
{
if (_direction == Direction::Forward)
{
_IncrementCoord(_coordNext);
}
else if (_direction == Direction::Backward)
{
_DecrementCoord(_coordNext);
}
else
{
THROW_HR(E_NOTIMPL);
}
// To reduce wrap-around time, if the next position is larger than
// the end position of the written text
// We put the next position to:
// Forward: (0, 0)
// Backward: the position of the end of the text buffer
const COORD bufferEndPosition = _uiaData.GetTextBufferEndPosition();
if (_coordNext.Y > bufferEndPosition.Y ||
(_coordNext.Y == bufferEndPosition.Y && _coordNext.X > bufferEndPosition.X))
{
if (_direction == Direction::Forward)
{
_coordNext = { 0 };
}
else
{
_coordNext = bufferEndPosition;
}
}
}
// Routine Description:
// - Creates a "needle" of the correct format for comparison to the screen buffer text data
// that we can use for our search
// Arguments:
// - wstr - String that will be our search term
// Return Value:
// - Structured text data for comparison to screen buffer text data.
std::vector<std::vector<wchar_t>> Search::s_CreateNeedleFromString(const std::wstring& wstr)
{
const auto charData = Utf16Parser::Parse(wstr);
std::vector<std::vector<wchar_t>> cells;
for (const auto chars : charData)
{
if (IsGlyphFullWidth(std::wstring_view{ chars.data(), chars.size() }))
{
cells.emplace_back(chars);
}
cells.emplace_back(chars);
}
return cells;
}