hterm: Handle announcements of selection changes

We currently don't make any announcements for text that isn't contained
in the terminal output. For example if a character is deleted or the
user moves the cursor over previously edited text. This CL adds code to
announce these kinds of changes. This is done by listening for cursor
changes and heuristically trying to detect what kind of a change
happened based on the location of the cursor, changes to the row of
text that the cursor is on and whether a key event has been recently
received.

Note that a heuristic is the best that can be done here because we
can't distinguish input from output in terminal and so can't know which
changes to the cursor/terminal are caused by the user or the
application. Also, some announcements would be inappropriate in certain
terminal applications/contexts. A best-effort approach must be taken
where we handle the common cases.

Change-Id: Ic71df5bcc67583caecd163620e287eb1dfa29ce8
Bug: 822490, 646690
Reviewed-on: https://chromium-review.googlesource.com/1121901
Tested-by: Raymes Khoury <raymes@chromium.org>
Reviewed-by: Mike Frysinger <vapier@chromium.org>
diff --git a/hterm/js/hterm_terminal.js b/hterm/js/hterm_terminal.js
index 3e7f544..12fb3ed 100644
--- a/hterm/js/hterm_terminal.js
+++ b/hterm/js/hterm_terminal.js
@@ -1421,8 +1421,8 @@
  * @param {string} str Sequence of characters to interpret or pass through.
  */
 hterm.Terminal.prototype.interpret = function(str) {
-  this.vt.interpret(str);
   this.scheduleSyncCursorPosition_();
+  this.vt.interpret(str);
 };
 
 /**
@@ -1460,6 +1460,7 @@
       this.prefs_.get('scroll-wheel-move-multiplier'));
 
   this.document_ = this.scrollPort_.getDocument();
+  this.accessibilityReader_.decorate(this.document_);
 
   this.document_.body.oncontextmenu = function() { return false; };
 
@@ -1786,6 +1787,8 @@
  * @param{string} str The string to print.
  */
 hterm.Terminal.prototype.print = function(str) {
+  this.scheduleSyncCursorPosition_();
+
   // Basic accessibility output for the screen reader.
   this.accessibilityReader_.announce(str);
 
@@ -1841,8 +1844,6 @@
     startOffset += count;
   }
 
-  this.scheduleSyncCursorPosition_();
-
   if (this.scrollOnOutput_)
     this.scrollPort_.scrollRowToBottom(this.getRowCount());
 };
@@ -2756,6 +2757,15 @@
   var cursorRowIndex = this.scrollbackRows_.length +
       this.screen_.cursorPosition.row;
 
+  if (this.accessibilityReader_.accessibilityEnabled) {
+    // Report the new position of the cursor for accessibility purposes.
+    const cursorColumnIndex = this.screen_.cursorPosition.column;
+    const cursorLineText =
+        this.screen_.rowsArray[this.screen_.cursorPosition.row].innerText;
+    this.accessibilityReader_.afterCursorChange(
+        cursorLineText, cursorRowIndex, cursorColumnIndex);
+  }
+
   if (cursorRowIndex > bottomRowIndex) {
     // Cursor is scrolled off screen, move it outside of the visible area.
     this.setCssVar('cursor-offset-row', '-1');
@@ -2831,12 +2841,24 @@
  * Synchronizes the visible cursor with the current cursor coordinates.
  *
  * The sync will happen asynchronously, soon after the call stack winds down.
- * Multiple calls will be coalesced into a single sync.
+ * Multiple calls will be coalesced into a single sync. This should be called
+ * prior to the cursor actually changing position.
  */
 hterm.Terminal.prototype.scheduleSyncCursorPosition_ = function() {
   if (this.timeouts_.syncCursor)
     return;
 
+  if (this.accessibilityReader_.accessibilityEnabled) {
+    // Report the previous position of the cursor for accessibility purposes.
+    const cursorRowIndex = this.scrollbackRows_.length +
+        this.screen_.cursorPosition.row;
+    const cursorColumnIndex = this.screen_.cursorPosition.column;
+    const cursorLineText =
+        this.screen_.rowsArray[this.screen_.cursorPosition.row].innerText;
+    this.accessibilityReader_.beforeCursorChange(
+        cursorLineText, cursorRowIndex, cursorColumnIndex);
+  }
+
   var self = this;
   this.timeouts_.syncCursor = setTimeout(function() {
       self.syncCursorPosition_();