Repository: nvaccess/nvda

Issue:
Indentation is not reported by NVDA in Visual Studio 2022
<!-- 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. Start NVDA.
2. Press insert+control+d or go to NVDA settings/Document formatting settings.
3. Tab to "Line indentation reporting" combo box and change the selection from off to speech or both speech and tone.
4. Open Visual Studio 2022.
5. Create a new project, (Any project type will work).
6. Go to the any file in solution explorer by pressing alt+control+l.
7. Click any of the file.
8. Write some code with different levels of indentation.
9. Navigate via arrow keys and observe.
10. NVDA will read the code, but won't report the indentation, neither via speech, nor via tone.
11. Please hear the attached video for the demo on the issue.

### Actual behavior:
NVDA doesn't report the indentation in Visual Studio 2022
Please hear the attached video for the demo on the issue.

### Expected behavior:
Indentation should be reported as per user's chosen setting.

### System configuration
#### NVDA installed/portable/running from source:
Installed

#### NVDA version:
NVDA version alpha-25106,153f5004

#### Windows version:
Windows 11 22H2 Insiders Build 22581.200

#### Name and version of other software in use when reproducing the issue:
Visual Studio 2022

#### Other information about your system:
N/A

### Other questions
#### Does the issue still occur after restarting your computer?
Yes, always.

#### Have you tried any other versions of NVDA? If so, please report their behaviors.
Happens with all the versions, tried many alpha snapshots as well as stable builds.

#### If NVDA add-ons are disabled, is your problem still occurring?
Yes, always.

#### Does the issue still occur after you run the COM Registration Fixing Tool in NVDA's tools menu?
Yes and it is not applicable.


https://user-images.githubusercontent.com/6381747/161447477-dba5d4f8-fd6a-418b-86e9-9957a5531f55.mp4




Discussion:
Also **NVDA+F** does not say the indentation  level.
Yes, true

> Also *NVDA+F* does not say the indentation level.

@leonardder are you able to reproduce this?
This is a known issue in Visual Studio. You will have to disable line numbers in Visual Studio's settings.

1. Open Visual Studio
2. Go to the tools menu (alt+t)
3. Select options
4. In the categories tree view, expand the text editor node
5. Select all languages
6. Press tab until you reach the option to display line numbers
7. Disable it
8. Press ok to close the options.

@leonardder 
Yeah, I'm aware about that work around actually, but if we disable the line numbers in Visual Studio then it is not possible/easy for us to know the line numbers.
Even the line numbers checkbox inside NVDA document formatting also doesn't have any effect.

That's true, but it really is an issue in Visual Studio we can't work around. UIA has no native support to report line numbers.
> That's true, but it really is an issue in Visual Studio we can't work around. UIA has no native support to report line numbers.


UIA may not have a native way to report line numbers, but the main issue here is that NVDA fails to report indentation when line numbers are enabled. Given that JAWS can (and they're using UIA for the editor control for some time now) closing this issue which clearly is an NVDA one feels wrong to me.
@lukaszgo1  Absolutely, I'm completely with you on this.
Since JAWS is able to do it very well, I think NVDA can also do that.
I agree, may be some extra efforts/work arounds may be needed at NVDA's side,but it is certainly doable.
The only thing which is important is that NVDA should be able to report indentation levels even though the line numbers are  enabled in visual studio.
@leonardder 
I think that issue should be reopened because it really impacts the productivity specially working with python.
I agree that closing this issue was a bit too soon. Let's leave this open to investigate.
Thanks @leonardder  for reopening.
I'm pretty curious how JAWS behaves for a line like this when line numbers are disabled in visual studio:
```1     hello```
In other words, a line that starts with a number, followed by 5 spaces and a word. Will it report no indentation or will it report 4 spaces of indentation for that line?
@leonardder I'm thinking out loud and wanted to share it with you.

The UIA and other Windows APIs eventually send NVDA the text with the focused text (in this case the line number and other code).
With the means at possibility, we can remove the line number from the beginning of the string sent to NVDA.

Here is an example:

```c#
1        Console.WriteLine("text");
```



We will convert String to this:

```c#
        Console.WriteLine("text");
```

Isn't it a viable idea?


The indentation is detected by means of a regular expression. We could implement something that expands that expression with numbers at the start of the line for Visual Studio, but then there's still the issue of what happens if line numbers are off in Visual Studio and a line starts with a number and white space. NV Access has always been very reluctant in accepting fixes like this, since they simply are ugly workarounds that could easily introduce other issues we don't know about beforehand.
That's why I wondered about how JAWS behaves in this case, see https://github.com/nvaccess/nvda/issues/13574#issuecomment-1096447768
Actually, it looks like that a collapsed range at the start of a line will return the line number with a space. We should be able to separate the line number from the text that way.

Resolving patch:
diff --git a/source/appModules/devenv.py b/source/appModules/devenv.py
--- a/source/appModules/devenv.py
+++ b/source/appModules/devenv.py
@@ -1,7 +1,8 @@
 # A part of NonVisual Desktop Access (NVDA)
 # This file is covered by the GNU General Public License.
 # See the file COPYING for more details.
-# Copyright (C) 2010-2019 NV Access Limited, Soronel Haetir, Babbage B.V., Francisco Del Roio
+# Copyright (C) 2010-2022 NV Access Limited, Soronel Haetir, Babbage B.V., Francisco Del Roio,
+# Leonard de Ruijter
 
 import objbase
 import comtypes
@@ -14,11 +15,12 @@
 from comtypes.automation import IDispatch
 from NVDAObjects.window import DisplayModelEditableText
 from NVDAObjects.IAccessible import IAccessible
-from NVDAObjects.UIA import UIA
+from NVDAObjects.UIA import UIA, WpfTextView, UIATextInfo
 from enum import IntEnum
 import appModuleHandler
 import controlTypes
 import threading
+import UIAHandler
 
 
 # A few helpful constants
@@ -43,9 +45,12 @@ def __init__(self, *args, **kwargs):
 		self.vsMajor, self.vsMinor = int(vsMajor), int(vsMinor)
 
 	def chooseNVDAObjectOverlayClasses(self, obj, clsList):
+		if WpfTextView in clsList:
+			clsList.remove(WpfTextView)
+			clsList.insert(0, VsWpfTextView)
 		# Only use this overlay class if the top level automation object for the IDE can be retrieved,
 		# as it will not work otherwise.
-		if obj.windowClassName == "VsTextEditPane" and self.DTE:
+		elif obj.windowClassName == "VsTextEditPane" and self.DTE:
 			try:
 				clsList.remove(DisplayModelEditableText)
 			except ValueError:
@@ -82,6 +87,44 @@ def _get_DTE(self):
 		return DTE
 
 
+class VsWpfTextViewTextInfo(UIATextInfo):
+
+	def _getLineNumberString(self, textRange):
+		# Visual Studio exposes line numbers as part of the actual text.
+		# We want to store the line number in a format field instead.
+		lineNumberRange = textRange.Clone()
+		lineNumberRange.MoveEndpointByRange(
+			UIAHandler.TextPatternRangeEndpoint_End,
+			lineNumberRange,
+			UIAHandler.TextPatternRangeEndpoint_Start
+		)
+		return lineNumberRange.GetText(-1)
+
+	def _getFormatFieldAtRange(self, textRange, formatConfig, ignoreMixedValues=False):
+		formatField = super()._getFormatFieldAtRange(textRange, formatConfig, ignoreMixedValues=ignoreMixedValues)
+		if not formatField or not formatConfig['reportLineNumber']:
+			return formatField
+		lineNumberStr = self._getLineNumberString(textRange)
+		if lineNumberStr:
+			try:
+				formatField.field['line-number'] = int(lineNumberStr)
+			except ValueError:
+				log.debugWarning(
+					f"Couldn't parse {lineNumberStr} as integer to report a line number",
+					exc_info=True
+				)
+		return formatField
+
+	def _getTextFromUIARange(self, textRange):
+		text = super()._getTextFromUIARange(textRange)
+		lineNumberStr = self._getLineNumberString(textRange)
+		return text[(0 if not lineNumberStr else len(lineNumberStr)):]
+
+
+class VsWpfTextView(WpfTextView):
+	TextInfo = VsWpfTextViewTextInfo
+
+
 class VsTextEditPaneTextInfo(textInfos.offsets.OffsetsTextInfo):
 
 	def _get__selectionObject(self):


Test patch: