You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by ga...@apache.org on 2013/10/29 16:40:29 UTC

[44/51] [partial] working replacement

http://git-wip-us.apache.org/repos/asf/couchdb/blob/9abd128c/src/fauxton/assets/js/libs/ace/editor_highlight_selected_word_test.js
----------------------------------------------------------------------
diff --git a/src/fauxton/assets/js/libs/ace/editor_highlight_selected_word_test.js b/src/fauxton/assets/js/libs/ace/editor_highlight_selected_word_test.js
new file mode 100644
index 0000000..13e19c2
--- /dev/null
+++ b/src/fauxton/assets/js/libs/ace/editor_highlight_selected_word_test.js
@@ -0,0 +1,223 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Distributed under the BSD license:
+ *
+ * Copyright (c) 2010, Ajax.org B.V.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ *       notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ *       notice, this list of conditions and the following disclaimer in the
+ *       documentation and/or other materials provided with the distribution.
+ *     * Neither the name of Ajax.org B.V. nor the
+ *       names of its contributors may be used to endorse or promote products
+ *       derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL AJAX.ORG B.V. BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+if (typeof process !== "undefined") {
+    require("amd-loader");
+    require("./test/mockdom");
+}
+
+define(function(require, exports, module) {
+"use strict";
+
+var EditSession = require("./edit_session").EditSession;
+var Editor = require("./editor").Editor;
+var MockRenderer = require("./test/mockrenderer").MockRenderer;
+var assert = require("./test/assertions");
+
+var lipsum = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. " +
+             "Mauris at arcu mi, eu lobortis mauris. Quisque ut libero eget " +
+             "diam congue vehicula. Quisque ut odio ut mi aliquam tincidunt. " +
+             "Duis lacinia aliquam lorem eget eleifend. Morbi eget felis mi. " +
+             "Duis quam ligula, consequat vitae convallis volutpat, blandit " +
+             "nec neque. Nulla facilisi. Etiam suscipit lorem ac justo " +
+             "sollicitudin tristique. Phasellus ut posuere nunc. Aliquam " +
+             "scelerisque mollis felis non gravida. Vestibulum lacus sem, " +
+             "posuere non bibendum id, luctus non dolor. Aenean id metus " +
+             "lorem, vel dapibus est. Donec gravida feugiat augue nec " +
+             "accumsan.Lorem ipsum dolor sit amet, consectetur adipiscing " +
+             "elit. Nulla vulputate, velit vitae tincidunt congue, nunc " +
+             "augue accumsan velit, eu consequat turpis lectus ac orci. " +
+             "Pellentesque ornare dolor feugiat dui auctor eu varius nulla " +
+             "fermentum. Sed aliquam odio at velit lacinia vel fermentum " +
+             "felis sodales. In dignissim magna eget nunc lobortis non " +
+             "fringilla nibh ullamcorper. Donec facilisis malesuada elit " +
+             "at egestas. Etiam bibendum, diam vitae tempor aliquet, dui " +
+             "libero vehicula odio, eget bibendum mauris velit eu lorem.\n" +
+             "consectetur";
+
+function callHighlighterUpdate(session, firstRow, lastRow) {
+    var rangeCount = 0;
+    var  mockMarkerLayer = { drawSingleLineMarker: function() {rangeCount++;} }
+    session.$searchHighlight.update([], mockMarkerLayer, session, {
+        firstRow: firstRow,
+        lastRow: lastRow
+    });
+    return rangeCount;
+}
+
+module.exports = {
+    setUp: function(next) {
+        this.session = new EditSession(lipsum);
+        this.editor = new Editor(new MockRenderer(), this.session);
+        this.selection = this.session.getSelection();
+        this.search = this.editor.$search;
+        next();
+    },
+
+    "test: highlight selected words by default": function() {
+        assert.equal(this.editor.getHighlightSelectedWord(), true);
+    },
+
+    "test: highlight a word": function() {
+        this.editor.moveCursorTo(0, 9);
+        this.selection.selectWord();
+
+        var highlighter = this.editor.session.$searchHighlight;
+        assert.ok(highlighter != null);
+
+        var range = this.selection.getRange();
+        assert.equal(this.session.getTextRange(range), "ipsum");
+        assert.equal(highlighter.cache.length, 0);
+        assert.equal(callHighlighterUpdate(this.session, 0, 0), 2);
+    },
+
+    "test: highlight a word and clear highlight": function() {
+        this.editor.moveCursorTo(0, 8);
+        this.selection.selectWord();
+
+        var range = this.selection.getRange();
+        assert.equal(this.session.getTextRange(range), "ipsum");
+        assert.equal(callHighlighterUpdate(this.session, 0, 0), 2);
+
+        this.session.highlight("");
+        assert.equal(this.session.$searchHighlight.cache.length, 0);
+        assert.equal(callHighlighterUpdate(this.session, 0, 0), 0);
+    },
+
+    "test: highlight another word": function() {
+        this.selection.moveCursorTo(0, 14);
+        this.selection.selectWord();
+
+        var range = this.selection.getRange();
+        assert.equal(this.session.getTextRange(range), "dolor");
+        assert.equal(callHighlighterUpdate(this.session, 0, 0), 4);
+    },
+
+    "test: no selection, no highlight": function() {
+        this.selection.clearSelection();
+        assert.equal(callHighlighterUpdate(this.session, 0, 0), 0);
+    },
+
+    "test: select a word, no highlight": function() {
+        this.selection.moveCursorTo(0, 14);
+        this.selection.selectWord();
+
+        this.editor.setHighlightSelectedWord(false);
+
+        var range = this.selection.getRange();
+        assert.equal(this.session.getTextRange(range), "dolor");
+        assert.equal(callHighlighterUpdate(this.session, 0, 0), 0);
+    },
+
+    "test: select a word with no matches": function() {
+        this.editor.setHighlightSelectedWord(true);
+
+        var currentOptions = this.search.getOptions();
+        var newOptions = {
+            wrap: true,
+            wholeWord: true,
+            caseSensitive: true,
+            needle: "Mauris"
+        };
+        this.search.set(newOptions);
+
+        var match = this.search.find(this.session);
+        assert.notEqual(match, null, "found a match for 'Mauris'");
+
+        this.search.set(currentOptions);
+
+        this.selection.setSelectionRange(match);
+
+        assert.equal(this.session.getTextRange(match), "Mauris");
+        assert.equal(callHighlighterUpdate(this.session, 0, 0), 1);
+    },
+
+    "test: partial word selection 1": function() {
+        this.selection.moveCursorTo(0, 14);
+        this.selection.selectWord();
+        this.selection.selectLeft();
+
+        var range = this.selection.getRange();
+        assert.equal(this.session.getTextRange(range), "dolo");
+        assert.equal(callHighlighterUpdate(this.session, 0, 0), 0);
+    },
+
+    "test: partial word selection 2": function() {
+        this.selection.moveCursorTo(0, 13);
+        this.selection.selectWord();
+        this.selection.selectRight();
+
+        var range = this.selection.getRange();
+        assert.equal(this.session.getTextRange(range), "dolor ");
+        assert.equal(callHighlighterUpdate(this.session, 0, 0), 0);
+    },
+
+    "test: partial word selection 3": function() {
+        this.selection.moveCursorTo(0, 14);
+        this.selection.selectWord();
+        this.selection.selectLeft();
+        this.selection.shiftSelection(1);
+
+        var range = this.selection.getRange();
+        assert.equal(this.session.getTextRange(range), "olor");
+        assert.equal(callHighlighterUpdate(this.session, 0, 0), 0);
+    },
+
+    "test: select last word": function() {
+        this.selection.moveCursorTo(0, 1);
+
+        var currentOptions = this.search.getOptions();
+        var newOptions = {
+            wrap: true,
+            wholeWord: true,
+            caseSensitive: true,
+            backwards: true,
+            needle: "consectetur"
+        };
+        this.search.set(newOptions);
+
+        var match = this.search.find(this.session);
+        assert.notEqual(match, null, "found a match for 'consectetur'");
+        assert.position(match.start, 1, 0);
+
+        this.search.set(currentOptions);
+
+        this.selection.setSelectionRange(match);
+
+        assert.equal(this.session.getTextRange(match), "consectetur");
+        assert.equal(callHighlighterUpdate(this.session, 0, 1), 3);
+    }
+};
+
+});
+
+if (typeof module !== "undefined" && module === require.main) {
+    require("asyncjs").test.testcase(module.exports).exec();
+}

