adobe/brackets

View on GitHub
src/editor/Editor.js

Summary

Maintainability
F
1 wk
Test Coverage
/*
 * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 *
 */

/**
 * Editor is a 1-to-1 wrapper for a CodeMirror editor instance. It layers on Brackets-specific
 * functionality and provides APIs that cleanly pass through the bits of CodeMirror that the rest
 * of our codebase may want to interact with. An Editor is always backed by a Document, and stays
 * in sync with its content; because Editor keeps the Document alive, it's important to always
 * destroy() an Editor that's going away so it can release its Document ref.
 *
 * For now, there's a distinction between the "master" Editor for a Document - which secretly acts
 * as the Document's internal model of the text state - and the multitude of "slave" secondary Editors
 * which, via Document, sync their changes to and from that master.
 *
 * For now, direct access to the underlying CodeMirror object is still possible via `_codeMirror` --
 * but this is considered deprecated and may go away.
 *
 * The Editor object dispatches the following events:
 *    - keydown, keypress, keyup -- When any key event happens in the editor (whether it changes the
 *      text or not). Handlers are passed `(BracketsEvent, Editor, KeyboardEvent)`. The 3nd arg is the
 *      raw DOM event. Note: most listeners will only want to listen for "keypress".
 *    - cursorActivity -- When the user moves the cursor or changes the selection, or an edit occurs.
 *      Note: do not listen to this in order to be generally informed of edits--listen to the
 *      "change" event on Document instead.
 *    - scroll -- When the editor is scrolled, either by user action or programmatically.
 *    - lostContent -- When the backing Document changes in such a way that this Editor is no longer
 *      able to display accurate text. This occurs if the Document's file is deleted, or in certain
 *      Document->editor syncing edge cases that we do not yet support (the latter cause will
 *      eventually go away).
 *    - optionChange -- Triggered when an option for the editor is changed. The 2nd arg to the listener
 *      is a string containing the editor option that is changing. The 3rd arg, which can be any
 *      data type, is the new value for the editor option.
 *    - beforeDestroy - Triggered before the object is about to dispose of all its internal state data
 *      so that listeners can cache things like scroll pos, etc...
 *
 * The Editor also dispatches "change" events internally, but you should listen for those on
 * Documents, not Editors.
 *
 * To listen for events, do something like this: (see EventDispatcher for details on this pattern)
 *     `editorInstance.on("eventname", handler);`
 */
