diff --git a/docs/API-Reference/language/CSSUtils.md b/docs/API-Reference/language/CSSUtils.md index df76ffeb94..82244a9d04 100644 --- a/docs/API-Reference/language/CSSUtils.md +++ b/docs/API-Reference/language/CSSUtils.md @@ -33,6 +33,43 @@ value of the specified property url for import **Kind**: global constant + + +## \_RE\_PAREN\_SEMI ⇒ [Array.<SelectorInfo>](#SelectorInfo) +Extracts all CSS selectors from the given text +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 + ruleStartChar: column in the line where the rule (including preceding comment) starts + selectorStartLine: line in the text where the selector appears + selectorStartChar: column in the line where the selector starts + selectorEndLine: line where the selector ends + selectorEndChar: column where the selector ends + selectorGroupStartLine: line where the comma-separated selector group (e.g. .foo, .bar, .baz) + starts that this selector (e.g. .baz) is part of. Particularly relevant for + groups that are on multiple lines. + selectorGroupStartChar: column in line where the selector group starts. + selectorGroup: the entire selector group containing this selector, or undefined if there + is only one selector in the rule. + declListStartLine: line where the declaration list for the rule starts + 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 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 + +**Kind**: global constant +**Returns**: [Array.<SelectorInfo>](#SelectorInfo) - Array with objects specifying selectors. + +| Param | Type | Description | +| --- | --- | --- | +| text | string | CSS text to extract from | +| documentMode | string | language mode of the document that text belongs to, default to css if undefined. | + ## isCSSPreprocessorFile(filePath) ⇒ boolean @@ -80,43 +117,6 @@ in info. | info | [SelectorInfo](#SelectorInfo) | | | [useGroup] | boolean | true to append selectorGroup instead of selector | - - -## extractAllSelectors(text, documentMode) ⇒ [Array.<SelectorInfo>](#SelectorInfo) -Extracts all CSS selectors from the given text -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 - ruleStartChar: column in the line where the rule (including preceding comment) starts - selectorStartLine: line in the text where the selector appears - selectorStartChar: column in the line where the selector starts - selectorEndLine: line where the selector ends - selectorEndChar: column where the selector ends - selectorGroupStartLine: line where the comma-separated selector group (e.g. .foo, .bar, .baz) - starts that this selector (e.g. .baz) is part of. Particularly relevant for - groups that are on multiple lines. - selectorGroupStartChar: column in line where the selector group starts. - selectorGroup: the entire selector group containing this selector, or undefined if there - is only one selector in the rule. - declListStartLine: line where the declaration list for the rule starts - 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 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 - -**Kind**: global function -**Returns**: [Array.<SelectorInfo>](#SelectorInfo) - Array with objects specifying selectors. - -| Param | Type | Description | -| --- | --- | --- | -| text | string | CSS text to extract from | -| documentMode | string | language mode of the document that text belongs to, default to css if undefined. | - ## findMatchingRules(selector, htmlDocument) ⇒ $.Promise diff --git a/src/LiveDevelopment/BrowserScripts/LiveDevProtocolRemote.js b/src/LiveDevelopment/BrowserScripts/LiveDevProtocolRemote.js index 85842984b6..eaf420dc83 100644 --- a/src/LiveDevelopment/BrowserScripts/LiveDevProtocolRemote.js +++ b/src/LiveDevelopment/BrowserScripts/LiveDevProtocolRemote.js @@ -138,10 +138,18 @@ * Evaluate an expresion and return its result. */ evaluate: function (msg) { - var result = eval(msg.params.expression); - MessageBroker.respond(msg, { - result: JSON.stringify(result) // TODO: in original protocol this is an object handle - }); + // an unanswered request leaves the editor side waiting forever + try { + var result = eval(msg.params.expression); + MessageBroker.respond(msg, { + result: JSON.stringify(result) // TODO: in original protocol this is an object handle + }); + } catch (e) { + console.error("[Brackets LiveDev] Runtime.evaluate failed", e); + MessageBroker.respond(msg, { + error: String(e && e.message || e) + }); + } } }; diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 7d049eb79f..3272d1f8ad 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -450,34 +450,78 @@ function RemoteFunctions(config = {}) { _overlayPool.push(overlay); } - // Update an existing overlay's position, dimensions, and colors to match the target element. - // No DOM elements are created or destroyed — only style properties are updated. - function _updateOverlay(overlay, element) { + // Everything an overlay needs read off the page. Split from the painting + // below so a batch of overlays can read first and write after: interleaving + // the two forces a layout per element. + // What screenOffset() needs off the body, read once for a whole batch + // instead of once per element. + function _bodyOffsetContext() { + const body = window.document.body; + if (window.getComputedStyle(body).position === "static") { + return { isStatic: true, x: window.pageXOffset, y: window.pageYOffset }; + } + const bodyBounds = body.getBoundingClientRect(); + return { isStatic: false, x: bodyBounds.left, y: bodyBounds.top }; + } + + function _offsetFromBounds(bounds, bodyOffset) { + if (bodyOffset.isStatic) { + return { left: bounds.left + bodyOffset.x, top: bounds.top + bodyOffset.y }; + } + return { left: bounds.left - bodyOffset.x, top: bounds.top - bodyOffset.y }; + } + + function _measureOverlay(element, bodyOffset) { const bounds = element.getBoundingClientRect(); if (bounds.width === 0 && bounds.height === 0) { + return null; + } + const cs = window.getComputedStyle(element); + return { + bounds: bounds, + scroll: _offsetFromBounds(bounds, bodyOffset || _bodyOffsetContext()), + bt: parseFloat(cs.borderTopWidth) || 0, + br: parseFloat(cs.borderRightWidth) || 0, + bb: parseFloat(cs.borderBottomWidth) || 0, + bl: parseFloat(cs.borderLeftWidth) || 0, + pt: parseFloat(cs.paddingTop) || 0, + pr: parseFloat(cs.paddingRight) || 0, + pb: parseFloat(cs.paddingBottom) || 0, + pl: parseFloat(cs.paddingLeft) || 0, + mt: parseFloat(cs.marginTop) || 0, + mr: parseFloat(cs.marginRight) || 0, + mb: parseFloat(cs.marginBottom) || 0, + ml: parseFloat(cs.marginLeft) || 0 + }; + } + + function _measureAll(elements) { + const bodyOffset = _bodyOffsetContext(); + const measured = []; + for (let i = 0; i < elements.length; i++) { + measured.push(_measureOverlay(elements[i], bodyOffset)); + } + return measured; + } + + // Update an existing overlay's position, dimensions, and colors to match the target element. + // No DOM elements are created or destroyed — only style properties are updated. + function _paintOverlay(overlay, element, measured) { + if (!measured) { overlay.classList.add('hidden'); return; } - const cs = window.getComputedStyle(element); + const bounds = measured.bounds; // Parse box model values (getComputedStyle always resolves to px) - const bt = parseFloat(cs.borderTopWidth) || 0, - br = parseFloat(cs.borderRightWidth) || 0, - bb = parseFloat(cs.borderBottomWidth) || 0, - bl = parseFloat(cs.borderLeftWidth) || 0; - const pt = parseFloat(cs.paddingTop) || 0, - pr = parseFloat(cs.paddingRight) || 0, - pb = parseFloat(cs.paddingBottom) || 0, - pl = parseFloat(cs.paddingLeft) || 0; - const mt = parseFloat(cs.marginTop) || 0, - mr = parseFloat(cs.marginRight) || 0, - mb = parseFloat(cs.marginBottom) || 0, - ml = parseFloat(cs.marginLeft) || 0; + const bt = measured.bt, br = measured.br, bb = measured.bb, bl = measured.bl; + const pt = measured.pt, pr = measured.pr, pb = measured.pb, pl = measured.pl; + const mt = measured.mt, mr = measured.mr, mb = measured.mb, ml = measured.ml; // Compute the 4 absolute boxes exactly like dev tools: // getBoundingClientRect() always returns the border box regardless of box-sizing. - const scroll = LivePreviewView.screenOffset(element); + const scroll = measured.scroll; const borderBox = { left: scroll.left, top: scroll.top, @@ -551,6 +595,10 @@ function RemoteFunctions(config = {}) { outlineStyle.border = `1px solid ${outlineColor}`; } + function _updateOverlay(overlay, element) { + _paintOverlay(overlay, element, _measureOverlay(element)); + } + function Highlight(trigger) { this.trigger = !!trigger; this.elements = []; @@ -573,6 +621,28 @@ function RemoteFunctions(config = {}) { _updateOverlay(overlay, element); }, + addAll: function (elements) { + const seen = new Set(this.elements); + const fresh = []; + for (let i = 0; i < elements.length; i++) { + const element = elements[i]; + if (element !== window.document && !seen.has(element)) { + seen.add(element); + fresh.push(element); + } + } + const measured = _measureAll(fresh); + for (let i = 0; i < fresh.length; i++) { + if (this.trigger) { + _trigger(fresh[i], "highlight", 1); + } + this.elements.push(fresh[i]); + const overlay = _getOverlay(); + this._overlays.push(overlay); + _paintOverlay(overlay, fresh[i], measured[i]); + } + }, + clear: function () { this._overlays.forEach(function (overlay) { _releaseOverlay(overlay); @@ -611,8 +681,9 @@ function RemoteFunctions(config = {}) { this.elements = elements; // Update all overlays in place — no DOM creation or destruction + const measured = _measureAll(elements); for (let i = 0; i < elements.length; i++) { - _updateOverlay(this._overlays[i], elements[i]); + _paintOverlay(this._overlays[i], elements[i], measured[i]); } } }; @@ -672,11 +743,6 @@ function RemoteFunctions(config = {}) { if (SHARED_STATE.isAutoScrolling || SHARED_STATE._isDraggingSVG) { return; } - if (customReturns.selectorBox && customReturns.selectorBox.isOpen && - customReturns.selectorBox.isOpen()) { - return; - } - const element = event.target; if (element === _lastHoverTarget) { @@ -742,7 +808,7 @@ function RemoteFunctions(config = {}) { * @param {Element} element - The DOM element to select * @param {boolean} [fromEditor] - If true, this is an editor-cursor-driven selection; * only lightweight highlights (outline, margin/padding overlay) are shown, not interactive - * UI like control box, spacing handles, or measurements. + * UI like the control box or the styles bar. * @param {boolean} [byName] - Selected by name (a layers panel row), so the edit * opt-out and the body block don't apply while it stays selected. */ @@ -1008,13 +1074,15 @@ function RemoteFunctions(config = {}) { // Highlight all matching elements except the selected one // (it already has a click highlight) _cssSelectorHighlight = new Highlight(); + const wanted = []; for (let i = 0; i < nodes.length; i++) { if (nodes[i] !== previouslySelectedElement && LivePreviewView.isElementInspectable(nodes[i], true) && nodes[i].nodeType === Node.ELEMENT_NODE) { - _cssSelectorHighlight.add(nodes[i]); + wanted.push(nodes[i]); } } + _cssSelectorHighlight.addAll(wanted); _cssSelectorHighlight.selector = rule; } @@ -1044,6 +1112,20 @@ function RemoteFunctions(config = {}) { } } + function highlightAll(elements) { + if (!_clickHighlight) { + _clickHighlight = new Highlight(); + } + const wanted = []; + for (let i = 0; i < elements.length; i++) { + if (LivePreviewView.isElementInspectable(elements[i], true) && + elements[i].nodeType === Node.ELEMENT_NODE) { + wanted.push(elements[i]); + } + } + _clickHighlight.addAll(wanted); + } + /** * Find the best element to select from a list of matched nodes * Prefers: previously selected element > parent of selected > first valid element @@ -1110,17 +1192,6 @@ function RemoteFunctions(config = {}) { const nodes = window.document.querySelectorAll(rule); - // Highlight all matching nodes. selectElement() will narrow _clickHighlight - // down to the chosen element below; createCssSelectorHighlight() then - // re-highlights the siblings in a separate overlay. - for (let i = 0; i < nodes.length; i++) { - highlight(nodes[i]); - } - - if (_clickHighlight) { - _clickHighlight.selector = rule; - } - // Both edit and highlight modes go through the same selection path: // selectElement() handles scroll-to-view and the prominent click-highlight, // createCssSelectorHighlight() shows siblings dimly. fromEditor=true @@ -1128,8 +1199,15 @@ function RemoteFunctions(config = {}) { // highlighting/scroll behavior without any UI boxes. const { element, skipSelection } = findBestElementToSelect(nodes, rule); - if (!skipSelection) { + if (skipSelection) { + // A recent preview click owns the selection and its open tools. + // Keep the existing selector highlight without re-selecting it. + highlightAll(nodes); + _clickHighlight.selector = rule; + } else { if (element) { + // Select first: drawing every match here would immediately be + // cleared by selectElement() and drawn again as siblings below. selectElement(element, true); } else { // No valid element found, dismiss UI diff --git a/src/LiveDevelopment/MultiBrowserImpl/documents/LiveDocument.js b/src/LiveDevelopment/MultiBrowserImpl/documents/LiveDocument.js index 0d8d745063..5401a88df9 100644 --- a/src/LiveDevelopment/MultiBrowserImpl/documents/LiveDocument.js +++ b/src/LiveDevelopment/MultiBrowserImpl/documents/LiveDocument.js @@ -35,6 +35,10 @@ define(function (require, exports, module) { */ var SYNC_ERROR_CLASS = "live-preview-sync-error"; + // A held arrow key moves the caret far faster than the rule under it can be + // resolved, so the highlight follows the caret once it settles. + const CURSOR_HIGHLIGHT_DEBOUNCE_MS = 80; + function _simpleHash(str) { let hash = 5381; for (let i = 0; i < str.length; ) { @@ -166,12 +170,14 @@ define(function (require, exports, module) { */ LiveDocument.prototype._detachFromEditor = function () { if (this.editor) { + this._cancelPendingHighlight(); this.hideHighlight(); this.editor.off("cursorActivity", this._onCursorActivity); } }; let _disableHighlightOnCursor = false; + let _cursorHighlightGeneration = 0; /** * If tur, it will disable highlights in live preview on cursor movement in editor @@ -180,6 +186,12 @@ define(function (require, exports, module) { LiveDocument.prototype.disableHighlightOnCursorActivity = function (shouldDisable) { // intentionally global. see usage for details _disableHighlightOnCursor = shouldDisable; + if (shouldDisable) { + // A preview click or source edit also supersedes timers queued by + // other live documents (for example the active CSS editor). + _cursorHighlightGeneration++; + this._cancelPendingHighlight(); + } }; /** @@ -197,11 +209,24 @@ define(function (require, exports, module) { * @param {Editor} editor */ LiveDocument.prototype._onCursorActivity = function (event, editor) { - if (!this.editor) { + this._cancelPendingHighlight(); + if (!this.editor || _disableHighlightOnCursor) { return; } - if(!_disableHighlightOnCursor){ - this.updateHighlight(); + const self = this; + const generation = _cursorHighlightGeneration; + this._highlightTimer = window.setTimeout(function () { + self._highlightTimer = null; + if (self.editor && !_disableHighlightOnCursor && generation === _cursorHighlightGeneration) { + self.updateHighlight(); + } + }, CURSOR_HIGHLIGHT_DEBOUNCE_MS); + }; + + LiveDocument.prototype._cancelPendingHighlight = function () { + if (this._highlightTimer) { + window.clearTimeout(this._highlightTimer); + this._highlightTimer = null; } }; @@ -296,6 +321,8 @@ define(function (require, exports, module) { if (!temporary) { this._lastHighlight = null; } + // The preview can have been selected directly or by another live + // document, so this document's cached selector cannot prove it is clear. this.protocol.evaluate("_LD.hideHighlight()"); }; diff --git a/src/LiveDevelopment/MultiBrowserImpl/language/HTMLInstrumentation.js b/src/LiveDevelopment/MultiBrowserImpl/language/HTMLInstrumentation.js index 401a60787b..c1c70941ef 100644 --- a/src/LiveDevelopment/MultiBrowserImpl/language/HTMLInstrumentation.js +++ b/src/LiveDevelopment/MultiBrowserImpl/language/HTMLInstrumentation.js @@ -867,20 +867,29 @@ define(function (require, exports, module) { _cachedValues = {}; } - function getPositionFromTagId(editor, tagId) { - var marks = editor._codeMirror.getAllMarks(), - markFound; - - markFound = _.find(marks, function (mark) { - return (mark.tagID === tagId); + function _rebuildTagIdMarkMap(cm) { + const map = new Map(); + cm.getAllMarks().forEach(function (mark) { + if (mark.hasOwnProperty("tagID")) { + map.set(mark.tagID, mark); + } }); - if (markFound) { - return { - from: markFound.find().from, - to: markFound.find().to - }; + cm._phTagIdMarks = map; + return map; + } + + function getPositionFromTagId(editor, tagId) { + const cm = editor._codeMirror; + let map = cm._phTagIdMarks || _rebuildTagIdMarkMap(cm); + let mark = map.get(tagId); + // a cleared mark has no range, so the map is rebuilt from the live marks + let range = mark && mark.find(); + if (!range) { + map = _rebuildTagIdMarkMap(cm); + mark = map.get(tagId); + range = mark && mark.find(); } - return null; + return range ? { from: range.from, to: range.to } : null; } // private methods diff --git a/src/language/CSSUtils.js b/src/language/CSSUtils.js index c217474942..c6aaeb2526 100644 --- a/src/language/CSSUtils.js +++ b/src/language/CSSUtils.js @@ -780,6 +780,13 @@ define(function (require, exports, module) { * @param {?string} documentMode language mode of the document that text belongs to, default to css if undefined. * @return {Array.} Array with objects specifying selectors. */ + // Tests that depend only on the line being tokenized. A minified stylesheet + // is one very long line, so running these per token is what made parsing it slow. + const _RE_PAREN_SEMI = /\([^)]+;/; + const _RE_OPEN_BRACE = /\{/; + const _RE_FOLLOWED_BY_PSEUDO = /\}:(enabled|disabled|checked|indeterminate|link|visited|hover|active|focus|target|lang|root|nth-|first-|last-|only-|empty|not)/; + const _RE_VAR_INTERPOLATED = /[@#]\{\S+\}(\s*:|.*;)/; + function extractAllSelectors(text, documentMode) { var state, lines, lineCount, token, style, stream, line, @@ -797,7 +804,23 @@ define(function (require, exports, module) { declListStartChar = -1, escapePattern = new RegExp("\\\\[^\\\\]+", "g"), validationPattern = new RegExp("\\\\([a-f0-9]{6}|[a-f0-9]{4}(\\s|\\\\|$)|[a-f0-9]{2}(\\s|\\\\|$)|.)", "i"), - _parseRuleList; + _parseRuleList, + _lineFlagsFor = null, + _lineFlags = {}, + _parentScanLevel = -1, + _parentScanFrom = 0, + _firstUnset = 0; + + function _lineFlag(name, compute) { + if (_lineFlagsFor !== stream.string) { + _lineFlagsFor = stream.string; + _lineFlags = {}; + } + if (_lineFlags[name] === undefined) { + _lineFlags[name] = compute(_lineFlagsFor); + } + return _lineFlags[name]; + } // implement _firstToken()/_nextToken() methods to // provide a single stream of tokens @@ -933,7 +956,8 @@ define(function (require, exports, module) { (state.state !== "top" && state.state !== "block" && state.state !== "pseudo" && // 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))); + _lineFlag("semi", function (str) { return str.indexOf(";") !== -1; }) && + !_lineFlag("parenSemi", function (str) { return _RE_PAREN_SEMI.test(str); }))); } function _skipProperty() { @@ -962,13 +986,22 @@ define(function (require, exports, module) { return true; // skip the entire property } + // Selectors already ruled out for this level can never qualify later: a + // closed rule stays closed and a level never drops. Remembering how far + // back that has been checked keeps a flat stylesheet from rescanning every + // selector it has seen for every selector it reads. function _getParentSelectors() { var j; - for (j = selectors.length - 1; j >= 0; j--) { + if (_parentScanLevel !== currentLevel) { + _parentScanLevel = currentLevel; + _parentScanFrom = 0; + } + for (j = selectors.length - 1; j >= _parentScanFrom; j--) { if (selectors[j].declListEndLine === -1 && selectors[j].level < currentLevel) { return getCompleteSelectors(selectors[j], true); } } + _parentScanFrom = selectors.length; return ""; } @@ -996,7 +1029,8 @@ define(function (require, exports, module) { // 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))) { + (state.state === "prop" && + !_lineFlag("brace", function (str) { return _RE_OPEN_BRACE.test(str); }))) { currentSelector = ""; } else if (token === "(") { // Collect everything inside the parentheses as a whole chunk so that @@ -1041,7 +1075,17 @@ define(function (require, exports, module) { currentSelector = currentSelector.trim(); var startChar = (selectorGroupStartLine === -1) ? selectorStartChar : selectorStartChar + 1; - var selectorStart = (stream.string.indexOf(currentSelector, selectorStartChar) !== -1) ? stream.string.indexOf(currentSelector, selectorStartChar - currentSelector.length) : startChar; + // The selector sits next to where it started, so it is looked for around + // there rather than down the whole line: a minified sheet is one very + // long line and scanning it per selector is quadratic. + var selectorLength = currentSelector.length; + var searchFrom = Math.max(0, selectorStartChar - selectorLength); + var searchTo = Math.min(stream.string.length, + Math.max(stream.start, selectorStartChar) + selectorLength + 1); + var searchText = stream.string.slice(searchFrom, searchTo); + var firstIndex = searchText.indexOf(currentSelector); + var fromStart = searchText.indexOf(currentSelector, Math.max(0, selectorStartChar - searchFrom)); + var selectorStart = (firstIndex !== -1 && fromStart !== -1) ? searchFrom + firstIndex : startChar; if (currentSelector !== "") { if (currentLevel < level) { @@ -1066,6 +1110,9 @@ define(function (require, exports, module) { level: currentLevel, parentSelectors: parentSelectors }); + if (_firstUnset > selectors.length - 1) { + _firstUnset = selectors.length - 1; + } currentSelector = ""; } selectorStartChar = -1; @@ -1074,7 +1121,8 @@ define(function (require, exports, module) { } function _parseSelectorList(level) { - selectorGroupStartLine = (stream.string.indexOf(",") !== -1) ? line : -1; + selectorGroupStartLine = + _lineFlag("comma", function (str) { return str.indexOf(",") !== -1; }) ? line : -1; selectorGroupStartChar = stream.start; if (!_parseSelector(stream.start, level)) { @@ -1120,7 +1168,7 @@ define(function (require, exports, module) { // assign this declaration list position and selector group to every selector on the stack // that doesn't have a declaration list start and end line - for (j = selectors.length - 1; j >= 0; j--) { + for (j = selectors.length - 1; j >= _firstUnset; j--) { if (selectors[j].level === level) { if (selectors[j].declListEndLine !== -1) { break; @@ -1160,9 +1208,13 @@ define(function (require, exports, module) { nested = _parseRuleList(undefined, currentLevel + 1); // assign this declaration list position to every selector on the stack - // that doesn't have a declaration list end line - for (j = selectors.length - 1; j >= 0; j--) { + // that doesn't have a declaration list end line. Everything before + // _firstUnset already carries one, so the walk stops there rather + // than running back over the whole stylesheet for every rule. + var closedAll = true; + for (j = selectors.length - 1; j >= _firstUnset; j--) { if (selectors[j].level < currentLevel) { + closedAll = false; break; } if (selectors[j].declListEndLine === -1) { @@ -1170,6 +1222,9 @@ define(function (require, exports, module) { selectors[j].declListEndChar = stream.pos - 1; // stream.pos actually points to the char after the } } } + if (closedAll) { + _firstUnset = selectors.length; + } } while (currentLevel > 0 && currentLevel === level); } @@ -1189,11 +1244,12 @@ define(function (require, exports, module) { } function _followedByPseudoSelector() { - return (/\}:(enabled|disabled|checked|indeterminate|link|visited|hover|active|focus|target|lang|root|nth-|first-|last-|only-|empty|not)/.test(stream.string)); + return _lineFlag("pseudo", function (str) { return _RE_FOLLOWED_BY_PSEUDO.test(str); }); } function _isVariableInterpolatedProperty() { - return (/[@#]\{\S+\}(\s*:|.*;)/.test(stream.string) && !_followedByPseudoSelector()); + return _lineFlag("varInterp", function (str) { return _RE_VAR_INTERPOLATED.test(str); }) && + !_followedByPseudoSelector(); } function _parseAtRule(level) { @@ -1609,7 +1665,7 @@ define(function (require, exports, module) { } } - if (!TokenUtils.movePrevToken(ctx)) { + if (!TokenUtils.movePrevToken(ctx, false)) { return; } } @@ -1621,7 +1677,7 @@ define(function (require, exports, module) { var selector = ""; // Skip over { - TokenUtils.movePrevToken(ctx); + TokenUtils.movePrevToken(ctx, false); while (true) { if (ctx.token.type !== "comment") { @@ -1642,7 +1698,7 @@ define(function (require, exports, module) { selector = ctx.token.string + selector; } - if (!TokenUtils.movePrevToken(ctx)) { + if (!TokenUtils.movePrevToken(ctx, false)) { break; } } @@ -1696,10 +1752,10 @@ define(function (require, exports, module) { if (!isPreprocessorDoc && _hasNonWhitespace(ctx.token.string)) { foundChars = true; } - TokenUtils.movePrevToken(ctx); + TokenUtils.movePrevToken(ctx, false); } } else { - TokenUtils.movePrevToken(ctx); + TokenUtils.movePrevToken(ctx, false); } } while (!TokenUtils.isAtStart(ctx)); diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index b904cc2a4d..7c2b73fd70 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -486,33 +486,6 @@ define({ "LIVE_DEV_IMAGE_GALLERY_ATTRIBUTION_ON": "on {0}", "LIVE_DEV_IMAGE_GALLERY_GET_PRO": "Get Pro", "LIVE_DEV_IMAGE_GALLERY_USAGE_THRESHOLD": "You've used {0}% of your free image searches ({1}/{2})", - "LIVE_DEV_LP_SELBOX_TITLE": "Save Changes", - "LIVE_DEV_LP_SELBOX_INLINE": "Inline", - "LIVE_DEV_LP_SELBOX_INLINE_SECONDARY": "element.style", - "LIVE_DEV_LP_SELBOX_LOADING": "Finding matching rules\u2026", - "LIVE_DEV_LP_SELBOX_FILE_EMBEDDED": "embedded", - "LIVE_DEV_LP_SELBOX_SAVE_TOOLTIP": "Save changes to the selected target (Enter)", - "LIVE_DEV_LP_SELBOX_CANCEL_TOOLTIP": "Revert changes (Esc)", - "LIVE_DEV_STYLES_PANEL_HEADER": "Style Editor", - "LIVE_DEV_STYLES_EDIT_TOOLTIP": "Edit Styles", - "LIVE_DEV_STYLES_PANEL_ADD": "+ Add", - "LIVE_DEV_STYLES_PANEL_NO_RULES": "No CSS rules found", - "LIVE_DEV_STYLES_PANEL_MATCHING_RULES": "Matching Rules ({0})", - "LIVE_DEV_STYLES_PANEL_LOADING": "Loading styles\u2026", - "LIVE_DEV_STYLES_PANEL_NO_STYLES": "No styles found", - "LIVE_DEV_STYLES_PANEL_PROPERTY_PLACEHOLDER": "property", - "LIVE_DEV_STYLES_PANEL_VALUE_PLACEHOLDER": "value", - "LIVE_DEV_STYLES_TAB_ADVANCED": "Styles", - "LIVE_DEV_STYLES_TAB_COMPUTED": "Computed", - "LIVE_DEV_STYLES_FILTER_ALL": "All", - "LIVE_DEV_STYLES_FILTER_LAYOUT": "Layout", - "LIVE_DEV_STYLES_FILTER_TYPOGRAPHY": "Typography", - "LIVE_DEV_STYLES_FILTER_COLOR": "Color", - "LIVE_DEV_STYLES_FILTER_EFFECTS": "Effects", - "LIVE_DEV_STYLES_FILTER_BOX_MODEL": "Box Model", - "LIVE_DEV_STYLES_COMPUTED_SEARCH": "Filter properties\u2026", - "LIVE_DEV_STYLES_COMPUTED_NO_RESULTS": "No results found", - "LIVE_DEV_STYLES_COMPUTED_USER_AGENT": "User Agent", "LIVE_DEV_FORMAT_BOLD": "Bold", "LIVE_DEV_FORMAT_ITALIC": "Italic", "LIVE_DEV_FORMAT_UNDERLINE": "Underline", @@ -864,6 +837,8 @@ define({ "LIVE_PREVIEW_LAYERS_EMPTY_PAGE_STYLES": "The page is empty. Add an element to see its styles.", "LIVE_PREVIEW_LAYERS_SHOW_SELECTED": "Show the selected element", "LIVE_PREVIEW_LAYERS_SCRUB_HINT": "Drag to adjust, double-click to edit", + "LIVE_PREVIEW_LAYERS_PROPERTY_PLACEHOLDER": "property", + "LIVE_PREVIEW_LAYERS_VALUE_PLACEHOLDER": "value", "LIVE_DEV_DETACHED_REPLACED_WITH_DEVTOOLS": "Live Preview was canceled because the browser's developer tools were opened", "LIVE_DEV_DETACHED_TARGET_CLOSED": "Live Preview was canceled because the page was closed in the browser", diff --git a/src/styles/Extn-LayersPanel.less b/src/styles/Extn-LayersPanel.less index ceee5f07c5..0baf7a0d58 100644 --- a/src/styles/Extn-LayersPanel.less +++ b/src/styles/Extn-LayersPanel.less @@ -29,6 +29,7 @@ @layers-input-bg-focus: rgba(255, 255, 255, 0.06); @layers-row-height: 26px; @layers-label-min-width: 72px; +@layers-row-intrinsic-width: 115px; @layers-tools-width: 96px; @layers-indent: 14px; @layers-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace; @@ -471,6 +472,13 @@ font-size: @sidebar-content-font-size; } + .layers-sprite { + position: absolute; + width: 0; + height: 0; + overflow: hidden; + } + .layers-tree.layers-kbd-nav .layers-node:hover:not(.layers-selected) { background: transparent; } @@ -488,6 +496,8 @@ display: flex; align-items: center; height: @layers-row-height; + content-visibility: auto; + contain-intrinsic-size: auto @layers-row-intrinsic-width auto @layers-row-height; padding-right: 6px; box-sizing: border-box; white-space: nowrap; diff --git a/src/utils/TokenUtils.js b/src/utils/TokenUtils.js index c111d31cfb..b2edc0c267 100644 --- a/src/utils/TokenUtils.js +++ b/src/utils/TokenUtils.js @@ -98,7 +98,9 @@ define(function (require, exports, module) { return { "editor": cm, "pos": pos, - "token": cm.getTokenAt(pos, true) + // Invalidate cached tokens too: cursorActivity and callers inside an + // operation can run before the "changes" event clears the cache. + "token": getTokenAt(cm, pos, true) }; } diff --git a/tracking-repos.json b/tracking-repos.json index a2ca12e38a..bfb5e2bd21 100644 --- a/tracking-repos.json +++ b/tracking-repos.json @@ -1,5 +1,5 @@ { "phoenixPro": { - "commitID": "d71aaac1a8ac3e4f0af5c5c2bbe5ff7c787a9945" + "commitID": "893b06e5612ad563f7fcf32ac132a3430273fb10" } }