http://git-wip-us.apache.org/repos/asf/couchdb/blob/9abd128c/src/fauxton/assets/js/libs/ace/editor_navigation_test.js
----------------------------------------------------------------------
diff --git a/src/fauxton/assets/js/libs/ace/editor_navigation_test.js b/src/fauxton/assets/js/libs/ace/editor_navigation_test.js
new file mode 100644
index 0000000..ab34824
--- /dev/null
+++ b/src/fauxton/assets/js/libs/ace/editor_navigation_test.js
@@ -0,0 +1,164 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Distributed under the BSD license:
+ *
+ * Copyright (c) 2010, Ajax.org B.V.
+ * All rights reserved.
+ * 
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ *       notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ *       notice, this list of conditions and the following disclaimer in the
+ *       documentation and/or other materials provided with the distribution.
+ *     * Neither the name of Ajax.org B.V. nor the
+ *       names of its contributors may be used to endorse or promote products
+ *       derived from this software without specific prior written permission.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL AJAX.ORG B.V. BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+if (typeof process !== "undefined") {
+    require("amd-loader");
+    require("./test/mockdom");
+}
+
+define(function(require, exports, module) {
+"use strict";
+
+var EditSession = require("./edit_session").EditSession;
+var Editor = require("./editor").Editor;
+var MockRenderer = require("./test/mockrenderer").MockRenderer;
+var assert = require("./test/assertions");
+
+module.exports = {
+    createEditSession : function(rows, cols) {
+        var line = new Array(cols + 1).join("a");
+        var text = new Array(rows).join(line + "\n") + line;
+        return new EditSession(text);
+    },
+
+    "test: navigate to end of file should scroll the last line into view" : function() {
+        var doc = this.createEditSession(200, 10);
+        var editor = new Editor(new MockRenderer(), doc);
+
+        editor.navigateFileEnd();
+        var cursor = editor.getCursorPosition();
+
+        assert.ok(editor.getFirstVisibleRow() <= cursor.row);
+        assert.ok(editor.getLastVisibleRow() >= cursor.row);
+    },
+
+    "test: navigate to start of file should scroll the first row into view" : function() {
+        var doc = this.createEditSession(200, 10);
+        var editor = new Editor(new MockRenderer(), doc);
+
+        editor.moveCursorTo(editor.getLastVisibleRow() + 20);
+        editor.navigateFileStart();
+
+        assert.equal(editor.getFirstVisibleRow(), 0);
+    },
+
+    "test: goto hidden line should scroll the line into the middle of the viewport" : function() {
+        var editor = new Editor(new MockRenderer(), this.createEditSession(200, 5));
+
+        editor.navigateTo(0, 0);
+        editor.gotoLine(101);
+        assert.position(editor.getCursorPosition(), 100, 0);
+        assert.equal(editor.getFirstVisibleRow(), 89);
+
+        editor.navigateTo(100, 0);
+        editor.gotoLine(11);
+        assert.position(editor.getCursorPosition(), 10, 0);
+        assert.equal(editor.getFirstVisibleRow(), 0);
+
+        editor.navigateTo(100, 0);
+        editor.gotoLine(6);
+        assert.position(editor.getCursorPosition(), 5, 0);
+        assert.equal(0, editor.getFirstVisibleRow(), 0);
+
+        editor.navigateTo(100, 0);
+        editor.gotoLine(1);
+        assert.position(editor.getCursorPosition(), 0, 0);
+        assert.equal(editor.getFirstVisibleRow(), 0);
+
+        editor.navigateTo(0, 0);
+        editor.gotoLine(191);
+        assert.position(editor.getCursorPosition(), 190, 0);
+        assert.equal(editor.getFirstVisibleRow(), 179);
+
+        editor.navigateTo(0, 0);
+        editor.gotoLine(196);
+        assert.position(editor.getCursorPosition(), 195, 0);
+        assert.equal(editor.getFirstVisibleRow(), 180);
+    },
+
+    "test: goto visible line should only move the cursor and not scroll": function() {
+        var editor = new Editor(new MockRenderer(), this.createEditSession(200, 5));
+
+        editor.navigateTo(0, 0);
+        editor.gotoLine(12);
+        assert.position(editor.getCursorPosition(), 11, 0);
+        assert.equal(editor.getFirstVisibleRow(), 0);
+
+        editor.navigateTo(30, 0);
+        editor.gotoLine(33);
+        assert.position(editor.getCursorPosition(), 32, 0);
+        assert.equal(editor.getFirstVisibleRow(), 30);
+    },
+
+    "test: navigate from the end of a long line down to a short line and back should maintain the curser column": function() {
+        var editor = new Editor(new MockRenderer(), new EditSession(["123456", "1"]));
+
+        editor.navigateTo(0, 6);
+        assert.position(editor.getCursorPosition(), 0, 6);
+
+        editor.navigateDown();
+        assert.position(editor.getCursorPosition(), 1, 1);
+
+        editor.navigateUp();
+        assert.position(editor.getCursorPosition(), 0, 6);
+    },
+
+    "test: reset desired column on navigate left or right": function() {
+        var editor = new Editor(new MockRenderer(), new EditSession(["123456", "12"]));
+
+        editor.navigateTo(0, 6);
+        assert.position(editor.getCursorPosition(), 0, 6);
+
+        editor.navigateDown();
+        assert.position(editor.getCursorPosition(), 1, 2);
+
+        editor.navigateLeft();
+        assert.position(editor.getCursorPosition(), 1, 1);
+
+        editor.navigateUp();
+        assert.position(editor.getCursorPosition(), 0, 1);
+    },
+    
+    "test: typing text should update the desired column": function() {
+        var editor = new Editor(new MockRenderer(), new EditSession(["1234", "1234567890"]));
+
+        editor.navigateTo(0, 3);
+        editor.insert("juhu");
+        
+        editor.navigateDown();
+        assert.position(editor.getCursorPosition(), 1, 7);
+    }
+};
+
+});
+
+if (typeof module !== "undefined" && module === require.main) {
+    require("asyncjs").test.testcase(module.exports).exec()
+}

