Repository: nvaccess/nvda

Issue:
Select-all Hotkey does not always select entire web document
When using any application utilising Browse Mode, including Thunderbird, Internet Explorer V11.576, or Firefox 51.0.1 (32-bit) on Windows 10 64 Bit and NVDA 2017.1 (installed), the behaviour of the select-all keystroke is intermittent depending on the location of the caret. Issuing a Control-A keystroke at the top or bottom of a web-page will issue the correct behaviour and select the entire page. If the caret moves one or more lines, but less than approximately 550 characters, control-A will select only from the start of page to the cursor location. If the caret is at the end of the page and control-A is issued, then the entire page is selected which is expected behaviour.


Discussion:
Confirmed.

Probably a regression introduced by 62f42c61.

Resolving patch:
diff --git a/source/cursorManager.py b/source/cursorManager.py
--- a/source/cursorManager.py
+++ b/source/cursorManager.py
@@ -1,6 +1,6 @@
 #cursorManager.py
 #A part of NonVisual Desktop Access (NVDA)
-#Copyright (C) 2006-2016 NV Access Limited, Joseph Lee, Derek Riemer
+#Copyright (C) 2006-2017 NV Access Limited, Joseph Lee, Derek Riemer
 #This file is covered by the GNU General Public License.
 #See the file COPYING for more details.
 
@@ -256,7 +256,11 @@ def _selectionMovementScriptHelper(self,unit=None,direction=None,toPosition=None
 			newInfo.move(unit, direction, endPoint="start" if direction < 0 else "end")
 			# Collapse this so we don't have to worry about which endpoint we used here.
 			newInfo.collapse(end=direction > 0)
-		if self._lastSelectionMovedStart:
+		# If we're selecting all, we're moving both endpoints.
+		# Otherwise, newInfo is the collapsed new active endpoint
+		# and we need to set the anchor endpoint.
+		movingSingleEndpoint = toPosition != textInfos.POSITION_ALL
+		if movingSingleEndpoint and self._lastSelectionMovedStart:
 			if newInfo.compareEndPoints(oldInfo, "startToEnd") > 0:
 				# We were selecting backwards, but now we're selecting forwards.
 				# For example:
@@ -264,13 +268,14 @@ def _selectionMovementScriptHelper(self,unit=None,direction=None,toPosition=None
 				# 2. Shift+leftArrow: selection (0, 1)
 				# 3. Shift+control+rightArrow: next word at 3, so selection (1, 3)
 				newInfo.setEndPoint(oldInfo, "startToEnd")
+				self._lastSelectionMovedStart = False
 			else:
 				# We're selecting backwards.
 				# For example:
 				# 1. Caret at 1; selection (1, 1)
 				# 2. Shift+leftArrow: selection (0, 1)
 				newInfo.setEndPoint(oldInfo, "endToEnd")
-		else:
+		elif movingSingleEndpoint:
 			if newInfo.compareEndPoints(oldInfo, "startToStart") < 0:
 				# We were selecting forwards, but now we're selecting backwards.
 				# For example:
@@ -278,6 +283,7 @@ def _selectionMovementScriptHelper(self,unit=None,direction=None,toPosition=None
 				# 2. Shift+rightArrow: selection (1, 2)
 				# 3. Shift+control+leftArrow: previous word at 0, so selection (0, 1)
 				newInfo.setEndPoint(oldInfo, "endToStart")
+				self._lastSelectionMovedStart = True
 			else:
 				# We're selecting forwards.
 				# For example:
@@ -318,11 +324,13 @@ def script_selectParagraph_back(self, gesture):
 		self._selectionMovementScriptHelper(unit=textInfos.UNIT_PARAGRAPH, direction=-1)
 
 	def script_selectToBeginningOfLine(self,gesture):
-		curInfo=self.makeTextInfo(textInfos.POSITION_SELECTION)
-		curInfo.collapse()
-		tempInfo=curInfo.copy()
-		tempInfo.expand(textInfos.UNIT_LINE)
-		if curInfo.compareEndPoints(tempInfo,"startToStart")>0:
+		# Make sure the active endpoint of the selection is after the start of the line.
+		sel=self.makeTextInfo(textInfos.POSITION_SELECTION)
+		line=sel.copy()
+		line.collapse()
+		line.expand(textInfos.UNIT_LINE)
+		compOp="startToStart" if self._lastSelectionMovedStart else "endToStart"
+		if sel.compareEndPoints(line,compOp)>0:
 			self._selectionMovementScriptHelper(unit=textInfos.UNIT_LINE,direction=-1)
 
 	def script_selectToEndOfLine(self,gesture):


Test patch:
diff --git a/tests/unit/test_cursorManager.py b/tests/unit/test_cursorManager.py
--- a/tests/unit/test_cursorManager.py
+++ b/tests/unit/test_cursorManager.py
@@ -29,27 +29,55 @@ def test_prevChar(self):
 
 class TestSelection(unittest.TestCase):
 
-	def test_selectNextChar(self):
+	def test_selForward(self):
 		cm = CursorManager(text="abc") # Caret at "a"
 		cm.script_selectCharacter_forward(None)
 		self.assertEqual(cm.selectionOffsets, (0, 1)) # "a" selected
 
-	def test_selectPrevChar(self):
+	def test_selBackward(self):
+		"""Same as test_selForward, but with reversed direction.
+		"""
 		cm = CursorManager(text="abc", selection=(1, 1)) # Caret at "b"
 		cm.script_selectCharacter_back(None)
 		self.assertEqual(cm.selectionOffsets, (0, 1)) # "a" selected
 
-	def test_unselectPrevChar(self):
-		"""Depends on behavior tested by test_selectNextChar.
+	def test_selForwardThenUnsel(self):
+		"""Depends on behavior tested by test_selForward.
 		"""
 		cm = CursorManager(text="abc") # Caret at "a"
 		cm.script_selectCharacter_forward(None) # "a" selected
 		cm.script_selectCharacter_back(None) # "a" unselected
 		self.assertEqual(cm.selectionOffsets, (0, 0)) # Caret at "a", no selection
 
+	def test_selBackwardThenUnsel(self):
+		"""Depends on behavior tested by test_selBackward.
+		Same as test_selForwardThenUnsel, but with reversed directions.
+		"""
+		cm = CursorManager(text="abc", selection=(1, 1)) # Caret at "b"
+		cm.script_selectCharacter_back(None) # "a" selected
+		cm.script_selectCharacter_forward(None) # "a" unselected
+		self.assertEqual(cm.selectionOffsets, (1, 1)) # Caret at "b", no selection
+
+	def test_selForwardTwice(self):
+		"""Depends on behavior tested in test_selForward.
+		"""
+		cm = CursorManager(text="abc") # Caret at "a"
+		cm.script_selectCharacter_forward(None) # "a" selected
+		cm.script_selectCharacter_forward(None) # "b" selected
+		self.assertEqual(cm.selectionOffsets, (0, 2)) # "ab" selected
+
+	def test_selBackwardTwice(self):
+		"""Depends on behavior tested in test_selBackward.
+		Same as test_selForwardTwice, but with reversed directions.
+		"""
+		cm = CursorManager(text="abc", selection=(2, 2)) # Caret at "c"
+		cm.script_selectCharacter_back(None) # "b" selected
+		cm.script_selectCharacter_back(None) # "a" selected
+		self.assertEqual(cm.selectionOffsets, (0, 2)) # "ab" selected
+
 	def test_selForwardThenUnselThenSelBackward(self):
 		"""Test selecting forward, then unselecting and selecting backward.
-		Depends on behavior tested by test_unselectPrevChar.
+		Depends on behavior tested by test_selForwardThenUnsel.
 		"""
 		cm = CursorManager(text="abc", selection=(1, 1)) # Caret at "b"
 		cm.script_selectCharacter_forward(None) # "b" selected
@@ -57,15 +85,101 @@ def test_selForwardThenUnselThenSelBackward(self):
 		cm.script_selectCharacter_back(None)
 		self.assertEqual(cm.selectionOffsets, (0, 1)) # "a" selected
 
-	def test_selectForwardThenSelBackward(self):
+	def test_selBackwardThenUnselThenSelForward(self):
+		"""Test selecting backward, then unselecting and selecting forward.
+		Depends on behavior tested by test_selBackwardThenUnsel.
+		Same as test_selForwardThenUnselThenSelBackward, but with reversed directions.
+		"""
+		cm = CursorManager(text="abc", selection=(1, 1)) # Caret at "b"
+		cm.script_selectCharacter_back(None) # "a" selected
+		cm.script_selectCharacter_forward(None) # "a" unselected, caret at "b"
+		cm.script_selectCharacter_forward(None)
+		self.assertEqual(cm.selectionOffsets, (1, 2)) # "b" selected
+
+	def test_selForwardThenSelBackward(self):
 		"""Test selecting forward, then selecting backward without unselecting.
-		Depends on behavior tested by test_selectNextChar.
+		Depends on behavior tested by test_selForward.
 		"""
 		cm = CursorManager(text="abc", selection=(1, 1)) # Caret at "b"
 		cm.script_selectCharacter_forward(None) # "b" selected
 		cm.script_selectWord_back(None) # "b" unselected, "a" selected
 		self.assertEqual(cm.selectionOffsets, (0, 1)) # "a" selected
 
+	def test_selBackwardThenSelForward(self):
+		"""Test selecting backward, then selecting forward without unselecting.
+		Same as test_selForwardThenSelBackward, but with reversed directions.
+		"""
+		cm = CursorManager(text="abc", selection=(2, 2)) # Caret at "c"
+		cm.script_selectCharacter_back(None) # "b" selected
+		cm.script_selectWord_forward(None) # "b" unselected, "c" selected
+		self.assertEqual(cm.selectionOffsets, (2, 3)) # "c" selected
+
+	def test_selForwardThenSelBackwardThenUnsel(self):
+		"""Test selecting forward, then selecting backward without unselecting, then unselecting forward.
+		Depends on behavior tested by test_selForwardThenSelBackward.
+		"""
+		cm = CursorManager(text="abc", selection=(1, 1)) # Caret at "b"
+		cm.script_selectCharacter_forward(None) # "b" selected
+		cm.script_selectWord_back(None) # "b" unselected, "a" selected
+		cm.script_selectCharacter_forward(None) # "a" unselected
+		self.assertEqual(cm.selectionOffsets, (1, 1)) # Caret at "b", no selection
+
+	def test_selBackwardThenSelForwardThenUnsel(self):
+		"""Test selecting backward, then selecting forward without unselecting, then unselecting backward.
+		Same as test_selForwardThenSelBackwardThenUnsel, but with reversed directions.
+		Depends on behavior tested by test_selBackwardThenSelForward.
+		"""
+		cm = CursorManager(text="abc", selection=(2, 2)) # Caret at "c"
+		cm.script_selectCharacter_back(None) # "b" selected
+		cm.script_selectWord_forward(None) # "b" unselected, "c" selected
+		cm.script_selectCharacter_back(None) # "c" unselected
+		self.assertEqual(cm.selectionOffsets, (2, 2)) # Caret at "c", no selection
+
+	def test_selToBottom(self):
+		cm = CursorManager(text="abc", selection=(1, 1)) # Caret at "b"
+		cm.script_selectToBottomOfDocument(None)
+		self.assertEqual(cm.selectionOffsets, (1, 3)) # "bc" selected
+
+	def test_selToTop(self):
+		cm = CursorManager(text="abc", selection=(2, 2)) # Caret at "c"
+		cm.script_selectToTopOfDocument(None)
+		self.assertEqual(cm.selectionOffsets, (0, 2)) # "ab" selected
+
+	def test_selToEndOfLine(self):
+		cm = CursorManager(text="ab\ncd", selection=(1, 1)) # Caret at "b"
+		cm.script_selectToEndOfLine(None)
+		self.assertEqual(cm.selectionOffsets, (1, 3)) # "b\n" selected
+
+	def test_selToBeginningOfLine(self):
+		cm = CursorManager(text="ab\ncd", selection=(4, 4)) # Caret at "d"
+		cm.script_selectToBeginningOfLine(None)
+		self.assertEqual(cm.selectionOffsets, (3, 4)) # "c" selected
+
+	def test_selToBeginningOfLineAtBeginning(self):
+		"""Test selecting to the beginning of the line when the caret is already at the beginning of the line.
+		In this case, nothing should happen.
+		"""
+		cm = CursorManager(text="ab\ncd", selection=(3, 3)) # Caret at "c"
+		cm.script_selectToBeginningOfLine(None)
+		self.assertEqual(cm.selectionOffsets, (3, 3)) # No selection
+
+	def test_selForwardThenSelToBeginningOfLine(self):
+		"""Depends on behavior tested by test_selForward.
+		"""
+		cm = CursorManager(text="ab\ncd", selection=(3, 3)) # Caret at "c"
+		cm.script_selectCharacter_forward(None) # "c" selected
+		cm.script_selectToBeginningOfLine(None) # "c" unselected
+		self.assertEqual(cm.selectionOffsets, (3, 3)) # Caret at "c", no selection
+
+	def test_selToEndThenBeginningOfLine(self):
+		"""Test for #5746.
+		Depends on behavior tested in test_selToEndOfLine and test_selToBeginningOfLine.
+		"""
+		cm = CursorManager(text="ab") # Caret at "a"
+		cm.script_selectToEndOfLine(None)
+		cm.script_selectToBeginningOfLine(None)
+		self.assertEqual(cm.selectionOffsets, (0, 0)) # Caret at "a", no selection
+
 class TestSelectAll(unittest.TestCase):
 	"""Tests the select all command starting from different caret positions.
 	"""
@@ -79,3 +193,9 @@ def _selectAllTest(self, caret):
 
 	def test_selectAllFromStart(self):
 		self._selectAllTest(0) # Caret at "a"
+
+	def test_selectAllFromMiddle(self):
+		self._selectAllTest(1) # Caret at "b"
+
+	def test_selectAllFromEnd(self):
+		self._selectAllTest(2) # Caret at "c"