diff --git a/src/editor/EditorCommandHandlers.js b/src/editor/EditorCommandHandlers.js index 5cb11d464c9..2b4caed3529 100644 --- a/src/editor/EditorCommandHandlers.js +++ b/src/editor/EditorCommandHandlers.js @@ -47,16 +47,55 @@ define(function (require, exports, module) { 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, + subExps = [], + prevChars = ""; + + for (i = lineSyntax.length; i < blockSyntax.length; i++) { + character = blockSyntax.charAt(i); + subExps.push(prevChars + "[^" + StringUtils.regexEscape(character) + "]"); + if (prevChars) { + subExps.push(prevChars + "$"); + } + prevChars += character; + } + return new RegExp("^\\s*" + StringUtils.regexEscape(lineSyntax) + "($|" + subExps.join("|") + ")"); + } + /** * @private * Creates regular expressions for multiple line comment prefixes - * @param {!Array.} prefixes - the line comment prefixes + * @param {!Array.} prefixes the line comment prefixes + * @param {string=} blockPrefix the block comment prefix + * @param {string=} blockSuffix the block comment suffix * @return {Array.} */ - function _createLineExpressions(prefixes) { - var lineExp = []; + function _createLineExpressions(prefixes, blockPrefix, blockSuffix) { + var lineExp = [], escapedPrefix, nothingPushed; + prefixes.forEach(function (prefix) { - lineExp.push(new RegExp("^\\s*" + StringUtils.regexEscape(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; } @@ -64,8 +103,8 @@ define(function (require, exports, module) { /** * @private * Returns true if any regular expression matches the given string - * @param {!string} string - where to look - * @param {!Array.} expressions - what to look + * @param {!string} string where to look + * @param {!Array.} expressions what to look * @return {boolean} */ function _matchExpressions(string, expressions) { @@ -77,11 +116,11 @@ define(function (require, exports, module) { /** * @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 throught all and returns the longest line + * 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.} expressions - the line comment regular expressions - * @param {!Array.} prefixes - the line comment prefixes + * @param {!string} string where to look + * @param {!Array.} expressions the line comment regular expressions + * @param {!Array.} prefixes the line comment prefixes * @return {string} */ function _getLinePrefix(string, expressions, prefixes) { @@ -96,27 +135,27 @@ define(function (require, exports, module) { /** * @private - * Searchs for an uncommented line between startLine and endLine + * 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.} lineExp - an array of line comment prefixes regular expressions + * @param {!number} startLine valid line inside the document + * @param {!number} endLine valid line inside the document + * @param {!Array.} lineExp an array of line comment prefixes regular expressions * @return {boolean} true if there is at least one uncommented line */ - function _containsUncommented(editor, startLine, endLine, lineExp) { - var containsUncommented = false; - var i; - var 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)) { - containsUncommented = true; + containsNotLineComment = true; break; } } - return containsUncommented; + return containsNotLineComment; } /** @@ -131,6 +170,8 @@ define(function (require, exports, module) { * * @param {!Editor} editor * @param {!Array.} prefixes, e.g. ["//"] + * @param {string=} blockPrefix, 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}>}} @@ -138,14 +179,15 @@ define(function (require, exports, module) { * 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}>}} + * 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, lineSel) { + function _getLineCommentPrefixEdit(editor, prefixes, blockPrefix, blockSuffix, lineSel) { var doc = editor.document, sel = lineSel.selectionForEdit, trackedSels = lineSel.selectionsToTrack, - lineExp = _createLineExpressions(prefixes), + lineExp = _createLineExpressions(prefixes, blockPrefix, blockSuffix), startLine = sel.start.line, endLine = sel.end.line, editGroup = []; @@ -158,14 +200,11 @@ define(function (require, exports, module) { // 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 containsUncommented = _containsUncommented(editor, startLine, endLine, lineExp); - var i; - var line; - var prefix; - var commentI; - var updateSelection = false; - - if (containsUncommented) { + var i, line, prefix, commentI, + containsNotLineComment = _containsNotLineComment(editor, startLine, endLine, lineExp), + updateSelection = false; + + if (containsNotLineComment) { // Comment out - prepend the first prefix to each line for (i = startLine; i <= endLine; i++) { editGroup.push({text: prefixes[0], start: {line: i, ch: 0}}); @@ -200,72 +239,59 @@ define(function (require, exports, module) { /** * @private - * Moves the token context to the token that starts the block-comment. Ctx starts in a block-comment. - * Returns the position of the prefix or null if gets to the start of the document and didn't found it. - * @param {!{editor:{CodeMirror}, pos:{ch:{string}, line:{number}}, token:{object}}} ctx - token context - * @param {!RegExp} prefixExp - a valid regular expression - * @return {?{line: number, ch: number}} + * 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.} lineExp an array of line comment prefixes regular expressions + * @return {boolean} */ - function _findCommentStart(ctx, prefixExp) { - var result = true; + function _isPrevTokenABlockComment(ctx, prefix, suffix, prefixExp, suffixExp, lineExp) { + // Start searching from the previous token + var result = TokenUtils.moveSkippingWhitespace(TokenUtils.movePrevToken, ctx); - while (result && !ctx.token.string.match(prefixExp)) { + // Look backwards until we find a none line comment token + while (result && _matchExpressions(ctx.token.string, lineExp)) { result = TokenUtils.moveSkippingWhitespace(TokenUtils.movePrevToken, ctx); } - return result ? {line: ctx.pos.line, ch: ctx.token.start} : null; - } - - /** - * @private - * Moves the token context to the token that ends the block-comment. Ctx starts in a block-comment. - * Returns the position of the sufix or null if gets to the end of the document and didn't found it. - * @param {!{editor:{CodeMirror}, pos:{ch:{string}, line:{number}}, token:{object}}} ctx - token context - * @param {!RegExp} suffixExp - a valid regular expression - * @param {!number} suffixLen - length of the suffix - * @return {?{line: number, ch: number}} - */ - function _findCommentEnd(ctx, suffixExp, suffixLen) { - var result = true; - - while (result && !ctx.token.string.match(suffixExp)) { - result = TokenUtils.moveSkippingWhitespace(TokenUtils.moveNextToken, ctx); - } - return result ? {line: ctx.pos.line, ch: ctx.token.end - suffixLen} : null; - } - - /** - * @private - * Moves the token context to the next block-comment if there is one before end. - * @param {!{editor:{CodeMirror}, pos:{ch:{string}, line:{number}}, token:{object}}} ctx - token context - * @param {!{line: number, ch: number}} end - where to stop searching - * @param {!RegExp} prefixExp - a valid regular expression - * @return {boolean} - true if it found a block-comment - */ - function _findNextBlockComment(ctx, end, prefixExp) { - var index = ctx.editor.indexFromPos(end), - inside = ctx.editor.indexFromPos(ctx.pos) <= index, - result = true; - while (result && inside && !ctx.token.string.match(prefixExp)) { - result = TokenUtils.moveSkippingWhitespace(TokenUtils.moveNextToken, ctx); - inside = ctx.editor.indexFromPos(ctx.pos) <= index; + // 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 result && inside && !!ctx.token.string.match(prefixExp); + return false; } + /** * 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 we uncomment; otherwise we comment out. + * 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. * - * As a special case, if slashComment is true and the start or end of the selection is inside a - * line-comment it needs to do a line uncomment if is not actually inside a bigger block comment and all - * the lines in the selection are line-commented. In this case, we return null to indicate to the caller - * that it needs to handle this selection as a line comment. + * 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. "