http://git-wip-us.apache.org/repos/asf/couchdb/blob/9abd128c/src/fauxton/assets/js/libs/ace/editor_text_edit_test.js
----------------------------------------------------------------------
diff --git a/src/fauxton/assets/js/libs/ace/editor_text_edit_test.js b/src/fauxton/assets/js/libs/ace/editor_text_edit_test.js
new file mode 100644
index 0000000..77ec34e
--- /dev/null
+++ b/src/fauxton/assets/js/libs/ace/editor_text_edit_test.js
@@ -0,0 +1,557 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Distributed under the BSD license:
+ *
+ * Copyright (c) 2010, Ajax.org B.V.
+ * All rights reserved.
+ * 
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ *       notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ *       notice, this list of conditions and the following disclaimer in the
+ *       documentation and/or other materials provided with the distribution.
+ *     * Neither the name of Ajax.org B.V. nor the
+ *       names of its contributors may be used to endorse or promote products
+ *       derived from this software without specific prior written permission.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL AJAX.ORG B.V. BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+if (typeof process !== "undefined") {
+    require("amd-loader");
+    require("./test/mockdom");
+}
+
+define(function(require, exports, module) {
+"use strict";
+
+var EditSession = require("./edit_session").EditSession;
+var Editor = require("./editor").Editor;
+var JavaScriptMode = require("./mode/javascript").Mode;
+var UndoManager = require("./undomanager").UndoManager;
+var MockRenderer = require("./test/mockrenderer").MockRenderer;
+var assert = require("./test/assertions");
+var whitespace = require("./ext/whitespace");
+
+module.exports = {
+    "test: delete line from the middle" : function() {
+        var session = new EditSession(["a", "b", "c", "d"].join("\n"));
+        var editor = new Editor(new MockRenderer(), session);
+
+        editor.moveCursorTo(1, 1);
+        editor.removeLines();
+
+        assert.equal(session.toString(), "a\nc\nd");
+        assert.position(editor.getCursorPosition(), 1, 0);
+
+        editor.removeLines();
+
+        assert.equal(session.toString(), "a\nd");
+        assert.position(editor.getCursorPosition(), 1, 0);
+
+        editor.removeLines();
+
+        assert.equal(session.toString(), "a");
+        assert.position(editor.getCursorPosition(), 0, 1);
+
+        editor.removeLines();
+
+        assert.equal(session.toString(), "");
+        assert.position(editor.getCursorPosition(), 0, 0);
+    },
+
+    "test: delete multiple selected lines" : function() {
+        var session = new EditSession(["a", "b", "c", "d"].join("\n"));
+        var editor = new Editor(new MockRenderer(), session);
+
+        editor.moveCursorTo(1, 1);
+        editor.getSelection().selectDown();
+
+        editor.removeLines();
+        assert.equal(session.toString(), "a\nd");
+        assert.position(editor.getCursorPosition(), 1, 0);
+    },
+
+    "test: delete first line" : function() {
+        var session = new EditSession(["a", "b", "c"].join("\n"));
+        var editor = new Editor(new MockRenderer(), session);
+
+        editor.removeLines();
+
+        assert.equal(session.toString(), "b\nc");
+        assert.position(editor.getCursorPosition(), 0, 0);
+    },
+
+    "test: delete last should also delete the new line of the previous line" : function() {
+        var session = new EditSession(["a", "b", "c", ""].join("\n"));
+        var editor = new Editor(new MockRenderer(), session);
+
+        editor.moveCursorTo(3, 0);
+
+        editor.removeLines();
+        assert.equal(session.toString(), "a\nb\nc");
+        assert.position(editor.getCursorPosition(), 2, 1);
+
+        editor.removeLines();
+        assert.equal(session.toString(), "a\nb");
+        assert.position(editor.getCursorPosition(), 1, 1);
+    },
+
+    "test: indent block" : function() {
+        var session = new EditSession(["a12345", "b12345", "c12345"].join("\n"));
+        var editor = new Editor(new MockRenderer(), session);
+
+        editor.moveCursorTo(1, 3);
+        editor.getSelection().selectDown();
+
+        editor.indent();
+
+        assert.equal(["a12345", "    b12345", "    c12345"].join("\n"), session.toString());
+
+        assert.position(editor.getCursorPosition(), 2, 7);
+
+        var range = editor.getSelectionRange();
+        assert.position(range.start, 1, 7);
+        assert.position(range.end, 2, 7);
+    },
+
+    "test: indent selected lines" : function() {
+        var session = new EditSession(["a12345", "b12345", "c12345"].join("\n"));
+        var editor = new Editor(new MockRenderer(), session);
+
+        editor.moveCursorTo(1, 0);
+        editor.getSelection().selectDown();
+
+        editor.indent();
+        assert.equal(["a12345", "    b12345", "c12345"].join("\n"), session.toString());
+    },
+
+    "test: no auto indent if cursor is before the {" : function() {
+        var session = new EditSession("{", new JavaScriptMode());
+        var editor = new Editor(new MockRenderer(), session);
+
+        editor.moveCursorTo(0, 0);
+        editor.onTextInput("\n");
+        assert.equal(["", "{"].join("\n"), session.toString());
+    },
+    
+    "test: outdent block" : function() {
+        var session = new EditSession(["        a12345", "    b12345", "        c12345"].join("\n"));
+        var editor = new Editor(new MockRenderer(), session);
+
+        editor.moveCursorTo(0, 5);
+        editor.getSelection().selectDown();
+        editor.getSelection().selectDown();
+
+        editor.blockOutdent();
+        assert.equal(session.toString(), ["    a12345", "b12345", "    c12345"].join("\n"));
+
+        assert.position(editor.getCursorPosition(), 2, 1);
+
+        var range = editor.getSelectionRange();
+        assert.position(range.start, 0, 1);
+        assert.position(range.end, 2, 1);
+
+        editor.blockOutdent();
+        assert.equal(session.toString(), ["a12345", "b12345", "c12345"].join("\n"));
+
+        var range = editor.getSelectionRange();
+        assert.position(range.start, 0, 0);
+        assert.position(range.end, 2, 0);
+    },
+
+    "test: outent without a selection should update cursor" : function() {
+        var session = new EditSession("        12");
+        var editor = new Editor(new MockRenderer(), session);
+
+        editor.moveCursorTo(0, 3);
+        editor.blockOutdent("  ");
+
+        assert.equal(session.toString(), "    12");
+        assert.position(editor.getCursorPosition(), 0, 0);
+    },
+
+    "test: comment lines should perserve selection" : function() {
+        var session = new EditSession(["  abc", "cde"].join("\n"), new JavaScriptMode());
+        var editor = new Editor(new MockRenderer(), session);
+        whitespace.detectIndentation(session);
+        
+        editor.moveCursorTo(0, 2);
+        editor.getSelection().selectDown();
+        editor.toggleCommentLines();
+
+        assert.equal(["//   abc", "// cde"].join("\n"), session.toString());
+
+        var selection = editor.getSelectionRange();
+        assert.position(selection.start, 0, 5);
+        assert.position(selection.end, 1, 5);
+    },
+
+    "test: uncomment lines should perserve selection" : function() {
+        var session = new EditSession(["//   abc", "//cde"].join("\n"), new JavaScriptMode());
+        var editor = new Editor(new MockRenderer(), session);
+        session.setTabSize(2);
+
+        editor.moveCursorTo(0, 1);
+        editor.getSelection().selectDown();
+        editor.getSelection().selectRight();
+        editor.getSelection().selectRight();
+
+        editor.toggleCommentLines();
+
+        assert.equal(["  abc", "cde"].join("\n"), session.toString());
+        assert.range(editor.getSelectionRange(), 0, 0, 1, 1);
+    },
+
+    "test: toggle comment lines twice should return the original text" : function() {
+        var session = new EditSession(["  abc", "cde", "fg"], new JavaScriptMode());
+        var editor = new Editor(new MockRenderer(), session);
+
+        editor.moveCursorTo(0, 0);
+        editor.getSelection().selectDown();
+        editor.getSelection().selectDown();
+
+        editor.toggleCommentLines();
+        editor.toggleCommentLines();
+
+        assert.equal(["  abc", "cde", "fg"].join("\n"), session.toString());
+    },
+
+
+    "test: comment lines - if the selection end is at the line start it should stay there": function() {
+        //select down
+        var session = new EditSession(["abc", "cde"].join("\n"), new JavaScriptMode());
+        var editor = new Editor(new MockRenderer(), session);
+
+        editor.moveCursorTo(0, 0);
+        editor.getSelection().selectDown();
+
+        editor.toggleCommentLines();
+        assert.range(editor.getSelectionRange(), 0, 3, 1, 0);
+
+        // select up
+        var session = new EditSession(["abc", "cde"].join("\n"), new JavaScriptMode());
+        var editor = new Editor(new MockRenderer(), session);
+
+        editor.moveCursorTo(1, 0);
+        editor.getSelection().selectUp();
+
+        editor.toggleCommentLines();
+        assert.range(editor.getSelectionRange(), 0, 3, 1, 0);
+    },
+
+    "test: move lines down should keep selection on moved lines" : function() {
+        var session = new EditSession(["11", "22", "33", "44"].join("\n"));
+        var editor = new Editor(new MockRenderer(), session);
+
+        editor.moveCursorTo(0, 1);
+        editor.getSelection().selectDown();
+
+        editor.moveLinesDown();
+        assert.equal(["33", "11", "22", "44"].join("\n"), session.toString());
+        assert.position(editor.getCursorPosition(), 2, 1);
+        assert.position(editor.getSelection().getSelectionAnchor(), 1, 1);
+        assert.position(editor.getSelection().getSelectionLead(), 2, 1);
+
+        editor.moveLinesDown();
+        assert.equal(["33", "44", "11", "22"].join("\n"), session.toString());
+        assert.position(editor.getCursorPosition(), 3, 1);
+        assert.position(editor.getSelection().getSelectionAnchor(), 2, 1);
+        assert.position(editor.getSelection().getSelectionLead(), 3, 1);
+
+        // moving again should have no effect
+        editor.moveLinesDown();
+        assert.equal(["33", "44", "11", "22"].join("\n"), session.toString());
+        assert.position(editor.getCursorPosition(), 3, 1);
+        assert.position(editor.getSelection().getSelectionAnchor(), 2, 1);
+        assert.position(editor.getSelection().getSelectionLead(), 3, 1);
+    },
+
+    "test: move lines up should keep selection on moved lines" : function() {
+        var session = new EditSession(["11", "22", "33", "44"].join("\n"));
+        var editor = new Editor(new MockRenderer(), session);
+
+        editor.moveCursorTo(2, 1);
+        editor.getSelection().selectDown();
+
+        editor.moveLinesUp();
+        assert.equal(session.toString(), ["11", "33", "44", "22"].join("\n"));
+        assert.position(editor.getCursorPosition(), 2, 1);
+        assert.position(editor.getSelection().getSelectionAnchor(), 1, 1);
+        assert.position(editor.getSelection().getSelectionLead(), 2, 1);
+
+        editor.moveLinesUp();
+        assert.equal(session.toString(), ["33", "44", "11", "22"].join("\n"));
+        assert.position(editor.getCursorPosition(), 1, 1);
+        assert.position(editor.getSelection().getSelectionAnchor(), 0, 1);
+        assert.position(editor.getSelection().getSelectionLead(), 1, 1);
+    },
+
+    "test: move line without active selection should not move cursor relative to the moved line" : function() {
+        var session = new EditSession(["11", "22", "33", "44"].join("\n"));
+        var editor = new Editor(new MockRenderer(), session);
+
+        editor.moveCursorTo(1, 1);
+        editor.clearSelection();
+
+        editor.moveLinesDown();
+        assert.equal(["11", "33", "22", "44"].join("\n"), session.toString());
+        assert.position(editor.getCursorPosition(), 2, 1);
+
+        editor.clearSelection();
+
+        editor.moveLinesUp();
+        assert.equal(["11", "22", "33", "44"].join("\n"), session.toString());
+        assert.position(editor.getCursorPosition(), 1, 1);
+    },
+
+    "test: copy lines down should keep selection" : function() {
+        var session = new EditSession(["11", "22", "33", "44"].join("\n"));
+        var editor = new Editor(new MockRenderer(), session);
+
+        editor.moveCursorTo(1, 1);
+        editor.getSelection().selectDown();
+
+        editor.copyLinesDown();
+        assert.equal(["11", "22", "33", "22", "33", "44"].join("\n"), session.toString());
+
+        assert.position(editor.getCursorPosition(), 4, 1);
+        assert.position(editor.getSelection().getSelectionAnchor(), 3, 1);
+        assert.position(editor.getSelection().getSelectionLead(), 4, 1);
+    },
+
+    "test: copy lines up should keep selection" : function() {
+        var session = new EditSession(["11", "22", "33", "44"].join("\n"));
+        var editor = new Editor(new MockRenderer(), session);
+
+        editor.moveCursorTo(1, 1);
+        editor.getSelection().selectDown();
+
+        editor.copyLinesUp();
+        assert.equal(["11", "22", "33", "22", "33", "44"].join("\n"), session.toString());
+
+        assert.position(editor.getCursorPosition(), 2, 1);
+        assert.position(editor.getSelection().getSelectionAnchor(), 1, 1);
+        assert.position(editor.getSelection().getSelectionLead(), 2, 1);
+    },
+
+    "test: input a tab with soft tab should convert it to spaces" : function() {
+        var session = new EditSession("");
+        var editor = new Editor(new MockRenderer(), session);
+
+        session.setTabSize(2);
+        session.setUseSoftTabs(true);
+
+        editor.onTextInput("\t");
+        assert.equal(session.toString(), "  ");
+
+        session.setTabSize(5);
+        editor.onTextInput("\t");
+        assert.equal(session.toString(), "       ");
+    },
+
+    "test: input tab without soft tabs should keep the tab character" : function() {
+        var session = new EditSession("");
+        var editor = new Editor(new MockRenderer(), session);
+
+        session.setUseSoftTabs(false);
+
+        editor.onTextInput("\t");
+        assert.equal(session.toString(), "\t");
+    },
+
+    "test: undo/redo for delete line" : function() {
+        var session = new EditSession(["111", "222", "333"]);
+        var undoManager = new UndoManager();
+        session.setUndoManager(undoManager);
+
+        var initialText = session.toString();
+        var editor = new Editor(new MockRenderer(), session);
+
+        editor.removeLines();
+        var step1 = session.toString();
+        assert.equal(step1, "222\n333");
+        session.$syncInformUndoManager();
+
+        editor.removeLines();
+        var step2 = session.toString();
+        assert.equal(step2, "333");
+        session.$syncInformUndoManager();
+
+        editor.removeLines();
+        var step3 = session.toString();
+        assert.equal(step3, "");
+        session.$syncInformUndoManager();
+
+        undoManager.undo();
+        session.$syncInformUndoManager();
+        assert.equal(session.toString(), step2);
+
+        undoManager.undo();
+        session.$syncInformUndoManager();
+        assert.equal(session.toString(), step1);
+
+        undoManager.undo();
+        session.$syncInformUndoManager();
+        assert.equal(session.toString(), initialText);
+
+        undoManager.undo();
+        session.$syncInformUndoManager();
+        assert.equal(session.toString(), initialText);
+    },
+
+    "test: remove left should remove character left of the cursor" : function() {
+        var session = new EditSession(["123", "456"]);
+
+        var editor = new Editor(new MockRenderer(), session);
+        editor.moveCursorTo(1, 1);
+        editor.remove("left");
+        assert.equal(session.toString(), "123\n56");
+    },
+
+    "test: remove left should remove line break if cursor is at line start" : function() {
+        var session = new EditSession(["123", "456"]);
+
+        var editor = new Editor(new MockRenderer(), session);
+        editor.moveCursorTo(1, 0);
+        editor.remove("left");
+        assert.equal(session.toString(), "123456");
+    },
+
+    "test: remove left should remove tabsize spaces if cursor is on a tab stop and preceeded by spaces" : function() {
+        var session = new EditSession(["123", "        456"]);
+        session.setUseSoftTabs(true);
+        session.setTabSize(4);
+
+        var editor = new Editor(new MockRenderer(), session);
+        editor.moveCursorTo(1, 8);
+        editor.remove("left");
+        assert.equal(session.toString(), "123\n    456");
+    },
+    
+    "test: transpose at line start should be a noop": function() {
+        var session = new EditSession(["123", "4567", "89"]);
+        
+        var editor = new Editor(new MockRenderer(), session);
+        editor.moveCursorTo(1, 0);
+        editor.transposeLetters();
+        
+        assert.equal(session.getValue(), ["123", "4567", "89"].join("\n"));
+    },
+    
+    "test: transpose in line should swap the charaters before and after the cursor": function() {
+        var session = new EditSession(["123", "4567", "89"]);
+        
+        var editor = new Editor(new MockRenderer(), session);
+        editor.moveCursorTo(1, 2);
+        editor.transposeLetters();
+        
+        assert.equal(session.getValue(), ["123", "4657", "89"].join("\n"));
+    },
+    
+    "test: transpose at line end should swap the last two characters": function() {
+        var session = new EditSession(["123", "4567", "89"]);
+        
+        var editor = new Editor(new MockRenderer(), session);
+        editor.moveCursorTo(1, 4);
+        editor.transposeLetters();
+        
+        assert.equal(session.getValue(), ["123", "4576", "89"].join("\n"));
+    },
+    
+    "test: transpose with non empty selection should be a noop": function() {
+        var session = new EditSession(["123", "4567", "89"]);
+        
+        var editor = new Editor(new MockRenderer(), session);
+        editor.moveCursorTo(1, 1);
+        editor.getSelection().selectRight();
+        editor.transposeLetters();
+        
+        assert.equal(session.getValue(), ["123", "4567", "89"].join("\n"));
+    },
+    
+    "test: transpose should move the cursor behind the last swapped character": function() {
+        var session = new EditSession(["123", "4567", "89"]);
+        
+        var editor = new Editor(new MockRenderer(), session);
+        editor.moveCursorTo(1, 2);
+        editor.transposeLetters();
+        assert.position(editor.getCursorPosition(), 1, 3);
+    },
+    
+    "test: remove to line end": function() {
+        var session = new EditSession(["123", "4567", "89"]);
+        
+        var editor = new Editor(new MockRenderer(), session);
+        editor.moveCursorTo(1, 2);
+        editor.removeToLineEnd();
+        assert.equal(session.getValue(), ["123", "45", "89"].join("\n"));
+    },
+    
+    "test: remove to line end at line end should remove the new line": function() {
+        var session = new EditSession(["123", "4567", "89"]);
+        
+        var editor = new Editor(new MockRenderer(), session);
+        editor.moveCursorTo(1, 4);
+        editor.removeToLineEnd();
+        assert.position(editor.getCursorPosition(), 1, 4);
+        assert.equal(session.getValue(), ["123", "456789"].join("\n"));
+    },
+
+    "test: transform selection to uppercase": function() {
+        var session = new EditSession(["ajax", "dot", "org"]);
+
+        var editor = new Editor(new MockRenderer(), session);
+        editor.moveCursorTo(1, 0);
+        editor.getSelection().selectLineEnd();
+        editor.toUpperCase()
+        assert.equal(session.getValue(), ["ajax", "DOT", "org"].join("\n"));
+    },
+
+    "test: transform word to uppercase": function() {
+        var session = new EditSession(["ajax", "dot", "org"]);
+
+        var editor = new Editor(new MockRenderer(), session);
+        editor.moveCursorTo(1, 0);
+        editor.toUpperCase()
+        assert.equal(session.getValue(), ["ajax", "DOT", "org"].join("\n"));
+        assert.position(editor.getCursorPosition(), 1, 0);
+    },
+
+    "test: transform selection to lowercase": function() {
+        var session = new EditSession(["AJAX", "DOT", "ORG"]);
+
+        var editor = new Editor(new MockRenderer(), session);
+        editor.moveCursorTo(1, 0);
+        editor.getSelection().selectLineEnd();
+        editor.toLowerCase()
+        assert.equal(session.getValue(), ["AJAX", "dot", "ORG"].join("\n"));
+    },
+
+    "test: transform word to lowercase": function() {
+        var session = new EditSession(["AJAX", "DOT", "ORG"]);
+
+        var editor = new Editor(new MockRenderer(), session);
+        editor.moveCursorTo(1, 0);
+        editor.toLowerCase()
+        assert.equal(session.getValue(), ["AJAX", "dot", "ORG"].join("\n"));
+        assert.position(editor.getCursorPosition(), 1, 0);
+    }
+};
+
+});
+
+if (typeof module !== "undefined" && module === require.main) {
+    require("asyncjs").test.testcase(module.exports).exec()
+}

