From 6bbc1d6d41f7489ecc562141e461d63a56838d21 Mon Sep 17 00:00:00 2001 From: Peter Xiao Date: Thu, 8 Aug 2013 15:51:20 -0700 Subject: [PATCH 01/13] Add chromevox theme. --- lib/ace/theme/chromevox.js | 843 +++++++++++++++++++++++++++++++++++++ 1 file changed, 843 insertions(+) create mode 100644 lib/ace/theme/chromevox.js diff --git a/lib/ace/theme/chromevox.js b/lib/ace/theme/chromevox.js new file mode 100644 index 00000000..396b6bc5 --- /dev/null +++ b/lib/ace/theme/chromevox.js @@ -0,0 +1,843 @@ +define(function(require, exports, module) { + /* ChromeVox Ace namespace. */ + var cvoxAce = {}; + + /* Typedefs for Closure compiler. */ + /** + * @typedef {{ + rate: number, + pitch: number, + volume: number, + relativePitch: number, + punctuationEcho: string + }} + */ + /* TODO(peterxiao): Export this typedef through cvox.Api. */ + cvoxAce.SpeechProperty; + + /** + * @typedef {{ + * row: number, + * column: number + * }} + */ + cvoxAce.Cursor; + + /** + * @typedef {{ + type: string, + value: string + }} + } + */ + cvoxAce.Token; + + /** + * These are errors and information that Ace will display in the gutter. + * @typedef {{ + row: number, + column: number, + value: string + }} + } + */ + cvoxAce.Annotation; + + /* Speech Properties. */ + /** + * Speech property for speaking constant tokens. + * @type {cvoxAce.SpeechProperty} + */ + var CONSTANT_PROP = { + 'rate': 0.8, + 'pitch': 0.4, + 'volume': 0.9 + }; + + /** + * Default speech property for speaking tokens. + * @type {cvoxAce.SpeechProperty} + */ + var DEFAULT_PROP = { + 'rate': 1, + 'pitch': 0.5, + 'volume': 0.9 + }; + + /** + * Speech property for speaking entity tokens. + * @type {cvoxAce.SpeechProperty} + */ + var ENTITY_PROP = { + 'rate': 0.8, + 'pitch': 0.8, + 'volume': 0.9 + }; + + /** + * Speech property for speaking keywords. + * @type {cvoxAce.SpeechProperty} + */ + var KEYWORD_PROP = { + 'rate': 0.8, + 'pitch': 0.3, + 'volume': 0.9 + }; + + /** + * Speech property for speaking storage tokens. + * @type {cvoxAce.SpeechProperty} + */ + var STORAGE_PROP = { + 'rate': 0.8, + 'pitch': 0.7, + 'volume': 0.9 + }; + + /** + * Speech property for speaking variable tokens. + * @type {cvoxAce.SpeechProperty} + */ + var VARIABLE_PROP = { + 'rate': 0.8, + 'pitch': 0.8, + 'volume': 0.9 + }; + + /** + * Speech property for speaking deleted text. + * @type {cvoxAce.SpeechProperty} + */ + var DELETED_PROP = { + 'punctuationEcho': 'none', + 'relativePitch': -0.6 + }; + + /* Constants for Earcons. */ + var ERROR_EARCON = 'ALERT_NONMODAL'; + var MODE_SWITCH_EARCON = 'ALERT_MODAL'; + var NO_MATCH_EARCON = 'INVALID_KEYPRESS'; + + /* Constants for vim state. */ + var INSERT_MODE_STATE = 'insertMode'; + var COMMAND_MODE_STATE = 'start'; + + /** + * Context menu commands. + */ + var Command = { + SPEAK_ANNOT: 'annots', + SPEAK_ALL_ANNOTS: 'all_annots', + TOGGLE_LOCATION: 'toggle_location', + SPEAK_MODE: 'mode', + SPEAK_ROW_COL: 'row_col', + TOGGLE_DISPLACEMENT: 'toggle_displacement', + FOCUS_TEXT: 'focus_text' + }; + + /** + * Key prefix for each shortcut. + */ + var KEY_PREFIX = 'CONTROL + SHIFT '; + + /* Globals. */ + /** + * Last cursor position. + * @type {!cvoxAce.Cursor} + */ + var lastCursor = ace.selection.getCursor(); + + /** + * Table of annotations. + * @typedef {!Object.>} + */ + var annotTable = {}; + + /** + * Whether to speak character, word, and then line. This allows blind users + * to know the location of the cursor when they change lines. + * @typedef {boolean} + */ + var shouldSpeakRowLocation = false; + + /** + * Whether to speak displacement. + * @typedef {boolean} + */ + var shouldSpeakDisplacement = false; + + /** + * Whether text was changed to cause a cursor change event. + * @typedef {boolean} + */ + var changed = false; + + /** + * Current state vim is in. + */ + var vimState = null; + + /** + * Mapping from key code to shortcut. + */ + var keyCodeToShortcutMap = {}; + + /** + * Mapping from command to shortcut. + */ + var cmdToShortcutMap = {}; + + /** + * Get shortcut string from keyCode. + * @param {number} keyCode Key code of shortcut. + * @return {string} String representation of shortcut. + */ + var getKeyShortcutString = function(keyCode) { + return KEY_PREFIX + String.fromCharCode(keyCode); + }; + + /** + * Return if in vim mode. + * @return {boolean} True if in Vim mode. + */ + var isVimMode = function() { + return ace.keyBinding.getKeyboardHandler().$id === 'ace/keyboard/vim'; + }; + + /** + * Gets the current token. + * @param {!cvoxAce.Cursor} cursor Current position of the cursor. + * @return {!cvoxAce.Token} Token at the current position. + */ + var getCurrentToken = function(cursor) { + return ace.getSession().getTokenAt(cursor.row, cursor.column + 1); + }; + + /** + * Gets the current line the cursor is under. + * @param {!cvoxAce.Cursor} cursor Current cursor position. + */ + var getCurrentLine = function(cursor) { + return ace.getSession().getLine(cursor.row); + }; + + /** + * Event handler for row changes. When the user changes rows we want to speak + * the line so the user can work on this line. If shouldSpeakRowLocation is on + * then we speak the character, then the row, then the line so the user knows + * where the cursor is. + * @param {!cvoxAce.Cursor} currCursor Current cursor position. + */ + var onRowChange = function(currCursor) { + /* Notify that this line has an annotation. */ + if (annotTable[currCursor.row]) { + cvox.Api.playEarcon(ERROR_EARCON); + } + if (shouldSpeakRowLocation) { + cvox.Api.stop(); + speakChar(currCursor); + speakTokenQueue(getCurrentToken(currCursor)); + speakLine(currCursor.row, 1); + } else { + speakLine(currCursor.row, 0); + } + }; + + /** + * Returns whether the cursor is at the beginning of a word. A word is + * a grouping of alphanumeric characters including underscores. + * @param {!cvoxAce.Cursor} cursor Current cursor position. + * @return {boolean} Whether there is word. + */ + var isWord = function(cursor) { + var line = getCurrentLine(cursor); + var lineSuffix = line.substr(cursor.column - 1); + if (cursor.column === 0) { + lineSuffix = ' ' + line; + } + /* Use regex to tell if the suffix is at the start of a new word. */ + var firstWordRegExp = /^\W(\w+)/; + var words = firstWordRegExp.exec(lineSuffix); + return words !== null; + }; + + /** + * A mapping of syntax type to speech properties. + */ + var rules = { + 'constant': CONSTANT_PROP, + 'entity': ENTITY_PROP, + 'keyword': KEYWORD_PROP, + 'storage': STORAGE_PROP, + 'variable': VARIABLE_PROP + }; + + /** + * Speak the line with syntax properties. + * @param {number} row Row to speak. + * @param {number} queue Queue mode to speak. + */ + var speakLine = function(row, queue) { + var tokens = ace.getSession().getTokens(row); + if (tokens.length === 0) { + return; + } + var firstToken = tokens[0]; + /* Filter out first token and spaces. */ + tokens = tokens.filter(function(token) { + return token !== firstToken && token.type !== 'text'; + }); + /* Speak first token separately to flush if queue. */ + speakToken_(firstToken, queue); + /* Speak rest of tokens. */ + tokens.forEach(speakTokenQueue); + }; + + /** + * Speak the token based on the syntax of the token, flushing. + * @param {!cvoxAce.Token} token Token to speak. + * @param {number} queue Queue mode. + */ + var speakTokenFlush = function(token) { + speakToken_(token, 0); + }; + + /** + * Speak the token based on the syntax of the token, queueing. + * @param {!cvoxAce.Token} token Token to speak. + * @param {number} queue Queue mode. + */ + var speakTokenQueue = function(token) { + speakToken_(token, 1); + }; + + /** + * Speak the token based on the syntax of the token. + * @private + * @param {!cvoxAce.Token} token Token to speak. + * @param {number} queue Queue mode. + */ + var speakToken_ = function(token, queue) { + /* Types are period delimited. In this case, we only syntax speak the outer + * most type of token. */ + if (!token || !token.type) { + return; + } + var split = token.type.split('.'); + if (split.length === 0) { + return; + } + var type = split[0]; + var prop = rules[type]; + if (!prop) { + prop = DEFAULT_PROP; + } + cvox.Api.speak(token.value, queue, prop); + }; + + /** + * Speaks the character under the cursor. This is queued. + * @param {!cvoxAce.Cursor} cursor Current cursor position. + * @return {string} Character. + */ + var speakChar = function(cursor) { + var line = getCurrentLine(cursor); + cvox.Api.speak(line[cursor.column], 1); + }; + + /** + * Speaks the jump from lastCursor to currCursor. This function assumes the + * jump takes place on the current line. + * @param {!cvoxAce.Cursor} lastCursor Previous cursor position. + * @param {!cvoxAce.Cursor} currCursor Current cursor position. + */ + var speakDisplacement = function(lastCursor, currCursor) { + cvox.Api.stop(); + var line = getCurrentLine(currCursor); + + /* Get the text that we jumped past. */ + var displace = line.substring(lastCursor.column, currCursor.column); + /* When going forward one space, we speak where we land instead. */ + if (currCursor.column - lastCursor.column === 1) { + displace = line.substring(lastCursor.column + 1, currCursor.column + 1); + } + /* Speak out loud spaces. */ + displace = displace.replace(/ /g, ' space '); + cvox.Api.speak(displace, 1); + }; + + /** + * Speaks the word if the cursor jumped to a new word or to the beginning + * of the line. Otherwise speak the charactor. + * @param {!cvoxAce.Cursor} lastCursor Previous cursor position. + * @param {!cvoxAce.Cursor} currCursor Current cursor position. + */ + var speakCharOrWordOrLine = function(lastCursor, currCursor) { + /* Say word only if jump. */ + if (Math.abs(lastCursor.column - currCursor.column) !== 1) { + var currLineLength = getCurrentLine(currCursor).length; + /* Speak line if jumping to beginning or end of line. */ + if (currCursor.column === 0 || currCursor.column === currLineLength) { + speakLine(currCursor.row, 0); + return; + } + if (isWord(currCursor)) { + cvox.Api.stop(); + speakTokenQueue(getCurrentToken(currCursor)); + return; + } + } + speakChar(currCursor); + }; + + /** + * Event handler for column changes. If shouldSpeakDisplacement is on, then + * we just speak displacements in row changes. Otherwise, we either speak + * the character for single character movements, the word when jumping to the + * next word, or the entire line if jumping to beginning or end of the line. + * @param {!cvoxAce.Cursor} lastCursor Previous cursor position. + * @param {!cvoxAce.Cursor} currCursor Current cursor position. + */ + var onColumnChange = function(lastCursor, currCursor) { + if (shouldSpeakDisplacement) { + speakDisplacement(lastCursor, currCursor); + } else { + speakCharOrWordOrLine(lastCursor, currCursor); + } + }; + + /** + * Event handler for cursor changes. Classify cursor changes as either row or + * column changes, then delegate accordingly. + * @param {!Event} evt The event. + */ + var onCursorChange = function(evt) { + /* Do not speak if cursor change was a result of text insertion. We want to + * speak the text that was inserted and not where the cursor lands. */ + if (changed) { + changed = false; + return; + } + var currCursor = ace.selection.getCursor(); + if (currCursor.row !== lastCursor.row) { + onRowChange(currCursor); + } else { + onColumnChange(lastCursor, currCursor); + } + lastCursor = currCursor; + }; + + /** + * Event handler for source changes. We want auditory feedback for inserting + * and deleting text. + * @param {!Event} evt The event. + */ + var onChange = function(evt) { + var data = evt.data; + switch (data.action) { + case 'removeText': + cvox.Api.speak(data.text, 0, DELETED_PROP); + /* Let the future cursor change event know it's from text change. */ + changed = true; + break; + case 'insertText': + cvox.Api.speak(data.text, 0); + /* Let the future cursor change event know it's from text change. */ + changed = true; + break; + } + }; + + /** + * Returns whether or not the annotation is new. + * @param {!cvoxAce.Annotation} annot Annotation in question. + * @return {boolean} Whether annot is new. + */ + var isNewAnnotation = function(annot) { + var row = annot.row; + var col = annot.column; + return !annotTable[row] || !annotTable[row][col]; + }; + + /** + * Populates the annotation table. + * @param {!Array.} annotations Array of annotations. + */ + var populateAnnotations = function(annotations) { + annotTable = {}; + for (var i = 0; i < annotations.length; i++) { + var annotation = annotations[i]; + var row = annotation.row; + var col = annotation.column; + if (!annotTable[row]) { + annotTable[row] = {}; + } + annotTable[row][col] = annotation; + } + }; + + /** + * Event handler for annotation changes. We want to notify the user when an + * a new annotation appears. + * @param {!Event} evt Event. + */ + var onAnnotationChange = function(evt) { + var annotations = ace.getSession().getAnnotations(); + var newAnnotations = annotations.filter(isNewAnnotation); + if (newAnnotations.length > 0) { + cvox.Api.playEarcon(ERROR_EARCON); + } + populateAnnotations(annotations); + }; + + /** + * Speak annotation. + * @param {!cvoxAce.Annotation} annot Annotation to speak. + */ + var speakAnnot = function(annot) { + var annotText = annot.type + ' ' + annot.text + ' on ' + + rowColToString(annot.row, annot.column); + annotText = annotText.replace(';', 'semicolon'); + cvox.Api.speak(annotText, 1); + }; + + /** + * Speak annotations in a row. + * @param {number} row Row of annotations to speak. + */ + var speakAnnotsByRow = function(row) { + var annots = annotTable[row]; + for (var col in annots) { + speakAnnot(annots[col]); + } + }; + + /** + * Get a string representation of a row and column. + * @param {boolean} row Zero indexed row. + * @param {boolean} col Zero indexed column. + * @return {string} Row and column to be spoken. + */ + var rowColToString = function(row, col) { + return 'row ' + (row + 1) + ' column ' + (col + 1); + }; + + /** + * Speaks the row and column. + */ + var speakCurrRowAndCol = function() { + cvox.Api.speak(rowColToString(lastCursor.row, lastCursor.column)); + }; + + /** + * Speaks all annotations. + */ + var speakAllAnnots = function() { + for (var row in annotTable) { + speakAnnotsByRow(row); + } + }; + + /** + * Speak the vim mode. If no vim mode, this function does nothing. + */ + var speakMode = function() { + if (!isVimMode()) { + return; + } + switch (ace.keyBinding.$data.state) { + case INSERT_MODE_STATE: + cvox.Api.speak('Insert mode'); + break; + case COMMAND_MODE_STATE: + cvox.Api.speak('Command mode'); + break; + } + }; + + /** + * Toggle speak location. + */ + var toggleSpeakRowLocation = function() { + shouldSpeakRowLocation = !shouldSpeakRowLocation; + /* Auditory feedback of the change. */ + if (shouldSpeakRowLocation) { + cvox.Api.speak('Speak location on row change enabled.'); + } else { + cvox.Api.speak('Speak location on row change disabled.'); + } + }; + + /** + * Toggle speak displacement. + */ + var toggleSpeakDisplacement = function() { + speakDisplacement = !speakDisplacement; + /* Auditory feedback of the change. */ + if (speakDisplacement) { + cvox.Api.speak('Speak displacement on column changes.'); + } else { + cvox.Api.speak('Speak current character or word on column changes.'); + } + }; + + /** + * Event handler for key down events. Gets the right shortcut from the map, + * and calls the associated function. + * @param {!Event} evt Keyboard event. + */ + var onKeyDown = function(evt) { + if (evt.ctrlKey && evt.shiftKey) { + var shortcut = keyCodeToShortcutMap[evt.keyCode]; + if (shortcut) { + shortcut.func(); + } + } + }; + + /** + * Event handler for status change events. Auditory feedback of changing + * between vim states. + * @param {!Event} evt Change status event. + * @param {!Object} editor Editor state. + */ + var onChangeStatus = function(evt, editor) { + if (!isVimMode()) { + return; + } + var state = editor.keyBinding.$data.state; + if (state === vimState) { + /* State hasn't changed, do nothing. */ + return; + } + switch (state) { + case INSERT_MODE_STATE: + cvox.Api.playEarcon(MODE_SWITCH_EARCON); + /* When in insert mode, we want to speak out keys as feedback. */ + cvox.Api.setKeyEcho(true); + break; + case COMMAND_MODE_STATE: + cvox.Api.playEarcon(MODE_SWITCH_EARCON); + /* When in command mode, we want don't speak out keys because those keys + * are not being inserted in the document. */ + cvox.Api.setKeyEcho(false); + break; + } + vimState = state; + }; + + /** + * Handles context menu events. This is a ChromeVox feature where hitting + * the shortcut ChromeVox + comma will open up a search bar where you can + * type in various commands. All keyboard shortcuts are also commands that + * can be invoked. This handles the event that ChromeVox sends to the page. + * @param {Event} evt Event received. + */ + var contextMenuHandler = function(evt) { + var cmd = evt.detail['customCommand']; + var shortcut = cmdToShortcutMap[cmd]; + if (shortcut) { + shortcut.func(); + /* ChromeVox will bring focus to an element near the cursor instead of the + * text input. */ + ace.focus(); + } + }; + + /** + * Initialize the ChromeVox context menu. + */ + var initContextMenu = function() { + var ACTIONS = SHORTCUTS.map(function(shortcut) { + return { + desc: shortcut.desc + getKeyShortcutString(shortcut.keyCode), + cmd: shortcut.cmd + }; + }); + + /* Attach ContextMenuActions. */ + var body = document.querySelector('body'); + body.setAttribute('contextMenuActions', JSON.stringify(ACTIONS)); + + /* Listen for ContextMenu events. */ + body.addEventListener('ATCustomEvent', contextMenuHandler, true); + }; + + /** + * Returns a mutations handler where f is applied to each mutation. + * @param {function} f Function to be applied to mutations. + */ + var getMutationHandler = function(f) { + return function(mutations) { + mutations.forEach(f); + }; + }; + + /** + * Watches and handles the mutation that is a result of a search. + * @param {Mutation} m Mutation. + */ + var watchForSearch = function(m) { + if (m.attributeName === 'class' && + m.target.className === 'ace_search_form ace_nomatch') { + /* No match, give auditory feedback! */ + cvox.Api.playEarcon(NO_MATCH_EARCON); + } else { + /* There is still a match! Speak the line. */ + speakLine(lastCursor.row, 0); + } + }; + + /** + * Configuration for mutation observer. + */ + var MO_CONFIG = { attributes: true, childList: true, characterData: true}; + + /** + * Watches and handles the mutation that adds the search bar to the DOM. + */ + var watchForStartSearch = function(m) { + for (var i = 0; i < m.addedNodes.length; i++) { + if (m.addedNodes.item(i).className === 'ace_search right') { + var mutationHandler = getMutationHandler(watchForSearch); + var searchObs = new MutationObserver(mutationHandler); + var target = m.addedNodes.item(i).querySelector('.ace_search_form'); + searchObs.observe(target, MO_CONFIG); + } + } + }; + + /** + * Focus to text input. + */ + var focus = function() { + ace.focus(); + }; + + /** + * Shortcut definitions. + */ + var SHORTCUTS = [ + { + /* 1 key. */ + keyCode: 49, + func: function() { + speakAnnotsByRow(lastCursor.row); + }, + cmd: Command.SPEAK_ANNOT, + desc: 'Speak annotations on line' + }, + { + /* 2 key. */ + keyCode: 50, + func: speakAllAnnots, + cmd: Command.SPEAK_ALL_ANNOTS, + desc: 'Speak all annotations' + }, + { + /* 3 key. */ + keyCode: 51, + func: speakMode, + cmd: Command.SPEAK_MODE, + desc: 'Speak Vim mode' + }, + { + /* 4 key. */ + keyCode: 52, + func: toggleSpeakRowLocation, + cmd: Command.TOGGLE_LOCATION, + desc: 'Toggle speak row location' + }, + { + /* 5 key. */ + keyCode: 53, + func: speakCurrRowAndCol, + cmd: Command.SPEAK_ROW_COL, + desc: 'Speak row and column' + }, + { + /* 6 key. */ + keyCode: 54, + func: toggleSpeakDisplacement, + cmd: Command.TOGGLE_DISPLACEMENT, + desc: 'Toggle speak displacement' + }, + { + /* 7 key. */ + keyCode: 55, + func: focus, + cmd: Command.FOCUS_TEXT, + desc: 'Focus text' + } + ]; + + /** + * Initialize the theme. + */ + var init = function() { + /* Construct maps. */ + SHORTCUTS.forEach(function(shortcut) { + keyCodeToShortcutMap[shortcut.keyCode] = shortcut; + cmdToShortcutMap[shortcut.cmd] = shortcut; + }); + + /* Set up listeners. */ + ace.getSession().selection.on('changeCursor', onCursorChange); + ace.getSession().on('change', onChange); + ace.getSession().on('changeAnnotation', onAnnotationChange); + ace.on('changeStatus', onChangeStatus); + window.addEventListener('keydown', onKeyDown); + + /* Assume we start in command mode if vim. */ + if (isVimMode()) { + cvox.Api.setKeyEcho(false); + } + initContextMenu(); + + var target = document.querySelector('.ace_editor'); + var mutationHandler = getMutationHandler(watchForStartSearch); + var observer = new MutationObserver(mutationHandler); + + observer.observe(target, MO_CONFIG); + + ace.focus(); + }; + + /** + * Returns if cvox exists, and the api exists. + * @return {boolean} Whether not Cvox Api exists. + */ + function cvoxApiExists() { + return (typeof(cvox) !== 'undefined') && cvox && cvox.Api; + } + + /** + * Number of tries for Cvox loading. + * @type {number} + */ + var tries = 0; + + /** + * Max number of tries to watch for Cvox loading. + * @type {number} + */ + var MAX_TRIES = 15; + + /** + * Check for ChromeVox load. + */ + function watchForCvoxLoad() { + if (cvoxApiExists()) { + init(); + } else { + tries++; + if (tries >= MAX_TRIES) { + return; + } + window.setTimeout(watchForCvoxLoad, 500); + } + } + + /* Initialize everything when ChromeVox has loaded. */ + watchForCvoxLoad(); +}); From 304b27954bb4d1e28d99d0b8dd7c85ab1ab77cbb Mon Sep 17 00:00:00 2001 From: Peter Xiao Date: Mon, 12 Aug 2013 13:04:12 -0700 Subject: [PATCH 02/13] Moved chromevox to ext --- lib/ace/{theme => ext}/chromevox.js | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename lib/ace/{theme => ext}/chromevox.js (100%) diff --git a/lib/ace/theme/chromevox.js b/lib/ace/ext/chromevox.js similarity index 100% rename from lib/ace/theme/chromevox.js rename to lib/ace/ext/chromevox.js From 38b42d20d3c5b45dda23c13b9dbbedd18e9fea97 Mon Sep 17 00:00:00 2001 From: Peter Xiao Date: Mon, 12 Aug 2013 15:58:09 -0700 Subject: [PATCH 03/13] Chromevox theme now an extension, added demo file, added findSearchBox event. --- demo/chromevox.html | 44 + lib/ace/ext/chromevox.js | 1545 ++++++++++++------------- lib/ace/ext/searchbox.js | 4 +- lib/ace/ext/themelist_utils/themes.js | 2 +- 4 files changed, 809 insertions(+), 786 deletions(-) create mode 100644 demo/chromevox.html diff --git a/demo/chromevox.html b/demo/chromevox.html new file mode 100644 index 00000000..23c9ba68 --- /dev/null +++ b/demo/chromevox.html @@ -0,0 +1,44 @@ + + + + + ACE Autocompletion demo + + + + +

+
+
+
+
+
+
+
+
+
+
diff --git a/lib/ace/ext/chromevox.js b/lib/ace/ext/chromevox.js
index 396b6bc5..1f51530a 100644
--- a/lib/ace/ext/chromevox.js
+++ b/lib/ace/ext/chromevox.js
@@ -1,843 +1,820 @@
 define(function(require, exports, module) {
-  /* ChromeVox Ace namespace. */
-  var cvoxAce = {};
 
-  /* Typedefs for Closure compiler. */
-  /**
-   * @typedef {{
-      rate: number,
-      pitch: number,
-      volume: number,
-      relativePitch: number,
-      punctuationEcho: string
-     }}
-   */
-  /* TODO(peterxiao): Export this typedef through cvox.Api. */
-  cvoxAce.SpeechProperty;
+/* ChromeVox Ace namespace. */
+var cvoxAce = {};
 
-  /**
-   * @typedef {{
-   *   row: number,
-   *   column: number
-   * }}
-   */
-  cvoxAce.Cursor;
+/* Typedefs for Closure compiler. */
+/**
+ * @typedef {{
+    rate: number,
+    pitch: number,
+    volume: number,
+    relativePitch: number,
+    punctuationEcho: string
+   }}
+ */
+/* TODO(peterxiao): Export this typedef through cvox.Api. */
+cvoxAce.SpeechProperty;
 
-  /**
-   * @typedef {{
-      type: string,
-      value: string
-     }}
-   }
-   */
-  cvoxAce.Token;
+/**
+ * @typedef {{
+ *   row: number,
+ *   column: number
+ * }}
+ */
+cvoxAce.Cursor;
 
-  /**
-   * These are errors and information that Ace will display in the gutter.
-   * @typedef {{
-      row: number,
-      column: number,
-      value: string
-     }}
-   }
-   */
-  cvoxAce.Annotation;
+/**
+ * @typedef {{
+    type: string,
+    value: string
+   }}
+ }
+ */
+cvoxAce.Token;
 
-  /* Speech Properties. */
-  /**
-   * Speech property for speaking constant tokens.
-   * @type {cvoxAce.SpeechProperty}
-   */
-  var CONSTANT_PROP = {
-    'rate': 0.8,
-    'pitch': 0.4,
-    'volume': 0.9
-  };
+/**
+ * These are errors and information that Ace will display in the gutter.
+ * @typedef {{
+    row: number,
+    column: number,
+    value: string
+   }}
+ }
+ */
+cvoxAce.Annotation;
 
-  /**
-   * Default speech property for speaking tokens.
-   * @type {cvoxAce.SpeechProperty}
-   */
-  var DEFAULT_PROP = {
-    'rate': 1,
-    'pitch': 0.5,
-    'volume': 0.9
-  };
+/* Speech Properties. */
+/**
+ * Speech property for speaking constant tokens.
+ * @type {cvoxAce.SpeechProperty}
+ */
+var CONSTANT_PROP = {
+  'rate': 0.8,
+  'pitch': 0.4,
+  'volume': 0.9
+};
 
-  /**
-   * Speech property for speaking entity tokens.
-   * @type {cvoxAce.SpeechProperty}
-   */
-  var ENTITY_PROP = {
-    'rate': 0.8,
-    'pitch': 0.8,
-    'volume': 0.9
-  };
+/**
+ * Default speech property for speaking tokens.
+ * @type {cvoxAce.SpeechProperty}
+ */
+var DEFAULT_PROP = {
+  'rate': 1,
+  'pitch': 0.5,
+  'volume': 0.9
+};
 
-  /**
-   * Speech property for speaking keywords.
-   * @type {cvoxAce.SpeechProperty}
-   */
-  var KEYWORD_PROP = {
-    'rate': 0.8,
-    'pitch': 0.3,
-    'volume': 0.9
-  };
+/**
+ * Speech property for speaking entity tokens.
+ * @type {cvoxAce.SpeechProperty}
+ */
+var ENTITY_PROP = {
+  'rate': 0.8,
+  'pitch': 0.8,
+  'volume': 0.9
+};
 
-  /**
-   * Speech property for speaking storage tokens.
-   * @type {cvoxAce.SpeechProperty}
-   */
-  var STORAGE_PROP = {
-    'rate': 0.8,
-    'pitch': 0.7,
-    'volume': 0.9
-  };
+/**
+ * Speech property for speaking keywords.
+ * @type {cvoxAce.SpeechProperty}
+ */
+var KEYWORD_PROP = {
+  'rate': 0.8,
+  'pitch': 0.3,
+  'volume': 0.9
+};
 
-  /**
-   * Speech property for speaking variable tokens.
-   * @type {cvoxAce.SpeechProperty}
-   */
-  var VARIABLE_PROP = {
-    'rate': 0.8,
-    'pitch': 0.8,
-    'volume': 0.9
-  };
+/**
+ * Speech property for speaking storage tokens.
+ * @type {cvoxAce.SpeechProperty}
+ */
+var STORAGE_PROP = {
+  'rate': 0.8,
+  'pitch': 0.7,
+  'volume': 0.9
+};
 
-  /**
-   * Speech property for speaking deleted text.
-   * @type {cvoxAce.SpeechProperty}
-   */
-  var DELETED_PROP = {
-    'punctuationEcho': 'none',
-    'relativePitch': -0.6
-  };
+/**
+ * Speech property for speaking variable tokens.
+ * @type {cvoxAce.SpeechProperty}
+ */
+var VARIABLE_PROP = {
+  'rate': 0.8,
+  'pitch': 0.8,
+  'volume': 0.9
+};
 
-  /* Constants for Earcons. */
-  var ERROR_EARCON = 'ALERT_NONMODAL';
-  var MODE_SWITCH_EARCON = 'ALERT_MODAL';
-  var NO_MATCH_EARCON = 'INVALID_KEYPRESS';
+/**
+ * Speech property for speaking deleted text.
+ * @type {cvoxAce.SpeechProperty}
+ */
+var DELETED_PROP = {
+  'punctuationEcho': 'none',
+  'relativePitch': -0.6
+};
 
-  /* Constants for vim state. */
-  var INSERT_MODE_STATE = 'insertMode';
-  var COMMAND_MODE_STATE = 'start';
+/* Constants for Earcons. */
+var ERROR_EARCON = 'ALERT_NONMODAL';
+var MODE_SWITCH_EARCON = 'ALERT_MODAL';
+var NO_MATCH_EARCON = 'INVALID_KEYPRESS';
 
-  /**
-   * Context menu commands.
-   */
-  var Command = {
-    SPEAK_ANNOT: 'annots',
-    SPEAK_ALL_ANNOTS: 'all_annots',
-    TOGGLE_LOCATION: 'toggle_location',
-    SPEAK_MODE: 'mode',
-    SPEAK_ROW_COL: 'row_col',
-    TOGGLE_DISPLACEMENT: 'toggle_displacement',
-    FOCUS_TEXT: 'focus_text'
-  };
+/* Constants for vim state. */
+var INSERT_MODE_STATE = 'insertMode';
+var COMMAND_MODE_STATE = 'start';
 
-  /**
-   * Key prefix for each shortcut.
-   */
-  var KEY_PREFIX = 'CONTROL + SHIFT ';
+/**
+ * Context menu commands.
+ */
+var Command = {
+  SPEAK_ANNOT: 'annots',
+  SPEAK_ALL_ANNOTS: 'all_annots',
+  TOGGLE_LOCATION: 'toggle_location',
+  SPEAK_MODE: 'mode',
+  SPEAK_ROW_COL: 'row_col',
+  TOGGLE_DISPLACEMENT: 'toggle_displacement',
+  FOCUS_TEXT: 'focus_text'
+};
 
-  /* Globals. */
-  /**
-   * Last cursor position.
-   * @type {!cvoxAce.Cursor}
-   */
-  var lastCursor = ace.selection.getCursor();
+/**
+ * Key prefix for each shortcut.
+ */
+var KEY_PREFIX = 'CONTROL + SHIFT ';
 
-  /**
-   * Table of annotations.
-   * @typedef {!Object.>}
-   */
-  var annotTable = {};
+/* Globals. */
+cvoxAce.editor = null;
+/**
+ * Last cursor position.
+ * @type {cvoxAce.Cursor}
+ */
+var lastCursor = null;
 
-  /**
-   * Whether to speak character, word, and then line. This allows blind users
-   * to know the location of the cursor when they change lines.
-   * @typedef {boolean}
-   */
-  var shouldSpeakRowLocation = false;
+/**
+ * Table of annotations.
+ * @typedef {!Object.>}
+ */
+var annotTable = {};
 
-  /**
-   * Whether to speak displacement.
-   * @typedef {boolean}
-   */
-  var shouldSpeakDisplacement = false;
+/**
+ * Whether to speak character, word, and then line. This allows blind users
+ * to know the location of the cursor when they change lines.
+ * @typedef {boolean}
+ */
+var shouldSpeakRowLocation = false;
 
-  /**
-   * Whether text was changed to cause a cursor change event.
-   * @typedef {boolean}
-   */
-  var changed = false;
+/**
+ * Whether to speak displacement.
+ * @typedef {boolean}
+ */
+var shouldSpeakDisplacement = false;
 
-  /**
-   * Current state vim is in.
-   */
-  var vimState = null;
+/**
+ * Whether text was changed to cause a cursor change event.
+ * @typedef {boolean}
+ */
+var changed = false;
 
-  /**
-   * Mapping from key code to shortcut.
-   */
-  var keyCodeToShortcutMap = {};
+/**
+ * Current state vim is in.
+ */
+var vimState = null;
 
-  /**
-   * Mapping from command to shortcut.
-   */
-  var cmdToShortcutMap = {};
+/**
+ * Mapping from key code to shortcut.
+ */
+var keyCodeToShortcutMap = {};
 
-  /**
-   * Get shortcut string from keyCode.
-   * @param {number} keyCode Key code of shortcut.
-   * @return {string} String representation of shortcut.
-   */
-  var getKeyShortcutString = function(keyCode) {
-    return KEY_PREFIX + String.fromCharCode(keyCode);
-  };
+/**
+ * Mapping from command to shortcut.
+ */
+var cmdToShortcutMap = {};
 
-  /**
-   * Return if in vim mode.
-   * @return {boolean} True if in Vim mode.
-   */
-  var isVimMode = function() {
-    return ace.keyBinding.getKeyboardHandler().$id === 'ace/keyboard/vim';
-  };
+/**
+ * Get shortcut string from keyCode.
+ * @param {number} keyCode Key code of shortcut.
+ * @return {string} String representation of shortcut.
+ */
+var getKeyShortcutString = function(keyCode) {
+  return KEY_PREFIX + String.fromCharCode(keyCode);
+};
 
-  /**
-   * Gets the current token.
-   * @param {!cvoxAce.Cursor} cursor Current position of the cursor.
-   * @return {!cvoxAce.Token} Token at the current position.
-   */
-  var getCurrentToken = function(cursor) {
-    return ace.getSession().getTokenAt(cursor.row, cursor.column + 1);
-  };
+/**
+ * Return if in vim mode.
+ * @return {boolean} True if in Vim mode.
+ */
+var isVimMode = function() {
+  var keyboardHandler = cvoxAce.editor.keyBinding.getKeyboardHandler();
+  return keyboardHandler.$id === 'ace/keyboard/vim';
+};
 
-  /**
-   * Gets the current line the cursor is under.
-   * @param {!cvoxAce.Cursor} cursor Current cursor position.
-   */
-  var getCurrentLine = function(cursor) {
-    return ace.getSession().getLine(cursor.row);
-  };
+/**
+ * Gets the current token.
+ * @param {!cvoxAce.Cursor} cursor Current position of the cursor.
+ * @return {!cvoxAce.Token} Token at the current position.
+ */
+var getCurrentToken = function(cursor) {
+  return cvoxAce.editor.getSession().getTokenAt(cursor.row, cursor.column + 1);
+};
 
-  /**
-   * Event handler for row changes. When the user changes rows we want to speak
-   * the line so the user can work on this line. If shouldSpeakRowLocation is on
-   * then we speak the character, then the row, then the line so the user knows
-   * where the cursor is.
-   * @param {!cvoxAce.Cursor} currCursor Current cursor position.
-   */
-  var onRowChange = function(currCursor) {
-    /* Notify that this line has an annotation. */
-    if (annotTable[currCursor.row]) {
-      cvox.Api.playEarcon(ERROR_EARCON);
-    }
-    if (shouldSpeakRowLocation) {
-      cvox.Api.stop();
-      speakChar(currCursor);
-      speakTokenQueue(getCurrentToken(currCursor));
-      speakLine(currCursor.row, 1);
-    } else {
-      speakLine(currCursor.row, 0);
-    }
-  };
+/**
+ * Gets the current line the cursor is under.
+ * @param {!cvoxAce.Cursor} cursor Current cursor position.
+ */
+var getCurrentLine = function(cursor) {
+  return cvoxAce.editor.getSession().getLine(cursor.row);
+};
 
-  /**
-   * Returns whether the cursor is at the beginning of a word. A word is
-   * a grouping of alphanumeric characters including underscores.
-   * @param {!cvoxAce.Cursor} cursor Current cursor position.
-   * @return {boolean} Whether there is word.
-   */
-  var isWord = function(cursor) {
-    var line = getCurrentLine(cursor);
-    var lineSuffix = line.substr(cursor.column - 1);
-    if (cursor.column === 0) {
-      lineSuffix = ' ' + line;
-    }
-    /* Use regex to tell if the suffix is at the start of a new word. */
-    var firstWordRegExp = /^\W(\w+)/;
-    var words = firstWordRegExp.exec(lineSuffix);
-    return words !== null;
-  };
-
-  /**
-   * A mapping of syntax type to speech properties.
-   */
-  var rules = {
-    'constant': CONSTANT_PROP,
-    'entity': ENTITY_PROP,
-    'keyword': KEYWORD_PROP,
-    'storage': STORAGE_PROP,
-    'variable': VARIABLE_PROP
-  };
-
-  /**
-   * Speak the line with syntax properties.
-   * @param {number} row Row to speak.
-   * @param {number} queue Queue mode to speak.
-   */
-  var speakLine = function(row, queue) {
-    var tokens = ace.getSession().getTokens(row);
-    if (tokens.length === 0) {
-      return;
-    }
-    var firstToken = tokens[0];
-    /* Filter out first token and spaces. */
-    tokens = tokens.filter(function(token) {
-      return token !== firstToken && token.type !== 'text';
-    });
-    /* Speak first token separately to flush if queue. */
-    speakToken_(firstToken, queue);
-    /* Speak rest of tokens. */
-    tokens.forEach(speakTokenQueue);
-  };
-
-  /**
-   * Speak the token based on the syntax of the token, flushing.
-   * @param {!cvoxAce.Token} token Token to speak.
-   * @param {number} queue Queue mode.
-   */
-  var speakTokenFlush = function(token) {
-    speakToken_(token, 0);
-  };
-
-  /**
-   * Speak the token based on the syntax of the token, queueing.
-   * @param {!cvoxAce.Token} token Token to speak.
-   * @param {number} queue Queue mode.
-   */
-  var speakTokenQueue = function(token) {
-    speakToken_(token, 1);
-  };
-
-  /**
-   * Speak the token based on the syntax of the token.
-   * @private
-   * @param {!cvoxAce.Token} token Token to speak.
-   * @param {number} queue Queue mode.
-   */
-  var speakToken_ = function(token, queue) {
-    /* Types are period delimited. In this case, we only syntax speak the outer
-     * most type of token. */
-    if (!token || !token.type) {
-      return;
-    }
-    var split = token.type.split('.');
-    if (split.length === 0) {
-      return;
-    }
-    var type = split[0];
-    var prop = rules[type];
-    if (!prop) {
-      prop = DEFAULT_PROP;
-    }
-    cvox.Api.speak(token.value, queue, prop);
-  };
-
-  /**
-   * Speaks the character under the cursor. This is queued.
-   * @param {!cvoxAce.Cursor} cursor Current cursor position.
-   * @return {string} Character.
-   */
-  var speakChar = function(cursor) {
-    var line = getCurrentLine(cursor);
-    cvox.Api.speak(line[cursor.column], 1);
-  };
-
-  /**
-   * Speaks the jump from lastCursor to currCursor. This function assumes the
-   * jump takes place on the current line.
-   * @param {!cvoxAce.Cursor} lastCursor Previous cursor position.
-   * @param {!cvoxAce.Cursor} currCursor Current cursor position.
-   */
-  var speakDisplacement = function(lastCursor, currCursor) {
+/**
+ * Event handler for row changes. When the user changes rows we want to speak
+ * the line so the user can work on this line. If shouldSpeakRowLocation is on
+ * then we speak the character, then the row, then the line so the user knows
+ * where the cursor is.
+ * @param {!cvoxAce.Cursor} currCursor Current cursor position.
+ */
+var onRowChange = function(currCursor) {
+  /* Notify that this line has an annotation. */
+  if (annotTable[currCursor.row]) {
+    cvox.Api.playEarcon(ERROR_EARCON);
+  }
+  if (shouldSpeakRowLocation) {
     cvox.Api.stop();
-    var line = getCurrentLine(currCursor);
-
-    /* Get the text that we jumped past. */
-    var displace = line.substring(lastCursor.column, currCursor.column);
-    /* When going forward one space, we speak where we land instead. */
-    if (currCursor.column - lastCursor.column === 1) {
-      displace = line.substring(lastCursor.column + 1, currCursor.column + 1);
-    }
-    /* Speak out loud spaces. */
-    displace = displace.replace(/ /g, ' space ');
-    cvox.Api.speak(displace, 1);
-  };
-
-  /**
-   * Speaks the word if the cursor jumped to a new word or to the beginning
-   * of the line. Otherwise speak the charactor.
-   * @param {!cvoxAce.Cursor} lastCursor Previous cursor position.
-   * @param {!cvoxAce.Cursor} currCursor Current cursor position.
-   */
-  var speakCharOrWordOrLine = function(lastCursor, currCursor) {
-    /* Say word only if jump. */
-    if (Math.abs(lastCursor.column - currCursor.column) !== 1) {
-      var currLineLength = getCurrentLine(currCursor).length;
-      /* Speak line if jumping to beginning or end of line. */
-      if (currCursor.column === 0 || currCursor.column === currLineLength) {
-        speakLine(currCursor.row, 0);
-        return;
-      }
-      if (isWord(currCursor)) {
-        cvox.Api.stop();
-        speakTokenQueue(getCurrentToken(currCursor));
-        return;
-      }
-    }
     speakChar(currCursor);
-  };
+    speakTokenQueue(getCurrentToken(currCursor));
+    speakLine(currCursor.row, 1);
+  } else {
+    speakLine(currCursor.row, 0);
+  }
+};
 
-  /**
-   * Event handler for column changes. If shouldSpeakDisplacement is on, then
-   * we just speak displacements in row changes. Otherwise, we either speak
-   * the character for single character movements, the word when jumping to the
-   * next word, or the entire line if jumping to beginning or end of the line.
-   * @param {!cvoxAce.Cursor} lastCursor Previous cursor position.
-   * @param {!cvoxAce.Cursor} currCursor Current cursor position.
-   */
-  var onColumnChange = function(lastCursor, currCursor) {
-    if (shouldSpeakDisplacement) {
-      speakDisplacement(lastCursor, currCursor);
-    } else {
-      speakCharOrWordOrLine(lastCursor, currCursor);
-    }
-  };
+/**
+ * Returns whether the cursor is at the beginning of a word. A word is
+ * a grouping of alphanumeric characters including underscores.
+ * @param {!cvoxAce.Cursor} cursor Current cursor position.
+ * @return {boolean} Whether there is word.
+ */
+var isWord = function(cursor) {
+  var line = getCurrentLine(cursor);
+  var lineSuffix = line.substr(cursor.column - 1);
+  if (cursor.column === 0) {
+    lineSuffix = ' ' + line;
+  }
+  /* Use regex to tell if the suffix is at the start of a new word. */
+  var firstWordRegExp = /^\W(\w+)/;
+  var words = firstWordRegExp.exec(lineSuffix);
+  return words !== null;
+};
 
-  /**
-   * Event handler for cursor changes. Classify cursor changes as either row or
-   * column changes, then delegate accordingly.
-   * @param {!Event} evt The event.
-   */
-  var onCursorChange = function(evt) {
-    /* Do not speak if cursor change was a result of text insertion. We want to
-     * speak the text that was inserted and not where the cursor lands. */
-    if (changed) {
-      changed = false;
+/**
+ * A mapping of syntax type to speech properties.
+ */
+var rules = {
+  'constant': CONSTANT_PROP,
+  'entity': ENTITY_PROP,
+  'keyword': KEYWORD_PROP,
+  'storage': STORAGE_PROP,
+  'variable': VARIABLE_PROP
+};
+
+/**
+ * Speak the line with syntax properties.
+ * @param {number} row Row to speak.
+ * @param {number} queue Queue mode to speak.
+ */
+var speakLine = function(row, queue) {
+  var tokens = cvoxAce.editor.getSession().getTokens(row);
+  if (tokens.length === 0) {
+    return;
+  }
+  var firstToken = tokens[0];
+  /* Filter out first token and spaces. */
+  tokens = tokens.filter(function(token) {
+    return token !== firstToken && token.type !== 'text';
+  });
+  /* Speak first token separately to flush if queue. */
+  speakToken_(firstToken, queue);
+  /* Speak rest of tokens. */
+  tokens.forEach(speakTokenQueue);
+};
+
+/**
+ * Speak the token based on the syntax of the token, flushing.
+ * @param {!cvoxAce.Token} token Token to speak.
+ * @param {number} queue Queue mode.
+ */
+var speakTokenFlush = function(token) {
+  speakToken_(token, 0);
+};
+
+/**
+ * Speak the token based on the syntax of the token, queueing.
+ * @param {!cvoxAce.Token} token Token to speak.
+ * @param {number} queue Queue mode.
+ */
+var speakTokenQueue = function(token) {
+  speakToken_(token, 1);
+};
+
+/**
+ * Speak the token based on the syntax of the token.
+ * @private
+ * @param {!cvoxAce.Token} token Token to speak.
+ * @param {number} queue Queue mode.
+ */
+var speakToken_ = function(token, queue) {
+  /* Types are period delimited. In this case, we only syntax speak the outer
+   * most type of token. */
+  if (!token || !token.type) {
+    return;
+  }
+  var split = token.type.split('.');
+  if (split.length === 0) {
+    return;
+  }
+  var type = split[0];
+  var prop = rules[type];
+  if (!prop) {
+    prop = DEFAULT_PROP;
+  }
+  cvox.Api.speak(token.value, queue, prop);
+};
+
+/**
+ * Speaks the character under the cursor. This is queued.
+ * @param {!cvoxAce.Cursor} cursor Current cursor position.
+ * @return {string} Character.
+ */
+var speakChar = function(cursor) {
+  var line = getCurrentLine(cursor);
+  cvox.Api.speak(line[cursor.column], 1);
+};
+
+/**
+ * Speaks the jump from lastCursor to currCursor. This function assumes the
+ * jump takes place on the current line.
+ * @param {!cvoxAce.Cursor} lastCursor Previous cursor position.
+ * @param {!cvoxAce.Cursor} currCursor Current cursor position.
+ */
+var speakDisplacement = function(lastCursor, currCursor) {
+  cvox.Api.stop();
+  var line = getCurrentLine(currCursor);
+
+  /* Get the text that we jumped past. */
+  var displace = line.substring(lastCursor.column, currCursor.column);
+  /* When going forward one space, we speak where we land instead. */
+  if (currCursor.column - lastCursor.column === 1) {
+    displace = line.substring(lastCursor.column + 1, currCursor.column + 1);
+  }
+  /* Speak out loud spaces. */
+  displace = displace.replace(/ /g, ' space ');
+  cvox.Api.speak(displace, 1);
+};
+
+/**
+ * Speaks the word if the cursor jumped to a new word or to the beginning
+ * of the line. Otherwise speak the charactor.
+ * @param {!cvoxAce.Cursor} lastCursor Previous cursor position.
+ * @param {!cvoxAce.Cursor} currCursor Current cursor position.
+ */
+var speakCharOrWordOrLine = function(lastCursor, currCursor) {
+  /* Say word only if jump. */
+  if (Math.abs(lastCursor.column - currCursor.column) !== 1) {
+    var currLineLength = getCurrentLine(currCursor).length;
+    /* Speak line if jumping to beginning or end of line. */
+    if (currCursor.column === 0 || currCursor.column === currLineLength) {
+      speakLine(currCursor.row, 0);
       return;
     }
-    var currCursor = ace.selection.getCursor();
-    if (currCursor.row !== lastCursor.row) {
-      onRowChange(currCursor);
-    } else {
-      onColumnChange(lastCursor, currCursor);
-    }
-    lastCursor = currCursor;
-  };
-
-  /**
-   * Event handler for source changes. We want auditory feedback for inserting
-   * and deleting text.
-   * @param {!Event} evt The event.
-   */
-  var onChange = function(evt) {
-    var data = evt.data;
-    switch (data.action) {
-    case 'removeText':
-      cvox.Api.speak(data.text, 0, DELETED_PROP);
-      /* Let the future cursor change event know it's from text change. */
-      changed = true;
-      break;
-    case 'insertText':
-      cvox.Api.speak(data.text, 0);
-      /* Let the future cursor change event know it's from text change. */
-      changed = true;
-      break;
-    }
-  };
-
-  /**
-   * Returns whether or not the annotation is new.
-   * @param {!cvoxAce.Annotation} annot Annotation in question.
-   * @return {boolean} Whether annot is new.
-   */
-  var isNewAnnotation = function(annot) {
-    var row = annot.row;
-    var col = annot.column;
-    return !annotTable[row] || !annotTable[row][col];
-  };
-
-  /**
-   * Populates the annotation table.
-   * @param {!Array.} annotations Array of annotations.
-   */
-  var populateAnnotations = function(annotations) {
-    annotTable = {};
-    for (var i = 0; i < annotations.length; i++) {
-      var annotation = annotations[i];
-      var row = annotation.row;
-      var col = annotation.column;
-      if (!annotTable[row]) {
-        annotTable[row] = {};
-      }
-      annotTable[row][col] = annotation;
-    }
-  };
-
-  /**
-   * Event handler for annotation changes. We want to notify the user when an
-   * a new annotation appears.
-   * @param {!Event} evt Event.
-   */
-  var onAnnotationChange = function(evt) {
-    var annotations = ace.getSession().getAnnotations();
-    var newAnnotations = annotations.filter(isNewAnnotation);
-    if (newAnnotations.length > 0) {
-      cvox.Api.playEarcon(ERROR_EARCON);
-    }
-    populateAnnotations(annotations);
-  };
-
-  /**
-   * Speak annotation.
-   * @param {!cvoxAce.Annotation} annot Annotation to speak.
-   */
-  var speakAnnot = function(annot) {
-    var annotText = annot.type + ' ' + annot.text + ' on ' +
-        rowColToString(annot.row, annot.column);
-    annotText = annotText.replace(';', 'semicolon');
-    cvox.Api.speak(annotText, 1);
-  };
-
-  /**
-   * Speak annotations in a row.
-   * @param {number} row Row of annotations to speak.
-   */
-  var speakAnnotsByRow = function(row) {
-    var annots = annotTable[row];
-    for (var col in annots) {
-      speakAnnot(annots[col]);
-    }
-  };
-
-  /**
-   * Get a string representation of a row and column.
-   * @param {boolean} row Zero indexed row.
-   * @param {boolean} col Zero indexed column.
-   * @return {string} Row and column to be spoken.
-   */
-  var rowColToString = function(row, col) {
-    return 'row ' + (row + 1) + ' column ' + (col + 1);
-  };
-
-  /**
-   * Speaks the row and column.
-   */
-  var speakCurrRowAndCol = function() {
-    cvox.Api.speak(rowColToString(lastCursor.row, lastCursor.column));
-  };
-
-  /**
-   * Speaks all annotations.
-   */
-  var speakAllAnnots = function() {
-    for (var row in annotTable) {
-      speakAnnotsByRow(row);
-    }
-  };
-
-  /**
-   * Speak the vim mode. If no vim mode, this function does nothing.
-   */
-  var speakMode = function() {
-    if (!isVimMode()) {
+    if (isWord(currCursor)) {
+      cvox.Api.stop();
+      speakTokenQueue(getCurrentToken(currCursor));
       return;
     }
-    switch (ace.keyBinding.$data.state) {
-    case INSERT_MODE_STATE:
-      cvox.Api.speak('Insert mode');
-      break;
-    case COMMAND_MODE_STATE:
-      cvox.Api.speak('Command mode');
-      break;
-    }
-  };
+  }
+  speakChar(currCursor);
+};
 
-  /**
-   * Toggle speak location.
-   */
-  var toggleSpeakRowLocation = function() {
-    shouldSpeakRowLocation = !shouldSpeakRowLocation;
-    /* Auditory feedback of the change. */
-    if (shouldSpeakRowLocation) {
-      cvox.Api.speak('Speak location on row change enabled.');
-    } else {
-      cvox.Api.speak('Speak location on row change disabled.');
-    }
-  };
+/**
+ * Event handler for column changes. If shouldSpeakDisplacement is on, then
+ * we just speak displacements in row changes. Otherwise, we either speak
+ * the character for single character movements, the word when jumping to the
+ * next word, or the entire line if jumping to beginning or end of the line.
+ * @param {!cvoxAce.Cursor} lastCursor Previous cursor position.
+ * @param {!cvoxAce.Cursor} currCursor Current cursor position.
+ */
+var onColumnChange = function(lastCursor, currCursor) {
+  if (shouldSpeakDisplacement) {
+    speakDisplacement(lastCursor, currCursor);
+  } else {
+    speakCharOrWordOrLine(lastCursor, currCursor);
+  }
+};
 
-  /**
-   * Toggle speak displacement.
-   */
-  var toggleSpeakDisplacement = function() {
-    speakDisplacement = !speakDisplacement;
-    /* Auditory feedback of the change. */
-    if (speakDisplacement) {
-      cvox.Api.speak('Speak displacement on column changes.');
-    } else {
-      cvox.Api.speak('Speak current character or word on column changes.');
-    }
-  };
+/**
+ * Event handler for cursor changes. Classify cursor changes as either row or
+ * column changes, then delegate accordingly.
+ * @param {!Event} evt The event.
+ */
+var onCursorChange = function(evt) {
+  /* Do not speak if cursor change was a result of text insertion. We want to
+   * speak the text that was inserted and not where the cursor lands. */
+  if (changed) {
+    changed = false;
+    return;
+  }
+  var currCursor = cvoxAce.editor.selection.getCursor();
+  if (currCursor.row !== lastCursor.row) {
+    onRowChange(currCursor);
+  } else {
+    onColumnChange(lastCursor, currCursor);
+  }
+  lastCursor = currCursor;
+};
 
-  /**
-   * Event handler for key down events. Gets the right shortcut from the map,
-   * and calls the associated function.
-   * @param {!Event} evt Keyboard event.
-   */
-  var onKeyDown = function(evt) {
-    if (evt.ctrlKey && evt.shiftKey) {
-      var shortcut = keyCodeToShortcutMap[evt.keyCode];
-      if (shortcut) {
-        shortcut.func();
-      }
-    }
-  };
+/**
+ * Event handler for source changes. We want auditory feedback for inserting
+ * and deleting text.
+ * @param {!Event} evt The event.
+ */
+var onChange = function(evt) {
+  var data = evt.data;
+  switch (data.action) {
+  case 'removeText':
+    cvox.Api.speak(data.text, 0, DELETED_PROP);
+    /* Let the future cursor change event know it's from text change. */
+    changed = true;
+    break;
+  case 'insertText':
+    cvox.Api.speak(data.text, 0);
+    /* Let the future cursor change event know it's from text change. */
+    changed = true;
+    break;
+  }
+};
 
-  /**
-   * Event handler for status change events. Auditory feedback of changing
-   * between vim states.
-   * @param {!Event} evt Change status event.
-   * @param {!Object} editor Editor state.
-   */
-  var onChangeStatus = function(evt, editor) {
-    if (!isVimMode()) {
-      return;
-    }
-    var state = editor.keyBinding.$data.state;
-    if (state === vimState) {
-      /* State hasn't changed, do nothing. */
-      return;
-    }
-    switch (state) {
-    case INSERT_MODE_STATE:
-      cvox.Api.playEarcon(MODE_SWITCH_EARCON);
-      /* When in insert mode, we want to speak out keys as feedback. */
-      cvox.Api.setKeyEcho(true);
-      break;
-    case COMMAND_MODE_STATE:
-      cvox.Api.playEarcon(MODE_SWITCH_EARCON);
-      /* When in command mode, we want don't speak out keys because those keys
-      * are not being inserted in the document. */
-      cvox.Api.setKeyEcho(false);
-      break;
-    }
-    vimState = state;
-  };
+/**
+ * Returns whether or not the annotation is new.
+ * @param {!cvoxAce.Annotation} annot Annotation in question.
+ * @return {boolean} Whether annot is new.
+ */
+var isNewAnnotation = function(annot) {
+  var row = annot.row;
+  var col = annot.column;
+  return !annotTable[row] || !annotTable[row][col];
+};
 
-  /**
-   * Handles context menu events. This is a ChromeVox feature where hitting
-   * the shortcut ChromeVox + comma will open up a search bar where you can
-   * type in various commands. All keyboard shortcuts are also commands that
-   * can be invoked. This handles the event that ChromeVox sends to the page.
-   * @param {Event} evt Event received.
-   */
-  var contextMenuHandler = function(evt) {
-    var cmd = evt.detail['customCommand'];
-    var shortcut = cmdToShortcutMap[cmd];
+/**
+ * Populates the annotation table.
+ * @param {!Array.} annotations Array of annotations.
+ */
+var populateAnnotations = function(annotations) {
+  annotTable = {};
+  for (var i = 0; i < annotations.length; i++) {
+    var annotation = annotations[i];
+    var row = annotation.row;
+    var col = annotation.column;
+    if (!annotTable[row]) {
+      annotTable[row] = {};
+    }
+    annotTable[row][col] = annotation;
+  }
+};
+
+/**
+ * Event handler for annotation changes. We want to notify the user when an
+ * a new annotation appears.
+ * @param {!Event} evt Event.
+ */
+var onAnnotationChange = function(evt) {
+  var annotations = cvoxAce.editor.getSession().getAnnotations();
+  var newAnnotations = annotations.filter(isNewAnnotation);
+  if (newAnnotations.length > 0) {
+    cvox.Api.playEarcon(ERROR_EARCON);
+  }
+  populateAnnotations(annotations);
+};
+
+/**
+ * Speak annotation.
+ * @param {!cvoxAce.Annotation} annot Annotation to speak.
+ */
+var speakAnnot = function(annot) {
+  var annotText = annot.type + ' ' + annot.text + ' on ' +
+      rowColToString(annot.row, annot.column);
+  annotText = annotText.replace(';', 'semicolon');
+  cvox.Api.speak(annotText, 1);
+};
+
+/**
+ * Speak annotations in a row.
+ * @param {number} row Row of annotations to speak.
+ */
+var speakAnnotsByRow = function(row) {
+  var annots = annotTable[row];
+  for (var col in annots) {
+    speakAnnot(annots[col]);
+  }
+};
+
+/**
+ * Get a string representation of a row and column.
+ * @param {boolean} row Zero indexed row.
+ * @param {boolean} col Zero indexed column.
+ * @return {string} Row and column to be spoken.
+ */
+var rowColToString = function(row, col) {
+  return 'row ' + (row + 1) + ' column ' + (col + 1);
+};
+
+/**
+ * Speaks the row and column.
+ */
+var speakCurrRowAndCol = function() {
+  cvox.Api.speak(rowColToString(lastCursor.row, lastCursor.column));
+};
+
+/**
+ * Speaks all annotations.
+ */
+var speakAllAnnots = function() {
+  for (var row in annotTable) {
+    speakAnnotsByRow(row);
+  }
+};
+
+/**
+ * Speak the vim mode. If no vim mode, this function does nothing.
+ */
+var speakMode = function() {
+  if (!isVimMode()) {
+    return;
+  }
+  switch (cvoxAce.editor.keyBinding.$data.state) {
+  case INSERT_MODE_STATE:
+    cvox.Api.speak('Insert mode');
+    break;
+  case COMMAND_MODE_STATE:
+    cvox.Api.speak('Command mode');
+    break;
+  }
+};
+
+/**
+ * Toggle speak location.
+ */
+var toggleSpeakRowLocation = function() {
+  shouldSpeakRowLocation = !shouldSpeakRowLocation;
+  /* Auditory feedback of the change. */
+  if (shouldSpeakRowLocation) {
+    cvox.Api.speak('Speak location on row change enabled.');
+  } else {
+    cvox.Api.speak('Speak location on row change disabled.');
+  }
+};
+
+/**
+ * Toggle speak displacement.
+ */
+var toggleSpeakDisplacement = function() {
+  speakDisplacement = !speakDisplacement;
+  /* Auditory feedback of the change. */
+  if (speakDisplacement) {
+    cvox.Api.speak('Speak displacement on column changes.');
+  } else {
+    cvox.Api.speak('Speak current character or word on column changes.');
+  }
+};
+
+/**
+ * Event handler for key down events. Gets the right shortcut from the map,
+ * and calls the associated function.
+ * @param {!Event} evt Keyboard event.
+ */
+var onKeyDown = function(evt) {
+  if (evt.ctrlKey && evt.shiftKey) {
+    var shortcut = keyCodeToShortcutMap[evt.keyCode];
     if (shortcut) {
       shortcut.func();
-      /* ChromeVox will bring focus to an element near the cursor instead of the
-       * text input. */
-      ace.focus();
     }
-  };
+  }
+};
 
-  /**
-   * Initialize the ChromeVox context menu.
-   */
-  var initContextMenu = function() {
-    var ACTIONS = SHORTCUTS.map(function(shortcut) {
-      return {
-        desc: shortcut.desc + getKeyShortcutString(shortcut.keyCode),
-        cmd: shortcut.cmd
-      };
-    });
+/**
+ * Event handler for status change events. Auditory feedback of changing
+ * between vim states.
+ * @param {!Event} evt Change status event.
+ * @param {!Object} editor Editor state.
+ */
+var onChangeStatus = function(evt, editor) {
+  if (!isVimMode()) {
+    return;
+  }
+  var state = editor.keyBinding.$data.state;
+  if (state === vimState) {
+    /* State hasn't changed, do nothing. */
+    return;
+  }
+  switch (state) {
+  case INSERT_MODE_STATE:
+    cvox.Api.playEarcon(MODE_SWITCH_EARCON);
+    /* When in insert mode, we want to speak out keys as feedback. */
+    cvox.Api.setKeyEcho(true);
+    break;
+  case COMMAND_MODE_STATE:
+    cvox.Api.playEarcon(MODE_SWITCH_EARCON);
+    /* When in command mode, we want don't speak out keys because those keys
+    * are not being inserted in the document. */
+    cvox.Api.setKeyEcho(false);
+    break;
+  }
+  vimState = state;
+};
 
-    /* Attach ContextMenuActions. */
-    var body = document.querySelector('body');
-    body.setAttribute('contextMenuActions', JSON.stringify(ACTIONS));
+/**
+ * Handles context menu events. This is a ChromeVox feature where hitting
+ * the shortcut ChromeVox + comma will open up a search bar where you can
+ * type in various commands. All keyboard shortcuts are also commands that
+ * can be invoked. This handles the event that ChromeVox sends to the page.
+ * @param {Event} evt Event received.
+ */
+var contextMenuHandler = function(evt) {
+  var cmd = evt.detail['customCommand'];
+  var shortcut = cmdToShortcutMap[cmd];
+  if (shortcut) {
+    shortcut.func();
+    /* ChromeVox will bring focus to an element near the cursor instead of the
+     * text input. */
+    cvoxAce.editor.focus();
+  }
+};
 
-    /* Listen for ContextMenu events. */
-    body.addEventListener('ATCustomEvent', contextMenuHandler, true);
-  };
-
-  /**
-   * Returns a mutations handler where f is applied to each mutation.
-   * @param {function} f Function to be applied to mutations.
-   */
-  var getMutationHandler = function(f) {
-    return function(mutations) {
-      mutations.forEach(f);
+/**
+ * Initialize the ChromeVox context menu.
+ */
+var initContextMenu = function() {
+  var ACTIONS = SHORTCUTS.map(function(shortcut) {
+    return {
+      desc: shortcut.desc + getKeyShortcutString(shortcut.keyCode),
+      cmd: shortcut.cmd
     };
-  };
+  });
 
-  /**
-   * Watches and handles the mutation that is a result of a search.
-   * @param {Mutation} m Mutation.
-   */
-  var watchForSearch = function(m) {
-    if (m.attributeName === 'class' &&
-        m.target.className === 'ace_search_form ace_nomatch') {
-      /* No match, give auditory feedback! */
-      cvox.Api.playEarcon(NO_MATCH_EARCON);
-    } else {
-      /* There is still a match! Speak the line. */
-      speakLine(lastCursor.row, 0);
-    }
-  };
+  /* Attach ContextMenuActions. */
+  var body = document.querySelector('body');
+  body.setAttribute('contextMenuActions', JSON.stringify(ACTIONS));
 
-  /**
-   * Configuration for mutation observer.
-   */
-  var MO_CONFIG = { attributes: true, childList: true, characterData: true};
+  /* Listen for ContextMenu events. */
+  body.addEventListener('ATCustomEvent', contextMenuHandler, true);
+};
 
-  /**
-   * Watches and handles the mutation that adds the search bar to the DOM.
-   */
-  var watchForStartSearch = function(m) {
-    for (var i = 0; i < m.addedNodes.length; i++) {
-      if (m.addedNodes.item(i).className === 'ace_search right') {
-        var mutationHandler = getMutationHandler(watchForSearch);
-        var searchObs = new MutationObserver(mutationHandler);
-        var target = m.addedNodes.item(i).querySelector('.ace_search_form');
-        searchObs.observe(target, MO_CONFIG);
-      }
-    }
-  };
-
-  /**
-   * Focus to text input.
-   */
-  var focus = function() {
-    ace.focus();
-  };
-
-  /**
-   * Shortcut definitions.
-   */
-  var SHORTCUTS = [
-    {
-      /* 1 key. */
-      keyCode: 49,
-      func: function() {
-        speakAnnotsByRow(lastCursor.row);
-      },
-      cmd: Command.SPEAK_ANNOT,
-      desc: 'Speak annotations on line'
-    },
-    {
-      /* 2 key. */
-      keyCode: 50,
-      func: speakAllAnnots,
-      cmd: Command.SPEAK_ALL_ANNOTS,
-      desc: 'Speak all annotations'
-    },
-    {
-      /* 3 key. */
-      keyCode: 51,
-      func: speakMode,
-      cmd: Command.SPEAK_MODE,
-      desc: 'Speak Vim mode'
-    },
-    {
-      /* 4 key. */
-      keyCode: 52,
-      func: toggleSpeakRowLocation,
-      cmd: Command.TOGGLE_LOCATION,
-      desc: 'Toggle speak row location'
-    },
-    {
-      /* 5 key. */
-      keyCode: 53,
-      func: speakCurrRowAndCol,
-      cmd: Command.SPEAK_ROW_COL,
-      desc: 'Speak row and column'
-    },
-    {
-      /* 6 key. */
-      keyCode: 54,
-      func: toggleSpeakDisplacement,
-      cmd: Command.TOGGLE_DISPLACEMENT,
-      desc: 'Toggle speak displacement'
-    },
-    {
-      /* 7 key. */
-      keyCode: 55,
-      func: focus,
-      cmd: Command.FOCUS_TEXT,
-      desc: 'Focus text'
-    }
-  ];
-
-  /**
-   * Initialize the theme.
-   */
-  var init = function() {
-    /* Construct maps. */
-    SHORTCUTS.forEach(function(shortcut) {
-      keyCodeToShortcutMap[shortcut.keyCode] = shortcut;
-      cmdToShortcutMap[shortcut.cmd] = shortcut;
-    });
-
-    /* Set up listeners. */
-    ace.getSession().selection.on('changeCursor', onCursorChange);
-    ace.getSession().on('change', onChange);
-    ace.getSession().on('changeAnnotation', onAnnotationChange);
-    ace.on('changeStatus', onChangeStatus);
-    window.addEventListener('keydown', onKeyDown);
-
-    /* Assume we start in command mode if vim. */
-    if (isVimMode()) {
-      cvox.Api.setKeyEcho(false);
-    }
-    initContextMenu();
-
-    var target = document.querySelector('.ace_editor');
-    var mutationHandler = getMutationHandler(watchForStartSearch);
-    var observer = new MutationObserver(mutationHandler);
-
-    observer.observe(target, MO_CONFIG);
-
-    ace.focus();
-  };
-
-  /**
-   * Returns if cvox exists, and the api exists.
-   * @return {boolean} Whether not Cvox Api exists.
-   */
-  function cvoxApiExists() {
-    return (typeof(cvox) !== 'undefined') && cvox && cvox.Api;
+var onFindSearchbox = function(evt) {
+  if (evt.match) {
+    /* There is still a match! Speak the line. */
+    speakLine(lastCursor.row, 0);
+  } else {
+    /* No match, give auditory feedback! */
+    cvox.Api.playEarcon(NO_MATCH_EARCON);
   }
+};
 
-  /**
-   * Number of tries for Cvox loading.
-   * @type {number}
-   */
-  var tries = 0;
+/**
+ * Focus to text input.
+ */
+var focus = function() {
+  cvoxAce.editor.focus();
+};
 
-  /**
-   * Max number of tries to watch for Cvox loading.
-   * @type {number}
-   */
-  var MAX_TRIES = 15;
-
-  /**
-   * Check for ChromeVox load.
-   */
-  function watchForCvoxLoad() {
-    if (cvoxApiExists()) {
-      init();
-    } else {
-      tries++;
-      if (tries >= MAX_TRIES) {
-        return;
-      }
-      window.setTimeout(watchForCvoxLoad, 500);
-    }
+/**
+ * Shortcut definitions.
+ */
+var SHORTCUTS = [
+  {
+    /* 1 key. */
+    keyCode: 49,
+    func: function() {
+      speakAnnotsByRow(lastCursor.row);
+    },
+    cmd: Command.SPEAK_ANNOT,
+    desc: 'Speak annotations on line'
+  },
+  {
+    /* 2 key. */
+    keyCode: 50,
+    func: speakAllAnnots,
+    cmd: Command.SPEAK_ALL_ANNOTS,
+    desc: 'Speak all annotations'
+  },
+  {
+    /* 3 key. */
+    keyCode: 51,
+    func: speakMode,
+    cmd: Command.SPEAK_MODE,
+    desc: 'Speak Vim mode'
+  },
+  {
+    /* 4 key. */
+    keyCode: 52,
+    func: toggleSpeakRowLocation,
+    cmd: Command.TOGGLE_LOCATION,
+    desc: 'Toggle speak row location'
+  },
+  {
+    /* 5 key. */
+    keyCode: 53,
+    func: speakCurrRowAndCol,
+    cmd: Command.SPEAK_ROW_COL,
+    desc: 'Speak row and column'
+  },
+  {
+    /* 6 key. */
+    keyCode: 54,
+    func: toggleSpeakDisplacement,
+    cmd: Command.TOGGLE_DISPLACEMENT,
+    desc: 'Toggle speak displacement'
+  },
+  {
+    /* 7 key. */
+    keyCode: 55,
+    func: focus,
+    cmd: Command.FOCUS_TEXT,
+    desc: 'Focus text'
   }
+];
+
+/**
+ * Initialize the theme.
+ */
+var init = function(editor) {
+  cvoxAce.editor = editor;
+  lastCursor = editor.selection.getCursor();
+  /* Construct maps. */
+  SHORTCUTS.forEach(function(shortcut) {
+    keyCodeToShortcutMap[shortcut.keyCode] = shortcut;
+    cmdToShortcutMap[shortcut.cmd] = shortcut;
+  });
+
+  /* Set up listeners. */
+  cvoxAce.editor.getSession().selection.on('changeCursor', onCursorChange);
+  cvoxAce.editor.getSession().on('change', onChange);
+  cvoxAce.editor.getSession().on('changeAnnotation', onAnnotationChange);
+  cvoxAce.editor.on('changeStatus', onChangeStatus);
+  cvoxAce.editor.on('findSearchBox', onFindSearchbox);
+  window.addEventListener('keydown', onKeyDown);
+
+  /* Assume we start in command mode if vim. */
+  if (isVimMode()) {
+    cvox.Api.setKeyEcho(false);
+  }
+  initContextMenu();
+
+  cvoxAce.editor.focus();
+};
+
+/**
+ * Returns if cvox exists, and the api exists.
+ * @return {boolean} Whether not Cvox Api exists.
+ */
+function cvoxApiExists() {
+  return (typeof(cvox) !== 'undefined') && cvox && cvox.Api;
+}
+
+/**
+ * Number of tries for Cvox loading.
+ * @type {number}
+ */
+var tries = 0;
+
+/**
+ * Max number of tries to watch for Cvox loading.
+ * @type {number}
+ */
+var MAX_TRIES = 15;
+
+/**
+ * Check for ChromeVox load.
+ * @param {Object} editor Editor to use.
+ */
+function watchForCvoxLoad(editor) {
+  if (cvoxApiExists()) {
+    init(editor);
+  } else {
+    tries++;
+    if (tries >= MAX_TRIES) {
+      return;
+    }
+    window.setTimeout(watchForCvoxLoad, 500, editor);
+  }
+}
+
+var Editor = require('../editor').Editor;
+require('../config').defineOptions(Editor.prototype, 'editor', {
+  enableChromevoxEnhancements: {
+    set: function(val) {
+      if (val) {
+        watchForCvoxLoad(this);
+      }
+    },
+    value: true // turn it on by default or check for window.cvox
+  }
+});
 
-  /* Initialize everything when ChromeVox has loaded. */
-  watchForCvoxLoad();
 });
diff --git a/lib/ace/ext/searchbox.js b/lib/ace/ext/searchbox.js
index c416b275..89fd1ddc 100644
--- a/lib/ace/ext/searchbox.js
+++ b/lib/ace/ext/searchbox.js
@@ -214,7 +214,9 @@ var SearchBox = function(editor, range, showReplaceForm) {
             caseSensitive: this.caseSensitiveOption.checked,
             wholeWord: this.wholeWordOption.checked
         });
-        dom.setCssClass(this.searchBox, "ace_nomatch", !range && this.searchInput.value);
+        var noMatch = !range && this.searchInput.value;
+        dom.setCssClass(this.searchBox, "ace_nomatch", noMatch);
+        this.editor._emit("findSearchBox", { match: !noMatch });
         this.highlight();
     };
     this.findNext = function() {
diff --git a/lib/ace/ext/themelist_utils/themes.js b/lib/ace/ext/themelist_utils/themes.js
index 6b20770a..2e490c9f 100644
--- a/lib/ace/ext/themelist_utils/themes.js
+++ b/lib/ace/ext/themelist_utils/themes.js
@@ -16,8 +16,8 @@ module.exports.themes = [
     "kr_theme",
     "merbivore",
     "merbivore_soft",
-    "monokai",
     "mono_industrial",
+    "monokai",
     "pastel_on_dark",
     "solarized_dark",
     "solarized_light",

From c3a681abd13ea94a7ec8cb454c8a347a040cf7bd Mon Sep 17 00:00:00 2001
From: Peter Xiao 
Date: Mon, 12 Aug 2013 16:15:09 -0700
Subject: [PATCH 04/13] Less aggressive filter for speaking tokens, add some
 docs.

---
 lib/ace/ext/chromevox.js | 11 +++++++++--
 1 file changed, 9 insertions(+), 2 deletions(-)

diff --git a/lib/ace/ext/chromevox.js b/lib/ace/ext/chromevox.js
index 1f51530a..5ad8ba18 100644
--- a/lib/ace/ext/chromevox.js
+++ b/lib/ace/ext/chromevox.js
@@ -286,9 +286,9 @@ var speakLine = function(row, queue) {
     return;
   }
   var firstToken = tokens[0];
-  /* Filter out first token and spaces. */
+  /* Filter out first token. */
   tokens = tokens.filter(function(token) {
-    return token !== firstToken && token.type !== 'text';
+    return token !== firstToken;
   });
   /* Speak first token separately to flush if queue. */
   speakToken_(firstToken, queue);
@@ -666,6 +666,12 @@ var initContextMenu = function() {
   body.addEventListener('ATCustomEvent', contextMenuHandler, true);
 };
 
+/**
+ * Event handler for find events. When there is a match, we want to speak the
+ * line we are now at. Otherwise, we want to notify the user there was no
+ * match
+ * @param {!Event} evt The event.
+ */
 var onFindSearchbox = function(evt) {
   if (evt.match) {
     /* There is still a match! Speak the line. */
@@ -742,6 +748,7 @@ var SHORTCUTS = [
 
 /**
  * Initialize the theme.
+ * @param {Object} editor Editor to use.
  */
 var init = function(editor) {
   cvoxAce.editor = editor;

From 41c82627f91dbb58ab69191dbbf66ae979e98042 Mon Sep 17 00:00:00 2001
From: Peter Xiao 
Date: Mon, 12 Aug 2013 16:59:12 -0700
Subject: [PATCH 05/13] Update comment, remove code in chromevox ext demo.

---
 demo/chromevox.html | 7 +------
 1 file changed, 1 insertion(+), 6 deletions(-)

diff --git a/demo/chromevox.html b/demo/chromevox.html
index 23c9ba68..d6e481d6 100644
--- a/demo/chromevox.html
+++ b/demo/chromevox.html
@@ -24,7 +24,7 @@
 
 
 
-
+
 
 
 
 

From 0f93b3098f8649f541bc2bc2e7cf92b418ba28c3 Mon Sep 17 00:00:00 2001
From: Peter Xiao 
Date: Tue, 13 Aug 2013 15:34:22 -0700
Subject: [PATCH 06/13] Start merging tokens.

---
 lib/ace/ext/chromevox.js | 42 ++++++++++++++++++++++++++++++++++++++++
 1 file changed, 42 insertions(+)

diff --git a/lib/ace/ext/chromevox.js b/lib/ace/ext/chromevox.js
index 5ad8ba18..3243a7a6 100644
--- a/lib/ace/ext/chromevox.js
+++ b/lib/ace/ext/chromevox.js
@@ -275,6 +275,47 @@ var rules = {
   'variable': VARIABLE_PROP
 };
 
+/**
+ * Merges tokens from start inclusive to end exclusive.
+ * @param {Array.} Tokens to be merged.
+ * @param {number} start Start index inclusive.
+ * @param {number} end End index exclusive.
+ * @return {cvoxAce.Token} Merged token.
+ */
+var mergeTokens = function(tokens, start, end) {
+  /* Different type of token found! Merge all previous like tokens. */
+  var newToken = {};
+  newToken.value = '';
+  newToken.type = tokens[start].type;
+  for (var j = start; j < end; j++) {
+    newToken.value += tokens[j].value;
+  }
+  return newToken;
+};
+
+/**
+ * Merges tokens that use the same speech properties.
+ * @param {Array.} tokens Tokens to be merged.
+ * @return {Array.} Merged tokens.
+ */
+var mergeLikeTokens = function(tokens) {
+  if (tokens.length <= 1) {
+    return tokens;
+  }
+  var newTokens = [];
+  var lastLikeIndex = 0;
+  for (var i = 1; i < tokens.length; i++) {
+    var lastLikeToken = tokens[lastLikeIndex];
+    var currToken = tokens[i];
+    if (getTokenProp(lastLikeToken) !== getTokenProp(currToken)) {
+      newTokens.push(mergeTokens(tokens, lastLikeIndex, i));
+      lastLikeIndex = i;
+    }
+  }
+  newTokens.push(mergeTokens(tokens, lastLikeIndex, tokens.length));
+  return newTokens;
+};
+
 /**
  * Speak the line with syntax properties.
  * @param {number} row Row to speak.
@@ -285,6 +326,7 @@ var speakLine = function(row, queue) {
   if (tokens.length === 0) {
     return;
   }
+  tokens = mergeLikeTokens(tokens);
   var firstToken = tokens[0];
   /* Filter out first token. */
   tokens = tokens.filter(function(token) {

From 16d13b6b5911bf177504c8d9d4e3afaaba5b3018 Mon Sep 17 00:00:00 2001
From: Peter Xiao 
Date: Tue, 13 Aug 2013 15:39:01 -0700
Subject: [PATCH 07/13] Like tokens are now merged for more fluid TTS.

---
 lib/ace/ext/chromevox.js | 22 ++++++++++++++++++++++
 1 file changed, 22 insertions(+)

diff --git a/lib/ace/ext/chromevox.js b/lib/ace/ext/chromevox.js
index 3243a7a6..e51800a4 100644
--- a/lib/ace/ext/chromevox.js
+++ b/lib/ace/ext/chromevox.js
@@ -356,6 +356,28 @@ var speakTokenQueue = function(token) {
   speakToken_(token, 1);
 };
 
+/**
+ * @param {!cvoxAce.Token} token Token to speak.
+ * Get the token speech property.
+ */
+var getTokenProp = function(token) {
+  /* Types are period delimited. In this case, we only syntax speak the outer
+   * most type of token. */
+  if (!token || !token.type) {
+    return;
+  }
+  var split = token.type.split('.');
+  if (split.length === 0) {
+    return;
+  }
+  var type = split[0];
+  var prop = rules[type];
+  if (!prop) {
+    prop = DEFAULT_PROP;
+  }
+  return prop;
+};
+
 /**
  * Speak the token based on the syntax of the token.
  * @private

From 5ea65fe246e4859c941487eb3d7bfe92696d4098 Mon Sep 17 00:00:00 2001
From: Peter Xiao 
Date: Tue, 13 Aug 2013 16:51:12 -0700
Subject: [PATCH 08/13] Add in expansion rules to speak more like how code is
 read.

---
 lib/ace/ext/chromevox.js | 99 +++++++++++++++++++++++++++++-----------
 1 file changed, 73 insertions(+), 26 deletions(-)

diff --git a/lib/ace/ext/chromevox.js b/lib/ace/ext/chromevox.js
index e51800a4..8c8656ef 100644
--- a/lib/ace/ext/chromevox.js
+++ b/lib/ace/ext/chromevox.js
@@ -123,6 +123,17 @@ var NO_MATCH_EARCON = 'INVALID_KEYPRESS';
 var INSERT_MODE_STATE = 'insertMode';
 var COMMAND_MODE_STATE = 'start';
 
+var REPLACE_LIST = [
+  {
+    substr: ';',
+    newSubstr: ' semicolon '
+  },
+  {
+    substr: ':',
+    newSubstr: ' colon '
+  }
+];
+
 /**
  * Context menu commands.
  */
@@ -265,14 +276,59 @@ var isWord = function(cursor) {
 };
 
 /**
- * A mapping of syntax type to speech properties.
+ * A mapping of syntax type to speech properties / expanding rules.
  */
 var rules = {
-  'constant': CONSTANT_PROP,
-  'entity': ENTITY_PROP,
-  'keyword': KEYWORD_PROP,
-  'storage': STORAGE_PROP,
-  'variable': VARIABLE_PROP
+  'constant': {
+    prop: CONSTANT_PROP
+  },
+  'entity': {
+    prop: ENTITY_PROP
+  },
+  'keyword': {
+    prop: KEYWORD_PROP
+  },
+  'storage': {
+    prop: STORAGE_PROP
+  },
+  'variable': {
+    prop: VARIABLE_PROP
+  },
+  'meta': {
+    prop: DEFAULT_PROP,
+    replace: [
+      {
+        substr: '<',
+        newSubstr: ' tag start '
+      },
+      {
+        substr: '>',
+        newSubstr: ' tag end '
+      }
+    ]
+  }
+};
+
+/**
+ * Default rule to be used.
+ */
+var DEFAULT_RULE = {
+  prop: DEFAULT_RULE
+};
+
+/**
+ * Expands substrings to how they are read based on the given rules.
+ * @param {string} value Text to be expanded.
+ * @param {Array.} replaceRules Rules to determine expansion.
+ * @return {string} New expanded value.
+ */
+var expand = function(value, replaceRules) {
+  var newValue = value;
+  for (var i = 0; i < replaceRules.length; i++) {
+    var replaceRule = replaceRules[i];
+    newValue = newValue.replace(replaceRule.substr, replaceRule.newSubstr);
+  }
+  return newValue;
 };
 
 /**
@@ -307,7 +363,7 @@ var mergeLikeTokens = function(tokens) {
   for (var i = 1; i < tokens.length; i++) {
     var lastLikeToken = tokens[lastLikeIndex];
     var currToken = tokens[i];
-    if (getTokenProp(lastLikeToken) !== getTokenProp(currToken)) {
+    if (getTokenRule(lastLikeToken) !== getTokenRule(currToken)) {
       newTokens.push(mergeTokens(tokens, lastLikeIndex, i));
       lastLikeIndex = i;
     }
@@ -360,7 +416,7 @@ var speakTokenQueue = function(token) {
  * @param {!cvoxAce.Token} token Token to speak.
  * Get the token speech property.
  */
-var getTokenProp = function(token) {
+var getTokenRule = function(token) {
   /* Types are period delimited. In this case, we only syntax speak the outer
    * most type of token. */
   if (!token || !token.type) {
@@ -371,11 +427,11 @@ var getTokenProp = function(token) {
     return;
   }
   var type = split[0];
-  var prop = rules[type];
-  if (!prop) {
-    prop = DEFAULT_PROP;
+  var rule = rules[type];
+  if (!rule) {
+    return DEFAULT_RULE;
   }
-  return prop;
+  return rule;
 };
 
 /**
@@ -385,21 +441,12 @@ var getTokenProp = function(token) {
  * @param {number} queue Queue mode.
  */
 var speakToken_ = function(token, queue) {
-  /* Types are period delimited. In this case, we only syntax speak the outer
-   * most type of token. */
-  if (!token || !token.type) {
-    return;
+  var rule = getTokenRule(token);
+  var value = expand(token.value, REPLACE_LIST);
+  if (rule.replace) {
+    value = expand(value, rule.replace);
   }
-  var split = token.type.split('.');
-  if (split.length === 0) {
-    return;
-  }
-  var type = split[0];
-  var prop = rules[type];
-  if (!prop) {
-    prop = DEFAULT_PROP;
-  }
-  cvox.Api.speak(token.value, queue, prop);
+  cvox.Api.speak(value, queue, rule.prop);
 };
 
 /**

From 1d7c9220ee8efccae435aa1912b075ba5039e54b Mon Sep 17 00:00:00 2001
From: Peter Xiao 
Date: Wed, 14 Aug 2013 13:29:34 -0700
Subject: [PATCH 09/13] No need to focus ace, add more expansions.

---
 lib/ace/ext/chromevox.js | 10 +++++++++-
 1 file changed, 9 insertions(+), 1 deletion(-)

diff --git a/lib/ace/ext/chromevox.js b/lib/ace/ext/chromevox.js
index 8c8656ef..5193fafa 100644
--- a/lib/ace/ext/chromevox.js
+++ b/lib/ace/ext/chromevox.js
@@ -304,6 +304,14 @@ var rules = {
       {
         substr: '>',
         newSubstr: ' tag end '
+      },
+      {
+        substr: '',
+        newSubstr: ' close tag '
       }
     ]
   }
@@ -876,7 +884,7 @@ var init = function(editor) {
   cvoxAce.editor.getSession().on('changeAnnotation', onAnnotationChange);
   cvoxAce.editor.on('changeStatus', onChangeStatus);
   cvoxAce.editor.on('findSearchBox', onFindSearchbox);
-  window.addEventListener('keydown', onKeyDown);
+  editor.container.addEventListener('keydown', onKeyDown);
 
   /* Assume we start in command mode if vim. */
   if (isVimMode()) {

From 5708ef899e9324db07ed5c3f1734db7d2f8b697d Mon Sep 17 00:00:00 2001
From: Peter Xiao 
Date: Wed, 14 Aug 2013 13:57:38 -0700
Subject: [PATCH 10/13] Support for multiple editors, reorder rules, and use
 replace all.

---
 lib/ace/ext/chromevox.js | 50 ++++++++++++++++++++++++----------------
 1 file changed, 30 insertions(+), 20 deletions(-)

diff --git a/lib/ace/ext/chromevox.js b/lib/ace/ext/chromevox.js
index 5193fafa..b7632123 100644
--- a/lib/ace/ext/chromevox.js
+++ b/lib/ace/ext/chromevox.js
@@ -297,14 +297,6 @@ var rules = {
   'meta': {
     prop: DEFAULT_PROP,
     replace: [
-      {
-        substr: '<',
-        newSubstr: ' tag start '
-      },
-      {
-        substr: '>',
-        newSubstr: ' tag end '
-      },
       {
         substr: '',
         newSubstr: ' close tag '
+      },
+      {
+        substr: '<',
+        newSubstr: ' tag start '
+      },
+      {
+        substr: '>',
+        newSubstr: ' tag end '
       }
     ]
   }
@@ -334,7 +334,8 @@ var expand = function(value, replaceRules) {
   var newValue = value;
   for (var i = 0; i < replaceRules.length; i++) {
     var replaceRule = replaceRules[i];
-    newValue = newValue.replace(replaceRule.substr, replaceRule.newSubstr);
+    var regexp = new RegExp(replaceRule.substr, 'g');
+    newValue = newValue.replace(regexp, replaceRule.newSubstr);
   }
   return newValue;
 };
@@ -865,34 +866,43 @@ var SHORTCUTS = [
   }
 ];
 
+/**
+ * Event handler for focus events.
+ */
+var onFocus = function() {
+  cvoxAce.editor = editor;
+
+  /* Set up listeners. */
+  editor.getSession().selection.on('changeCursor', onCursorChange);
+  editor.getSession().on('change', onChange);
+  editor.getSession().on('changeAnnotation', onAnnotationChange);
+  editor.on('changeStatus', onChangeStatus);
+  editor.on('findSearchBox', onFindSearchbox);
+  editor.container.addEventListener('keydown', onKeyDown);
+
+  lastCursor = editor.selection.getCursor();
+};
+
 /**
  * Initialize the theme.
  * @param {Object} editor Editor to use.
  */
 var init = function(editor) {
-  cvoxAce.editor = editor;
-  lastCursor = editor.selection.getCursor();
+  onFocus();
+
   /* Construct maps. */
   SHORTCUTS.forEach(function(shortcut) {
     keyCodeToShortcutMap[shortcut.keyCode] = shortcut;
     cmdToShortcutMap[shortcut.cmd] = shortcut;
   });
 
-  /* Set up listeners. */
-  cvoxAce.editor.getSession().selection.on('changeCursor', onCursorChange);
-  cvoxAce.editor.getSession().on('change', onChange);
-  cvoxAce.editor.getSession().on('changeAnnotation', onAnnotationChange);
-  cvoxAce.editor.on('changeStatus', onChangeStatus);
-  cvoxAce.editor.on('findSearchBox', onFindSearchbox);
-  editor.container.addEventListener('keydown', onKeyDown);
+  editor.on('focus', onFocus);
 
   /* Assume we start in command mode if vim. */
   if (isVimMode()) {
     cvox.Api.setKeyEcho(false);
   }
   initContextMenu();
-
-  cvoxAce.editor.focus();
 };
 
 /**

From 1a54f050752b82183b6ff862c1ce27157040de84 Mon Sep 17 00:00:00 2001
From: Peter Xiao 
Date: Thu, 15 Aug 2013 17:18:07 -0700
Subject: [PATCH 11/13] Play earcon on empty or whitespace rows. Add support
 for selection.

---
 lib/ace/ext/chromevox.js | 41 ++++++++++++++++++++++++++++++++--------
 1 file changed, 33 insertions(+), 8 deletions(-)

diff --git a/lib/ace/ext/chromevox.js b/lib/ace/ext/chromevox.js
index b7632123..055a96dd 100644
--- a/lib/ace/ext/chromevox.js
+++ b/lib/ace/ext/chromevox.js
@@ -381,6 +381,17 @@ var mergeLikeTokens = function(tokens) {
   return newTokens;
 };
 
+/**
+ * Returns if given row is a whitespace row.
+ * @param {number} row Row.
+ * @return {boolean} True if row is whitespaces.
+ */
+var isRowWhiteSpace = function(row) {
+  var line = cvoxAce.editor.getSession().getLine(row);
+  var whiteSpaceRegexp = /^\s*$/;
+  return whiteSpaceRegexp.exec(line) !== null;
+};
+
 /**
  * Speak the line with syntax properties.
  * @param {number} row Row to speak.
@@ -388,7 +399,8 @@ var mergeLikeTokens = function(tokens) {
  */
 var speakLine = function(row, queue) {
   var tokens = cvoxAce.editor.getSession().getTokens(row);
-  if (tokens.length === 0) {
+  if (tokens.length === 0 || isRowWhiteSpace(row)) {
+    cvox.Api.playEarcon('EDITABLE_TEXT');
     return;
   }
   tokens = mergeLikeTokens(tokens);
@@ -480,10 +492,7 @@ var speakDisplacement = function(lastCursor, currCursor) {
 
   /* Get the text that we jumped past. */
   var displace = line.substring(lastCursor.column, currCursor.column);
-  /* When going forward one space, we speak where we land instead. */
-  if (currCursor.column - lastCursor.column === 1) {
-    displace = line.substring(lastCursor.column + 1, currCursor.column + 1);
-  }
+
   /* Speak out loud spaces. */
   displace = displace.replace(/ /g, ' space ');
   cvox.Api.speak(displace, 1);
@@ -522,7 +531,11 @@ var speakCharOrWordOrLine = function(lastCursor, currCursor) {
  * @param {!cvoxAce.Cursor} currCursor Current cursor position.
  */
 var onColumnChange = function(lastCursor, currCursor) {
-  if (shouldSpeakDisplacement) {
+  if (!cvoxAce.editor.selection.isEmpty()) {
+    speakDisplacement(lastCursor, currCursor);
+    cvox.Api.speak('selected', 1);
+  }
+  else if (shouldSpeakDisplacement) {
     speakDisplacement(lastCursor, currCursor);
   } else {
     speakCharOrWordOrLine(lastCursor, currCursor);
@@ -550,6 +563,17 @@ var onCursorChange = function(evt) {
   lastCursor = currCursor;
 };
 
+/**
+ * Event handler for selection changes.
+ * @param {!Event} evt The event.
+ */
+var onSelectionChange = function(evt) {
+  /* Assumes that when selection changes to empty, the user has unselected. */
+  if (cvoxAce.editor.selection.isEmpty()) {
+    cvox.Api.speak('unselected');
+  }
+};
+
 /**
  * Event handler for source changes. We want auditory feedback for inserting
  * and deleting text.
@@ -695,9 +719,9 @@ var toggleSpeakRowLocation = function() {
  * Toggle speak displacement.
  */
 var toggleSpeakDisplacement = function() {
-  speakDisplacement = !speakDisplacement;
+  shouldSpeakDisplacement = !shouldSpeakDisplacement;
   /* Auditory feedback of the change. */
-  if (speakDisplacement) {
+  if (shouldSpeakDisplacement) {
     cvox.Api.speak('Speak displacement on column changes.');
   } else {
     cvox.Api.speak('Speak current character or word on column changes.');
@@ -874,6 +898,7 @@ var onFocus = function() {
 
   /* Set up listeners. */
   editor.getSession().selection.on('changeCursor', onCursorChange);
+  editor.getSession().selection.on('changeSelection', onSelectionChange);
   editor.getSession().on('change', onChange);
   editor.getSession().on('changeAnnotation', onAnnotationChange);
   editor.on('changeStatus', onChangeStatus);

From ca1f431df454fb77ee081eb7ee3498f0a58aa4bf Mon Sep 17 00:00:00 2001
From: Peter Xiao 
Date: Thu, 15 Aug 2013 17:20:36 -0700
Subject: [PATCH 12/13] Flush out extra unselected speech.

---
 lib/ace/ext/chromevox.js | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/lib/ace/ext/chromevox.js b/lib/ace/ext/chromevox.js
index 055a96dd..9f7a7996 100644
--- a/lib/ace/ext/chromevox.js
+++ b/lib/ace/ext/chromevox.js
@@ -487,7 +487,6 @@ var speakChar = function(cursor) {
  * @param {!cvoxAce.Cursor} currCursor Current cursor position.
  */
 var speakDisplacement = function(lastCursor, currCursor) {
-  cvox.Api.stop();
   var line = getCurrentLine(currCursor);
 
   /* Get the text that we jumped past. */
@@ -495,7 +494,7 @@ var speakDisplacement = function(lastCursor, currCursor) {
 
   /* Speak out loud spaces. */
   displace = displace.replace(/ /g, ' space ');
-  cvox.Api.speak(displace, 1);
+  cvox.Api.speak(displace);
 };
 
 /**

From 12d2c298bb89665fcf37f0600b9fa8690b1a9c50 Mon Sep 17 00:00:00 2001
From: Peter Xiao 
Date: Thu, 15 Aug 2013 17:40:36 -0700
Subject: [PATCH 13/13] Update old title.

---
 demo/chromevox.html | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/demo/chromevox.html b/demo/chromevox.html
index d6e481d6..9aa65bae 100644
--- a/demo/chromevox.html
+++ b/demo/chromevox.html
@@ -2,7 +2,7 @@
 
 
   
-  ACE Autocompletion demo
+  ACE ChromeVox demo