Repository: nvaccess/nvda

Issue:
UIA in Windows Console: Space Character is not Announced when Deleting Characters
### Steps to reproduce:
Open the command prompt and type a command, such as help help, and press backspace to delete the characters.
### Actual behavior:
The space character is not announced when deleting characters, and the character to the left is announced instead of the space.
### Expected behavior:
The space character should be announced.
### System configuration
#### NVDA installed/portable/running from source:
Installed.
#### NVDA version:
alpha-17373,98dc8278
#### Windows version:
Windows 10, Version	10.0.18898 Build 18898
#### Name and version of other software in use when reproducing the issue:
Windows Command Prompt.
#### Other information about your system:
N/a.
### 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 is probably related to the caret movement issues on 1903. CC @michaelDCurran 

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
@@ -21,19 +21,25 @@
 
 class consoleUIATextInfo(UIATextInfo):
 	#: At least on Windows 10 1903, expanding then collapsing the text info
-	#: causes review to get stuck, so disable it.
+	#: caused review to get stuck, so disable it.
+	#: There may be no need to disable this anymore, but doing so doesn't seem
+	#: to do much good either.
 	_expandCollapseBeforeReview = False
 
-	def __init__(self, obj, position, _rangeObj=None):
-		super(consoleUIATextInfo, self).__init__(obj, position, _rangeObj)
-		if position == textInfos.POSITION_CARET and isWin10(1903, atLeast=False):
-			# The UIA implementation in 1903 causes the caret to be
-			# off-by-one, so move it one position to the right
-			# to compensate.
-			self._rangeObj.MoveEndpointByUnit(
+	def collapse(self,end=False):
+		"""Works around a UIA bug on Windows 10 1903 and later."""
+		if not isWin10(1903):
+			return super(consoleUIATextInfo, self).collapse(end=end)
+		# When collapsing, consoles seem to incorrectly push the start of the
+		# textRange back one character.
+		# Correct this by bringing the start back up to where the end is.
+		oldInfo=self.copy()
+		super(consoleUIATextInfo,self).collapse()
+		if not end:
+			self._rangeObj.MoveEndpointByRange(
 				UIAHandler.TextPatternRangeEndpoint_Start,
-				UIAHandler.NVDAUnitsToUIAUnits[textInfos.UNIT_CHARACTER],
-				1
+				oldInfo._rangeObj,
+				UIAHandler.TextPatternRangeEndpoint_Start
 			)
 
 	def move(self, unit, direction, endPoint=None):
@@ -139,6 +145,17 @@ def expand(self, unit):
 		else:
 			return super(consoleUIATextInfo, self).expand(unit)
 
+	def _get_isCollapsed(self):
+		"""Works around a UIA bug on Windows 10 1903 and later."""
+		if not isWin10(1903):
+			return super(consoleUIATextInfo, self)._get_isCollapsed()
+		# Even when a console textRange's start and end have been moved to the
+		# same position, the console incorrectly reports the end as being
+		# past the start.
+		# Therefore to decide if the textRange is collapsed,
+		# Check if it has no text.
+		return not bool(self._rangeObj.getText(1))
+
 	def _getCurrentOffsetInThisLine(self, lineInfo):
 		"""
 		Given a caret textInfo expanded to line, returns the index into the
@@ -187,6 +204,10 @@ def _getWordOffsetsInThisLine(self, offset, lineInfo):
 			min(end.value, max(1, len(lineText) - 2))
 		)
 
+	def __ne__(self,other):
+		"""Support more accurate caret move detection."""
+		return not self==other
+
 
 class consoleUIAWindow(Window):
 	def _get_focusRedirect(self):
@@ -215,6 +236,8 @@ class WinConsoleUIA(Terminal):
 	#: Whether the console got new text lines in its last update.
 	#: Used to determine if typed character/word buffers should be flushed.
 	_hasNewLines = False
+	#: the caret in consoles can take a while to move on Windows 10 1903 and later.
+	_caretMovementTimeoutMultiplier = 1.5
 
 	def _reportNewText(self, line):
 		# Additional typed character filtering beyond that in LiveText
diff --git a/source/editableText.py b/source/editableText.py
--- a/source/editableText.py
+++ b/source/editableText.py
@@ -48,6 +48,8 @@ class EditableText(TextContainerObject,ScriptableObject):
 
 	_hasCaretMoved_minWordTimeoutMs=30 #: The minimum amount of time that should elapse before checking if the word under the caret has changed
 
+	_caretMovementTimeoutMultiplier = 1
+	
 	def _hasCaretMoved(self, bookmark, retryInterval=0.01, timeout=None, origWord=None):
 		"""
 		Waits for the caret to move, for a timeout to elapse, or for a new focus event or script to be queued.
@@ -69,6 +71,7 @@ def _hasCaretMoved(self, bookmark, retryInterval=0.01, timeout=None, origWord=No
 		else:
 			# This function's arguments are in seconds, but we want ms.
 			timeoutMs = timeout * 1000
+		timeoutMs *= self._caretMovementTimeoutMultiplier
 		# time.sleep accepts seconds, so retryInterval is in seconds.
 		# Convert to integer ms to avoid floating point precision errors when adding to elapsed.
 		retryMs = int(retryInterval * 1000)


Test patch: