From 719cc71029fd2f24a1599d1f851a38a7c46828b4 Mon Sep 17 00:00:00 2001 From: Pluto Date: Wed, 9 Sep 2026 12:29:44 +0530 Subject: [PATCH 01/17] feat: remove the strings from old styles panel feature --- src/nls/root/strings.js | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index b904cc2a4d..5537200ece 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -493,26 +493,8 @@ define({ "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", From 1592a11f39fdc4694181f006e9163472f0fb7584 Mon Sep 17 00:00:00 2001 From: Pluto Date: Wed, 9 Sep 2026 12:35:54 +0530 Subject: [PATCH 02/17] feat: remove all the element handles related strings --- src/LiveDevelopment/BrowserScripts/RemoteFunctions.js | 5 ----- src/nls/root/strings.js | 7 ------- 2 files changed, 12 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 7d049eb79f..2279edf57f 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -672,11 +672,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) { diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index 5537200ece..6d3465d924 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -486,13 +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_PROPERTY_PLACEHOLDER": "property", "LIVE_DEV_STYLES_PANEL_VALUE_PLACEHOLDER": "value", "LIVE_DEV_FORMAT_BOLD": "Bold", From 8a70834bdfa306131bfbe5ba6b30dbf246373897 Mon Sep 17 00:00:00 2001 From: Pluto Date: Wed, 9 Sep 2026 13:11:32 +0530 Subject: [PATCH 03/17] refactor: update string keys to layers panel convention --- src/LiveDevelopment/BrowserScripts/RemoteFunctions.js | 2 +- src/nls/root/strings.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 2279edf57f..fae99aae0e 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -737,7 +737,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. */ diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index 6d3465d924..7c2b73fd70 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -486,8 +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_STYLES_PANEL_PROPERTY_PLACEHOLDER": "property", - "LIVE_DEV_STYLES_PANEL_VALUE_PLACEHOLDER": "value", "LIVE_DEV_FORMAT_BOLD": "Bold", "LIVE_DEV_FORMAT_ITALIC": "Italic", "LIVE_DEV_FORMAT_UNDERLINE": "Underline", @@ -839,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", From 458dea1aa9b4db99969fd55e54bbb14441031e77 Mon Sep 17 00:00:00 2001 From: Pluto Date: Wed, 9 Sep 2026 13:49:07 +0530 Subject: [PATCH 04/17] fix: reply with the error when a live preview evaluate throws --- .../BrowserScripts/LiveDevProtocolRemote.js | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) 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) + }); + } } }; From ddc05ccf628e573d11678e02d191896cd01ee990 Mon Sep 17 00:00:00 2001 From: Pluto Date: Wed, 9 Sep 2026 13:50:26 +0530 Subject: [PATCH 05/17] perf: look up instrumented tags by id through a mark map --- .../language/HTMLInstrumentation.js | 33 ++++++++++++------- 1 file changed, 21 insertions(+), 12 deletions(-) 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 From 5c4543ca84b1312c426f6f5621669dd566bfc0be Mon Sep 17 00:00:00 2001 From: Pluto Date: Wed, 9 Sep 2026 14:14:40 +0530 Subject: [PATCH 06/17] perf: skip layout of offscreen layers rows --- src/styles/Extn-LayersPanel.less | 10 ++++++++++ 1 file changed, 10 insertions(+) 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; From a429bc47146d0150130b6646090a186239130fb8 Mon Sep 17 00:00:00 2001 From: Pluto Date: Wed, 9 Sep 2026 14:25:56 +0530 Subject: [PATCH 07/17] perf: cache per-line tests while extracting css selectors --- src/language/CSSUtils.js | 36 ++++++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/src/language/CSSUtils.js b/src/language/CSSUtils.js index c217474942..c3be21b5a2 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,20 @@ 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 = {}; + + 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 +953,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() { @@ -996,7 +1017,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 @@ -1074,7 +1096,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)) { @@ -1189,11 +1212,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) { From 862e65e8175a0f264d334021ad2f3ae825e2648b Mon Sep 17 00:00:00 2001 From: Pluto Date: Wed, 9 Sep 2026 14:29:28 +0530 Subject: [PATCH 08/17] perf: batch highlight measurements before painting overlays --- .../BrowserScripts/RemoteFunctions.js | 101 ++++++++++++++---- 1 file changed, 79 insertions(+), 22 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index fae99aae0e..8cc4414dfe 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -450,34 +450,51 @@ 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. + function _measureOverlay(element) { const bounds = element.getBoundingClientRect(); if (bounds.width === 0 && bounds.height === 0) { + return null; + } + const cs = window.getComputedStyle(element); + return { + bounds: bounds, + scroll: LivePreviewView.screenOffset(element), + 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 + }; + } + + // 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 +568,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 +594,27 @@ function RemoteFunctions(config = {}) { _updateOverlay(overlay, element); }, + addAll: function (elements) { + const fresh = []; + for (let i = 0; i < elements.length; i++) { + const element = elements[i]; + if (element !== window.document && !this.elements.includes(element) && + fresh.indexOf(element) === -1) { + fresh.push(element); + } + } + const measured = fresh.map(_measureOverlay); + 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 +653,9 @@ function RemoteFunctions(config = {}) { this.elements = elements; // Update all overlays in place — no DOM creation or destruction + const measured = elements.map(_measureOverlay); for (let i = 0; i < elements.length; i++) { - _updateOverlay(this._overlays[i], elements[i]); + _paintOverlay(this._overlays[i], elements[i], measured[i]); } } }; @@ -1003,13 +1046,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; } @@ -1039,6 +1084,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 @@ -1108,9 +1167,7 @@ function RemoteFunctions(config = {}) { // 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]); - } + highlightAll(nodes); if (_clickHighlight) { _clickHighlight.selector = rule; From c0be01f521fc1381aaecf95960e66861347b71d6 Mon Sep 17 00:00:00 2001 From: Pluto Date: Wed, 9 Sep 2026 14:48:34 +0530 Subject: [PATCH 09/17] perf: stop rescanning selectors already resolved while parsing css --- src/language/CSSUtils.js | 44 ++++++++++++++++++++++++++++++++++------ 1 file changed, 38 insertions(+), 6 deletions(-) diff --git a/src/language/CSSUtils.js b/src/language/CSSUtils.js index c3be21b5a2..5302c120bf 100644 --- a/src/language/CSSUtils.js +++ b/src/language/CSSUtils.js @@ -806,7 +806,10 @@ define(function (require, exports, module) { validationPattern = new RegExp("\\\\([a-f0-9]{6}|[a-f0-9]{4}(\\s|\\\\|$)|[a-f0-9]{2}(\\s|\\\\|$)|.)", "i"), _parseRuleList, _lineFlagsFor = null, - _lineFlags = {}; + _lineFlags = {}, + _parentScanLevel = -1, + _parentScanFrom = 0, + _firstUnset = 0; function _lineFlag(name, compute) { if (_lineFlagsFor !== stream.string) { @@ -983,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 ""; } @@ -1063,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) { @@ -1088,6 +1110,9 @@ define(function (require, exports, module) { level: currentLevel, parentSelectors: parentSelectors }); + if (_firstUnset > selectors.length - 1) { + _firstUnset = selectors.length - 1; + } currentSelector = ""; } selectorStartChar = -1; @@ -1143,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; @@ -1183,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) { @@ -1193,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); } From 825842483cf27f81ee4ed6578830fadec38b0607 Mon Sep 17 00:00:00 2001 From: Pluto Date: Wed, 9 Sep 2026 15:48:49 +0530 Subject: [PATCH 10/17] perf: measure highlight overlays without re-reading each element --- .../BrowserScripts/RemoteFunctions.js | 35 ++++++++++++++++--- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 8cc4414dfe..7c28d81cb6 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -453,7 +453,25 @@ function RemoteFunctions(config = {}) { // 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. - function _measureOverlay(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; @@ -461,7 +479,7 @@ function RemoteFunctions(config = {}) { const cs = window.getComputedStyle(element); return { bounds: bounds, - scroll: LivePreviewView.screenOffset(element), + scroll: _offsetFromBounds(bounds, bodyOffset || _bodyOffsetContext()), bt: parseFloat(cs.borderTopWidth) || 0, br: parseFloat(cs.borderRightWidth) || 0, bb: parseFloat(cs.borderBottomWidth) || 0, @@ -477,6 +495,15 @@ function RemoteFunctions(config = {}) { }; } + 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) { @@ -603,7 +630,7 @@ function RemoteFunctions(config = {}) { fresh.push(element); } } - const measured = fresh.map(_measureOverlay); + const measured = _measureAll(fresh); for (let i = 0; i < fresh.length; i++) { if (this.trigger) { _trigger(fresh[i], "highlight", 1); @@ -653,7 +680,7 @@ function RemoteFunctions(config = {}) { this.elements = elements; // Update all overlays in place — no DOM creation or destruction - const measured = elements.map(_measureOverlay); + const measured = _measureAll(elements); for (let i = 0; i < elements.length; i++) { _paintOverlay(this._overlays[i], elements[i], measured[i]); } From 777fb098a38b9f1b68462ab25c1657fa7ddf21bd Mon Sep 17 00:00:00 2001 From: Pluto Date: Wed, 9 Sep 2026 15:49:04 +0530 Subject: [PATCH 11/17] perf: dedupe highlight elements through a set --- src/LiveDevelopment/BrowserScripts/RemoteFunctions.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 7c28d81cb6..832c722bbf 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -622,11 +622,12 @@ function RemoteFunctions(config = {}) { }, 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 && !this.elements.includes(element) && - fresh.indexOf(element) === -1) { + if (element !== window.document && !seen.has(element)) { + seen.add(element); fresh.push(element); } } From 2339021980daa0f3bf30b0eb193aac0c83a570ae Mon Sep 17 00:00:00 2001 From: Pluto Date: Thu, 10 Sep 2026 13:08:08 +0530 Subject: [PATCH 12/17] chore: auto update api docs --- docs/API-Reference/language/CSSUtils.md | 74 ++++++++++++------------- 1 file changed, 37 insertions(+), 37 deletions(-) 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 From 01b56e77410c46a4a92b35bb85b338ef50d3c5ca Mon Sep 17 00:00:00 2001 From: Pluto Date: Thu, 10 Sep 2026 13:11:46 +0530 Subject: [PATCH 13/17] perf: reuse cached tokens while scanning back for a selector --- src/language/CSSUtils.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/language/CSSUtils.js b/src/language/CSSUtils.js index 5302c120bf..c6aaeb2526 100644 --- a/src/language/CSSUtils.js +++ b/src/language/CSSUtils.js @@ -1665,7 +1665,7 @@ define(function (require, exports, module) { } } - if (!TokenUtils.movePrevToken(ctx)) { + if (!TokenUtils.movePrevToken(ctx, false)) { return; } } @@ -1677,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") { @@ -1698,7 +1698,7 @@ define(function (require, exports, module) { selector = ctx.token.string + selector; } - if (!TokenUtils.movePrevToken(ctx)) { + if (!TokenUtils.movePrevToken(ctx, false)) { break; } } @@ -1752,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)); From 7f8d0d47391ed390bcddde2286cfbdcf36b31f15 Mon Sep 17 00:00:00 2001 From: Pluto Date: Thu, 10 Sep 2026 13:12:10 +0530 Subject: [PATCH 14/17] perf: move the live preview highlight to once the caret settles --- .../documents/LiveDocument.js | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/LiveDevelopment/MultiBrowserImpl/documents/LiveDocument.js b/src/LiveDevelopment/MultiBrowserImpl/documents/LiveDocument.js index 0d8d745063..06f95015cb 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,6 +170,7 @@ define(function (require, exports, module) { */ LiveDocument.prototype._detachFromEditor = function () { if (this.editor) { + this._cancelPendingHighlight(); this.hideHighlight(); this.editor.off("cursorActivity", this._onCursorActivity); } @@ -197,11 +202,23 @@ define(function (require, exports, module) { * @param {Editor} editor */ LiveDocument.prototype._onCursorActivity = function (event, editor) { - if (!this.editor) { + if (!this.editor || _disableHighlightOnCursor) { return; } - if(!_disableHighlightOnCursor){ - this.updateHighlight(); + const self = this; + this._cancelPendingHighlight(); + this._highlightTimer = window.setTimeout(function () { + self._highlightTimer = null; + if (self.editor) { + self.updateHighlight(); + } + }, CURSOR_HIGHLIGHT_DEBOUNCE_MS); + }; + + LiveDocument.prototype._cancelPendingHighlight = function () { + if (this._highlightTimer) { + window.clearTimeout(this._highlightTimer); + this._highlightTimer = null; } }; From da99d9f4dc52a4cae6da4a8c80660302f2505ba9 Mon Sep 17 00:00:00 2001 From: Pluto Date: Thu, 10 Sep 2026 13:12:28 +0530 Subject: [PATCH 15/17] perf: skip the hide call when nothing is highlighted --- src/LiveDevelopment/MultiBrowserImpl/documents/LiveDocument.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/LiveDevelopment/MultiBrowserImpl/documents/LiveDocument.js b/src/LiveDevelopment/MultiBrowserImpl/documents/LiveDocument.js index 06f95015cb..2f42f75bef 100644 --- a/src/LiveDevelopment/MultiBrowserImpl/documents/LiveDocument.js +++ b/src/LiveDevelopment/MultiBrowserImpl/documents/LiveDocument.js @@ -311,6 +311,9 @@ define(function (require, exports, module) { */ LiveDocument.prototype.hideHighlight = function (temporary) { if (!temporary) { + if (this._lastHighlight === null) { + return; + } this._lastHighlight = null; } this.protocol.evaluate("_LD.hideHighlight()"); From ebd3d173cbbea444b923fd98b42220f2c7d2a77e Mon Sep 17 00:00:00 2001 From: Pluto Date: Thu, 10 Sep 2026 15:14:05 +0530 Subject: [PATCH 16/17] fix: deferred source lookup to improve performance --- .../BrowserScripts/RemoteFunctions.js | 18 ++++++++---------- .../MultiBrowserImpl/documents/LiveDocument.js | 17 ++++++++++++----- src/utils/TokenUtils.js | 4 +++- 3 files changed, 23 insertions(+), 16 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 832c722bbf..3272d1f8ad 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -1192,15 +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. - highlightAll(nodes); - - 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 @@ -1208,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 2f42f75bef..5401a88df9 100644 --- a/src/LiveDevelopment/MultiBrowserImpl/documents/LiveDocument.js +++ b/src/LiveDevelopment/MultiBrowserImpl/documents/LiveDocument.js @@ -177,6 +177,7 @@ define(function (require, exports, module) { }; let _disableHighlightOnCursor = false; + let _cursorHighlightGeneration = 0; /** * If tur, it will disable highlights in live preview on cursor movement in editor @@ -185,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(); + } }; /** @@ -202,14 +209,15 @@ define(function (require, exports, module) { * @param {Editor} editor */ LiveDocument.prototype._onCursorActivity = function (event, editor) { + this._cancelPendingHighlight(); if (!this.editor || _disableHighlightOnCursor) { return; } const self = this; - this._cancelPendingHighlight(); + const generation = _cursorHighlightGeneration; this._highlightTimer = window.setTimeout(function () { self._highlightTimer = null; - if (self.editor) { + if (self.editor && !_disableHighlightOnCursor && generation === _cursorHighlightGeneration) { self.updateHighlight(); } }, CURSOR_HIGHLIGHT_DEBOUNCE_MS); @@ -311,11 +319,10 @@ define(function (require, exports, module) { */ LiveDocument.prototype.hideHighlight = function (temporary) { if (!temporary) { - if (this._lastHighlight === null) { - return; - } 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/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) }; } From c74fadf9ea75ad29ff511425eded192b9dc927c0 Mon Sep 17 00:00:00 2001 From: Pluto Date: Thu, 10 Sep 2026 15:18:31 +0530 Subject: [PATCH 17/17] build: update pro deps --- tracking-repos.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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" } }