Repository: nvaccess/nvda

Issue:
Tab behaves strangely when not being used for completion in Python Console
I am not sure what the tab is meant to do in the Python Console.  However, since I am used to using it for indentation, I do, and it normally works fine for this purpose.
However, I recently had occasion to need to indent some code I had written further than it previously was.
I can just use space, but by default, I am used to using tab.

### Steps to reproduce:

1.  Press keys: NVDA+n, t, p, to open the python console.
2.  Type some code such as this:
    ```python
    print("Some test code.")
    ```
3.  That code runs fine. But now, let's say you need to put that code in a function. Obviously this example is easy to just retype, but imagine if it was 10 lines of very complex code, and you now wanted to encapsulate it in a function. First, enter:
    ```python
    def testFunc(parameter):
    ```
4.  So far, so good. But now, up arrow and try to include your code from before. You can't just accept the print statement as entered, because it isn't indented. You can press home to get to the start of the line, and then press tab to indent it.

### Actual behavior:

When you press tab, the cursor jumps back to the end of the line, and **then** the tab is inserted.
So the tab appears at the end of the line, instead of at the beginning where it was expected.

### Expected behavior:

If a tab is going to show up anywhere in this situation, I would expect it to be at the beginning of the line (I.E. where the cursor is.).
A tab on a blank line, produces a tab character at the start of that line. I would expect a tab at the start of any line to do the same, since there is no completion context for tab to operate on.

### System configuration

NVDA portable alpha-20793,9a4074bc.
Windows 10 2004.



Discussion:
cc: @feerrenrut, @JulienCochuyt 


Resolving patch:
diff --git a/source/pythonConsole.py b/source/pythonConsole.py
--- a/source/pythonConsole.py
+++ b/source/pythonConsole.py
@@ -306,9 +306,11 @@ def historyMove(self, movement):
 		return True
 
 	RE_COMPLETE_UNIT = re.compile(r"[\w.]*$")
+
 	def complete(self):
+		textBeforeCursor = self.inputCtrl.GetRange(0, self.inputCtrl.GetSelection()[0])
 		try:
-			original = self.RE_COMPLETE_UNIT.search(self.inputCtrl.GetValue()).group(0)
+			original = self.RE_COMPLETE_UNIT.search(textBeforeCursor).group(0)
 		except AttributeError:
 			return False
 
@@ -373,16 +375,20 @@ def _insertCompletion(self, original, completed):
 		insert = completed[len(original):]
 		if not insert:
 			return
-		self.inputCtrl.SetValue(self.inputCtrl.GetValue() + insert)
+		inputCtrl = self.inputCtrl
+		selStartPos, selEndPos = inputCtrl.GetSelection()
+		prefix = inputCtrl.GetRange(0, selStartPos)
+		suffix = inputCtrl.GetRange(selEndPos, inputCtrl.GetLastPosition())
+		inputCtrl.SetValue(prefix + insert + suffix)
 		queueHandler.queueFunction(queueHandler.eventQueue, speech.speakText, insert)
-		self.inputCtrl.SetInsertionPointEnd()
+		inputCtrl.SetInsertionPoint(selStartPos + len(insert))
 
 	def onInputChar(self, evt):
 		key = evt.GetKeyCode()
 
 		if key == wx.WXK_TAB:
-			line = self.inputCtrl.GetValue()
-			if line and not line.isspace():
+			textBeforeCursor = self.inputCtrl.GetRange(0, self.inputCtrl.GetSelection()[0])
+			if textBeforeCursor and not textBeforeCursor.isspace():
 				if not self.complete():
 					wx.Bell()
 				return


Test patch: