Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor UiaTextRange For Improved Navigation and Reliability #4018

Merged
24 commits merged into from
Jan 31, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
719676c
utr refactor
carlos-zamora Dec 18, 2019
e7555dc
Refactor UiaTextRange:
carlos-zamora Dec 19, 2019
76a26e3
remove accidental file
carlos-zamora Dec 19, 2019
d4c4e29
Add accessibilityMode to GetWordStart/End and Viewport functions.
carlos-zamora Dec 20, 2019
3017aaa
Testing!!!!
carlos-zamora Dec 20, 2019
f6b2ea2
wtvr
carlos-zamora Dec 20, 2019
9a40869
Let's try doing word nav this way
Dec 21, 2019
ea22c8f
Add testing for Compare, CompareEndpoint, Expand, and MoveEndpointByR…
Jan 16, 2020
bcfc522
Turns out, the bug is in our wrapper class. Not supposed to return nu…
carlos-zamora Jan 22, 2020
71d50cb
- Code format
carlos-zamora Jan 22, 2020
c075ab2
Finish fixing NVDA issues with UiaTextRange (scoped for this PR)
carlos-zamora Jan 27, 2020
d11a7b6
Merge branch 'master' into dev/cazamor/utr-refactor-1
carlos-zamora Jan 27, 2020
9ee0d72
Good amount of PR Changes
carlos-zamora Jan 30, 2020
e44db54
A few dumb errors on my part
carlos-zamora Jan 30, 2020
4571d42
Polish non-testing area. Up next: testing polish
carlos-zamora Jan 30, 2020
1e9cf75
Polish tests
carlos-zamora Jan 30, 2020
7bcfa72
static analysis fixes
carlos-zamora Jan 31, 2020
706620c
whoops! missed a const!
carlos-zamora Jan 31, 2020
f676af3
pass SA
carlos-zamora Jan 31, 2020
d21fbdf
dead code
carlos-zamora Jan 31, 2020
1caf2ca
fix the build!!
carlos-zamora Jan 31, 2020
f8e0548
Dustin's PR Comments + added noexcepts
carlos-zamora Jan 31, 2020
306013f
added comments about my pain
carlos-zamora Jan 31, 2020
25de3ed
wrong parameter name
carlos-zamora Jan 31, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
305 changes: 278 additions & 27 deletions src/buffer/out/textBuffer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -956,87 +956,338 @@ 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())
{
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:
Expand Down
10 changes: 8 additions & 2 deletions src/buffer/out/textBuffer.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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;
Expand Down
Loading