/
githubmirror
/
brackets
Обзор
Документация
Войти
/
githubmirror
/
brackets
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
src/editor/EditorCommandHandlers.js
1 216 строк
58 KB
ficristo
Return 0 from _firstNotWs
20 апр 2017, 21:02
20 апр 2017, 21:02
8a71b9f
Код
Авторство
О чём код?
/* * 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); });