adobe/brackets

View on GitHub
src/editor/EditorCommandHandlers.js

Summary

Maintainability
F
5 days
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.
 *
 */

/**
 * Text-editing commands that apply to whichever Editor is currently focused
 */
define(function (require, exports, module) {
    "use strict";

    // Load dependent modules
    var Commands           = require("command/Commands"),
        Strings            = require("strings"),
        Editor              = require("editor/Editor").Editor,
        CommandManager     = require("command/CommandManager"),
        EditorManager      = require("editor/EditorManager"),
        StringUtils        = require("utils/StringUtils"),
        TokenUtils         = require("utils/TokenUtils"),
        CodeMirror         = require("thirdparty/CodeMirror/lib/codemirror"),
        _                  = require("thirdparty/lodash");

    /**
     * List of constants
     */
    var DIRECTION_UP    = -1;
    var DIRECTION_DOWN  = +1;

    /**
     * @private
     * Creates special regular expressions that matches the line prefix but not the block prefix or suffix
     * @param {!string} lineSyntax  a line comment prefix
     * @param {!string} blockSyntax  a block comment prefix or suffix
     * @return {RegExp}
     */
    function _createSpecialLineExp(lineSyntax, blockSyntax) {
        var i, character, escapedCharacter,
            subExps   = [],
            prevChars = "";

        for (i = lineSyntax.length; i < blockSyntax.length; i++) {
            character = blockSyntax.charAt(i);
            escapedCharacter = StringUtils.regexEscape(character);
            subExps.push(prevChars + "[^" + escapedCharacter + "]");
            if (prevChars) {
                subExps.push(prevChars + "$");
            }
            prevChars += escapedCharacter;
        }
        return new RegExp("^\\s*" + StringUtils.regexEscape(lineSyntax) + "($|" + subExps.join("|") + ")");
    }

    /**
     * @private
     * Creates regular expressions for multiple line comment prefixes
     * @param {!Array.<string>} prefixes  the line comment prefixes
     * @param {string=} blockPrefix  the block comment prefix
     * @param {string=} blockSuffix  the block comment suffix
     * @return {Array.<RegExp>}
     */
    function _createLineExpressions(prefixes, blockPrefix, blockSuffix) {
        var lineExp = [], escapedPrefix, nothingPushed;

        prefixes.forEach(function (prefix) {
            escapedPrefix = StringUtils.regexEscape(prefix);
            nothingPushed = true;

            if (blockPrefix && blockPrefix.indexOf(prefix) === 0) {
                lineExp.push(_createSpecialLineExp(prefix, blockPrefix));
                nothingPushed = false;
            }
            if (blockSuffix && blockPrefix !== blockSuffix && blockSuffix.indexOf(prefix) === 0) {
                lineExp.push(_createSpecialLineExp(prefix, blockSuffix));
                nothingPushed = false;
            }
            if (nothingPushed) {
                lineExp.push(new RegExp("^\\s*" + escapedPrefix));
            }
        });
        return lineExp;
    }

    /**
     * @private
     * Returns true if any regular expression matches the given string
     * @param {!string} string  where to look
     * @param {!Array.<RegExp>} expressions  what to look
     * @return {boolean}
     */
    function _matchExpressions(string, expressions) {
        return expressions.some(function (exp) {
            return string.match(exp);
        });
    }

    /**
     * @private
     * Returns the line comment prefix that best matches the string. Since there might be line comment prefixes
     * that are prefixes of other line comment prefixes, it searches through all and returns the longest line
     * comment prefix that matches the string.
     * @param {!string} string  where to look
     * @param {!Array.<RegExp>} expressions  the line comment regular expressions
     * @param {!Array.<string>} prefixes  the line comment prefixes
     * @return {string}
     */
    function _getLinePrefix(string, expressions, prefixes) {
        var result = null;
        expressions.forEach(function (exp, index) {
            if (string.match(exp) && ((result && result.length < prefixes[index].length) || !result)) {
                result = prefixes[index];
            }
        });
        return result;
    }

    /**
     * @private
     * Searches between startLine and endLine to check if there is at least one line commented with a line comment, and
     * skips all the block comments.
     * @param {!Editor} editor
     * @param {!number} startLine  valid line inside the document
     * @param {!number} endLine  valid line inside the document
     * @param {!Array.<RegExp>} lineExp  an array of line comment prefixes regular expressions
     * @return {boolean} true if there is at least one uncommented line
     */
    function _containsNotLineComment(editor, startLine, endLine, lineExp) {
        var i, line,
            containsNotLineComment = false;

        for (i = startLine; i <= endLine; i++) {
            line = editor.document.getLine(i);
            // A line is commented out if it starts with 0-N whitespace chars, then a line comment prefix
            if (line.match(/\S/) && !_matchExpressions(line, lineExp)) {
                containsNotLineComment = true;
                break;
            }
        }
        return containsNotLineComment;
    }

    /**
     * @private
     * Generates an edit that adds or removes line-comment tokens to all the lines in the selected range,
     * preserving selection and cursor position. Applies to currently focused Editor. The given selection
     * must already be a line selection in the form returned by `Editor.convertToLineSelections()`.
     *
     * If all non-whitespace lines are already commented out, then we uncomment; otherwise we comment
     * out. Commenting out adds the prefix at column 0 of every line. Uncommenting removes the first prefix
     * on each line (if any - empty lines might not have one).
     *
     * @param {!Editor} editor
     * @param {!Array.<string>} prefixes, e.g. ["//"]
     * @param {string=} blockPrefix, e.g. "<!--"
     * @param {string=} blockSuffix, e.g. "-->"
     * @param {!Editor} editor The editor to edit within.
     * @param {!{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}>}}
     *      lineSel A line selection as returned from `Editor.convertToLineSelections()`. `selectionForEdit` is the selection to perform
     *      the line comment operation on, and `selectionsToTrack` are a set of selections associated with this line that need to be
     *      tracked through the edit.
     * @return {{edit: {text: string, start:{line: number, ch: number}, end:?{line: number, ch: number}}|Array.<{text: string, start:{line: number, ch: number}, end:?{line: number, ch: number}}>,
     *                  selection: {start:{line:number, ch:number}, end:{line:number, ch:number}, primary:boolean, reversed: boolean, isBeforeEdit: boolean}>}|
     *                  Array.<{start:{line:number, ch:number}, end:{line:number, ch:number}, primary:boolean, reversed: boolean, isBeforeEdit: boolean}>}}
     *      An edit description suitable for including in the edits array passed to `Document.doMultipleEdits()`.
     */
    function _getLineCommentPrefixEdit(editor, prefixes, blockPrefix, blockSuffix, lineSel) {
        var doc         = editor.document,
            sel         = lineSel.selectionForEdit,
            trackedSels = lineSel.selectionsToTrack,
            lineExp     = _createLineExpressions(prefixes, blockPrefix, blockSuffix),
            startLine   = sel.start.line,
            endLine     = sel.end.line,
            editGroup   = [];

        // In full-line selection, cursor pos is start of next line - but don't want to modify that line
        if (sel.end.ch === 0) {
            endLine--;
        }

        // Decide if we're commenting vs. un-commenting
        // Are there any non-blank lines that aren't commented out? (We ignore blank lines because
        // some editors like Sublime don't comment them out)
        var i, line, prefix, commentI,
            containsNotLineComment = _containsNotLineComment(editor, startLine, endLine, lineExp);

        if (containsNotLineComment) {
            // Comment out - prepend the first prefix to each line
            line = doc.getLine(startLine);
            var originalCursorPosition = line.search(/\S|$/);
            
            var firstCharPosition, cursorPosition = originalCursorPosition;
            
            for (i = startLine; i <= endLine; i++) {
                //check if preference for indent line comment is available otherwise go back to default indentation
                if (Editor.getIndentLineComment()) {
                    //ignore the first line and recalculate cursor position for first non white space char of every line
                    if (i !== startLine) {
                        line = doc.getLine(i);
                        firstCharPosition = line.search(/\S|$/);
                    }
                    //if the non space first character position is before original start position , put comment at the new position otherwise older pos
                    if (firstCharPosition < originalCursorPosition) {
                        cursorPosition = firstCharPosition;
                    } else {
                        cursorPosition = originalCursorPosition;
                    }
                    
                    editGroup.push({text: prefixes[0], start: {line: i, ch: cursorPosition}});
                } else {
                    editGroup.push({text: prefixes[0], start: {line: i, ch: 0}});
                }
            }

            // Make sure tracked selections include the prefix that was added at start of range
            _.each(trackedSels, function (trackedSel) {
                if (trackedSel.start.ch === 0 && CodeMirror.cmpPos(trackedSel.start, trackedSel.end) !== 0) {
                    trackedSel.start = {line: trackedSel.start.line, ch: 0};
                    trackedSel.end = {line: trackedSel.end.line, ch: (trackedSel.end.line === endLine ? trackedSel.end.ch + prefixes[0].length : 0)};
                } else {
                    trackedSel.isBeforeEdit = true;
                }
            });
        } else {
            // Uncomment - remove the prefix on each line (if any)
            for (i = startLine; i <= endLine; i++) {
                line   = doc.getLine(i);
                prefix = _getLinePrefix(line, lineExp, prefixes);

                if (prefix) {
                    commentI = line.indexOf(prefix);
                    editGroup.push({text: "", start: {line: i, ch: commentI}, end: {line: i, ch: commentI + prefix.length}});
                }
            }
            _.each(trackedSels, function (trackedSel) {
                trackedSel.isBeforeEdit = true;
            });
        }
        return {edit: editGroup, selection: trackedSels};
    }

    /**
     * @private
     * Given a token context it will search backwards to determine if the given token is part of a block comment
     * that doesn't start at the initial token. This is used to know if a line comment is part of a block comment
     * or if a block delimiter is the prefix or suffix, by passing a token context at that position. Since the
     * token context will be moved backwards a lot, it is better to pass a new context.
     *
     * @param {!{editor:{CodeMirror}, pos:{ch:{number}, line:{number}}, token:{object}}} ctx  token context
     * @param {!string} prefix  the block comment prefix
     * @param {!string} suffix  the block comment suffix
     * @param {!RegExp} prefixExp  a block comment prefix regular expression
     * @param {!RegExp} suffixExp  a block comment suffix regular expression
     * @param {!Array.<RegExp>} lineExp  an array of line comment prefixes regular expressions
     * @return {boolean}
     */
    function _isPrevTokenABlockComment(ctx, prefix, suffix, prefixExp, suffixExp, lineExp) {
        // Start searching from the previous token
        var result = TokenUtils.moveSkippingWhitespace(TokenUtils.movePrevToken, ctx);

        // Look backwards until we find a none line comment token
        while (result && _matchExpressions(ctx.token.string, lineExp)) {
            result = TokenUtils.moveSkippingWhitespace(TokenUtils.movePrevToken, ctx);
        }

        // If we are now in a block comment token
        if (result && ctx.token.type === "comment") {
            // If it doesnt matches either prefix or suffix, we know is a block comment
            if (!ctx.token.string.match(prefixExp) && !ctx.token.string.match(suffixExp)) {
                return true;
            // We found a line with just a block comment delimiter, but we can't tell which one it is, so we will
            // keep searching recursively and return the opposite value
            } else if (prefix === suffix && ctx.token.string.length === prefix.length) {
                return !_isPrevTokenABlockComment(ctx, prefix, suffix, prefixExp, suffixExp, lineExp);
            // We can just now the result by checking if the string matches the prefix
            } else {
                return ctx.token.string.match(prefixExp);
            }
        }
        return false;
    }

    /**
     * Return the column of the first non whitespace char in the given line.
     *
     * @private
     * @param {!Document} doc
     * @param {number} lineNum
     * @returns {number} the column index or null
     */
    function _firstNotWs(doc, lineNum) {
        var text = doc.getLine(lineNum);
        if (text === null || text === undefined) {
            return 0;
        }

        return text.search(/\S|$/);
    }

    /**
     * Generates an edit that adds or removes block-comment tokens to the selection, preserving selection
     * and cursor position. Applies to the currently focused Editor.
     *
     * If the selection is inside a block-comment or one block-comment is inside or partially inside the selection
     * it will uncomment, otherwise it will comment out, unless if there are multiple block comments inside the selection,
     * where it does nothing.
     * Commenting out adds the prefix before the selection and the suffix after.
     * Uncommenting removes them.
     *
     * If all the lines inside the selection are line-comment and if the selection is not inside a block-comment, it will
     * line uncomment all the lines, otherwise it will block comment/uncomment. In the first case, we return null to
     * indicate to the caller that it needs to handle this selection as a line comment.
     *
     * @param {!Editor} editor
     * @param {!string} prefix, e.g. "<!--"
     * @param {!string} suffix, e.g. "-->"
     * @param {!Array.<string>} linePrefixes, e.g. ["//"]
     * @param {!{start:{line:number, ch:number}, end:{line:number, ch:number}, reversed:boolean, primary:boolean}} sel
     *      The selection to block comment/uncomment.
     * @param {?Array.<{!{start:{line:number, ch:number}, end:{line:number, ch:number}, reversed:boolean, primary:boolean}}>} selectionsToTrack
     *      An array of selections that should be tracked through this edit.
     * @param {String} command The command callee. It cans be "line" or "block".
     * @return {{edit: {text: string, start:{line: number, ch: number}, end:?{line: number, ch: number}}|Array.<{text: string, start:{line: number, ch: number}, end:?{line: number, ch: number}}>,
     *                  selection: {start:{line:number, ch:number}, end:{line:number, ch:number}, primary:boolean, reversed: boolean, isBeforeEdit: boolean}>}|
     *                  Array.<{start:{line:number, ch:number}, end:{line:number, ch:number}, primary:boolean, reversed: boolean, isBeforeEdit: boolean}>}}
     *      An edit description suitable for including in the edits array passed to `Document.doMultipleEdits()`.
     */
    function _getBlockCommentPrefixSuffixEdit(editor, prefix, suffix, linePrefixes, sel, selectionsToTrack, command) {
        var doc            = editor.document,
            ctx            = TokenUtils.getInitialContext(editor._codeMirror, {line: sel.start.line, ch: sel.start.ch}),
            selEndIndex    = editor.indexFromPos(sel.end),
            lineExp        = _createLineExpressions(linePrefixes, prefix, suffix),
            prefixExp      = new RegExp("^" + StringUtils.regexEscape(prefix), "g"),
            suffixExp      = new RegExp(StringUtils.regexEscape(suffix) + "$", "g"),
            prefixPos      = null,
            suffixPos      = null,
            commentAtStart = true,
            isBlockComment = false,
            canComment     = false,
            invalidComment = false,
            lineUncomment  = false,
            result         = true,
            editGroup      = [],
            edit;

        var searchCtx, atSuffix, suffixEnd, initialPos, endLine;

        var indentLineComment = Editor.getIndentLineComment();

        function isIndentLineCommand() {
            return indentLineComment && command === "line";
        }

        if (!selectionsToTrack) {
            // Track the original selection.
            selectionsToTrack = [_.cloneDeep(sel)];
        }

        // First move the context to the first none white-space token
        if (!ctx.token.type && !/\S/.test(ctx.token.string)) {
            result = TokenUtils.moveSkippingWhitespace(TokenUtils.moveNextToken, ctx);
        }

        // Next, move forwards until we find a comment inside the selection
        while (result && ctx.token.type !== "comment") {
            result = TokenUtils.moveSkippingWhitespace(TokenUtils.moveNextToken, ctx) && editor.indexFromPos(ctx.pos) <= selEndIndex;
            commentAtStart = false;
        }

        // We are now in a comment, lets check if it is a block or a line comment
        if (result && ctx.token.type === "comment") {
            // This token might be at a line comment, but we can't be sure yet
            if (_matchExpressions(ctx.token.string, lineExp)) {
                // If the token starts at ch 0 with no starting white spaces, then this might be a block comment or a line
                // comment over the whole line, and if we found this comment at the start of the selection, we need to search
                // backwards until we get can tell if we are in a block or a line comment
                if (ctx.token.start === 0 && !ctx.token.string.match(/^\\s*/) && commentAtStart) {
                    searchCtx      = TokenUtils.getInitialContext(editor._codeMirror, {line: ctx.pos.line, ch: ctx.token.start});
                    isBlockComment = _isPrevTokenABlockComment(searchCtx, prefix, suffix, prefixExp, suffixExp, lineExp);

                // If not, we already know that is a line comment
                } else {
                    isBlockComment = false;
                }

            // If it was not a line comment, it has to be a block comment
            } else {
                isBlockComment = true;

                // If we are in a line that only has a prefix or a suffix and the prefix and suffix are the same string,
                // lets find first if this is a prefix or suffix and move the context position to the inside of the block comment.
                // This means that the token will be anywere inside the block comment, including the lines with the delimiters.
                // This is required so that later we can find the prefix by moving backwards and the suffix by moving forwards.
                if (ctx.token.string === prefix && prefix === suffix) {
                    searchCtx = TokenUtils.getInitialContext(editor._codeMirror, {line: ctx.pos.line, ch: ctx.token.start});
                    atSuffix  = _isPrevTokenABlockComment(searchCtx, prefix, suffix, prefixExp, suffixExp, lineExp);
                    if (atSuffix) {
                        TokenUtils.moveSkippingWhitespace(TokenUtils.movePrevToken, ctx);
                    } else {
                        TokenUtils.moveSkippingWhitespace(TokenUtils.moveNextToken, ctx);
                    }
                }
            }

            if (isBlockComment) {
                // Save the initial position to start searching for the suffix from here
                initialPos = _.cloneDeep(ctx.pos);

                // Find the position of the start of the prefix
                result = true;
                while (result && !ctx.token.string.match(prefixExp)) {
                    result = TokenUtils.moveSkippingWhitespace(TokenUtils.movePrevToken, ctx);
                }
                prefixPos = result && {line: ctx.pos.line, ch: ctx.token.start};

                // Restore the context at the initial position to find the position of the start of the suffix,
                // but only when we found the prefix alone in one line
                if (ctx.token.string === prefix && prefix === suffix) {
                    ctx = TokenUtils.getInitialContext(editor._codeMirror, _.cloneDeep(initialPos));
                }

                while (result && !ctx.token.string.match(suffixExp)) {
                    result = TokenUtils.moveSkippingWhitespace(TokenUtils.moveNextToken, ctx);
                }
                suffixPos = result && {line: ctx.pos.line, ch: ctx.token.end - suffix.length};

                // Lets check if there are more comments in the selection. We do nothing if there is one
                do {
                    result = TokenUtils.moveSkippingWhitespace(TokenUtils.moveNextToken, ctx) &&
                        editor.indexFromPos(ctx.pos) <= selEndIndex;
                } while (result && !ctx.token.string.match(prefixExp));
                invalidComment = result && !!ctx.token.string.match(prefixExp);

                // Make sure we didn't search so far backward or forward that we actually found a block comment
                // that's entirely before or after the selection.
                suffixEnd = suffixPos && { line: suffixPos.line, ch: suffixPos.ch + suffix.length };
                if ((suffixEnd && CodeMirror.cmpPos(sel.start, suffixEnd) > 0) || (prefixPos && CodeMirror.cmpPos(sel.end, prefixPos) < 0)) {
                    canComment = true;
                }

            } else {
                // In full-line selection, cursor pos is at the start of next line - but don't want to modify that line
                endLine = sel.end.line;
                if (sel.end.ch === 0 && editor.hasSelection()) {
                    endLine--;
                }
                // Find if all the lines are line-commented.
                if (!_containsNotLineComment(editor, sel.start.line, endLine, lineExp)) {
                    lineUncomment = true;
                } else {
                    canComment = true;
                }
            }
        // If not, we can comment
        } else {
            canComment = true;
        }


        // Make the edit
        if (invalidComment) {
            // We don't want to do an edit, but we still want to track selections associated with it.
            edit = {edit: [], selection: selectionsToTrack};

        } else if (lineUncomment) {
            // Return a null edit. This is a signal to the caller that we should delegate to the
            // line commenting code. We don't want to just generate the edit here, because the edit
            // might need to be coalesced with other line-uncomment edits generated by cursors on the
            // same line.
            edit = null;

        } else {
            // Comment out - add the suffix to the start and the prefix to the end.
            if (canComment) {
                var completeLineSel = sel.start.ch === 0 && sel.end.ch === 0 && sel.start.line < sel.end.line;
                var startCh = _firstNotWs(doc, sel.start.line);
                if (completeLineSel) {
                    if (isIndentLineCommand()) {
                        var endCh = _firstNotWs(doc, sel.end.line - 1);
                        var useTabChar = Editor.getUseTabChar(editor.document.file.fullPath);
                        var indentChar = useTabChar ? "\t" : " ";
                        editGroup.push({
                            text: _.repeat(indentChar, endCh) + suffix + "\n",
                            start: {line: sel.end.line, ch: 0}
                        });
                        editGroup.push({
                            text: prefix + "\n" + _.repeat(indentChar, startCh),
                            start: {line: sel.start.line, ch: startCh}
                        });
                    } else {
                        editGroup.push({text: suffix + "\n", start: sel.end});
                        editGroup.push({text: prefix + "\n", start: sel.start});
                    }
                } else {
                    editGroup.push({text: suffix, start: sel.end});
                    if (isIndentLineCommand()) {
                        editGroup.push({text: prefix, start: { line: sel.start.line, ch: startCh }});
                    } else {
                        editGroup.push({text: prefix, start: sel.start});
                    }
                }

                // Correct the tracked selections. We can't just use the default selection fixup,
                // because it will push the end of the selection past the inserted content. Also,
                // it's possible that we have to deal with tracked selections that might be outside
                // the bounds of the edit.
                _.each(selectionsToTrack, function (trackedSel) {
                    function updatePosForEdit(pos) {
                        // First adjust for the suffix insertion. Don't adjust
                        // positions that are exactly at the suffix insertion point.
                        if (CodeMirror.cmpPos(pos, sel.end) > 0) {
                            if (completeLineSel) {
                                pos.line++;
                            } else if (pos.line === sel.end.line) {
                                pos.ch += suffix.length;
                            }
                        }
                        // Now adjust for the prefix insertion. In this case, we do
                        // want to adjust positions that are exactly at the insertion
                        // point.
                        if (CodeMirror.cmpPos(pos, sel.start) >= 0) {
                            if (completeLineSel) {
                                // Just move the line down.
                                pos.line++;
                            } else if (pos.line === sel.start.line && !(isIndentLineCommand() && pos.ch < startCh)) {
                                pos.ch += prefix.length;
                            }
                        }
                    }

                    updatePosForEdit(trackedSel.start);
                    updatePosForEdit(trackedSel.end);
                });

            // Uncomment - remove prefix and suffix.
            } else {
                // Find if the prefix and suffix are at the ch 0 and if they are the only thing in the line.
                // If both are found we assume that a complete line selection comment added new lines, so we remove them.
                var line          = doc.getLine(prefixPos.line).trim(),
                    prefixAtStart = prefixPos.ch === 0 && prefix.length === line.length,
                    prefixIndented = indentLineComment && prefix.length === line.length,
                    suffixAtStart = false,
                    suffixIndented = false;

                if (suffixPos) {
                    line = doc.getLine(suffixPos.line).trim();
                    suffixAtStart = suffixPos.ch === 0 && suffix.length === line.length;
                    suffixIndented = indentLineComment && suffix.length === line.length;
                }

                // Remove the suffix if there is one
                if (suffixPos) {
                    if (suffixIndented) {
                        editGroup.push({text: "", start: {line: suffixPos.line, ch: 0}, end: {line: suffixPos.line + 1, ch: 0}});
                    } else if (prefixAtStart && suffixAtStart) {
                        editGroup.push({text: "", start: suffixPos, end: {line: suffixPos.line + 1, ch: 0}});
                    } else {
                        editGroup.push({text: "", start: suffixPos, end: {line: suffixPos.line, ch: suffixPos.ch + suffix.length}});
                    }
                }

                // Remove the prefix
                if (prefixIndented) {
                    editGroup.push({text: "", start: {line: prefixPos.line, ch: 0}, end: {line: prefixPos.line + 1, ch: 0}});
                } else if (prefixAtStart && suffixAtStart) {
                    editGroup.push({text: "", start: prefixPos, end: {line: prefixPos.line + 1, ch: 0}});
                } else {
                    editGroup.push({text: "", start: prefixPos, end: {line: prefixPos.line, ch: prefixPos.ch + prefix.length}});
                }

                // Don't fix up the tracked selections here - let the edit fix them up.
                _.each(selectionsToTrack, function (trackedSel) {
                    trackedSel.isBeforeEdit = true;
                });
            }

            edit = {edit: editGroup, selection: selectionsToTrack};
        }

        return edit;
    }


    /**
     * Generates an edit that adds or removes block-comment tokens to the selection, preserving selection
     * and cursor position. Applies to the currently focused Editor. The selection must already be a
     * line selection in the form returned by `Editor.convertToLineSelections()`.
     *
     * The implementation uses blockCommentPrefixSuffix, with the exception of the case where
     * there is no selection on a uncommented and not empty line. In this case the whole lines gets
     * commented in a block-comment.
     *
     * @param {!Editor} editor
     * @param {!String} prefix
     * @param {!String} suffix
     * @param {!{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}>}}
     *      lineSel A line selection as returned from `Editor.convertToLineSelections()`. `selectionForEdit` is the selection to perform
     *      the line comment operation on, and `selectionsToTrack` are a set of selections associated with this line that need to be
     *      tracked through the edit.
     * @param {String} command The command callee. It cans be "line" or "block".
     * @return {{edit: {text: string, start:{line: number, ch: number}, end:?{line: number, ch: number}}|Array.<{text: string, start:{line: number, ch: number}, end:?{line: number, ch: number}}>,
     *                  selection: {start:{line:number, ch:number}, end:{line:number, ch:number}, primary:boolean, reversed: boolean, isBeforeEdit: boolean}>}|
     *                  Array.<{start:{line:number, ch:number}, end:{line:number, ch:number}, primary:boolean, reversed: boolean, isBeforeEdit: boolean}>}}
     *      An edit description suitable for including in the edits array passed to `Document.doMultipleEdits()`.
     */
    function _getLineCommentPrefixSuffixEdit(editor, prefix, suffix, lineSel, command) {
        var sel = lineSel.selectionForEdit;

        // For one-line selections, we shrink the selection to exclude the trailing newline.
        if (sel.end.line === sel.start.line + 1 && sel.end.ch === 0) {
            sel.end = {line: sel.start.line, ch: editor.document.getLine(sel.start.line).length};
        }

        // Now just run the standard block comment code, but make sure to track any associated selections
        // that were subsumed into this line selection.
        return _getBlockCommentPrefixSuffixEdit(editor, prefix, suffix, [], sel, lineSel.selectionsToTrack, command);
    }

    /**
     * @private
     * Generates an array of edits for toggling line comments on the given selections.
     *
     * @param {!Editor} editor The editor to edit within.
     * @param {Array.<{start:{line:number, ch:number}, end:{line:number, ch:number}, primary:boolean, reversed: boolean, isBeforeEdit: boolean}>}
     *      selections The selections we want to line-comment.
     * @param {String} command The command callee. It cans be "line" or "block".
     * @return {Array.<{edit: {text: string, start:{line: number, ch: number}, end:?{line: number, ch: number}}|Array.<{text: string, start:{line: number, ch: number}, end:?{line: number, ch: number}}>,
     *                  selection: {start:{line:number, ch:number}, end:{line:number, ch:number}, primary:boolean, reversed: boolean, isBeforeEdit: boolean}>}|
     *                  Array.<{start:{line:number, ch:number}, end:{line:number, ch:number}, primary:boolean, reversed: boolean, isBeforeEdit: boolean}>}>}
     *      An array of edit descriptions suitable for including in the edits array passed to `Document.doMultipleEdits()`.
     */
    function _getLineCommentEdits(editor, selections, command) {
        // We need to expand line selections in order to coalesce cursors on the same line, but we
        // don't want to merge adjacent line selections.
        var lineSelections = editor.convertToLineSelections(selections, { mergeAdjacent: false }),
            edits = [];
        _.each(lineSelections, function (lineSel) {
            var sel = lineSel.selectionForEdit,
                mode = editor.getModeForRange(sel.start, sel.end),
                edit;
            if (mode) {
                var language = editor.document.getLanguage().getLanguageForMode(mode.name || mode);

                if (language.hasLineCommentSyntax()) {
                    edit = _getLineCommentPrefixEdit(editor, language.getLineCommentPrefixes(), language.getBlockCommentPrefix(), language.getBlockCommentSuffix(), lineSel);
                } else if (language.hasBlockCommentSyntax()) {
                    edit = _getLineCommentPrefixSuffixEdit(editor, language.getBlockCommentPrefix(), language.getBlockCommentSuffix(), lineSel, command);
                }
            }
            if (!edit) {
                // Even if we didn't want to do an edit, we still need to track the selection.
                edit = {selection: lineSel.selectionsToTrack};
            }
            edits.push(edit);
        });
        return edits;
    }

    /**
     * Invokes a language-specific line-comment/uncomment handler
     * @param {?Editor} editor If unspecified, applies to the currently focused editor
     */
    function lineComment(editor) {
        editor = editor || EditorManager.getFocusedEditor();
        if (!editor) {
            return;
        }

        editor.setSelections(editor.document.doMultipleEdits(_getLineCommentEdits(editor, editor.getSelections(), "line")));
    }

    /**
     * Invokes a language-specific block-comment/uncomment handler
     * @param {?Editor} editor If unspecified, applies to the currently focused editor
     */
    function blockComment(editor) {
        editor = editor || EditorManager.getFocusedEditor();
        if (!editor) {
            return;
        }

        var edits = [],
            lineCommentSels = [];
        _.each(editor.getSelections(), function (sel) {
            var mode = editor.getModeForRange(sel.start, sel.end),
                edit = {edit: [], selection: [sel]}; // default edit in case we don't have a mode for this selection
            if (mode) {
                var language = editor.document.getLanguage().getLanguageForMode(mode.name || mode);

                if (language.hasBlockCommentSyntax()) {
                    // getLineCommentPrefixes always return an array, and will be empty if no line comment syntax is defined
                    edit = _getBlockCommentPrefixSuffixEdit(editor, language.getBlockCommentPrefix(), language.getBlockCommentSuffix(),
                                                            language.getLineCommentPrefixes(), sel);
                    if (!edit) {
                        // This is only null if the block comment code found that the selection is within a line-commented line.
                        // Add this to the list of line-comment selections we need to handle. Since edit is null, we'll skip
                        // pushing anything onto the edit list for this selection.
                        lineCommentSels.push(sel);
                    }
                }
            }
            if (edit) {
                edits.push(edit);
            }
        });

        // Handle any line-comment edits. It's okay if these are out-of-order with the other edits, since
        // they shouldn't overlap, and `doMultipleEdits()` will take care of sorting the edits so the
        // selections can be tracked appropriately.
        edits.push.apply(edits, _getLineCommentEdits(editor, lineCommentSels, "block"));

        editor.setSelections(editor.document.doMultipleEdits(edits));
    }

    /**
     * Duplicates the selected text, or current line if no selection. The cursor/selection is left
     * on the second copy.
     */
    function duplicateText(editor) {
        editor = editor || EditorManager.getFocusedEditor();
        if (!editor) {
            return;
        }

        var selections = editor.getSelections(),
            delimiter = "",
            edits = [],
            rangeSels = [],
            cursorSels = [],
            doc = editor.document;

        // When there are multiple selections, we want to handle all the cursors first (duplicating
        // their lines), then all the ranges (duplicating the ranges).
        _.each(selections, function (sel) {
            if (CodeMirror.cmpPos(sel.start, sel.end) === 0) {
                cursorSels.push(sel);
            } else {
                rangeSels.push(sel);
            }
        });

        var cursorLineSels = editor.convertToLineSelections(cursorSels);
        _.each(cursorLineSels, function (lineSel, index) {
            var sel = lineSel.selectionForEdit;
            if (sel.end.line === editor.lineCount()) {
                delimiter = "\n";
            }
            // Don't need to explicitly track selections since we are doing the edits in such a way that
            // the existing selections will get appropriately updated.
            edits.push({edit: {text: doc.getRange(sel.start, sel.end) + delimiter, start: sel.start }});
        });
        _.each(rangeSels, function (sel) {
            edits.push({edit: {text: doc.getRange(sel.start, sel.end), start: sel.start }});
        });

        doc.doMultipleEdits(edits);
    }

    /**
     * Deletes the current line if there is no selection or the lines for the selection
     * (removing the end of line too)
     */
    function deleteCurrentLines(editor) {
        editor = editor || EditorManager.getFocusedEditor();
        if (!editor) {
            return;
        }

        // Walk the selections, calculating the deletion edits we need to do as we go;
        // document.doMultipleEdits() will take care of adjusting the edit locations when
        // it actually performs the edits.
        var doc = editor.document,
            from,
            to,
            lineSelections = editor.convertToLineSelections(editor.getSelections()),
            edits = [];

        _.each(lineSelections, function (lineSel, index) {
            var sel = lineSel.selectionForEdit;

            from = sel.start;
            to = sel.end; // this is already at the beginning of the line after the last selected line
            if (to.line === editor.getLastVisibleLine() + 1) {
                // Instead of deleting the newline after the last line, delete the newline
                // before the beginning of the line--unless this is the entire visible content
                // of the editor, in which case just delete the line content.
                if (from.line > editor.getFirstVisibleLine()) {
                    from.line -= 1;
                    from.ch = doc.getLine(from.line).length;
                }
                to.line -= 1;
                to.ch = doc.getLine(to.line).length;
            }

            // We don't need to track the original selections, since they'll get collapsed as
            // part of the various deletions that occur.
            edits.push({edit: {text: "", start: from, end: to}});
        });
        doc.doMultipleEdits(edits);
    }

    /**
     * Moves the selected text, or current line if no selection. The cursor/selection
     * moves with the line/lines.
     * @param {Editor} editor - target editor
     * @param {Number} direction - direction of the move (-1,+1) => (Up,Down)
     */
    function moveLine(editor, direction) {
        editor = editor || EditorManager.getFocusedEditor();
        if (!editor) {
            return;
        }

        var doc             = editor.document,
            lineSelections  = editor.convertToLineSelections(editor.getSelections()),
            isInlineWidget  = !!EditorManager.getFocusedInlineWidget(),
            firstLine       = editor.getFirstVisibleLine(),
            lastLine        = editor.getLastVisibleLine(),
            totalLines      = editor.lineCount(),
            lineLength      = 0,
            edits           = [],
            newSels         = [],
            pos             = {};

        _.each(lineSelections, function (lineSel) {
            var sel = lineSel.selectionForEdit,
                editGroup = [];

            // Make the move
            switch (direction) {
            case DIRECTION_UP:
                if (sel.start.line !== firstLine) {
                    var prevText = doc.getRange({ line: sel.start.line - 1, ch: 0 }, sel.start);

                    if (sel.end.line === lastLine + 1) {
                        if (isInlineWidget) {
                            prevText   = prevText.substring(0, prevText.length - 1);
                            lineLength = doc.getLine(sel.end.line - 1).length;
                            editGroup.push({text: "\n", start: { line: sel.end.line - 1, ch: lineLength }});
                        } else {
                            prevText = "\n" + prevText.substring(0, prevText.length - 1);
                        }
                    }

                    editGroup.push({text: "", start: { line: sel.start.line - 1, ch: 0 }, end: sel.start});
                    editGroup.push({text: prevText, start: { line: sel.end.line - 1, ch: 0 }});

                    // Make sure CodeMirror hasn't expanded the selection to include
                    // the line we inserted below.
                    _.each(lineSel.selectionsToTrack, function (originalSel) {
                        originalSel.start.line--;
                        originalSel.end.line--;
                    });

                    edits.push({edit: editGroup, selection: lineSel.selectionsToTrack});
                }
                break;
            case DIRECTION_DOWN:
                if (sel.end.line <= lastLine) {
                    var nextText      = doc.getRange(sel.end, { line: sel.end.line + 1, ch: 0 }),
                        deletionStart = sel.end;

                    if (sel.end.line === lastLine) {
                        if (isInlineWidget) {
                            if (sel.end.line === totalLines - 1) {
                                nextText += "\n";
                            }
                            lineLength = doc.getLine(sel.end.line - 1).length;
                            editGroup.push({text: "\n", start: { line: sel.end.line, ch: doc.getLine(sel.end.line).length }});
                        } else {
                            nextText     += "\n";
                            deletionStart = { line: sel.end.line - 1, ch: doc.getLine(sel.end.line - 1).length };
                        }
                    }

                    editGroup.push({text: "", start: deletionStart, end: { line: sel.end.line + 1, ch: 0 }});
                    if (lineLength) {
                        editGroup.push({text: "", start: { line: sel.end.line - 1, ch: lineLength }, end: { line: sel.end.line, ch: 0 }});
                    }
                    editGroup.push({text: nextText, start: { line: sel.start.line, ch: 0 }});

                    // In this case, we don't need to track selections, because the edits are done in such a way that
                    // the existing selections will automatically be updated properly by CodeMirror as it does the edits.
                    edits.push({edit: editGroup});
                }
                break;
            }
        });

        // Make sure selections are correct and primary selection is scrolled into view
        if (edits.length) {
            newSels = doc.doMultipleEdits(edits);

            pos.ch = 0;

            if (direction === DIRECTION_UP) {
                editor.setSelections(newSels);
                pos.line = editor.getSelection().start.line;
            } else if (direction === DIRECTION_DOWN) {
                pos.line = editor.getSelection().end.line;
            } else {
                console.error("EditorCommandHandler.moveLine() called with invalid argument 'direction' = %d", direction);
                pos = null;
            }

            editor._codeMirror.scrollIntoView(pos);
        }
    }

    /**
     * Moves the selected text, or current line if no selection, one line up. The cursor/selection
     * moves with the line/lines.
     */
    function moveLineUp(editor) {
        moveLine(editor, DIRECTION_UP);
    }

    /**
     * Moves the selected text, or current line if no selection, one line down. The cursor/selection
     * moves with the line/lines.
     */
    function moveLineDown(editor) {
        moveLine(editor, DIRECTION_DOWN);
    }

    /**
     * Inserts a new and smart indented line above/below the selected text, or current line if no selection.
     * The cursor is moved in the new line.
     * @param {Editor} editor - target editor
     * @param {Number} direction - direction where to place the new line (-1,+1) => (Up,Down)
     */
    function openLine(editor, direction) {
        editor = editor || EditorManager.getFocusedEditor();
        if (!editor) {
            return;
        }

        var selections     = editor.getSelections(),
            isInlineWidget = !!EditorManager.getFocusedInlineWidget(),
            lastLine       = editor.getLastVisibleLine(),
            doc            = editor.document,
            edits          = [],
            newSelections,
            line;

        // First, insert all the newlines (skipping multiple selections on the same line),
        // then indent them all. (We can't easily do them all at once, because doMultipleEdits()
        // won't do the indentation for us, but we want its help tracking any selection changes
        // as the result of the edits.)

        // Note that we don't just use `editor.getLineSelections()` here because we don't actually want
        // to coalesce adjacent selections - we just want to ignore dupes.

        doc.batchOperation(function () {
            _.each(selections, function (sel, index) {
                if (index === 0 ||
                        (direction === DIRECTION_UP && sel.start.line > selections[index - 1].start.line) ||
                        (direction === DIRECTION_DOWN && sel.end.line > selections[index - 1].end.line)) {
                    // Insert the new line
                    switch (direction) {
                    case DIRECTION_UP:
                        line = sel.start.line;
                        break;
                    case DIRECTION_DOWN:
                        line = sel.end.line;
                        if (!(CodeMirror.cmpPos(sel.start, sel.end) !== 0 && sel.end.ch === 0)) {
                            // If not linewise selection
                            line++;
                        }
                        break;
                    }

                    var insertPos;
                    if (line > lastLine && isInlineWidget) {
                        insertPos = {line: line - 1, ch: doc.getLine(line - 1).length};
                    } else {
                        insertPos = {line: line, ch: 0};
                    }
                    // We want the selection after this edit to be right before the \n we just inserted.
                    edits.push({edit: {text: "\n", start: insertPos}, selection: {start: insertPos, end: insertPos, primary: sel.primary}});
                } else {
                    // We just want to discard this selection, since we've already operated on the
                    // same line and it would just collapse to the same location. But if this was
                    // primary, make sure the last selection we did operate on ends up as primary.
                    if (sel.primary) {
                        edits[edits.length - 1].selections[0].primary = true;
                    }
                }
            });
            newSelections = doc.doMultipleEdits(edits, "+input");

            // Now indent each added line (which doesn't mess up any line numbers, and
            // we're going to set the character offset to the last position on each line anyway).
            _.each(newSelections, function (sel) {
                // This is a bit of a hack. The document is the one that batches operations, but we want
                // to use CodeMirror's "smart indent" operation. So we need to use the document's own backing editor's
                // CodeMirror to do the indentation. A better way to fix this would be to expose this
                // operation on Document, but I'm not sure we want to sign up for that as a public API.
                doc._masterEditor._codeMirror.indentLine(sel.start.line, "smart", true);
                sel.start.ch = null; // last character on line
                sel.end = sel.start;
            });
        });
        editor.setSelections(newSelections);
    }

    /**
     * Inserts a new and smart indented line above the selected text, or current line if no selection.
     * The cursor is moved in the new line.
     * @param {Editor} editor - target editor
     */
    function openLineAbove(editor) {
        openLine(editor, DIRECTION_UP);
    }

    /**
     * Inserts a new and smart indented line below the selected text, or current line if no selection.
     * The cursor is moved in the new line.
     * @param {Editor} editor - target editor
     */
    function openLineBelow(editor) {
        openLine(editor, DIRECTION_DOWN);
    }

    /**
     * Indent a line of text if no selection. Otherwise, indent all lines in selection.
     */
    function indentText() {
        var editor = EditorManager.getFocusedEditor();
        if (!editor) {
            return;
        }

        editor._codeMirror.execCommand("indentMore");
    }

    /**
     * Unindent a line of text if no selection. Otherwise, unindent all lines in selection.
     */
    function unindentText() {
        var editor = EditorManager.getFocusedEditor();
        if (!editor) {
            return;
        }

        editor._codeMirror.execCommand("indentLess");
    }

    function selectLine(editor) {
        editor = editor || EditorManager.getFocusedEditor();
        if (editor) {
            // We can just use `convertToLineSelections`, but throw away the original tracked selections and just use the
            // coalesced selections.
            editor.setSelections(_.pluck(editor.convertToLineSelections(editor.getSelections(), { expandEndAtStartOfLine: true }), "selectionForEdit"));
        }
    }

    /**
     * @private
     * Takes the current selection and splits each range into separate selections, one per line.
     * @param {!Editor} editor The editor to operate on.
     */
    function splitSelIntoLines(editor) {
        editor = editor || EditorManager.getFocusedEditor();
        if (editor) {
            editor._codeMirror.execCommand("splitSelectionByLine");
        }
    }

    /**
     * @private
     * Adds a cursor on the next/previous line after/before each selected range to the selection.
     * @param {!Editor} editor The editor to operate on.
     * @param {number} dir The direction to add - 1 is down, -1 is up.
     */
    function addCursorToSelection(editor, dir) {
        editor = editor || EditorManager.getFocusedEditor();
        if (editor) {
            var origSels = editor.getSelections(),
                newSels = [];
            _.each(origSels, function (sel) {
                var pos, colOffset;
                if ((dir === -1 && sel.start.line > editor.getFirstVisibleLine()) || (dir === 1 && sel.end.line < editor.getLastVisibleLine())) {
                    // Add a new cursor on the next line up/down. It's okay if it overlaps another selection, because CM
                    // will take care of throwing it away in that case. It will also take care of clipping the char position
                    // to the end of the new line if the line is shorter.
                    pos = _.clone(dir === -1 ? sel.start : sel.end);

                    // get sel column of current selection
                    colOffset = editor.getColOffset(pos);

                    pos.line += dir;

                    // translate column to ch in line of new selection
                    pos.ch = editor.getCharIndexForColumn(pos.line, colOffset);


                    // If this is the primary selection, we want the new cursor we're adding to become the
                    // primary selection.
                    newSels.push({start: pos, end: pos, primary: sel.primary});
                    sel.primary = false;
                }
            });
            // CM will take care of sorting the selections.
            editor.setSelections(origSels.concat(newSels));
        }
    }

    /**
     * @private
     * Adds a cursor on the previous line before each selected range to the selection.
     * @param {!Editor} editor The editor to operate on.
     */
    function addCursorToPrevLine(editor) {
        addCursorToSelection(editor, -1);
    }

    /**
     * @private
     * Adds a cursor on the next line after each selected range to the selection.
     * @param {!Editor} editor The editor to operate on.
     */
    function addCursorToNextLine(editor) {
        addCursorToSelection(editor, 1);
    }

    function handleUndoRedo(operation) {
        var editor = EditorManager.getFocusedEditor();
        var result = new $.Deferred();

        if (editor) {
            editor[operation]();
            result.resolve();
        } else {
            result.reject();
        }

        return result.promise();
    }

    function handleUndo() {
        return handleUndoRedo("undo");
    }

    function handleRedo() {
        return handleUndoRedo("redo");
    }

    function _handleSelectAll() {
        var result = new $.Deferred(),
            editor = EditorManager.getFocusedEditor();

        if (editor) {
            editor.selectAllNoScroll();
            result.resolve();
        } else {
            result.reject();    // command not handled
        }

        return result.promise();
    }

    function _execCommand(cmd) {
        window.document.execCommand(cmd);
    }
    function _execCommandCut() {
        _execCommand("cut");
    }
    function _execCommandCopy() {
        _execCommand("copy");
    }
    function _execCommandPaste() {
        _execCommand("paste");
    }

    // Register commands
    CommandManager.register(Strings.CMD_INDENT,                 Commands.EDIT_INDENT,                 indentText);
    CommandManager.register(Strings.CMD_UNINDENT,               Commands.EDIT_UNINDENT,               unindentText);
    CommandManager.register(Strings.CMD_COMMENT,                Commands.EDIT_LINE_COMMENT,           lineComment);
    CommandManager.register(Strings.CMD_BLOCK_COMMENT,          Commands.EDIT_BLOCK_COMMENT,          blockComment);
    CommandManager.register(Strings.CMD_DUPLICATE,              Commands.EDIT_DUPLICATE,              duplicateText);
    CommandManager.register(Strings.CMD_DELETE_LINES,           Commands.EDIT_DELETE_LINES,           deleteCurrentLines);
    CommandManager.register(Strings.CMD_LINE_UP,                Commands.EDIT_LINE_UP,                moveLineUp);
    CommandManager.register(Strings.CMD_LINE_DOWN,              Commands.EDIT_LINE_DOWN,              moveLineDown);
    CommandManager.register(Strings.CMD_OPEN_LINE_ABOVE,        Commands.EDIT_OPEN_LINE_ABOVE,        openLineAbove);
    CommandManager.register(Strings.CMD_OPEN_LINE_BELOW,        Commands.EDIT_OPEN_LINE_BELOW,        openLineBelow);
    CommandManager.register(Strings.CMD_SELECT_LINE,            Commands.EDIT_SELECT_LINE,            selectLine);
    CommandManager.register(Strings.CMD_SPLIT_SEL_INTO_LINES,   Commands.EDIT_SPLIT_SEL_INTO_LINES,   splitSelIntoLines);
    CommandManager.register(Strings.CMD_ADD_CUR_TO_NEXT_LINE,   Commands.EDIT_ADD_CUR_TO_NEXT_LINE,   addCursorToNextLine);
    CommandManager.register(Strings.CMD_ADD_CUR_TO_PREV_LINE,   Commands.EDIT_ADD_CUR_TO_PREV_LINE,   addCursorToPrevLine);

    CommandManager.register(Strings.CMD_UNDO,                   Commands.EDIT_UNDO,                   handleUndo);
    CommandManager.register(Strings.CMD_REDO,                   Commands.EDIT_REDO,                   handleRedo);
    CommandManager.register(Strings.CMD_CUT,                    Commands.EDIT_CUT,                    _execCommandCut);
    CommandManager.register(Strings.CMD_COPY,                   Commands.EDIT_COPY,                   _execCommandCopy);
    CommandManager.register(Strings.CMD_PASTE,                  Commands.EDIT_PASTE,                  _execCommandPaste);
    CommandManager.register(Strings.CMD_SELECT_ALL,             Commands.EDIT_SELECT_ALL,             _handleSelectAll);
});