define(function (require, exports, module) {
    "use strict";

    var AnimationUtils     = require("utils/AnimationUtils"),
        Async              = require("utils/Async"),
        CodeMirror         = require("thirdparty/CodeMirror/lib/codemirror"),
        LanguageManager    = require("language/LanguageManager"),
        EventDispatcher    = require("utils/EventDispatcher"),
        Menus              = require("command/Menus"),
        PerfUtils          = require("utils/PerfUtils"),
        PopUpManager       = require("widgets/PopUpManager"),
        PreferencesManager = require("preferences/PreferencesManager"),
        Strings            = require("strings"),
        TextRange          = require("document/TextRange").TextRange,
        TokenUtils         = require("utils/TokenUtils"),
        ValidationUtils    = require("utils/ValidationUtils"),
        HTMLUtils          = require("language/HTMLUtils"),
        ViewUtils          = require("utils/ViewUtils"),
        MainViewManager    = require("view/MainViewManager"),
        _                  = require("thirdparty/lodash");

    /** Editor preferences */

    var CLOSE_BRACKETS      = "closeBrackets",
        CLOSE_TAGS          = "closeTags",
        DRAG_DROP           = "dragDropText",
        HIGHLIGHT_MATCHES   = "highlightMatches",
        LINEWISE_COPY_CUT   = "lineWiseCopyCut",
        SCROLL_PAST_END     = "scrollPastEnd",
        SHOW_CURSOR_SELECT  = "showCursorWhenSelecting",
        SHOW_LINE_NUMBERS   = "showLineNumbers",
        SMART_INDENT        = "smartIndent",
        SOFT_TABS           = "softTabs",
        SPACE_UNITS         = "spaceUnits",
        STYLE_ACTIVE_LINE   = "styleActiveLine",
        TAB_SIZE            = "tabSize",
        UPPERCASE_COLORS    = "uppercaseColors",
        USE_TAB_CHAR        = "useTabChar",
        WORD_WRAP           = "wordWrap",
        AUTO_HIDE_SEARCH    = "autoHideSearch",
        INDENT_LINE_COMMENT   = "indentLineComment",
        INDENT_LINE_COMMENT = "indentLineComment",
        INPUT_STYLE         = "inputStyle";


    /**
      * A list of gutter name and priorities currently registered for editors.
      * The line number gutter is defined as { name: LINE_NUMBER_GUTTER, priority: 100 }
      * @type {Array.<{name: string, priority: number, languageIds: Array}}
      */
    var registeredGutters = [];

    var cmOptions         = {};

    /**
     * Constants
     * @type {number}
     */
    var MIN_SPACE_UNITS         =  1,
        MIN_TAB_SIZE            =  1,
        DEFAULT_SPACE_UNITS     =  4,
        DEFAULT_TAB_SIZE        =  4,
        MAX_SPACE_UNITS         = 10,
        MAX_TAB_SIZE            = 10;

    var LINE_NUMBER_GUTTER = "CodeMirror-linenumbers",
        LINE_NUMBER_GUTTER_PRIORITY     = 100,
        CODE_FOLDING_GUTTER_PRIORITY    = 1000;

    // Mappings from Brackets preferences to CodeMirror options
    cmOptions[CLOSE_BRACKETS]     = "autoCloseBrackets";
    cmOptions[CLOSE_TAGS]         = "autoCloseTags";
    cmOptions[DRAG_DROP]          = "dragDrop";
    cmOptions[HIGHLIGHT_MATCHES]  = "highlightSelectionMatches";
    cmOptions[LINEWISE_COPY_CUT]  = "lineWiseCopyCut";
    cmOptions[SCROLL_PAST_END]    = "scrollPastEnd";
    cmOptions[SHOW_CURSOR_SELECT] = "showCursorWhenSelecting";
    cmOptions[SHOW_LINE_NUMBERS]  = "lineNumbers";
    cmOptions[SMART_INDENT]       = "smartIndent";
    cmOptions[SPACE_UNITS]        = "indentUnit";
    cmOptions[STYLE_ACTIVE_LINE]  = "styleActiveLine";
    cmOptions[TAB_SIZE]           = "tabSize";
    cmOptions[USE_TAB_CHAR]       = "indentWithTabs";
    cmOptions[WORD_WRAP]          = "lineWrapping";
    cmOptions[INPUT_STYLE]        = "inputStyle";

    PreferencesManager.definePreference(CLOSE_BRACKETS,     "boolean", true, {
        description: Strings.DESCRIPTION_CLOSE_BRACKETS
    });

    // CodeMirror, html mode, set some tags do not close automatically.
    // We do not initialize "dontCloseTags" because otherwise we would overwrite the default behavior of CodeMirror.
    PreferencesManager.definePreference(CLOSE_TAGS,         "object", { whenOpening: true, whenClosing: true, indentTags: [] }, {
        description: Strings.DESCRIPTION_CLOSE_TAGS,
        keys: {
            dontCloseTags: {
                type: "array",
                description: Strings.DESCRIPTION_CLOSE_TAGS_DONT_CLOSE_TAGS
            },
            whenOpening: {
                type: "boolean",
                description: Strings.DESCRIPTION_CLOSE_TAGS_WHEN_OPENING,
                initial: true
            },
            whenClosing: {
                type: "boolean",
                description: Strings.DESCRIPTION_CLOSE_TAGS_WHEN_CLOSING,
                initial: true
            },
            indentTags: {
                type: "array",
                description: Strings.DESCRIPTION_CLOSE_TAGS_INDENT_TAGS
            }
        }
    });
    PreferencesManager.definePreference(DRAG_DROP,          "boolean", false, {
        description: Strings.DESCRIPTION_DRAG_DROP_TEXT
    });
    PreferencesManager.definePreference(HIGHLIGHT_MATCHES,  "boolean", false, {
        description: Strings.DESCRIPTION_HIGHLIGHT_MATCHES,
        keys: {
            showToken: {
                type: "boolean",
                description: Strings.DESCRIPTION_HIGHLIGHT_MATCHES_SHOW_TOKEN,
                initial: false
            },
            wordsOnly: {
                type: "boolean",
                description: Strings.DESCRIPTION_HIGHLIGHT_MATCHES_WORDS_ONLY,
                initial: false
            }
        }
    });
    PreferencesManager.definePreference(LINEWISE_COPY_CUT,  "boolean", true, {
        description: Strings.DESCRIPTION_LINEWISE_COPY_CUT
    });
    PreferencesManager.definePreference(SCROLL_PAST_END,    "boolean", false, {
        description: Strings.DESCRIPTION_SCROLL_PAST_END
    });
    PreferencesManager.definePreference(SHOW_CURSOR_SELECT, "boolean", false, {
        description: Strings.DESCRIPTION_SHOW_CURSOR_WHEN_SELECTING
    });
    PreferencesManager.definePreference(SHOW_LINE_NUMBERS,  "boolean", true, {
        description: Strings.DESCRIPTION_SHOW_LINE_NUMBERS
    });
    PreferencesManager.definePreference(SMART_INDENT,       "boolean", true, {
        description: Strings.DESCRIPTION_SMART_INDENT
    });
    PreferencesManager.definePreference(SOFT_TABS,          "boolean", true, {
        description: Strings.DESCRIPTION_SOFT_TABS
    });
    PreferencesManager.definePreference(SPACE_UNITS,        "number", DEFAULT_SPACE_UNITS, {
        validator: _.partialRight(ValidationUtils.isIntegerInRange, MIN_SPACE_UNITS, MAX_SPACE_UNITS),
        description: Strings.DESCRIPTION_SPACE_UNITS
    });
    PreferencesManager.definePreference(STYLE_ACTIVE_LINE,  "boolean", false, {
        description: Strings.DESCRIPTION_STYLE_ACTIVE_LINE
    });
    PreferencesManager.definePreference(TAB_SIZE,           "number", DEFAULT_TAB_SIZE, {
        validator: _.partialRight(ValidationUtils.isIntegerInRange, MIN_TAB_SIZE, MAX_TAB_SIZE),
        description: Strings.DESCRIPTION_TAB_SIZE
    });
    PreferencesManager.definePreference(UPPERCASE_COLORS,   "boolean", false, {
        description: Strings.DESCRIPTION_UPPERCASE_COLORS
    });
    PreferencesManager.definePreference(USE_TAB_CHAR,       "boolean", false, {
        description: Strings.DESCRIPTION_USE_TAB_CHAR
    });
    PreferencesManager.definePreference(WORD_WRAP,          "boolean", true, {
        description: Strings.DESCRIPTION_WORD_WRAP
    });
  
    PreferencesManager.definePreference(AUTO_HIDE_SEARCH,   "boolean", true, {
        description: Strings.DESCRIPTION_SEARCH_AUTOHIDE
    });

    PreferencesManager.definePreference(INDENT_LINE_COMMENT,  "boolean", false, {
        description: Strings.DESCRIPTION_INDENT_LINE_COMMENT
    });
    PreferencesManager.definePreference(INPUT_STYLE,  "string", "textarea", {
        description: Strings.DESCRIPTION_INPUT_STYLE
    });

    var editorOptions = Object.keys(cmOptions);

    /** Editor preferences */

    /**
     * Guard flag to prevent focus() reentrancy (via blur handlers), even across Editors
     * @type {boolean}
     */
    var _duringFocus = false;

    /**
     * Constant: ignore upper boundary when centering text
     * @type {number}
     */
    var BOUNDARY_CHECK_NORMAL   = 0,
        BOUNDARY_IGNORE_TOP     = 1;

    /**
     * @private
     * Create a copy of the given CodeMirror position
     * @param {!CodeMirror.Pos} pos
     * @return {CodeMirror.Pos}
     */
    function _copyPos(pos) {
        return new CodeMirror.Pos(pos.line, pos.ch);
    }

    /**
     * Helper functions to check options.
     * @param {number} options BOUNDARY_CHECK_NORMAL or BOUNDARY_IGNORE_TOP
     */
    function _checkTopBoundary(options) {
        return (options !== BOUNDARY_IGNORE_TOP);
    }

    function _checkBottomBoundary(options) {
        return true;
    }

    /**
     * Helper function to build preferences context based on the full path of
     * the file.
     *
     * @param {string} fullPath Full path of the file
     *
     * @return {*} A context for the specified file name
     */
    function _buildPreferencesContext(fullPath) {
        return PreferencesManager._buildContext(fullPath,
            fullPath ? LanguageManager.getLanguageForPath(fullPath).getId() : undefined);
    }

    /**
     * List of all current (non-destroy()ed) Editor instances. Needed when changing global preferences
     * that affect all editors, e.g. tabbing or color scheme settings.
     * @type {Array.<Editor>}
     */
    var _instances = [];

    /**
     * Creates a new CodeMirror editor instance bound to the given Document. The Document need not have
     * a "master" Editor realized yet, even if makeMasterEditor is false; in that case, the first time
     * an edit occurs we will automatically ask EditorManager to create a "master" editor to render the
     * Document modifiable.
     *
     * ALWAYS call destroy() when you are done with an Editor - otherwise it will leak a Document ref.
     *
     * @constructor
     *
     * @param {!Document} document
     * @param {!boolean} makeMasterEditor  If true, this Editor will set itself as the (secret) "master"
     *          Editor for the Document. If false, this Editor will attach to the Document as a "slave"/
     *          secondary editor.
     * @param {!jQueryObject|DomNode} container  Container to add the editor to.
     * @param {{startLine: number, endLine: number}=} range If specified, range of lines within the document
     *          to display in this editor. Inclusive.
     * @param {!Object} options If specified, contains editor options that can be passed to CodeMirror
     */
    function Editor(document, makeMasterEditor, container, range, options) {
        var self = this;

        var isReadOnly = (options && options.isReadOnly) || !document.editable;

        _instances.push(this);

        // Attach to document: add ref & handlers
        this.document = document;
        document.addRef();

        if (container.jquery) {
            // CodeMirror wants a DOM element, not a jQuery wrapper
            container = container.get(0);
        }

        var $container = $(container);

        if (range) {    // attach this first: want range updated before we process a change
            this._visibleRange = new TextRange(document, range.startLine, range.endLine);
        }

        // store this-bound version of listeners so we can remove them later
        this._handleDocumentChange = this._handleDocumentChange.bind(this);
        this._handleDocumentDeleted = this._handleDocumentDeleted.bind(this);
        this._handleDocumentLanguageChanged = this._handleDocumentLanguageChanged.bind(this);
        this._doWorkingSetSync = this._doWorkingSetSync.bind(this);
        document.on("change", this._handleDocumentChange);
        document.on("deleted", this._handleDocumentDeleted);
        document.on("languageChanged", this._handleDocumentLanguageChanged);
        // To sync working sets if the view is for same doc across panes
        document.on("_dirtyFlagChange", this._doWorkingSetSync);

        var mode = this._getModeFromDocument();

        // (if makeMasterEditor, we attach the Doc back to ourselves below once we're fully initialized)

        this._inlineWidgets = [];
        this._inlineWidgetQueues = {};
        this._hideMarks = [];
        this._lastEditorWidth = null;

        this._$messagePopover = null;

        // To track which pane the editor is being attached to if it's a full editor
        this._paneId = null;

        // To track the parent editor ( host editor at that time of creation) of an inline editor
        this._hostEditor = null;

        // Editor supplies some standard keyboard behavior extensions of its own
        var codeMirrorKeyMap = {
            "Tab": function () { self._handleTabKey(); },
            "Shift-Tab": "indentLess",

            "Left": function (instance) {
                self._handleSoftTabNavigation(-1, "moveH");
            },
            "Right": function (instance) {
                self._handleSoftTabNavigation(1, "moveH");
            },
            "Backspace": function (instance) {
                self._handleSoftTabNavigation(-1, "deleteH");
            },
            "Delete": function (instance) {
                self._handleSoftTabNavigation(1, "deleteH");
            },
            "Esc": function (instance) {
                if (self.getSelections().length > 1) {
                    CodeMirror.commands.singleSelection(instance);
                } else {
                    self.removeAllInlineWidgets();
                }
            },
            "Home":      "goLineLeftSmart",
            "Cmd-Left":  "goLineLeftSmart",
            "End":       "goLineRight",
            "Cmd-Right": "goLineRight"
        };

        var currentOptions = this._currentOptions = _.zipObject(
            editorOptions,
            _.map(editorOptions, function (prefName) {
                return self._getOption(prefName);
            })
        );

        // When panes are created *after* the showLineNumbers option has been turned off
        //  we need to apply the show-line-padding class or the text will be juxtaposed
        //  to the edge of the editor which makes it not easy to read.  The code below to handle
        //  that the option change only applies the class to panes that have already been created
        // This line ensures that the class is applied to any editor created after the fact
        $container.toggleClass("show-line-padding", Boolean(!this._getOption("showLineNumbers")));

        // Create the CodeMirror instance
        // (note: CodeMirror doesn't actually require using 'new', but jslint complains without it)
        this._codeMirror = new CodeMirror(container, {
            autoCloseBrackets           : currentOptions[CLOSE_BRACKETS],
            autoCloseTags               : currentOptions[CLOSE_TAGS],
            coverGutterNextToScrollbar  : true,
            cursorScrollMargin          : 3,
            dragDrop                    : currentOptions[DRAG_DROP],
            electricChars               : true,
            extraKeys                   : codeMirrorKeyMap,
            highlightSelectionMatches   : currentOptions[HIGHLIGHT_MATCHES],
            indentUnit                  : currentOptions[USE_TAB_CHAR] ? currentOptions[TAB_SIZE] : currentOptions[SPACE_UNITS],
            indentWithTabs              : currentOptions[USE_TAB_CHAR],
            inputStyle                  : currentOptions[INPUT_STYLE],
            lineNumbers                 : currentOptions[SHOW_LINE_NUMBERS],
            lineWiseCopyCut             : currentOptions[LINEWISE_COPY_CUT],
            lineWrapping                : currentOptions[WORD_WRAP],
            matchBrackets               : { maxScanLineLength: 50000, maxScanLines: 1000 },
            matchTags                   : { bothTags: true },
            scrollPastEnd               : !range && currentOptions[SCROLL_PAST_END],
            showCursorWhenSelecting     : currentOptions[SHOW_CURSOR_SELECT],
            smartIndent                 : currentOptions[SMART_INDENT],
            styleActiveLine             : currentOptions[STYLE_ACTIVE_LINE],
            tabSize                     : currentOptions[TAB_SIZE],
            readOnly                    : isReadOnly
        });

        // Can't get CodeMirror's focused state without searching for
        // CodeMirror-focused. Instead, track focus via onFocus and onBlur
        // options and track state with this._focused
        this._focused = false;

        this._installEditorListeners();

        this._renderGutters();

        this.on("cursorActivity", function (event, editor) {
            self._handleCursorActivity(event);
        });
        this.on("keypress", function (event, editor, domEvent) {
            self._handleKeypressEvents(domEvent);
        });
        this.on("change", function (event, editor, changeList) {
            self._handleEditorChange(changeList);
        });
        this.on("focus", function (event, editor) {
            if (self._hostEditor) {
                // Mark the host editor as the master editor for the hosting document
                self._hostEditor.document._toggleMasterEditor(self._hostEditor);
            } else {
                // Set this full editor as master editor for the document
                self.document._toggleMasterEditor(self);
            }
        });

        // Set code-coloring mode BEFORE populating with text, to avoid a flash of uncolored text
        this._codeMirror.setOption("mode", mode);

        // Initially populate with text. This will send a spurious change event, so need to make
        // sure this is understood as a 'sync from document' case, not a genuine edit
        this._duringSync = true;
        this._resetText(document.getText());
        this._duringSync = false;

        if (range) {
            this._updateHiddenLines();
            this.setCursorPos(range.startLine, 0);
        }

        // Now that we're fully initialized, we can point the document back at us if needed
        if (makeMasterEditor) {
            document._makeEditable(this);
        }

        // Add scrollTop property to this object for the scroll shadow code to use
        Object.defineProperty(this, "scrollTop", {
            get: function () {
                return this._codeMirror.getScrollInfo().top;
            }
        });

        // Add an $el getter for Pane Views
        Object.defineProperty(this,  "$el", {
            get: function () {
                return $(this.getRootElement());
            }
        });
    }

    EventDispatcher.makeEventDispatcher(Editor.prototype);
    EventDispatcher.markDeprecated(Editor.prototype, "keyEvent", "'keydown/press/up'");

    Editor.prototype.markPaneId = function (paneId) {
        this._paneId = paneId;

        // Also add this to the pool of full editors
        this.document._associateEditor(this);

        // In case this Editor is initialized not as the first full editor for the document
        // and the document is already dirty and present in another working set, make sure
        // to add this documents to the new panes working set.
        this._doWorkingSetSync(null, this.document);
    };

    Editor.prototype._doWorkingSetSync = function (event, doc) {
        if (doc === this.document && this._paneId && this.document.isDirty) {
            MainViewManager.addToWorkingSet(this._paneId, this.document.file, -1, false);
        }
    };

    /**
     * Removes this editor from the DOM and detaches from the Document. If this is the "master"
     * Editor that is secretly providing the Document's backing state, then the Document reverts to
     * a read-only string-backed mode.
     */
    Editor.prototype.destroy = function () {
        this.trigger("beforeDestroy", this);

        // CodeMirror docs for getWrapperElement() say all you have to do is "Remove this from your
        // tree to delete an editor instance."
        $(this.getRootElement()).remove();

        _instances.splice(_instances.indexOf(this), 1);

        // Disconnect from Document
        this.document.releaseRef();
        this.document.off("change", this._handleDocumentChange);
        this.document.off("deleted", this._handleDocumentDeleted);
        this.document.off("languageChanged", this._handleDocumentLanguageChanged);
        this.document.off("_dirtyFlagChange", this._doWorkingSetSync);

        if (this._visibleRange) {   // TextRange also refs the Document
            this._visibleRange.dispose();
        }

        // If we're the Document's master editor, disconnecting from it has special meaning
        if (this.document._masterEditor === this) {
            this.document._makeNonEditable();
        } else {
            this.document._disassociateEditor(this);
        }

        // Destroying us destroys any inline widgets we're hosting. Make sure their closeCallbacks
        // run, at least, since they may also need to release Document refs
        var self = this;
        this._inlineWidgets.forEach(function (inlineWidget) {
            self._removeInlineWidgetInternal(inlineWidget);
        });
    };

    /**
     * @private
     * Handle any cursor movement in editor, including selecting and unselecting text.
     * @param {!Event} event
     */
    Editor.prototype._handleCursorActivity = function (event) {
        this._updateStyleActiveLine();
    };

    /**
     * @private
     * Removes any whitespace after one of ]{}) to prevent trailing whitespace when auto-indenting
     */
    Editor.prototype._handleWhitespaceForElectricChars = function () {
        var self        = this,
            instance    = this._codeMirror,
            selections,
            lineStr;

        selections = this.getSelections().map(function (sel) {
            lineStr = instance.getLine(sel.end.line);

            if (lineStr && !/\S/.test(lineStr)) {
                // if the line is all whitespace, move the cursor to the end of the line
                // before indenting so that embedded whitespace such as indents are not
                // orphaned to the right of the electric char being inserted
                sel.end.ch = self.document.getLine(sel.end.line).length;
            }
            return sel;
        });
        this.setSelections(selections);
    };

    /**
     * @private
     * Handle CodeMirror key events.
     * @param {!Event} event
     */
    Editor.prototype._handleKeypressEvents = function (event) {
        var keyStr = String.fromCharCode(event.which || event.keyCode);

        if (/[\]\{\}\)]/.test(keyStr)) {
            this._handleWhitespaceForElectricChars();
        }
    };

    /**
     * @private
     * Helper function for `_handleTabKey()` (case 2) - see comment in that function.
     * @param {Array.<{start:{line:number, ch:number}, end:{line:number, ch:number}, reversed:boolean, primary:boolean}>} selections
     *     The selections to indent.
     */
    Editor.prototype._addIndentAtEachSelection = function (selections) {
        var instance = this._codeMirror,
            usingTabs = instance.getOption("indentWithTabs"),
            indentUnit = instance.getOption("indentUnit"),
            edits = [];

        _.each(selections, function (sel) {
            var indentStr = "", i, numSpaces;
            if (usingTabs) {
                indentStr = "\t";
            } else {
                numSpaces = indentUnit - (sel.start.ch % indentUnit);
                for (i = 0; i < numSpaces; i++) {
                    indentStr += " ";
                }
            }
            edits.push({edit: {text: indentStr, start: sel.start}});
        });

        this.document.doMultipleEdits(edits);
    };

    /**
     * @private
     * Helper function for `_handleTabKey()` (case 3) - see comment in that function.
     * @param {Array.<{start:{line:number, ch:number}, end:{line:number, ch:number}, reversed:boolean, primary:boolean}>} selections
     *     The selections to indent.
     */
    Editor.prototype._autoIndentEachSelection = function (selections) {
        // Capture all the line lengths, so we can tell if anything changed.
        // Note that this function should only be called if all selections are within a single line.
        var instance = this._codeMirror,
            lineLengths = {};
        _.each(selections, function (sel) {
            lineLengths[sel.start.line] = instance.getLine(sel.start.line).length;
        });

        // First, try to do a smart indent on all selections.
        CodeMirror.commands.indentAuto(instance);

        // If there were no code or selection changes, then indent each selection one more indent.
        var changed = false,
            newSelections = this.getSelections();
        if (newSelections.length === selections.length) {
            _.each(selections, function (sel, index) {
                var newSel = newSelections[index];
                if (CodeMirror.cmpPos(sel.start, newSel.start) !== 0 ||
                        CodeMirror.cmpPos(sel.end, newSel.end) !== 0 ||
                        instance.getLine(sel.start.line).length !== lineLengths[sel.start.line]) {
                    changed = true;
                    // Bail - we don't need to look any further once we've found a change.
                    return false;
                }
            });
        } else {
            changed = true;
        }

        if (!changed) {
            CodeMirror.commands.indentMore(instance);
        }
    };

    /**
     * @private
     * Handle Tab key press.
     */
    Editor.prototype._handleTabKey = function () {
        // Tab key handling is done as follows:
        // 1. If any of the selections are multiline, just add one indent level to the
        //    beginning of all lines that intersect any selection.
        // 2. Otherwise, if any of the selections is a cursor or single-line range that
        //    ends at or after the first non-whitespace character in a line:
        //    - if indentation is set to tabs, just insert a hard tab before each selection.
        //    - if indentation is set to spaces, insert the appropriate number of spaces before
        //      each selection to get to its next soft tab stop.
        // 3. Otherwise (all selections are cursors or single-line, and are in the whitespace
        //    before their respective lines), try to autoindent each line based on the mode.
        //    If none of the cursors moved and no space was added, then add one indent level
        //    to the beginning of all lines.

        // Note that in case 2, we do the "dumb" insertion even if the cursor is immediately
        // before the first non-whitespace character in a line. It might seem more convenient
        // to do autoindent in that case. However, the problem is if that line is already
        // indented past its "proper" location. In that case, we don't want Tab to
        // *outdent* the line. If we had more control over the autoindent algorithm or
        // implemented it ourselves, we could handle that case separately.

        var instance = this._codeMirror,
            selectionType = "indentAuto",
            selections = this.getSelections();

        _.each(selections, function (sel) {
            if (sel.start.line !== sel.end.line) {
                // Case 1 - we found a multiline selection. We can bail as soon as we find one of these.
                selectionType = "indentAtBeginning";
                return false;
            } else if (sel.end.ch > 0 && sel.end.ch >= instance.getLine(sel.end.line).search(/\S/)) {
                // Case 2 - we found a selection that ends at or after the first non-whitespace
                // character on the line. We need to keep looking in case we find a later multiline
                // selection though.
                selectionType = "indentAtSelection";
            }
        });

        switch (selectionType) {
        case "indentAtBeginning":
            // Case 1
            CodeMirror.commands.indentMore(instance);
            break;

        case "indentAtSelection":
            // Case 2
            this._addIndentAtEachSelection(selections);
            break;

        case "indentAuto":
            // Case 3
            this._autoIndentEachSelection(selections);
            break;
        }
    };

    /**
     * @private
     * Handle left arrow, right arrow, backspace and delete keys when soft tabs are used.
     * @param {number} direction Direction of movement: 1 for forward, -1 for backward
     * @param {string} functionName name of the CodeMirror function to call if we handle the key
     */
    Editor.prototype._handleSoftTabNavigation = function (direction, functionName) {
        var instance = this._codeMirror,
            overallJump = null;

        if (!instance.getOption("indentWithTabs") && PreferencesManager.get(SOFT_TABS)) {
            var indentUnit = instance.getOption("indentUnit");

            _.each(this.getSelections(), function (sel) {
                if (CodeMirror.cmpPos(sel.start, sel.end) !== 0) {
                    // This is a range - it will just collapse/be deleted regardless of the jump we set, so
                    // we can just ignore it and continue. (We don't want to return false in this case since
                    // we want to keep looking at other ranges.)
                    return;
                }

                var cursor = sel.start,
                    jump   = (indentUnit === 0) ? 1 : cursor.ch % indentUnit,
                    line   = instance.getLine(cursor.line);

                // Don't do any soft tab handling if there are non-whitespace characters before the cursor in
                // any of the selections.
                if (line.substr(0, cursor.ch).search(/\S/) !== -1) {
                    jump = null;
                } else if (direction === 1) { // right
                    if (indentUnit) {
                        jump = indentUnit - jump;
                    }

                    // Don't jump if it would take us past the end of the line, or if there are
                    // non-whitespace characters within the jump distance.
                    if (cursor.ch + jump > line.length || line.substr(cursor.ch, jump).search(/\S/) !== -1) {
                        jump = null;
                    }
                } else { // left
                    // If we are on the tab boundary, jump by the full amount,
                    // but not beyond the start of the line.
                    if (jump === 0) {
                        jump = indentUnit;
                    }
                    if (cursor.ch - jump < 0) {
                        jump = null;
                    } else {
                        // We're moving left, so negate the jump.
                        jump = -jump;
                    }
                }

                // Did we calculate a jump, and is this jump value either the first one or
                // consistent with all the other jumps? If so, we're good. Otherwise, bail
                // out of the foreach, since as soon as we hit an inconsistent jump we don't
                // have to look any further.
                if (jump !== null &&
                        (overallJump === null || overallJump === jump)) {
                    overallJump = jump;
                } else {
                    overallJump = null;
                    return false;
                }
            });
        }

        if (overallJump === null) {
            // Just do the default move, which is one char in the given direction.
            overallJump = direction;
        }
        instance[functionName](overallJump, "char");
    };

    /**
     * Determine the mode to use from the document's language
     * Uses "text/plain" if the language does not define a mode
     * @return {string} The mode to use
     */
    Editor.prototype._getModeFromDocument = function () {
        // We'd like undefined/null/"" to mean plain text mode. CodeMirror defaults to plaintext for any
        // unrecognized mode, but it complains on the console in that fallback case: so, convert
        // here so we're always explicit, avoiding console noise.
        return this.document.getLanguage().getMode() || "text/plain";
    };


    /**
     * Selects all text and maintains the current scroll position.
     */
    Editor.prototype.selectAllNoScroll = function () {
        var cm = this._codeMirror,
            info = this._codeMirror.getScrollInfo();

        // Note that we do not have to check for the visible range here. This
        // concern is handled internally by code mirror.
        cm.operation(function () {
            cm.scrollTo(info.left, info.top);
            cm.execCommand("selectAll");
        });
    };

    /**
     * @return {boolean} True if editor is not showing the entire text of the document (i.e. an inline editor)
     */
    Editor.prototype.isTextSubset = function () {
        return Boolean(this._visibleRange);
    };

    /**
     * Ensures that the lines that are actually hidden in the inline editor correspond to
     * the desired visible range.
     */
    Editor.prototype._updateHiddenLines = function () {
        if (this._visibleRange) {
            var cm = this._codeMirror,
                self = this;
            cm.operation(function () {
                self._hideMarks.forEach(function (mark) {
                    if (mark) {
                        mark.clear();
                    }
                });
                self._hideMarks = [];
                self._hideMarks.push(self._hideLines(0, self._visibleRange.startLine));
                self._hideMarks.push(self._hideLines(self._visibleRange.endLine + 1, self.lineCount()));
            });
        }
    };

    Editor.prototype._applyChanges = function (changeList) {
        // _visibleRange has already updated via its own Document listener. See if this change caused
        // it to lose sync. If so, our whole view is stale - signal our owner to close us.
        if (this._visibleRange) {
            if (this._visibleRange.startLine === null || this._visibleRange.endLine === null) {
                this.trigger("lostContent");
                return;
            }
        }

        // Apply text changes to CodeMirror editor
        var cm = this._codeMirror;
        cm.operation(function () {
            var change, newText, i;
            for (i = 0; i < changeList.length; i++) {
                change = changeList[i];
                newText = change.text.join('\n');
                if (!change.from || !change.to) {
                    if (change.from || change.to) {
                        console.error("Change record received with only one end undefined--replacing entire text");
                    }
                    cm.setValue(newText);
                } else {
                    cm.replaceRange(newText, change.from, change.to, change.origin);
                }

            }
        });

        // The update above may have inserted new lines - must hide any that fall outside our range
        this._updateHiddenLines();
    };

    /**
     * Responds to changes in the CodeMirror editor's text, syncing the changes to the Document.
     * There are several cases where we want to ignore a CodeMirror change:
     *  - if we're the master editor, editor changes can be ignored because Document is already listening
     *    for our changes
     *  - if we're a secondary editor, editor changes should be ignored if they were caused by us reacting
     *    to a Document change
     */
    Editor.prototype._handleEditorChange = function (changeList) {
        // we're currently syncing from the Document, so don't echo back TO the Document
        if (this._duringSync) {
            return;
        }

        // Secondary editor: force creation of "master" editor backing the model, if doesn't exist yet
        this.document._ensureMasterEditor();

        if (this.document._masterEditor !== this) {
            // Secondary editor:
            // we're not the ground truth; if we got here, this was a real editor change (not a
            // sync from the real ground truth), so we need to sync from us into the document
            // (which will directly push the change into the master editor).
            // FUTURE: Technically we should add a replaceRange() method to Document and go through
            // that instead of talking to its master editor directly. It's not clear yet exactly
            // what the right Document API would be, though.
            this._duringSync = true;
            this.document._masterEditor._applyChanges(changeList);
            this._duringSync = false;

            // Update which lines are hidden inside our editor, since we're not going to go through
            // _applyChanges() in our own editor.
            this._updateHiddenLines();
        }
        // Else, Master editor:
        // we're the ground truth; nothing else to do, since Document listens directly to us
        // note: this change might have been a real edit made by the user, OR this might have
        // been a change synced from another editor

        // The "editorChange" event is mostly for the use of the CodeHintManager.
        // It differs from the normal "change" event, that it's actually publicly usable,
        // whereas the "change" event should be listened to on the document. Also the
        // Editor dispatches a change event before this event is dispatched, because
        // CodeHintManager needs to hook in here when other things are already done.
        this.trigger("editorChange", this, changeList);
    };

    /**
     * Responds to changes in the Document's text, syncing the changes into our CodeMirror instance.
     * There are several cases where we want to ignore a Document change:
     *  - if we're the master editor, Document changes should be ignored because we already have the right
     *    text (either the change originated with us, or it has already been set into us by Document)
     *  - if we're a secondary editor, Document changes should be ignored if they were caused by us sending
     *    the document an editor change that originated with us
     */
    Editor.prototype._handleDocumentChange = function (event, doc, changeList) {
        // we're currently syncing to the Document, so don't echo back FROM the Document
        if (this._duringSync) {
            return;
        }

        if (this.document._masterEditor !== this) {
            // Secondary editor:
            // we're not the ground truth; and if we got here, this was a Document change that
            // didn't come from us (e.g. a sync from another editor, a direct programmatic change
            // to the document, or a sync from external disk changes)... so sync from the Document
            this._duringSync = true;
            this._applyChanges(changeList);
            this._duringSync = false;
        }
        // Else, Master editor:
        // we're the ground truth; nothing to do since Document change is just echoing our
        // editor changes
    };

    /**
     * Responds to the Document's underlying file being deleted. The Document is now basically dead,
     * so we must close.
     */
    Editor.prototype._handleDocumentDeleted = function (event) {
        // Pass the delete event along as the cause (needed in MultiRangeInlineEditor)
        this.trigger("lostContent", event);
    };

    /**
     * Responds to language changes, for instance when the file extension is changed.
     */
    Editor.prototype._handleDocumentLanguageChanged = function (event) {
        this._codeMirror.setOption("mode", this._getModeFromDocument());
    };


    /**
     * Install event handlers on the CodeMirror instance, translating them into
     * jQuery events on the Editor instance.
     */
    Editor.prototype._installEditorListeners = function () {
        var self = this;

        // Redispatch these CodeMirror key events as Editor events
        function _onKeyEvent(instance, event) {
            self.trigger("keyEvent", self, event);  // deprecated
            self.trigger(event.type, self, event);
            return event.defaultPrevented;   // false tells CodeMirror we didn't eat the event
        }
        this._codeMirror.on("keydown",  _onKeyEvent);
        this._codeMirror.on("keypress", _onKeyEvent);
        this._codeMirror.on("keyup",    _onKeyEvent);

        // FUTURE: if this list grows longer, consider making this a more generic mapping
        // NOTE: change is a "private" event--others shouldn't listen to it on Editor, only on
        // Document
        // Also, note that we use the new "changes" event in v4, which provides an array of
        // change objects. Our own event is still called just "change".
        this._codeMirror.on("changes", function (instance, changeList) {
            self.trigger("change", self, changeList);
        });
        this._codeMirror.on("beforeChange", function (instance, changeObj) {
            self.trigger("beforeChange", self, changeObj);
        });
        this._codeMirror.on("cursorActivity", function (instance) {
            self.trigger("cursorActivity", self);
        });
        this._codeMirror.on("beforeSelectionChange", function (instance, selectionObj) {
            self.trigger("beforeSelectionChange", selectionObj);
        });
        this._codeMirror.on("scroll", function (instance) {
            // If this editor is visible, close all dropdowns on scroll.
            // (We don't want to do this if we're just scrolling in a non-visible editor
            // in response to some document change event.)
            if (self.isFullyVisible()) {
                Menus.closeAll();
            }

            self.trigger("scroll", self);
        });

        // Convert CodeMirror onFocus events to EditorManager activeEditorChanged
        this._codeMirror.on("focus", function () {
            self._focused = true;
            self.trigger("focus", self);
            
        });

        this._codeMirror.on("blur", function () {
            self._focused = false;
            self.trigger("blur", self);
        });

        this._codeMirror.on("update", function (instance) {
            self.trigger("update", self);
        });
        this._codeMirror.on("overwriteToggle", function (instance, newstate) {
            self.trigger("overwriteToggle", self, newstate);
        });

        // Disable CodeMirror's drop handling if a file/folder is dropped
        this._codeMirror.on("drop", function (cm, event) {
            var files = event.dataTransfer.files;
            if (files && files.length) {
                event.preventDefault();
            }
        });
        // For word wrap. Code adapted from https://codemirror.net/demo/indentwrap.html#
        this._codeMirror.on("renderLine", function (cm, line, elt) {
            var charWidth = self._codeMirror.defaultCharWidth();
            var off = CodeMirror.countColumn(line.text, null, cm.getOption("tabSize")) * charWidth;
            elt.style.textIndent = "-" + off + "px";
            elt.style.paddingLeft = off + "px";
        });
    };

    /**
     * Sets the contents of the editor, clears the undo/redo history and marks the document clean. Dispatches a change event.
     * Semi-private: only Document should call this.
     * @param {!string} text
     */
    Editor.prototype._resetText = function (text) {
        var currentText = this._codeMirror.getValue();

        // compare with ignoring line-endings, issue #11826
        var textLF = text ? text.replace(/(\r\n|\r|\n)/g, "\n") : null;
        var currentTextLF = currentText ? currentText.replace(/(\r\n|\r|\n)/g, "\n") : null;
        if (textLF === currentTextLF) {
            // there's nothing to reset
            return;
        }

        var perfTimerName = PerfUtils.markStart("Editor._resetText()\t" + (!this.document || this.document.file.fullPath));

        var cursorPos = this.getCursorPos(),
            scrollPos = this.getScrollPos();

        // This *will* fire a change event, but we clear the undo immediately afterward
        this._codeMirror.setValue(text);
        this._codeMirror.refresh();

        // Make sure we can't undo back to the empty state before setValue(), and mark
        // the document clean.
        this._codeMirror.clearHistory();
        this._codeMirror.markClean();

        // restore cursor and scroll positions
        this.setCursorPos(cursorPos);
        this.setScrollPos(scrollPos.x, scrollPos.y);

        PerfUtils.addMeasurement(perfTimerName);
    };

   /**
    * Gets the file associated with this editor
    * This is a required Pane-View interface method
    * @return {!File} the file associated with this editor
    */
    Editor.prototype.getFile = function () {
        return this.document.file;
    };

    /**
     * Gets the current cursor position within the editor.
     * @param {boolean} expandTabs  If true, return the actual visual column number instead of the character offset in
     *      the "ch" property.
     * @param {?string} which Optional string indicating which end of the
     *  selection to return. It may be "start", "end", "head" (the side of the
     *  selection that moves when you press shift+arrow), or "anchor" (the
     *  fixed side of the selection). Omitting the argument is the same as
     *  passing "head". A {line, ch} object will be returned.)
     * @return {!{line:number, ch:number}}
     */
    Editor.prototype.getCursorPos = function (expandTabs, which) {
        // Translate "start" and "end" to the official CM names (it actually
        // supports them as-is, but that isn't documented and we don't want to
        // rely on it).
        if (which === "start") {
            which = "from";
        } else if (which === "end") {
            which = "to";
        }
        var cursor = _copyPos(this._codeMirror.getCursor(which));

        if (expandTabs) {
            cursor.ch = this.getColOffset(cursor);
        }
        return cursor;
    };

    /**
     * Returns the display column (zero-based) for a given string-based pos. Differs from pos.ch only
     * when the line contains preceding \t chars. Result depends on the current tab size setting.
     * @param {!{line:number, ch:number}} pos
     * @return {number}
     */
    Editor.prototype.getColOffset = function (pos) {
        var line    = this._codeMirror.getRange({line: pos.line, ch: 0}, pos),
            tabSize = null,
            column  = 0,
            i;

        for (i = 0; i < line.length; i++) {
            if (line[i] === '\t') {
                if (tabSize === null) {
                    tabSize = Editor.getTabSize();
                }
                if (tabSize > 0) {
                    column += (tabSize - (column % tabSize));
                }
            } else {
                column++;
            }
        }
        return column;
    };

    /**
     * Returns the string-based pos for a given display column (zero-based) in given line. Differs from column
     * only when the line contains preceding \t chars. Result depends on the current tab size setting.
     * @param {number} lineNum Line number
     * @param {number} column Display column number
     * @return {number}
     */
    Editor.prototype.getCharIndexForColumn = function (lineNum, column) {
        var line    = this._codeMirror.getLine(lineNum),
            tabSize = null,
            iCol    = 0,
            i;

        for (i = 0; iCol < column; i++) {
            if (line[i] === '\t') {
                if (tabSize === null) {
                    tabSize = Editor.getTabSize();
                }
                if (tabSize > 0) {
                    iCol += (tabSize - (iCol % tabSize));
                }
            } else {
                iCol++;
            }
        }
        return i;
    };

    /**
     * Sets the cursor position within the editor. Removes any selection.
     * @param {number} line  The 0 based line number.
     * @param {number} ch  The 0 based character position; treated as 0 if unspecified.
     * @param {boolean=} center  True if the view should be centered on the new cursor position.
     * @param {boolean=} expandTabs  If true, use the actual visual column number instead of the character offset as
     *      the "ch" parameter.
     */
    Editor.prototype.setCursorPos = function (line, ch, center, expandTabs) {
        if (expandTabs) {
            ch = this.getColOffset({line: line, ch: ch});
        }
        this._codeMirror.setCursor(line, ch);
        if (center) {
            this.centerOnCursor();
        }
    };

    /**
     * Set the editor size in pixels or percentage
     * @param {(number|string)} width
     * @param {(number|string)} height
     */
    Editor.prototype.setSize = function (width, height) {
        this._codeMirror.setSize(width, height);
    };

    /** @const */
    var CENTERING_MARGIN = 0.15;

    /**
     * Scrolls the editor viewport to vertically center the line with the cursor,
     * but only if the cursor is currently near the edges of the viewport or
     * entirely outside the viewport.
     *
     * This does not alter the horizontal scroll position.
     *
     * @param {number} centerOptions Option value, or 0 for no options; one of the BOUNDARY_* constants above.
     */
    Editor.prototype.centerOnCursor = function (centerOptions) {
        var $scrollerElement = $(this.getScrollerElement());
        var editorHeight = $scrollerElement.height();

        // we need to make adjustments for the statusbar's padding on the bottom and the menu bar on top.
        var statusBarHeight = $scrollerElement.outerHeight() - editorHeight;
        var menuBarHeight = $scrollerElement.offset().top;

        var documentCursorPosition = this._codeMirror.cursorCoords(null, "local").bottom;
        var screenCursorPosition = this._codeMirror.cursorCoords(null, "page").bottom - menuBarHeight;

        // If the cursor is already reasonably centered, we won't
        // make any change. "Reasonably centered" is defined as
        // not being within CENTERING_MARGIN of the top or bottom
        // of the editor (where CENTERING_MARGIN is a percentage
        // of the editor height).
        // For finding the first item (i.e. find while typing), do
        // not center if hit is in first half of screen because this
        // appears to be an unnecesary scroll.
        if ((_checkTopBoundary(centerOptions) && (screenCursorPosition < editorHeight * CENTERING_MARGIN)) ||
                (_checkBottomBoundary(centerOptions) && (screenCursorPosition > editorHeight * (1 - CENTERING_MARGIN)))) {

            var pos = documentCursorPosition - editorHeight / 2 + statusBarHeight;
            var info = this._codeMirror.getScrollInfo();
            pos = Math.min(Math.max(pos, 0), (info.height - info.clientHeight));
            this.setScrollPos(null, pos);
        }
    };

    /**
     * Given a position, returns its index within the text (assuming \n newlines)
     * @param {!{line:number, ch:number}}
     * @return {number}
     */
    Editor.prototype.indexFromPos = function (coords) {
        return this._codeMirror.indexFromPos(coords);
    };

    Editor.prototype.posFromIndex = function (index) {
        return this._codeMirror.posFromIndex(index);
    };

    /**
     * Returns true if pos is between start and end (INclusive at start; EXclusive at end by default,
     * but overridable via the endInclusive flag).
     * @param {{line:number, ch:number}} pos
     * @param {{line:number, ch:number}} start
     * @param {{line:number, ch:number}} end
     * @param {boolean} endInclusive
     *
     */
    Editor.prototype.posWithinRange = function (pos, start, end, endInclusive) {
        if (start.line <= pos.line && end.line >= pos.line) {
            if (endInclusive) {
                return (start.line < pos.line || start.ch <= pos.ch) &&  // inclusive
                    (end.line > pos.line   || end.ch >= pos.ch);      // inclusive
            } else {
                return (start.line < pos.line || start.ch <= pos.ch) &&  // inclusive
                    (end.line > pos.line   || end.ch > pos.ch);       // exclusive
            }

        }
        return false;
    };

    /**
     * @return {boolean} True if there's a text selection; false if there's just an insertion point
     */
    Editor.prototype.hasSelection = function () {
        return this._codeMirror.somethingSelected();
    };

    /**
     * @private
     * Takes an anchor/head pair and returns a start/end pair where the start is guaranteed to be <= end, and a "reversed" flag indicating
     * if the head is before the anchor.
     * @param {!{line: number, ch: number}} anchorPos
     * @param {!{line: number, ch: number}} headPos
     * @return {!{start:{line:number, ch:number}, end:{line:number, ch:number}}, reversed:boolean} the normalized range with start <= end
     */
    function _normalizeRange(anchorPos, headPos) {
        if (headPos.line < anchorPos.line || (headPos.line === anchorPos.line && headPos.ch < anchorPos.ch)) {
            return {start: _copyPos(headPos), end: _copyPos(anchorPos), reversed: true};
        } else {
            return {start: _copyPos(anchorPos), end: _copyPos(headPos), reversed: false};
        }
    }

    /**
     * Gets the current selection; if there is more than one selection, returns the primary selection
     * (generally the last one made). Start is inclusive, end is exclusive. If there is no selection,
     * returns the current cursor position as both the start and end of the range (i.e. a selection
     * of length zero). If `reversed` is set, then the head of the selection (the end of the selection
     * that would be changed if the user extended the selection) is before the anchor.
     * @return {!{start:{line:number, ch:number}, end:{line:number, ch:number}}, reversed:boolean}
     */
    Editor.prototype.getSelection = function () {
        return _normalizeRange(this.getCursorPos(false, "anchor"), this.getCursorPos(false, "head"));
    };

    /**
     * Returns an array of current selections, nonoverlapping and sorted in document order.
     * Each selection is a start/end pair, with the start guaranteed to come before the end.
     * Cursors are represented as a range whose start is equal to the end.
     * If `reversed` is set, then the head of the selection
     * (the end of the selection that would be changed if the user extended the selection)
     * is before the anchor.
     * If `primary` is set, then that selection is the primary selection.
     * @return {Array.<{start:{line:number, ch:number}, end:{line:number, ch:number}, reversed:boolean, primary:boolean}>}
     */
    Editor.prototype.getSelections = function () {
        var primarySel = this.getSelection();
        return _.map(this._codeMirror.listSelections(), function (sel) {
            var result = _normalizeRange(sel.anchor, sel.head);
            if (result.start.line === primarySel.start.line && result.start.ch === primarySel.start.ch &&
                    result.end.line === primarySel.end.line && result.end.ch === primarySel.end.ch) {
                result.primary = true;
            } else {
                result.primary = false;
            }
            return result;
        });
    };

    /**
     * Takes the given selections, and expands each selection so it encompasses whole lines. Merges
     * adjacent line selections together. Keeps track of each original selection associated with a given
     * line selection (there might be multiple if individual selections were merged into a single line selection).
     * Useful for doing multiple-selection-aware line edits.
     *
     * @param {Array.<{start:{line:number, ch:number}, end:{line:number, ch:number}, reversed:boolean, primary:boolean}>} selections
     *      The selections to expand.
     * @param {{expandEndAtStartOfLine: boolean, mergeAdjacent: boolean}} options
     *      expandEndAtStartOfLine: true if a range selection that ends at the beginning of a line should be expanded
     *          to encompass the line. Default false.
     *      mergeAdjacent: true if adjacent line ranges should be merged. Default true.
     * @return {Array.<{selectionForEdit: {start:{line:number, ch:number}, end:{line:number, ch:number}, reversed:boolean, primary:boolean},
     *                  selectionsToTrack: Array.<{start:{line:number, ch:number}, end:{line:number, ch:number}, reversed:boolean, primary:boolean}>}>}
     *      The combined line selections. For each selection, `selectionForEdit` is the line selection, and `selectionsToTrack` is
     *      the set of original selections that combined to make up the given line selection. Note that the selectionsToTrack will
     *      include the original objects passed in `selections`, so if it is later mutated the original passed-in selections will be
     *      mutated as well.
     */
    Editor.prototype.convertToLineSelections = function (selections, options) {
        var self = this;
        options = options || {};
        _.defaults(options, { expandEndAtStartOfLine: false, mergeAdjacent: true });

        // Combine adjacent lines with selections so they don't collide with each other, as they would
        // if we did them individually.
        var combinedSelections = [], prevSel;
        _.each(selections, function (sel) {
            var newSel = _.cloneDeep(sel);

            // Adjust selection to encompass whole lines.
            newSel.start.ch = 0;
            // The end of the selection becomes the start of the next line, if it isn't already
            // or if expandEndAtStartOfLine is set.
            var hasSelection = (newSel.start.line !== newSel.end.line) || (newSel.start.ch !== newSel.end.ch);
            if (options.expandEndAtStartOfLine || !hasSelection || newSel.end.ch !== 0) {
                newSel.end = {line: newSel.end.line + 1, ch: 0};
            }

            // If the start of the new selection is within the range of the previous (expanded) selection, merge
            // the two selections together, but keep track of all the original selections that were related to this
            // selection, so they can be properly adjusted. (We only have to check for the start being inside the previous
            // range - it can't be before it because the selections started out sorted.)
            if (prevSel && self.posWithinRange(newSel.start, prevSel.selectionForEdit.start, prevSel.selectionForEdit.end, options.mergeAdjacent)) {
                prevSel.selectionForEdit.end.line = newSel.end.line;
                prevSel.selectionsToTrack.push(sel);
            } else {
                prevSel = {selectionForEdit: newSel, selectionsToTrack: [sel]};
                combinedSelections.push(prevSel);
            }
        });
        return combinedSelections;
    };

    /**
     * Returns the currently selected text, or "" if no selection. Includes \n if the
     * selection spans multiple lines (does NOT reflect the Document's line-endings style). By
     * default, returns only the contents of the primary selection, unless `allSelections` is true.
     * @param {boolean=} allSelections Whether to return the contents of all selections (separated
     *     by newlines) instead of just the primary selection. Default false.
     * @return {!string} The selected text.
     */
    Editor.prototype.getSelectedText = function (allSelections) {
        if (allSelections) {
            return this._codeMirror.getSelection();
        } else {
            var sel = this.getSelection();
            return this.document.getRange(sel.start, sel.end);
        }
    };

    /**
     * Sets the current selection. Start is inclusive, end is exclusive. Places the cursor at the
     * end of the selection range. Optionally centers around the cursor after
     * making the selection
     *
     * @param {!{line:number, ch:number}} start
     * @param {{line:number, ch:number}=} end If not specified, defaults to start.
     * @param {boolean} center true to center the viewport
     * @param {number} centerOptions Option value, or 0 for no options; one of the BOUNDARY_* constants above.
     * @param {?string} origin An optional string that describes what other selection or edit operations this
     *      should be merged with for the purposes of undo. See {@link Document#replaceRange} for more details.
     */
    Editor.prototype.setSelection = function (start, end, center, centerOptions, origin) {
        this.setSelections([{start: start, end: end || start}], center, centerOptions, origin);
    };

    /**
     * Sets a multiple selection, with the "primary" selection (the one returned by
     * getSelection() and getCursorPos()) defaulting to the last if not specified.
     * Overlapping ranges will be automatically merged, and the selection will be sorted.
     * Optionally centers around the primary selection after making the selection.
     * @param {!Array<{start:{line:number, ch:number}, end:{line:number, ch:number}, primary:boolean, reversed: boolean}>} selections
     *      The selection ranges to set. If the start and end of a range are the same, treated as a cursor.
     *      If reversed is true, set the anchor of the range to the end instead of the start.
     *      If primary is true, this is the primary selection. Behavior is undefined if more than
     *      one selection has primary set to true. If none has primary set to true, the last one is primary.
     * @param {boolean} center true to center the viewport around the primary selection.
     * @param {number} centerOptions Option value, or 0 for no options; one of the BOUNDARY_* constants above.
     * @param {?string} origin An optional string that describes what other selection or edit operations this
     *      should be merged with for the purposes of undo. See {@link Document#replaceRange} for more details.
     */
    Editor.prototype.setSelections = function (selections, center, centerOptions, origin) {
        var primIndex = selections.length - 1, options;
        if (origin) {
            options = { origin: origin };
        }
        this._codeMirror.setSelections(_.map(selections, function (sel, index) {
            if (sel.primary) {
                primIndex = index;
            }
            return { anchor: sel.reversed ? sel.end : sel.start, head: sel.reversed ? sel.start : sel.end };
        }), primIndex, options);
        if (center) {
            this.centerOnCursor(centerOptions);
        }
    };

    /**
     * Sets the editors overwrite mode state. If null is passed, the state is toggled.
     *
     * @param {?boolean} start
     */
    Editor.prototype.toggleOverwrite = function (state) {
        this._codeMirror.toggleOverwrite(state);
    };

    /**
     * Selects word that the given pos lies within or adjacent to. If pos isn't touching a word
     * (e.g. within a token like "//"), moves the cursor to pos without selecting a range.
     * @param {!{line:number, ch:number}}
     */
    Editor.prototype.selectWordAt = function (pos) {
        var word = this._codeMirror.findWordAt(pos);
        this.setSelection(word.anchor, word.head);
    };

    /**
     * Gets the total number of lines in the document (includes lines not visible in the viewport)
     * @return {!number}
     */
    Editor.prototype.lineCount = function () {
        return this._codeMirror.lineCount();
    };

    /**
     * Deterines if line is fully visible.
     * @param {number} zero-based index of the line to test
     * @return {boolean} true if the line is fully visible, false otherwise
     */
    Editor.prototype.isLineVisible = function (line) {
        var coords = this._codeMirror.charCoords({line: line, ch: 0}, "local"),
            scrollInfo = this._codeMirror.getScrollInfo(),
            top = scrollInfo.top,
            bottom = scrollInfo.top + scrollInfo.clientHeight;

        // Check top and bottom and return false for partially visible lines.
        return (coords.top >= top && coords.bottom <= bottom);
    };

    /**
     * Gets the number of the first visible line in the editor.
     * @return {number} The 0-based index of the first visible line.
     */
    Editor.prototype.getFirstVisibleLine = function () {
        return (this._visibleRange ? this._visibleRange.startLine : 0);
    };

    /**
     * Gets the number of the last visible line in the editor.
     * @return {number} The 0-based index of the last visible line.
     */
    Editor.prototype.getLastVisibleLine = function () {
        return (this._visibleRange ? this._visibleRange.endLine : this.lineCount() - 1);
    };

    /* Hides the specified line number in the editor
     * @param {!from} line to start hiding from (inclusive)
     * @param {!to} line to end hiding at (exclusive)
     * @return {TextMarker} The CodeMirror mark object that's hiding the lines
     */
    Editor.prototype._hideLines = function (from, to) {
        if (to <= from) {
            return;
        }

        // We set clearWhenEmpty: false so that if there's a blank line at the beginning or end of
        // the document, and that's the only hidden line, we can still actually hide it. Doing so
        // requires us to create a 0-length marked span, which would ordinarily be cleaned up by CM
        // if clearWithEmpty is true. See https://groups.google.com/forum/#!topic/codemirror/RB8VNF8ow2w
        var value = this._codeMirror.markText(
            {line: from, ch: 0},
            {line: to - 1, ch: this._codeMirror.getLine(to - 1).length},
            {collapsed: true, inclusiveLeft: true, inclusiveRight: true, clearWhenEmpty: false}
        );

        return value;
    };

    /**
     * Gets the total height of the document in pixels (not the viewport)
     * @return {!number} height in pixels
     */
    Editor.prototype.totalHeight = function () {
        return this.getScrollerElement().scrollHeight;
    };

    /**
     * Gets the scroller element from the editor.
     * @return {!HTMLDivElement} scroller
     */
    Editor.prototype.getScrollerElement = function () {
        return this._codeMirror.getScrollerElement();
    };

    /**
     * Gets the root DOM node of the editor.
     * @return {!HTMLDivElement} The editor's root DOM node.
     */
    Editor.prototype.getRootElement = function () {
        return this._codeMirror.getWrapperElement();
    };


    /**
     * Gets the lineSpace element within the editor (the container around the individual lines of code).
     * FUTURE: This is fairly CodeMirror-specific. Logic that depends on this may break if we switch
     * editors.
     * @return {!HTMLDivElement} The editor's lineSpace element.
     */
    Editor.prototype._getLineSpaceElement = function () {
        return $(".CodeMirror-lines", this.getScrollerElement()).children().get(0);
    };

    /**
     * Returns the current scroll position of the editor.
     * @return {{x:number, y:number}} The x,y scroll position in pixels
     */
    Editor.prototype.getScrollPos = function () {
        var scrollInfo = this._codeMirror.getScrollInfo();
        return { x: scrollInfo.left, y: scrollInfo.top };
    };

    /**
     * Restores and adjusts the current scroll position of the editor.
     * @param {{x:number, y:number}} scrollPos - The x,y scroll position in pixels
     * @param {!number} heightDelta - The amount of delta H to apply to the scroll position
     */
    Editor.prototype.adjustScrollPos = function (scrollPos, heightDelta) {
        this._codeMirror.scrollTo(scrollPos.x, scrollPos.y + heightDelta);
    };

    /**
     * Sets the current scroll position of the editor.
     * @param {number} x scrollLeft position in pixels
     * @param {number} y scrollTop position in pixels
     */
    Editor.prototype.setScrollPos = function (x, y) {
        this._codeMirror.scrollTo(x, y);
    };

    /*
     * Returns the current text height of the editor.
     * @return {number} Height of the text in pixels
     */
    Editor.prototype.getTextHeight = function () {
        return this._codeMirror.defaultTextHeight();
    };

    /**
     * Adds an inline widget below the given line. If any inline widget was already open for that
     * line, it is closed without warning.
     * @param {!{line:number, ch:number}} pos  Position in text to anchor the inline.
     * @param {!InlineWidget} inlineWidget The widget to add.
     * @param {boolean=} scrollLineIntoView Scrolls the associated line into view. Default true.
     * @return {$.Promise} A promise object that is resolved when the widget has been added (but might
     *     still be animating open). Never rejected.
     */
    Editor.prototype.addInlineWidget = function (pos, inlineWidget, scrollLineIntoView) {
        var self = this,
            queue = this._inlineWidgetQueues[pos.line],
            deferred = new $.Deferred();
        if (!queue) {
            queue = new Async.PromiseQueue();
            this._inlineWidgetQueues[pos.line] = queue;
        }
        queue.add(function () {
            self._addInlineWidgetInternal(pos, inlineWidget, scrollLineIntoView, deferred);
            return deferred.promise();
        });
        return deferred.promise();
    };

    /**
     * @private
     * Does the actual work of addInlineWidget().
     */
    Editor.prototype._addInlineWidgetInternal = function (pos, inlineWidget, scrollLineIntoView, deferred) {
        var self = this;

        this.removeAllInlineWidgetsForLine(pos.line).done(function () {
            if (scrollLineIntoView === undefined) {
                scrollLineIntoView = true;
            }

            if (scrollLineIntoView) {
                self._codeMirror.scrollIntoView(pos);
            }

            inlineWidget.info = self._codeMirror.addLineWidget(pos.line, inlineWidget.htmlContent,
                                                               { coverGutter: true, noHScroll: true });
            CodeMirror.on(inlineWidget.info.line, "delete", function () {
                self._removeInlineWidgetInternal(inlineWidget);
            });
            self._inlineWidgets.push(inlineWidget);

            // Set up the widget to start closed, then animate open when its initial height is set.
            inlineWidget.$htmlContent.height(0);
            AnimationUtils.animateUsingClass(inlineWidget.htmlContent, "animating")
                .done(function () {
                    deferred.resolve();
                });

            // Callback to widget once parented to the editor. The widget should call back to
            // setInlineWidgetHeight() in order to set its initial height and animate open.
            inlineWidget.onAdded();
        });
    };

    /**
     * Removes all inline widgets
     */
    Editor.prototype.removeAllInlineWidgets = function () {
        // copy the array because _removeInlineWidgetInternal will modify the original
        var widgets = [].concat(this.getInlineWidgets());

        return Async.doInParallel(
            widgets,
            this.removeInlineWidget.bind(this)
        );
    };

    /**
     * Removes the given inline widget.
     * @param {number} inlineWidget The widget to remove.
     * @return {$.Promise} A promise that is resolved when the inline widget is fully closed and removed from the DOM.
     */
    Editor.prototype.removeInlineWidget = function (inlineWidget) {
        var deferred = new $.Deferred(),
            self = this;

        function finishRemoving() {
            self._codeMirror.removeLineWidget(inlineWidget.info);
            self._removeInlineWidgetInternal(inlineWidget);
            deferred.resolve();
        }

        if (!inlineWidget.closePromise) {
            // Remove the inline widget from our internal list immediately, so
            // everyone external to us knows it's essentially already gone. We
            // don't want to wait until it's done animating closed (but we do want
            // the other stuff in _removeInlineWidgetInternal to wait until then).
            self._removeInlineWidgetFromList(inlineWidget);

            // If we're not visible (in which case the widget will have 0 client height),
            // don't try to do the animation, because nothing will happen and we won't get
            // called back right away. (The animation would happen later when we switch
            // back to the editor.)
            if (self.isFullyVisible()) {
                AnimationUtils.animateUsingClass(inlineWidget.htmlContent, "animating")
                    .done(finishRemoving);
                inlineWidget.$htmlContent.height(0);
            } else {
                finishRemoving();
            }
            inlineWidget.closePromise = deferred.promise();
        }
        return inlineWidget.closePromise;
    };

    /**
     * Removes all inline widgets for a given line
     * @param {number} lineNum The line number to modify
     */
    Editor.prototype.removeAllInlineWidgetsForLine = function (lineNum) {
        var lineInfo = this._codeMirror.lineInfo(lineNum),
            widgetInfos = (lineInfo && lineInfo.widgets) ? [].concat(lineInfo.widgets) : null,
            self = this;

        if (widgetInfos && widgetInfos.length) {
            // Map from CodeMirror LineWidget to Brackets InlineWidget
            var inlineWidget,
                allWidgetInfos = this._inlineWidgets.map(function (w) {
                    return w.info;
                });

            return Async.doInParallel(
                widgetInfos,
                function (info) {
                    // Lookup the InlineWidget object using the same index
                    inlineWidget = self._inlineWidgets[allWidgetInfos.indexOf(info)];
                    if (inlineWidget) {
                        return self.removeInlineWidget(inlineWidget);
                    } else {
                        return new $.Deferred().resolve().promise();
                    }
                }
            );
        } else {
            return new $.Deferred().resolve().promise();
        }
    };

    /**
     * Cleans up the given inline widget from our internal list of widgets. It's okay
     * to call this multiple times for the same widget--it will just do nothing if
     * the widget has already been removed.
     * @param {InlineWidget} inlineWidget  an inline widget.
     */
    Editor.prototype._removeInlineWidgetFromList = function (inlineWidget) {
        var l = this._inlineWidgets.length,
            i;
        for (i = 0; i < l; i++) {
            if (this._inlineWidgets[i] === inlineWidget) {
                this._inlineWidgets.splice(i, 1);
                break;
            }
        }
    };

    /**
     * Removes the inline widget from the editor and notifies it to clean itself up.
     * @param {InlineWidget} inlineWidget  an inline widget.
     */
    Editor.prototype._removeInlineWidgetInternal = function (inlineWidget) {
        if (!inlineWidget.isClosed) {
            this._removeInlineWidgetFromList(inlineWidget);
            inlineWidget.onClosed();
            inlineWidget.isClosed = true;
        }
    };

    /**
     * Returns a list of all inline widgets currently open in this editor. Each entry contains the
     * inline's id, and the data parameter that was passed to addInlineWidget().
     * @return {!Array.<{id:number, data:Object}>}
     */
    Editor.prototype.getInlineWidgets = function () {
        return this._inlineWidgets;
    };

      /**
     * Returns the currently focused inline widget, if any.
     * @return {?InlineWidget}
     */
    Editor.prototype.getFocusedInlineWidget = function () {
        var result = null;

        this.getInlineWidgets().forEach(function (widget) {
            if (widget.hasFocus()) {
                result = widget;
            }
        });

        return result;
    };

    /**
     * Display temporary popover message at current cursor position. Display message above
     * cursor if space allows, otherwise below.
     *
     * @param {string} errorMsg Error message to display
     */
    Editor.prototype.displayErrorMessageAtCursor = function (errorMsg) {
        var arrowBelow, cursorPos, cursorCoord, popoverRect,
            top, left, clip, arrowCenter, arrowLeft,
            self = this,
            POPOVER_MARGIN = 10,
            POPOVER_ARROW_HALF_WIDTH = 10,
            POPOVER_ARROW_HALF_BASE = POPOVER_ARROW_HALF_WIDTH + 3; // 3 is border radius

        function _removeListeners() {
            self.off(".msgbox");
        }

        // PopUpManager.removePopUp() callback
        function _clearMessagePopover() {
            if (self._$messagePopover && self._$messagePopover.length > 0) {
                // self._$messagePopover.remove() is done by PopUpManager
                self._$messagePopover = null;
            }
            _removeListeners();
        }

        // PopUpManager.removePopUp() is called either directly by this closure, or by
        // PopUpManager as a result of another popup being invoked.
        function _removeMessagePopover() {
            if (self._$messagePopover) {
                PopUpManager.removePopUp(self._$messagePopover);
            }
        }

        function _addListeners() {
            self
                .on("blur.msgbox",           _removeMessagePopover)
                .on("change.msgbox",         _removeMessagePopover)
                .on("cursorActivity.msgbox", _removeMessagePopover)
                .on("update.msgbox",         _removeMessagePopover);
        }

        // Only 1 message at a time
        if (this._$messagePopover) {
            _removeMessagePopover();
        }

        // Make sure cursor is in view
        cursorPos = this.getCursorPos();
        this._codeMirror.scrollIntoView(cursorPos);

        // Determine if arrow is above or below
        cursorCoord = this._codeMirror.charCoords(cursorPos);

        // Assume popover height is max of 2 lines
        arrowBelow = (cursorCoord.top > 100);

        // Text is dynamic, so build popover first so we can measure final width
        this._$messagePopover = $("<div/>").addClass("popover-message").appendTo($("body"));
        if (!arrowBelow) {
            $("<div/>").addClass("arrowAbove").appendTo(this._$messagePopover);
        }
        $("<div/>").addClass("text").appendTo(this._$messagePopover).html(errorMsg);
        if (arrowBelow) {
            $("<div/>").addClass("arrowBelow").appendTo(this._$messagePopover);
        }

        // Estimate where to position popover.
        top = (arrowBelow) ? cursorCoord.top - this._$messagePopover.height() - POPOVER_MARGIN
                           : cursorCoord.bottom + POPOVER_MARGIN;
        left = cursorCoord.left - (this._$messagePopover.width() / 2);

        popoverRect = {
            top:    top,
            left:   left,
            height: this._$messagePopover.height(),
            width:  this._$messagePopover.width()
        };

        // See if popover is clipped on any side
        clip = ViewUtils.getElementClipSize($("#editor-holder"), popoverRect);

        // Prevent horizontal clipping
        if (clip.left > 0) {
            left += clip.left;
        } else if (clip.right > 0) {
            left -= clip.right;
        }

        // Popover text and arrow are positioned individually
        this._$messagePopover.css({"top": top, "left": left});

        // Position popover arrow centered over/under cursor...
        arrowCenter = cursorCoord.left - left;

        // ... but don't let it slide off text box
        arrowCenter = Math.min(popoverRect.width - POPOVER_ARROW_HALF_BASE,
                               Math.max(arrowCenter, POPOVER_ARROW_HALF_BASE));

        arrowLeft = arrowCenter - POPOVER_ARROW_HALF_WIDTH;
        if (arrowBelow) {
            this._$messagePopover.find(".arrowBelow").css({"margin-left": arrowLeft});
        } else {
            this._$messagePopover.find(".arrowAbove").css({"margin-left": arrowLeft});
        }

        // Add listeners
        PopUpManager.addPopUp(this._$messagePopover, _clearMessagePopover, true);
        _addListeners();

        // Animate open
        AnimationUtils.animateUsingClass(this._$messagePopover[0], "animateOpen").done(function () {
            // Make sure we still have a popover
            if (self._$messagePopover && self._$messagePopover.length > 0) {
                self._$messagePopover.addClass("open");

                // Don't add scroll listeners until open so we don't get event
                // from scrolling cursor into view
                self.on("scroll.msgbox", _removeMessagePopover);

                // Animate closed -- which includes delay to show message
                AnimationUtils.animateUsingClass(self._$messagePopover[0], "animateClose", 6000)
                    .done(_removeMessagePopover);
            }
        });
    };

    /**
     * Returns the offset of the top of the virtual scroll area relative to the browser window (not the editor
     * itself). Mainly useful for calculations related to scrollIntoView(), where you're starting with the
     * offset() of a child widget (relative to the browser window) and need to figure out how far down it is from
     * the top of the virtual scroll area (excluding the top padding).
     * @return {number}
     */
    Editor.prototype.getVirtualScrollAreaTop = function () {
        var topPadding = this._getLineSpaceElement().offsetTop, // padding within mover
            scroller = this.getScrollerElement();
        return $(scroller).offset().top - scroller.scrollTop + topPadding;
    };

    /**
     * Sets the height of an inline widget in this editor.
     * @param {!InlineWidget} inlineWidget The widget whose height should be set.
     * @param {!number} height The height of the widget.
     * @param {boolean=} ensureVisible Whether to scroll the entire widget into view. Default false.
     */
    Editor.prototype.setInlineWidgetHeight = function (inlineWidget, height, ensureVisible) {
        var self = this,
            node = inlineWidget.htmlContent,
            oldHeight = (node && $(node).height()) || 0,
            changed = (oldHeight !== height),
            isAttached = inlineWidget.info !== undefined;

        function updateHeight() {
            // Notify CodeMirror for the height change.
            if (isAttached) {
                inlineWidget.info.changed();
            }
        }

        function setOuterHeight() {
            function finishAnimating(e) {
                if (e.target === node) {
                    updateHeight();
                    $(node).off("webkitTransitionEnd", finishAnimating);
                }
            }
            $(node).height(height);
            if ($(node).hasClass("animating")) {
                $(node).on("webkitTransitionEnd", finishAnimating);
            } else {
                updateHeight();
            }
        }

        // Make sure we set an explicit height on the widget, so children can use things like
        // min-height if they want.
        if (changed || !node.style.height) {
            // If we're animating, set the wrapper's height on a timeout so the layout is finished before we animate.
            if ($(node).hasClass("animating")) {
                window.setTimeout(setOuterHeight, 0);
            } else {
                setOuterHeight();
            }
        }

        if (ensureVisible && isAttached) {
            var offset = $(node).offset(), // offset relative to document
                position = $(node).position(), // position within parent linespace
                scrollerTop = self.getVirtualScrollAreaTop();

            self._codeMirror.scrollIntoView({
                left: position.left,
                top: offset.top - scrollerTop,
                right: position.left, // don't try to make the right edge visible
                bottom: offset.top + height - scrollerTop
            });
        }
    };

    /**
     * @private
     * Get the starting line number for an inline widget.
     * @param {!InlineWidget} inlineWidget
     * @return {number} The line number of the widget or -1 if not found.
     */
    Editor.prototype._getInlineWidgetLineNumber = function (inlineWidget) {
        return this._codeMirror.getLineNumber(inlineWidget.info.line);
    };

    /** Gives focus to the editor control */
    Editor.prototype.focus = function () {
        // Focusing an editor synchronously triggers focus/blur handlers. If a blur handler attemps to focus
        // another editor, we'll put CM in a bad state (because CM assumes programmatically focusing itself
        // will always succeed, and if you're in the middle of another focus change that appears to be untrue).
        // So instead, we simply ignore reentrant focus attempts.
        // See bug #2951 for an example of this happening and badly hosing things.
        if (_duringFocus) {
            return;
        }

        _duringFocus = true;
        try {
            this._codeMirror.focus();
        } finally {
            _duringFocus = false;
        }
    };

    /** Returns true if the editor has focus */
    Editor.prototype.hasFocus = function () {
        return this._focused;
    };

    /*
     * @typedef {scrollPos:{x:number, y:number},Array.<{start:{line:number, ch:number},end:{line:number, ch:number}}>} EditorViewState
     */

    /*
     * returns the view state for the editor
     * @return {!EditorViewState}
     */
    Editor.prototype.getViewState = function () {
        return {
            selections: this.getSelections(),
            scrollPos: this.getScrollPos()
        };

    };

    /*
     * Restores the view state
     * @param {!EditorViewState} viewState - the view state object to restore
     */
    Editor.prototype.restoreViewState = function (viewState) {
        if (viewState.selection) {
            // We no longer write out single-selection, but there might be some view state
            // from an older version.
            this.setSelection(viewState.selection.start, viewState.selection.end);
        }
        if (viewState.selections) {
            this.setSelections(viewState.selections);
        }
        if (viewState.scrollPos) {
            this.setScrollPos(viewState.scrollPos.x, viewState.scrollPos.y);
        }
    };

    /**
     * Re-renders the editor UI
     * @param {boolean=} handleResize true if this is in response to resizing the editor. Default false.
     */
    Editor.prototype.refresh = function (handleResize) {
        // If focus is currently in a child of the CodeMirror editor (e.g. in an inline widget), but not in
        // the CodeMirror input field itself, remember the focused item so we can restore focus after the
        // refresh (which might cause the widget to be removed from the display list temporarily).
        var focusedItem = window.document.activeElement,
            restoreFocus = $.contains(this._codeMirror.getScrollerElement(), focusedItem);
        this._codeMirror.refresh();
        if (restoreFocus) {
            focusedItem.focus();
        }
    };

    /**
     * Re-renders the editor, and all children inline editors.
     * @param {boolean=} handleResize true if this is in response to resizing the editor. Default false.
     */
    Editor.prototype.refreshAll = function (handleResize) {
        this.refresh(handleResize);
        this.getInlineWidgets().forEach(function (inlineWidget) {
            inlineWidget.refresh();
        });
    };

    /** Undo the last edit. */
    Editor.prototype.undo = function () {
        this._codeMirror.undo();
    };

    /** Redo the last un-done edit. */
    Editor.prototype.redo = function () {
        this._codeMirror.redo();
    };

    /**
     * View API Visibility Change Notification handler.  This is also
     * called by the native "setVisible" API which refresh can be optimized
     * @param {boolean} show true to show the editor, false to hide it
     * @param {boolean} refresh true (default) to refresh the editor, false to skip refreshing it
     */
    Editor.prototype.notifyVisibilityChange = function (show, refresh) {
        if (show && (refresh || refresh === undefined)) {
            this.refresh();
        }
        if (show) {
            this._inlineWidgets.forEach(function (inlineWidget) {
                inlineWidget.onParentShown();
            });
        }
    };

    /**
     * Shows or hides the editor within its parent. Does not force its ancestors to
     * become visible.
     * @param {boolean} show true to show the editor, false to hide it
     * @param {boolean} refresh true (default) to refresh the editor, false to skip refreshing it
     */
    Editor.prototype.setVisible = function (show, refresh) {
        this.$el.css("display", (show ? "" : "none"));
        this.notifyVisibilityChange(show, refresh);
    };

    /**
     * Returns true if the editor is fully visible--i.e., is in the DOM, all ancestors are
     * visible, and has a non-zero width/height.
     */
    Editor.prototype.isFullyVisible = function () {
        return $(this.getRootElement()).is(":visible");
    };

    /**
     * Gets the syntax-highlighting mode for the given range.
     * Returns null if the mode at the start of the selection differs from the mode at the end -
     * an *approximation* of whether the mode is consistent across the whole range (a pattern like
     * A-B-A would return A as the mode, not null).
     *
     * @param {!{line: number, ch: number}} start The start of the range to check.
     * @param {!{line: number, ch: number}} end The end of the range to check.
     * @param {boolean=} knownMixed Whether we already know we're in a mixed mode and need to check both
     *     the start and end.
     * @return {?(Object|string)} Name of syntax-highlighting mode, or object containing a "name" property
     *     naming the mode along with configuration options required by the mode.
     * @see {@link LanguageManager::#getLanguageForPath} and {@link LanguageManager::Language#getMode}.
     */
    Editor.prototype.getModeForRange = function (start, end, knownMixed) {
        var outerMode = this._codeMirror.getMode(),
            startMode = TokenUtils.getModeAt(this._codeMirror, start),
            endMode = TokenUtils.getModeAt(this._codeMirror, end);
        if (!knownMixed && outerMode.name === startMode.name) {
            // Mode does not vary: just use the editor-wide mode name
            return this._codeMirror.getOption("mode");
        } else if (!startMode || !endMode || startMode.name !== endMode.name) {
            return null;
        } else {
            return startMode;
        }
    };

    /**
     * Gets the syntax-highlighting mode for the current selection or cursor position. (The mode may
     * vary within one file due to embedded languages, e.g. JS embedded in an HTML script block). See
     * `getModeForRange()` for how this is determined for a single selection.
     *
     * If there are multiple selections, this will return a mode only if all the selections are individually
     * consistent and resolve to the same mode.
     *
     * @return {?(Object|string)} Name of syntax-highlighting mode, or object containing a "name" property
     *     naming the mode along with configuration options required by the mode.
     * @see {@link LanguageManager::#getLanguageForPath} and {@link LanguageManager::Language#getMode}.
     */
    Editor.prototype.getModeForSelection = function () {
        // Check for mixed mode info
        var self        = this,
            sels        = this.getSelections(),
            primarySel  = this.getSelection(),
            outerMode   = this._codeMirror.getMode(),
            startMode   = TokenUtils.getModeAt(this._codeMirror, primarySel.start),
            isMixed     = (outerMode.name !== startMode.name);

        if (isMixed) {
            // This is the magic code to let the code view know that we are in 'css' context
            // if the CodeMirror outermode is 'htmlmixed' and we are in 'style' attributes
            // value context. This has to be done as CodeMirror doesn't yet think this as 'css'
            // This magic is executed only when user is having a cursor and not selection
            // We will enable selection handling one we figure a way out to handle mixed scope selection
            if (outerMode.name === 'htmlmixed' && primarySel.start.line === primarySel.end.line && primarySel.start.ch === primarySel.end.ch) {
                var tagInfo = HTMLUtils.getTagInfo(this, primarySel.start, true),
                    tokenType = tagInfo.position.tokenType;
 
                if (tokenType === HTMLUtils.ATTR_VALUE && tagInfo.attr.name.toLowerCase() === 'style') {
                    return 'css';
                }
            }
            // Shortcut the first check to avoid getModeAt(), which can be expensive
            if (primarySel.start.line !== primarySel.end.line || primarySel.start.ch !== primarySel.end.ch) {
                var endMode = TokenUtils.getModeAt(this._codeMirror, primarySel.end);

                if (startMode.name !== endMode.name) {
                    return null;
                }
            }

            // If mixed mode, check that mode is the same at start & end of each selection
            var hasMixedSel = _.some(sels, function (sel) {
                if (sels === primarySel) {
                    // We already checked this before, so we know it's not mixed.
                    return false;
                }

                var rangeMode = self.getModeForRange(sel.start, sel.end, true);
                return (!rangeMode || rangeMode.name !== startMode.name);
            });
            if (hasMixedSel) {
                return null;
            }

            return startMode.name;
        } else {
            // Mode does not vary: just use the editor-wide mode
            return this._codeMirror.getOption("mode");
        }
    };

    /*
     * gets the language for the selection. (Javascript selected from an HTML document or CSS selected from an HTML document, etc...)
     * @return {!Language}
     */
    Editor.prototype.getLanguageForSelection = function () {
        return this.document.getLanguage().getLanguageForMode(this.getModeForSelection());
    };

    /**
     * Gets the syntax-highlighting mode for the document.
     *
     * @return {Object|String} Object or Name of syntax-highlighting mode
     * @see {@link LanguageManager::#getLanguageForPath|LanguageManager.getLanguageForPath} and {@link LanguageManager::Language#getMode|Language.getMode}.
     */
    Editor.prototype.getModeForDocument = function () {
        return this._codeMirror.getOption("mode");
    };

    /**
     * The Document we're bound to
     * @type {!Document}
     */
    Editor.prototype.document = null;


    /**
     * The Editor's last known width.
     * Used in conjunction with updateLayout to recompute the layout
     * if the parent container changes its size since our last layout update.
     * @type {?number}
     */
    Editor.prototype._lastEditorWidth = null;


    /**
     * If true, we're in the middle of syncing to/from the Document. Used to ignore spurious change
     * events caused by us (vs. change events caused by others, which we need to pay attention to).
     * @type {!boolean}
     */
    Editor.prototype._duringSync = false;

    /**
     * @private
     * NOTE: this is actually "semi-private": EditorManager also accesses this field... as well as
     * a few other modules. However, we should try to gradually move most code away from talking to
     * CodeMirror directly.
     * @type {!CodeMirror}
     */
    Editor.prototype._codeMirror = null;

    /**
     * @private
     * @type {!Array.<{id:number, data:Object}>}
     */
    Editor.prototype._inlineWidgets = null;

    /**
     * @private
     * @type {?TextRange}
     */
    Editor.prototype._visibleRange = null;

    /**
     * @private
     * @type {Object}
     * Promise queues for inline widgets being added to a given line.
     */
    Editor.prototype._inlineWidgetQueues = null;

    /**
     * @private
     * @type {Array}
     * A list of objects corresponding to the markers that are hiding lines in the current editor.
     */
    Editor.prototype._hideMarks = null;

    /**
     * @private
     *
     * Retrieve the value of the named preference for this document.
     *
     * @param {string} prefName Name of preference to retrieve.
     * @return {*} current value of that pref
     */
    Editor.prototype._getOption = function (prefName) {
        return PreferencesManager.get(prefName, PreferencesManager._buildContext(this.document.file.fullPath, this.document.getLanguage().getId()));
    };

    /**
     * @private
     *
     * Updates the editor to the current value of prefName for the file being edited.
     *
     * @param {string} prefName Name of the preference to visibly update
     */
    Editor.prototype._updateOption = function (prefName) {
        var oldValue = this._currentOptions[prefName],
            newValue = this._getOption(prefName);

        if (oldValue !== newValue) {
            this._currentOptions[prefName] = newValue;

            if (prefName === USE_TAB_CHAR) {
                this._codeMirror.setOption(cmOptions[prefName], newValue);
                this._codeMirror.setOption("indentUnit", newValue === true ?
                                           this._currentOptions[TAB_SIZE] :
                                           this._currentOptions[SPACE_UNITS]
                                          );
            } else if (prefName === STYLE_ACTIVE_LINE) {
                this._updateStyleActiveLine();
            } else if (prefName === SCROLL_PAST_END && this._visibleRange) {
                // Do not apply this option to inline editors
                return;
            } else if (prefName === SHOW_LINE_NUMBERS) {
                Editor._toggleLinePadding(!newValue);
                this._codeMirror.setOption(cmOptions[SHOW_LINE_NUMBERS], newValue);
                if (newValue) {
                    Editor.registerGutter(LINE_NUMBER_GUTTER, LINE_NUMBER_GUTTER_PRIORITY);
                } else {
                    Editor.unregisterGutter(LINE_NUMBER_GUTTER);
                }
                this.refreshAll();
            } else {
                this._codeMirror.setOption(cmOptions[prefName], newValue);
            }

            this.trigger("optionChange", prefName, newValue);
        }
    };

    /**
     * @private
     *
     * Used to ensure that "style active line" is turned off when there is a selection.
     */
    Editor.prototype._updateStyleActiveLine = function () {
        if (this.hasSelection()) {
            if (this._codeMirror.getOption("styleActiveLine")) {
                this._codeMirror.setOption("styleActiveLine", false);
            }
        } else {
            this._codeMirror.setOption("styleActiveLine", this._currentOptions[STYLE_ACTIVE_LINE]);
        }
    };

    /**
     * resizes the editor to fill its parent container
     * should not be used on inline editors
     * @param {boolean=} forceRefresh - forces the editor to update its layout
     *                                   even if it already matches the container's height / width
     */
    Editor.prototype.updateLayout = function (forceRefresh) {
        var curRoot = this.getRootElement(),
            curWidth = $(curRoot).width(),
            $editorHolder = this.$el.parent(),
            editorAreaHt = $editorHolder.height();

        if (!curRoot.style.height || $(curRoot).height() !== editorAreaHt) {
            // Call setSize() instead of $.height() to allow CodeMirror to
            // check for options like line wrapping
            this.setSize(null, editorAreaHt);
            if (forceRefresh === undefined) {
                forceRefresh = true;
            }
        } else if (curWidth !== this._lastEditorWidth) {
            if (forceRefresh === undefined) {
                forceRefresh = true;
            }
        }
        this._lastEditorWidth = curWidth;

        if (forceRefresh) {
            this.refreshAll(forceRefresh);
        }
    };

    /**
     * Clears all marks from the gutter with the specified name.
     * @param {string} name The name of the gutter to clear.
     */
    Editor.prototype.clearGutter = function (name) {
        this._codeMirror.clearGutter(name);
    };

    /**
     * Renders all registered gutters
     * @private
     */
    Editor.prototype._renderGutters = function () {
        var languageId = this.document.getLanguage().getId();

        function _filterByLanguages(gutter) {
            return !gutter.languages || gutter.languages.indexOf(languageId) > -1;
        }

        function _sortByPriority(a, b) {
            return a.priority - b.priority;
        }

        function _getName(gutter) {
            return gutter.name;
        }

        var gutters = registeredGutters.map(_getName),
            rootElement = this.getRootElement();

        // If the line numbers gutter has not been explicitly registered and the CodeMirror lineNumbes option is
        // set to true, we explicitly add the line numbers gutter. This case occurs the first time the editor loads
        // and showLineNumbers is set to true in preferences
        if (gutters.indexOf(LINE_NUMBER_GUTTER) < 0 && this._codeMirror.getOption(cmOptions[SHOW_LINE_NUMBERS])) {
            registeredGutters.push({name: LINE_NUMBER_GUTTER, priority: LINE_NUMBER_GUTTER_PRIORITY});
        }

        gutters = registeredGutters.sort(_sortByPriority)
            .filter(_filterByLanguages)
            .map(_getName);

        this._codeMirror.setOption("gutters", gutters);
        this._codeMirror.refresh();

        if (gutters.indexOf(LINE_NUMBER_GUTTER) < 0) {
            $(rootElement).addClass("linenumber-disabled");
        } else {
            $(rootElement).removeClass("linenumber-disabled");
        }
    };

    /**
     * Sets the marker for the specified gutter on the specified line number
     * @param   {string}   lineNumber The line number for the inserted gutter marker
     * @param   {string}   gutterName The name of the gutter
     * @param   {object}   marker     The dom element representing the marker to the inserted in the gutter
     */
    Editor.prototype.setGutterMarker = function (lineNumber, gutterName, marker) {
        var gutterNameRegistered = registeredGutters.some(function (gutter) {
            return gutter.name === gutterName;
        });

        if (!gutterNameRegistered) {
            console.warn("Gutter name must be registered before calling editor.setGutterMarker");
            return;
        }

        this._codeMirror.setGutterMarker(lineNumber, gutterName, marker);
    };

    /**
     * Returns the list of gutters current registered on all editors.
     * @return {!Array.<{name: string, priority: number}>}
     */
    Editor.getRegisteredGutters = function () {
        return registeredGutters;
    };

    /**
     * Registers the gutter with the specified name at the given priority.
     * @param {string} name    The name of the gutter.
     * @param {number} priority  A number denoting the priority of the gutter. Priorities higher than LINE_NUMBER_GUTTER_PRIORITY appear after the line numbers. Priority less than LINE_NUMBER_GUTTER_PRIORITY appear before.
     * @param {?Array<string>} languageIds A list of language ids that this gutter is valid for. If no language ids are passed, then the gutter is valid in all languages.
     */
    Editor.registerGutter = function (name, priority, languageIds) {
        if (isNaN(priority)) {
            console.warn("A non-numeric priority value was passed to registerGutter. The value will default to 0.");
            priority = 0;
        }

        if (!name || typeof name !== "string") {
            console.error("The name of the registered gutter must be a string.");
            return;
        }

        var gutter = {name: name, priority: priority, languages: languageIds},
            gutterExists = registeredGutters.some(function (gutter) {
                return gutter.name === name;
            });

        if (!gutterExists) {
            registeredGutters.push(gutter);
        }

        Editor.forEveryEditor(function (editor) {
            editor._renderGutters();
        });
    };

    /**
     * Unregisters the gutter with the specified name and removes it from the UI.
     * @param {string} name The name of the gutter to be unregistered.
     */
    Editor.unregisterGutter = function (name) {
        var i, gutter;
        registeredGutters = registeredGutters.filter(function (gutter) {
            return gutter.name !== name;
        });

        Editor.forEveryEditor(function (editor) {
            editor._renderGutters();
        });
    };

    // Global settings that affect Editor instances that share the same preference locations

    /**
     * Sets whether to use tab characters (vs. spaces) when inserting new text.
     * Affects any editors that share the same preference location.
     * @param {boolean} value
     * @param {string=} fullPath Path to file to get preference for
     * @return {boolean} true if value was valid
     */
    Editor.setUseTabChar = function (value, fullPath) {
        var options = fullPath && {context: fullPath};
        return PreferencesManager.set(USE_TAB_CHAR, value, options);
    };

    /**
     * Gets whether the specified or current file uses tab characters (vs. spaces) when inserting new text
     * @param {string=} fullPath Path to file to get preference for
     * @return {boolean}
     */
    Editor.getUseTabChar = function (fullPath) {
        return PreferencesManager.get(USE_TAB_CHAR, _buildPreferencesContext(fullPath));
    };

    /**
     * Sets tab character width.
     * Affects any editors that share the same preference location.
     * @param {number} value
     * @param {string=} fullPath Path to file to get preference for
     * @return {boolean} true if value was valid
     */
    Editor.setTabSize = function (value, fullPath) {
        var options = fullPath && {context: fullPath};
        return PreferencesManager.set(TAB_SIZE, value, options);
    };

    /**
     * Get indent unit
     * @param {string=} fullPath Path to file to get preference for
     * @return {number}
     */
    Editor.getTabSize = function (fullPath) {
        return PreferencesManager.get(TAB_SIZE, _buildPreferencesContext(fullPath));
    };

    /**
     * Sets indentation width.
     * Affects any editors that share the same preference location.
     * @param {number} value
     * @param {string=} fullPath Path to file to get preference for
     * @return {boolean} true if value was valid
     */
    Editor.setSpaceUnits = function (value, fullPath) {
        var options = fullPath && {context: fullPath};
        return PreferencesManager.set(SPACE_UNITS, value, options);
    };

    /**
     * Get indentation width
     * @param {string=} fullPath Path to file to get preference for
     * @return {number}
     */
    Editor.getSpaceUnits = function (fullPath) {
        return PreferencesManager.get(SPACE_UNITS, _buildPreferencesContext(fullPath));
    };

    /**
     * Sets the auto close brackets.
     * Affects any editors that share the same preference location.
     * @param {boolean} value
     * @param {string=} fullPath Path to file to get preference for
     * @return {boolean} true if value was valid
     */
    Editor.setCloseBrackets = function (value, fullPath) {
        var options = fullPath && {context: fullPath};
        return PreferencesManager.set(CLOSE_BRACKETS, value, options);
    };

    /**
     * Gets whether the specified or current file uses auto close brackets
     * @param {string=} fullPath Path to file to get preference for
     * @return {boolean}
     */
    Editor.getCloseBrackets = function (fullPath) {
        return PreferencesManager.get(CLOSE_BRACKETS, _buildPreferencesContext(fullPath));
    };

    /**
     * Sets show line numbers option.
     * Affects any editors that share the same preference location.
     * @param {boolean} value
     * @param {string=} fullPath Path to file to get preference for
     * @return {boolean} true if value was valid
     */
    Editor.setShowLineNumbers = function (value, fullPath) {
        var options = fullPath && {context: fullPath};
        return PreferencesManager.set(SHOW_LINE_NUMBERS, value, options);
    };

    /**
     * Returns true if show line numbers is enabled for the specified or current file
     * @param {string=} fullPath Path to file to get preference for
     * @return {boolean}
     */
    Editor.getShowLineNumbers = function (fullPath) {
        return PreferencesManager.get(SHOW_LINE_NUMBERS, _buildPreferencesContext(fullPath));
    };

    /**
     * Sets show active line option.
     * Affects any editors that share the same preference location.
     * @param {boolean} value
     * @param {string=} fullPath Path to file to get preference for
     * @return {boolean} true if value was valid
     */
    Editor.setShowActiveLine = function (value, fullPath) {
        return PreferencesManager.set(STYLE_ACTIVE_LINE, value);
    };

    /**
     * Returns true if show active line is enabled for the specified or current file
     * @param {string=} fullPath Path to file to get preference for
     * @return {boolean}
     */
    Editor.getShowActiveLine = function (fullPath) {
        return PreferencesManager.get(STYLE_ACTIVE_LINE, _buildPreferencesContext(fullPath));
    };

    /**
     * Sets word wrap option.
     * Affects any editors that share the same preference location.
     * @param {boolean} value
     * @param {string=} fullPath Path to file to get preference for
     * @return {boolean} true if value was valid
     */
    Editor.setWordWrap = function (value, fullPath) {
        var options = fullPath && {context: fullPath};
        return PreferencesManager.set(WORD_WRAP, value, options);
    };

    /**
     * Returns true if word wrap is enabled for the specified or current file
     * @param {string=} fullPath Path to file to get preference for
     * @return {boolean}
     */
    Editor.getWordWrap = function (fullPath) {
        return PreferencesManager.get(WORD_WRAP, _buildPreferencesContext(fullPath));
    };

    /**
     * Sets indentLineComment option.
     * Affects any editors that share the same preference location.
     * @param {boolean} value
     * @param {string=} fullPath Path to file to get preference for
     * @return {boolean} true if value was valid
     */
    Editor.setIndentLineComment = function (value, fullPath) {
        var options = fullPath && {context: fullPath};
        return PreferencesManager.set(INDENT_LINE_COMMENT, value, options);
    };

    /**
     * Returns true if indentLineComment is enabled for the specified or current file
     * @param {string=} fullPath Path to file to get preference for
     * @return {boolean}
     */
    Editor.getIndentLineComment = function (fullPath) {
        return PreferencesManager.get(INDENT_LINE_COMMENT, _buildPreferencesContext(fullPath));
    };

    /**
     * Runs callback for every Editor instance that currently exists
     * @param {!function(!Editor)} callback
     */
    Editor.forEveryEditor = function (callback) {
        _instances.forEach(callback);
    };

    /**
     * @private
     * Toggles the left padding of all code editors.  Used to provide more
     * space between the code text and the left edge of the editor when
     * line numbers are hidden.
     * @param {boolean} showLinePadding
     */
    Editor._toggleLinePadding = function (showLinePadding) {
        // apply class to all pane DOM nodes
        var $holders = [];
        _instances.forEach(function (editor) {
            var $editorHolder = editor.$el.parent();
            if ($holders.indexOf($editorHolder) === -1) {
                $holders.push($editorHolder);
            }
        });

        _.each($holders, function ($holder) {
            $holder.toggleClass("show-line-padding", Boolean(showLinePadding));
        });
    };
    
    Editor.LINE_NUMBER_GUTTER_PRIORITY = LINE_NUMBER_GUTTER_PRIORITY;
    Editor.CODE_FOLDING_GUTTER_PRIORITY = CODE_FOLDING_GUTTER_PRIORITY;
    
    // Set up listeners for preference changes
    editorOptions.forEach(function (prefName) {
        PreferencesManager.on("change", prefName, function () {
            _instances.forEach(function (editor) {
                editor._updateOption(prefName);
            });
        });
    });

    // Define public API
    exports.Editor                  = Editor;
    exports.BOUNDARY_CHECK_NORMAL   = BOUNDARY_CHECK_NORMAL;
    exports.BOUNDARY_IGNORE_TOP     = BOUNDARY_IGNORE_TOP;
});