Repository: nvaccess/nvda

Issue:
When moving by words with text review commands in Scintilla based editors NVDA treats  spaces as separate words
### Steps to reproduce:
1. open a  Scintilla based editor (I've tested with Notepad++ and TeXnicCenter).
2. Write some text
3. Move navigator object to focus.
4. Start moving with Numpad 4 and 6 between words.
### Expected behavior:
NVDA should report only words without spaces between them like in all other cases.

### Actual behavior:
NVDA reports every space as a separate word.

### System configuration:
NVDA version:
next-15133,1f3d46d2

NVDA Installed or portable:
Installed

Windows version:
Windows 8 x64

Name and version of other software in use when reproducing the issue:
Notepad++ v7.5.4 (64-bit), TeXnicCenter 2.02 Stable (64 bit)
### Other questions:

Does the issue still occur after restarting your PC?
Yes
Have you tried any other versions of NVDA?
Yes from 2017,2 onward and it didn't work.



Discussion:
I can confirm this.
It seems that by default space isn't considered a word separator by Scintilla so when calculating word start with SCI_WORDSTARTPOSITION and word end with SCI_WORDENDPOSITION space is a separate word. There is a Scintilla message SCI_SETWHITESPACECHARS which should fix this. I don't know however how to send space using it.
Sending messages to controls in order to changed their behavior is 
honestly not what I'd prefer. If there are implementations that don't 
respect spaces as word separators, this should be covered as part of the 
implementation of that particular control. In Short, if navigating with 
control+arrows doesn't jump across spaces, enabling NVDA shouldn't 
change anything about this.

@leonardder I'm afraid I do not understand your last comment. When navigating with ctrl+arrows the editor determines what is a word separator and what isn't. When working with object review it is an NVDA job to determine this and since in Scintilla the word separators aren't set to space by default when NVDA sends Scintilla messages asking about start and end of given word space is returned.
cc @DataTriny given your recent work on Scintilla.
and also cc: @francipvb 
[**@lukaszgo1**](https://github.com/lukaszgo1) commented on [24 May 2019, 14:02 CEST](https://github.com/nvaccess/nvda/issues/8295#issuecomment-495594616 "2019-05-24T12:02:01Z - Replied by Github Reply Comments"):
> When navigating with ctrl+arrows the editor determines what is a word separator and what isn't. 

This is true.

> When working with object review it is an NVDA job to determine this.

This is not entirely true. When moving to the next word, NVDA asks the TextInfo object for the particular control to move to the next word. How a TextInfo object calculates word boundaries is mostly determined by the underlying TextINfo implementation and thus by the underlying control.

> since in Scintilla the word separators aren't set to space by default when NVDA sends Scintilla messages asking about start and end of given word space is returned.

This actually makes sense. Moving with ctrl+arrows does include spaces, which is inconsistent.
I looked at the code and it looks like NVDA has a certain workaround in it to ensure that word offsets are correct, with the downside of space treated as a separator.
On closer inspection, at least for Notepad++, it is Scintilla that's failing here. Scintilla is definitely treating one space or a group of spaces as a word when using SCI_WORDSTARTPOSITION and SCI_WORDENDPOSITION.
Which is exactly what I am saying. According to this quote from the Scintilla documentation when we want Scintilla not to do that we have to set spaves as word separators ourselves. 
> SCI_SETWHITESPACECHARS(<unused>, const char *characters)
SCI_GETWHITESPACECHARS(<unused>, char *characters) → int
Similar to SCI_SETWORDCHARS, this message allows the user to define which chars Scintilla considers as whitespace. Setting the whitespace chars allows the user to fine-tune Scintilla's behaviour doing such things as moving the cursor to the start or end of a word; for example, by defining punctuation chars as whitespace, they will be skipped over when the user presses ctrl+left or ctrl+right. This function should be called after SCI_SETWORDCHARS as it will reset the whitespace characters to the default set. SCI_GETWHITESPACECHARS behaves similarly to SCI_GETWORDCHARS.


The problem is that the docs say:

> for example, by defining punctuation chars as whitespace, they will be skipped over when the user presses ctrl+left or ctrl+right.

However, the movement behaviour of ctrl+left/rightArrow doesn't match with the offsets returned by SCI_WORDSTARTPOSITION and SCI_WORDENDPOSITION 

Resolving patch:
diff --git a/source/NVDAObjects/window/scintilla.py b/source/NVDAObjects/window/scintilla.py
--- a/source/NVDAObjects/window/scintilla.py
+++ b/source/NVDAObjects/window/scintilla.py
@@ -1,7 +1,8 @@
-#A part of NonVisual Desktop Access (NVDA)
-#Copyright (C) 2009-2019 NV Access Limited, Arnold Loubriat, Babbage B.V.
-#This file is covered by the GNU General Public License.
-#See the file COPYING for more details.
+# -*- coding: UTF-8 -*-
+# A part of NonVisual Desktop Access (NVDA)
+# Copyright (C) 2009-2019 NV Access Limited, Arnold Loubriat, Babbage B.V., Łukasz Golonka
+# This file is covered by the GNU General Public License.
+# See the file COPYING for more details.
 
 import ctypes
 import IAccessibleHandler
@@ -42,6 +43,7 @@
 SCI_POSITIONFROMLINE=2167
 SCI_LINELENGTH=2350
 SCI_GETSTYLEAT=2010
+SCI_GETCHARAT = 2007
 SCI_STYLEGETFONT=2486
 SCI_STYLEGETSIZE=2485
 SCI_STYLEGETBOLD=2483
@@ -59,6 +61,7 @@
 INVALID_POSITION=-1
 STYLE_DEFAULT=32
 SC_CP_UTF8=65001
+space = 32
 
 class CharacterRangeStruct(ctypes.Structure):
 	_fields_=[
@@ -195,6 +198,12 @@ def _getWordOffsets(self,offset):
 		if end<=offset:
 			start=end
 			end=watchdog.cancellableSendMessage(self.obj.windowHandle,SCI_WORDENDPOSITION,offset,0)
+		# #8295: When calculating offsets with Scintilla messages spaces are considered to be words.
+		# Therefore check if character at offset is  a space, and if so calculate it again.
+		if watchdog.cancellableSendMessage(self.obj.windowHandle, SCI_GETCHARAT, end, 0) == space:
+			end = watchdog.cancellableSendMessage(self.obj.windowHandle, SCI_WORDENDPOSITION, end, 0)
+		if watchdog.cancellableSendMessage(self.obj.windowHandle, SCI_GETCHARAT, start, 0) == space:
+			start = watchdog.cancellableSendMessage(self.obj.windowHandle, SCI_WORDSTARTPOSITION, start, 0)
 		return [start,end]
 
 	def _getLineNumFromOffset(self,offset):


Test patch: