Refactor UiaTextRange For Improved Navigation and Reliability (#4018)

## Summary of the Pull Request
This pull request is intended to achieve the following goals...
1) reduce duplicate code
2) remove static functions
3) improve readability
4) improve reliability
5) improve code-coverage for testing
6) establish functioning text buffer navigation in Narrator and NVDA

This also required a change to the wrapper class `XamlUiaTextRange` that has been causing issues with Narrator and NVDA.

See below for additional context.

## References
#3976 - I believe this might have been a result of improperly handling degenerate ranges. Fixed here.
#3895 - reduced the duplicate code. No need to separate into different files
#2160 - same as #3976 above
#1993 - I think just about everything is no longer static

## PR Checklist
* [x] Closes #3895, Closes #1993, Closes #3976, Closes #2160 
* [x] CLA signed
* [x] Tests added/passed

## Detailed Description of the Pull Request / Additional comments

### UiaTextRange
- converted endpoints into the COORD system in the TextBuffer coordinate space
- `start` is inclusive, `end` is exclusive. A degenerate range is when start == end.
- all functions are no longer static
- `MoveByUnit()` functions now rely on `MoveEndpointByUnit()` functions
- removed unnecessary typedefs like `Endpoint`, `ScreenInfoRow`, etc..
- relied more heavily on existing functionality from `TextBuffer` and `Viewport`

### XamlUiaTextRange
- `GetAttributeValue()` must return a special HRESULT that signifies that the requested attribute is not supported. This was the cause of a number of inconsistencies between Narrator and NVDA.
- `FindText()` should return `nullptr` if nothing was found. #4373 properly fixes this functionality now that Search is a shared module

### TextBuffer
- Word navigation functionality is entirely in `TextBuffer` for proper abstraction
- a total of 6 functions are now dedicated to word navigation to get a good understanding of the differences between a "word" in Accessibility and a "word" in selection

As an example, consider a buffer with this text in it:
"  word   other  "
In selection, a "word" is defined as the range between two delimiters, so the words in the example include ["  ", "word", "   ", "other", "  "].
In accessibility , a "word" includes the delimiters after a range of readable characters, so the words in the example include ["word   ", "other  "].

Additionally, accessibility word navigation must be able to detect if it is on the first or last word. This resulted in a slight variant of word navigation functions that return a boolean instead of a COORD.

Ideally, these functions can be consolidated, but that is too risky for a PR of this size as it can have an effect on selection.

### Viewport
- the concept of `EndExclusive` is added. This is used by UiaTextRange's `end` anchor as it is exclusive. To signify that the last character in the buffer is included in this buffer, `end` must be one past the end of the buffer. This is `EndExclusive`
- Since many functions check if the given `COORD` is in bounds, a flag must be set to allow `EndExclusive` as a valid `COORD` that is in bounds.

### Testing
- word navigation testing relies more heavily on TextBuffer tests
- additional testing was created for non-movement focused functions of UiaTextRange
- The results have been compared to Microsoft Word and some have been verified by UiAutomation/Narrator contacts as expected results.

## Validation Steps Performed
Tests pass
Narrator works
NVDA works
This commit is contained in:
Carlos Zamora 2020-01-31 12:59:39 -08:00 committed by GitHub
parent bba0527af9
commit 29df540174
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 1846 additions and 3497 deletions

View file

