-
Notifications
You must be signed in to change notification settings - Fork 7.5k
Adding support for multiple lineComment prefixes #3121
Changes from 3 commits
36e60ac
a07aff9
a3352f2
b6a7769
25287ad
379af7b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -46,23 +46,53 @@ define(function (require, exports, module) { | |
| var DIRECTION_DOWN = +1; | ||
|
|
||
|
|
||
| /** | ||
| * @private | ||
| * Creates regular expressions for multiple line comments prefixes | ||
| * @param {Array.<string>} prefixes - Description | ||
| * @return {Array.<RegExp>} | ||
| */ | ||
| function _createLineExpressions(prefixes) { | ||
| var lineExps = []; | ||
| prefixes.forEach(function (prefix) { | ||
| lineExps.push(new RegExp("^\\s*" + StringUtils.regexEscape(prefix))); | ||
| }); | ||
| return lineExps; | ||
| } | ||
|
|
||
| /** | ||
| * @private | ||
| * Returns true if any regular expression matches the given string | ||
| * @param {string} string | ||
| * @param {Array.<RegExp>} expressions | ||
| * @return {boolean} | ||
| */ | ||
| function _matchExpressions(string, expressions) { | ||
| var matchAny = false; | ||
| expressions.forEach(function (exp) { | ||
| matchAny = matchAny || string.match(exp); | ||
| }); | ||
| return matchAny; | ||
| } | ||
|
|
||
| /** | ||
| * @private | ||
| * Searchs for an uncommented line between startLine and endLine | ||
| * @param {!Editor} editor | ||
| * @param {!number} startLine - valid line inside the document | ||
| * @param {!number} endLine - valid line inside the document | ||
| * @param {!Array.<string>} prefixes - Array of line comment prefixes | ||
| * @return {boolean} true if there is at least one uncommented line | ||
| */ | ||
| function _containsUncommented(editor, startLine, endLine, prefix) { | ||
| var lineExp = new RegExp("^\\s*" + StringUtils.regexEscape(prefix)); | ||
| function _containsUncommented(editor, startLine, endLine, prefixes) { | ||
| var lineExp = _createLineExpressions(prefixes); | ||
| var containsUncommented = false; | ||
| var i; | ||
| var line; | ||
| 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 "//" | ||
| if (!line.match(lineExp) && line.match(/\S/)) { | ||
| if (!_matchExpressions(line, lineExp) && line.match(/\S/)) { | ||
| containsUncommented = true; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The second condition will probably evaluate faster than the first condition, so the order should be reversed: |
||
| break; | ||
| } | ||
|
|
@@ -77,8 +107,11 @@ define(function (require, exports, module) { | |
| * 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. ["//"] | ||
| */ | ||
| function lineCommentPrefix(editor, prefix) { | ||
| function lineCommentPrefix(editor, prefixes) { | ||
| var doc = editor.document; | ||
| var sel = editor.getSelection(); | ||
| var startLine = sel.start.line; | ||
|
|
@@ -95,32 +128,38 @@ 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, prefix); | ||
| var i; | ||
| var containsUncommented = _containsUncommented(editor, startLine, endLine, prefixes); | ||
| var i, j; | ||
| var line; | ||
| var trimmedLine; | ||
| var commentI; | ||
| var updateSelection = false; | ||
|
|
||
| // Make the edit | ||
| doc.batchOperation(function () { | ||
|
|
||
| if (containsUncommented) { | ||
| // Comment out - prepend "//" to each line | ||
| // Comment out - prepend the first prefix to each line | ||
| for (i = startLine; i <= endLine; i++) { | ||
| doc.replaceRange(prefix, {line: i, ch: 0}); | ||
| doc.replaceRange(prefixes[0], {line: i, ch: 0}); | ||
| } | ||
|
|
||
| // Make sure selection includes "//" that was added at start of range | ||
| // Make sure selection includes the prefix that was added at start of range | ||
| if (sel.start.ch === 0 && hasSelection) { | ||
| updateSelection = true; | ||
| } | ||
|
|
||
| } else { | ||
| // Uncomment - remove first "//" on each line (if any) | ||
| // Uncomment - remove first every prefix on each line (if any) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Improved wording: remove prefix on each line (if any) |
||
| for (i = startLine; i <= endLine; i++) { | ||
| line = doc.getLine(i); | ||
| var commentI = line.indexOf(prefix); | ||
| if (commentI !== -1) { | ||
| doc.replaceRange("", {line: i, ch: commentI}, {line: i, ch: commentI + prefix.length}); | ||
| trimmedLine = line.trim(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes. I just needed to know if the prefix is at the start of the string, since if you comment with both prefixes, it could unprefix the wrong one. For example in |
||
| for (j = 0; j < prefixes.length; j++) { | ||
| commentI = line.indexOf(prefixes[j]); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's no need to search for |
||
| if (trimmedLine.indexOf(prefixes[j]) === 0) { | ||
| doc.replaceRange("", {line: i, ch: commentI}, {line: i, ch: commentI + prefixes[j].length}); | ||
| break; | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
@@ -206,9 +245,9 @@ define(function (require, exports, module) { | |
| * @param {!Editor} editor | ||
| * @param {!string} prefix, e.g. "<!--" | ||
| * @param {!string} suffix, e.g. "-->" | ||
| * @param {?string} linePrefix, e.g. "//" | ||
| * @param {?Array.<string>} linePrefixes, e.g. ["//"] | ||
| */ | ||
| function blockCommentPrefixSuffix(editor, prefix, suffix, linePrefix) { | ||
| function blockCommentPrefixSuffix(editor, prefix, suffix, linePrefixes) { | ||
|
|
||
| var doc = editor.document, | ||
| sel = editor.getSelection(), | ||
|
|
@@ -217,7 +256,7 @@ define(function (require, exports, module) { | |
| endCtx = TokenUtils.getInitialContext(editor._codeMirror, {line: sel.end.line, ch: sel.end.ch}), | ||
| prefixExp = new RegExp("^" + StringUtils.regexEscape(prefix), "g"), | ||
| suffixExp = new RegExp(StringUtils.regexEscape(suffix) + "$", "g"), | ||
| lineExp = linePrefix ? new RegExp("^" + StringUtils.regexEscape(linePrefix)) : null, | ||
| lineExp = linePrefixes ? _createLineExpressions(linePrefixes) : null, | ||
| prefixPos = null, | ||
| suffixPos = null, | ||
| canComment = false, | ||
|
|
@@ -233,13 +272,13 @@ define(function (require, exports, module) { | |
| } | ||
|
|
||
| // Check if we should just do a line uncomment (if all lines in the selection are commented). | ||
| if (lineExp && (ctx.token.string.match(lineExp) || endCtx.token.string.match(lineExp))) { | ||
| if (lineExp && (_matchExpressions(ctx.token.string, lineExp) || _matchExpressions(endCtx.token.string, lineExp))) { | ||
| var startCtxIndex = editor.indexFromPos({line: ctx.pos.line, ch: ctx.token.start}); | ||
| var endCtxIndex = editor.indexFromPos({line: endCtx.pos.line, ch: endCtx.token.start + endCtx.token.string.length}); | ||
|
|
||
| // Find if we aren't actually inside a block-comment | ||
| result = true; | ||
| while (result && ctx.token.string.match(lineExp)) { | ||
| while (result && _matchExpressions(ctx.token.string, lineExp)) { | ||
| result = TokenUtils.moveSkippingWhitespace(TokenUtils.movePrevToken, ctx); | ||
| } | ||
|
|
||
|
|
@@ -255,7 +294,7 @@ define(function (require, exports, module) { | |
| } | ||
|
|
||
| // Find if all the lines are line-commented. | ||
| if (!_containsUncommented(editor, sel.start.line, endLine, linePrefix)) { | ||
| if (!_containsUncommented(editor, sel.start.line, endLine, linePrefixes)) { | ||
| lineUncomment = true; | ||
|
|
||
| // Block-comment in all the other cases | ||
|
|
@@ -327,7 +366,7 @@ define(function (require, exports, module) { | |
| return; | ||
|
|
||
| } else if (lineUncomment) { | ||
| lineCommentPrefix(editor, linePrefix); | ||
| lineCommentPrefix(editor, linePrefixes); | ||
|
|
||
| } else { | ||
| doc.batchOperation(function () { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -266,6 +266,7 @@ define(function (require, exports, module) { | |
| this._fileExtensions = []; | ||
| this._fileNames = []; | ||
| this._modeToLanguageMap = {}; | ||
| this._lineCommentSyntax = []; | ||
| } | ||
|
|
||
|
|
||
|
|
@@ -284,12 +285,12 @@ define(function (require, exports, module) { | |
| /** @type {Array.<string>} File names for extensionless files that use this language */ | ||
| Language.prototype._fileNames = null; | ||
|
|
||
| /** @type {{ prefix: string }} Line comment syntax */ | ||
| Language.prototype._lineCommentSyntax = null; | ||
|
|
||
| /** @type {Object.<string,Language>} Which language to use for what CodeMirror mode */ | ||
| Language.prototype._modeToLanguageMap = null; | ||
|
|
||
| /** @type {Array.<string>} Line comment syntax */ | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Move this code back up where it originally was above |
||
| Language.prototype._lineCommentSyntax = null; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is an array, so why isn't this (and many other similar data members) initialized as an empty array?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually this doesn't really initialize anything, since prototype functions and arguments are shared by every instance of the object. It doesn't even make sense to have this here, besides documentation since they need to be different for every instance. The real initialization is done inside the constructor or the setters, where I was actually curious about the purpose of this properties since aren't really used. When accessing the members of an instance it first searches for the members set with
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good question. @DennisKehrig @peterflynn @jasonsanjose What's the purpose of the
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess this doesn't need to be solved here. My main concern was dereferencing a null object reference, but this code always uses
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @redmunds re: What's the purpose of the Language.prototype data members? I think they are mainly there so we have a place to add documentation to. They might also have a positive impact on performance as V8 has its own concept of classes and assigns objects with the same properties the same class. So if you take an empty object, and add properties to it one by one, its class changes every time. If all properties are known from the start, one class object is used for all instances, resulting in better optimization.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fwiw, the main motivations in my mind are (a) a place to hang documentation off of (without cluttering up the constructor body code), and (b) having all 'class members' listed out in one centralized place. The V8 benefit is neat too though -- hadn't occurred to me, but that meshes with what I've read about V8 too. |
||
|
|
||
| /** @type {{ prefix: string, suffix: string }} Block comment syntax */ | ||
| Language.prototype._blockCommentSyntax = null; | ||
|
|
||
|
|
@@ -449,26 +450,35 @@ define(function (require, exports, module) { | |
| * @return {boolean} Whether line comments are supported | ||
| */ | ||
| Language.prototype.hasLineCommentSyntax = function () { | ||
| return Boolean(this._lineCommentSyntax); | ||
| return Boolean(this._lineCommentSyntax.length); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The intent of the code would be more clear to return a boolean expression than to convert an integer to Boolean type: |
||
| }; | ||
|
|
||
| /** | ||
| * Returns the prefix to use for line comments. | ||
| * @return {string} The prefix | ||
| * @return {Array.<string>} The prefixes | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If this "Returns the prefix to use for line comments", then the return type is still {string}, right? |
||
| */ | ||
| Language.prototype.getLineCommentPrefix = function () { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should be changed to should be changed to |
||
| return this._lineCommentSyntax && this._lineCommentSyntax.prefix; | ||
| return this._lineCommentSyntax.length && this._lineCommentSyntax; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should return the item at index 0 or an empty string:
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The comment should now be, "Returns the prefixes used to line comments". Both line commenting and block commenting now handle multiple line comment prefixes, since they need to, so it easier if they always receive an array, even if it is of one element as in most cases. I don't think that there could be a use to have the first comment prefix, except when there is one, but to cover all cases an array is better. I also don't think that this is used by extensions yet and is only used in brackets. But if needed I could make 2 factions for backwards compatibility.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK, but if it's returning the array, why does it need to check the length? Can it just be:
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, it should work after changing BlockComment to identify the empty array as null.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't understand what you mean by this.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Block comment always receives as a third parameter getLineCommentPrefix() and checks if is defined or not when creating the line prefix, so it works with languages that don't have line comment prefixes. This was changed with the addition of the LanguageManager, since before it received a bool. But it can be changed to check for the length of the array too, or make it always receive an array and check for the length, to make it work for languages that don't have line prefixes. |
||
| }; | ||
|
|
||
| /** | ||
| * Sets the prefix to use for line comments in this language. | ||
| * @param {!string} prefix Prefix string to use for block comments (i.e. "//") | ||
| * @param {!string|Array.<string>} prefix Prefix string or and array of prefix strings | ||
| * to use for line comments (i.e. "//" or ["//", "#"]) | ||
| */ | ||
| Language.prototype.setLineCommentSyntax = function (prefix) { | ||
| _validateNonEmptyString(prefix, "prefix"); | ||
| var prefixes = !Array.isArray(prefix) ? [prefix] : prefix; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It adds an extra level of abstraction to reverse the logic since it has to work either way. This is simpler: |
||
| var i; | ||
|
|
||
| this._lineCommentSyntax = { prefix: prefix }; | ||
| this._wasModified(); | ||
| if (prefixes.length) { | ||
| this._lineCommentSyntax = []; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Reseting array to empty should be done outside the
UPDATED: I see that the code throws an exception if string passed in or any array item is empty, so maybe instead you should throw an exception if an empty array is passed in.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| for (i = 0; i < prefixes.length; i++) { | ||
| _validateNonEmptyString(String(prefixes[i]), "prefix"); | ||
|
|
||
| this._lineCommentSyntax.push(prefixes[i]); | ||
| } | ||
| this._wasModified(); | ||
| } | ||
| }; | ||
|
|
||
| /** | ||
|
|
@@ -564,7 +574,7 @@ define(function (require, exports, module) { | |
| * @param {!string} definition.name Human-readable name of the language, as it's commonly referred to (i.e. "C++") | ||
| * @param {Array.<string>} definition.fileExtensions List of file extensions used by this language (i.e. ["php", "php3"]) | ||
| * @param {Array.<string>} definition.blockComment Array with two entries defining the block comment prefix and suffix (i.e. ["<!--", "-->"]) | ||
| * @param {string} definition.lineComment Line comment prefix (i.e. "//") | ||
| * @param {string|Array.<string>} definition.lineComment Line comment prefixes (i.e. "//" or ["//", "#"]) | ||
| * @param {string|Array.<string>} definition.mode CodeMirror mode (i.e. "htmlmixed"), optionally with a MIME mode defined by that mode ["clike", "text/x-c++src"] | ||
| * Unless the mode is located in thirdparty/CodeMirror2/mode/<name>/<name>.js, you need to first load it yourself. | ||
| * | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This loop searches for every expression no matter what. It should stop searching after a match is found.