diff --git a/demo/chromevox.html b/demo/chromevox.html
new file mode 100644
index 00000000..9aa65bae
--- /dev/null
+++ b/demo/chromevox.html
@@ -0,0 +1,39 @@
+
+
+
+
+ ACE ChromeVox demo
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/lib/ace/ext/chromevox.js b/lib/ace/ext/chromevox.js
new file mode 100644
index 00000000..9f7a7996
--- /dev/null
+++ b/lib/ace/ext/chromevox.js
@@ -0,0 +1,980 @@
+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';
+
+var REPLACE_LIST = [
+ {
+ substr: ';',
+ newSubstr: ' semicolon '
+ },
+ {
+ substr: ':',
+ newSubstr: ' colon '
+ }
+];
+
+/**
+ * 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. */
+cvoxAce.editor = null;
+/**
+ * Last cursor position.
+ * @type {cvoxAce.Cursor}
+ */
+var lastCursor = null;
+
+/**
+ * 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() {
+ var keyboardHandler = cvoxAce.editor.keyBinding.getKeyboardHandler();
+ return keyboardHandler.$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 cvoxAce.editor.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 cvoxAce.editor.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 / expanding rules.
+ */
+var rules = {
+ '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: ' closing tag '
+ },
+ {
+ substr: '/>',
+ newSubstr: ' close tag '
+ },
+ {
+ 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.