Repository: nvaccess/nvda

Issue:
UI Automation in Windows Console: text review behaves inconsistently when the window is maximized
<!--
Please thoroughly read NVDA's wiki article on how to fill in this template, including how to provide the required files.
Issues may be closed if the required information is not present.
https://github.com/nvaccess/nvda/wiki/Github-issue-template-explanation-and-examples
-->

### Steps to reproduce:
1. Open Command Prompt with UIA enabled.
2. Maximize the window.
3. In a git repository, run `git log`.

### Actual behavior:
Text review gets stuck.

### Expected behavior:
Text review shows all the visible text.

### System configuration
#### NVDA installed/portable/running from source:
Source and installed.

#### NVDA version:
Latest master.

#### Windows version:
Windows 10 1903.

#### Name and version of other software in use when reproducing the issue:
Command Prompt/Windows Console

#### Other information about your system:

### Other questions
#### Does the issue still occur after restarting your PC?
Yes.

#### Have you tried any other versions of NVDA? If so, please report their behaviors.
No.



Discussion:
This bug is almost definitely in the bounds checking code in `consoleUIATextInfo.move`, as it does not occur when running #9735 with review unbounded. Cc @feerrenrut @leonardder 

Resolving patch:
diff --git a/source/NVDAObjects/UIA/winConsoleUIA.py b/source/NVDAObjects/UIA/winConsoleUIA.py
--- a/source/NVDAObjects/UIA/winConsoleUIA.py
+++ b/source/NVDAObjects/UIA/winConsoleUIA.py
@@ -9,6 +9,8 @@
 import textInfos
 import UIAHandler
 
+from comtypes import COMError
+from UIAUtils import isTextRangeOffscreen
 from winVersion import isWin10
 from . import UIATextInfo
 from ..behaviors import KeyboardHandlerBasedTypedCharSupport
@@ -22,6 +24,21 @@ class consoleUIATextInfo(UIATextInfo):
 	#: to do much good either.
 	_expandCollapseBeforeReview = False
 
+	def __init__(self,obj,position,_rangeObj=None):
+		super(consoleUIATextInfo, self).__init__(obj, position, _rangeObj)
+		# Re-implement POSITION_FIRST and POSITION_LAST in terms of
+		# visible ranges to fix review top/bottom scripts.
+		if position==textInfos.POSITION_FIRST:
+			visiRanges = self.obj.UIATextPattern.GetVisibleRanges()
+			firstVisiRange = visiRanges.GetElement(0)
+			self._rangeObj = firstVisiRange
+			self.collapse()
+		elif position==textInfos.POSITION_LAST:
+			visiRanges = self.obj.UIATextPattern.GetVisibleRanges()
+			lastVisiRange = visiRanges.GetElement(visiRanges.length - 1)
+			self._rangeObj = lastVisiRange
+			self.collapse(True)
+
 	def collapse(self,end=False):
 		"""Works around a UIA bug on Windows 10 1903 and later."""
 		if not isWin10(1903):
@@ -46,8 +63,6 @@ def move(self, unit, direction, endPoint=None):
 			visiRanges = self.obj.UIATextPattern.GetVisibleRanges()
 			visiLength = visiRanges.length
 			if visiLength > 0:
-				firstVisiRange = visiRanges.GetElement(0)
-				lastVisiRange = visiRanges.GetElement(visiLength - 1)
 				oldRange = self._rangeObj.clone()
 		if unit == textInfos.UNIT_WORD and direction != 0:
 			# UIA doesn't implement word movement, so we need to do it manually.
@@ -90,7 +105,6 @@ def move(self, unit, direction, endPoint=None):
 					lineInfo.expand(textInfos.UNIT_LINE)
 					offset = self._getCurrentOffsetInThisLine(lineInfo)
 				# Finally using the new offset,
-
 				# Calculate the current word offsets and move to the start of
 				# this word if we are not already there.
 				start, end = self._getWordOffsetsInThisLine(offset, lineInfo)
@@ -104,15 +118,16 @@ def move(self, unit, direction, endPoint=None):
 		else:  # moving by a unit other than word
 			res = super(consoleUIATextInfo, self).move(unit, direction,
 														endPoint)
-		if oldRange and (
-			self._rangeObj.CompareEndPoints(
-				UIAHandler.TextPatternRangeEndpoint_Start, firstVisiRange,
-				UIAHandler.TextPatternRangeEndpoint_Start) < 0
-			or self._rangeObj.CompareEndPoints(
-				UIAHandler.TextPatternRangeEndpoint_Start, lastVisiRange,
-				UIAHandler.TextPatternRangeEndpoint_End) >= 0):
-			self._rangeObj = oldRange
-			return 0
+		try:
+			if (
+				oldRange
+				and isTextRangeOffscreen(self._rangeObj, visiRanges)
+				and not isTextRangeOffscreen(oldRange, visiRanges)
+			):
+				self._rangeObj = oldRange
+				return 0
+		except (COMError, RuntimeError):
+			pass
 		return res
 
 	def expand(self, unit):
@@ -236,9 +251,12 @@ def _get_caretMovementDetectionUsesEvents(self):
 
 	def _getTextLines(self):
 		# Filter out extraneous empty lines from UIA
-		ptr = self.UIATextPattern.GetVisibleRanges()
-		res = [ptr.GetElement(i).GetText(-1) for i in range(ptr.length)]
-		return res
+		return (
+			self.makeTextInfo(textInfos.POSITION_ALL)
+			._rangeObj.getText(-1)
+			.rstrip()
+			.split("\r\n")
+		)
 
 
 def findExtraOverlayClasses(obj, clsList):
diff --git a/source/UIAUtils.py b/source/UIAUtils.py
--- a/source/UIAUtils.py
+++ b/source/UIAUtils.py
@@ -172,6 +172,23 @@ def getChildrenWithCacheFromUIATextRange(textRange,cacheRequest):
 	c=CacheableUIAElementArray(c)
 	return c
 
+def isTextRangeOffscreen(textRange, visiRanges):
+	"""Given a UIA text range and a visible textRanges array (returned from obj.UIATextPattern.GetVisibleRanges), determines if the given textRange is not within the visible textRanges."""
+	visiLength = visiRanges.length
+	if visiLength > 0:
+		firstVisiRange = visiRanges.GetElement(0)
+		lastVisiRange = visiRanges.GetElement(visiLength - 1)
+		return textRange.CompareEndPoints(
+			UIAHandler.TextPatternRangeEndpoint_Start, firstVisiRange,
+			UIAHandler.TextPatternRangeEndpoint_Start
+		) < 0 or textRange.CompareEndPoints(
+			UIAHandler.TextPatternRangeEndpoint_Start, lastVisiRange,
+			UIAHandler.TextPatternRangeEndpoint_End) >= 0
+	else:
+		# Visible textRanges not available.
+		raise RuntimeError("Visible textRanges array is empty or invalid.")
+
+
 class UIATextRangeAttributeValueFetcher(object):
 
 	def __init__(self,textRange):


Test patch: