Skip to content

Commit

Permalink
diff-match-patch for LiveText (PR #11639)
Browse files Browse the repository at this point in the history
Refactor LiveText to use diff-match-patch via IPC with another process

Changes diffing functions to operate at the string rather than line level.
Adds diff-match-patch (DMP) as an optional diffing algorithm for LiveText objects.
It is anticipated that DMP will become the default in a future NVDA release pending positive user testing.

Unlike #11500, this PR does not import or dynamically link to DMP due to licensing issues.
Instead, a Python application is run in another process that calls the DMP extension, and communicates over standard IO.

Co-authored-by: Michael Curran <michaelDCurran@users.noreply.github.com>
Co-authored-by: Reef Turner <feerrenrut@users.noreply.github.com>
  • Loading branch information
3 people authored Dec 31, 2020
1 parent 7e5ffde commit 9889285
Show file tree
Hide file tree
Showing 13 changed files with 364 additions and 105 deletions.
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@
[submodule "include/javaAccessBridge32"]
path = include/javaAccessBridge32
url = https://github.com/nvaccess/javaAccessBridge32-bin.git
[submodule "include/nvda_dmp"]
path = include/nvda_dmp
url = https://github.com/codeofdusk/nvda_dmp
[submodule "include/w3c-aria-practices"]
path = include/w3c-aria-practices
url = https://github.com/w3c/aria-practices
Expand Down
1 change: 1 addition & 0 deletions include/nvda_dmp
Submodule nvda_dmp added at b2ccf2
45 changes: 34 additions & 11 deletions source/NVDAObjects/IAccessible/winConsole.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,45 @@
#NVDAObjects/IAccessible/WinConsole.py
#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) 2007-2019 NV Access Limited, Bill Dengler
# 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) 2007-2020 NV Access Limited, Bill Dengler

import config

from NVDAObjects.behaviors import KeyboardHandlerBasedTypedCharSupport
from winVersion import isWin10

from . import IAccessible
from ..window import winConsole

class WinConsole(winConsole.WinConsole, IAccessible):
"The legacy console implementation for situations where UIA isn't supported."
pass

class EnhancedLegacyWinConsole(KeyboardHandlerBasedTypedCharSupport, winConsole.WinConsole, IAccessible):
"""
A hybrid approach to console access, using legacy APIs to read output
and KeyboardHandlerBasedTypedCharSupport for input.
"""
#: Legacy consoles take quite a while to send textChange events.
#: This significantly impacts typing performance, so don't queue chars.
_supportsTextChange = False


class LegacyWinConsole(winConsole.WinConsole, IAccessible):
"""
NVDA's original console support, used by default on Windows versions
before 1607.
"""

def _get_diffAlgo(self):
# Non-enhanced legacy consoles use caret proximity to detect
# typed/deleted text.
# Single-character changes are not reported as
# they are confused for typed characters.
# Force difflib to keep meaningful edit reporting in these consoles.
from diffHandler import get_difflib_algo
return get_difflib_algo()


def findExtraOverlayClasses(obj, clsList):
if isWin10(1607) and config.conf['terminals']['keyboardSupportInLegacy']:
from NVDAObjects.behaviors import KeyboardHandlerBasedTypedCharSupport
clsList.append(KeyboardHandlerBasedTypedCharSupport)
clsList.append(WinConsole)
clsList.append(EnhancedLegacyWinConsole)
else:
clsList.append(LegacyWinConsole)
15 changes: 0 additions & 15 deletions source/NVDAObjects/UIA/winConsoleUIA.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
# NVDAObjects/UIA/winConsoleUIA.py
# A part of NonVisual Desktop Access (NVDA)
# This file is covered by the GNU General Public License.
# See the file COPYING for more details.
Expand Down Expand Up @@ -363,20 +362,6 @@ def _get_TextInfo(self):
movement."""
return consoleUIATextInfo if self.is21H1Plus else consoleUIATextInfoPre21H1

def _getTextLines(self):
if self.is21H1Plus:
# #11760: the 21H1 UIA console wraps across lines.
# When text wraps, NVDA starts reading from the beginning of the visible text for every new line of output.
# Use the superclass _getTextLines instead.
return super()._getTextLines()
# This override of _getTextLines takes advantage of the fact that
# the console text contains linefeeds for every line
# Thus a simple string splitlines is much faster than splitting by unit line.
ti = self.makeTextInfo(textInfos.POSITION_ALL)
text = ti.text or ""
return text.splitlines()


def detectPossibleSelectionChange(self):
try:
return super().detectPossibleSelectionChange()
Expand Down
96 changes: 32 additions & 64 deletions source/NVDAObjects/behaviors.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
# -*- coding: UTF-8 -*-
# NVDAObjects/behaviors.py
# A part of NonVisual Desktop Access (NVDA)
# This file is covered by the GNU General Public License.
# See the file COPYING for more details.
Expand All @@ -12,7 +11,6 @@
import os
import time
import threading
import difflib
import tones
import queueHandler
import eventHandler
Expand All @@ -30,6 +28,8 @@
import braille
import nvwave
import globalVars
from typing import List
import diffHandler


class ProgressBar(NVDAObject):
Expand Down Expand Up @@ -265,15 +265,31 @@ def event_textChange(self):
"""
self._event.set()

def _getTextLines(self):
"""Retrieve the text of this object in lines.
def _get_diffAlgo(self):
"""
This property controls which diffing algorithm should be used by
this object. Most subclasses should simply use the base
implementation, which returns DMP (character-based diffing).
@Note: DMP is experimental, and can be disallowed via user
preference. In this case, the prior stable implementation, Difflib
(line-based diffing), will be used.
"""
return diffHandler.get_dmp_algo()

def _get_devInfo(self):
info = super().devInfo
info.append(f"diffing algorithm: {self.diffAlgo}")
return info

def _getText(self) -> str:
"""Retrieve the text of this object.
This will be used to determine the new text to speak.
The base implementation uses the L{TextInfo}.
However, subclasses should override this if there is a better way to retrieve the text.
@return: The current lines of text.
@rtype: list of str
"""
return list(self.makeTextInfo(textInfos.POSITION_ALL).getTextInChunks(textInfos.UNIT_LINE))
ti = self.makeTextInfo(textInfos.POSITION_ALL)
return self.diffAlgo._getText(ti)

def _reportNewLines(self, lines):
"""
Expand All @@ -291,10 +307,10 @@ def _reportNewText(self, line):

def _monitor(self):
try:
oldLines = self._getTextLines()
oldText = self._getText()
except:
log.exception("Error getting initial lines")
oldLines = []
log.exception("Error getting initial text")
oldText = ""

while self._keepMonitoring:
self._event.wait()
Expand All @@ -309,71 +325,23 @@ def _monitor(self):
self._event.clear()

try:
newLines = self._getTextLines()
newText = self._getText()
if config.conf["presentation"]["reportDynamicContentChanges"]:
outLines = self._calculateNewText(newLines, oldLines)
outLines = self._calculateNewText(newText, oldText)
if len(outLines) == 1 and len(outLines[0].strip()) == 1:
# This is only a single character,
# which probably means it is just a typed character,
# so ignore it.
del outLines[0]
if outLines:
queueHandler.queueFunction(queueHandler.eventQueue, self._reportNewLines, outLines)
oldLines = newLines
oldText = newText
except:
log.exception("Error getting lines or calculating new text")

def _calculateNewText(self, newLines, oldLines):
outLines = []

prevLine = None
for line in difflib.ndiff(oldLines, newLines):
if line[0] == "?":
# We're never interested in these.
continue
if line[0] != "+":
# We're only interested in new lines.
prevLine = line
continue
text = line[2:]
if not text or text.isspace():
prevLine = line
continue

if prevLine and prevLine[0] == "-" and len(prevLine) > 2:
# It's possible that only a few characters have changed in this line.
# If so, we want to speak just the changed section, rather than the entire line.
prevText = prevLine[2:]
textLen = len(text)
prevTextLen = len(prevText)
# Find the first character that differs between the two lines.
for pos in range(min(textLen, prevTextLen)):
if text[pos] != prevText[pos]:
start = pos
break
else:
# We haven't found a differing character so far and we've hit the end of one of the lines.
# This means that the differing text starts here.
start = pos + 1
# Find the end of the differing text.
if textLen != prevTextLen:
# The lines are different lengths, so assume the rest of the line changed.
end = textLen
else:
for pos in range(textLen - 1, start - 1, -1):
if text[pos] != prevText[pos]:
end = pos + 1
break

if end - start < 15:
# Less than 15 characters have changed, so only speak the changed chunk.
text = text[start:end]
log.exception("Error getting or calculating new text")

if text and not text.isspace():
outLines.append(text)
prevLine = line
def _calculateNewText(self, newText: str, oldText: str) -> List[str]:
return self.diffAlgo.diff(newText, oldText)

return outLines

class Terminal(LiveText, EditableText):
"""An object which both accepts text input and outputs text which should be reported automatically.
Expand Down
21 changes: 7 additions & 14 deletions source/NVDAObjects/window/winConsole.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
#NVDAObjects/WinConsole.py
#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) 2007-2019 NV Access Limited, Bill Dengler
# 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) 2007-2020 NV Access Limited, Bill Dengler

import winConsoleHandler
from . import Window
Expand All @@ -14,18 +13,12 @@

class WinConsole(Terminal, EditableTextWithoutAutoSelectDetection, Window):
"""
NVDA's legacy Windows Console support.
Base class for NVDA's legacy Windows Console support.
This is used in situations where UIA isn't available.
Please consider using NVDAObjects.UIA.winConsoleUIA instead.
"""
STABILIZE_DELAY = 0.03

def initOverlayClass(self):
# Legacy consoles take quite a while to send textChange events.
# This significantly impacts typing performance, so don't queue chars.
if isinstance(self, KeyboardHandlerBasedTypedCharSupport):
self._supportsTextChange = False

def _get_windowThreadID(self):
# #10113: Windows forces the thread of console windows to match the thread of the first attached process.
# However, To correctly handle speaking of typed characters,
Expand Down Expand Up @@ -69,8 +62,8 @@ def event_loseFocus(self):
def event_nameChange(self):
pass

def _getTextLines(self):
return winConsoleHandler.getConsoleVisibleLines()
def _getText(self):
return '\n'.join(winConsoleHandler.getConsoleVisibleLines())

def script_caret_backspaceCharacter(self, gesture):
super(WinConsole, self).script_caret_backspaceCharacter(gesture)
Expand Down
1 change: 1 addition & 0 deletions source/config/configSpec.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@
[terminals]
speakPasswords = boolean(default=false)
keyboardSupportInLegacy = boolean(default=True)
diffAlgo = option("auto", "dmp", "difflib", default="auto")
[update]
autoCheck = boolean(default=true)
Expand Down
8 changes: 8 additions & 0 deletions source/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -605,6 +605,14 @@ def run(self):
_terminate(speech)
_terminate(addonHandler)
_terminate(garbageHandler)
# DMP is only started if needed.
# Terminate manually (and let it write to the log if necessary)
# as core._terminate always writes an entry.
try:
import diffHandler
diffHandler._dmp._terminate()
except Exception:
log.exception("Exception while terminating DMP")

if not globalVars.appArgs.minimal and config.conf["general"]["playStartAndExitSounds"]:
try:
Expand Down
Loading

0 comments on commit 9889285

Please sign in to comment.