http://git-wip-us.apache.org/repos/asf/couchdb/blob/9abd128c/src/fauxton/assets/js/libs/ace/ext/chromevox.js
----------------------------------------------------------------------
diff --git a/src/fauxton/assets/js/libs/ace/ext/chromevox.js b/src/fauxton/assets/js/libs/ace/ext/chromevox.js
new file mode 100644
index 0000000..9f7a799
--- /dev/null
+++ b/src/fauxton/assets/js/libs/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.<number, Object<number, cvoxAce.Annotation>>}
+ */
+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.<Object>} 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];
+    var regexp = new RegExp(replaceRule.substr, 'g');
+    newValue = newValue.replace(regexp, replaceRule.newSubstr);
+  }
+  return newValue;
+};
+
+/**
+ * Merges tokens from start inclusive to end exclusive.
+ * @param {Array.<cvoxAce.Token>} 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.<cvoxAce.Token>} tokens Tokens to be merged.
+ * @return {Array.<cvoxAce.Token>} 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 (getTokenRule(lastLikeToken) !== getTokenRule(currToken)) {
+      newTokens.push(mergeTokens(tokens, lastLikeIndex, i));
+      lastLikeIndex = i;
+    }
+  }
+  newTokens.push(mergeTokens(tokens, lastLikeIndex, tokens.length));
+  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.
+ * @param {number} queue Queue mode to speak.
+ */
+var speakLine = function(row, queue) {
+  var tokens = cvoxAce.editor.getSession().getTokens(row);
+  if (tokens.length === 0 || isRowWhiteSpace(row)) {
+    cvox.Api.playEarcon('EDITABLE_TEXT');
+    return;
+  }
+  tokens = mergeLikeTokens(tokens);
+  var firstToken = tokens[0];
+  /* Filter out first token. */
+  tokens = tokens.filter(function(token) {
+    return token !== firstToken;
+  });
+  /* 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);
+};
+
+/**
+ * @param {!cvoxAce.Token} token Token to speak.
+ * Get the token speech property.
+ */
+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) {
+    return;
+  }
+  var split = token.type.split('.');
+  if (split.length === 0) {
+    return;
+  }
+  var type = split[0];
+  var rule = rules[type];
+  if (!rule) {
+    return DEFAULT_RULE;
+  }
+  return rule;
+};
+
+/**
+ * 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) {
+  var rule = getTokenRule(token);
+  var value = expand(token.value, REPLACE_LIST);
+  if (rule.replace) {
+    value = expand(value, rule.replace);
+  }
+  cvox.Api.speak(value, queue, rule.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) {
+  var line = getCurrentLine(currCursor);
+
+  /* Get the text that we jumped past. */
+  var displace = line.substring(lastCursor.column, currCursor.column);
+
+  /* Speak out loud spaces. */
+  displace = displace.replace(/ /g, ' space ');
+  cvox.Api.speak(displace);
+};
+
+/**
+ * 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 (!cvoxAce.editor.selection.isEmpty()) {
+    speakDisplacement(lastCursor, currCursor);
+    cvox.Api.speak('selected', 1);
+  }
+  else 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 = cvoxAce.editor.selection.getCursor();
+  if (currCursor.row !== lastCursor.row) {
+    onRowChange(currCursor);
+  } else {
+    onColumnChange(lastCursor, currCursor);
+  }
+  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.
+ * @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.<cvoxAce.Annotation>} 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() {
+  shouldSpeakDisplacement = !shouldSpeakDisplacement;
+  /* Auditory feedback of the change. */
+  if (shouldSpeakDisplacement) {
+    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. */
+    cvoxAce.editor.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);
+};
+
+/**
+ * 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. */
+    speakLine(lastCursor.row, 0);
+  } else {
+    /* No match, give auditory feedback! */
+    cvox.Api.playEarcon(NO_MATCH_EARCON);
+  }
+};
+
+/**
+ * Focus to text input.
+ */
+var focus = function() {
+  cvoxAce.editor.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'
+  }
+];
+
+/**
+ * Event handler for focus events.
+ */
+var onFocus = function() {
+  cvoxAce.editor = editor;
+
+  /* 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);
+  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) {
+  onFocus();
+
+  /* Construct maps. */
+  SHORTCUTS.forEach(function(shortcut) {
+    keyCodeToShortcutMap[shortcut.keyCode] = shortcut;
+    cmdToShortcutMap[shortcut.cmd] = shortcut;
+  });
+
+  editor.on('focus', onFocus);
+
+  /* Assume we start in command mode if vim. */
+  if (isVimMode()) {
+    cvox.Api.setKeyEcho(false);
+  }
+  initContextMenu();
+};
+
+/**
+ * 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
+  }
+});
+
+});

http://git-wip-us.apache.org/repos/asf/couchdb/blob/9abd128c/src/fauxton/assets/js/libs/ace/ext/elastic_tabstops_lite.js
----------------------------------------------------------------------
diff --git a/src/fauxton/assets/js/libs/ace/ext/elastic_tabstops_lite.js b/src/fauxton/assets/js/libs/ace/ext/elastic_tabstops_lite.js
new file mode 100644
index 0000000..9901c5d
--- /dev/null
+++ b/src/fauxton/assets/js/libs/ace/ext/elastic_tabstops_lite.js
@@ -0,0 +1,319 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Distributed under the BSD license:
+ *
+ * Copyright (c) 2012, Ajax.org B.V.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ *       notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ *       notice, this list of conditions and the following disclaimer in the
+ *       documentation and/or other materials provided with the distribution.
+ *     * Neither the name of Ajax.org B.V. nor the
+ *       names of its contributors may be used to endorse or promote products
+ *       derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL AJAX.ORG B.V. BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+define(function(require, exports, module) {
+"use strict";
+
+var ElasticTabstopsLite = function(editor) {
+    this.$editor = editor;
+    var self = this;
+    var changedRows = [];
+    var recordChanges = false;
+    this.onAfterExec = function() {
+        recordChanges = false;
+        self.processRows(changedRows);
+        changedRows = [];
+    };
+    this.onExec = function() {
+        recordChanges = true;
+    };
+    this.onChange = function(e) {
+        var range = e.data.range
+        if (recordChanges) {
+            if (changedRows.indexOf(range.start.row) == -1)
+                changedRows.push(range.start.row);
+            if (range.end.row != range.start.row)
+                changedRows.push(range.end.row);
+        }
+    };
+};
+
+(function() {
+    this.processRows = function(rows) {
+        this.$inChange = true;
+        var checkedRows = [];
+
+        for (var r = 0, rowCount = rows.length; r < rowCount; r++) {
+            var row = rows[r];
+
+            if (checkedRows.indexOf(row) > -1)
+                continue;
+
+            var cellWidthObj = this.$findCellWidthsForBlock(row);
+            var cellWidths = this.$setBlockCellWidthsToMax(cellWidthObj.cellWidths);
+            var rowIndex = cellWidthObj.firstRow;
+
+            for (var w = 0, l = cellWidths.length; w < l; w++) {
+                var widths = cellWidths[w];
+                checkedRows.push(rowIndex);
+                this.$adjustRow(rowIndex, widths);
+                rowIndex++;
+            }
+        }
+        this.$inChange = false;
+    };
+
+    this.$findCellWidthsForBlock = function(row) {
+        var cellWidths = [], widths;
+
+        // starting row and backward
+        var rowIter = row;
+        while (rowIter >= 0) {
+            widths = this.$cellWidthsForRow(rowIter);
+            if (widths.length == 0)
+                break;
+
+            cellWidths.unshift(widths);
+            rowIter--;
+        }
+        var firstRow = rowIter + 1;
+
+        // forward (not including starting row)
+        rowIter = row;
+        var numRows = this.$editor.session.getLength();
+
+        while (rowIter < numRows - 1) {
+            rowIter++;
+
+            widths = this.$cellWidthsForRow(rowIter);
+            if (widths.length == 0)
+                break;
+
+            cellWidths.push(widths);
+        }
+
+        return { cellWidths: cellWidths, firstRow: firstRow };
+    };
+
+    this.$cellWidthsForRow = function(row) {
+        var selectionColumns = this.$selectionColumnsForRow(row);
+        // todo: support multicursor
+
+        var tabs = [-1].concat(this.$tabsForRow(row));
+        var widths = tabs.map(function(el) { return 0; } ).slice(1);
+        var line = this.$editor.session.getLine(row);
+
+        for (var i = 0, len = tabs.length - 1; i < len; i++) {
+            var leftEdge = tabs[i]+1;
+            var rightEdge = tabs[i+1];
+
+            var rightmostSelection = this.$rightmostSelectionInCell(selectionColumns, rightEdge);
+            var cell = line.substring(leftEdge, rightEdge);
+            widths[i] = Math.max(cell.replace(/\s+$/g,'').length, rightmostSelection - leftEdge);
+        }
+
+        return widths;
+    };
+
+    this.$selectionColumnsForRow = function(row) {
+        var selections = [], cursor = this.$editor.getCursorPosition();
+        if (this.$editor.session.getSelection().isEmpty()) {
+            // todo: support multicursor
+            if (row == cursor.row)
+                selections.push(cursor.column);
+        }
+
+        return selections;
+    };
+
+    this.$setBlockCellWidthsToMax = function(cellWidths) {
+        var startingNewBlock = true, blockStartRow, blockEndRow, maxWidth;
+        var columnInfo = this.$izip_longest(cellWidths);
+
+        for (var c = 0, l = columnInfo.length; c < l; c++) {
+            var column = columnInfo[c];
+            if (!column.push) {
+                console.error(column);
+                continue;
+            }
+            // add an extra None to the end so that the end of the column automatically
+            // finishes a block
+            column.push(NaN);
+
+            for (var r = 0, s = column.length; r < s; r++) {
+                var width = column[r];
+                if (startingNewBlock) {
+                    blockStartRow = r;
+                    maxWidth = 0;
+                    startingNewBlock = false;
+                }
+                if (isNaN(width)) {
+                    // block ended
+                    blockEndRow = r;
+
+                    for (var j = blockStartRow; j < blockEndRow; j++) {
+                        cellWidths[j][c] = maxWidth;
+                    }
+                    startingNewBlock = true;
+                }
+
+                maxWidth = Math.max(maxWidth, width);
+            }
+        }
+
+        return cellWidths;
+    };
+
+    this.$rightmostSelectionInCell = function(selectionColumns, cellRightEdge) {
+        var rightmost = 0;
+
+        if (selectionColumns.length) {
+            var lengths = [];
+            for (var s = 0, length = selectionColumns.length; s < length; s++) {
+                if (selectionColumns[s] <= cellRightEdge)
+                    lengths.push(s);
+                else
+                    lengths.push(0);
+            }
+            rightmost = Math.max.apply(Math, lengths);
+        }
+
+        return rightmost;
+    };
+
+    this.$tabsForRow = function(row) {
+        var rowTabs = [], line = this.$editor.session.getLine(row),
+            re = /\t/g, match;
+
+        while ((match = re.exec(line)) != null) {
+            rowTabs.push(match.index);
+        }
+
+        return rowTabs;
+    };
+
+    this.$adjustRow = function(row, widths) {
+        var rowTabs = this.$tabsForRow(row);
+
+        if (rowTabs.length == 0)
+            return;
+
+        var bias = 0, location = -1;
+
+        // this always only contains two elements, so we're safe in the loop below
+        var expandedSet = this.$izip(widths, rowTabs);
+
+        for (var i = 0, l = expandedSet.length; i < l; i++) {
+            var w = expandedSet[i][0], it = expandedSet[i][1];
+            location += 1 + w;
+            it += bias;
+            var difference = location - it;
+
+            if (difference == 0)
+                continue;
+
+            var partialLine = this.$editor.session.getLine(row).substr(0, it );
+            var strippedPartialLine = partialLine.replace(/\s*$/g, "");
+            var ispaces = partialLine.length - strippedPartialLine.length;
+
+            if (difference > 0) {
+                // put the spaces after the tab and then delete the tab, so any insertion
+                // points behave as expected
+                this.$editor.session.getDocument().insertInLine({row: row, column: it + 1}, Array(difference + 1).join(" ") + "\t");
+                this.$editor.session.getDocument().removeInLine(row, it, it + 1);
+
+                bias += difference;
+            }
+
+            if (difference < 0 && ispaces >= -difference) {
+                this.$editor.session.getDocument().removeInLine(row, it + difference, it);
+                bias += difference;
+            }
+        }
+    };
+
+    // the is a (naive) Python port--but works for these purposes
+    this.$izip_longest = function(iterables) {
+        if (!iterables[0])
+            return [];
+        var longest = iterables[0].length;
+        var iterablesLength = iterables.length;
+
+        for (var i = 1; i < iterablesLength; i++) {
+            var iLength = iterables[i].length;
+            if (iLength > longest)
+                longest = iLength;
+        }
+
+        var expandedSet = [];
+
+        for (var l = 0; l < longest; l++) {
+            var set = [];
+            for (var i = 0; i < iterablesLength; i++) {
+                if (iterables[i][l] === "")
+                    set.push(NaN);
+                else
+                    set.push(iterables[i][l]);
+            }
+
+            expandedSet.push(set);
+        }
+
+
+        return expandedSet;
+    };
+
+    // an even more (naive) Python port
+    this.$izip = function(widths, tabs) {
+        // grab the shorter size
+        var size = widths.length >= tabs.length ? tabs.length : widths.length;
+
+        var expandedSet = [];
+        for (var i = 0; i < size; i++) {
+            var set = [ widths[i], tabs[i] ];
+            expandedSet.push(set);
+        }
+        return expandedSet;
+    };
+
+}).call(ElasticTabstopsLite.prototype);
+
+exports.ElasticTabstopsLite = ElasticTabstopsLite;
+
+var Editor = require("../editor").Editor;
+require("../config").defineOptions(Editor.prototype, "editor", {
+    useElasticTabstops: {
+        set: function(val) {
+            if (val) {
+                if (!this.elasticTabstops)
+                    this.elasticTabstops = new ElasticTabstopsLite(this);
+                this.commands.on("afterExec", this.elasticTabstops.onAfterExec);
+                this.commands.on("exec", this.elasticTabstops.onExec);
+                this.on("change", this.elasticTabstops.onChange);
+            } else if (this.elasticTabstops) {
+                this.commands.removeListener("afterExec", this.elasticTabstops.onAfterExec);
+                this.commands.removeListener("exec", this.elasticTabstops.onExec);
+                this.removeListener("change", this.elasticTabstops.onChange);
+            }
+        }
+    }
+});
+
+});
\ No newline at end of file