@ -956,37 +956,115 @@ Microsoft::Console::Render::IRenderTarget& TextBuffer::GetRenderTarget() noexcep
// Arguments:
// - target - a COORD on the word you are currently on
// - wordDelimiters - what characters are we considering for the separation of words
// - includeCharacterRun - include the character run located at the beginning of the word
// - accessibilityMode - when enabled, we continue expanding left until we are at the beginning of a readable word.
// Otherwise, expand left until a character of a new delimiter class is found
// (or a row boundary is encountered)
// Return Value:
// - The COORD for the first character on the "word" (inclusive)
const COORD TextBuffer::GetWordStart(const COORD target, const std::wstring_view wordDelimiters, bool includeCharacterRun) const
// - The COORD for the first character on the "word" (inclusive)
const COORD TextBuffer::GetWordStart(const COORD target, const std::wstring_view wordDelimiters, bool accessibilityMode) const
{
const auto bufferSize = GetSize();
COORD result = target;
// Consider a buffer with this text in it:
// " word other "
// In selection (accessibilityMode = false),
// a "word" is defined as the range between two delimiters
// so the words in the example include [" ", "word", " ", "other", " "]
// In accessibility (accessibilityMode = true),
// a "word" includes the delimiters after a range of readable characters
// so the words in the example include ["word ", "other "]
// NOTE: the start anchor (this one) is inclusive, whereas the end anchor (GetWordEnd) is exclusive
// can't expand left
if (target.X == bufferSize.Left())
if (target.X == GetSize().Left())
{
return result;
return target;
}
if (accessibilityMode)
{
return _GetWordStartForAccessibility(target, wordDelimiters);
}
else
{
return _GetWordStartForSelection(target, wordDelimiters);
}
}
// Method Description:
// - Helper method for GetWordStart(). Get the COORD for the beginning of the word (accessibility definition) you are on
// Arguments:
// - target - a COORD on the word you are currently on
// - wordDelimiters - what characters are we considering for the separation of words
// Return Value:
// - The COORD for the first character on the current/previous READABLE "word" (inclusive)
const COORD TextBuffer::_GetWordStartForAccessibility(const COORD target, const std::wstring_view wordDelimiters) const
{
COORD result = target;
const auto bufferSize = GetSize();
bool stayAtOrigin = false;
auto bufferIterator = GetTextDataAt(result);
// ignore left boundary. Continue until readable text found
while (_GetDelimiterClass(*bufferIterator, wordDelimiters) != DelimiterClass::RegularChar)
{
if (bufferSize.DecrementInBounds(result))
{
--bufferIterator;
}
else
{
// first char in buffer is a DelimiterChar or ControlChar
// we can't move any further back
stayAtOrigin = true;
break;
}
}
// make sure we expand to the left boundary or the beginning of the word
while (_GetDelimiterClass(*bufferIterator, wordDelimiters) == DelimiterClass::RegularChar)
{
if (bufferSize.DecrementInBounds(result))
{
--bufferIterator;
}
else
{
// first char in buffer is a RegularChar
// we can't move any further back
break;
}
}
// move off of delimiter and onto word start
if (!stayAtOrigin && _GetDelimiterClass(*bufferIterator, wordDelimiters) != DelimiterClass::RegularChar)
{
bufferSize.IncrementInBounds(result);
}
return result;
}
// Method Description:
// - Helper method for GetWordStart(). Get the COORD for the beginning of the word (selection definition) you are on
// Arguments:
// - target - a COORD on the word you are currently on
// - wordDelimiters - what characters are we considering for the separation of words
// Return Value:
// - The COORD for the first character on the current word or delimiter run (stopped by the left margin)
const COORD TextBuffer::_GetWordStartForSelection(const COORD target, const std::wstring_view wordDelimiters) const
{
COORD result = target;
const auto bufferSize = GetSize();
auto bufferIterator = GetTextDataAt(result);
const auto initialDelimiter = _GetDelimiterClass(*bufferIterator, wordDelimiters);
// expand left until we hit the left boundary or a different delimiter class
while (result.X > bufferSize.Left() && (_GetDelimiterClass(*bufferIterator, wordDelimiters) == initialDelimiter))
{
bufferSize.DecrementInBounds(result);
--bufferIterator;
}
if (includeCharacterRun)
{
// include character run for readable word
if (_GetDelimiterClass(*bufferIterator, wordDelimiters) == DelimiterClass::RegularChar)
{
result = GetWordStart(result, wordDelimiters);
}
}
else if (_GetDelimiterClass(*bufferIterator, wordDelimiters) != initialDelimiter)
if (_GetDelimiterClass(*bufferIterator, wordDelimiters) != initialDelimiter)
{
// move off of delimiter
bufferSize.IncrementInBounds(result);
@ -996,17 +1074,96 @@ const COORD TextBuffer::GetWordStart(const COORD target, const std::wstring_view
}
// Method Description:
// - Get the COORD for the end of the word you are on
// - Get the COORD for the beginning of the NEXT word
// Arguments:
// - target - a COORD on the word you are currently on
// - wordDelimiters - what characters are we considering for the separation of words
// - includeDelimiterRun - include the delimiter runs located at the end of the word
// - accessibilityMode - when enabled, we continue expanding right until we are at the beginning of the next READABLE word
// Otherwise, expand right until a character of a new delimiter class is found
// (or a row boundary is encountered)
// Return Value:
// - The COORD for the last character on the "word" (inclusive)
const COORD TextBuffer::GetWordEnd(const COORD target, const std::wstring_view wordDelimiters, bool includeDelimiterRun) const
const COORD TextBuffer::GetWordEnd(const COORD target, const std::wstring_view wordDelimiters, bool accessibilityMode) const
{
// Consider a buffer with this text in it:
// " word other "
// In selection (accessibilityMode = false),
// a "word" is defined as the range between two delimiters
// so the words in the example include [" ", "word", " ", "other", " "]
// In accessibility (accessibilityMode = true),
// a "word" includes the delimiters after a range of readable characters
// so the words in the example include ["word ", "other "]
// NOTE: the end anchor (this one) is exclusive, whereas the start anchor (GetWordStart) is inclusive
if (accessibilityMode)
{
return _GetWordEndForAccessibility(target, wordDelimiters);
}
else
{
return _GetWordEndForSelection(target, wordDelimiters);
}
}
// Method Description:
// - Helper method for GetWordEnd(). Get the COORD for the beginning of the next READABLE word
// Arguments:
// - target - a COORD on the word you are currently on
// - wordDelimiters - what characters are we considering for the separation of words
// Return Value:
// - The COORD for the first character of the next readable "word". If no next word, return one past the end of the buffer
const COORD TextBuffer::_GetWordEndForAccessibility(const COORD target, const std::wstring_view wordDelimiters) const
{
const auto bufferSize = GetSize();
COORD result = target;
auto bufferIterator = GetTextDataAt(result);
// ignore right boundary. Continue through readable text found
while (_GetDelimiterClass(*bufferIterator, wordDelimiters) == DelimiterClass::RegularChar)
{
if (bufferSize.IncrementInBounds(result, true))
{
++bufferIterator;
}
else
{
// last char in buffer is a RegularChar
// we can't move any further forward
break;
}
}
// make sure we expand to the beginning of the NEXT word
while (_GetDelimiterClass(*bufferIterator, wordDelimiters) != DelimiterClass::RegularChar)
{
if (bufferSize.IncrementInBounds(result, true))
{
++bufferIterator;
}
else
{
// we are at the EndInclusive COORD
// this signifies that we must include the last char in the buffer
// but the position of the COORD points to nothing
break;
}
}
return result;
}
// Method Description:
// - Helper method for GetWordEnd(). Get the COORD for the beginning of the NEXT word
// Arguments:
// - target - a COORD on the word you are currently on
// - wordDelimiters - what characters are we considering for the separation of words
// Return Value:
// - The COORD for the last character of the current word or delimiter run (stopped by right margin)
const COORD TextBuffer::_GetWordEndForSelection(const COORD target, const std::wstring_view wordDelimiters) const
{
const auto bufferSize = GetSize();
COORD result = target;
auto bufferIterator = GetTextDataAt(result);
// can't expand right
if (target.X == bufferSize.RightInclusive())
@ -1014,23 +1171,16 @@ const COORD TextBuffer::GetWordEnd(const COORD target, const std::wstring_view w
return result;
}
auto bufferIterator = GetTextDataAt(result);
const auto initialDelimiter = _GetDelimiterClass(*bufferIterator, wordDelimiters);
// expand right until we hit the right boundary or a different delimiter class
while (result.X < bufferSize.RightInclusive() && (_GetDelimiterClass(*bufferIterator, wordDelimiters) == initialDelimiter))
{
bufferSize.IncrementInBounds(result);
++bufferIterator;
}
if (includeDelimiterRun)
{
// include delimiter run after word
if (_GetDelimiterClass(*bufferIterator, wordDelimiters) != DelimiterClass::RegularChar)
{
result = GetWordEnd(result, wordDelimiters);
}
}
else if (_GetDelimiterClass(*bufferIterator, wordDelimiters) != initialDelimiter)
if (_GetDelimiterClass(*bufferIterator, wordDelimiters) != initialDelimiter)
{
// move off of delimiter
bufferSize.DecrementInBounds(result);
@ -1039,6 +1189,107 @@ const COORD TextBuffer::GetWordEnd(const COORD target, const std::wstring_view w
return result;
}
// Method Description:
// - Update pos to be the position of the first character of the next word. This is used for accessibility
// Arguments:
// - pos - a COORD on the word you are currently on
// - wordDelimiters - what characters are we considering for the separation of words
// - lastCharPos - the position of the last nonspace character in the text buffer (to improve performance)
// Return Value:
// - true, if successfully updated pos. False, if we are unable to move (usually due to a buffer boundary)
// - pos - The COORD for the first character on the "word" (inclusive)
bool TextBuffer::MoveToNextWord(COORD& pos, const std::wstring_view wordDelimiters, COORD lastCharPos) const
{
auto copy = pos;
const auto bufferSize = GetSize();
auto text = GetTextDataAt(copy)->data();
auto delimiterClass = _GetDelimiterClass(text, wordDelimiters);
// started on a word, continue until the end of the word
while (delimiterClass == DelimiterClass::RegularChar)
{
if (!bufferSize.IncrementInBounds(copy))
{
// last char in buffer is a RegularChar
// thus there is no next word
return false;
}
text = GetTextDataAt(copy)->data();
delimiterClass = _GetDelimiterClass(text, wordDelimiters);
}
// we are already on/past the last RegularChar
if (bufferSize.CompareInBounds(copy, lastCharPos) >= 0)
{
return false;
}
// on whitespace, continue until the beginning of the next word
while (delimiterClass != DelimiterClass::RegularChar)
{
if (!bufferSize.IncrementInBounds(copy))
{
// last char in buffer is a DelimiterChar or ControlChar
// there is no next word
return false;
}
text = GetTextDataAt(copy)->data();
delimiterClass = _GetDelimiterClass(text, wordDelimiters);
}
// successful move, copy result out
pos = copy;
return true;
}
// Method Description:
// - Update pos to be the position of the first character of the previous word. This is used for accessibility
// Arguments:
// - pos - a COORD on the word you are currently on
// - wordDelimiters - what characters are we considering for the separation of words
// Return Value:
// - true, if successfully updated pos. False, if we are unable to move (usually due to a buffer boundary)
// - pos - The COORD for the first character on the "word" (inclusive)
bool TextBuffer::MoveToPreviousWord(COORD& pos, std::wstring_view wordDelimiters) const
{
auto copy = pos;
auto bufferSize = GetSize();
auto text = GetTextDataAt(copy)->data();
auto delimiterClass = _GetDelimiterClass(text, wordDelimiters);
// started on whitespace/delimiter, continue until the end of the previous word
while (delimiterClass != DelimiterClass::RegularChar)
{
if (!bufferSize.DecrementInBounds(copy))
{
// first char in buffer is a DelimiterChar or ControlChar
// there is no previous word
return false;
}
text = GetTextDataAt(copy)->data();
delimiterClass = _GetDelimiterClass(text, wordDelimiters);
}
// on a word, continue until the beginning of the word
while (delimiterClass == DelimiterClass::RegularChar)
{
if (!bufferSize.DecrementInBounds(copy))
{
// first char in buffer is a RegularChar
// there is no previous word
return false;
}
text = GetTextDataAt(copy)->data();
delimiterClass = _GetDelimiterClass(text, wordDelimiters);
}
// successful move, copy result out
pos = copy;
return true;
}
// Method Description:
// - get delimiter class for buffer cell data
// - used for double click selection and uia word navigation

View file

@ -130,8 +130,10 @@ public:
Microsoft::Console::Render::IRenderTarget& GetRenderTarget() noexcept;
const COORD GetWordStart(const COORD target, const std::wstring_view wordDelimiters, bool includeCharacterRun = false) const;
const COORD GetWordEnd(const COORD target, const std::wstring_view wordDelimiters, bool includeDelimiterRun = false) const;
const COORD GetWordStart(const COORD target, const std::wstring_view wordDelimiters, bool accessibilityMode = false) const;
const COORD GetWordEnd(const COORD target, const std::wstring_view wordDelimiters, bool accessibilityMode = false) const;
bool MoveToNextWord(COORD& pos, const std::wstring_view wordDelimiters, COORD lastCharPos) const;
bool MoveToPreviousWord(COORD& pos, const std::wstring_view wordDelimiters) const;
class TextAndColor
{
@ -198,6 +200,10 @@ private:
RegularChar
};
DelimiterClass _GetDelimiterClass(const std::wstring_view cellChar, const std::wstring_view wordDelimiters) const noexcept;
const COORD _GetWordStartForAccessibility(const COORD target, const std::wstring_view wordDelimiters) const;
const COORD _GetWordStartForSelection(const COORD target, const std::wstring_view wordDelimiters) const;
const COORD _GetWordEndForAccessibility(const COORD target, const std::wstring_view wordDelimiters) const;
const COORD _GetWordEndForSelection(const COORD target, const std::wstring_view wordDelimiters) const;
#ifdef UNIT_TESTING
friend class TextBufferTests;

View file

@ -141,16 +141,15 @@ HRESULT TermControlUiaProvider::CreateTextRange(_In_ IRawElementProviderSimple*
}
HRESULT TermControlUiaProvider::CreateTextRange(_In_ IRawElementProviderSimple* const pProvider,
const Endpoint start,
const Endpoint end,
const bool degenerate,
const COORD start,
const COORD end,
const std::wstring_view wordDelimiters,
_COM_Outptr_result_maybenull_ UiaTextRangeBase** ppUtr)
{
RETURN_HR_IF_NULL(E_INVALIDARG, ppUtr);
*ppUtr = nullptr;
UiaTextRange* result = nullptr;
RETURN_IF_FAILED(MakeAndInitialize<UiaTextRange>(&result, _pData, pProvider, start, end, degenerate, wordDelimiters));
RETURN_IF_FAILED(MakeAndInitialize<UiaTextRange>(&result, _pData, pProvider, start, end, wordDelimiters));
*ppUtr = result;
return S_OK;
}

View file

@ -60,9 +60,8 @@ namespace Microsoft::Terminal
// specific endpoint range
HRESULT CreateTextRange(_In_ IRawElementProviderSimple* const pProvider,
const Endpoint start,
const Endpoint end,
const bool degenerate,
const COORD start,
const COORD end,
const std::wstring_view wordDelimiters,
_COM_Outptr_result_maybenull_ Microsoft::Console::Types::UiaTextRangeBase** ppUtr) override;

View file

@ -24,12 +24,11 @@ HRESULT UiaTextRange::GetSelectionRanges(_In_ IUiaData* pData,
// create a range for each row
for (const auto& rect : rectangles)
{
ScreenInfoRow currentRow = rect.Top();
Endpoint start = _screenInfoRowToEndpoint(pData, currentRow) + rect.Left();
Endpoint end = _screenInfoRowToEndpoint(pData, currentRow) + rect.RightInclusive();
const auto start = rect.Origin();
const auto end = rect.EndExclusive();
ComPtr<UiaTextRange> range;
RETURN_IF_FAILED(MakeAndInitialize<UiaTextRange>(&range, pData, pProvider, start, end, false, wordDelimiters));
RETURN_IF_FAILED(MakeAndInitialize<UiaTextRange>(&range, pData, pProvider, start, end, wordDelimiters));
temporaryResult.emplace_back(std::move(range));
}
std::swap(temporaryResult, ranges);
@ -54,12 +53,11 @@ HRESULT UiaTextRange::RuntimeClassInitialize(_In_ IUiaData* pData,
HRESULT UiaTextRange::RuntimeClassInitialize(_In_ IUiaData* pData,
_In_ IRawElementProviderSimple* const pProvider,
const Endpoint start,
const Endpoint end,
const bool degenerate,
const COORD start,
const COORD end,
const std::wstring_view wordDelimiters)
{
return UiaTextRangeBase::RuntimeClassInitialize(pData, pProvider, start, end, degenerate, wordDelimiters);
return UiaTextRangeBase::RuntimeClassInitialize(pData, pProvider, start, end, wordDelimiters);
}
// returns a degenerate text range of the start of the row closest to the y value of point

View file

@ -44,9 +44,8 @@ namespace Microsoft::Terminal
// specific endpoint range
HRESULT RuntimeClassInitialize(_In_ Microsoft::Console::Types::IUiaData* pData,
_In_ IRawElementProviderSimple* const pProvider,
const Endpoint start,
const Endpoint end,
const bool degenerate,
const COORD start,
const COORD end,
const std::wstring_view wordDelimiters = DefaultWordDelimiter);
// range from a UiaPoint

View file

@ -5,6 +5,10 @@
#include "XamlUiaTextRange.h"
#include "UiaTextRange.hpp"
// the same as COR_E_NOTSUPPORTED
// we don't want to import the CLR headers to get it
#define XAML_E_NOT_SUPPORTED 0x80131515L
namespace UIA
{
using ::ITextRangeProvider;
@ -76,7 +80,7 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
// TODO GitHub #605: Search functionality
// we need to wrap this around the UiaTextRange FindText() function
// but right now it returns E_NOTIMPL, so let's just return nullptr for now.
throw winrt::hresult_not_implemented();
return nullptr;
}
winrt::Windows::Foundation::IInspectable XamlUiaTextRange::GetAttributeValue(int32_t textAttributeId) const
@ -88,7 +92,12 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
}
else
{
return nullptr;
// We _need_ to return XAML_E_NOT_SUPPORTED here.
// Returning nullptr is an improper implementation of it being unsupported.
// UIA Clients rely on this HRESULT to signify that the requested attribute is undefined.
// Anything else will result in the UIA Client refusing to read when navigating by word
// Magically, this doesn't affect other forms of navigation...
winrt::throw_hresult(XAML_E_NOT_SUPPORTED);
}
}

View file

@ -174,6 +174,7 @@ void Terminal::SelectNewRegion(const COORD coordStart, const COORD coordEnd)
SetSelectionAnchor(realCoordStart);
SetEndSelectionPosition(realCoordEnd);
_buffer->GetRenderTarget().TriggerSelection();
}
const std::wstring Terminal::GetConsoleTitle() const noexcept

View file

@ -145,6 +145,9 @@ class TextBufferTests
TEST_METHOD(ResizeTraditionalHighUnicodeColumnRemoval);
TEST_METHOD(TestBurrito);
void WriteLinesToBuffer(const std::vector<std::wstring>& text, TextBuffer& buffer);
TEST_METHOD(GetWordBoundaries);
};
void TextBufferTests::TestBufferCreate()
@ -2014,3 +2017,122 @@ void TextBufferTests::TestBurrito()
_buffer->IncrementCursor();
VERIFY_IS_FALSE(afterBurritoIter);
}
void TextBufferTests::WriteLinesToBuffer(const std::vector<std::wstring>& text, TextBuffer& buffer)
{
for (size_t row = 0; row < text.size(); ++row)
{
auto line = text[row];
OutputCellIterator iter{ line };
buffer.WriteLine(iter, { 0, gsl::narrow<SHORT>(row) });
}
}
void TextBufferTests::GetWordBoundaries()
{
COORD bufferSize{ 80, 9001 };
UINT cursorSize = 12;
TextAttribute attr{ 0x7f };
auto _buffer = std::make_unique<TextBuffer>(bufferSize, attr, cursorSize, _renderTarget);
// Setup: Write lines of text to the buffer
const std::vector<std::wstring> text = { L"word other",
L" more words" };
WriteLinesToBuffer(text, *_buffer);
// Test Data:
// - COORD - starting position
// - COORD - expected result (accessibilityMode = false)
// - COORD - expected result (accessibilityMode = true)
struct ExpectedResult
{
COORD accessibilityModeDisabled;
COORD accessibilityModeEnabled;
};
struct Test
{
COORD startPos;
ExpectedResult expected;
};
// Set testData for GetWordStart tests
// clang-format off
std::vector<Test> testData = {
// tests for first line of text
{ { 0, 0 }, {{ 0, 0 }, { 0, 0 }} },
{ { 1, 0 }, {{ 0, 0 }, { 0, 0 }} },
{ { 3, 0 }, {{ 0, 0 }, { 0, 0 }} },
{ { 4, 0 }, {{ 4, 0 }, { 0, 0 }} },
{ { 5, 0 }, {{ 5, 0 }, { 5, 0 }} },
{ { 6, 0 }, {{ 5, 0 }, { 5, 0 }} },
{ { 20, 0 }, {{ 10, 0 }, { 5, 0 }} },
{ { 79, 0 }, {{ 10, 0 }, { 5, 0 }} },
// tests for second line of text
{ { 0, 1 }, {{ 0, 1 }, { 0, 1 }} },
{ { 1, 1 }, {{ 0, 1 }, { 5, 0 }} },
{ { 2, 1 }, {{ 2, 1 }, { 2, 1 }} },
{ { 3, 1 }, {{ 2, 1 }, { 2, 1 }} },
{ { 5, 1 }, {{ 2, 1 }, { 2, 1 }} },
{ { 6, 1 }, {{ 6, 1 }, { 2, 1 }} },
{ { 7, 1 }, {{ 6, 1 }, { 2, 1 }} },
{ { 9, 1 }, {{ 9, 1 }, { 9, 1 }} },
{ { 10, 1 }, {{ 9, 1 }, { 9, 1 }} },
{ { 20, 1 }, {{14, 1 }, { 9, 1 }} },
{ { 79, 1 }, {{14, 1 }, { 9, 1 }} },
};
// clang-format on
BEGIN_TEST_METHOD_PROPERTIES()
TEST_METHOD_PROPERTY(L"Data:accessibilityMode", L"{false, true}")
END_TEST_METHOD_PROPERTIES();
bool accessibilityMode;
VERIFY_SUCCEEDED(TestData::TryGetValue(L"accessibilityMode", accessibilityMode), L"Get accessibility mode variant");
const std::wstring_view delimiters = L" ";
for (const auto& test : testData)
{
Log::Comment(NoThrowString().Format(L"COORD (%hd, %hd)", test.startPos.X, test.startPos.Y));
const auto result = _buffer->GetWordStart(test.startPos, delimiters, accessibilityMode);
const auto expected = accessibilityMode ? test.expected.accessibilityModeEnabled : test.expected.accessibilityModeDisabled;
VERIFY_ARE_EQUAL(expected, result);
}
// Update testData for GetWordEnd tests
// clang-format off
testData = {
// tests for first line of text
{ { 0, 0 }, { { 3, 0 }, { 5, 0 } } },
{ { 1, 0 }, { { 3, 0 }, { 5, 0 } } },
{ { 3, 0 }, { { 3, 0 }, { 5, 0 } } },
{ { 4, 0 }, { { 4, 0 }, { 5, 0 } } },
{ { 5, 0 }, { { 9, 0 }, { 2, 1 } } },
{ { 6, 0 }, { { 9, 0 }, { 2, 1 } } },
{ { 20, 0 }, { { 79, 0 }, { 2, 1 } } },
{ { 79, 0 }, { { 79, 0 }, { 2, 1 } } },
// tests for second line of text
{ { 0, 1 }, { { 1, 1 }, { 2, 1 } } },
{ { 1, 1 }, { { 1, 1 }, { 2, 1 } } },
{ { 2, 1 }, { { 5, 1 }, { 9, 1 } } },
{ { 3, 1 }, { { 5, 1 }, { 9, 1 } } },
{ { 5, 1 }, { { 5, 1 }, { 9, 1 } } },
{ { 6, 1 }, { { 8, 1 }, { 9, 1 } } },
{ { 7, 1 }, { { 8, 1 }, { 9, 1 } } },
{ { 9, 1 }, { { 13, 1 }, { 0, 9001 } } },
{ { 10, 1 }, { { 13, 1 }, { 0, 9001 } } },
{ { 20, 1 }, { { 79, 1 }, { 0, 9001 } } },
{ { 79, 1 }, { { 79, 1 }, { 0, 9001 } } },
};
// clang-format on
for (const auto& test : testData)
{
Log::Comment(NoThrowString().Format(L"COORD (%hd, %hd)", test.startPos.X, test.startPos.Y));
COORD result = _buffer->GetWordEnd(test.startPos, delimiters, accessibilityMode);
const auto expected = accessibilityMode ? test.expected.accessibilityModeEnabled : test.expected.accessibilityModeDisabled;
VERIFY_ARE_EQUAL(expected, result);
}
}

View file

@ -70,7 +70,10 @@
#include <CppCoreCheck/Warnings.h>
// Chromium Numerics (safe math)
#pragma warning(push)
#pragma warning(disable:4100) // unreferenced parameter
#include <base/numerics/safe_math.h>
#pragma warning(pop)
// IntSafe
#define ENABLE_INTSAFE_SIGNED_FUNCTIONS

View file

@ -143,16 +143,15 @@ HRESULT ScreenInfoUiaProvider::CreateTextRange(_In_ IRawElementProviderSimple* c
}
HRESULT ScreenInfoUiaProvider::CreateTextRange(_In_ IRawElementProviderSimple* const pProvider,
const Endpoint start,
const Endpoint end,
const bool degenerate,
const COORD start,
const COORD end,
const std::wstring_view wordDelimiters,
_COM_Outptr_result_maybenull_ UiaTextRangeBase** ppUtr)
{
RETURN_HR_IF_NULL(E_INVALIDARG, ppUtr);
*ppUtr = nullptr;
UiaTextRange* result = nullptr;
RETURN_IF_FAILED(MakeAndInitialize<UiaTextRange>(&result, _pData, pProvider, start, end, degenerate, wordDelimiters));
RETURN_IF_FAILED(MakeAndInitialize<UiaTextRange>(&result, _pData, pProvider, start, end, wordDelimiters));
*ppUtr = result;
return S_OK;
}

View file

@ -56,9 +56,8 @@ namespace Microsoft::Console::Interactivity::Win32
// specific endpoint range
HRESULT CreateTextRange(_In_ IRawElementProviderSimple* const pProvider,
const Endpoint start,
const Endpoint end,
const bool degenerate,
const COORD start,
const COORD end,
const std::wstring_view wordDelimiters,
_COM_Outptr_result_maybenull_ Microsoft::Console::Types::UiaTextRangeBase** ppUtr) override;

View file

@ -28,12 +28,11 @@ HRESULT UiaTextRange::GetSelectionRanges(_In_ IUiaData* pData,
// create a range for each row
for (const auto& rect : rectangles)
{
ScreenInfoRow currentRow = rect.Top();
Endpoint start = _screenInfoRowToEndpoint(pData, currentRow) + rect.Left();
Endpoint end = _screenInfoRowToEndpoint(pData, currentRow) + rect.RightInclusive();
const auto start = rect.Origin();
const auto end = rect.EndExclusive();
ComPtr<UiaTextRange> range;
RETURN_IF_FAILED(MakeAndInitialize<UiaTextRange>(&range, pData, pProvider, start, end, false, wordDelimiters));
RETURN_IF_FAILED(MakeAndInitialize<UiaTextRange>(&range, pData, pProvider, start, end, wordDelimiters));
temporaryResult.emplace_back(std::move(range));
}
std::swap(temporaryResult, ranges);
@ -60,12 +59,11 @@ HRESULT UiaTextRange::RuntimeClassInitialize(_In_ IUiaData* pData,
// specific endpoint range
HRESULT UiaTextRange::RuntimeClassInitialize(_In_ IUiaData* pData,
_In_ IRawElementProviderSimple* const pProvider,
const Endpoint start,
const Endpoint end,
const bool degenerate,
const COORD start,
const COORD end,
const std::wstring_view wordDelimiters)
{
return UiaTextRangeBase::RuntimeClassInitialize(pData, pProvider, start, end, degenerate, wordDelimiters);
return UiaTextRangeBase::RuntimeClassInitialize(pData, pProvider, start, end, wordDelimiters);
}
// returns a degenerate text range of the start of the row closest to the y value of point
@ -122,7 +120,7 @@ IFACEMETHODIMP UiaTextRange::FindText(_In_ BSTR text,
const auto sensitivity = ignoreCase ? Search::Sensitivity::CaseInsensitive : Search::Sensitivity::CaseSensitive;
auto searchDirection = Search::Direction::Forward;
Endpoint searchAnchor = _start;
auto searchAnchor = _start;
if (searchBackward)
{
searchDirection = Search::Direction::Backward;
@ -131,17 +129,19 @@ IFACEMETHODIMP UiaTextRange::FindText(_In_ BSTR text,
CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
THROW_HR_IF(E_POINTER, !gci.HasActiveOutputBuffer());
Search searcher{ gci.renderData, wstr, searchDirection, sensitivity, _endpointToCoord(_pData, searchAnchor) };
Search searcher{ gci.renderData, wstr, searchDirection, sensitivity, searchAnchor };
HRESULT hr = S_OK;
if (searcher.FindNext())
{
const auto foundLocation = searcher.GetFoundLocation();
const Endpoint start = _coordToEndpoint(_pData, foundLocation.first);
const Endpoint end = _coordToEndpoint(_pData, foundLocation.second);
const auto start = foundLocation.first;
const auto end = foundLocation.second;
const auto bufferSize = _pData->GetTextBuffer().GetSize();
// make sure what was found is within the bounds of the current range
if ((searchDirection == Search::Direction::Forward && end < _end) ||
(searchDirection == Search::Direction::Backward && start > _start))
if ((searchDirection == Search::Direction::Forward && bufferSize.CompareInBounds(end, _end) < 0) ||
(searchDirection == Search::Direction::Backward && bufferSize.CompareInBounds(start, _start) > 0))
{
hr = Clone(ppRetVal);
if (SUCCEEDED(hr))
@ -149,7 +149,6 @@ IFACEMETHODIMP UiaTextRange::FindText(_In_ BSTR text,
UiaTextRange& range = static_cast<UiaTextRange&>(**ppRetVal);
range._start = start;
range._end = end;
range._degenerate = false;
}
}
}

View file

@ -45,9 +45,8 @@ namespace Microsoft::Console::Interactivity::Win32
// specific endpoint range
HRESULT RuntimeClassInitialize(_In_ Microsoft::Console::Types::IUiaData* pData,
_In_ IRawElementProviderSimple* const pProvider,
const Endpoint start,
const Endpoint end,
const bool degenerate,
_In_ const COORD start,
_In_ const COORD end,
_In_ const std::wstring_view wordDelimiters = DefaultWordDelimiter);
// range from a UiaPoint

View file

@ -340,12 +340,11 @@ IFACEMETHODIMP ScreenInfoUiaProviderBase::GetVisibleRanges(_Outptr_result_mayben
RETURN_HR_IF_NULL(E_INVALIDARG, ppRetVal);
*ppRetVal = nullptr;
const auto viewport = _getViewport();
const COORD screenBufferCoords = _getScreenBufferCoords();
const int totalLines = screenBufferCoords.Y;
const auto bufferSize = _pData->GetTextBuffer().GetSize();
const auto viewport = bufferSize.ConvertToOrigin(_getViewport());
// make a safe array
const size_t rowCount = viewport.Height();
const auto rowCount = viewport.Height();
*ppRetVal = SafeArrayCreateVector(VT_UNKNOWN, 0, gsl::narrow<ULONG>(rowCount));
if (*ppRetVal == nullptr)
{
@ -353,19 +352,17 @@ IFACEMETHODIMP ScreenInfoUiaProviderBase::GetVisibleRanges(_Outptr_result_mayben
}
// stuff each visible line in the safearray
for (size_t i = 0; i < rowCount; ++i)
for (short i = 0; i < rowCount; ++i)
{
const int lineNumber = (viewport.Top() + i) % totalLines;
const int start = lineNumber * screenBufferCoords.X;
// - 1 to get the last column in the row
const int end = start + screenBufferCoords.X - 1;
// end is exclusive so add 1
const COORD start{ viewport.Left(), viewport.Top() + i };
const COORD end{ start.X, start.Y + 1 };
HRESULT hr = S_OK;
WRL::ComPtr<UiaTextRangeBase> range;
hr = CreateTextRange(this,
start,
end,
false,
_wordDelimiters,
&range);
if (FAILED(hr))

View file

@ -90,9 +90,8 @@ namespace Microsoft::Console::Types
// specific endpoint range
virtual HRESULT CreateTextRange(_In_ IRawElementProviderSimple* const pProvider,
const Endpoint start,
const Endpoint end,
const bool degenerate,
const COORD start,
const COORD end,
const std::wstring_view wordDelimiters,
_COM_Outptr_result_maybenull_ UiaTextRangeBase** ppUtr) = 0;

File diff suppressed because it is too large Load diff

View file

@ -33,44 +33,12 @@ Author(s):
class UiaTextRangeTests;
#endif
// The UiaTextRangeBase deals with several data structures that have
// similar semantics. In order to keep the information from these data
// structures separated, each structure has its own naming for a
// row.
//
// There is the generic Row, which does not know which data structure
// the row came from.
//
// There is the ViewportRow, which is a 0-indexed row value from the
// viewport. The top row of the viewport is at 0, rows below the top
// row increase in value and rows above the top row get increasingly
// negative.
//
// ScreenInfoRow is a row from the screen info data structure. They
// start at 0 at the top of screen info buffer. Their positions do not
// change but their associated row in the text buffer does change each
// time a new line is written.
//
// TextBufferRow is a row from the text buffer. It is not a ROW
// struct, but rather the index of a row. This is also 0-indexed. A
// TextBufferRow with a value of 0 does not necessarily refer to the
// top row of the console.
typedef int Row;
typedef int ViewportRow;
typedef unsigned int ScreenInfoRow;
typedef unsigned int TextBufferRow;
typedef unsigned long long IdType;
// A Column is a row agnostic value that refers to the column an
// endpoint is equivalent to. It is 0-indexed.
typedef unsigned int Column;
// an endpoint is a char location in the text buffer. endpoint 0 is
// the first char of the 0th row in the text buffer row array.
typedef unsigned int Endpoint;
constexpr IdType InvalidId = 0;
namespace Microsoft::Console::Types
@ -89,55 +57,6 @@ namespace Microsoft::Console::Types
Backward
};
// valid increment amounts for forward and
// backward movement
enum class MovementIncrement
{
Forward = 1,
Backward = -1
};
// common information used by the variety of
// movement operations
struct MoveState
{
// screen/column position of _start
ScreenInfoRow StartScreenInfoRow;
Column StartColumn;
// screen/column position of _end
ScreenInfoRow EndScreenInfoRow;
Column EndColumn;
// last row in the direction being moved
ScreenInfoRow LimitingRow;
// first column in the direction being moved
Column FirstColumnInRow;
// last column in the direction being moved
Column LastColumnInRow;
// increment amount
MovementIncrement Increment;
// direction moving
MovementDirection Direction;
MoveState(IUiaData* pData,
const UiaTextRangeBase& range,
const MovementDirection direction);
private:
MoveState(const ScreenInfoRow startScreenInfoRow,
const Column startColumn,
const ScreenInfoRow endScreenInfoRow,
const Column endColumn,
const ScreenInfoRow limitingRow,
const Column firstColumnInRow,
const Column lastColumnInRow,
const MovementIncrement increment,
const MovementDirection direction) noexcept;
#ifdef UNIT_TESTING
friend class ::UiaTextRangeTests;
#endif
};
public:
// The default word delimiter for UiaTextRanges
static constexpr std::wstring_view DefaultWordDelimiter{ &UNICODE_SPACE, 1 };
@ -150,15 +69,14 @@ namespace Microsoft::Console::Types
// degenerate range at cursor position
HRESULT RuntimeClassInitialize(_In_ IUiaData* pData,
_In_ IRawElementProviderSimple* const pProvider,
const Cursor& cursor,
_In_ const Cursor& cursor,
_In_ std::wstring_view wordDelimiters = DefaultWordDelimiter) noexcept;
// specific endpoint range
HRESULT RuntimeClassInitialize(_In_ IUiaData* pData,
_In_ IRawElementProviderSimple* const pProvider,
const Endpoint start,
const Endpoint end,
const bool degenerate,
_In_ const COORD start,
_In_ const COORD end,
_In_ std::wstring_view wordDelimiters = DefaultWordDelimiter) noexcept;
HRESULT RuntimeClassInitialize(const UiaTextRangeBase& a) noexcept;
@ -169,14 +87,10 @@ namespace Microsoft::Console::Types
~UiaTextRangeBase() = default;
const IdType GetId() const noexcept;
const Endpoint GetStart() const noexcept;
const Endpoint GetEnd() const noexcept;
const COORD GetEndpoint(TextPatternRangeEndpoint endpoint) const noexcept;
bool SetEndpoint(TextPatternRangeEndpoint endpoint, const COORD val);
const bool IsDegenerate() const noexcept;
// TODO GitHub #605:
// only used for UiaData::FindText. Remove after Search added properly
void SetRangeValues(const Endpoint start, const Endpoint end, const bool isDegenerate) noexcept;
// ITextRangeProvider methods
virtual IFACEMETHODIMP Clone(_Outptr_result_maybenull_ ITextRangeProvider** ppRetVal) = 0;
IFACEMETHODIMP Compare(_In_opt_ ITextRangeProvider* pRange, _Out_ BOOL* pRetVal) noexcept override;
@ -184,7 +98,7 @@ namespace Microsoft::Console::Types
_In_ ITextRangeProvider* pTargetRange,
_In_ TextPatternRangeEndpoint targetEndpoint,
_Out_ int* pRetVal) noexcept override;
IFACEMETHODIMP ExpandToEnclosingUnit(_In_ TextUnit unit) override;
IFACEMETHODIMP ExpandToEnclosingUnit(_In_ TextUnit unit) noexcept override;
IFACEMETHODIMP FindAttribute(_In_ TEXTATTRIBUTEID textAttributeId,
_In_ VARIANT val,
_In_ BOOL searchBackward,
@ -195,30 +109,29 @@ namespace Microsoft::Console::Types
_Outptr_result_maybenull_ ITextRangeProvider** ppRetVal) = 0;
IFACEMETHODIMP GetAttributeValue(_In_ TEXTATTRIBUTEID textAttributeId,
_Out_ VARIANT* pRetVal) noexcept override;
IFACEMETHODIMP GetBoundingRectangles(_Outptr_result_maybenull_ SAFEARRAY** ppRetVal) override;
IFACEMETHODIMP GetEnclosingElement(_Outptr_result_maybenull_ IRawElementProviderSimple** ppRetVal) override;
IFACEMETHODIMP GetBoundingRectangles(_Outptr_result_maybenull_ SAFEARRAY** ppRetVal) noexcept override;
IFACEMETHODIMP GetEnclosingElement(_Outptr_result_maybenull_ IRawElementProviderSimple** ppRetVal) noexcept override;
IFACEMETHODIMP GetText(_In_ int maxLength,
_Out_ BSTR* pRetVal) override;
_Out_ BSTR* pRetVal) noexcept override;
IFACEMETHODIMP Move(_In_ TextUnit unit,
_In_ int count,
_Out_ int* pRetVal) override;
_Out_ int* pRetVal) noexcept override;
IFACEMETHODIMP MoveEndpointByUnit(_In_ TextPatternRangeEndpoint endpoint,
_In_ TextUnit unit,
_In_ int count,
_Out_ int* pRetVal) override;
_Out_ int* pRetVal) noexcept override;
IFACEMETHODIMP MoveEndpointByRange(_In_ TextPatternRangeEndpoint endpoint,
_In_ ITextRangeProvider* pTargetRange,
_In_ TextPatternRangeEndpoint targetEndpoint) override;
IFACEMETHODIMP Select() override;
_In_ TextPatternRangeEndpoint targetEndpoint) noexcept override;
IFACEMETHODIMP Select() noexcept override;
IFACEMETHODIMP AddToSelection() noexcept override;
IFACEMETHODIMP RemoveFromSelection() noexcept override;
IFACEMETHODIMP ScrollIntoView(_In_ BOOL alignToTop) override;
IFACEMETHODIMP ScrollIntoView(_In_ BOOL alignToTop) noexcept override;
IFACEMETHODIMP GetChildren(_Outptr_result_maybenull_ SAFEARRAY** ppRetVal) noexcept override;
protected:
UiaTextRangeBase() = default;
#if _DEBUG
void _outputRowConversions(IUiaData* pData);
void _outputObjectState();
#endif
IUiaData* _pData;
@ -237,206 +150,42 @@ namespace Microsoft::Console::Types
// between the provider and the client
IdType _id;
// measure units in the form [_start, _end]. _start
// may be a bigger number than _end if the range
// wraps around the end of the text buffer.
//
// In this scenario, _start <= _end
// 0 ............... N (text buffer line indices)
// s-----e (_start to _end)
//
// In this scenario, _start >= end
// 0 ............... N (text buffer line indices)
// ---e s----- (_start to _end)
//
Endpoint _start;
Endpoint _end;
// The msdn documentation (and hence this class) talks a bunch about a
// degenerate range. A range is degenerate if it contains
// no text (both the start and end endpoints are the same). Note that
// a degenerate range may have a position in the text. We indicate a
// degenerate range internally with a bool. If a range is degenerate
// then both endpoints will contain the same value.
bool _degenerate;
// measure units in the form [_start, _end).
// These are in the TextBuffer coordinate space.
// NOTE: _start is inclusive, but _end is exclusive
COORD _start;
COORD _end;
RECT _getTerminalRect() const;
static const COORD _getScreenBufferCoords(gsl::not_null<IUiaData*> pData);
virtual const COORD _getScreenFontSize() const;
const unsigned int _getViewportHeight(const SMALL_RECT viewport) const noexcept;
static const unsigned int _getTotalRows(gsl::not_null<IUiaData*> pData) noexcept;
static const unsigned int _getRowWidth(gsl::not_null<IUiaData*> pData);
void _getBoundingRect(_In_ const COORD startAnchor, _In_ const COORD endAnchor, _Inout_ std::vector<double>& coords) const;
static const unsigned int _getFirstScreenInfoRowIndex() noexcept;
static const unsigned int _getLastScreenInfoRowIndex(gsl::not_null<IUiaData*> pData) noexcept;
void
_moveEndpointByUnitCharacter(_In_ const int moveCount,
_In_ const TextPatternRangeEndpoint endpoint,
_Out_ gsl::not_null<int*> const pAmountMoved,
_In_ const bool preventBufferEnd = false);
static const Column _getFirstColumnIndex() noexcept;
static const Column _getLastColumnIndex(gsl::not_null<IUiaData*> pData);
void
_moveEndpointByUnitWord(_In_ const int moveCount,
_In_ const TextPatternRangeEndpoint endpoint,
_Out_ gsl::not_null<int*> const pAmountMoved,
_In_ const bool preventBufferEnd = false);
const unsigned int _rowCountInRange(gsl::not_null<IUiaData*> pData) const;
void
_moveEndpointByUnitLine(_In_ const int moveCount,
_In_ const TextPatternRangeEndpoint endpoint,
_Out_ gsl::not_null<int*> const pAmountMoved,
_In_ const bool preventBufferEnd = false);
static const TextBufferRow _endpointToTextBufferRow(gsl::not_null<IUiaData*> pData,
const Endpoint endpoint);
static const ScreenInfoRow _textBufferRowToScreenInfoRow(gsl::not_null<IUiaData*> pData,
const TextBufferRow row) noexcept;
static const TextBufferRow _screenInfoRowToTextBufferRow(gsl::not_null<IUiaData*> pData,
const ScreenInfoRow row) noexcept;
static const Endpoint _textBufferRowToEndpoint(gsl::not_null<IUiaData*> pData, const TextBufferRow row);
static const Endpoint _wordBeginEndpoint(gsl::not_null<IUiaData*> pData, Endpoint target, const std::wstring_view wordDelimiters);
static const Endpoint _wordEndEndpoint(gsl::not_null<IUiaData*> pData, Endpoint target, const std::wstring_view wordDelimiters);
static const ScreenInfoRow _endpointToScreenInfoRow(gsl::not_null<IUiaData*> pData,
const Endpoint endpoint);
static const Endpoint _screenInfoRowToEndpoint(gsl::not_null<IUiaData*> pData,
const ScreenInfoRow row);
static COORD _endpointToCoord(gsl::not_null<IUiaData*> pData,
const Endpoint endpoint);
static Endpoint _coordToEndpoint(gsl::not_null<IUiaData*> pData,
const COORD coord);
static const Column _endpointToColumn(gsl::not_null<IUiaData*> pData,
const Endpoint endpoint);
static const Row _normalizeRow(gsl::not_null<IUiaData*> pData, const Row row) noexcept;
static const ViewportRow _screenInfoRowToViewportRow(gsl::not_null<IUiaData*> pData,
const ScreenInfoRow row) noexcept;
// Routine Description:
// - Converts a ScreenInfoRow to a ViewportRow.
// Arguments:
// - row - the ScreenInfoRow to convert
// - viewport - the viewport to use for the conversion
// Return Value:
// - the equivalent ViewportRow.
static constexpr const ViewportRow _screenInfoRowToViewportRow(const ScreenInfoRow row,
const SMALL_RECT viewport) noexcept
{
return row - viewport.Top;
}
static const bool _isScreenInfoRowInViewport(gsl::not_null<IUiaData*> pData,
const ScreenInfoRow row) noexcept;
static const bool _isScreenInfoRowInViewport(const ScreenInfoRow row,
const SMALL_RECT viewport) noexcept;
static const unsigned int _getViewportHeight(const SMALL_RECT viewport) noexcept;
static const unsigned int _getViewportWidth(const SMALL_RECT viewport) noexcept;
void _addScreenInfoRowBoundaries(gsl::not_null<IUiaData*> pData,
const ScreenInfoRow screenInfoRow,
_Inout_ std::vector<double>& coords) const;
static const int _compareScreenCoords(gsl::not_null<IUiaData*> pData,
const ScreenInfoRow rowA,
const Column colA,
const ScreenInfoRow rowB,
const Column colB);
static std::pair<Endpoint, Endpoint> _moveByCharacter(gsl::not_null<IUiaData*> pData,
const int moveCount,
const MoveState moveState,
_Out_ gsl::not_null<int*> const pAmountMoved);
static std::pair<Endpoint, Endpoint> _moveByCharacterForward(gsl::not_null<IUiaData*> pData,
const int moveCount,
const MoveState moveState,
_Out_ gsl::not_null<int*> const pAmountMoved);
static std::pair<Endpoint, Endpoint> _moveByCharacterBackward(gsl::not_null<IUiaData*> pData,
const int moveCount,
const MoveState moveState,
_Out_ gsl::not_null<int*> const pAmountMoved);
static std::pair<Endpoint, Endpoint> _moveByWord(gsl::not_null<IUiaData*> pData,
const int moveCount,
const MoveState moveState,
const std::wstring_view wordDelimiters,
_Out_ gsl::not_null<int*> const pAmountMoved);
static std::pair<Endpoint, Endpoint> _moveByWordForward(gsl::not_null<IUiaData*> pData,
const int moveCount,
const MoveState moveState,
const std::wstring_view wordDelimiters,
_Out_ gsl::not_null<int*> const pAmountMoved);
static std::pair<Endpoint, Endpoint> _moveByWordBackward(gsl::not_null<IUiaData*> pData,
const int moveCount,
const MoveState moveState,
const std::wstring_view wordDelimiters,
_Out_ gsl::not_null<int*> const pAmountMoved);
static std::pair<Endpoint, Endpoint> _moveByLine(gsl::not_null<IUiaData*> pData,
const int moveCount,
const MoveState moveState,
_Out_ gsl::not_null<int*> const pAmountMoved);
static std::pair<Endpoint, Endpoint> _moveByDocument(gsl::not_null<IUiaData*> pData,
const int moveCount,
const MoveState moveState,
_Out_ gsl::not_null<int*> const pAmountMoved);
static std::tuple<Endpoint, Endpoint, bool>
_moveEndpointByUnitCharacter(gsl::not_null<IUiaData*> pData,
const int moveCount,
const TextPatternRangeEndpoint endpoint,
const MoveState moveState,
_Out_ gsl::not_null<int*> const pAmountMoved);
static std::tuple<Endpoint, Endpoint, bool>
_moveEndpointByUnitCharacterForward(gsl::not_null<IUiaData*> pData,
const int moveCount,
const TextPatternRangeEndpoint endpoint,
const MoveState moveState,
_Out_ gsl::not_null<int*> const pAmountMoved);
static std::tuple<Endpoint, Endpoint, bool>
_moveEndpointByUnitCharacterBackward(gsl::not_null<IUiaData*> pData,
const int moveCount,
const TextPatternRangeEndpoint endpoint,
const MoveState moveState,
_Out_ gsl::not_null<int*> const pAmountMoved);
static std::tuple<Endpoint, Endpoint, bool>
_moveEndpointByUnitWord(gsl::not_null<IUiaData*> pData,
const int moveCount,
const TextPatternRangeEndpoint endpoint,
const MoveState moveState,
const std::wstring_view wordDelimiters,
_Out_ gsl::not_null<int*> const pAmountMoved);
static std::tuple<Endpoint, Endpoint, bool>
_moveEndpointByUnitWordForward(gsl::not_null<IUiaData*> pData,
const int moveCount,
const TextPatternRangeEndpoint endpoint,
const MoveState moveState,
const std::wstring_view wordDelimiters,
_Out_ gsl::not_null<int*> const pAmountMoved);
static std::tuple<Endpoint, Endpoint, bool>
_moveEndpointByUnitWordBackward(gsl::not_null<IUiaData*> pData,
const int moveCount,
const TextPatternRangeEndpoint endpoint,
const MoveState moveState,
const std::wstring_view wordDelimiters,
_Out_ gsl::not_null<int*> const pAmountMoved);
static std::tuple<Endpoint, Endpoint, bool>
_moveEndpointByUnitLine(gsl::not_null<IUiaData*> pData,
const int moveCount,
const TextPatternRangeEndpoint endpoint,
const MoveState moveState,
_Out_ gsl::not_null<int*> const pAmountMoved);
static std::tuple<Endpoint, Endpoint, bool>
_moveEndpointByUnitDocument(gsl::not_null<IUiaData*> pData,
const int moveCount,
const TextPatternRangeEndpoint endpoint,
const MoveState moveState,
_Out_ gsl::not_null<int*> const pAmountMoved);
void
_moveEndpointByUnitDocument(_In_ const int moveCount,
_In_ const TextPatternRangeEndpoint endpoint,
_Out_ gsl::not_null<int*> const pAmountMoved,
_In_ const bool preventBufferEnd = false);
#ifdef UNIT_TESTING
friend class ::UiaTextRangeTests;
@ -499,8 +248,8 @@ namespace Microsoft::Console::Types
struct ApiMsgExpandToEnclosingUnit : public IApiMsg
{
TextUnit Unit;
Endpoint OriginalStart;
Endpoint OriginalEnd;
COORD OriginalStart;
COORD OriginalEnd;
};
struct ApiMsgGetText : IApiMsg
@ -510,8 +259,8 @@ namespace Microsoft::Console::Types
struct ApiMsgMove : IApiMsg
{
Endpoint OriginalStart;
Endpoint OriginalEnd;
COORD OriginalStart;
COORD OriginalEnd;
TextUnit Unit;
int RequestedCount;
int MovedCount;
@ -519,8 +268,8 @@ namespace Microsoft::Console::Types
struct ApiMsgMoveEndpointByUnit : IApiMsg
{
Endpoint OriginalStart;
Endpoint OriginalEnd;
COORD OriginalStart;
COORD OriginalEnd;
TextPatternRangeEndpoint Endpoint;
TextUnit Unit;
int RequestedCount;
@ -529,8 +278,8 @@ namespace Microsoft::Console::Types
struct ApiMsgMoveEndpointByRange : IApiMsg
{
Endpoint OriginalStart;
Endpoint OriginalEnd;
COORD OriginalStart;
COORD OriginalEnd;
TextPatternRangeEndpoint Endpoint;
TextPatternRangeEndpoint TargetEndpoint;
IdType OtherId;

View file

@ -57,20 +57,21 @@ namespace Microsoft::Console::Types
SHORT Height() const noexcept;
SHORT Width() const noexcept;
COORD Origin() const noexcept;
COORD EndExclusive() const noexcept;
COORD Dimensions() const noexcept;
bool IsInBounds(const Viewport& other) const noexcept;
bool IsInBounds(const COORD& pos) const noexcept;
bool IsInBounds(const COORD& pos, bool allowEndExclusive = false) const noexcept;
void Clamp(COORD& pos) const;
Viewport Clamp(const Viewport& other) const noexcept;
bool MoveInBounds(const ptrdiff_t move, COORD& pos) const noexcept;
bool IncrementInBounds(COORD& pos) const noexcept;
bool IncrementInBounds(COORD& pos, bool allowEndExclusive = false) const noexcept;
bool IncrementInBoundsCircular(COORD& pos) const noexcept;
bool DecrementInBounds(COORD& pos) const noexcept;
bool DecrementInBounds(COORD& pos, bool allowEndExclusive = false) const noexcept;
bool DecrementInBoundsCircular(COORD& pos) const noexcept;
int CompareInBounds(const COORD& first, const COORD& second) const noexcept;
int CompareInBounds(const COORD& first, const COORD& second, bool allowEndExclusive = false) const noexcept;
enum class XWalk
{
@ -90,8 +91,8 @@ namespace Microsoft::Console::Types
const YWalk y;
};
bool WalkInBounds(COORD& pos, const WalkDir dir) const noexcept;
bool WalkInBoundsCircular(COORD& pos, const WalkDir dir) const noexcept;
bool WalkInBounds(COORD& pos, const WalkDir dir, bool allowEndExclusive = false) const noexcept;
bool WalkInBoundsCircular(COORD& pos, const WalkDir dir, bool allowEndExclusive = false) const noexcept;
COORD GetWalkOrigin(const WalkDir dir) const noexcept;
static WalkDir DetermineWalkDirection(const Viewport& source, const Viewport& target) noexcept;

View file

@ -137,6 +137,19 @@ COORD Viewport::Origin() const noexcept
return { Left(), Top() };
}
// Method Description:
// - For Accessibility, get a COORD representing the end of this viewport in exclusive terms.
// - This is needed to represent an exclusive endpoint in UiaTextRange that includes the last
// COORD's text in the buffer at (RightInclusive(), BottomInclusive())
// Arguments:
// - <none>
// Return Value:
// - the coordinates of this viewport's end.
COORD Viewport::EndExclusive() const noexcept
{
return { Left(), BottomExclusive() };
}
// Method Description:
// - Get a coord representing the dimensions of this viewport.
// Arguments:
@ -166,10 +179,18 @@ bool Viewport::IsInBounds(const Viewport& other) const noexcept
// - Determines if the given coordinate position lies within this viewport.
// Arguments:
// - pos - Coordinate position
// - allowEndExclusive - if true, allow the EndExclusive COORD as a valid position.
// Used in accessibility to signify that the exclusive end
// includes the last COORD in a given viewport.
// Return Value:
// - True if it lies inside the viewport. False otherwise.
bool Viewport::IsInBounds(const COORD& pos) const noexcept
bool Viewport::IsInBounds(const COORD& pos, bool allowEndExclusive) const noexcept
{
if (allowEndExclusive && pos == EndExclusive())
{
return true;
}
return pos.X >= Left() && pos.X < RightExclusive() &&
pos.Y >= Top() && pos.Y < BottomExclusive();
}
@ -255,11 +276,14 @@ bool Viewport::MoveInBounds(const ptrdiff_t move, COORD& pos) const noexcept
// - Increments the given coordinate within the bounds of this viewport.
// Arguments:
// - pos - Coordinate position that will be incremented, if it can be.
// - allowEndExclusive - if true, allow the EndExclusive COORD as a valid position.
// Used in accessibility to signify that the exclusive end
// includes the last COORD in a given viewport.
// Return Value:
// - True if it could be incremented. False if it would move outside.
bool Viewport::IncrementInBounds(COORD& pos) const noexcept
bool Viewport::IncrementInBounds(COORD& pos, bool allowEndExclusive) const noexcept
{
return WalkInBounds(pos, { XWalk::LeftToRight, YWalk::TopToBottom });
return WalkInBounds(pos, { XWalk::LeftToRight, YWalk::TopToBottom }, allowEndExclusive);
}
// Method Description:
@ -279,11 +303,14 @@ bool Viewport::IncrementInBoundsCircular(COORD& pos) const noexcept
// - Decrements the given coordinate within the bounds of this viewport.
// Arguments:
// - pos - Coordinate position that will be incremented, if it can be.
// - allowEndExclusive - if true, allow the EndExclusive COORD as a valid position.
// Used in accessibility to signify that the exclusive end
// includes the last COORD in a given viewport.
// Return Value:
// - True if it could be incremented. False if it would move outside.
bool Viewport::DecrementInBounds(COORD& pos) const noexcept
bool Viewport::DecrementInBounds(COORD& pos, bool allowEndExclusive) const noexcept
{
return WalkInBounds(pos, { XWalk::RightToLeft, YWalk::BottomToTop });
return WalkInBounds(pos, { XWalk::RightToLeft, YWalk::BottomToTop }, allowEndExclusive);
}
// Method Description:
@ -304,6 +331,9 @@ bool Viewport::DecrementInBoundsCircular(COORD& pos) const noexcept
// Arguments:
// - first- The first coordinate position
// - second - The second coordinate position
// - allowEndExclusive - if true, allow the EndExclusive COORD as a valid position.
// Used in accessibility to signify that the exclusive end
// includes the last COORD in a given viewport.
// Return Value:
// - Negative if First is to the left of the Second.
// - 0 if First and Second are the same coordinate.
@ -311,11 +341,11 @@ bool Viewport::DecrementInBoundsCircular(COORD& pos) const noexcept
// - This is so you can do s_CompareCoords(first, second) <= 0 for "first is left or the same as second".
// (the < looks like a left arrow :D)
// - The magnitude of the result is the distance between the two coordinates when typing characters into the buffer (left to right, top to bottom)
int Viewport::CompareInBounds(const COORD& first, const COORD& second) const noexcept
int Viewport::CompareInBounds(const COORD& first, const COORD& second, bool allowEndExclusive) const noexcept
{
// Assert that our coordinates are within the expected boundaries
FAIL_FAST_IF(!IsInBounds(first));
FAIL_FAST_IF(!IsInBounds(second));
FAIL_FAST_IF(!IsInBounds(first, allowEndExclusive));
FAIL_FAST_IF(!IsInBounds(second, allowEndExclusive));
// First set the distance vertically
// If first is on row 4 and second is on row 6, first will be -2 rows behind second * an 80 character row would be -160.
@ -342,12 +372,15 @@ int Viewport::CompareInBounds(const COORD& first, const COORD& second) const noe
// Arguments:
// - pos - Coordinate position that will be adjusted, if it can be.
// - dir - Walking direction specifying which direction to go when reaching the end of a row/column
// - allowEndExclusive - if true, allow the EndExclusive COORD as a valid position.
// Used in accessibility to signify that the exclusive end
// includes the last COORD in a given viewport.
// Return Value:
// - True if it could be adjusted as specified and remain in bounds. False if it would move outside.
bool Viewport::WalkInBounds(COORD& pos, const WalkDir dir) const noexcept
bool Viewport::WalkInBounds(COORD& pos, const WalkDir dir, bool allowEndExclusive) const noexcept
{
auto copy = pos;
if (WalkInBoundsCircular(copy, dir))
if (WalkInBoundsCircular(copy, dir, allowEndExclusive))
{
pos = copy;
return true;
@ -365,25 +398,37 @@ bool Viewport::WalkInBounds(COORD& pos, const WalkDir dir) const noexcept
// Arguments:
// - pos - Coordinate position that will be adjusted.
// - dir - Walking direction specifying which direction to go when reaching the end of a row/column
// - allowEndExclusive - if true, allow the EndExclusive COORD as a valid position.
// Used in accessibility to signify that the exclusive end
// includes the last COORD in a given viewport.
// Return Value:
// - True if it could be adjusted inside the viewport.
// - False if it rolled over from the final corner back to the initial corner
// for the specified walk direction.
bool Viewport::WalkInBoundsCircular(COORD& pos, const WalkDir dir) const noexcept
bool Viewport::WalkInBoundsCircular(COORD& pos, const WalkDir dir, bool allowEndExclusive) const noexcept
{
// Assert that the position given fits inside this viewport.
FAIL_FAST_IF(!IsInBounds(pos));
FAIL_FAST_IF(!IsInBounds(pos, allowEndExclusive));
if (dir.x == XWalk::LeftToRight)
{
if (pos.X == RightInclusive())
if (allowEndExclusive && pos.X == Left() && pos.Y == BottomExclusive())
{
pos.Y = Top();
return false;
}
else if (pos.X == RightInclusive())
{
pos.X = Left();
if (dir.y == YWalk::TopToBottom)
{
pos.Y++;
if (pos.Y > BottomInclusive())
if (allowEndExclusive && pos.Y == BottomExclusive())
{
return true;
}
else if (pos.Y > BottomInclusive())
{
pos.Y = Top();
return false;