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"
}
}