Repository: nvaccess/nvda

Issue:
Focus issues reading by paragrph in Google docs involving blank lines
<!-- Please read the text in this edit field before filling it in.
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/blob/master/devDocs/githubIssueTemplateExplanationAndExamples.md
Please also note that the NVDA project has a Citizen and Contributor Code of Conduct which can be found at https://github.com/nvaccess/nvda/blob/master/CODE_OF_CONDUCT.md. NV Access expects that all contributors and other community members read and abide by the rules set out in this document while participating or contributing to this project. This includes creating or commenting on issues and pull requests. 

Each of the questions and sections below start with multiple hash symbols (#). Place your answers and information on the blank line below each question.
-->

### Steps to reproduce:
1. Make a google doc.
2. Write some words, press enter twice, write something else (and continue pattern as desired for testing).
3. Move around using the previous and next paragraph commands (control+up and control+down)
### Actual behavior:
NVDA announces "blank" not only on the blank lines but on the lines of text as well.
The issue has something to do with the blank lines because if there's two lines of text next to each other,, or a line of text at the bottom of the doc with no blank line underneath, it's announced.
### Expected behavior:
NVDA should focus correctly to read the text.
### System configuration
#### NVDA installed/portable/running from source:
Installed
#### NVDA version:
Version: alpha-23589,c9d6729b

#### Windows version:
Windows 10 Home
Version	21H1
OS build	19043.1165

#### Name and version of other software in use when reproducing the issue:
Chrome Version 92.0.4515.159 (Official Build) (64-bit)
#### Other information about your system:

### Other questions
#### Does the issue still occur after restarting your computer?
Yes
#### Have you tried any other versions of NVDA? If so, please report their behaviors.
This started recently. Whether as an NVDA update or Chrome update I'm not sure. the reverse used to happen, in fact - paragraphs would double-read on the blank lines.
#### If add-ons are disabled, is your problem still occurring?
Yes
#### Does the issue still occur after you run the COM Registration Fixing Tool in NVDA's tools menu?
Yes


Discussion:
I can reproduce this in the latest alpha `nvda_snapshot_alpha-23598,a4524704` but not in `2021.2 beta 1`. This indicates it is caused by a change in NVDA. @michaelDCurran do you have any suggestions here?
Running a bisect, it looks like it broke in fab35ec3c03038b22c9c288d78520aa03d4f2155.
Report aria-description always pr #12500


Resolving patch:
diff --git a/source/NVDAObjects/IAccessible/ia2TextMozilla.py b/source/NVDAObjects/IAccessible/ia2TextMozilla.py
--- a/source/NVDAObjects/IAccessible/ia2TextMozilla.py
+++ b/source/NVDAObjects/IAccessible/ia2TextMozilla.py
@@ -65,8 +65,8 @@ def _getEmbedded(obj, offset) -> typing.Optional[IAccessible]:
 
 class MozillaCompoundTextInfo(CompoundTextInfo):
 
-	def _getControlFieldForObject(self, obj):
-		controlField = super()._getControlFieldForObject(obj)
+	def _getControlFieldForObject(self, obj, ignoreEditableText=True):
+		controlField = super()._getControlFieldForObject(obj, ignoreEditableText=ignoreEditableText)
 		if controlField is None:
 			return None
 		# Set the uniqueID of the controlField if we can get one
@@ -267,13 +267,7 @@ def _iterRecursiveText(self, ti: offsets.OffsetsTextInfo, controlStack, formatCo
 			elif isinstance(item, int): # Embedded object.
 				embedded: typing.Optional[IAccessible] = _getEmbedded(ti.obj, item)
 				notText = _getRawTextInfo(embedded) is NVDAObjectTextInfo
-				if (
-					controlStack is not None
-					and not (
-						self._isObjectEditableText(embedded)
-						or self._isNamedlinkDestination(embedded)
-					)
-				):
+				if controlStack is not None:
 					controlField = self._getControlFieldForObject(embedded)
 					controlStack.append(controlField)
 					if controlField:
@@ -319,11 +313,8 @@ def _getText(self, withFields, formatConfig=None):
 			ti = self._start
 			cannotBeStart = False
 			while obj and obj != rootObj:
-				if not (
-					self._isObjectEditableText(obj)
-					or self._isNamedlinkDestination(obj)
-				):
-					field = self._getControlFieldForObject(obj)
+				field = self._getControlFieldForObject(obj)
+				if field:
 					if ti._startOffset == 0:
 						if not cannotBeStart:
 							field["_startOfNode"] = True
@@ -331,7 +322,7 @@ def _getText(self, withFields, formatConfig=None):
 						# We're not at the start of this object, which also means we're not at the start of any ancestors.
 						cannotBeStart = True
 					fields.insert(0, textInfos.FieldCommand("controlStart", field))
-					controlStack.insert(0, field)
+				controlStack.insert(0, field)
 				ti = self._getEmbedding(obj)
 				if not ti:
 					log.debugWarning(
@@ -350,7 +341,7 @@ def _getText(self, withFields, formatConfig=None):
 			# (We already handled collapsed above.)
 			return fields
 		obj = self._startObj
-		while fields[-1] is not None and controlStack:
+		while fields[-1] is not None:
 			# The end hasn't yet been reached, which means it isn't a descendant of obj.
 			# Therefore, continue from where obj was embedded.
 			if withFields:
@@ -363,9 +354,10 @@ def _getText(self, withFields, formatConfig=None):
 					# range is invalid, so just return nothing.
 					log.debugWarning("Tried to walk up past the root. Objects probably dead.")
 					return []
-				# This object had a control field.
-				field["_endOfNode"] = True
-				fields.append(textInfos.FieldCommand("controlEnd", None))
+				if field:
+					# This object had a control field.
+					field["_endOfNode"] = True
+					fields.append(textInfos.FieldCommand("controlEnd", None))
 			ti = self._getEmbedding(obj)
 			if not ti:
 				log.debugWarning(
@@ -385,11 +377,13 @@ def _getText(self, withFields, formatConfig=None):
 			# Determine whether the range covers the end of any ancestors of endObj.
 			obj = self._endObj
 			ti = self._end
-			while obj and obj != rootObj and controlStack:
+			while obj and obj != rootObj:
 				field = controlStack.pop()
-				fields.append(textInfos.FieldCommand("controlEnd", None))
+				if field:
+					fields.append(textInfos.FieldCommand("controlEnd", None))
 				if ti.compareEndPoints(self._makeRawTextInfo(obj, textInfos.POSITION_ALL), "endToEnd") == 0:
-					field["_endOfNode"] = True
+					if field:
+						field["_endOfNode"] = True
 				else:
 					# We're not at the end of this object, which also means we're not at the end of any ancestors.
 					break
diff --git a/source/appModules/kindle.py b/source/appModules/kindle.py
--- a/source/appModules/kindle.py
+++ b/source/appModules/kindle.py
@@ -346,8 +346,8 @@ def updateSelection(self):
 		log.debug("Setting selection to (%d, %d)" % (sel._startOffset, sel._endOffset))
 		sel.updateSelection()
 
-	def _getControlFieldForObject(self, obj):
-		field = super(BookPageViewTextInfo, self)._getControlFieldForObject(obj)
+	def _getControlFieldForObject(self, obj, ignoreEditableText=True):
+		field = super(BookPageViewTextInfo, self)._getControlFieldForObject(obj, ignoreEditableText=ignoreEditableText)
 		if field and field["role"] == controlTypes.Role.MATH:
 			try:
 				field["mathMl"] = obj.mathMl
diff --git a/source/compoundDocuments.py b/source/compoundDocuments.py
--- a/source/compoundDocuments.py
+++ b/source/compoundDocuments.py
@@ -136,9 +136,15 @@ def _isNamedlinkDestination(self, obj: NVDAObject) -> bool:
 			and controlTypes.State.LINKED not in obj.states
 		)
 
-	def _getControlFieldForObject(self, obj: NVDAObject):
+	def _getControlFieldForObject(self, obj, ignoreEditableText=True):
+		if ignoreEditableText and self._isObjectEditableText(obj):
+			# This is basically just a text node.
+			return None
 		role = obj.role
 		states = obj.states
+		if role == controlTypes.Role.LINK and controlTypes.State.LINKED not in states:
+			# Named link destination, not a link that can be activated.
+			return None
 		field = textInfos.ControlField()
 		field["role"] = role
 		field['roleText'] = obj.roleText
@@ -271,11 +277,8 @@ def getTextWithFields(self, formatConfig=None):
 		rootObj = self.obj.rootNVDAObject
 		obj = self._startObj
 		while obj and obj != rootObj:
-			if not (
-				self._isObjectEditableText(obj)
-				or self._isNamedlinkDestination(obj)
-			):
-				field = self._getControlFieldForObject(obj)
+			field = self._getControlFieldForObject(obj)
+			if field:
 				fields.insert(0, textInfos.FieldCommand("controlStart", field))
 			obj = obj.parent
 
@@ -287,23 +290,16 @@ def getTextWithFields(self, formatConfig=None):
 						embedIndex = self._getFirstEmbedIndex(ti)
 					else:
 						embedIndex += 1
-					childObject: NVDAObject = ti.obj.getChild(embedIndex)
-					if not (
-						# Don't check for self._isObjectEditableText
-						# Only for named link destinations.
-						self._isNamedlinkDestination(obj)
-					):
-						controlField = self._getControlFieldForObject(childObject)
-						controlField["content"] = childObject.name
-						fields.extend((
-							textInfos.FieldCommand("controlStart", controlField),
-							textUtils.OBJ_REPLACEMENT_CHAR,
-							textInfos.FieldCommand("controlEnd", None),
-						))
-				else:  # str or fieldCommand
-					if not isinstance(textWithEmbeddedObjectsItem, (str, textInfos.FieldCommand)):
-						log.error(f"Unexpected type: {textWithEmbeddedObjectsItem!r}")
-					fields.append(textWithEmbeddedObjectsItem)
+					field = ti.obj.getChild(embedIndex)
+					controlField = self._getControlFieldForObject(field, ignoreEditableText=False)
+					controlField["content"] = field.name
+					fields.extend((
+						textInfos.FieldCommand("controlStart", controlField),
+						textUtils.OBJ_REPLACEMENT_CHAR,
+						textInfos.FieldCommand("controlEnd", None)
+					))
+				else:
+					fields.append(field)
 		return fields
 
 	def _findNextContent(self, origin, moveBack=False):


Test patch: