diff --git a/src/LiveDevelopment/LiveDevelopment.js b/src/LiveDevelopment/LiveDevelopment.js index bd00ca45c28..20d5ec0c381 100644 --- a/src/LiveDevelopment/LiveDevelopment.js +++ b/src/LiveDevelopment/LiveDevelopment.js @@ -756,6 +756,34 @@ define(function LiveDevelopment(require, exports, module) { return result.promise(); } + /** + * If the current editor is for a CSS preprocessor file, then add it to the style sheet + * so that we can track cursor positions in the editor to show live preview highlighting. + * For normal CSS we only do highlighting from files we know for sure are referenced by the + * current live preview document, but for preprocessors we just assume that any preprocessor + * file you edit is probably related to the live preview. + * + * @param {Event} event (unused) + * @param {Editor} current Current editor + * @param {Editor} previous Previous editor + * + */ + function onActiveEditorChange(event, current, previous) { + if (previous && previous.document && + FileUtils.isCSSPreprocessorFile(previous.document.file.fullPath)) { + var prevDocUrl = _server && _server.pathToUrl(previous.document.file.fullPath); + + if (_relatedDocuments && _relatedDocuments[prevDocUrl]) { + _closeRelatedDocument(_relatedDocuments[prevDocUrl]); + } + } + if (current && current.document && + FileUtils.isCSSPreprocessorFile(current.document.file.fullPath)) { + var docUrl = _server && _server.pathToUrl(current.document.file.fullPath); + _styleSheetAdded(null, docUrl); + } + } + /** * @private * While still connected to the Inspector, do cleanup for agents, @@ -768,6 +796,8 @@ define(function LiveDevelopment(require, exports, module) { deferred = new $.Deferred(), connected = Inspector.connected(); + $(EditorManager).off("activeEditorChange", onActiveEditorChange); + $(Inspector.Page).off(".livedev"); $(Inspector).off(".livedev"); @@ -1209,6 +1239,15 @@ define(function LiveDevelopment(require, exports, module) { // open browser to the interstitial page to prepare for loading agents _openInterstitialPage(); + + // Setup activeEditorChange event listener so that we can track cursor positions in + // CSS preprocessor files and perform live preview highlighting on all elements with + // the current selector in the preprocessor file. + $(EditorManager).on("activeEditorChange", onActiveEditorChange); + + // Explicitly trigger onActiveEditorChange so that live preview highlighting + // can be set up for the preprocessor files. + onActiveEditorChange(null, EditorManager.getActiveEditor(), null); } function _prepareServer(doc) { @@ -1253,34 +1292,6 @@ define(function LiveDevelopment(require, exports, module) { return deferred.promise(); } - /** - * If the current editor is for a preprocessor file, then add it to the style sheet - * so that we can track cursor positions in the editor to show live preview highlighting. - * For normal CSS we only do highlighting from files we know for sure are referenced by the - * current live preview document, but for preprocessors we just assume that any preprocessor - * file you edit is probably related to the live preview. - * - * @param {Event} event (unused) - * @param {Editor} current Current editor - * @param {Editor} previous Previous editor - * - */ - function onActiveEditorChange(event, current, previous) { - if (previous && previous.document && - FileUtils.isCSSPreprocessorFile(previous.document.file.fullPath)) { - var prevDocUrl = _server && _server.pathToUrl(previous.document.file.fullPath); - - if (_relatedDocuments && _relatedDocuments[prevDocUrl]) { - _closeRelatedDocument(_relatedDocuments[prevDocUrl]); - } - } - if (current && current.document && - FileUtils.isCSSPreprocessorFile(current.document.file.fullPath)) { - var docUrl = _server && _server.pathToUrl(current.document.file.fullPath); - _styleSheetAdded(null, docUrl); - } - } - /** * Open the Connection and go live * @@ -1326,10 +1337,6 @@ define(function LiveDevelopment(require, exports, module) { prepareServerPromise .done(function () { _doLaunchAfterServerReady(doc); - - // Explicitly trigger onActiveEditorChange so that live preview highlighting - // can be set up for the preprocessor files. - onActiveEditorChange(null, EditorManager.getActiveEditor(), null); }) .fail(function () { _showWrongDocError(); @@ -1486,10 +1493,6 @@ define(function LiveDevelopment(require, exports, module) { return _server && _server.getBaseUrl(); } - // Setup activeEditorChange event listener so that we can track cursor positions in - // preprocessor files and perform live preview highlighting on all elements with - // the current selector in the preprocessor file. - $(EditorManager).on("activeEditorChange", onActiveEditorChange); // For unit testing diff --git a/src/file/FileUtils.js b/src/file/FileUtils.js index 8ed8c6dee20..df5a238dfcf 100644 --- a/src/file/FileUtils.js +++ b/src/file/FileUtils.js @@ -417,12 +417,13 @@ define(function (require, exports, module) { } /** - * Determine if file extension is a CSS preprocessor file extension that Brackets supports. + * Determines if file extension is a CSS preprocessor file extension that Brackets supports. * @param {string} filePath could be a path, a file name - * @return {boolean} Returns true if file extension is either less or scss. + * @return {boolean} true if LanguageManager identifies filePath as less or scss language. */ function isCSSPreprocessorFile(filePath) { - return (/(less|scss)/i.test(getFileExtension(filePath))); + var languageId = LanguageManager.getLanguageForPath(filePath).getId(); + return (languageId === "less" || languageId === "scss"); } /** diff --git a/src/language/CSSUtils.js b/src/language/CSSUtils.js index 151cae727d9..87d39c667a9 100644 --- a/src/language/CSSUtils.js +++ b/src/language/CSSUtils.js @@ -624,9 +624,9 @@ define(function (require, exports, module) { /** * Return a string that shows the literal parent hierarchy of the selector - * in selectorInfo. + * in info. * - * @param {!SelectorInfo} info SelectorInfo object with `selector`, `selectorGroup`, `parentSelectors` and other properties + * @param {!SelectorInfo} info * @param {boolean=} useGroup true to append selectorGroup instead of selector * @return {string} the literal parent hierarchy of the selector */ @@ -645,9 +645,28 @@ define(function (require, exports, module) { return info.selector; } + /** + * @typedef {{selector: !string, + * ruleStartLine: number, + * ruleStartChar: number, + * selectorStartLine: number, + * selectorStartChar: number, + * selectorEndLine: number, + * selectorEndChar: number, + * selectorGroupStartLine: number, + * selectorGroupStartChar: number, + * selectorGroup: ?string, + * declListStartLine: number, + * declListStartChar: number, + * declListEndLine: number, + * declListEndChar: number, + * level: number, + * parentSelectors: ?string}} SelectorInfo + */ + /** * Extracts all CSS selectors from the given text - * Returns an array of selectors. Each selector is an object with the following properties: + * Returns an array of SelectorInfo. Each SelectorInfo is an object with the following properties: selector: the text of the selector (note: comma separated selector groups like "h1, h2" are broken into separate selectors) ruleStartLine: line in the text where the rule (including preceding comment) appears @@ -666,11 +685,14 @@ define(function (require, exports, module) { declListStartChar: column in line where the declaration list for the rule starts declListEndLine: line where the declaration list for the rule ends declListEndChar: column in the line where the declaration list for the rule ends - level: the level of the current selector + level: the level of the current selector including any containing @media block in the + nesting level count. Use this property with caution since it is primarily for internal + parsing use. For example, two sibling selectors may have different levels if one + of them is nested inside an @media block and it should not be used for sibling info. parentSelectors: all ancestor selectors separated with '/' if the current selector is a nested one * @param {!string} text CSS text to extract from - * @param {!string} documentMode language mode of the document that text belongs to - * @return {Array.} Array with objects specifying selectors. + * @param {?string} documentMode language mode of the document that text belongs to, default to css if undefined. + * @return {Array.} Array with objects specifying selectors. */ function extractAllSelectors(text, documentMode) { var state, lines, lineCount, @@ -795,22 +817,6 @@ define(function (require, exports, module) { return true; } - function _maybeProperty() { - return (state.state !== "top" && state.state !== "block" && - stream.string.indexOf(";") !== -1); - } - - function _skipProperty() { - while (token !== ";") { - // If there is a '{' or '}' or a comment before the ';', - // then stop skipping. - if (token === "{" || token === "}" || style === "comment") { - return; - } - _nextTokenSkippingWhitespace(); - } - } - function _skipToClosingBracket(startChar) { var skippedText = "", unmatchedBraces = 0; @@ -818,13 +824,12 @@ define(function (require, exports, module) { startChar = "{"; } while (true) { - // Use regexp to detect '{' so that something like - // #{$class} in scss files won't be missed. - if ((startChar === "{" && /\{$/.test(token)) || startChar === token) { + if (token.indexOf(startChar) !== -1 && token.indexOf(_bracketPairs[startChar]) === -1) { + // Found an opening bracket but not the matching closing bracket in the same token unmatchedBraces++; } else if (token === _bracketPairs[startChar]) { unmatchedBraces--; - if (unmatchedBraces === 0) { + if (unmatchedBraces <= 0) { skippedText += token; return skippedText; } @@ -837,6 +842,38 @@ define(function (require, exports, module) { } } + function _maybeProperty() { + return (state.state !== "top" && state.state !== "block" && + // Has a semicolon as in "rgb(0,0,0);", but not one of those after a LESS + // mixin parameter variable as in ".size(@width; @height)" + stream.string.indexOf(";") !== -1 && !/\([^)]+;/.test(stream.string)); + } + + function _skipProperty() { + var prevToken = ""; + while (token !== ";") { + // Skip tokens until the closing brace if we find an interpolated variable. + if (/#\{$/.test(token) || (token === "{" && /@$/.test(prevToken))) { + _skipToClosingBracket("{"); + if (token === "}") { + _nextToken(); // Skip the closing brace + } + if (token === ";") { + break; + } + } + // If there is a '{' or '}' before the ';', + // then stop skipping. + if (token === "{" || token === "}") { + return; + } + prevToken = token; + if (!_nextTokenSkippingComments()) { + break; + } + } + } + function _getParentSelectors() { var j; for (j = selectors.length - 1; j >= 0; j--) { @@ -854,19 +891,25 @@ define(function (require, exports, module) { selectorStartLine = line; // Everything until the next ',' or '{' is part of the current selector - while (token !== "," && token !== "{") { - if (token === "}" && - (!currentSelector || /:\s*\S/.test(currentSelector) || !/\{\w/.test(currentSelector))) { + while ((token !== "," && token !== "{") || + (token === "{" && /@$/.test(currentSelector))) { + if (token === "{") { + // Append the interpolated variable to selector + currentSelector += _skipToClosingBracket("{"); + _nextToken(); // skip the closing brace + } else if (token === "}" && + (!currentSelector || /:\s*\S/.test(currentSelector) || !/#\{.+/.test(currentSelector))) { // Either empty currentSelector or currentSelector is a CSS property // but not a selector that is in the form of #{$class} return false; } - if (token === ";" || + // Clear currentSelector if we're in a property, but make sure we don't treat + // the semicolors inside a parameter as a property separators. + if ((token === ";" && state.state !== "parens") || // Make sure that something like `> li > a {` is not identified as a property (state.state === "prop" && !/\{/.test(stream.string))) { - // Clear currentSelector if we're in a property. currentSelector = ""; - } else if (token === "(" && /,/.test(stream.string)) { + } else if (token === "(") { // Collect everything inside the parentheses as a whole chunk so that // commas inside the parentheses won't be identified as selector separators // by while loop. @@ -1045,7 +1088,15 @@ define(function (require, exports, module) { function _isStartAtRule() { // Exclude @mixin from at-rule so that we can parse it like a normal rule list - return (/^@/.test(token) && !/^@mixin/i.test(token)); + return (/^@/.test(token) && !/^@mixin/i.test(token) && token !== "@"); + } + + function _followedByPseudoSelector() { + return (/\}:(enabled|disabled|checked|indeterminate|link|visited|hover|active|focus|target|lang|root|nth-|first-|last-|only-|empty|not)/.test(stream.string)); + } + + function _isVariableInterpolatedProperty() { + return (/[@#]\{\S+\}(\s*:|.*;)/.test(stream.string) && !_followedByPseudoSelector()); } function _parseAtRule(level) { @@ -1064,13 +1115,13 @@ define(function (require, exports, module) { // Skip everything until the opening '{' while (token !== "{") { if (!_nextTokenSkippingComments()) { - return; // eof + return false; // eof } } // skip past '{', to next non-ws token if (!_nextTokenSkippingWhitespace()) { - return; // eof + return false; // eof } if (currentLevel <= level) { @@ -1086,7 +1137,7 @@ define(function (require, exports, module) { currentLevel--; } - } else if (/@(charset|import|namespace|include|extend)/i.test(token) || + } else if (/@(charset|import|namespace|include|extend|warn)/i.test(token) || !/\{/.test(stream.string)) { // This code handles @rules in this format: @@ -1095,7 +1146,7 @@ define(function (require, exports, module) { // Skip everything until the next ';' while (token !== ";") { if (!_nextTokenSkippingComments()) { - return; // eof + return false; // eof } } @@ -1105,7 +1156,11 @@ define(function (require, exports, module) { // such as @page, @keyframes (also -webkit-keyframes, etc.), and @font-face. // Skip everything including nested braces until the next matching '}' _skipToClosingBracket("{"); + if (token === "}") { + return false; + } } + return true; } // parse a style rule @@ -1119,11 +1174,17 @@ define(function (require, exports, module) { } _parseRuleList = function (escapeToken, level) { + var skipNext = true; while ((!escapeToken) || token !== escapeToken) { - if (_isStartAtRule()) { + if (_isVariableInterpolatedProperty()) { + _skipProperty(); + } else if (_isStartAtRule()) { // @rule - _parseAtRule(level); - + if (!_parseAtRule(level) && level > 0) { + skipNext = false; + } else { + skipNext = true; + } } else if (_isStartComment()) { // comment - make this part of style rule if (includeCommentInNextRule()) { @@ -1141,6 +1202,7 @@ define(function (require, exports, module) { return false; } } else { + skipNext = true; // reset skipNext // Otherwise, it's style rule if (!_parseRule(level === undefined ? 0 : level) && level > 0) { return false; @@ -1154,7 +1216,7 @@ define(function (require, exports, module) { ruleStartLine = -1; } - if (!_nextTokenSkippingWhitespace()) { + if (skipNext && !_nextTokenSkippingWhitespace()) { break; } } @@ -1248,7 +1310,7 @@ define(function (require, exports, module) { * Converts the results of _findAllMatchingSelectorsInText() into a simpler bag of data and * appends those new objects to the given 'resultSelectors' Array. * @param {Array.<{document:Document, lineStart:number, lineEnd:number}>} resultSelectors - * @param {Array.<{selectorGroupStartLine:number, declListEndLine:number, selector:string}>} selectorsToAdd + * @param {Array.} selectorsToAdd * @param {!Document} sourceDoc * @param {!number} lineOffset Amount to offset all line number info by. Used if the first line * of the parsed CSS text is not the first line of the sourceDoc. @@ -1426,7 +1488,7 @@ define(function (require, exports, module) { unmatchedBraces++; } else if (ctx.token.string.match(_invertedBracketPairs[startChar])) { unmatchedBraces--; - if (unmatchedBraces === 0) { + if (unmatchedBraces <= 0) { return; } } diff --git a/src/project/ProjectManager.js b/src/project/ProjectManager.js index 09b797d9d25..dbfcea414e2 100644 --- a/src/project/ProjectManager.js +++ b/src/project/ProjectManager.js @@ -2152,7 +2152,7 @@ define(function (require, exports, module) { /** * Returns a filter for use with getAllFiles() that filters files based on LanguageManager language id - * @param {!string} languageId + * @param {!(string|Array.)} languageId a single string of a language id or an array of language ids * @return {!function(File):boolean} */ function getLanguageFilter(languageId) {