diff --git a/src/buffer/out/textBuffer.cpp b/src/buffer/out/textBuffer.cpp index 99cdbc78e54..4fb59de5d3f 100644 --- a/src/buffer/out/textBuffer.cpp +++ b/src/buffer/out/textBuffer.cpp @@ -956,57 +956,214 @@ 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) + if (_GetDelimiterClass(*bufferIterator, wordDelimiters) != initialDelimiter) + { + // move off of delimiter + bufferSize.IncrementInBounds(result); + } + + return result; +} + +// Method Description: +// - 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 +// - 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 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) { - // include character run for readable word - if (_GetDelimiterClass(*bufferIterator, wordDelimiters) == DelimiterClass::RegularChar) + if (bufferSize.IncrementInBounds(result, true)) { - result = GetWordStart(result, wordDelimiters); + ++bufferIterator; + } + else + { + // last char in buffer is a RegularChar + // we can't move any further forward + break; } } - else if (_GetDelimiterClass(*bufferIterator, wordDelimiters) != initialDelimiter) + + // make sure we expand to the beginning of the NEXT word + while (_GetDelimiterClass(*bufferIterator, wordDelimiters) != DelimiterClass::RegularChar) { - // move off of delimiter - bufferSize.IncrementInBounds(result); + 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: -// - Get the COORD for the end of the word you are on +// - 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 -// - includeDelimiterRun - include the delimiter runs located at the end of the word // 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 +// - 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,29 +1171,123 @@ 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) + if (_GetDelimiterClass(*bufferIterator, wordDelimiters) != initialDelimiter) + { + // move off of delimiter + bufferSize.DecrementInBounds(result); + } + + 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) { - // include delimiter run after word - if (_GetDelimiterClass(*bufferIterator, wordDelimiters) != DelimiterClass::RegularChar) + return false; + } + + // on whitespace, continue until the beginning of the next word + while (delimiterClass != DelimiterClass::RegularChar) + { + if (!bufferSize.IncrementInBounds(copy)) { - result = GetWordEnd(result, wordDelimiters); + // last char in buffer is a DelimiterChar or ControlChar + // there is no next word + return false; } + text = GetTextDataAt(copy)->data(); + delimiterClass = _GetDelimiterClass(text, wordDelimiters); } - else if (_GetDelimiterClass(*bufferIterator, wordDelimiters) != initialDelimiter) + + // 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) { - // move off of delimiter - bufferSize.DecrementInBounds(result); + 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); } - return result; + // 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: diff --git a/src/buffer/out/textBuffer.hpp b/src/buffer/out/textBuffer.hpp index 4d70bb48303..7400efb353a 100644 --- a/src/buffer/out/textBuffer.hpp +++ b/src/buffer/out/textBuffer.hpp @@ -130,8 +130,10 @@ class TextBuffer final 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 @@ class TextBuffer final 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; diff --git a/src/cascadia/TerminalControl/TermControlUiaProvider.cpp b/src/cascadia/TerminalControl/TermControlUiaProvider.cpp index 9610feb51cd..353d30a28bf 100644 --- a/src/cascadia/TerminalControl/TermControlUiaProvider.cpp +++ b/src/cascadia/TerminalControl/TermControlUiaProvider.cpp @@ -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(&result, _pData, pProvider, start, end, degenerate, wordDelimiters)); + RETURN_IF_FAILED(MakeAndInitialize(&result, _pData, pProvider, start, end, wordDelimiters)); *ppUtr = result; return S_OK; } diff --git a/src/cascadia/TerminalControl/TermControlUiaProvider.hpp b/src/cascadia/TerminalControl/TermControlUiaProvider.hpp index 914962e123e..c90d3bf6ca3 100644 --- a/src/cascadia/TerminalControl/TermControlUiaProvider.hpp +++ b/src/cascadia/TerminalControl/TermControlUiaProvider.hpp @@ -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; diff --git a/src/cascadia/TerminalControl/UiaTextRange.cpp b/src/cascadia/TerminalControl/UiaTextRange.cpp index 6552acb38c9..9b82aa1e8ed 100644 --- a/src/cascadia/TerminalControl/UiaTextRange.cpp +++ b/src/cascadia/TerminalControl/UiaTextRange.cpp @@ -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 range; - RETURN_IF_FAILED(MakeAndInitialize(&range, pData, pProvider, start, end, false, wordDelimiters)); + RETURN_IF_FAILED(MakeAndInitialize(&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 diff --git a/src/cascadia/TerminalControl/UiaTextRange.hpp b/src/cascadia/TerminalControl/UiaTextRange.hpp index f3e30b91e6d..0113b485e34 100644 --- a/src/cascadia/TerminalControl/UiaTextRange.hpp +++ b/src/cascadia/TerminalControl/UiaTextRange.hpp @@ -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 diff --git a/src/cascadia/TerminalControl/XamlUiaTextRange.cpp b/src/cascadia/TerminalControl/XamlUiaTextRange.cpp index 80a9b623d40..d282eedd08b 100644 --- a/src/cascadia/TerminalControl/XamlUiaTextRange.cpp +++ b/src/cascadia/TerminalControl/XamlUiaTextRange.cpp @@ -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); } } diff --git a/src/cascadia/TerminalCore/terminalrenderdata.cpp b/src/cascadia/TerminalCore/terminalrenderdata.cpp index 81794c4fba2..5cae603791b 100644 --- a/src/cascadia/TerminalCore/terminalrenderdata.cpp +++ b/src/cascadia/TerminalCore/terminalrenderdata.cpp @@ -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 diff --git a/src/host/ut_host/TextBufferTests.cpp b/src/host/ut_host/TextBufferTests.cpp index e4af5b5e1ab..5a9dbbb3eb1 100644 --- a/src/host/ut_host/TextBufferTests.cpp +++ b/src/host/ut_host/TextBufferTests.cpp @@ -145,6 +145,9 @@ class TextBufferTests TEST_METHOD(ResizeTraditionalHighUnicodeColumnRemoval); TEST_METHOD(TestBurrito); + + void WriteLinesToBuffer(const std::vector& 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& 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(row) }); + } +} + +void TextBufferTests::GetWordBoundaries() +{ + COORD bufferSize{ 80, 9001 }; + UINT cursorSize = 12; + TextAttribute attr{ 0x7f }; + auto _buffer = std::make_unique(bufferSize, attr, cursorSize, _renderTarget); + + // Setup: Write lines of text to the buffer + const std::vector 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 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); + } +} diff --git a/src/inc/LibraryIncludes.h b/src/inc/LibraryIncludes.h index ef0331689ba..61399cde05d 100644 --- a/src/inc/LibraryIncludes.h +++ b/src/inc/LibraryIncludes.h @@ -70,7 +70,10 @@ #include // Chromium Numerics (safe math) +#pragma warning(push) +#pragma warning(disable:4100) // unreferenced parameter #include +#pragma warning(pop) // IntSafe #define ENABLE_INTSAFE_SIGNED_FUNCTIONS diff --git a/src/interactivity/win32/screenInfoUiaProvider.cpp b/src/interactivity/win32/screenInfoUiaProvider.cpp index d1259b8c373..b8feb5589f4 100644 --- a/src/interactivity/win32/screenInfoUiaProvider.cpp +++ b/src/interactivity/win32/screenInfoUiaProvider.cpp @@ -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(&result, _pData, pProvider, start, end, degenerate, wordDelimiters)); + RETURN_IF_FAILED(MakeAndInitialize(&result, _pData, pProvider, start, end, wordDelimiters)); *ppUtr = result; return S_OK; } diff --git a/src/interactivity/win32/screenInfoUiaProvider.hpp b/src/interactivity/win32/screenInfoUiaProvider.hpp index ef3fb641c56..bf53dbcd571 100644 --- a/src/interactivity/win32/screenInfoUiaProvider.hpp +++ b/src/interactivity/win32/screenInfoUiaProvider.hpp @@ -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; diff --git a/src/interactivity/win32/uiaTextRange.cpp b/src/interactivity/win32/uiaTextRange.cpp index fb43f3cf8d4..dcbabcf7130 100644 --- a/src/interactivity/win32/uiaTextRange.cpp +++ b/src/interactivity/win32/uiaTextRange.cpp @@ -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 range; - RETURN_IF_FAILED(MakeAndInitialize(&range, pData, pProvider, start, end, false, wordDelimiters)); + RETURN_IF_FAILED(MakeAndInitialize(&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(**ppRetVal); range._start = start; range._end = end; - range._degenerate = false; } } } diff --git a/src/interactivity/win32/uiaTextRange.hpp b/src/interactivity/win32/uiaTextRange.hpp index a97579161d9..c6755499fb0 100644 --- a/src/interactivity/win32/uiaTextRange.hpp +++ b/src/interactivity/win32/uiaTextRange.hpp @@ -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 diff --git a/src/interactivity/win32/ut_interactivity_win32/UiaTextRangeTests.cpp b/src/interactivity/win32/ut_interactivity_win32/UiaTextRangeTests.cpp index 1f307d90e8b..cb28540272c 100644 --- a/src/interactivity/win32/ut_interactivity_win32/UiaTextRangeTests.cpp +++ b/src/interactivity/win32/ut_interactivity_win32/UiaTextRangeTests.cpp @@ -23,6 +23,7 @@ using namespace Microsoft::Console::Interactivity::Win32; // for unit tests so instead we'll use this one. We don't care about // it not doing anything for its implementation because it is not used // during the unit tests below. + class DummyElementProvider final : public IRawElementProviderSimple { public: @@ -79,6 +80,32 @@ class UiaTextRangeTests UiaTextRange* _range; IUiaData* _pUiaData; + struct ExpectedResult + { + int moveAmt; + COORD start; + COORD end; + }; + + struct MoveTest + { + std::wstring comment; + COORD start; + COORD end; + int moveAmt; + ExpectedResult expected; + }; + + struct MoveEndpointTest + { + std::wstring comment; + COORD start; + COORD end; + int moveAmt; + TextPatternRangeEndpoint endpoint; + ExpectedResult expected; + }; + TEST_METHOD_SETUP(MethodSetup) { CONSOLE_INFORMATION& gci = Microsoft::Console::Interactivity::ServiceLocator::LocateGlobals().getConsoleInformation(); @@ -100,18 +127,10 @@ class UiaTextRangeTests auto& charRow = row.GetCharRow(); for (auto& cell : charRow) { - cell.Char() = L'a'; + cell.Char() = L' '; } } - // set up default range - Microsoft::WRL::MakeAndInitialize(&_range, - _pUiaData, - &_dummyProvider, - 0, - 0, - false); - return true; } @@ -129,1518 +148,935 @@ class UiaTextRangeTests return true; } - const size_t _getRowWidth() const - { - const CharRow& charRow = _pTextBuffer->_GetFirstRow().GetCharRow(); - return charRow.MeasureRight() - charRow.MeasureLeft(); - } - TEST_METHOD(DegenerateRangesDetected) { + const auto bufferSize = _pTextBuffer->GetSize(); + const auto origin = bufferSize.Origin(); + // make a degenerate range and verify that it reports degenerate Microsoft::WRL::ComPtr degenerate; - Microsoft::WRL::MakeAndInitialize(°enerate, - _pUiaData, - &_dummyProvider, - 20, - 19, - true); + THROW_IF_FAILED(Microsoft::WRL::MakeAndInitialize(°enerate, + _pUiaData, + &_dummyProvider, + origin, + origin)); VERIFY_IS_TRUE(degenerate->IsDegenerate()); - VERIFY_ARE_EQUAL(0u, degenerate->_rowCountInRange(_pUiaData)); VERIFY_ARE_EQUAL(degenerate->_start, degenerate->_end); // make a non-degenerate range and verify that it reports as such - Microsoft::WRL::ComPtr notDegenerate1; - Microsoft::WRL::MakeAndInitialize(¬Degenerate1, - _pUiaData, - &_dummyProvider, - 20, - 20, - false); - VERIFY_IS_FALSE(notDegenerate1->IsDegenerate()); - VERIFY_ARE_EQUAL(1u, notDegenerate1->_rowCountInRange(_pUiaData)); + const COORD end = { origin.X + 1, origin.Y }; + Microsoft::WRL::ComPtr notDegenerate; + THROW_IF_FAILED(Microsoft::WRL::MakeAndInitialize(¬Degenerate, + _pUiaData, + &_dummyProvider, + origin, + end)); + VERIFY_IS_FALSE(notDegenerate->IsDegenerate()); + VERIFY_ARE_NOT_EQUAL(notDegenerate->_start, notDegenerate->_end); } - TEST_METHOD(CanCheckIfScreenInfoRowIsInViewport) + TEST_METHOD(CompareRange) { - // check a viewport that's one line tall - SMALL_RECT viewport; - viewport.Top = 0; - viewport.Bottom = 0; - - VERIFY_IS_TRUE(_range->_isScreenInfoRowInViewport(0, viewport)); - VERIFY_IS_FALSE(_range->_isScreenInfoRowInViewport(1, viewport)); - - // check a slightly larger viewport - viewport.Bottom = 5; - for (auto i = 0; i <= viewport.Bottom; ++i) - { - VERIFY_IS_TRUE(_range->_isScreenInfoRowInViewport(i, viewport), - NoThrowString().Format(L"%d should be in viewport", i)); - } - VERIFY_IS_FALSE(_range->_isScreenInfoRowInViewport(viewport.Bottom + 1, viewport)); + const auto bufferSize = _pTextBuffer->GetSize(); + const auto origin = bufferSize.Origin(); + + Microsoft::WRL::ComPtr utr1; + THROW_IF_FAILED(Microsoft::WRL::MakeAndInitialize(&utr1, + _pUiaData, + &_dummyProvider, + origin, + origin)); + + // utr2 initialized to have the same start/end as utr1 + Microsoft::WRL::ComPtr utr2; + THROW_IF_FAILED(utr1->Clone(&utr2)); + + BOOL comparison; + Log::Comment(L"_start and _end should match"); + THROW_IF_FAILED(utr1->Compare(utr2.Get(), &comparison)); + VERIFY_IS_TRUE(comparison); + + // utr2 redefined to have different end from utr1 + const COORD end = { origin.X + 2, origin.Y }; + THROW_IF_FAILED(Microsoft::WRL::MakeAndInitialize(&utr2, + _pUiaData, + &_dummyProvider, + origin, + end)); + + Log::Comment(L"_end is different"); + THROW_IF_FAILED(utr1->Compare(utr2.Get(), &comparison)); + VERIFY_IS_FALSE(comparison); } - TEST_METHOD(CanTranslateScreenInfoRowToViewport) + TEST_METHOD(CompareEndpoints) { - const int totalRows = _pTextBuffer->TotalRowCount(); - - SMALL_RECT viewport; - viewport.Top = 0; - viewport.Bottom = 10; - - std::vector> viewportSizes = { - { 0, 10 }, // viewport at top - { 2, 10 }, // shifted viewport - { totalRows - 5, totalRows + 3 } // viewport with 0th row - }; + const auto bufferSize = _pTextBuffer->GetSize(); + const auto origin = bufferSize.Origin(); + + Microsoft::WRL::ComPtr utr1; + THROW_IF_FAILED(Microsoft::WRL::MakeAndInitialize(&utr1, + _pUiaData, + &_dummyProvider, + origin, + origin)); + + Microsoft::WRL::ComPtr utr2; + THROW_IF_FAILED(utr1->Clone(&utr2)); + + int comparison; + Log::Comment(L"For a degenerate range, comparing _start and _end should return 0"); + VERIFY_IS_TRUE(utr1->IsDegenerate()); + THROW_IF_FAILED(utr1->CompareEndpoints(TextPatternRangeEndpoint_Start, utr1.Get(), TextPatternRangeEndpoint_End, &comparison)); + + Log::Comment(L"_start and _end should match"); + THROW_IF_FAILED(utr1->CompareEndpoints(TextPatternRangeEndpoint_Start, utr2.Get(), TextPatternRangeEndpoint_Start, &comparison)); + VERIFY_IS_TRUE(comparison == 0); + THROW_IF_FAILED(utr1->CompareEndpoints(TextPatternRangeEndpoint_End, utr2.Get(), TextPatternRangeEndpoint_End, &comparison)); + VERIFY_IS_TRUE(comparison == 0); + + // utr2 redefined to have different end from utr1 + const COORD end = { origin.X + 2, origin.Y }; + THROW_IF_FAILED(Microsoft::WRL::MakeAndInitialize(&utr2, + _pUiaData, + &_dummyProvider, + origin, + end)); + + Log::Comment(L"_start should match"); + THROW_IF_FAILED(utr1->CompareEndpoints(TextPatternRangeEndpoint_Start, utr2.Get(), TextPatternRangeEndpoint_Start, &comparison)); + VERIFY_IS_TRUE(comparison == 0); + + Log::Comment(L"_start and end should be 2 units apart. Sign depends on order of comparison."); + THROW_IF_FAILED(utr1->CompareEndpoints(TextPatternRangeEndpoint_End, utr2.Get(), TextPatternRangeEndpoint_End, &comparison)); + VERIFY_IS_TRUE(comparison == -2); + THROW_IF_FAILED(utr2->CompareEndpoints(TextPatternRangeEndpoint_End, utr1.Get(), TextPatternRangeEndpoint_End, &comparison)); + VERIFY_IS_TRUE(comparison == 2); + } - for (auto it = viewportSizes.begin(); it != viewportSizes.end(); ++it) + TEST_METHOD(ExpandToEnclosingUnit) + { + // Let's start by filling the text buffer with something useful: + for (UINT i = 0; i < _pTextBuffer->TotalRowCount(); ++i) { - viewport.Top = static_cast(it->first); - viewport.Bottom = static_cast(it->second); - for (int i = viewport.Top; _range->_isScreenInfoRowInViewport(i, viewport); ++i) + ROW& row = _pTextBuffer->GetRowByOffset(i); + auto& charRow = row.GetCharRow(); + for (size_t j = 0; j < charRow.size(); ++j) { - VERIFY_ARE_EQUAL(i - viewport.Top, _range->_screenInfoRowToViewportRow(i, viewport)); + // every 5th cell is a space, otherwise a letter + // this is used to simulate words + CharRowCellReference cell = charRow.GlyphAt(j); + if (j % 5 == 0) + { + cell = L" "; + } + else + { + cell = L"x"; + } } } - // ScreenInfoRows that are above the viewport return a - // negative value - viewport.Top = 5; - viewport.Bottom = 10; - - VERIFY_ARE_EQUAL(-1, _range->_screenInfoRowToViewportRow(4, viewport)); - VERIFY_ARE_EQUAL(-2, _range->_screenInfoRowToViewportRow(3, viewport)); - } + // According to https://docs.microsoft.com/en-us/windows/win32/winauto/uiauto-implementingtextandtextrange#manipulating-a-text-range-by-text-unit + // there are 9 examples of how ExpandToEnclosingUnit should behave. See the diagram there for reference. + // Some of the relevant text has been copied below... + // 1-2) If the text range starts at the beginning of a text unit + // and ends at the beginning of, or before, the next text unit + // boundary, the ending endpoint is moved to the next text unit boundary + // 3-4) If the text range starts at the beginning of a text unit + // and ends at, or after, the next unit boundary, + // the ending endpoint stays or is moved backward to + // the next unit boundary after the starting endpoint + // NOTE: If there is more than one text unit boundary between + // the starting and ending endpoints, the ending endpoint + // is moved backward to the next unit boundary after + // the starting endpoint, resulting in a text range that is + // one text unit in length. + // 5-8) If the text range starts in a middle of the text unit, + // the starting endpoint is moved backward to the beginning + // of the text unit, and the ending endpoint is moved forward + // or backward, as necessary, to the next unit boundary + // after the starting endpoint + // 9) (same as 1) If the text range starts and ends at the beginning of + // a text unit boundary, the ending endpoint is moved to the next text unit boundary + + // We will abstract these tests so that we can define the beginning and end of a text unit boundary, + // based on the text unit we are testing + constexpr TextUnit supportedUnits[] = { TextUnit_Character, TextUnit_Word, TextUnit_Line, TextUnit_Document }; + + auto toString = [&](TextUnit unit) { + // if a format is not supported, it goes to the next largest text unit + switch (unit) + { + case TextUnit_Character: + return L"Character"; + case TextUnit_Format: + case TextUnit_Word: + return L"Word"; + case TextUnit_Line: + return L"Line"; + case TextUnit_Paragraph: + case TextUnit_Page: + case TextUnit_Document: + return L"Document"; + default: + throw E_INVALIDARG; + } + }; - TEST_METHOD(CanTranslateEndpointToTextBufferRow) - { - const auto rowWidth = _getRowWidth(); - for (auto i = 0; i < 300; ++i) + struct TextUnitBoundaries { - VERIFY_ARE_EQUAL(i / rowWidth, _range->_endpointToTextBufferRow(_pUiaData, i)); - } - } + COORD start; + COORD end; + }; - TEST_METHOD(CanTranslateTextBufferRowToEndpoint) - { - const auto rowWidth = _getRowWidth(); - for (unsigned int i = 0; i < 5; ++i) - { - VERIFY_ARE_EQUAL(i * rowWidth, _range->_textBufferRowToEndpoint(_pUiaData, i)); - // make sure that the translation is reversible - VERIFY_ARE_EQUAL(i, _range->_endpointToTextBufferRow(_pUiaData, _range->_textBufferRowToEndpoint(_pUiaData, i))); - } - } + const std::map textUnitBoundaries = { + { TextUnit_Character, + TextUnitBoundaries{ + { 0, 0 }, + { 1, 0 } } }, + { TextUnit_Word, + TextUnitBoundaries{ + { 1, 0 }, + { 6, 0 } } }, + { TextUnit_Line, + TextUnitBoundaries{ + { 0, 0 }, + { 0, 1 } } }, + { TextUnit_Document, + TextUnitBoundaries{ + { 0, 0 }, + _pTextBuffer->GetSize().EndExclusive() } } + }; - TEST_METHOD(CanTranslateTextBufferRowToScreenInfoRow) - { - const auto rowWidth = _getRowWidth(); - for (unsigned int i = 0; i < 5; ++i) - { - VERIFY_ARE_EQUAL(i, _range->_textBufferRowToScreenInfoRow(_pUiaData, _range->_screenInfoRowToTextBufferRow(_pUiaData, i))); - } - } + Microsoft::WRL::ComPtr utr; + auto verifyExpansion = [&](TextUnit textUnit, COORD utrStart, COORD utrEnd) { + THROW_IF_FAILED(Microsoft::WRL::MakeAndInitialize(&utr, + _pUiaData, + &_dummyProvider, + utrStart, + utrEnd)); + THROW_IF_FAILED(utr->ExpandToEnclosingUnit(textUnit)); + + const auto boundaries = textUnitBoundaries.at(textUnit); + VERIFY_ARE_EQUAL(utr->GetEndpoint(TextPatternRangeEndpoint_Start), boundaries.start); + VERIFY_ARE_EQUAL(utr->GetEndpoint(TextPatternRangeEndpoint_End), boundaries.end); + }; - TEST_METHOD(CanTranslateEndpointToColumn) - { - const auto rowWidth = _getRowWidth(); - for (auto i = 0; i < 300; ++i) + for (auto textUnit : supportedUnits) { - const auto column = i % rowWidth; - VERIFY_ARE_EQUAL(column, _range->_endpointToColumn(_pUiaData, i)); - } - } - - TEST_METHOD(CanGetTotalRows) - { - const auto totalRows = _pTextBuffer->TotalRowCount(); - VERIFY_ARE_EQUAL(totalRows, - _range->_getTotalRows(_pUiaData)); - } - - TEST_METHOD(CanGetRowWidth) - { - const auto rowWidth = _getRowWidth(); - VERIFY_ARE_EQUAL(rowWidth, _range->_getRowWidth(_pUiaData)); - } + const auto boundaries = textUnitBoundaries.at(textUnit); - TEST_METHOD(CanNormalizeRow) - { - const int totalRows = _pTextBuffer->TotalRowCount(); - std::vector> rowMappings = { - { 0, 0 }, - { totalRows / 2, totalRows / 2 }, - { totalRows - 1, totalRows - 1 }, - { totalRows, 0 }, - { totalRows + 1, 1 }, - { -1, totalRows - 1 } - }; + // Test 1 + Log::Comment(NoThrowString().Format(L"%s - Test 1", toString(textUnit))); + verifyExpansion(textUnit, boundaries.start, boundaries.start); - for (auto it = rowMappings.begin(); it != rowMappings.end(); ++it) - { - VERIFY_ARE_EQUAL(static_cast(it->second), _range->_normalizeRow(_pUiaData, it->first)); - } - } + // Test 2 (impossible for TextUnit_Character) + if (textUnit != TextUnit_Character) + { + Log::Comment(NoThrowString().Format(L"%s - Test 2", toString(textUnit))); + const COORD end = { boundaries.start.X + 1, boundaries.start.Y }; + verifyExpansion(textUnit, boundaries.start, end); + } - TEST_METHOD(CanGetViewportHeight) - { - SMALL_RECT viewport; - viewport.Top = 0; - viewport.Bottom = 0; + // Test 3 + Log::Comment(NoThrowString().Format(L"%s - Test 3", toString(textUnit))); + verifyExpansion(textUnit, boundaries.start, boundaries.end); - // Viewports are inclusive, so Top == Bottom really means 1 row - VERIFY_ARE_EQUAL(1u, _range->_getViewportHeight(viewport)); + // Test 4 (impossible for TextUnit_Character and TextUnit_Document) + if (textUnit != TextUnit_Character && textUnit != TextUnit_Document) + { + Log::Comment(NoThrowString().Format(L"%s - Test 4", toString(textUnit))); + const COORD end = { boundaries.end.X + 1, boundaries.end.Y }; + verifyExpansion(textUnit, boundaries.start, end); + } - // make the viewport 10 rows tall - viewport.Top = 3; - viewport.Bottom = 12; - VERIFY_ARE_EQUAL(10u, _range->_getViewportHeight(viewport)); - } + // Test 5 (impossible for TextUnit_Character) + if (textUnit != TextUnit_Character) + { + Log::Comment(NoThrowString().Format(L"%s - Test 5", toString(textUnit))); + const COORD start = { boundaries.start.X + 1, boundaries.start.Y }; + verifyExpansion(textUnit, start, start); + } - TEST_METHOD(CanGetViewportWidth) - { - SMALL_RECT viewport; - viewport.Left = 0; - viewport.Right = 0; + // Test 6 (impossible for TextUnit_Character) + if (textUnit != TextUnit_Character) + { + Log::Comment(NoThrowString().Format(L"%s - Test 6", toString(textUnit))); + const COORD start = { boundaries.start.X + 1, boundaries.start.Y }; + const COORD end = { start.X + 1, start.Y }; + verifyExpansion(textUnit, start, end); + } - // Viewports are inclusive, Left == Right is really 1 column - VERIFY_ARE_EQUAL(1u, _range->_getViewportWidth(viewport)); + // Test 7 (impossible for TextUnit_Character) + if (textUnit != TextUnit_Character) + { + Log::Comment(NoThrowString().Format(L"%s - Test 7", toString(textUnit))); + const COORD start = { boundaries.start.X + 1, boundaries.start.Y }; + verifyExpansion(textUnit, start, boundaries.end); + } - // test a more normal size - viewport.Right = 300; - VERIFY_ARE_EQUAL(viewport.Right + 1u, _range->_getViewportWidth(viewport)); + // Test 8 (impossible for TextUnit_Character and TextUnit_Document) + if (textUnit != TextUnit_Character && textUnit != TextUnit_Document) + { + Log::Comment(NoThrowString().Format(L"%s - Test 8", toString(textUnit))); + const COORD start = { boundaries.start.X + 1, boundaries.start.Y }; + const COORD end = { boundaries.end.X + 1, boundaries.end.Y }; + verifyExpansion(textUnit, start, end); + } + } } - TEST_METHOD(CanCompareScreenCoords) + TEST_METHOD(MoveEndpointByRange) { - const std::vector> testData = { - { 0, 0, 0, 0, 0 }, - { 5, 0, 5, 0, 0 }, - { 2, 3, 2, 3, 0 }, - { 0, 6, 0, 6, 0 }, - { 1, 5, 2, 5, -1 }, - { 5, 4, 7, 3, -1 }, - { 3, 4, 3, 5, -1 }, - { 2, 0, 1, 9, 1 }, - { 4, 5, 4, 3, 1 } + const COORD start{ 0, 1 }; + const COORD end{ 1, 2 }; + Microsoft::WRL::ComPtr utr; + THROW_IF_FAILED(Microsoft::WRL::MakeAndInitialize(&utr, + _pUiaData, + &_dummyProvider, + start, + end)); + + const auto bufferSize = _pTextBuffer->GetSize(); + const auto origin = bufferSize.Origin(); + Microsoft::WRL::ComPtr target; + + auto resetTargetUTR = [&]() { + THROW_IF_FAILED(Microsoft::WRL::MakeAndInitialize(&target, + _pUiaData, + &_dummyProvider, + origin, + origin)); }; - for (auto data : testData) + Log::Comment(L"Move target's end to utr1's start"); { - VERIFY_ARE_EQUAL(std::get<4>(data), - UiaTextRange::_compareScreenCoords(_pUiaData, - std::get<0>(data), - std::get<1>(data), - std::get<2>(data), - std::get<3>(data))); + resetTargetUTR(); + THROW_IF_FAILED(target->MoveEndpointByRange(TextPatternRangeEndpoint_End, + utr.Get(), + TextPatternRangeEndpoint_Start)); + VERIFY_ARE_EQUAL(target->GetEndpoint(TextPatternRangeEndpoint_Start), origin); + VERIFY_ARE_EQUAL(target->GetEndpoint(TextPatternRangeEndpoint_End), utr->GetEndpoint(TextPatternRangeEndpoint_Start)); } - } - - TEST_METHOD(CanMoveByCharacter) - { - const Column firstColumnIndex = 0; - const Column lastColumnIndex = _pScreenInfo->GetBufferSize().Width() - 1; - const ScreenInfoRow topRow = 0; - const ScreenInfoRow bottomRow = _pTextBuffer->TotalRowCount() - 1; - // clang-format off - const std::vector> testData = + Log::Comment(L"Move target's start/end to utr1's start/end respectively"); { - { - L"can't move backward from (0, 0)", - { - 0, 0, - 0, 2, - topRow, - lastColumnIndex, - firstColumnIndex, - UiaTextRange::MovementIncrement::Backward, - UiaTextRange::MovementDirection::Backward - }, - -1, - 0, - 0u, - 0u - }, - - { - L"can move backward within a row", - { - 0, 1, - 0, 2, - topRow, - lastColumnIndex, - firstColumnIndex, - UiaTextRange::MovementIncrement::Backward, - UiaTextRange::MovementDirection::Backward - }, - -1, - -1, - 0u, - 0u - }, - - { - L"can move forward in a row", - { - 2, 1, - 4, 5, - bottomRow, - firstColumnIndex, - lastColumnIndex, - UiaTextRange::MovementIncrement::Forward, - UiaTextRange::MovementDirection::Forward - }, - 5, - 5, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, 2) + 6, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, 2) + 6 - }, - - { - L"can't move past the last column in the last row", - { - bottomRow, lastColumnIndex, - bottomRow, lastColumnIndex, - bottomRow, - firstColumnIndex, - lastColumnIndex, - UiaTextRange::MovementIncrement::Forward, - UiaTextRange::MovementDirection::Forward - }, - 5, - 0, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, bottomRow) + lastColumnIndex, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, bottomRow) + lastColumnIndex - }, - - { - L"can move to a new row when necessary when moving forward", - { - topRow, lastColumnIndex, - topRow, lastColumnIndex, - bottomRow, - firstColumnIndex, - lastColumnIndex, - UiaTextRange::MovementIncrement::Forward, - UiaTextRange::MovementDirection::Forward - }, - 5, - 5, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, topRow + 1) + 4, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, topRow + 1) + 4 - }, + resetTargetUTR(); + THROW_IF_FAILED(target->MoveEndpointByRange(TextPatternRangeEndpoint_End, + utr.Get(), + TextPatternRangeEndpoint_End)); + VERIFY_ARE_EQUAL(target->GetEndpoint(TextPatternRangeEndpoint_Start), origin); + VERIFY_ARE_EQUAL(target->GetEndpoint(TextPatternRangeEndpoint_End), utr->GetEndpoint(TextPatternRangeEndpoint_End)); + + THROW_IF_FAILED(target->MoveEndpointByRange(TextPatternRangeEndpoint_Start, + utr.Get(), + TextPatternRangeEndpoint_Start)); + VERIFY_ARE_EQUAL(target->GetEndpoint(TextPatternRangeEndpoint_Start), utr->GetEndpoint(TextPatternRangeEndpoint_Start)); + VERIFY_ARE_EQUAL(target->GetEndpoint(TextPatternRangeEndpoint_End), utr->GetEndpoint(TextPatternRangeEndpoint_End)); + } - { - L"can move to a new row when necessary when moving backward", - { - topRow + 1, firstColumnIndex, - topRow + 1, lastColumnIndex, - topRow, - lastColumnIndex, - firstColumnIndex, - UiaTextRange::MovementIncrement::Backward, - UiaTextRange::MovementDirection::Backward - }, - -5, - -5, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, topRow) + (lastColumnIndex - 4), - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, topRow) + (lastColumnIndex - 4) - } - }; - // clang-format on + Log::Comment(L"(Clone utr1) Collapse onto itself"); + { + // Move start to end + ComPtr temp; + THROW_IF_FAILED(utr->Clone(&temp)); + target = static_cast(temp.Get()); + THROW_IF_FAILED(target->MoveEndpointByRange(TextPatternRangeEndpoint_Start, + target.Get(), + TextPatternRangeEndpoint_End)); + VERIFY_ARE_EQUAL(target->GetEndpoint(TextPatternRangeEndpoint_Start), utr->GetEndpoint(TextPatternRangeEndpoint_End)); + VERIFY_ARE_EQUAL(target->GetEndpoint(TextPatternRangeEndpoint_End), utr->GetEndpoint(TextPatternRangeEndpoint_End)); + + // Move end to start + THROW_IF_FAILED(utr->Clone(&temp)); + target = static_cast(temp.Get()); + THROW_IF_FAILED(target->MoveEndpointByRange(TextPatternRangeEndpoint_End, + target.Get(), + TextPatternRangeEndpoint_Start)); + VERIFY_ARE_EQUAL(target->GetEndpoint(TextPatternRangeEndpoint_Start), utr->GetEndpoint(TextPatternRangeEndpoint_Start)); + VERIFY_ARE_EQUAL(target->GetEndpoint(TextPatternRangeEndpoint_End), utr->GetEndpoint(TextPatternRangeEndpoint_Start)); + } - for (auto data : testData) + Log::Comment(L"Cross endpoints (force degenerate range)"); { - Log::Comment(std::get<0>(data).c_str()); - int amountMoved; - std::pair newEndpoints = UiaTextRange::_moveByCharacter(_pUiaData, - std::get<2>(data), - std::get<1>(data), - &amountMoved); - - VERIFY_ARE_EQUAL(std::get<3>(data), amountMoved); - VERIFY_ARE_EQUAL(std::get<4>(data), newEndpoints.first); - VERIFY_ARE_EQUAL(std::get<5>(data), newEndpoints.second); + // move start past end + resetTargetUTR(); + THROW_IF_FAILED(target->MoveEndpointByRange(TextPatternRangeEndpoint_Start, + utr.Get(), + TextPatternRangeEndpoint_End)); + VERIFY_ARE_EQUAL(target->GetEndpoint(TextPatternRangeEndpoint_Start), utr->GetEndpoint(TextPatternRangeEndpoint_End)); + VERIFY_ARE_EQUAL(target->GetEndpoint(TextPatternRangeEndpoint_End), utr->GetEndpoint(TextPatternRangeEndpoint_End)); + VERIFY_IS_TRUE(target->IsDegenerate()); + + // move end past start + THROW_IF_FAILED(target->MoveEndpointByRange(TextPatternRangeEndpoint_End, + utr.Get(), + TextPatternRangeEndpoint_Start)); + VERIFY_ARE_EQUAL(target->GetEndpoint(TextPatternRangeEndpoint_Start), utr->GetEndpoint(TextPatternRangeEndpoint_Start)); + VERIFY_ARE_EQUAL(target->GetEndpoint(TextPatternRangeEndpoint_End), utr->GetEndpoint(TextPatternRangeEndpoint_Start)); + VERIFY_IS_TRUE(target->IsDegenerate()); } } - TEST_METHOD(CanMoveByWord_EmptyBuffer) + TEST_METHOD(CanMoveByCharacter) { - const Column firstColumnIndex = 0; - const Column lastColumnIndex = _pScreenInfo->GetBufferSize().Width() - 1; - const ScreenInfoRow topRow = 0; - const ScreenInfoRow bottomRow = _pTextBuffer->TotalRowCount() - 1; + const SHORT lastColumnIndex = _pScreenInfo->GetBufferSize().Width() - 1; + const SHORT bottomRow = gsl::narrow(_pTextBuffer->TotalRowCount() - 1); // clang-format off - const std::vector> testData = + const std::vector testData { - { + MoveTest{ L"can't move backward from (0, 0)", - { - 0, 0, - 0, 2, - topRow, - lastColumnIndex, - firstColumnIndex, - UiaTextRange::MovementIncrement::Backward, - UiaTextRange::MovementDirection::Backward - }, + { 0, 0 }, + { 2, 0 }, -1, - 0, - 0u, - lastColumnIndex + { + 0, + {0,0}, + {2,0} + } }, - { + MoveTest{ L"can move backward within a row", - { - 0, 1, - 0, 2, - topRow, - lastColumnIndex, - firstColumnIndex, - UiaTextRange::MovementIncrement::Backward, - UiaTextRange::MovementDirection::Backward - }, + { 1, 0 }, + { 2, 0 }, -1, - -1, - 0u, - lastColumnIndex + { + -1, + {0, 0}, + {1, 0} + } }, - { + MoveTest{ L"can move forward in a row", - { - 2, 1, - 4, 5, - bottomRow, - firstColumnIndex, - lastColumnIndex, - UiaTextRange::MovementIncrement::Forward, - UiaTextRange::MovementDirection::Forward - }, - 5, + { 1, 2 }, + { 5, 4 }, 5, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, 8), - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, 8) + lastColumnIndex + { + 5, + {6,2}, + {7,2} + } }, - { + MoveTest{ L"can't move past the last column in the last row", - { - bottomRow, lastColumnIndex, - bottomRow, lastColumnIndex, - bottomRow, - firstColumnIndex, - lastColumnIndex, - UiaTextRange::MovementIncrement::Forward, - UiaTextRange::MovementDirection::Forward - }, + { lastColumnIndex, bottomRow }, + { lastColumnIndex, bottomRow }, 5, - 0, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, bottomRow), - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, bottomRow) + lastColumnIndex + { + 0, + { lastColumnIndex, bottomRow }, + { lastColumnIndex, bottomRow }, + } }, - { + MoveTest{ L"can move to a new row when necessary when moving forward", - { - topRow, lastColumnIndex, - topRow, lastColumnIndex, - bottomRow, - firstColumnIndex, - lastColumnIndex, - UiaTextRange::MovementIncrement::Forward, - UiaTextRange::MovementDirection::Forward - }, - 5, + { lastColumnIndex, 0 }, + { lastColumnIndex, 0 }, 5, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, topRow + 5), - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, topRow + 5) + lastColumnIndex + { + 5, + {4 , 0 + 1}, + {5 , 0 + 1} + } }, - { + MoveTest{ L"can move to a new row when necessary when moving backward", - { - topRow + 1, firstColumnIndex, - topRow + 1, lastColumnIndex, - topRow, - lastColumnIndex, - firstColumnIndex, - UiaTextRange::MovementIncrement::Backward, - UiaTextRange::MovementDirection::Backward - }, + { 0, 0 + 1 }, + { lastColumnIndex, 0 + 1 }, -5, - -2, - 0u, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, topRow) + lastColumnIndex + { + -5, + {lastColumnIndex - 4, 0}, + {lastColumnIndex - 3, 0} + } } }; // clang-format on - for (auto data : testData) + Microsoft::WRL::ComPtr utr; + for (const auto& test : testData) { - Log::Comment(std::get<0>(data).c_str()); + Log::Comment(test.comment.data()); int amountMoved; - std::pair newEndpoints = UiaTextRange::_moveByWord(_pUiaData, - std::get<2>(data), - std::get<1>(data), - L"", - &amountMoved); - - VERIFY_ARE_EQUAL(std::get<3>(data), amountMoved); - VERIFY_ARE_EQUAL(std::get<4>(data), newEndpoints.first); - VERIFY_ARE_EQUAL(std::get<5>(data), newEndpoints.second); - } - } - - TEST_METHOD(CanMoveByWord_NonEmptyBuffer) - { - const Column firstColumnIndex = 0; - const Column lastColumnIndex = _pScreenInfo->GetBufferSize().Width() - 1; - const ScreenInfoRow topRow = 0; - const ScreenInfoRow bottomRow = _pTextBuffer->TotalRowCount() - 1; - - const std::wstring_view text[] = { - L"word1 word2 word3", - L"word4 word5 word6" - }; - - for (auto i = 0; i < 2; i++) - { - _pTextBuffer->WriteLine(text[i], { 0, gsl::narrow(i) }); - } - - // clang-format off - const std::vector> testData = - { - { - L"move backwards on the word by (0,0)", - { - 0, 1, - 0, 2, - topRow, - lastColumnIndex, - firstColumnIndex, - UiaTextRange::MovementIncrement::Backward, - UiaTextRange::MovementDirection::Backward - }, - -1, - -1, - 0u, - 6 - }, - - { - L"get next word while on first word", - { - 0, 0, - 0, 0, - bottomRow, - firstColumnIndex, - lastColumnIndex, - UiaTextRange::MovementIncrement::Forward, - UiaTextRange::MovementDirection::Forward - }, - 1, - 1, - 0, - 6 - }, - - { - L"get next word twice while on first word", - { - 0, 0, - 0, 0, - bottomRow, - firstColumnIndex, - lastColumnIndex, - UiaTextRange::MovementIncrement::Forward, - UiaTextRange::MovementDirection::Forward - }, - 2, - 2, - 7, - 14 - }, - - { - L"move forward to next row with word", - { - topRow, lastColumnIndex, - topRow, lastColumnIndex, - bottomRow, - firstColumnIndex, - lastColumnIndex, - UiaTextRange::MovementIncrement::Forward, - UiaTextRange::MovementDirection::Forward - }, - 1, - 1, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, topRow + 1), - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, topRow + 1) + 6 - }, - { - L"move backwards to previous row with word", - { - topRow + 1, firstColumnIndex, - topRow + 1, lastColumnIndex, - topRow, - lastColumnIndex, - firstColumnIndex, - UiaTextRange::MovementIncrement::Backward, - UiaTextRange::MovementDirection::Backward - }, - -1, - -1, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, topRow) + 15, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, topRow) + lastColumnIndex - } - }; - // clang-format on + THROW_IF_FAILED(Microsoft::WRL::MakeAndInitialize(&utr, _pUiaData, &_dummyProvider, test.start, test.end)); + utr->Move(TextUnit::TextUnit_Character, test.moveAmt, &amountMoved); - for (auto data : testData) - { - Log::Comment(std::get<0>(data).c_str()); - int amountMoved; - std::pair newEndpoints = UiaTextRange::_moveByWord(_pUiaData, - std::get<2>(data), - std::get<1>(data), - L"", - &amountMoved); - - VERIFY_ARE_EQUAL(std::get<3>(data), amountMoved); - VERIFY_ARE_EQUAL(std::get<4>(data), newEndpoints.first); - VERIFY_ARE_EQUAL(std::get<5>(data), newEndpoints.second); + VERIFY_ARE_EQUAL(test.expected.moveAmt, amountMoved); + VERIFY_ARE_EQUAL(test.expected.start, utr->_start); + VERIFY_ARE_EQUAL(test.expected.end, utr->_end); } } TEST_METHOD(CanMoveByLine) { - const Column firstColumnIndex = 0; - const Column lastColumnIndex = _pScreenInfo->GetBufferSize().Width() - 1; - const ScreenInfoRow topRow = 0; - const ScreenInfoRow bottomRow = _pTextBuffer->TotalRowCount() - 1; + const SHORT lastColumnIndex = _pScreenInfo->GetBufferSize().Width() - 1; + const SHORT bottomRow = gsl::narrow(_pTextBuffer->TotalRowCount() - 1); // clang-format off - const std::vector> testData = + const std::vector testData { - { + MoveTest{ L"can't move backward from top row", - { - topRow, firstColumnIndex, - topRow, lastColumnIndex, - topRow, - lastColumnIndex, - firstColumnIndex, - UiaTextRange::MovementIncrement::Backward, - UiaTextRange::MovementDirection::Backward - }, + {0, 0}, + {0, lastColumnIndex}, -4, - 0, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, topRow) + firstColumnIndex, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, topRow) + lastColumnIndex + { + 0, + {0, 0}, + {0, lastColumnIndex} + } }, - { + MoveTest{ L"can move forward from top row", - { - topRow, firstColumnIndex, - topRow, lastColumnIndex, - bottomRow, - firstColumnIndex, - lastColumnIndex, - UiaTextRange::MovementIncrement::Forward, - UiaTextRange::MovementDirection::Forward - }, + {0, 0}, + {0, lastColumnIndex}, 4, - 4, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, topRow + 4) + firstColumnIndex, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, topRow + 4) + lastColumnIndex + { + 4, + {0, 4}, + {0, 5} + } }, - { + MoveTest{ L"can't move forward from bottom row", - { - bottomRow, firstColumnIndex, - bottomRow, lastColumnIndex, - bottomRow, - firstColumnIndex, - lastColumnIndex, - UiaTextRange::MovementIncrement::Forward, - UiaTextRange::MovementDirection::Forward - }, + {0, bottomRow}, + {lastColumnIndex, bottomRow}, 3, - 0, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, bottomRow) + firstColumnIndex, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, bottomRow) + lastColumnIndex + { + 0, + {0, bottomRow}, + {lastColumnIndex, bottomRow}, + } }, - { + MoveTest{ L"can move backward from bottom row", - { - bottomRow, firstColumnIndex, - bottomRow, lastColumnIndex, - topRow, - lastColumnIndex, - firstColumnIndex, - UiaTextRange::MovementIncrement::Backward, - UiaTextRange::MovementDirection::Backward - }, - -3, + {0, bottomRow}, + {lastColumnIndex, bottomRow}, -3, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, bottomRow - 3) + firstColumnIndex, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, bottomRow - 3) + lastColumnIndex + { + -3, + {0, bottomRow - 3}, + {0, bottomRow - 2} + } }, - { + MoveTest{ L"can't move backward when part of the top row is in the range", - { - topRow, firstColumnIndex + 5, - topRow, lastColumnIndex, - topRow, - lastColumnIndex, - firstColumnIndex, - UiaTextRange::MovementIncrement::Backward, - UiaTextRange::MovementDirection::Backward - }, + {5, 0}, + {lastColumnIndex, 0}, -1, - 0, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, topRow) + firstColumnIndex + 5, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, topRow) + lastColumnIndex + { + 0, + {5, 0}, + {lastColumnIndex, 0}, + } }, - { + MoveTest{ L"can't move forward when part of the bottom row is in the range", - { - bottomRow, firstColumnIndex, - bottomRow, firstColumnIndex, - bottomRow, - firstColumnIndex, - lastColumnIndex, - UiaTextRange::MovementIncrement::Forward, - UiaTextRange::MovementDirection::Forward - }, + {0, bottomRow}, + {0, bottomRow}, 1, - 0, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, bottomRow) + firstColumnIndex, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, bottomRow) + firstColumnIndex + { + 0, + {0, bottomRow}, + {0, bottomRow} + } } }; // clang-format on - for (auto data : testData) + Microsoft::WRL::ComPtr utr; + for (const auto& test : testData) { - Log::Comment(std::get<0>(data).c_str()); + Log::Comment(test.comment.data()); int amountMoved; - std::pair newEndpoints = UiaTextRange::_moveByLine(_pUiaData, - std::get<2>(data), - std::get<1>(data), - &amountMoved); - - VERIFY_ARE_EQUAL(std::get<3>(data), amountMoved); - VERIFY_ARE_EQUAL(std::get<4>(data), newEndpoints.first); - VERIFY_ARE_EQUAL(std::get<5>(data), newEndpoints.second); + + THROW_IF_FAILED(Microsoft::WRL::MakeAndInitialize(&utr, _pUiaData, &_dummyProvider, test.start, test.end)); + THROW_IF_FAILED(utr->Move(TextUnit::TextUnit_Line, test.moveAmt, &amountMoved)); + + VERIFY_ARE_EQUAL(test.expected.moveAmt, amountMoved); + VERIFY_ARE_EQUAL(test.expected.start, utr->_start); + VERIFY_ARE_EQUAL(test.expected.end, utr->_end); } } TEST_METHOD(CanMoveEndpointByUnitCharacter) { - const Column firstColumnIndex = 0; - const Column lastColumnIndex = _pScreenInfo->GetBufferSize().Width() - 1; - const ScreenInfoRow topRow = 0; - const ScreenInfoRow bottomRow = _pTextBuffer->TotalRowCount() - 1; + const SHORT lastColumnIndex = _pScreenInfo->GetBufferSize().Width() - 1; + const SHORT bottomRow = static_cast(_pTextBuffer->TotalRowCount() - 1); // clang-format off - const std::vector> testData = + const std::vector testData { - { + MoveEndpointTest{ L"can't move _start past the beginning of the document when _start is positioned at the beginning", - { - topRow, firstColumnIndex, - topRow, lastColumnIndex, - topRow, - lastColumnIndex, - firstColumnIndex, - UiaTextRange::MovementIncrement::Backward, - UiaTextRange::MovementDirection::Backward - }, + {0, 0}, + {lastColumnIndex, 0}, -1, - 0, - TextPatternRangeEndpoint::TextPatternRangeEndpoint_Start, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, topRow) + firstColumnIndex, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, topRow) + lastColumnIndex, - false + TextPatternRangeEndpoint_Start, + { + 0, + {0, 0}, + {lastColumnIndex, 0} + } }, - { + MoveEndpointTest{ L"can partially move _start to the begining of the document when it is closer than the move count requested", - { - topRow, firstColumnIndex + 3, - topRow, lastColumnIndex, - topRow, - lastColumnIndex, - firstColumnIndex, - UiaTextRange::MovementIncrement::Backward, - UiaTextRange::MovementDirection::Backward - }, + {3, 0}, + {lastColumnIndex, 0}, -5, - -3, - TextPatternRangeEndpoint::TextPatternRangeEndpoint_Start, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, topRow) + firstColumnIndex, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, topRow) + lastColumnIndex, - false + TextPatternRangeEndpoint_Start, + { + -3, + {0, 0}, + {lastColumnIndex, 0} + } }, - { + MoveEndpointTest{ L"can't move _end past the begining of the document", - { - topRow, firstColumnIndex, - topRow, firstColumnIndex + 4, - topRow, - lastColumnIndex, - firstColumnIndex, - UiaTextRange::MovementIncrement::Backward, - UiaTextRange::MovementDirection::Backward - }, + {0, 0}, + {4, 0}, -5, - -4, - TextPatternRangeEndpoint::TextPatternRangeEndpoint_End, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, topRow) + firstColumnIndex, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, topRow) + firstColumnIndex, - false + TextPatternRangeEndpoint_End, + { + -4, + {0, 0}, + {0, 0} + } }, - { + MoveEndpointTest{ L"_start follows _end when passed during movement", - { - topRow, firstColumnIndex + 5, - topRow, firstColumnIndex + 10, - topRow, - lastColumnIndex, - firstColumnIndex, - UiaTextRange::MovementIncrement::Backward, - UiaTextRange::MovementDirection::Backward - }, - -7, + {5, 0}, + {10, 0}, -7, - TextPatternRangeEndpoint::TextPatternRangeEndpoint_End, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, topRow) + 3, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, topRow) + 3, - true + TextPatternRangeEndpoint_End, + { + -7, + {3, 0}, + {3, 0} + } }, - { + MoveEndpointTest{ L"can't move _end past the beginning of the document when _end is positioned at the end", - { - bottomRow, firstColumnIndex, - bottomRow, lastColumnIndex, - bottomRow, - firstColumnIndex, - lastColumnIndex, - UiaTextRange::MovementIncrement::Forward, - UiaTextRange::MovementDirection::Forward - }, + {0, bottomRow}, + {0, bottomRow+1}, 1, - 0, - TextPatternRangeEndpoint::TextPatternRangeEndpoint_End, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, bottomRow) + firstColumnIndex, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, bottomRow) + lastColumnIndex, - false + TextPatternRangeEndpoint_End, + { + 0, + {0, bottomRow}, + {0, bottomRow+1}, + } }, - { + MoveEndpointTest{ L"can partially move _end to the end of the document when it is closer than the move count requested", - { - topRow, firstColumnIndex, - bottomRow, lastColumnIndex - 3, - bottomRow, - firstColumnIndex, - lastColumnIndex, - UiaTextRange::MovementIncrement::Forward, - UiaTextRange::MovementDirection::Forward - }, + {0, 0}, + {lastColumnIndex - 3, bottomRow}, 5, - 3, - TextPatternRangeEndpoint::TextPatternRangeEndpoint_End, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, topRow) + firstColumnIndex, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, bottomRow) + lastColumnIndex, - false + TextPatternRangeEndpoint_End, + { + 4, + {0, 0}, + {0, bottomRow+1}, + } }, - { + MoveEndpointTest{ L"can't move _start past the end of the document", - { - bottomRow, lastColumnIndex - 4, - bottomRow, lastColumnIndex, - bottomRow, - firstColumnIndex, - lastColumnIndex, - UiaTextRange::MovementIncrement::Forward, - UiaTextRange::MovementDirection::Forward - }, + {lastColumnIndex - 4, bottomRow}, + {0, bottomRow+1}, 5, - 4, - TextPatternRangeEndpoint::TextPatternRangeEndpoint_Start, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, bottomRow) + lastColumnIndex, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, bottomRow) + lastColumnIndex, - false + TextPatternRangeEndpoint_Start, + { + 5, + {0, bottomRow+1}, + {0, bottomRow+1}, + } }, - { + MoveEndpointTest{ L"_end follows _start when passed during movement", - { - topRow, firstColumnIndex + 5, - topRow, firstColumnIndex + 10, - topRow, - lastColumnIndex, - firstColumnIndex, - UiaTextRange::MovementIncrement::Forward, - UiaTextRange::MovementDirection::Forward - }, + {5, 0}, + {10, 0}, 7, - 7, - TextPatternRangeEndpoint::TextPatternRangeEndpoint_Start, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, topRow) + 12, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, topRow) + 12, - true + TextPatternRangeEndpoint_Start, + { + 7, + {12, 0}, + {12, 0} + } }, }; // clang-format on - for (auto data : testData) + Microsoft::WRL::ComPtr utr; + for (const auto& test : testData) { - Log::Comment(std::get<0>(data).c_str()); - std::tuple result; + Log::Comment(test.comment.data()); int amountMoved; - result = UiaTextRange::_moveEndpointByUnitCharacter(_pUiaData, - std::get<2>(data), - std::get<4>(data), - std::get<1>(data), - &amountMoved); - - VERIFY_ARE_EQUAL(std::get<3>(data), amountMoved); - VERIFY_ARE_EQUAL(std::get<5>(data), std::get<0>(result)); - VERIFY_ARE_EQUAL(std::get<6>(data), std::get<1>(result)); - VERIFY_ARE_EQUAL(std::get<7>(data), std::get<2>(result)); - } - } - TEST_METHOD(CanMoveEndpointByUnitWord) - { - const Column firstColumnIndex = 0; - const Column lastColumnIndex = _pScreenInfo->GetBufferSize().Width() - 1; - const ScreenInfoRow topRow = 0; - const ScreenInfoRow bottomRow = _pTextBuffer->TotalRowCount() - 1; - - const std::wstring_view text[] = { - L"word1 word2 word3", - L"word4 word5 word6" - }; + THROW_IF_FAILED(Microsoft::WRL::MakeAndInitialize(&utr, _pUiaData, &_dummyProvider, test.start, test.end)); + THROW_IF_FAILED(utr->MoveEndpointByUnit(test.endpoint, TextUnit::TextUnit_Character, test.moveAmt, &amountMoved)); - for (auto i = 0; i < 2; i++) - { - _pTextBuffer->WriteLine(text[i], { 0, gsl::narrow(i) }); - } - - // clang-format off - const std::vector> testData = - { - { - L"can't move _start past the beginning of the document when _start is positioned at the beginning", - { - topRow, firstColumnIndex, - topRow, lastColumnIndex, - topRow, - lastColumnIndex, - firstColumnIndex, - UiaTextRange::MovementIncrement::Backward, - UiaTextRange::MovementDirection::Backward - }, - -1, - 0, - TextPatternRangeEndpoint::TextPatternRangeEndpoint_Start, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, topRow) + firstColumnIndex, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, topRow) + lastColumnIndex, - false - }, - - { - L"can partially move _start to the begining of the document when it is closer than the move count requested", - { - topRow, firstColumnIndex + 15, - topRow, lastColumnIndex, - topRow, - lastColumnIndex, - firstColumnIndex, - UiaTextRange::MovementIncrement::Backward, - UiaTextRange::MovementDirection::Backward - }, - -5, - -2, - TextPatternRangeEndpoint::TextPatternRangeEndpoint_Start, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, topRow) + firstColumnIndex, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, topRow) + lastColumnIndex, - false - }, - - { - L"can't move _end past the begining of the document", - { - topRow, firstColumnIndex, - topRow, firstColumnIndex + 2, - topRow, - lastColumnIndex, - firstColumnIndex, - UiaTextRange::MovementIncrement::Backward, - UiaTextRange::MovementDirection::Backward - }, - -2, - -1, - TextPatternRangeEndpoint::TextPatternRangeEndpoint_End, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, topRow) + firstColumnIndex, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, topRow) + firstColumnIndex, - false - }, - - { - L"_start follows _end when passed during movement", - { - topRow + 1, firstColumnIndex + 2, - topRow + 1, firstColumnIndex + 10, - topRow, - lastColumnIndex, - firstColumnIndex, - UiaTextRange::MovementIncrement::Backward, - UiaTextRange::MovementDirection::Backward - }, - -4, - -4, - TextPatternRangeEndpoint::TextPatternRangeEndpoint_End, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, topRow) + 6, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, topRow) + 6, - true - }, - - { - L"can't move _end past the beginning of the document when _end is positioned at the end", - { - bottomRow, firstColumnIndex, - bottomRow, lastColumnIndex, - bottomRow, - firstColumnIndex, - lastColumnIndex, - UiaTextRange::MovementIncrement::Forward, - UiaTextRange::MovementDirection::Forward - }, - 1, - 0, - TextPatternRangeEndpoint::TextPatternRangeEndpoint_End, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, bottomRow) + firstColumnIndex, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, bottomRow) + lastColumnIndex, - false - }, - - { - L"can partially move _end to the end of the document when it is closer than the move count requested", - { - topRow, firstColumnIndex, - bottomRow, lastColumnIndex - 3, - bottomRow, - firstColumnIndex, - lastColumnIndex, - UiaTextRange::MovementIncrement::Forward, - UiaTextRange::MovementDirection::Forward - }, - 5, - 1, - TextPatternRangeEndpoint::TextPatternRangeEndpoint_End, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, topRow) + firstColumnIndex, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, bottomRow) + lastColumnIndex, - false - }, - - { - L"can't move _start past the end of the document", - { - bottomRow, lastColumnIndex - 4, - bottomRow, lastColumnIndex, - bottomRow, - firstColumnIndex, - lastColumnIndex, - UiaTextRange::MovementIncrement::Forward, - UiaTextRange::MovementDirection::Forward - }, - 5, - 1, - TextPatternRangeEndpoint::TextPatternRangeEndpoint_Start, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, bottomRow) + lastColumnIndex, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, bottomRow) + lastColumnIndex, - false - }, - - { - L"_end follows _start when passed during movement", - { - topRow, firstColumnIndex, - topRow, firstColumnIndex + 3, - topRow, - lastColumnIndex, - firstColumnIndex, - UiaTextRange::MovementIncrement::Forward, - UiaTextRange::MovementDirection::Forward - }, - 2, - 2, - TextPatternRangeEndpoint::TextPatternRangeEndpoint_Start, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, topRow)+15, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, topRow)+15, - true - }, - }; - // clang-format on - - for (auto data : testData) - { - Log::Comment(std::get<0>(data).c_str()); - std::tuple result; - int amountMoved; - result = UiaTextRange::_moveEndpointByUnitWord(_pUiaData, - std::get<2>(data), - std::get<4>(data), - std::get<1>(data), - L" ", - &amountMoved); - - VERIFY_ARE_EQUAL(std::get<3>(data), amountMoved); - VERIFY_ARE_EQUAL(std::get<5>(data), std::get<0>(result)); - VERIFY_ARE_EQUAL(std::get<6>(data), std::get<1>(result)); - VERIFY_ARE_EQUAL(std::get<7>(data), std::get<2>(result)); + VERIFY_ARE_EQUAL(test.expected.moveAmt, amountMoved); + VERIFY_ARE_EQUAL(test.expected.start, utr->_start); + VERIFY_ARE_EQUAL(test.expected.end, utr->_end); } } TEST_METHOD(CanMoveEndpointByUnitLine) { - const Column firstColumnIndex = 0; - const Column lastColumnIndex = _pScreenInfo->GetBufferSize().Width() - 1; - const ScreenInfoRow topRow = 0; - const ScreenInfoRow bottomRow = _pTextBuffer->TotalRowCount() - 1; + const SHORT lastColumnIndex = _pScreenInfo->GetBufferSize().Width() - 1; + const SHORT bottomRow = gsl::narrow(_pTextBuffer->TotalRowCount() - 1); // clang-format off - const std::vector> testData = + const std::vector testData { - { + MoveEndpointTest{ L"can move _end forward without affecting _start", - { - topRow, firstColumnIndex, - topRow, lastColumnIndex, - bottomRow, - firstColumnIndex, - lastColumnIndex, - UiaTextRange::MovementIncrement::Forward, - UiaTextRange::MovementDirection::Forward - }, + {0, 0}, + {lastColumnIndex, 0}, 1, + TextPatternRangeEndpoint_End, 1, - TextPatternRangeEndpoint::TextPatternRangeEndpoint_End, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, topRow) + firstColumnIndex, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, topRow + 1) + lastColumnIndex, - false + {0, 0}, + {0, 1} }, - { + MoveEndpointTest{ L"can move _end backward without affecting _start", - { - topRow + 1, firstColumnIndex, - topRow + 5, lastColumnIndex, - topRow, - lastColumnIndex, - firstColumnIndex, - UiaTextRange::MovementIncrement::Backward, - UiaTextRange::MovementDirection::Backward - }, + {0, 1}, + {lastColumnIndex, 5}, -2, + TextPatternRangeEndpoint_End, -2, - TextPatternRangeEndpoint::TextPatternRangeEndpoint_End, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, topRow + 1) + firstColumnIndex, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, topRow + 3) + lastColumnIndex, - false + {0, 1}, + {0, 4} }, - { + MoveEndpointTest{ L"can move _start forward without affecting _end", - { - topRow + 1, firstColumnIndex, - topRow + 5, lastColumnIndex, - bottomRow, - firstColumnIndex, - lastColumnIndex, - UiaTextRange::MovementIncrement::Forward, - UiaTextRange::MovementDirection::Forward - }, + {0, 1}, + {lastColumnIndex, 5}, 2, + TextPatternRangeEndpoint_Start, 2, - TextPatternRangeEndpoint::TextPatternRangeEndpoint_Start, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, topRow + 3) + firstColumnIndex, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, topRow + 5) + lastColumnIndex, - false + {0, 3}, + {lastColumnIndex, 5} }, - { + MoveEndpointTest{ L"can move _start backward without affecting _end", - { - topRow + 2, firstColumnIndex, - topRow + 5, lastColumnIndex, - topRow, - lastColumnIndex, - firstColumnIndex, - UiaTextRange::MovementIncrement::Backward, - UiaTextRange::MovementDirection::Backward - }, + {0, 2}, + {lastColumnIndex, 5}, -1, + TextPatternRangeEndpoint_Start, -1, - TextPatternRangeEndpoint::TextPatternRangeEndpoint_Start, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, topRow + 1) + firstColumnIndex, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, topRow + 5) + lastColumnIndex, - false + {0, 1}, + {lastColumnIndex, 5} }, - { + MoveEndpointTest{ L"can move _start backwards when it's already on the top row", - { - topRow, lastColumnIndex, - topRow, lastColumnIndex, - topRow, - lastColumnIndex, - firstColumnIndex, - UiaTextRange::MovementIncrement::Backward, - UiaTextRange::MovementDirection::Backward - }, + {lastColumnIndex, 0}, + {lastColumnIndex, 0}, -1, + TextPatternRangeEndpoint_Start, -1, - TextPatternRangeEndpoint::TextPatternRangeEndpoint_Start, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, topRow) + firstColumnIndex, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, topRow) + lastColumnIndex, - false + {0, 0}, + {lastColumnIndex, 0}, }, - { + MoveEndpointTest{ L"can't move _start backwards when it's at the start of the document already", - { - topRow, firstColumnIndex, - topRow, lastColumnIndex, - topRow, - lastColumnIndex, - firstColumnIndex, - UiaTextRange::MovementIncrement::Backward, - UiaTextRange::MovementDirection::Backward - }, + {0, 0}, + {lastColumnIndex, 0}, -1, + TextPatternRangeEndpoint_Start, 0, - TextPatternRangeEndpoint::TextPatternRangeEndpoint_Start, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, topRow) + firstColumnIndex, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, topRow) + lastColumnIndex, - false + {0, 0}, + {lastColumnIndex, 0} }, - { + MoveEndpointTest{ L"can move _end forwards when it's on the bottom row", - { - topRow, firstColumnIndex, - bottomRow, lastColumnIndex - 3, - bottomRow, - lastColumnIndex, - firstColumnIndex, - UiaTextRange::MovementIncrement::Forward, - UiaTextRange::MovementDirection::Forward - }, + {0, 0}, + {lastColumnIndex - 3, bottomRow}, 1, + TextPatternRangeEndpoint_End, 1, - TextPatternRangeEndpoint::TextPatternRangeEndpoint_End, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, topRow) + firstColumnIndex, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, bottomRow) + lastColumnIndex, - false + {0, 0}, + {0, bottomRow+1} }, - { + MoveEndpointTest{ L"can't move _end forwards when it's at the end of the document already", - { - topRow, firstColumnIndex, - bottomRow, lastColumnIndex, - bottomRow, - lastColumnIndex, - firstColumnIndex, - UiaTextRange::MovementIncrement::Forward, - UiaTextRange::MovementDirection::Forward - }, + {0, 0}, + {0, bottomRow+1}, 1, + TextPatternRangeEndpoint_End, 0, - TextPatternRangeEndpoint::TextPatternRangeEndpoint_End, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, topRow) + firstColumnIndex, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, bottomRow) + lastColumnIndex, - false + {0, 0}, + {0, bottomRow+1} }, - { + MoveEndpointTest{ L"moving _start forward when it's already on the bottom row creates a degenerate range at the document end", - { - bottomRow, firstColumnIndex, - bottomRow, lastColumnIndex, - bottomRow, - firstColumnIndex, - lastColumnIndex, - UiaTextRange::MovementIncrement::Forward, - UiaTextRange::MovementDirection::Forward - }, + {0, bottomRow}, + {lastColumnIndex, bottomRow}, 1, + TextPatternRangeEndpoint_Start, 1, - TextPatternRangeEndpoint::TextPatternRangeEndpoint_Start, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, bottomRow) + lastColumnIndex, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, bottomRow) + lastColumnIndex, - true + {0, bottomRow+1}, + {0, bottomRow+1} }, - { + MoveEndpointTest{ L"moving _end backward when it's already on the top row creates a degenerate range at the document start", - { - topRow, firstColumnIndex + 4, - topRow, lastColumnIndex - 5, - topRow, - lastColumnIndex, - firstColumnIndex, - UiaTextRange::MovementIncrement::Backward, - UiaTextRange::MovementDirection::Backward - }, + {4, 0}, + {lastColumnIndex - 5, 0}, -1, + TextPatternRangeEndpoint_End, -1, - TextPatternRangeEndpoint::TextPatternRangeEndpoint_End, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, topRow) + firstColumnIndex, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, topRow) + firstColumnIndex, - true + {0, 0}, + {0, 0} } }; // clang-format on - for (auto data : testData) + Microsoft::WRL::ComPtr utr; + for (const auto& test : testData) { - Log::Comment(std::get<0>(data).c_str()); - std::tuple result; + Log::Comment(test.comment.data()); int amountMoved; - result = UiaTextRange::_moveEndpointByUnitLine(_pUiaData, - std::get<2>(data), - std::get<4>(data), - std::get<1>(data), - &amountMoved); - - VERIFY_ARE_EQUAL(std::get<3>(data), amountMoved); - VERIFY_ARE_EQUAL(std::get<5>(data), std::get<0>(result)); - VERIFY_ARE_EQUAL(std::get<6>(data), std::get<1>(result)); - VERIFY_ARE_EQUAL(std::get<7>(data), std::get<2>(result)); + + THROW_IF_FAILED(Microsoft::WRL::MakeAndInitialize(&utr, _pUiaData, &_dummyProvider, test.start, test.end)); + THROW_IF_FAILED(utr->MoveEndpointByUnit(test.endpoint, TextUnit::TextUnit_Line, test.moveAmt, &amountMoved)); + + VERIFY_ARE_EQUAL(test.expected.moveAmt, amountMoved); + VERIFY_ARE_EQUAL(test.expected.start, utr->_start); + VERIFY_ARE_EQUAL(test.expected.end, utr->_end); } } TEST_METHOD(CanMoveEndpointByUnitDocument) { - const Column firstColumnIndex = 0; - const Column lastColumnIndex = _pScreenInfo->GetBufferSize().Width() - 1; - const ScreenInfoRow topRow = 0; - const ScreenInfoRow bottomRow = _pTextBuffer->TotalRowCount() - 1; + const SHORT lastColumnIndex = _pScreenInfo->GetBufferSize().Width() - 1; + const SHORT bottomRow = gsl::narrow(_pTextBuffer->TotalRowCount() - 1); // clang-format off - const std::vector> testData = + const std::vector testData = { - { + MoveEndpointTest{ L"can move _end forward to end of document without affecting _start", - { - topRow, firstColumnIndex + 4, - topRow, firstColumnIndex + 4, - bottomRow, - firstColumnIndex, - lastColumnIndex, - UiaTextRange::MovementIncrement::Forward, - UiaTextRange::MovementDirection::Forward - }, - 1, + {0, 4}, + {0, 4}, 1, - TextPatternRangeEndpoint::TextPatternRangeEndpoint_End, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, topRow) + firstColumnIndex + 4, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, bottomRow) + lastColumnIndex, - false + TextPatternRangeEndpoint_End, + { + 1, + {0, 4}, + {0, bottomRow+1} + } }, - { + MoveEndpointTest{ L"can move _start backward to end of document without affect _end", - { - topRow, firstColumnIndex + 4, - topRow, firstColumnIndex + 4, - topRow, - lastColumnIndex, - firstColumnIndex, - UiaTextRange::MovementIncrement::Backward, - UiaTextRange::MovementDirection::Backward - }, + {0, 4}, + {0, 4}, -1, - -1, - TextPatternRangeEndpoint::TextPatternRangeEndpoint_Start, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, topRow) + firstColumnIndex, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, topRow) + 4, - false + TextPatternRangeEndpoint_Start, + { + -1, + {0, 0}, + {0, 4} + } }, - { + MoveEndpointTest{ L"can't move _end forward when it's already at the end of the document", - { - topRow + 3, firstColumnIndex + 2, - bottomRow, lastColumnIndex, - bottomRow, - firstColumnIndex, - lastColumnIndex, - UiaTextRange::MovementIncrement::Forward, - UiaTextRange::MovementDirection::Forward - }, + {3, 2}, + {0, bottomRow+1}, 1, - 0, - TextPatternRangeEndpoint::TextPatternRangeEndpoint_End, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, topRow + 3) + firstColumnIndex + 2, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, bottomRow) + lastColumnIndex, - false + TextPatternRangeEndpoint_End, + { + 0, + {3, 2}, + {0, bottomRow+1} + } }, - { + MoveEndpointTest{ L"can't move _start backward when it's already at the start of the document", - { - topRow, firstColumnIndex, - topRow + 5, firstColumnIndex + 6, - topRow, - lastColumnIndex, - firstColumnIndex, - UiaTextRange::MovementIncrement::Backward, - UiaTextRange::MovementDirection::Backward - }, + {0, 0}, + {5, 6}, -1, - 0, - TextPatternRangeEndpoint::TextPatternRangeEndpoint_Start, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, topRow) + firstColumnIndex, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, topRow + 5) + 6, - false + TextPatternRangeEndpoint_Start, + { + 0, + {0, 0}, + {5, 6} + } }, - { + MoveEndpointTest{ L"moving _end backward creates degenerate range at start of document", - { - topRow + 5, firstColumnIndex + 2, - topRow + 5, firstColumnIndex + 6, - topRow, - lastColumnIndex, - firstColumnIndex, - UiaTextRange::MovementIncrement::Backward, - UiaTextRange::MovementDirection::Backward - }, + {5, 2}, + {5, 6}, -1, - -1, - TextPatternRangeEndpoint::TextPatternRangeEndpoint_End, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, topRow) + firstColumnIndex, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, topRow) + firstColumnIndex, - true + TextPatternRangeEndpoint_End, + { + -1, + {0, 0}, + {0, 0} + } }, - { + MoveEndpointTest{ L"moving _start forward creates degenerate range at end of document", - { - topRow + 5, firstColumnIndex + 2, - topRow + 5, firstColumnIndex + 6, - bottomRow, - firstColumnIndex, - lastColumnIndex, - UiaTextRange::MovementIncrement::Forward, - UiaTextRange::MovementDirection::Forward - }, - 1, + {5, 2}, + {5, 6}, 1, - TextPatternRangeEndpoint::TextPatternRangeEndpoint_Start, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, bottomRow) + lastColumnIndex, - UiaTextRange::_screenInfoRowToEndpoint(_pUiaData, bottomRow) + lastColumnIndex, - true + TextPatternRangeEndpoint_Start, + { + 1, + {0, bottomRow+1}, + {0, bottomRow+1} + } } }; // clang-format on - for (auto data : testData) + Microsoft::WRL::ComPtr utr; + for (auto test : testData) { - Log::Comment(std::get<0>(data).c_str()); - std::tuple result; + Log::Comment(test.comment.c_str()); int amountMoved; - result = UiaTextRange::_moveEndpointByUnitDocument(_pUiaData, - std::get<2>(data), - std::get<4>(data), - std::get<1>(data), - &amountMoved); - - VERIFY_ARE_EQUAL(std::get<3>(data), amountMoved); - VERIFY_ARE_EQUAL(std::get<5>(data), std::get<0>(result)); - VERIFY_ARE_EQUAL(std::get<6>(data), std::get<1>(result)); - VERIFY_ARE_EQUAL(std::get<7>(data), std::get<2>(result)); + + THROW_IF_FAILED(Microsoft::WRL::MakeAndInitialize(&utr, _pUiaData, &_dummyProvider, test.start, test.end)); + THROW_IF_FAILED(utr->MoveEndpointByUnit(test.endpoint, TextUnit::TextUnit_Document, test.moveAmt, &amountMoved)); + + VERIFY_ARE_EQUAL(test.expected.moveAmt, amountMoved); + VERIFY_ARE_EQUAL(test.expected.start, utr->_start); + VERIFY_ARE_EQUAL(test.expected.end, utr->_end); } } }; diff --git a/src/types/ScreenInfoUiaProviderBase.cpp b/src/types/ScreenInfoUiaProviderBase.cpp index 452bf3c9022..bc95e3ba972 100644 --- a/src/types/ScreenInfoUiaProviderBase.cpp +++ b/src/types/ScreenInfoUiaProviderBase.cpp @@ -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(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 range; hr = CreateTextRange(this, start, end, - false, _wordDelimiters, &range); if (FAILED(hr)) diff --git a/src/types/ScreenInfoUiaProviderBase.h b/src/types/ScreenInfoUiaProviderBase.h index 3491730873d..50f4f76b508 100644 --- a/src/types/ScreenInfoUiaProviderBase.h +++ b/src/types/ScreenInfoUiaProviderBase.h @@ -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; diff --git a/src/types/UiaTextRangeBase.cpp b/src/types/UiaTextRangeBase.cpp index d1ad0f02729..d226263e59d 100644 --- a/src/types/UiaTextRangeBase.cpp +++ b/src/types/UiaTextRangeBase.cpp @@ -9,91 +9,22 @@ using namespace Microsoft::Console::Types; using namespace Microsoft::Console::Types::UiaTextRangeBaseTracing; // toggle these for additional logging in a debug build +//#define _DEBUG 1 //#define UIATEXTRANGE_DEBUG_MSGS 1 #undef UIATEXTRANGE_DEBUG_MSGS IdType UiaTextRangeBase::id = 1; -UiaTextRangeBase::MoveState::MoveState(IUiaData* pData, - const UiaTextRangeBase& range, - const MovementDirection direction) : - StartScreenInfoRow{ UiaTextRangeBase::_endpointToScreenInfoRow(pData, range.GetStart()) }, - StartColumn{ UiaTextRangeBase::_endpointToColumn(pData, range.GetStart()) }, - EndScreenInfoRow{ UiaTextRangeBase::_endpointToScreenInfoRow(pData, range.GetEnd()) }, - EndColumn{ UiaTextRangeBase::_endpointToColumn(pData, range.GetEnd()) }, - Direction{ direction } -{ - if (direction == MovementDirection::Forward) - { - LimitingRow = UiaTextRangeBase::_getLastScreenInfoRowIndex(pData); - FirstColumnInRow = UiaTextRangeBase::_getFirstColumnIndex(); - LastColumnInRow = UiaTextRangeBase::_getLastColumnIndex(pData); - Increment = MovementIncrement::Forward; - } - else - { - LimitingRow = UiaTextRangeBase::_getFirstScreenInfoRowIndex(); - FirstColumnInRow = UiaTextRangeBase::_getLastColumnIndex(pData); - LastColumnInRow = UiaTextRangeBase::_getFirstColumnIndex(); - Increment = MovementIncrement::Backward; - } -} - -UiaTextRangeBase::MoveState::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 : - StartScreenInfoRow{ startScreenInfoRow }, - StartColumn{ startColumn }, - EndScreenInfoRow{ endScreenInfoRow }, - EndColumn{ endColumn }, - LimitingRow{ limitingRow }, - FirstColumnInRow{ firstColumnInRow }, - LastColumnInRow{ lastColumnInRow }, - Increment{ increment }, - Direction{ direction } -{ -} - #if _DEBUG #include -// This is a debugging function that prints out the current -// relationship between screen info rows, text buffer rows, and -// endpoints. -void UiaTextRangeBase::_outputRowConversions(IUiaData* pData) -{ - try - { - unsigned int totalRows = _getTotalRows(pData); - OutputDebugString(L"screenBuffer\ttextBuffer\tendpoint\n"); - for (unsigned int i = 0; i < totalRows; ++i) - { - std::wstringstream ss; - ss << i << "\t" << _screenInfoRowToTextBufferRow(pData, i) << "\t" << _screenInfoRowToEndpoint(pData, i) << "\n"; - std::wstring str = ss.str(); - OutputDebugString(str.c_str()); - } - OutputDebugString(L"\n"); - } - catch (...) - { - LOG_HR(wil::ResultFromCaughtException()); - } -} - void UiaTextRangeBase::_outputObjectState() { std::wstringstream ss; ss << "Object State"; ss << " _id: " << _id; - ss << " _start: " << _start; - ss << " _end: " << _end; - ss << " _degenerate: " << _degenerate; + ss << " _start: { " << _start.X << ", " << _start.Y << " }"; + ss << " _end: { " << _end.X << ", " << _end.Y << " }"; + ss << " _degenerate: " << IsDegenerate(); std::wstring str = ss.str(); OutputDebugString(str.c_str()); @@ -110,10 +41,9 @@ try RETURN_HR_IF_NULL(E_INVALIDARG, pData); _pProvider = pProvider; - _start = 0; - _end = 0; - _degenerate = true; _pData = pData; + _start = pData->GetViewport().Origin(); + _end = pData->GetViewport().Origin(); _wordDelimiters = wordDelimiters; _id = id; @@ -132,15 +62,14 @@ CATCH_RETURN(); #pragma warning(suppress : 26434) // WRL RuntimeClassInitialize base is a no-op and we need this for MakeAndInitialize HRESULT UiaTextRangeBase::RuntimeClassInitialize(_In_ IUiaData* pData, _In_ IRawElementProviderSimple* const pProvider, - const Cursor& cursor, + _In_ const Cursor& cursor, _In_ std::wstring_view wordDelimiters) noexcept { RETURN_IF_FAILED(RuntimeClassInitialize(pData, pProvider, wordDelimiters)); try { - _degenerate = true; - _start = _screenInfoRowToEndpoint(_pData, cursor.GetPosition().Y) + cursor.GetPosition().X; + _start = cursor.GetPosition(); _end = _start; #if defined(_DEBUG) && defined(UIATEXTRANGE_DEBUG_MSGS) @@ -158,17 +87,14 @@ HRESULT UiaTextRangeBase::RuntimeClassInitialize(_In_ IUiaData* pData, #pragma warning(suppress : 26434) // WRL RuntimeClassInitialize base is a no-op and we need this for MakeAndInitialize HRESULT UiaTextRangeBase::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) noexcept { RETURN_IF_FAILED(RuntimeClassInitialize(pData, pProvider, wordDelimiters)); - RETURN_HR_IF(E_INVALIDARG, !degenerate && start > end); - _degenerate = degenerate; _start = start; - _end = degenerate ? start : end; + _end = end; #if defined(_DEBUG) && defined(UIATEXTRANGE_DEBUG_MSGS) OutputDebugString(L"Constructor\n"); @@ -186,7 +112,7 @@ void UiaTextRangeBase::Initialize(_In_ const UiaPoint point) // get row that point resides in const RECT windowRect = _getTerminalRect(); const SMALL_RECT viewport = _pData->GetViewport().ToInclusive(); - ScreenInfoRow row = 0; + short row = 0; if (clientPoint.y <= windowRect.top) { row = viewport.Top; @@ -201,21 +127,21 @@ void UiaTextRangeBase::Initialize(_In_ const UiaPoint point) _TranslatePointFromScreen(&clientPoint); const COORD currentFontSize = _getScreenFontSize(); - row = (clientPoint.y / currentFontSize.Y) + viewport.Top; + row = gsl::narrow(clientPoint.y / static_cast(currentFontSize.Y)) + viewport.Top; } - _start = _screenInfoRowToEndpoint(_pData, row); + _start = { 0, row }; _end = _start; - _degenerate = true; } #pragma warning(suppress : 26434) // WRL RuntimeClassInitialize base is a no-op and we need this for MakeAndInitialize HRESULT UiaTextRangeBase::RuntimeClassInitialize(const UiaTextRangeBase& a) noexcept +try { _pProvider = a._pProvider; _start = a._start; _end = a._end; - _degenerate = a._degenerate; _pData = a._pData; + _wordDelimiters = a._wordDelimiters; _id = id; ++id; @@ -227,20 +153,60 @@ HRESULT UiaTextRangeBase::RuntimeClassInitialize(const UiaTextRangeBase& a) noex return S_OK; } +CATCH_RETURN(); const IdType UiaTextRangeBase::GetId() const noexcept { return _id; } -const Endpoint UiaTextRangeBase::GetStart() const noexcept +const COORD UiaTextRangeBase::GetEndpoint(TextPatternRangeEndpoint endpoint) const noexcept { - return _start; + switch (endpoint) + { + case TextPatternRangeEndpoint::TextPatternRangeEndpoint_End: + return _end; + case TextPatternRangeEndpoint::TextPatternRangeEndpoint_Start: + default: + return _start; + } } -const Endpoint UiaTextRangeBase::GetEnd() const noexcept +// Routine Description: +// - sets the target endpoint to the given COORD value +// - if the target endpoint crosses the other endpoint, become a degenerate range +// Arguments: +// - endpoint - the target endpoint (start or end) +// - val - the value that it will be set to +// Return Value: +// - true if range is degenerate, false otherwise. +bool UiaTextRangeBase::SetEndpoint(TextPatternRangeEndpoint endpoint, const COORD val) { - return _end; + const auto bufferSize = _pData->GetTextBuffer().GetSize(); + switch (endpoint) + { + case TextPatternRangeEndpoint::TextPatternRangeEndpoint_End: + _end = val; + // if end is before start... + if (bufferSize.CompareInBounds(_end, _start, true) < 0) + { + // make this range degenerate at end + _start = _end; + } + break; + case TextPatternRangeEndpoint::TextPatternRangeEndpoint_Start: + _start = val; + // if start is after end... + if (bufferSize.CompareInBounds(_start, _end, true) > 0) + { + // make this range degenerate at start + _end = _start; + } + break; + default: + break; + } + return IsDegenerate(); } // Routine Description: @@ -251,14 +217,7 @@ const Endpoint UiaTextRangeBase::GetEnd() const noexcept // - true if range is degenerate, false otherwise. const bool UiaTextRangeBase::IsDegenerate() const noexcept { - return _degenerate; -} - -void UiaTextRangeBase::SetRangeValues(const Endpoint start, const Endpoint end, const bool isDegenerate) noexcept -{ - _start = start; - _end = end; - _degenerate = isDegenerate; + return _start == _end; } #pragma region ITextRangeProvider @@ -275,9 +234,8 @@ IFACEMETHODIMP UiaTextRangeBase::Compare(_In_opt_ ITextRangeProvider* pRange, _O const UiaTextRangeBase* other = static_cast(pRange); if (other) { - *pRetVal = !!(_start == other->GetStart() && - _end == other->GetEnd() && - _degenerate == other->IsDegenerate()); + *pRetVal = (_start == other->GetEndpoint(TextPatternRangeEndpoint_Start) && + _end == other->GetEndpoint(TextPatternRangeEndpoint_End)); } // TODO GitHub #1914: Re-attach Tracing to UIA Tree // tracing @@ -293,6 +251,7 @@ IFACEMETHODIMP UiaTextRangeBase::CompareEndpoints(_In_ TextPatternRangeEndpoint _In_ ITextRangeProvider* pTargetRange, _In_ TextPatternRangeEndpoint targetEndpoint, _Out_ int* pRetVal) noexcept +try { RETURN_HR_IF(E_INVALIDARG, pRetVal == nullptr); *pRetVal = 0; @@ -305,13 +264,13 @@ IFACEMETHODIMP UiaTextRangeBase::CompareEndpoints(_In_ TextPatternRangeEndpoint } // get endpoint value that we're comparing to - const Endpoint theirValue = targetEndpoint == TextPatternRangeEndpoint::TextPatternRangeEndpoint_Start ? range->GetStart() : range->GetEnd() + 1; + const auto other = range->GetEndpoint(targetEndpoint); // get the values of our endpoint - const Endpoint ourValue = endpoint == TextPatternRangeEndpoint::TextPatternRangeEndpoint_Start ? _start : _end + 1; + const auto mine = GetEndpoint(endpoint); // compare them - *pRetVal = std::clamp(static_cast(ourValue) - static_cast(theirValue), -1, 1); + *pRetVal = _pData->GetTextBuffer().GetSize().CompareInBounds(mine, other, true); // TODO GitHub #1914: Re-attach Tracing to UIA Tree // tracing @@ -324,8 +283,9 @@ IFACEMETHODIMP UiaTextRangeBase::CompareEndpoints(_In_ TextPatternRangeEndpoint return S_OK; } +CATCH_RETURN(); -IFACEMETHODIMP UiaTextRangeBase::ExpandToEnclosingUnit(_In_ TextUnit unit) +IFACEMETHODIMP UiaTextRangeBase::ExpandToEnclosingUnit(_In_ TextUnit unit) noexcept { _pData->LockConsole(); auto Unlock = wil::scope_exit([&]() noexcept { @@ -339,40 +299,37 @@ IFACEMETHODIMP UiaTextRangeBase::ExpandToEnclosingUnit(_In_ TextUnit unit) try { - const ScreenInfoRow topRow = _getFirstScreenInfoRowIndex(); - const ScreenInfoRow bottomRow = _getLastScreenInfoRowIndex(_pData); + const auto& buffer = _pData->GetTextBuffer(); + const auto bufferSize = buffer.GetSize(); + const auto bufferEnd = bufferSize.EndExclusive(); if (unit == TextUnit::TextUnit_Character) { _end = _start; + bufferSize.IncrementInBounds(_end, true); } else if (unit <= TextUnit::TextUnit_Word) { // expand to word - const auto target = _start; - _start = _wordBeginEndpoint(_pData, target, _wordDelimiters); - _end = _wordEndEndpoint(_pData, target, _wordDelimiters); - FAIL_FAST_IF(!(_start <= _end)); + _start = buffer.GetWordStart(_start, _wordDelimiters, true); + _end = buffer.GetWordEnd(_start, _wordDelimiters, true); } else if (unit <= TextUnit::TextUnit_Line) { // expand to line - _start = _textBufferRowToEndpoint(_pData, _endpointToTextBufferRow(_pData, _start)); - _end = _start + _getLastColumnIndex(_pData); - FAIL_FAST_IF(!(_start <= _end)); + _start.X = 0; + _end.X = 0; + _end.Y = base::ClampAdd(_start.Y, 1); } else { // expand to document - _start = _screenInfoRowToEndpoint(_pData, topRow); - _end = _screenInfoRowToEndpoint(_pData, bottomRow) + _getLastColumnIndex(_pData); + _start = bufferSize.Origin(); + _end = bufferSize.EndExclusive(); } - _degenerate = false; - // TODO GitHub #1914: Re-attach Tracing to UIA Tree //Tracing::s_TraceUia(this, ApiCall::ExpandToEnclosingUnit, &apiMsg); - return S_OK; } CATCH_RETURN(); @@ -409,7 +366,7 @@ IFACEMETHODIMP UiaTextRangeBase::GetAttributeValue(_In_ TEXTATTRIBUTEID textAttr return S_OK; } -IFACEMETHODIMP UiaTextRangeBase::GetBoundingRectangles(_Outptr_result_maybenull_ SAFEARRAY** ppRetVal) +IFACEMETHODIMP UiaTextRangeBase::GetBoundingRectangles(_Outptr_result_maybenull_ SAFEARRAY** ppRetVal) noexcept { _pData->LockConsole(); auto Unlock = wil::scope_exit([&]() noexcept { @@ -425,23 +382,64 @@ IFACEMETHODIMP UiaTextRangeBase::GetBoundingRectangles(_Outptr_result_maybenull_ // order: left, top, width, height. each line will have its own // set of coords. std::vector coords; - const TextBufferRow startRow = _endpointToTextBufferRow(_pData, _start); - if (_degenerate && _isScreenInfoRowInViewport(_pData, startRow)) + const auto bufferSize = _pData->GetTextBuffer().GetSize(); + + // these viewport vars are converted to the buffer coordinate space + const auto viewport = bufferSize.ConvertToOrigin(_pData->GetViewport()); + const auto viewportOrigin = viewport.Origin(); + const auto viewportEnd = viewport.EndExclusive(); + + // startAnchor: the earliest COORD we will get a bounding rect for + auto startAnchor = GetEndpoint(TextPatternRangeEndpoint_Start); + if (bufferSize.CompareInBounds(startAnchor, viewportOrigin, true) < 0) + { + // earliest we can be is the origin + startAnchor = viewportOrigin; + } + + // endAnchor: the latest COORD we will get a bounding rect for + auto endAnchor = GetEndpoint(TextPatternRangeEndpoint_End); + if (bufferSize.CompareInBounds(endAnchor, viewportEnd, true) > 0) + { + // latest we can be is the viewport end + endAnchor = viewportEnd; + } + + // _end is exclusive, let's be inclusive so we don't have to think about it anymore for bounding rects + bufferSize.DecrementInBounds(endAnchor, true); + + if (IsDegenerate() || bufferSize.CompareInBounds(_start, viewportEnd, true) > 0 || bufferSize.CompareInBounds(_end, viewportOrigin, true) < 0) { - _addScreenInfoRowBoundaries(_pData, _textBufferRowToScreenInfoRow(_pData, startRow), coords); + // An empty array is returned for a degenerate (empty) text range. + // reference: https://docs.microsoft.com/en-us/windows/win32/api/uiautomationclient/nf-uiautomationclient-iuiautomationtextrange-getboundingrectangles + + // Remember, start cannot be past end, so + // if start is past the viewport end, + // or end is past the viewport origin + // draw nothing } else { - const unsigned int totalRowsInRange = _rowCountInRange(_pData); - for (unsigned int i = 0; i < totalRowsInRange; ++i) + for (auto row = startAnchor.Y; row <= endAnchor.Y; ++row) { - const ScreenInfoRow screenInfoRow = _textBufferRowToScreenInfoRow(_pData, startRow + i); - if (!_isScreenInfoRowInViewport(_pData, screenInfoRow)) + // assume that we are going to draw the entire row + COORD startCoord = { 0, row }; + COORD endCoord = { viewport.RightInclusive(), row }; + + if (row == startAnchor.Y) + { + // first row --> reduce left side + startCoord.X = startAnchor.X; + } + + if (row == endAnchor.Y) { - continue; + // last row --> reduce right side + endCoord.X = endAnchor.X; } - _addScreenInfoRowBoundaries(_pData, screenInfoRow, coords); + + _getBoundingRect(startCoord, endCoord, coords); } } @@ -471,16 +469,20 @@ IFACEMETHODIMP UiaTextRangeBase::GetBoundingRectangles(_Outptr_result_maybenull_ return S_OK; } -IFACEMETHODIMP UiaTextRangeBase::GetEnclosingElement(_Outptr_result_maybenull_ IRawElementProviderSimple** ppRetVal) +IFACEMETHODIMP UiaTextRangeBase::GetEnclosingElement(_Outptr_result_maybenull_ IRawElementProviderSimple** ppRetVal) noexcept +try { RETURN_HR_IF(E_INVALIDARG, ppRetVal == nullptr); *ppRetVal = nullptr; //Tracing::s_TraceUia(this, ApiCall::GetBoundingRectangles, nullptr); +#pragma warning(suppress : 26447) // QueryInterface's exception should be caught by the try-CATCH_RETURN block to allow this to be noexcept return _pProvider->QueryInterface(IID_PPV_ARGS(ppRetVal)); } +CATCH_RETURN(); -IFACEMETHODIMP UiaTextRangeBase::GetText(_In_ int maxLength, _Out_ BSTR* pRetVal) +IFACEMETHODIMP UiaTextRangeBase::GetText(_In_ int maxLength, _Out_ BSTR* pRetVal) noexcept +try { _pData->LockConsole(); auto Unlock = wil::scope_exit([&]() noexcept { @@ -501,67 +503,63 @@ IFACEMETHODIMP UiaTextRangeBase::GetText(_In_ int maxLength, _Out_ BSTR* pRetVal // truncated. const bool getPartialText = maxLength != -1; - if (!_degenerate) + if (!IsDegenerate()) { - try - { - const ScreenInfoRow startScreenInfoRow = _endpointToScreenInfoRow(_pData, _start); - const Column startColumn = _endpointToColumn(_pData, _start); - const ScreenInfoRow endScreenInfoRow = _endpointToScreenInfoRow(_pData, _end); - const Column endColumn = _endpointToColumn(_pData, _end); - const unsigned int totalRowsInRange = _rowCountInRange(_pData); - const TextBuffer& textBuffer = _pData->GetTextBuffer(); - #if defined(_DEBUG) && defined(UIATEXTRANGE_DEBUG_MSGS) - std::wstringstream ss; - ss << L"---Initial span start=" << _start << L" and end=" << _end << L"\n"; - ss << L"----Retrieving sr:" << startScreenInfoRow << L" sc:" << startColumn << L" er:" << endScreenInfoRow << L" ec:" << endColumn << L"\n"; - OutputDebugString(ss.str().c_str()); + std::wstringstream ss; + ss << L"---Initial span start={" << _start.X << L", " << _start.Y << L"} and end={" << _end.X << ", " << _end.Y << L"}\n"; + OutputDebugString(ss.str().c_str()); #endif - ScreenInfoRow currentScreenInfoRow = 0; - for (unsigned int i = 0; i < totalRowsInRange; ++i) + // if _end is at 0, we ignore that row because _end is exclusive + const auto& buffer = _pData->GetTextBuffer(); + const short totalRowsInRange = (_end.X == buffer.GetSize().Left()) ? + base::ClampSub(_end.Y, _start.Y) : + base::ClampAdd(base::ClampSub(_end.Y, _start.Y), base::ClampedNumeric(1)); + const short lastRowInRange = _start.Y + totalRowsInRange - 1; + + short currentScreenInfoRow = 0; + for (short i = 0; i < totalRowsInRange; ++i) + { + currentScreenInfoRow = _start.Y + i; + const ROW& row = buffer.GetRowByOffset(currentScreenInfoRow); + if (row.GetCharRow().ContainsText()) { - currentScreenInfoRow = startScreenInfoRow + i; - const ROW& row = textBuffer.GetRowByOffset(currentScreenInfoRow); - if (row.GetCharRow().ContainsText()) + const size_t rowRight = row.GetCharRow().MeasureRight(); + size_t startIndex = 0; + size_t endIndex = rowRight; + if (currentScreenInfoRow == _start.Y) { - const size_t rowRight = row.GetCharRow().MeasureRight(); - size_t startIndex = 0; - size_t endIndex = rowRight; - if (currentScreenInfoRow == startScreenInfoRow) - { - startIndex = startColumn; - } - if (currentScreenInfoRow == endScreenInfoRow) - { - // prevent the end from going past the last non-whitespace char in the row - endIndex = std::min(gsl::narrow_cast(endColumn) + 1, rowRight); - } - - // if startIndex >= endIndex then _start is - // further to the right than the last - // non-whitespace char in the row so there - // wouldn't be any text to grab. - if (startIndex < endIndex) - { - wstr += row.GetText().substr(startIndex, endIndex - startIndex); - } + startIndex = _start.X; } - if (currentScreenInfoRow != endScreenInfoRow) + if (currentScreenInfoRow == _end.Y) { - wstr += L"\r\n"; + // prevent the end from going past the last non-whitespace char in the row + endIndex = std::max(startIndex + 1, std::min(gsl::narrow_cast(_end.X), rowRight)); } - if (getPartialText && wstr.size() > static_cast(maxLength)) + // if startIndex >= endIndex then _start is + // further to the right than the last + // non-whitespace char in the row so there + // wouldn't be any text to grab. + if (startIndex < endIndex) { - wstr.resize(maxLength); - break; + wstr += row.GetText().substr(startIndex, endIndex - startIndex); } } + + if (currentScreenInfoRow != lastRowInRange) + { + wstr += L"\r\n"; + } + + if (getPartialText && wstr.size() > static_cast(maxLength)) + { + wstr.resize(maxLength); + break; + } } - CATCH_RETURN(); } *pRetVal = SysAllocString(wstr.c_str()); @@ -580,16 +578,12 @@ IFACEMETHODIMP UiaTextRangeBase::GetText(_In_ int maxLength, _Out_ BSTR* pRetVal return S_OK; } +CATCH_RETURN(); IFACEMETHODIMP UiaTextRangeBase::Move(_In_ TextUnit unit, _In_ int count, - _Out_ int* pRetVal) + _Out_ int* pRetVal) noexcept { - _pData->LockConsole(); - auto Unlock = wil::scope_exit([&]() noexcept { - _pData->UnlockConsole(); - }); - RETURN_HR_IF(E_INVALIDARG, pRetVal == nullptr); *pRetVal = 0; if (count == 0) @@ -607,46 +601,48 @@ IFACEMETHODIMP UiaTextRangeBase::Move(_In_ TextUnit unit, _outputObjectState(); std::wstringstream ss; + ss << L" unit: " << unit; ss << L" count: " << count; std::wstring data = ss.str(); OutputDebugString(data.c_str()); OutputDebugString(L"\n"); - _outputRowConversions(); #endif - std::function(gsl::not_null, const int, const MoveState, gsl::not_null const)> moveFunc = &_moveByDocument; - if (unit == TextUnit::TextUnit_Character) - { - moveFunc = &_moveByCharacter; - } - else if (unit <= TextUnit::TextUnit_Word) - { - moveFunc = std::bind(&_moveByWord, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, _wordDelimiters, std::placeholders::_4); - } - else if (unit <= TextUnit::TextUnit_Line) - { - moveFunc = &_moveByLine; - } - - const MovementDirection moveDirection = (count > 0) ? MovementDirection::Forward : MovementDirection::Backward; - std::pair newEndpoints; + _pData->LockConsole(); + auto Unlock = wil::scope_exit([&]() noexcept { + _pData->UnlockConsole(); + }); + // We can abstract this movement by moving _start, but disallowing moving to the end of the buffer + constexpr auto endpoint = TextPatternRangeEndpoint::TextPatternRangeEndpoint_Start; + constexpr auto preventBufferEnd = true; try { - const MoveState moveState{ _pData, *this, moveDirection }; - newEndpoints = moveFunc(_pData, - count, - moveState, - pRetVal); + if (unit == TextUnit::TextUnit_Character) + { + _moveEndpointByUnitCharacter(count, endpoint, pRetVal, preventBufferEnd); + } + else if (unit <= TextUnit::TextUnit_Word) + { + _moveEndpointByUnitWord(count, endpoint, pRetVal, preventBufferEnd); + } + else if (unit <= TextUnit::TextUnit_Line) + { + _moveEndpointByUnitLine(count, endpoint, pRetVal, preventBufferEnd); + } + else if (unit <= TextUnit::TextUnit_Document) + { + _moveEndpointByUnitDocument(count, endpoint, pRetVal, preventBufferEnd); + } } CATCH_RETURN(); - _start = newEndpoints.first; - _end = newEndpoints.second; - - // a range can't be degenerate after both endpoints have been - // moved. - _degenerate = false; + // If we actually moved... + if (*pRetVal != 0) + { + // then just expand to get our _end + ExpandToEnclosingUnit(unit); + } // TODO GitHub #1914: Re-attach Tracing to UIA Tree // tracing @@ -659,13 +655,8 @@ IFACEMETHODIMP UiaTextRangeBase::Move(_In_ TextUnit unit, IFACEMETHODIMP UiaTextRangeBase::MoveEndpointByUnit(_In_ TextPatternRangeEndpoint endpoint, _In_ TextUnit unit, _In_ int count, - _Out_ int* pRetVal) + _Out_ int* pRetVal) noexcept { - _pData->LockConsole(); - auto Unlock = wil::scope_exit([&]() noexcept { - _pData->UnlockConsole(); - }); - RETURN_HR_IF(E_INVALIDARG, pRetVal == nullptr); *pRetVal = 0; if (count == 0) @@ -689,43 +680,34 @@ IFACEMETHODIMP UiaTextRangeBase::MoveEndpointByUnit(_In_ TextPatternRangeEndpoin std::wstring data = ss.str(); OutputDebugString(data.c_str()); OutputDebugString(L"\n"); - _outputRowConversions(); #endif - const MovementDirection moveDirection = (count > 0) ? MovementDirection::Forward : MovementDirection::Backward; - - std::function moveFunc = &_moveEndpointByUnitDocument; - if (unit == TextUnit::TextUnit_Character) - { - moveFunc = &_moveEndpointByUnitCharacter; - } - else if (unit <= TextUnit::TextUnit_Word) - { - // bind all params of this function, except put a _wordDelimiters in there - // NOTE: using a lambda function here because... - // - lambda is cheaper than std::bind - // - _move* functions are static, but this particular consumer needs access to a member (may be fixed by TODO GH #1993) - moveFunc = [=](auto&& pData, auto&& moveCount, auto&& endpoint, auto&& moveState, auto&& pAmountMoved) { - return _moveEndpointByUnitWord(pData, moveCount, endpoint, moveState, _wordDelimiters, pAmountMoved); - }; - } - else if (unit <= TextUnit::TextUnit_Line) - { - moveFunc = &_moveEndpointByUnitLine; - } + _pData->LockConsole(); + auto Unlock = wil::scope_exit([&]() noexcept { + _pData->UnlockConsole(); + }); - std::tuple moveResults; try { - const MoveState moveState{ _pData, *this, moveDirection }; - moveResults = moveFunc(_pData, count, endpoint, moveState, pRetVal); + if (unit == TextUnit::TextUnit_Character) + { + _moveEndpointByUnitCharacter(count, endpoint, pRetVal); + } + else if (unit <= TextUnit::TextUnit_Word) + { + _moveEndpointByUnitWord(count, endpoint, pRetVal); + } + else if (unit <= TextUnit::TextUnit_Line) + { + _moveEndpointByUnitLine(count, endpoint, pRetVal); + } + else if (unit <= TextUnit::TextUnit_Document) + { + _moveEndpointByUnitDocument(count, endpoint, pRetVal); + } } CATCH_RETURN(); - _start = std::get<0>(moveResults); - _end = std::get<1>(moveResults); - _degenerate = std::get<2>(moveResults); - // TODO GitHub #1914: Re-attach Tracing to UIA Tree // tracing /*apiMsg.MovedCount = *pRetVal; @@ -736,7 +718,8 @@ IFACEMETHODIMP UiaTextRangeBase::MoveEndpointByUnit(_In_ TextPatternRangeEndpoin IFACEMETHODIMP UiaTextRangeBase::MoveEndpointByRange(_In_ TextPatternRangeEndpoint endpoint, _In_ ITextRangeProvider* pTargetRange, - _In_ TextPatternRangeEndpoint targetEndpoint) + _In_ TextPatternRangeEndpoint targetEndpoint) noexcept +try { _pData->LockConsole(); auto Unlock = wil::scope_exit([&]() noexcept { @@ -766,104 +749,41 @@ IFACEMETHODIMP UiaTextRangeBase::MoveEndpointByRange(_In_ TextPatternRangeEndpoi std::wstring data = ss.str(); OutputDebugString(data.c_str()); OutputDebugString(L"\n"); - _outputRowConversions(); #endif - // get the value that we're updating to - Endpoint targetEndpointValue = 0; - if (targetEndpoint == TextPatternRangeEndpoint::TextPatternRangeEndpoint_Start) - { - targetEndpointValue = range->GetStart(); - - // If we're moving our end relative to their start, we actually have to back up one from - // their start position because this operation treats it as exclusive. - if (endpoint == TextPatternRangeEndpoint::TextPatternRangeEndpoint_End) - { - if (targetEndpointValue > 0) - { - targetEndpointValue--; - } - } - } - else - { - targetEndpointValue = range->GetEnd(); - - // If we're moving our start relative to their end, we actually have to sit one after - // their end position as it was stored inclusive and we're doing this as an exclusive operation. - if (endpoint == TextPatternRangeEndpoint::TextPatternRangeEndpoint_Start) - { - targetEndpointValue++; - } - } - - try - { - // convert then endpoints to screen info rows/columns - const auto startScreenInfoRow = _endpointToScreenInfoRow(_pData, _start); - const auto startColumn = _endpointToColumn(_pData, _start); - const auto endScreenInfoRow = _endpointToScreenInfoRow(_pData, _end); - const auto endColumn = _endpointToColumn(_pData, _end); - const auto targetScreenInfoRow = _endpointToScreenInfoRow(_pData, targetEndpointValue); - const auto targetColumn = _endpointToColumn(_pData, targetEndpointValue); - - // set endpoint value and check for crossed endpoints - bool crossedEndpoints = false; - if (endpoint == TextPatternRangeEndpoint::TextPatternRangeEndpoint_Start) - { - _start = targetEndpointValue; - if (_compareScreenCoords(_pData, endScreenInfoRow, endColumn, targetScreenInfoRow, targetColumn) == -1) - { - // endpoints were crossed - _end = _start; - crossedEndpoints = true; - } - } - else - { - _end = targetEndpointValue; - if (_compareScreenCoords(_pData, startScreenInfoRow, startColumn, targetScreenInfoRow, targetColumn) == 1) - { - // endpoints were crossed - _start = _end; - crossedEndpoints = true; - } - } - _degenerate = crossedEndpoints; - } - CATCH_RETURN(); + SetEndpoint(endpoint, range->GetEndpoint(targetEndpoint)); // TODO GitHub #1914: Re-attach Tracing to UIA Tree //Tracing::s_TraceUia(this, ApiCall::MoveEndpointByRange, &apiMsg); return S_OK; } +CATCH_RETURN(); -IFACEMETHODIMP UiaTextRangeBase::Select() +IFACEMETHODIMP UiaTextRangeBase::Select() noexcept +try { _pData->LockConsole(); auto Unlock = wil::scope_exit([&]() noexcept { _pData->UnlockConsole(); }); - if (_degenerate) + if (IsDegenerate()) { // calling Select on a degenerate range should clear any current selections _pData->ClearSelection(); } else { - const COORD coordStart{ gsl::narrow(_endpointToColumn(_pData, _start)), - gsl::narrow(_endpointToScreenInfoRow(_pData, _start)) }; - const COORD coordEnd{ gsl::narrow(_endpointToColumn(_pData, _end)), - gsl::narrow(_endpointToScreenInfoRow(_pData, _end)) }; - - _pData->SelectNewRegion(coordStart, coordEnd); + auto temp = _end; + _pData->GetTextBuffer().GetSize().DecrementInBounds(temp); + _pData->SelectNewRegion(_start, temp); } // TODO GitHub #1914: Re-attach Tracing to UIA Tree //Tracing::s_TraceUia(this, ApiCall::Select, nullptr); return S_OK; } +CATCH_RETURN(); // we don't support this IFACEMETHODIMP UiaTextRangeBase::AddToSelection() noexcept @@ -881,79 +801,78 @@ IFACEMETHODIMP UiaTextRangeBase::RemoveFromSelection() noexcept return E_NOTIMPL; } -IFACEMETHODIMP UiaTextRangeBase::ScrollIntoView(_In_ BOOL alignToTop) +IFACEMETHODIMP UiaTextRangeBase::ScrollIntoView(_In_ BOOL alignToTop) noexcept +try { _pData->LockConsole(); auto Unlock = wil::scope_exit([&]() noexcept { _pData->UnlockConsole(); }); - try + const auto oldViewport = _pData->GetViewport().ToInclusive(); + const auto viewportHeight = _getViewportHeight(oldViewport); + // range rows + const base::ClampedNumeric startScreenInfoRow = _start.Y; + const base::ClampedNumeric endScreenInfoRow = _end.Y; + // screen buffer rows + const base::ClampedNumeric topRow = 0; + const base::ClampedNumeric bottomRow = _pData->GetTextBuffer().TotalRowCount() - 1; + + SMALL_RECT newViewport = oldViewport; + + // there's a bunch of +1/-1s here for setting the viewport. These + // are to account for the inclusivity of the viewport boundaries. + if (alignToTop) { - const auto oldViewport = _pData->GetViewport().ToInclusive(); - const auto viewportHeight = _getViewportHeight(oldViewport); - // range rows - const auto startScreenInfoRow = _endpointToScreenInfoRow(_pData, _start); - const auto endScreenInfoRow = _endpointToScreenInfoRow(_pData, _end); - // screen buffer rows - const auto topRow = _getFirstScreenInfoRowIndex(); - const auto bottomRow = _getLastScreenInfoRowIndex(_pData); - - SMALL_RECT newViewport = oldViewport; - - // there's a bunch of +1/-1s here for setting the viewport. These - // are to account for the inclusivity of the viewport boundaries. - if (alignToTop) + // determine if we can align the start row to the top + if (startScreenInfoRow + viewportHeight <= bottomRow) { - // determine if we can align the start row to the top - if (startScreenInfoRow + viewportHeight <= bottomRow) - { - // we can align to the top - newViewport.Top = gsl::narrow(startScreenInfoRow); - newViewport.Bottom = gsl::narrow(startScreenInfoRow + viewportHeight - 1); - } - else - { - // we can align to the top so we'll just move the viewport - // to the bottom of the screen buffer - newViewport.Bottom = gsl::narrow(bottomRow); - newViewport.Top = gsl::narrow(bottomRow - viewportHeight + 1); - } + // we can align to the top + newViewport.Top = startScreenInfoRow; + newViewport.Bottom = startScreenInfoRow + viewportHeight - 1; } else { - // we need to align to the bottom - // check if we can align to the bottom - if (endScreenInfoRow >= viewportHeight) - { - // we can align to bottom - newViewport.Bottom = gsl::narrow(endScreenInfoRow); - newViewport.Top = gsl::narrow(endScreenInfoRow - viewportHeight + 1); - } - else - { - // we can't align to bottom so we'll move the viewport to - // the top of the screen buffer - newViewport.Top = gsl::narrow(topRow); - newViewport.Bottom = gsl::narrow(topRow + viewportHeight - 1); - } + // we can align to the top so we'll just move the viewport + // to the bottom of the screen buffer + newViewport.Bottom = bottomRow; + newViewport.Top = bottomRow - viewportHeight + 1; + } + } + else + { + // we need to align to the bottom + // check if we can align to the bottom + if (static_cast(endScreenInfoRow) >= viewportHeight) + { + // we can align to bottom + newViewport.Bottom = endScreenInfoRow; + newViewport.Top = endScreenInfoRow - viewportHeight + 1; } + else + { + // we can't align to bottom so we'll move the viewport to + // the top of the screen buffer + newViewport.Top = topRow; + newViewport.Bottom = topRow + viewportHeight - 1; + } + } - FAIL_FAST_IF(!(newViewport.Top >= gsl::narrow(topRow))); - FAIL_FAST_IF(!(newViewport.Bottom <= gsl::narrow(bottomRow))); - FAIL_FAST_IF(!(_getViewportHeight(oldViewport) == _getViewportHeight(newViewport))); + FAIL_FAST_IF(!(newViewport.Top >= topRow)); + FAIL_FAST_IF(!(newViewport.Bottom <= bottomRow)); + FAIL_FAST_IF(!(_getViewportHeight(oldViewport) == _getViewportHeight(newViewport))); - _ChangeViewport(newViewport); + _ChangeViewport(newViewport); - // TODO GitHub #1914: Re-attach Tracing to UIA Tree - // tracing - /*ApiMsgScrollIntoView apiMsg; + // TODO GitHub #1914: Re-attach Tracing to UIA Tree + // tracing + /*ApiMsgScrollIntoView apiMsg; apiMsg.AlignToTop = !!alignToTop; Tracing::s_TraceUia(this, ApiCall::ScrollIntoView, &apiMsg);*/ - } - CATCH_RETURN(); + return S_OK; } +CATCH_RETURN(); IFACEMETHODIMP UiaTextRangeBase::GetChildren(_Outptr_result_maybenull_ SAFEARRAY** ppRetVal) noexcept { @@ -973,11 +892,6 @@ IFACEMETHODIMP UiaTextRangeBase::GetChildren(_Outptr_result_maybenull_ SAFEARRAY #pragma endregion -const COORD UiaTextRangeBase::_getScreenBufferCoords(gsl::not_null pData) -{ - return pData->GetTextBuffer().GetSize().Dimensions(); -} - const COORD UiaTextRangeBase::_getScreenFontSize() const { COORD coordRet = _pData->GetFontInfo().GetSize(); @@ -990,1534 +904,359 @@ const COORD UiaTextRangeBase::_getScreenFontSize() const } // Routine Description: -// - Gets the number of rows in the output text buffer. +// - Gets the viewport height, measured in char rows. // Arguments: -// - pData - the UiaData for the terminal we are operating on +// - viewport - The viewport to measure // Return Value: -// - The number of rows -const unsigned int UiaTextRangeBase::_getTotalRows(gsl::not_null pData) noexcept +// - The viewport height +const unsigned int UiaTextRangeBase::_getViewportHeight(const SMALL_RECT viewport) const noexcept { - return pData->GetTextBuffer().TotalRowCount(); + FAIL_FAST_IF(!(viewport.Bottom >= viewport.Top)); + // + 1 because COORD is inclusive on both sides so subtracting top + // and bottom gets rid of 1 more then it should. + return viewport.Bottom - viewport.Top + 1; } // Routine Description: -// - Gets the width of the screen buffer rows +// - adds the relevant coordinate points from the row to coords. +// - it is assumed that startAnchor and endAnchor are within the same row +// and NOT DEGENERATE // Arguments: -// - pData - the UiaData for the terminal we are operating on +// - startAnchor - the start anchor of interested data within the viewport. In text buffer coordinate space. Inclusive. +// - endAnchor - the end anchor of interested data within the viewport. In text buffer coordinate space. Inclusive +// - coords - vector to add the calculated coords to // Return Value: -// - The row width -const unsigned int UiaTextRangeBase::_getRowWidth(gsl::not_null pData) +// - +void UiaTextRangeBase::_getBoundingRect(_In_ const COORD startAnchor, _In_ const COORD endAnchor, _Inout_ std::vector& coords) const { - // make sure that we can't leak a 0 - return std::max(static_cast(_getScreenBufferCoords(pData).X), 1u); -} + FAIL_FAST_IF(startAnchor.Y != endAnchor.Y); + FAIL_FAST_IF(startAnchor.X <= endAnchor.X); -// Routine Description: -// - calculates the column refered to by the endpoint. -// Arguments: -// - pData - the UiaData for the terminal we are operating on -// - endpoint - the endpoint to translate -// Return Value: -// - the column value -const Column UiaTextRangeBase::_endpointToColumn(gsl::not_null pData, const Endpoint endpoint) -{ - return endpoint % _getRowWidth(pData); -} + const auto viewport = _pData->GetViewport(); + const auto currentFontSize = _getScreenFontSize(); -// Routine Description: -// - converts an Endpoint into its equivalent text buffer row. -// Arguments: -// - pData - the UiaData for the terminal we are operating on -// - endpoint - the endpoint to convert -// Return Value: -// - the text buffer row value -const TextBufferRow UiaTextRangeBase::_endpointToTextBufferRow(gsl::not_null pData, - const Endpoint endpoint) -{ - return endpoint / _getRowWidth(pData); + POINT topLeft{ 0 }; + POINT bottomRight{ 0 }; + + // startAnchor is converted to the viewport coordinate space +#pragma warning(suppress : 26496) // analysis can't see this, TODO GH: 4015 to improve Viewport to be less bad because it'd go away if ConvertToOrigin returned instead of inout'd. + auto startCoord = startAnchor; + viewport.ConvertToOrigin(&startCoord); + + // we want to clamp to a long (output type), not a short (input type) + // so we need to explicitly say + topLeft.x = base::ClampMul(startCoord.X, currentFontSize.X); + topLeft.y = base::ClampMul(startCoord.Y, currentFontSize.Y); + + // endAnchor is converted to the viewport coordinate space +#pragma warning(suppress : 26496) // analysis can't see this, TODO GH: 4015 to improve Viewport to be less bad because it'd go away if ConvertToOrigin returned instead of inout'd. + auto endCoord = endAnchor; + viewport.ConvertToOrigin(&endCoord); + bottomRight.x = base::ClampMul(endCoord.X, currentFontSize.X); + bottomRight.y = base::ClampMul(base::ClampAdd(endCoord.Y, 1), currentFontSize.Y); + + // convert the coords to be relative to the screen instead of + // the client window + _TranslatePointToScreen(&topLeft); + _TranslatePointToScreen(&bottomRight); + + const long width = base::ClampSub(bottomRight.x, topLeft.x); + const long height = base::ClampSub(bottomRight.y, topLeft.y); + + // insert the coords + coords.push_back(topLeft.x); + coords.push_back(topLeft.y); + coords.push_back(width); + coords.push_back(height); } // Routine Description: -// - counts the number of rows that are fully or partially part of the -// range. +// - moves the UTR's endpoint by moveCount times by character. +// - if endpoints crossed, the degenerate range is created and both endpoints are moved // Arguments: -// - pData - the UiaData for the terminal we are operating on +// - moveCount - the number of times to move +// - endpoint - the endpoint to move +// - pAmountMoved - the number of times that the return values are "moved" +// - preventBufferEnd - when enabled, prevent endpoint from being at the end of the buffer +// This is used for general movement, where you are not allowed to +// create a degenerate range // Return Value: -// - The number of rows in the range. -const unsigned int UiaTextRangeBase::_rowCountInRange(gsl::not_null pData) const +// - +void UiaTextRangeBase::_moveEndpointByUnitCharacter(_In_ const int moveCount, + _In_ const TextPatternRangeEndpoint endpoint, + _Out_ gsl::not_null const pAmountMoved, + _In_ const bool preventBufferEnd) { - if (_degenerate) + *pAmountMoved = 0; + + if (moveCount == 0) { - return 0; + return; } - const ScreenInfoRow startScreenInfoRow = _endpointToScreenInfoRow(pData, _start); - const Column startColumn = _endpointToColumn(pData, _start); - const ScreenInfoRow endScreenInfoRow = _endpointToScreenInfoRow(pData, _end); - const Column endColumn = _endpointToColumn(pData, _end); + const bool allowBottomExclusive = !preventBufferEnd; + const MovementDirection moveDirection = (moveCount > 0) ? MovementDirection::Forward : MovementDirection::Backward; + const auto bufferSize = _pData->GetTextBuffer().GetSize(); - FAIL_FAST_IF(!(_compareScreenCoords(pData, startScreenInfoRow, startColumn, endScreenInfoRow, endColumn) <= 0)); + bool success = true; + auto target = GetEndpoint(endpoint); + while (abs(*pAmountMoved) < abs(moveCount) && success) + { + switch (moveDirection) + { + case MovementDirection::Forward: + success = bufferSize.IncrementInBounds(target, allowBottomExclusive); + if (success) + { + (*pAmountMoved)++; + } + break; + case MovementDirection::Backward: + success = bufferSize.DecrementInBounds(target, allowBottomExclusive); + if (success) + { + (*pAmountMoved)--; + } + break; + default: + break; + } + } - // + 1 to balance subtracting ScreenInfoRows from each other - return endScreenInfoRow - startScreenInfoRow + 1; + SetEndpoint(endpoint, target); } // Routine Description: -// - Converts a TextBufferRow to a ScreenInfoRow. +// - moves the UTR's endpoint by moveCount times by word. +// - if endpoints crossed, the degenerate range is created and both endpoints are moved // Arguments: -// - pData - the UiaData for the terminal we are operating on -// - row - the TextBufferRow to convert -// Return Value: -// - the equivalent ScreenInfoRow. -const ScreenInfoRow UiaTextRangeBase::_textBufferRowToScreenInfoRow(gsl::not_null pData, - const TextBufferRow row) noexcept -{ - const int firstRowIndex = pData->GetTextBuffer().GetFirstRowIndex(); - return _normalizeRow(pData, row - firstRowIndex); -} - -// Routine Description: -// - Converts a ScreenInfoRow to a ViewportRow. Uses the default -// viewport for the conversion. -// Arguments: -// - pData - the UiaData for the terminal we are operating on -// - row - the ScreenInfoRow to convert -// Return Value: -// - the equivalent ViewportRow. -const ViewportRow UiaTextRangeBase::_screenInfoRowToViewportRow(gsl::not_null pData, const ScreenInfoRow row) noexcept -{ - const SMALL_RECT viewport = pData->GetViewport().ToInclusive(); - return _screenInfoRowToViewportRow(row, viewport); -} - -// Routine Description: -// - normalizes the row index to within the bounds of the output -// buffer. The output buffer stores the text in a circular buffer so -// this method makes sure that we circle around gracefully. -// Arguments: -// - pData - the UiaData for the terminal we are operating on -// - row - the non-normalized row index -// Return Value: -// - the normalized row index -const Row UiaTextRangeBase::_normalizeRow(gsl::not_null pData, const Row row) noexcept -{ - const unsigned int totalRows = _getTotalRows(pData); - return ((row + totalRows) % totalRows); -} - -// Routine Description: -// - Gets the viewport height, measured in char rows. -// Arguments: -// - viewport - The viewport to measure -// Return Value: -// - The viewport height -const unsigned int UiaTextRangeBase::_getViewportHeight(const SMALL_RECT viewport) noexcept -{ - FAIL_FAST_IF(!(viewport.Bottom >= viewport.Top)); - // + 1 because COORD is inclusive on both sides so subtracting top - // and bottom gets rid of 1 more then it should. - return viewport.Bottom - viewport.Top + 1; -} - -// Routine Description: -// - Gets the viewport width, measured in char columns. -// Arguments: -// - viewport - The viewport to measure -// Return Value: -// - The viewport width -const unsigned int UiaTextRangeBase::_getViewportWidth(const SMALL_RECT viewport) noexcept -{ - FAIL_FAST_IF(!(viewport.Right >= viewport.Left)); - - // + 1 because COORD is inclusive on both sides so subtracting left - // and right gets rid of 1 more then it should. - return (viewport.Right - viewport.Left + 1); -} - -// Routine Description: -// - checks if the row is currently visible in the viewport. Uses the -// default viewport. -// Arguments: -// - pData - the UiaData for the terminal we are operating on -// - row - the screen info row to check -// Return Value: -// - true if the row is within the bounds of the viewport -const bool UiaTextRangeBase::_isScreenInfoRowInViewport(gsl::not_null pData, - const ScreenInfoRow row) noexcept -{ - return _isScreenInfoRowInViewport(row, pData->GetViewport().ToInclusive()); -} - -// Routine Description: -// - checks if the row is currently visible in the viewport -// Arguments: -// - row - the row to check -// - viewport - the viewport to use for the bounds -// Return Value: -// - true if the row is within the bounds of the viewport -const bool UiaTextRangeBase::_isScreenInfoRowInViewport(const ScreenInfoRow row, - const SMALL_RECT viewport) noexcept -{ - const ViewportRow viewportRow = _screenInfoRowToViewportRow(row, viewport); - return viewportRow >= 0 && - viewportRow < gsl::narrow(_getViewportHeight(viewport)); -} - -// Routine Description: -// - Converts a ScreenInfoRow to a TextBufferRow. -// Arguments: -// - pData - the UiaData for the terminal we are operating on -// - row - the ScreenInfoRow to convert -// Return Value: -// - the equivalent TextBufferRow. -const TextBufferRow UiaTextRangeBase::_screenInfoRowToTextBufferRow(gsl::not_null pData, - const ScreenInfoRow row) noexcept -{ - const TextBufferRow firstRowIndex = pData->GetTextBuffer().GetFirstRowIndex(); - return _normalizeRow(pData, row + firstRowIndex); -} - -// Routine Description: -// - Converts a TextBufferRow to an Endpoint. -// Arguments: -// - pData - the UiaData for the terminal we are operating on -// - row - the TextBufferRow to convert -// Return Value: -// - the equivalent Endpoint, starting at the beginning of the TextBufferRow. -const Endpoint UiaTextRangeBase::_textBufferRowToEndpoint(gsl::not_null pData, const TextBufferRow row) -{ - return _getRowWidth(pData) * row; -} - -// Routine Description: -// - Finds the beginning of the word (Endpoint) for the current target (Endpoint). -// Arguments: -// - pData - the UiaData for the terminal we are operating on -// - target - the Endpoint that is on the word we want to expand to -// - wordDelimiters - characters to determine the separation between words -// Return Value: -// - the equivalent Endpoint, starting at the beginning of the word where _start is located. -const Endpoint UiaTextRangeBase::_wordBeginEndpoint(gsl::not_null pData, Endpoint target, const std::wstring_view wordDelimiters) -{ - auto coord = _endpointToCoord(pData, target); - coord = pData->GetTextBuffer().GetWordStart(coord, wordDelimiters); - return _coordToEndpoint(pData, coord); -} - -// Routine Description: -// - Finds the end of the word (Endpoint) for the current target (Endpoint). -// - The "end of the word" is defined to include the following: -// any word break characters that are present at the end of the "word", but before the start of the next word. -// Arguments: -// - pData - the UiaData for the terminal we are operating on -// - target - the Endpoint that is on the word we want to expand to -// - wordDelimiters - characters to determine the separation between words -// Return Value: -// - the equivalent Endpoint, starting at the end of the word where _start is located. -const Endpoint UiaTextRangeBase::_wordEndEndpoint(gsl::not_null pData, Endpoint target, const std::wstring_view wordDelimiters) -{ - auto coord = _endpointToCoord(pData, target); - coord = pData->GetTextBuffer().GetWordEnd(coord, wordDelimiters, true); - return _coordToEndpoint(pData, coord); -} - -// Routine Description: -// - Converts a ScreenInfoRow to an Endpoint. -// Arguments: -// - pData - the UiaData for the terminal we are operating on -// - row - the ScreenInfoRow to convert -// Return Value: -// - the equivalent Endpoint. -const Endpoint UiaTextRangeBase::_screenInfoRowToEndpoint(gsl::not_null pData, - const ScreenInfoRow row) -{ - return _textBufferRowToEndpoint(pData, _screenInfoRowToTextBufferRow(pData, row)); -} - -// Routine Description: -// - Converts an Endpoint to an ScreenInfoRow. -// Arguments: -// - pData - the UiaData for the terminal we are operating on -// - endpoint - the endpoint to convert -// Return Value: -// - the equivalent ScreenInfoRow. -const ScreenInfoRow UiaTextRangeBase::_endpointToScreenInfoRow(gsl::not_null pData, - const Endpoint endpoint) -{ - return _textBufferRowToScreenInfoRow(pData, _endpointToTextBufferRow(pData, endpoint)); -} - -// Routine Description: -// - adds the relevant coordinate points from screenInfoRow to coords. -// Arguments: -// - pData - the UiaData for the terminal we are operating on -// - screenInfoRow - row to calculate coordinate positions from -// - coords - vector to add the calucated coords to -// Return Value: -// - -// Notes: -// - alters coords. may throw an exception. -void UiaTextRangeBase::_addScreenInfoRowBoundaries(gsl::not_null pData, - const ScreenInfoRow screenInfoRow, - _Inout_ std::vector& coords) const -{ - const COORD currentFontSize = _getScreenFontSize(); - - POINT topLeft{ 0 }; - POINT bottomRight{ 0 }; - - if (_endpointToScreenInfoRow(pData, _start) == screenInfoRow) - { - // start is somewhere in this row so we start from its position - topLeft.x = _endpointToColumn(pData, _start) * currentFontSize.X; - } - else - { - // otherwise we start from the beginning of the row - topLeft.x = 0; - } - - topLeft.y = _screenInfoRowToViewportRow(pData, screenInfoRow) * currentFontSize.Y; - - if (_endpointToScreenInfoRow(pData, _end) == screenInfoRow) - { - // the endpoints are on the same row - bottomRight.x = (_endpointToColumn(pData, _end) + 1) * currentFontSize.X; - } - else - { - // _end is not on this row so span to the end of the row - bottomRight.x = _getViewportWidth(_pData->GetViewport().ToInclusive()) * currentFontSize.X; - } - - // we add the font height only once here because we are adding each line individually - bottomRight.y = topLeft.y + currentFontSize.Y; - - // convert the coords to be relative to the screen instead of - // the client window - _TranslatePointToScreen(&topLeft); - _TranslatePointToScreen(&bottomRight); - - const LONG width = bottomRight.x - topLeft.x; - const LONG height = bottomRight.y - topLeft.y; - - // insert the coords - coords.push_back(topLeft.x); - coords.push_back(topLeft.y); - coords.push_back(width); - coords.push_back(height); -} - -// Routine Description: -// - returns the index of the first row of the screen info -// Arguments: -// - -// Return Value: -// - the index of the first row (0-indexed) of the screen info -const unsigned int UiaTextRangeBase::_getFirstScreenInfoRowIndex() noexcept -{ - return 0; -} - -// Routine Description: -// - returns the index of the last row of the screen info -// Arguments: -// - pData - the UiaData for the terminal we are operating on -// Return Value: -// - the index of the last row (0-indexed) of the screen info -const unsigned int UiaTextRangeBase::_getLastScreenInfoRowIndex(gsl::not_null pData) noexcept -{ - return _getTotalRows(pData) - 1; -} - -// Routine Description: -// - returns the index of the first column of the screen info rows -// Arguments: -// - -// Return Value: -// - the index of the first column (0-indexed) of the screen info rows -const Column UiaTextRangeBase::_getFirstColumnIndex() noexcept -{ - return 0; -} - -// Routine Description: -// - returns the index of the last column of the screen info rows -// Arguments: -// - pData - the UiaData for the terminal we are operating on -// Return Value: -// - the index of the last column (0-indexed) of the screen info rows -const Column UiaTextRangeBase::_getLastColumnIndex(gsl::not_null pData) -{ - return _getRowWidth(pData) - 1; -} - -// Routine Description: -// - Compares two sets of screen info coordinates -// Arguments: -// - pData - the UiaData for the terminal we are operating on -// - rowA - the row index of the first position -// - colA - the column index of the first position -// - rowB - the row index of the second position -// - colB - the column index of the second position -// Return Value: -// -1 if A < B -// 1 if A > B -// 0 if A == B -const int UiaTextRangeBase::_compareScreenCoords(gsl::not_null pData, - const ScreenInfoRow rowA, - const Column colA, - const ScreenInfoRow rowB, - const Column colB) -{ - FAIL_FAST_IF(!(rowA >= _getFirstScreenInfoRowIndex())); - FAIL_FAST_IF(!(rowA <= _getLastScreenInfoRowIndex(pData))); - - FAIL_FAST_IF(!(colA >= _getFirstColumnIndex())); - FAIL_FAST_IF(!(colA <= _getLastColumnIndex(pData))); - - FAIL_FAST_IF(!(rowB >= _getFirstScreenInfoRowIndex())); - FAIL_FAST_IF(!(rowB <= _getLastScreenInfoRowIndex(pData))); - - FAIL_FAST_IF(!(colB >= _getFirstColumnIndex())); - FAIL_FAST_IF(!(colB <= _getLastColumnIndex(pData))); - - if (rowA < rowB) - { - return -1; - } - else if (rowA > rowB) - { - return 1; - } - // rowA == rowB - else if (colA < colB) - { - return -1; - } - else if (colA > colB) - { - return 1; - } - // colA == colB - return 0; -} - -// Routine Description: -// - calculates new Endpoints if they were to be moved moveCount times -// by character. -// Arguments: -// - pData - the UiaData for the terminal we are operating on -// - moveCount - the number of times to move -// - moveState - values indicating the state of the console for the -// move operation -// - pAmountMoved - the number of times that the return values are "moved" -// Return Value: -// - a pair of endpoints of the form -std::pair UiaTextRangeBase::_moveByCharacter(gsl::not_null pData, - const int moveCount, - const MoveState moveState, - _Out_ gsl::not_null const pAmountMoved) -{ - if (moveState.Direction == MovementDirection::Forward) - { - return _moveByCharacterForward(pData, moveCount, moveState, pAmountMoved); - } - else - { - return _moveByCharacterBackward(pData, moveCount, moveState, pAmountMoved); - } -} - -std::pair UiaTextRangeBase::_moveByCharacterForward(gsl::not_null pData, - const int moveCount, - const MoveState moveState, - _Out_ gsl::not_null const pAmountMoved) -{ - *pAmountMoved = 0; - ScreenInfoRow currentScreenInfoRow = moveState.StartScreenInfoRow; - Column currentColumn = moveState.StartColumn; - - for (int i = 0; i < abs(moveCount); ++i) - { - // get the current row's right - const ROW& row = pData->GetTextBuffer().GetRowByOffset(currentScreenInfoRow); - const size_t right = row.GetCharRow().MeasureRight(); - const auto expectedColumn = gsl::narrow_cast(currentColumn) + 1; - - // check if we're at the edge of the screen info buffer - if (currentScreenInfoRow == moveState.LimitingRow && - expectedColumn >= right) - { - break; - } - else if (expectedColumn >= right) - { - // we're at the edge of a row and need to go to the next one - currentColumn = moveState.FirstColumnInRow; - currentScreenInfoRow += static_cast(moveState.Increment); - } - else - { - // moving somewhere away from the edges of a row - currentColumn += static_cast(moveState.Increment); - } - *pAmountMoved += static_cast(moveState.Increment); - - FAIL_FAST_IF(!(currentColumn >= _getFirstColumnIndex())); - FAIL_FAST_IF(!(currentColumn <= _getLastColumnIndex(pData))); - FAIL_FAST_IF(!(currentScreenInfoRow >= _getFirstScreenInfoRowIndex())); - FAIL_FAST_IF(!(currentScreenInfoRow <= _getLastScreenInfoRowIndex(pData))); - } - - Endpoint start = _screenInfoRowToEndpoint(pData, currentScreenInfoRow) + currentColumn; - Endpoint end = start; - return std::make_pair(std::move(start), std::move(end)); -} - -std::pair UiaTextRangeBase::_moveByCharacterBackward(gsl::not_null pData, - const int moveCount, - const MoveState moveState, - _Out_ gsl::not_null const pAmountMoved) -{ - *pAmountMoved = 0; - ScreenInfoRow currentScreenInfoRow = moveState.StartScreenInfoRow; - Column currentColumn = moveState.StartColumn; - - for (int i = 0; i < abs(moveCount); ++i) - { - // check if we're at the edge of the screen info buffer - if (currentScreenInfoRow == moveState.LimitingRow && - currentColumn == moveState.LastColumnInRow) - { - break; - } - else if (currentColumn == moveState.LastColumnInRow) - { - // we're at the edge of a row and need to go to the - // next one. move to the cell with the last non-whitespace charactor - - currentScreenInfoRow += static_cast(moveState.Increment); - // get the right cell for the next row - const ROW& row = pData->GetTextBuffer().GetRowByOffset(currentScreenInfoRow); - const size_t right = row.GetCharRow().MeasureRight(); - currentColumn = gsl::narrow((right == 0) ? 0 : right - 1); - } - else - { - // moving somewhere away from the edges of a row - currentColumn += static_cast(moveState.Increment); - } - *pAmountMoved += static_cast(moveState.Increment); - - FAIL_FAST_IF(!(currentColumn >= _getFirstColumnIndex())); - FAIL_FAST_IF(!(currentColumn <= _getLastColumnIndex(pData))); - FAIL_FAST_IF(!(currentScreenInfoRow >= _getFirstScreenInfoRowIndex())); - FAIL_FAST_IF(!(currentScreenInfoRow <= _getLastScreenInfoRowIndex(pData))); - } - - Endpoint start = _screenInfoRowToEndpoint(pData, currentScreenInfoRow) + currentColumn; - Endpoint end = start; - return std::make_pair(std::move(start), std::move(end)); -} - -// Routine Description: -// - calculates new Endpoints if they were to be moved moveCount times -// by word. -// Arguments: -// - pData - the UiaData for the terminal we are operating on -// - moveCount - the number of times to move -// - moveState - values indicating the state of the console for the -// move operation -// - wordDelimiters - the list of characters that define a separation of different words -// - pAmountMoved - the number of times that the return values are "moved" -// Return Value: -// - a pair of endpoints of the form -std::pair UiaTextRangeBase::_moveByWord(gsl::not_null pData, - const int moveCount, - const MoveState moveState, - const std::wstring_view wordDelimiters, - _Out_ gsl::not_null const pAmountMoved) -{ - if (moveState.Direction == MovementDirection::Forward) - { - return _moveByWordForward(pData, moveCount, moveState, wordDelimiters, pAmountMoved); - } - else - { - return _moveByWordBackward(pData, moveCount, moveState, wordDelimiters, pAmountMoved); - } -} - -// Routine Description: -// - calculates new Endpoints if they were to be moved moveCount times in the forward direction -// by word. -// Arguments: -// - pData - the UiaData for the terminal we are operating on -// - moveCount - the number of times to move -// - moveState - values indicating the state of the console for the -// move operation -// - wordDelimiters - the list of characters that define a separation of different words -// - pAmountMoved - the number of times that the return values are "moved" -// Return Value: -// - a pair of endpoints of the form -std::pair UiaTextRangeBase::_moveByWordForward(gsl::not_null pData, - const int moveCount, - const MoveState moveState, - const std::wstring_view wordDelimiters, - _Out_ gsl::not_null const pAmountMoved) -{ - // STRATEGY: - // - move the "end" Endpoint to the proper place (if need to move multiple times, do it here) - // - find the "start" Endpoint based on that - - *pAmountMoved = 0; - ScreenInfoRow currentScreenInfoRow = moveState.EndScreenInfoRow; - Column currentColumn = moveState.EndColumn; - - auto& buffer = pData->GetTextBuffer(); - for (int i = 0; i < abs(moveCount); ++i) - { - // get the current row's right - const ROW& row = buffer.GetRowByOffset(currentScreenInfoRow); - const size_t right = row.GetCharRow().MeasureRight(); - const auto expectedColumn = gsl::narrow_cast(currentColumn) + 1; - - // check if we're at the edge of the screen info buffer - if (currentScreenInfoRow == moveState.LimitingRow && - expectedColumn >= right) - { - break; - } - else if (expectedColumn >= right) - { - // we're at the edge of a row and need to go to the next one - currentColumn = moveState.FirstColumnInRow; - currentScreenInfoRow += static_cast(moveState.Increment); - - const auto point = _screenInfoRowToEndpoint(pData, currentScreenInfoRow) + currentColumn; - auto target = _endpointToCoord(pData, point); - - target = buffer.GetWordEnd(target, wordDelimiters, true); - - currentColumn = target.X; - } - else - { - // moving somewhere away from the edges of a row - const Endpoint point = _screenInfoRowToEndpoint(pData, currentScreenInfoRow) + currentColumn; - auto target = _endpointToCoord(pData, point); - buffer.GetSize().IncrementInBounds(target); - - target = (moveState.Increment == MovementIncrement::Forward) ? - buffer.GetWordEnd(target, wordDelimiters, true) : - buffer.GetWordStart(target, wordDelimiters, true); - - currentColumn = target.X; - } - *pAmountMoved += static_cast(moveState.Increment); - - FAIL_FAST_IF(!(currentColumn >= _getFirstColumnIndex())); - FAIL_FAST_IF(!(currentColumn <= _getLastColumnIndex(pData))); - FAIL_FAST_IF(!(currentScreenInfoRow >= _getFirstScreenInfoRowIndex())); - FAIL_FAST_IF(!(currentScreenInfoRow <= _getLastScreenInfoRowIndex(pData))); - } - - Endpoint end = _screenInfoRowToEndpoint(pData, currentScreenInfoRow) + currentColumn; - - auto target = _endpointToCoord(pData, end); - target = buffer.GetWordStart(target, wordDelimiters, true); - Endpoint start = _coordToEndpoint(pData, target); - - return std::make_pair(std::move(start), std::move(end)); -} - -// Routine Description: -// - calculates new Endpoints if they were to be moved moveCount times in the backwards direction -// by word. -// Arguments: -// - pData - the UiaData for the terminal we are operating on -// - moveCount - the number of times to move -// - moveState - values indicating the state of the console for the -// move operation -// - wordDelimiters - the list of characters that define a separation of different words -// - pAmountMoved - the number of times that the return values are "moved" -// Return Value: -// - a pair of endpoints of the form -std::pair UiaTextRangeBase::_moveByWordBackward(gsl::not_null pData, - const int moveCount, - const MoveState moveState, - const std::wstring_view wordDelimiters, - _Out_ gsl::not_null const pAmountMoved) -{ - // STRATEGY: - // - move the "start" Endpoint to the proper place (if need to move multiple times, do it here) - // - find the "end" Endpoint based on that - - *pAmountMoved = 0; - ScreenInfoRow currentScreenInfoRow = moveState.StartScreenInfoRow; - Column currentColumn = moveState.StartColumn; - - auto& buffer = pData->GetTextBuffer(); - for (int i = 0; i < abs(moveCount); ++i) - { - // check if we're at the edge of the screen info buffer - if (currentScreenInfoRow == moveState.LimitingRow && - currentColumn == moveState.LastColumnInRow) - { - break; - } - else if (currentColumn == moveState.LastColumnInRow) - { - // we're at the edge of a row and need to go to the - // previous one. move to the cell with the last non-whitespace charactor - - currentScreenInfoRow += static_cast(moveState.Increment); - - // get the right-most char for the previous row - const ROW& row = buffer.GetRowByOffset(currentScreenInfoRow); - const size_t right = row.GetCharRow().MeasureRight(); - currentColumn = gsl::narrow((right == 0) ? 0 : right - 1); - - // get the right-most word for the previous row - const auto point = _screenInfoRowToEndpoint(pData, currentScreenInfoRow) + currentColumn; - auto target = _endpointToCoord(pData, point); - - target = buffer.GetWordStart(target, wordDelimiters, true); - buffer.GetSize().IncrementInBounds(target); - - currentColumn = target.X; - } - else - { - // moving somewhere away from the edges of a row - const Endpoint point = _screenInfoRowToEndpoint(pData, currentScreenInfoRow) + currentColumn; - auto target = _endpointToCoord(pData, point); - buffer.GetSize().DecrementInBounds(target); - - target = (moveState.Increment == MovementIncrement::Forward) ? - buffer.GetWordEnd(target, wordDelimiters, true) : - buffer.GetWordStart(target, wordDelimiters, true); - - currentColumn = target.X; - } - *pAmountMoved += static_cast(moveState.Increment); - - FAIL_FAST_IF(!(currentColumn >= _getFirstColumnIndex())); - FAIL_FAST_IF(!(currentColumn <= _getLastColumnIndex(pData))); - FAIL_FAST_IF(!(currentScreenInfoRow >= _getFirstScreenInfoRowIndex())); - FAIL_FAST_IF(!(currentScreenInfoRow <= _getLastScreenInfoRowIndex(pData))); - } - - Endpoint start = _screenInfoRowToEndpoint(pData, currentScreenInfoRow) + currentColumn; - - auto target = _endpointToCoord(pData, start); - target = buffer.GetWordEnd(target, wordDelimiters, true); - Endpoint end = _coordToEndpoint(pData, target); - - return std::make_pair(std::move(start), std::move(end)); -} - -// Routine Description: -// - calculates new Endpoints if they were to be moved moveCount times -// by line. -// Arguments: -// - pData - the UiaData for the terminal we are operating on -// - moveCount - the number of times to move -// - moveState - values indicating the state of the console for the -// move operation -// - pAmountMoved - the number of times that the return values are "moved" -// Return Value: -// - a pair of endpoints of the form -std::pair UiaTextRangeBase::_moveByLine(gsl::not_null pData, - const int moveCount, - const MoveState moveState, - _Out_ gsl::not_null const pAmountMoved) -{ - *pAmountMoved = 0; - Endpoint start = _screenInfoRowToEndpoint(pData, moveState.StartScreenInfoRow) + moveState.StartColumn; - Endpoint end = _screenInfoRowToEndpoint(pData, moveState.EndScreenInfoRow) + moveState.EndColumn; - ScreenInfoRow currentScreenInfoRow = moveState.StartScreenInfoRow; - // we don't want to move the range if we're already in the - // limiting row and trying to move off the end of the screen buffer - const bool illegalMovement = (currentScreenInfoRow == moveState.LimitingRow && - ((moveCount < 0 && moveState.Increment == MovementIncrement::Backward) || - (moveCount > 0 && moveState.Increment == MovementIncrement::Forward))); - - if (moveCount != 0 && !illegalMovement) - { - // move the range - for (int i = 0; i < abs(moveCount); ++i) - { - if (currentScreenInfoRow == moveState.LimitingRow) - { - break; - } - currentScreenInfoRow += static_cast(moveState.Increment); - *pAmountMoved += static_cast(moveState.Increment); - - FAIL_FAST_IF(!(currentScreenInfoRow >= _getFirstScreenInfoRowIndex())); - FAIL_FAST_IF(!(currentScreenInfoRow <= _getLastScreenInfoRowIndex(pData))); - } - start = _screenInfoRowToEndpoint(pData, currentScreenInfoRow); - end = start + _getLastColumnIndex(pData); - } - - return std::make_pair(std::move(start), std::move(end)); -} - -// Routine Description: -// - calculates new Endpoints if they were to be moved moveCount times -// by document. -// Arguments: -// - pData - the UiaData for the terminal we are operating on -// - moveCount - the number of times to move -// - moveState - values indicating the state of the console for the -// move operation -// - pAmountMoved - the number of times that the return values are "moved" -// Return Value: -// - a pair of endpoints of the form -std::pair UiaTextRangeBase::_moveByDocument(gsl::not_null pData, - const int /*moveCount*/, - const MoveState moveState, - _Out_ gsl::not_null const pAmountMoved) -{ - // We can't move by anything larger than a line, so move by document will apply and will - // just report that it can't do that. - *pAmountMoved = 0; - - // We then have to return the same endpoints as what we initially had so nothing happens. - Endpoint start = _screenInfoRowToEndpoint(pData, moveState.StartScreenInfoRow) + moveState.StartColumn; - Endpoint end = _screenInfoRowToEndpoint(pData, moveState.EndScreenInfoRow) + moveState.EndColumn; - - return std::make_pair(std::move(start), std::move(end)); -} - -// Routine Description: -// - calculates new Endpoints/degenerate state if the indicated -// endpoint was moved moveCount times by character. -// Arguments: -// - pData - the UiaData for the terminal we are operating on // - moveCount - the number of times to move // - endpoint - the endpoint to move -// - moveState - values indicating the state of the console for the -// move operation // - pAmountMoved - the number of times that the return values are "moved" +// - preventBufferEnd - when enabled, prevent endpoint from being at the end of the buffer +// This is used for general movement, where you are not allowed to +// create a degenerate range // Return Value: -// - A tuple of elements of the form -std::tuple UiaTextRangeBase::_moveEndpointByUnitCharacter(gsl::not_null pData, - const int moveCount, - const TextPatternRangeEndpoint endpoint, - const MoveState moveState, - _Out_ gsl::not_null const pAmountMoved) -{ - if (moveState.Direction == MovementDirection::Forward) - { - return _moveEndpointByUnitCharacterForward(pData, moveCount, endpoint, moveState, pAmountMoved); - } - else - { - return _moveEndpointByUnitCharacterBackward(pData, moveCount, endpoint, moveState, pAmountMoved); - } -} - -std::tuple -UiaTextRangeBase::_moveEndpointByUnitCharacterForward(gsl::not_null pData, - const int moveCount, - const TextPatternRangeEndpoint endpoint, - const MoveState moveState, - _Out_ gsl::not_null const pAmountMoved) +// - +void UiaTextRangeBase::_moveEndpointByUnitWord(_In_ const int moveCount, + _In_ const TextPatternRangeEndpoint endpoint, + _Out_ gsl::not_null const pAmountMoved, + _In_ const bool preventBufferEnd) { *pAmountMoved = 0; - ScreenInfoRow currentScreenInfoRow = 0; - Column currentColumn = 0; - // set current location vars - if (endpoint == TextPatternRangeEndpoint::TextPatternRangeEndpoint_Start) - { - currentScreenInfoRow = moveState.StartScreenInfoRow; - currentColumn = moveState.StartColumn; - } - else + if (moveCount == 0) { - currentScreenInfoRow = moveState.EndScreenInfoRow; - currentColumn = moveState.EndColumn; + return; } - for (int i = 0; i < abs(moveCount); ++i) - { - // get the current row's right - const ROW& row = pData->GetTextBuffer().GetRowByOffset(currentScreenInfoRow); - const size_t right = row.GetCharRow().MeasureRight(); - const auto expectedColumn = gsl::narrow_cast(currentColumn) + 1; - - // check if we're at the edge of the screen info buffer - if (currentScreenInfoRow == moveState.LimitingRow && - expectedColumn >= right) - { - break; - } - else if (expectedColumn >= right) - { - // we're at the edge of a row and need to go to the next one - currentColumn = moveState.FirstColumnInRow; - currentScreenInfoRow += static_cast(moveState.Increment); - } - else - { - // moving somewhere away from the edges of a row - currentColumn += static_cast(moveState.Increment); - } - *pAmountMoved += static_cast(moveState.Increment); + const bool allowBottomExclusive = !preventBufferEnd; + const MovementDirection moveDirection = (moveCount > 0) ? MovementDirection::Forward : MovementDirection::Backward; + const auto& buffer = _pData->GetTextBuffer(); + const auto bufferSize = buffer.GetSize(); + const auto bufferOrigin = bufferSize.Origin(); + const auto bufferEnd = bufferSize.EndExclusive(); + const auto lastCharPos = buffer.GetLastNonSpaceCharacter(); - FAIL_FAST_IF(!(currentColumn >= _getFirstColumnIndex())); - FAIL_FAST_IF(!(currentColumn <= _getLastColumnIndex(pData))); - FAIL_FAST_IF(!(currentScreenInfoRow >= _getFirstScreenInfoRowIndex())); - FAIL_FAST_IF(!(currentScreenInfoRow <= _getLastScreenInfoRowIndex(pData))); - } + auto resultPos = GetEndpoint(endpoint); + auto nextPos = resultPos; - // translate the row back to an endpoint and handle any crossed endpoints - const Endpoint convertedEndpoint = _screenInfoRowToEndpoint(pData, currentScreenInfoRow) + currentColumn; - Endpoint start = _screenInfoRowToEndpoint(pData, moveState.StartScreenInfoRow) + moveState.StartColumn; - Endpoint end = _screenInfoRowToEndpoint(pData, moveState.EndScreenInfoRow) + moveState.EndColumn; - bool degenerate = false; - if (endpoint == TextPatternRangeEndpoint::TextPatternRangeEndpoint_Start) - { - start = convertedEndpoint; - if (_compareScreenCoords(pData, - currentScreenInfoRow, - currentColumn, - moveState.EndScreenInfoRow, - moveState.EndColumn) == 1) - { - end = start; - degenerate = true; - } - } - else + bool success = true; + while (std::abs(*pAmountMoved) < std::abs(moveCount) && success) { - end = convertedEndpoint; - if (_compareScreenCoords(pData, - currentScreenInfoRow, - currentColumn, - moveState.StartScreenInfoRow, - moveState.StartColumn) == -1) + nextPos = resultPos; + switch (moveDirection) { - start = end; - degenerate = true; - } - } - return std::make_tuple(start, end, degenerate); -} - -std::tuple -UiaTextRangeBase::_moveEndpointByUnitCharacterBackward(gsl::not_null pData, - const int moveCount, - const TextPatternRangeEndpoint endpoint, - const MoveState moveState, - _Out_ gsl::not_null const pAmountMoved) -{ - *pAmountMoved = 0; - ScreenInfoRow currentScreenInfoRow = 0; - Column currentColumn = 0; - - // set current location vars - if (endpoint == TextPatternRangeEndpoint::TextPatternRangeEndpoint_Start) - { - currentScreenInfoRow = moveState.StartScreenInfoRow; - currentColumn = moveState.StartColumn; - } - else - { - currentScreenInfoRow = moveState.EndScreenInfoRow; - currentColumn = moveState.EndColumn; - } - - for (int i = 0; i < abs(moveCount); ++i) - { - // check if we're at the edge of the screen info buffer - if (currentScreenInfoRow == moveState.LimitingRow && - currentColumn == moveState.LastColumnInRow) + case MovementDirection::Forward: { + if (nextPos == bufferEnd) + { + success = false; + } + else if (buffer.MoveToNextWord(nextPos, _wordDelimiters, lastCharPos)) + { + resultPos = nextPos; + (*pAmountMoved)++; + } + else if (allowBottomExclusive) + { + resultPos = bufferEnd; + (*pAmountMoved)++; + } break; } - else if (currentColumn == moveState.LastColumnInRow) + case MovementDirection::Backward: { - // we're at the edge of a row and need to go to the - // next one. move to the cell with the last non-whitespace charactor - - currentScreenInfoRow += static_cast(moveState.Increment); - // get the right cell for the next row - const ROW& row = pData->GetTextBuffer().GetRowByOffset(currentScreenInfoRow); - const size_t right = row.GetCharRow().MeasureRight(); - currentColumn = gsl::narrow((right == 0) ? 0 : right - 1); + if (nextPos == bufferOrigin) + { + success = false; + } + else if (buffer.MoveToPreviousWord(nextPos, _wordDelimiters)) + { + resultPos = nextPos; + (*pAmountMoved)--; + } + else + { + resultPos = bufferOrigin; + } + break; } - else - { - // moving somewhere away from the edges of a row - currentColumn += static_cast(moveState.Increment); + default: + return; } - *pAmountMoved += static_cast(moveState.Increment); - - FAIL_FAST_IF(!(currentColumn >= _getFirstColumnIndex())); - FAIL_FAST_IF(!(currentColumn <= _getLastColumnIndex(pData))); - FAIL_FAST_IF(!(currentScreenInfoRow >= _getFirstScreenInfoRowIndex())); - FAIL_FAST_IF(!(currentScreenInfoRow <= _getLastScreenInfoRowIndex(pData))); } - // translate the row back to an endpoint and handle any crossed endpoints - const Endpoint convertedEndpoint = _screenInfoRowToEndpoint(pData, currentScreenInfoRow) + currentColumn; - Endpoint start = _screenInfoRowToEndpoint(pData, moveState.StartScreenInfoRow) + moveState.StartColumn; - Endpoint end = _screenInfoRowToEndpoint(pData, moveState.EndScreenInfoRow) + moveState.EndColumn; - bool degenerate = false; - if (endpoint == TextPatternRangeEndpoint::TextPatternRangeEndpoint_Start) - { - start = convertedEndpoint; - if (_compareScreenCoords(pData, - currentScreenInfoRow, - currentColumn, - moveState.EndScreenInfoRow, - moveState.EndColumn) == 1) - { - end = start; - degenerate = true; - } - } - else - { - end = convertedEndpoint; - if (_compareScreenCoords(pData, - currentScreenInfoRow, - currentColumn, - moveState.StartScreenInfoRow, - moveState.StartColumn) == -1) - { - start = end; - degenerate = true; - } - } - return std::make_tuple(start, end, degenerate); + SetEndpoint(endpoint, resultPos); } // Routine Description: -// - calculates new Endpoints/degenerate state if the indicated -// endpoint was moved moveCount times by word. +// - moves the UTR's endpoint by moveCount times by line. +// - if endpoints crossed, the degenerate range is created and both endpoints are moved +// - a successful movement on start entails start being at Left() +// - a successful movement on end entails end being at Left() of the NEXT line // Arguments: -// - pData - the UiaData for the terminal we are operating on // - moveCount - the number of times to move // - endpoint - the endpoint to move -// - moveState - values indicating the state of the console for the -// move operation // - pAmountMoved - the number of times that the return values are "moved" +// - preventBufferEnd - when enabled, prevent endpoint from being at the end of the buffer +// This is used for general movement, where you are not allowed to +// create a degenerate range // Return Value: -// - A tuple of elements of the form -std::tuple UiaTextRangeBase::_moveEndpointByUnitWord(gsl::not_null pData, - const int moveCount, - const TextPatternRangeEndpoint endpoint, - const MoveState moveState, - const std::wstring_view wordDelimiters, - _Out_ gsl::not_null const pAmountMoved) -{ - if (moveState.Direction == MovementDirection::Forward) - { - return _moveEndpointByUnitWordForward(pData, moveCount, endpoint, moveState, wordDelimiters, pAmountMoved); - } - else - { - return _moveEndpointByUnitWordBackward(pData, moveCount, endpoint, moveState, wordDelimiters, pAmountMoved); - } -} - -std::tuple -UiaTextRangeBase::_moveEndpointByUnitWordForward(gsl::not_null pData, - const int moveCount, - const TextPatternRangeEndpoint endpoint, - const MoveState moveState, - const std::wstring_view wordDelimiters, - _Out_ gsl::not_null const pAmountMoved) +// - +void UiaTextRangeBase::_moveEndpointByUnitLine(_In_ const int moveCount, + _In_ const TextPatternRangeEndpoint endpoint, + _Out_ gsl::not_null const pAmountMoved, + _In_ const bool preventBufferEnd) { *pAmountMoved = 0; - ScreenInfoRow currentScreenInfoRow = 0; - Column currentColumn = 0; - // set current location vars - if (endpoint == TextPatternRangeEndpoint::TextPatternRangeEndpoint_Start) - { - currentScreenInfoRow = moveState.StartScreenInfoRow; - currentColumn = moveState.StartColumn; - } - else + if (moveCount == 0) { - currentScreenInfoRow = moveState.EndScreenInfoRow; - currentColumn = moveState.EndColumn; + return; } - auto& buffer = pData->GetTextBuffer(); - for (int i = 0; i < abs(moveCount); ++i) + const bool allowBottomExclusive = !preventBufferEnd; + const MovementDirection moveDirection = (moveCount > 0) ? MovementDirection::Forward : MovementDirection::Backward; + const auto bufferSize = _pData->GetTextBuffer().GetSize(); + + bool success = true; + auto resultPos = GetEndpoint(endpoint); + while (abs(*pAmountMoved) < abs(moveCount) && success) { - // get the current row's right - const ROW& row = buffer.GetRowByOffset(currentScreenInfoRow); - const size_t right = row.GetCharRow().MeasureRight(); - const auto expectedColumn = gsl::narrow_cast(currentColumn) + 1; - - // check if we're at the edge of the screen info buffer - if (currentScreenInfoRow == moveState.LimitingRow && - expectedColumn >= right) - { - break; - } - else if (expectedColumn >= right) + auto nextPos = resultPos; + switch (moveDirection) { - // we're at the edge of a row and need to go to the next one - currentColumn = moveState.FirstColumnInRow; - currentScreenInfoRow += static_cast(moveState.Increment); - - // when moving end, we need to encompass the word - if (endpoint == TextPatternRangeEndpoint::TextPatternRangeEndpoint_End) - { - const auto point = _screenInfoRowToEndpoint(pData, currentScreenInfoRow) + currentColumn; - auto target = _endpointToCoord(pData, point); - - target = buffer.GetWordEnd(target, wordDelimiters, true); - - currentColumn = target.X; - } - } - else + case MovementDirection::Forward: { - // moving somewhere away from the edges of a row - const Endpoint point = _screenInfoRowToEndpoint(pData, currentScreenInfoRow) + currentColumn; - auto target = _endpointToCoord(pData, point); - buffer.GetSize().IncrementInBounds(target); - - target = (moveState.Increment == MovementIncrement::Forward) ? - buffer.GetWordEnd(target, wordDelimiters, true) : - buffer.GetWordStart(target, wordDelimiters, true); - - if (endpoint == TextPatternRangeEndpoint::TextPatternRangeEndpoint_Start) + // can't move past end + if (nextPos.Y >= bufferSize.BottomInclusive()) { - buffer.GetSize().IncrementInBounds(target); - - if (static_cast(target.X) == moveState.FirstColumnInRow && currentScreenInfoRow != moveState.LimitingRow) + if (preventBufferEnd || nextPos == bufferSize.EndExclusive()) { - currentScreenInfoRow += static_cast(moveState.Increment); + success = false; + break; } } - currentColumn = target.X; - } - *pAmountMoved += static_cast(moveState.Increment); - - FAIL_FAST_IF(!(currentColumn >= _getFirstColumnIndex())); - FAIL_FAST_IF(!(currentColumn <= _getLastColumnIndex(pData))); - FAIL_FAST_IF(!(currentScreenInfoRow >= _getFirstScreenInfoRowIndex())); - FAIL_FAST_IF(!(currentScreenInfoRow <= _getLastScreenInfoRowIndex(pData))); - } - - // translate the row back to an endpoint and handle any crossed endpoints - const Endpoint convertedEndpoint = _screenInfoRowToEndpoint(pData, currentScreenInfoRow) + currentColumn; - Endpoint start = _screenInfoRowToEndpoint(pData, moveState.StartScreenInfoRow) + moveState.StartColumn; - Endpoint end = _screenInfoRowToEndpoint(pData, moveState.EndScreenInfoRow) + moveState.EndColumn; - bool degenerate = false; - if (endpoint == TextPatternRangeEndpoint::TextPatternRangeEndpoint_Start) - { - start = convertedEndpoint; - if (_compareScreenCoords(pData, - currentScreenInfoRow, - currentColumn, - moveState.EndScreenInfoRow, - moveState.EndColumn) == 1) - { - end = start; - degenerate = true; - } - } - else - { - end = convertedEndpoint; - if (_compareScreenCoords(pData, - currentScreenInfoRow, - currentColumn, - moveState.StartScreenInfoRow, - moveState.StartColumn) == -1) - { - start = end; - degenerate = true; - } - } - return std::make_tuple(start, end, degenerate); -} - -std::tuple -UiaTextRangeBase::_moveEndpointByUnitWordBackward(gsl::not_null pData, - const int moveCount, - const TextPatternRangeEndpoint endpoint, - const MoveState moveState, - const std::wstring_view wordDelimiters, - _Out_ gsl::not_null const pAmountMoved) -{ - *pAmountMoved = 0; - ScreenInfoRow currentScreenInfoRow = 0; - Column currentColumn = 0; - - // set current location vars - if (endpoint == TextPatternRangeEndpoint::TextPatternRangeEndpoint_Start) - { - currentScreenInfoRow = moveState.StartScreenInfoRow; - currentColumn = moveState.StartColumn; - } - else - { - currentScreenInfoRow = moveState.EndScreenInfoRow; - currentColumn = moveState.EndColumn; - } - - auto& buffer = pData->GetTextBuffer(); - for (int i = 0; i < abs(moveCount); ++i) - { - // check if we're at the edge of the screen info buffer - if (currentScreenInfoRow == moveState.LimitingRow && - currentColumn == moveState.LastColumnInRow) - { + nextPos.X = bufferSize.RightInclusive(); + success = bufferSize.IncrementInBounds(nextPos, allowBottomExclusive); + if (success) + { + resultPos = nextPos; + (*pAmountMoved)++; + } break; } - else if (currentColumn == moveState.LastColumnInRow) + case MovementDirection::Backward: { - // we're at the edge of a row and need to go to the - // next one. move to the cell with the last non-whitespace charactor - - currentScreenInfoRow += static_cast(moveState.Increment); - // get the right cell for the next row - const ROW& row = buffer.GetRowByOffset(currentScreenInfoRow); - const size_t right = row.GetCharRow().MeasureRight(); - currentColumn = gsl::narrow((right == 0) ? 0 : right - 1); - - // get the right-most word for the previous row - const auto point = _screenInfoRowToEndpoint(pData, currentScreenInfoRow) + currentColumn; - auto target = _endpointToCoord(pData, point); - - target = buffer.GetWordStart(target, wordDelimiters, true); - - if (endpoint == TextPatternRangeEndpoint::TextPatternRangeEndpoint_Start) + // can't move past top + if (!allowBottomExclusive && nextPos.Y == bufferSize.Top()) { - buffer.GetSize().IncrementInBounds(target); + success = false; + break; } - currentColumn = target.X; - } - else - { - // moving somewhere away from the edges of a row - const Endpoint point = _screenInfoRowToEndpoint(pData, currentScreenInfoRow) + currentColumn; - auto target = _endpointToCoord(pData, point); - buffer.GetSize().DecrementInBounds(target); - - target = (moveState.Increment == MovementIncrement::Forward) ? - buffer.GetWordEnd(target, wordDelimiters, true) : - buffer.GetWordStart(target, wordDelimiters, true); + // NOTE: Automatically detects if we are trying to move past origin + success = bufferSize.DecrementInBounds(nextPos, allowBottomExclusive); - if (endpoint == TextPatternRangeEndpoint::TextPatternRangeEndpoint_End) + if (success) { - buffer.GetSize().DecrementInBounds(target); - - if (static_cast(target.X) == moveState.FirstColumnInRow && currentScreenInfoRow != moveState.LimitingRow) - { - currentScreenInfoRow += static_cast(moveState.Increment); - } + nextPos.X = bufferSize.Left(); + resultPos = nextPos; + (*pAmountMoved)--; } - - currentColumn = target.X; - } - *pAmountMoved += static_cast(moveState.Increment); - - FAIL_FAST_IF(!(currentColumn >= _getFirstColumnIndex())); - FAIL_FAST_IF(!(currentColumn <= _getLastColumnIndex(pData))); - FAIL_FAST_IF(!(currentScreenInfoRow >= _getFirstScreenInfoRowIndex())); - FAIL_FAST_IF(!(currentScreenInfoRow <= _getLastScreenInfoRowIndex(pData))); - } - - // translate the row back to an endpoint and handle any crossed endpoints - const Endpoint convertedEndpoint = _screenInfoRowToEndpoint(pData, currentScreenInfoRow) + currentColumn; - Endpoint start = _screenInfoRowToEndpoint(pData, moveState.StartScreenInfoRow) + moveState.StartColumn; - Endpoint end = _screenInfoRowToEndpoint(pData, moveState.EndScreenInfoRow) + moveState.EndColumn; - bool degenerate = false; - if (endpoint == TextPatternRangeEndpoint::TextPatternRangeEndpoint_Start) - { - start = convertedEndpoint; - if (_compareScreenCoords(pData, - currentScreenInfoRow, - currentColumn, - moveState.EndScreenInfoRow, - moveState.EndColumn) == 1) - { - end = start; - degenerate = true; + break; } - } - else - { - end = convertedEndpoint; - if (_compareScreenCoords(pData, - currentScreenInfoRow, - currentColumn, - moveState.StartScreenInfoRow, - moveState.StartColumn) == -1) - { - start = end; - degenerate = true; + default: + break; } } - return std::make_tuple(start, end, degenerate); + + SetEndpoint(endpoint, resultPos); } // Routine Description: -// - calculates new Endpoints/degenerate state if the indicated -// endpoint was moved moveCount times by line. +// - moves the UTR's endpoint by moveCount times by document. +// - if endpoints crossed, the degenerate range is created and both endpoints are moved // Arguments: -// - pData - the UiaData for the terminal we are operating on // - moveCount - the number of times to move // - endpoint - the endpoint to move -// - moveState - values indicating the state of the console for the -// move operation // - pAmountMoved - the number of times that the return values are "moved" +// - preventBufferEnd - when enabled, prevent endpoint from being at the end of the buffer +// This is used for general movement, where you are not allowed to +// create a degenerate range // Return Value: -// - A tuple of elements of the form -std::tuple UiaTextRangeBase::_moveEndpointByUnitLine(gsl::not_null pData, - const int moveCount, - const TextPatternRangeEndpoint endpoint, - const MoveState moveState, - _Out_ gsl::not_null const pAmountMoved) +// - +void UiaTextRangeBase::_moveEndpointByUnitDocument(_In_ const int moveCount, + _In_ const TextPatternRangeEndpoint endpoint, + _Out_ gsl::not_null const pAmountMoved, + _In_ const bool preventBufferEnd) { *pAmountMoved = 0; - int count = moveCount; - ScreenInfoRow currentScreenInfoRow = 0; - Column currentColumn = 0; - bool forceDegenerate = false; - Endpoint start = _screenInfoRowToEndpoint(pData, moveState.StartScreenInfoRow) + moveState.StartColumn; - Endpoint end = _screenInfoRowToEndpoint(pData, moveState.EndScreenInfoRow) + moveState.EndColumn; - bool degenerate = false; if (moveCount == 0) { - return std::make_tuple(start, end, degenerate); + return; } const MovementDirection moveDirection = (moveCount > 0) ? MovementDirection::Forward : MovementDirection::Backward; + const auto bufferSize = _pData->GetTextBuffer().GetSize(); - if (endpoint == TextPatternRangeEndpoint::TextPatternRangeEndpoint_Start) - { - currentScreenInfoRow = moveState.StartScreenInfoRow; - currentColumn = moveState.StartColumn; - } - else - { - currentScreenInfoRow = moveState.EndScreenInfoRow; - currentColumn = moveState.EndColumn; - } - - // check if we can't be moved anymore - if (currentScreenInfoRow == moveState.LimitingRow && - currentColumn == moveState.LastColumnInRow) - { - return std::make_tuple(start, end, degenerate); - } - else if (endpoint == TextPatternRangeEndpoint::TextPatternRangeEndpoint_Start && - moveDirection == MovementDirection::Forward) - { - if (moveState.StartScreenInfoRow == moveState.LimitingRow) - { - // _start is somewhere on the limiting row but not at - // the very end. move to the end of the last row - count -= static_cast(moveState.Increment); - *pAmountMoved += static_cast(moveState.Increment); - currentColumn = _getLastColumnIndex(pData); - forceDegenerate = true; - } - if (moveState.StartColumn != _getFirstColumnIndex()) - { - // _start is somewhere in the middle of a row, so do a - // partial movement to the beginning of the next row - count -= static_cast(moveState.Increment); - *pAmountMoved += static_cast(moveState.Increment); - currentScreenInfoRow += static_cast(moveState.Increment); - currentColumn = _getFirstColumnIndex(); - } - } - else if (endpoint == TextPatternRangeEndpoint::TextPatternRangeEndpoint_Start && - moveDirection == MovementDirection::Backward) - { - if (moveState.StartColumn != _getFirstColumnIndex()) - { - // moving backwards when we weren't already at the beginning of - // the row so move there first to align with the text unit boundary - count -= static_cast(moveState.Increment); - *pAmountMoved += static_cast(moveState.Increment); - currentColumn = _getFirstColumnIndex(); - } - } - else if (endpoint == TextPatternRangeEndpoint::TextPatternRangeEndpoint_End && - moveDirection == MovementDirection::Forward) - { - if (moveState.EndColumn != _getLastColumnIndex(pData)) - { - // _end is not at the last column in a row, so we move - // forward to it with a partial movement - count -= static_cast(moveState.Increment); - *pAmountMoved += static_cast(moveState.Increment); - currentColumn = _getLastColumnIndex(pData); - } - } - else - { - // _end moving backwards - if (moveState.EndScreenInfoRow == moveState.LimitingRow) - { - // _end is somewhere on the limiting row but not at the - // front. move it there - count -= static_cast(moveState.Increment); - *pAmountMoved += static_cast(moveState.Increment); - currentColumn = _getFirstColumnIndex(); - forceDegenerate = true; - } - else if (moveState.EndColumn != _getLastColumnIndex(pData)) - { - // _end is not at the last column in a row, so we move it - // backwards to it with a partial move - count -= static_cast(moveState.Increment); - *pAmountMoved += static_cast(moveState.Increment); - currentColumn = _getLastColumnIndex(pData); - currentScreenInfoRow += static_cast(moveState.Increment); - } - } - - FAIL_FAST_IF(!(currentColumn >= _getFirstColumnIndex())); - FAIL_FAST_IF(!(currentColumn <= _getLastColumnIndex(pData))); - FAIL_FAST_IF(!(currentScreenInfoRow >= _getFirstScreenInfoRowIndex())); - FAIL_FAST_IF(!(currentScreenInfoRow <= _getLastScreenInfoRowIndex(pData))); - - // move the row that the endpoint corresponds to - while (count != 0 && currentScreenInfoRow != moveState.LimitingRow) - { - count -= static_cast(moveState.Increment); - currentScreenInfoRow += static_cast(moveState.Increment); - *pAmountMoved += static_cast(moveState.Increment); - - FAIL_FAST_IF(!(currentScreenInfoRow >= _getFirstScreenInfoRowIndex())); - FAIL_FAST_IF(!(currentScreenInfoRow <= _getLastScreenInfoRowIndex(pData))); - } - - // translate the row back to an endpoint and handle any crossed endpoints - const Endpoint convertedEndpoint = _screenInfoRowToEndpoint(pData, currentScreenInfoRow) + currentColumn; - if (endpoint == TextPatternRangeEndpoint::TextPatternRangeEndpoint_Start) - { - start = convertedEndpoint; - if (currentScreenInfoRow > moveState.EndScreenInfoRow || forceDegenerate) - { - degenerate = true; - end = start; - } - } - else + const auto target = GetEndpoint(endpoint); + switch (moveDirection) { - end = convertedEndpoint; - if (currentScreenInfoRow < moveState.StartScreenInfoRow || forceDegenerate) - { - degenerate = true; - start = end; - } - } - - return std::make_tuple(start, end, degenerate); -} - -// Routine Description: -// - calculates new Endpoints/degenerate state if the indicate -// endpoint was moved moveCount times by document. -// Arguments: -// - pData - the UiaData for the terminal we are operating on -// - moveCount - the number of times to move -// - endpoint - the endpoint to move -// - moveState - values indicating the state of the console for the -// move operation -// - pAmountMoved - the number of times that the return values are "moved" -// Return Value: -// - A tuple of elements of the form -std::tuple UiaTextRangeBase::_moveEndpointByUnitDocument(gsl::not_null pData, - const int moveCount, - const TextPatternRangeEndpoint endpoint, - const MoveState moveState, - _Out_ gsl::not_null const pAmountMoved) -{ - *pAmountMoved = 0; - - Endpoint start = 0; - Endpoint end = 0; - bool degenerate = false; - if (endpoint == TextPatternRangeEndpoint::TextPatternRangeEndpoint_Start) + case MovementDirection::Forward: { - if (moveCount < 0) + const auto documentEnd = bufferSize.EndExclusive(); + if (preventBufferEnd || target == documentEnd) { - // moving _start backwards - start = _screenInfoRowToEndpoint(pData, _getFirstScreenInfoRowIndex()) + _getFirstColumnIndex(); - end = _screenInfoRowToEndpoint(pData, moveState.EndScreenInfoRow) + moveState.EndColumn; - if (!(moveState.StartScreenInfoRow == _getFirstScreenInfoRowIndex() && - moveState.StartColumn == _getFirstColumnIndex())) - { - *pAmountMoved += static_cast(moveState.Increment); - } + return; } else { - // moving _start forwards - start = _screenInfoRowToEndpoint(pData, _getLastScreenInfoRowIndex(pData)) + _getLastColumnIndex(pData); - end = start; - degenerate = true; - if (!(moveState.StartScreenInfoRow == _getLastScreenInfoRowIndex(pData) && - moveState.StartColumn == _getLastColumnIndex(pData))) - { - *pAmountMoved += static_cast(moveState.Increment); - } + SetEndpoint(endpoint, documentEnd); + (*pAmountMoved)++; } + break; } - else + case MovementDirection::Backward: { - if (moveCount < 0) + const auto documentBegin = bufferSize.Origin(); + if (target == documentBegin) { - // moving _end backwards - end = _screenInfoRowToEndpoint(pData, _getFirstScreenInfoRowIndex()) + _getFirstColumnIndex(); - start = end; - degenerate = true; - if (!(moveState.EndScreenInfoRow == _getFirstScreenInfoRowIndex() && - moveState.EndColumn == _getFirstColumnIndex())) - { - *pAmountMoved += static_cast(moveState.Increment); - } + return; } else { - // moving _end forwards - end = _screenInfoRowToEndpoint(pData, _getLastScreenInfoRowIndex(pData)) + _getLastColumnIndex(pData); - start = _screenInfoRowToEndpoint(pData, moveState.StartScreenInfoRow) + moveState.StartColumn; - if (!(moveState.EndScreenInfoRow == _getLastScreenInfoRowIndex(pData) && - moveState.EndColumn == _getLastColumnIndex(pData))) - { - *pAmountMoved += static_cast(moveState.Increment); - } + SetEndpoint(endpoint, documentBegin); + (*pAmountMoved)--; } + break; + } + default: + break; } - - return std::make_tuple(start, end, degenerate); -} - -COORD UiaTextRangeBase::_endpointToCoord(gsl::not_null pData, const Endpoint endpoint) -{ - return { gsl::narrow(_endpointToColumn(pData, endpoint)), gsl::narrow(_endpointToScreenInfoRow(pData, endpoint)) }; -} - -Endpoint UiaTextRangeBase::_coordToEndpoint(gsl::not_null pData, - const COORD coord) -{ - return _screenInfoRowToEndpoint(pData, coord.Y) + coord.X; } RECT UiaTextRangeBase::_getTerminalRect() const diff --git a/src/types/UiaTextRangeBase.hpp b/src/types/UiaTextRangeBase.hpp index daae0a6aecd..6ba481dc42e 100644 --- a/src/types/UiaTextRangeBase.hpp +++ b/src/types/UiaTextRangeBase.hpp @@ -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 pData); virtual const COORD _getScreenFontSize() const; - - static const unsigned int _getTotalRows(gsl::not_null pData) noexcept; - static const unsigned int _getRowWidth(gsl::not_null pData); - - static const unsigned int _getFirstScreenInfoRowIndex() noexcept; - static const unsigned int _getLastScreenInfoRowIndex(gsl::not_null pData) noexcept; - - static const Column _getFirstColumnIndex() noexcept; - static const Column _getLastColumnIndex(gsl::not_null pData); - - const unsigned int _rowCountInRange(gsl::not_null pData) const; - - static const TextBufferRow _endpointToTextBufferRow(gsl::not_null pData, - const Endpoint endpoint); - static const ScreenInfoRow _textBufferRowToScreenInfoRow(gsl::not_null pData, - const TextBufferRow row) noexcept; - - static const TextBufferRow _screenInfoRowToTextBufferRow(gsl::not_null pData, - const ScreenInfoRow row) noexcept; - static const Endpoint _textBufferRowToEndpoint(gsl::not_null pData, const TextBufferRow row); - - static const Endpoint _wordBeginEndpoint(gsl::not_null pData, Endpoint target, const std::wstring_view wordDelimiters); - static const Endpoint _wordEndEndpoint(gsl::not_null pData, Endpoint target, const std::wstring_view wordDelimiters); - - static const ScreenInfoRow _endpointToScreenInfoRow(gsl::not_null pData, - const Endpoint endpoint); - static const Endpoint _screenInfoRowToEndpoint(gsl::not_null pData, - const ScreenInfoRow row); - - static COORD _endpointToCoord(gsl::not_null pData, - const Endpoint endpoint); - static Endpoint _coordToEndpoint(gsl::not_null pData, - const COORD coord); - - static const Column _endpointToColumn(gsl::not_null pData, - const Endpoint endpoint); - - static const Row _normalizeRow(gsl::not_null pData, const Row row) noexcept; - - static const ViewportRow _screenInfoRowToViewportRow(gsl::not_null 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 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 pData, - const ScreenInfoRow screenInfoRow, - _Inout_ std::vector& coords) const; - - static const int _compareScreenCoords(gsl::not_null pData, - const ScreenInfoRow rowA, - const Column colA, - const ScreenInfoRow rowB, - const Column colB); - - static std::pair _moveByCharacter(gsl::not_null pData, - const int moveCount, - const MoveState moveState, - _Out_ gsl::not_null const pAmountMoved); - - static std::pair _moveByCharacterForward(gsl::not_null pData, - const int moveCount, - const MoveState moveState, - _Out_ gsl::not_null const pAmountMoved); - - static std::pair _moveByCharacterBackward(gsl::not_null pData, - const int moveCount, - const MoveState moveState, - _Out_ gsl::not_null const pAmountMoved); - - static std::pair _moveByWord(gsl::not_null pData, - const int moveCount, - const MoveState moveState, - const std::wstring_view wordDelimiters, - _Out_ gsl::not_null const pAmountMoved); - - static std::pair _moveByWordForward(gsl::not_null pData, - const int moveCount, - const MoveState moveState, - const std::wstring_view wordDelimiters, - _Out_ gsl::not_null const pAmountMoved); - - static std::pair _moveByWordBackward(gsl::not_null pData, - const int moveCount, - const MoveState moveState, - const std::wstring_view wordDelimiters, - _Out_ gsl::not_null const pAmountMoved); - - static std::pair _moveByLine(gsl::not_null pData, - const int moveCount, - const MoveState moveState, - _Out_ gsl::not_null const pAmountMoved); - - static std::pair _moveByDocument(gsl::not_null pData, - const int moveCount, - const MoveState moveState, - _Out_ gsl::not_null const pAmountMoved); - - static std::tuple - _moveEndpointByUnitCharacter(gsl::not_null pData, - const int moveCount, - const TextPatternRangeEndpoint endpoint, - const MoveState moveState, - _Out_ gsl::not_null const pAmountMoved); - - static std::tuple - _moveEndpointByUnitCharacterForward(gsl::not_null pData, - const int moveCount, - const TextPatternRangeEndpoint endpoint, - const MoveState moveState, - _Out_ gsl::not_null const pAmountMoved); - - static std::tuple - _moveEndpointByUnitCharacterBackward(gsl::not_null pData, - const int moveCount, - const TextPatternRangeEndpoint endpoint, - const MoveState moveState, - _Out_ gsl::not_null const pAmountMoved); - - static std::tuple - _moveEndpointByUnitWord(gsl::not_null pData, - const int moveCount, - const TextPatternRangeEndpoint endpoint, - const MoveState moveState, - const std::wstring_view wordDelimiters, - _Out_ gsl::not_null const pAmountMoved); - - static std::tuple - _moveEndpointByUnitWordForward(gsl::not_null pData, - const int moveCount, - const TextPatternRangeEndpoint endpoint, - const MoveState moveState, - const std::wstring_view wordDelimiters, - _Out_ gsl::not_null const pAmountMoved); - - static std::tuple - _moveEndpointByUnitWordBackward(gsl::not_null pData, - const int moveCount, - const TextPatternRangeEndpoint endpoint, - const MoveState moveState, - const std::wstring_view wordDelimiters, - _Out_ gsl::not_null const pAmountMoved); - - static std::tuple - _moveEndpointByUnitLine(gsl::not_null pData, - const int moveCount, - const TextPatternRangeEndpoint endpoint, - const MoveState moveState, - _Out_ gsl::not_null const pAmountMoved); - - static std::tuple - _moveEndpointByUnitDocument(gsl::not_null pData, - const int moveCount, - const TextPatternRangeEndpoint endpoint, - const MoveState moveState, - _Out_ gsl::not_null const pAmountMoved); + const unsigned int _getViewportHeight(const SMALL_RECT viewport) const noexcept; + + void _getBoundingRect(_In_ const COORD startAnchor, _In_ const COORD endAnchor, _Inout_ std::vector& coords) const; + + void + _moveEndpointByUnitCharacter(_In_ const int moveCount, + _In_ const TextPatternRangeEndpoint endpoint, + _Out_ gsl::not_null const pAmountMoved, + _In_ const bool preventBufferEnd = false); + + void + _moveEndpointByUnitWord(_In_ const int moveCount, + _In_ const TextPatternRangeEndpoint endpoint, + _Out_ gsl::not_null const pAmountMoved, + _In_ const bool preventBufferEnd = false); + + void + _moveEndpointByUnitLine(_In_ const int moveCount, + _In_ const TextPatternRangeEndpoint endpoint, + _Out_ gsl::not_null const pAmountMoved, + _In_ const bool preventBufferEnd = false); + + void + _moveEndpointByUnitDocument(_In_ const int moveCount, + _In_ const TextPatternRangeEndpoint endpoint, + _Out_ gsl::not_null 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; diff --git a/src/types/inc/viewport.hpp b/src/types/inc/viewport.hpp index 711c5a02764..9ad56ade855 100644 --- a/src/types/inc/viewport.hpp +++ b/src/types/inc/viewport.hpp @@ -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; diff --git a/src/types/viewport.cpp b/src/types/viewport.cpp index ae07de924c3..daa8e7e4e19 100644 --- a/src/types/viewport.cpp +++ b/src/types/viewport.cpp @@ -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: +// - +// 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;