Repository: nvaccess/nvda

Issue:
UIA in Word: When document ends with a heading, the heading is duplicated in quick nav
### Steps to reproduce:
1. Open MS Word
2. Create a single line and make it a heading with ctrl+alt+1/3
3. Enable browse mode
4. Press H multiple times.

### Actual behavior:
NVDA keeps reporting the heading. When opening the elements list, NVDA shows the heading twice.

### Expected behavior:
NVDA reports "no next heading". Only one heading is shown in the elements list.

### System configuration
#### NVDA installed/portable/running from source:
Installed

#### NVDA version:
alpha-17134

#### Windows version:
Windows 10 1903 uild 18362.86

#### Name and version of other software in use when reproducing the issue:
Word 365 16.0.11601.20144

### 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:
Is this the same issue that sometimes results in the line above being read a 
couple of times when you press enter to get a new line in older versions of 
Word. Its not too annoying but it sounds like slow removal of what nvda has 
buffered.

 Brian

bglists@blueyonder.co.uk
Sent via blueyonder.
Please address personal E-mail to:-
briang1@blueyonder.co.uk, putting 'Brian Gaff'
in the display name field.
-----  


Confirmed. Requires more investigation.

Resolving patch:
diff --git a/source/UIABrowseMode.py b/source/UIABrowseMode.py
--- a/source/UIABrowseMode.py
+++ b/source/UIABrowseMode.py
@@ -3,6 +3,7 @@
 # See the file COPYING for more details.
 # Copyright (C) 2015-2020 NV Access Limited, Babbage B.V., Accessolutions, Julien Cochuyt
 
+from typing import Optional
 from ctypes import byref
 from comtypes import COMError
 from comtypes.automation import VARIANT
@@ -151,28 +152,41 @@ def isChild(self,parent):
 			return False
 		return self.level>parent.level
 
-def UIAHeadingQuicknavIterator(itemType,document,position,direction="next"):
-	if position:
-		curPosition=position
+
+def UIAHeadingQuicknavIterator(
+		itemType: str,
+		document: "UIABrowseModeDocument",
+		position: Optional["UIABrowseModeDocumentTextInfo"],
+		direction: str = "next"
+):
+	reverse = bool(direction == "previous")
+	entireDocument = document.makeTextInfo(textInfos.POSITION_ALL)
+	if position is None:
+		searchArea = entireDocument
 	else:
-		curPosition=document.makeTextInfo(textInfos.POSITION_LAST if direction=="previous" else textInfos.POSITION_FIRST)
-	stop=False
+		searchArea = position.copy()
+		if reverse:
+			searchArea.start = entireDocument.start
+		else:
+			searchArea.end = entireDocument.end
 	firstLoop=True
-	while not stop:
-		tempInfo=curPosition.copy()
-		tempInfo.expand(textInfos.UNIT_CHARACTER)
-		styleIDValue=getUIATextAttributeValueFromRange(tempInfo._rangeObj,UIAHandler.UIA_StyleIdAttributeId,ignoreMixedValues=True)
+	for subrange in iterUIARangeByUnit(searchArea._rangeObj, UIAHandler.TextUnit_Paragraph, reverse=reverse):
+		if firstLoop:
+			firstLoop = False
+			if position and not reverse:
+				# We are starting to search forward from a specific position
+				# Skip the first subrange as it is the one we started on.
+				continue
+		styleIDValue = subrange.getAttributeValue(UIAHandler.UIA_StyleIdAttributeId)
 		# #9842: styleIDValue can sometimes be a pointer to IUnknown.
 		# In Python 3, comparing an int with a pointer raises a TypeError.
 		if isinstance(styleIDValue, int) and UIAHandler.StyleId_Heading1 <= styleIDValue <= UIAHandler.StyleId_Heading9:
 			foundLevel = (styleIDValue - UIAHandler.StyleId_Heading1) + 1
 			wantedLevel = int(itemType[7:]) if len(itemType) > 7 else None
 			if not wantedLevel or wantedLevel==foundLevel: 
-				if not firstLoop or not position:
-					tempInfo.expand(textInfos.UNIT_PARAGRAPH)
-					yield HeadingUIATextInfoQuickNavItem(itemType,document,tempInfo,level=foundLevel)
-		stop=(curPosition.move(textInfos.UNIT_PARAGRAPH,1 if direction=="next" else -1)==0)
-		firstLoop=False
+				tempInfo = document.makeTextInfo(subrange)
+				yield HeadingUIATextInfoQuickNavItem(itemType, document, tempInfo, level=foundLevel)
+
 
 def UIAControlQuicknavIterator(itemType,document,position,UIACondition,direction="next",itemClass=UIATextRangeQuickNavItem):
 	# A part from the condition given, we must always match on the root of the document so we know when to stop walking


Test patch: