Web Inspector: Styles: variable swatch not shown for var() with a fallback
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / SpreadsheetStyleProperty.js
index c518b23..ed1d7b5 100644 (file)
@@ -25,7 +25,7 @@
 
 WI.SpreadsheetStyleProperty = class SpreadsheetStyleProperty extends WI.Object
 {
-    constructor(delegate, property, index)
+    constructor(delegate, property, options = {})
     {
         super();
 
@@ -33,35 +33,92 @@ WI.SpreadsheetStyleProperty = class SpreadsheetStyleProperty extends WI.Object
 
         this._delegate = delegate || null;
         this._property = property;
+        this._readOnly = options.readOnly || false;
         this._element = document.createElement("div");
-        this._element.dataset.propertyIndex = index;
 
         this._contentElement = null;
         this._nameElement = null;
         this._valueElement = null;
+        this._jumpToEffectivePropertyButton = null;
 
         this._nameTextField = null;
         this._valueTextField = null;
 
-        this._property.__propertyView = this;
-
+        this._selected = false;
         this._hasInvalidVariableValue = false;
 
-        this._update();
+        this.update();
         property.addEventListener(WI.CSSProperty.Event.OverriddenStatusChanged, this.updateStatus, this);
         property.addEventListener(WI.CSSProperty.Event.Changed, this.updateStatus, this);
+
+        if (!this._readOnly) {
+            this._element.tabIndex = -1;
+            property.addEventListener(WI.CSSProperty.Event.ModifiedChanged, this.updateStatus, this);
+
+            this._element.addEventListener("blur", (event) => {
+                // Keep selection after tabbing out of Web Inspector window and back.
+                if (document.activeElement === this._element)
+                    return;
+
+                if (this._delegate.spreadsheetStylePropertyBlur)
+                    this._delegate.spreadsheetStylePropertyBlur(event, this);
+            });
+
+            this._element.addEventListener("mouseenter", (event) => {
+                if (this._delegate.spreadsheetStylePropertyMouseEnter)
+                    this._delegate.spreadsheetStylePropertyMouseEnter(event, this);
+            });
+
+            new WI.KeyboardShortcut(WI.KeyboardShortcut.Modifier.CommandOrControl, WI.KeyboardShortcut.Key.Slash, () => {
+                this._toggle();
+                this._select();
+            }, this._element);
+        }
     }
 
     // Public
 
     get element() { return this._element; }
-    get nameTextField() { return this._nameTextField; }
-    get valueTextField() { return this._valueTextField; }
+    get property() { return this._property; }
+    get enabled() { return this._property.enabled; }
 
-    detached()
+    set index(index)
+    {
+        this._element.dataset.propertyIndex = index;
+    }
+
+    get selected()
+    {
+        return this._selected;
+    }
+
+    set selected(value)
+    {
+        if (value === this._selected)
+            return;
+
+        this._selected = value;
+        this.updateStatus();
+    }
+
+    startEditingName()
     {
-        this._property.__propertyView = null;
+        if (!this._nameTextField)
+            return;
+
+        this._nameTextField.startEditing();
+    }
+
+    startEditingValue()
+    {
+        if (!this._valueTextField)
+            return;
 
+        this._valueTextField.startEditing();
+    }
+
+    detached()
+    {
         if (this._nameTextField)
             this._nameTextField.detached();
 
@@ -69,9 +126,120 @@ WI.SpreadsheetStyleProperty = class SpreadsheetStyleProperty extends WI.Object
             this._valueTextField.detached();
     }
 
-    highlight()
+    hidden()
+    {
+        if (this._nameTextField && this._nameTextField.editing)
+            this._nameTextField.element.blur();
+        else if (this._valueTextField && this._valueTextField.editing)
+            this._valueTextField.element.blur();
+    }
+
+    remove(replacement = null)
+    {
+        if (this._delegate && typeof this._delegate.spreadsheetStylePropertyWillRemove === "function")
+            this._delegate.spreadsheetStylePropertyWillRemove(this);
+
+        this.element.remove();
+
+        if (replacement)
+            this._property.replaceWithText(replacement);
+        else
+            this._property.remove();
+
+        this.detached();
+    }
+
+    update()
     {
-        this._element.classList.add("highlighted");
+        this.element.removeChildren();
+
+        if (this._isEditable()) {
+            this._checkboxElement = this.element.appendChild(document.createElement("input"));
+            this._checkboxElement.classList.add("property-toggle");
+            this._checkboxElement.type = "checkbox";
+            this._checkboxElement.checked = this._property.enabled;
+            this._checkboxElement.tabIndex = -1;
+            this._checkboxElement.addEventListener("click", (event) => {
+                event.stopPropagation();
+                this._toggle();
+                console.assert(this._checkboxElement.checked === this._property.enabled);
+            });
+        }
+
+        this._contentElement = this.element.appendChild(document.createElement("span"));
+        this._contentElement.className = "content";
+
+        if (!this._property.enabled)
+            this._contentElement.append("/* ");
+
+        this._nameElement = this._contentElement.appendChild(document.createElement("span"));
+        this._nameElement.classList.add("name");
+        this._nameElement.textContent = this._property.name;
+
+        let colonElement = this._contentElement.appendChild(document.createElement("span"));
+        colonElement.classList.add("colon");
+        colonElement.textContent = ": ";
+
+        this._valueElement = this._contentElement.appendChild(document.createElement("span"));
+        this._valueElement.classList.add("value");
+        this._renderValue(this._property.rawValue);
+
+        if (this._isEditable() && this._property.enabled) {
+            this._nameElement.tabIndex = 0;
+            this._nameElement.addEventListener("beforeinput", this._handleNameBeforeInput.bind(this));
+            this._nameElement.addEventListener("paste", this._handleNamePaste.bind(this));
+
+            this._nameTextField = new WI.SpreadsheetTextField(this, this._nameElement, this._nameCompletionDataProvider.bind(this));
+
+            this._valueElement.tabIndex = 0;
+            this._valueElement.addEventListener("beforeinput", this._handleValueBeforeInput.bind(this));
+
+            this._valueTextField = new WI.SpreadsheetTextField(this, this._valueElement, this._valueCompletionDataProvider.bind(this));
+        }
+
+        if (this._isEditable()) {
+            this._setupJumpToSymbol(this._nameElement);
+            this._setupJumpToSymbol(this._valueElement);
+        }
+
+        let semicolonElement = this._contentElement.appendChild(document.createElement("span"));
+        semicolonElement.classList.add("semicolon");
+        semicolonElement.textContent = ";";
+
+        if (this._property.enabled) {
+            this._warningElement = this.element.appendChild(document.createElement("span"));
+            this._warningElement.className = "warning";
+        } else
+            this._contentElement.append(" */");
+
+        if (!this._property.implicit && this._property.ownerStyle.type === WI.CSSStyleDeclaration.Type.Computed) {
+            let effectiveProperty = this._property.ownerStyle.nodeStyles.effectivePropertyForName(this._property.name);
+            if (effectiveProperty && !effectiveProperty.styleSheetTextRange)
+                effectiveProperty = effectiveProperty.relatedShorthandProperty;
+
+            let ownerRule = effectiveProperty ? effectiveProperty.ownerStyle.ownerRule : null;
+
+            let arrowElement = this._contentElement.appendChild(WI.createGoToArrowButton());
+            arrowElement.addEventListener("click", (event) => {
+                if (!effectiveProperty || !ownerRule || !event.altKey) {
+                    if (this._delegate.spreadsheetStylePropertyShowProperty)
+                        this._delegate.spreadsheetStylePropertyShowProperty(this, this._property);
+                    return;
+                }
+
+                let sourceCode = ownerRule.sourceCodeLocation.sourceCode;
+                let {startLine, startColumn} = effectiveProperty.styleSheetTextRange;
+                WI.showSourceCodeLocation(sourceCode.createSourceCodeLocation(startLine, startColumn), {
+                    ignoreNetworkTab: true,
+                    ignoreSearchTab: true,
+                });
+            });
+
+            if (effectiveProperty && ownerRule)
+                arrowElement.title = WI.UIString("Option-click to show source");
+        }
+
+        this.updateStatus();
     }
 
     updateStatus()
@@ -79,7 +247,7 @@ WI.SpreadsheetStyleProperty = class SpreadsheetStyleProperty extends WI.Object
         let duplicatePropertyExistsBelow = (cssProperty) => {
             let propertyFound = false;
 
-            for (let property of this._property.ownerStyle.properties) {
+            for (let property of this._property.ownerStyle.enabledProperties) {
                 if (property === cssProperty)
                     propertyFound = true;
                 else if (property.name === cssProperty.name && propertyFound)
@@ -93,6 +261,22 @@ WI.SpreadsheetStyleProperty = class SpreadsheetStyleProperty extends WI.Object
         let elementTitle = "";
 
         if (this._property.overridden) {
+            if (!this._jumpToEffectivePropertyButton && this._delegate && this._delegate.spreadsheetStylePropertySelectByProperty && WI.settings.experimentalEnableStylesJumpToEffective.value) {
+                console.assert(this._property.overridingProperty, `Overridden property is missing overridingProperty: ${this._property.formattedText}`);
+                if (this._property.overridingProperty) {
+                    this._jumpToEffectivePropertyButton = WI.createGoToArrowButton();
+                    this._jumpToEffectivePropertyButton.classList.add("select-effective-property");
+                    this._jumpToEffectivePropertyButton.dataset.value = this._property.overridingProperty.rawValue;
+                    this._element.append(this._jumpToEffectivePropertyButton);
+
+                    this._jumpToEffectivePropertyButton.addEventListener("click", (event) => {
+                        console.assert(this._property.overridingProperty);
+                        event.stop();
+                        this._delegate.spreadsheetStylePropertySelectByProperty(this._property.overridingProperty);
+                    });
+                }
+            }
+
             classNames.push("overridden");
             if (duplicatePropertyExistsBelow(this._property)) {
                 classNames.push("has-warning");
@@ -127,6 +311,12 @@ WI.SpreadsheetStyleProperty = class SpreadsheetStyleProperty extends WI.Object
         if (!this._property.enabled)
             classNames.push("disabled");
 
+        if (this._property.modified && this._property.name && this._property.rawValue)
+            classNames.push("modified");
+
+        if (this._selected)
+            classNames.push("selected");
+
         this._element.className = classNames.join(" ");
         this._element.title = elementTitle;
     }
@@ -134,86 +324,16 @@ WI.SpreadsheetStyleProperty = class SpreadsheetStyleProperty extends WI.Object
     applyFilter(filterText)
     {
         let matchesName = this._nameElement.textContent.includes(filterText);
+        this._nameElement.classList.toggle(WI.GeneralStyleDetailsSidebarPanel.FilterMatchSectionClassName, !!matchesName);
+
         let matchesValue = this._valueElement.textContent.includes(filterText);
+        this._valueElement.classList.toggle(WI.GeneralStyleDetailsSidebarPanel.FilterMatchSectionClassName, !!matchesValue);
+
         let matches = matchesName || matchesValue;
-        this._contentElement.classList.toggle(WI.GeneralStyleDetailsSidebarPanel.FilterMatchSectionClassName, matches);
         this._contentElement.classList.toggle(WI.GeneralStyleDetailsSidebarPanel.NoFilterMatchInPropertyClassName, !matches);
         return matches;
     }
 
-    // Private
-
-    _remove()
-    {
-        this.element.remove();
-        this._property.remove();
-        this.detached();
-
-        if (this._delegate && typeof this._delegate.spreadsheetStylePropertyRemoved === "function")
-            this._delegate.spreadsheetStylePropertyRemoved(this);
-    }
-
-    _update()
-    {
-        this.element.removeChildren();
-
-        if (this._property.editable) {
-            this._checkboxElement = this.element.appendChild(document.createElement("input"));
-            this._checkboxElement.classList.add("property-toggle");
-            this._checkboxElement.type = "checkbox";
-            this._checkboxElement.checked = this._property.enabled;
-            this._checkboxElement.tabIndex = -1;
-            this._checkboxElement.addEventListener("click", (event) => {
-                event.stopPropagation();
-                let disabled = !this._checkboxElement.checked;
-                this._property.commentOut(disabled);
-                this._update();
-            });
-        }
-
-        this._contentElement = this.element.appendChild(document.createElement("span"));
-
-        if (!this._property.enabled)
-            this._contentElement.append("/* ");
-
-        this._nameElement = this._contentElement.appendChild(document.createElement("span"));
-        this._nameElement.classList.add("name");
-        this._nameElement.textContent = this._property.name;
-
-        this._contentElement.append(": ");
-
-        this._valueElement = this._contentElement.appendChild(document.createElement("span"));
-        this._valueElement.classList.add("value");
-        this._renderValue(this._property.rawValue);
-
-        if (this._property.editable && this._property.enabled) {
-            this._nameElement.tabIndex = 0;
-            this._nameElement.addEventListener("beforeinput", this._handleNameBeforeInput.bind(this));
-
-            this._nameTextField = new WI.SpreadsheetTextField(this, this._nameElement, this._nameCompletionDataProvider.bind(this));
-
-            this._valueElement.tabIndex = 0;
-            this._valueElement.addEventListener("beforeinput", this._handleValueBeforeInput.bind(this));
-
-            this._valueTextField = new WI.SpreadsheetTextField(this, this._valueElement, this._valueCompletionDataProvider.bind(this));
-        }
-
-        if (this._property.editable) {
-            this._setupJumpToSymbol(this._nameElement);
-            this._setupJumpToSymbol(this._valueElement);
-        }
-
-        this._contentElement.append(";");
-
-        if (this._property.enabled) {
-            this._warningElement = this.element.appendChild(document.createElement("span"));
-            this._warningElement.className = "warning";
-        } else
-            this._contentElement.append(" */");
-
-        this.updateStatus();
-    }
-
     // SpreadsheetTextField delegate
 
     spreadsheetTextFieldWillStartEditing(textField)
@@ -225,23 +345,21 @@ WI.SpreadsheetStyleProperty = class SpreadsheetStyleProperty extends WI.Object
     spreadsheetTextFieldDidChange(textField)
     {
         if (textField === this._valueTextField)
-            this.debounce(WI.SpreadsheetStyleProperty.CommitCoalesceDelay)._handleValueChange();
+            this._handleValueChange();
         else if (textField === this._nameTextField)
-            this.debounce(WI.SpreadsheetStyleProperty.CommitCoalesceDelay)._handleNameChange();
+            this._handleNameChange();
     }
 
     spreadsheetTextFieldDidCommit(textField, {direction})
     {
-        let propertyName = this._nameTextField.value.trim();
-        let propertyValue = this._valueTextField.value.trim();
         let willRemoveProperty = false;
         let isEditingName = textField === this._nameTextField;
 
-        if (!propertyName || (!propertyValue && !isEditingName && direction === "forward"))
+        if (!this._property.name || (!this._property.rawValue && !isEditingName && direction === "forward"))
             willRemoveProperty = true;
 
         if (!isEditingName && !willRemoveProperty)
-            this._renderValue(propertyValue);
+            this._renderValue(this._property.rawValue);
 
         if (direction === "forward") {
             if (isEditingName && !willRemoveProperty) {
@@ -257,25 +375,31 @@ WI.SpreadsheetStyleProperty = class SpreadsheetStyleProperty extends WI.Object
             }
         }
 
-        if (typeof this._delegate.spreadsheetCSSStyleDeclarationEditorFocusMoved === "function") {
+        if (typeof this._delegate.spreadsheetStylePropertyFocusMoved === "function") {
             // Move focus away from the current property, to the next or previous one, if exists, or to the next or previous rule, if exists.
-            this._delegate.spreadsheetCSSStyleDeclarationEditorFocusMoved({direction, willRemoveProperty, movedFromProperty: this});
+            this._delegate.spreadsheetStylePropertyFocusMoved(this, {direction, willRemoveProperty});
         }
 
         if (willRemoveProperty)
-            this._remove();
+            this.remove();
     }
 
-    spreadsheetTextFieldDidBlur(textField, event)
+    spreadsheetTextFieldDidBlur(textField, event, changed)
     {
         let focusedOutsideThisProperty = event.relatedTarget !== this._nameElement && event.relatedTarget !== this._valueElement;
         if (focusedOutsideThisProperty && (!this._nameTextField.value.trim() || !this._valueTextField.value.trim())) {
-            this._remove();
+            this.remove();
             return;
         }
 
         if (textField === this._valueTextField)
-            this._renderValue(this._valueElement.textContent);
+            this._renderValue(this._property.rawValue);
+
+        if (typeof this._delegate.spreadsheetStylePropertyFocusMoved === "function")
+            this._delegate.spreadsheetStylePropertyFocusMoved(this, {direction: null});
+
+        if (changed && window.DOMAgent)
+            DOMAgent.markUndoableState();
     }
 
     spreadsheetTextFieldDidBackspace(textField)
@@ -286,8 +410,36 @@ WI.SpreadsheetStyleProperty = class SpreadsheetStyleProperty extends WI.Object
             this._nameTextField.startEditing();
     }
 
+    spreadsheetTextFieldDidPressEsc(textField, textBeforeEditing)
+    {
+        let isNewProperty = !textBeforeEditing;
+        if (isNewProperty)
+            this.remove();
+        else if (this._delegate.spreadsheetStylePropertyDidPressEsc)
+            this._delegate.spreadsheetStylePropertyDidPressEsc(this);
+    }
+
     // Private
 
+    _toggle()
+    {
+        this._property.commentOut(this.property.enabled);
+        this.update();
+    }
+
+    _select()
+    {
+        if (this._delegate && this._delegate.spreadsheetStylePropertySelect) {
+            let index = parseInt(this._element.dataset.propertyIndex);
+            this._delegate.spreadsheetStylePropertySelect(index);
+        }
+    }
+
+    _isEditable()
+    {
+        return !this._readOnly && this._property.editable;
+    }
+
     _renderValue(value)
     {
         this._hasInvalidVariableValue = false;
@@ -295,14 +447,8 @@ WI.SpreadsheetStyleProperty = class SpreadsheetStyleProperty extends WI.Object
         const maxValueLength = 150;
         let tokens = WI.tokenizeCSSValue(value);
 
-        if (this._property.enabled) {
-            // FIXME: <https://webkit.org/b/178636> Web Inspector: Styles: Make inline widgets work with CSS functions (var(), calc(), etc.)
-            tokens = this._addGradientTokens(tokens);
-            tokens = this._addColorTokens(tokens);
-            tokens = this._addTimingFunctionTokens(tokens, "cubic-bezier");
-            tokens = this._addTimingFunctionTokens(tokens, "spring");
-            tokens = this._addVariableTokens(tokens);
-        }
+        if (this._property.enabled)
+            tokens = this._replaceSpecialTokens(tokens);
 
         tokens = tokens.map((token) => {
             if (token instanceof Element)
@@ -322,7 +468,11 @@ WI.SpreadsheetStyleProperty = class SpreadsheetStyleProperty extends WI.Object
             if (className) {
                 let span = document.createElement("span");
                 span.classList.add(className);
-                span.textContent = token.value.trimMiddle(maxValueLength);
+                span.textContent = token.value.truncateMiddle(maxValueLength);
+
+                if (token.type && token.type.includes("link"))
+                    span.addEventListener("contextmenu", this._handleLinkContextMenu.bind(this, token));
+
                 return span;
             }
 
@@ -333,13 +483,20 @@ WI.SpreadsheetStyleProperty = class SpreadsheetStyleProperty extends WI.Object
         this._valueElement.append(...tokens);
     }
 
-    _createInlineSwatch(type, text, valueObject)
+    _createInlineSwatch(type, contents, valueObject)
     {
         let tokenElement = document.createElement("span");
         let innerElement = document.createElement("span");
-        innerElement.textContent = text;
+        for (let item of contents) {
+            if (item instanceof Node)
+                innerElement.appendChild(item);
+            else if (typeof item === "object")
+                innerElement.append(item.value);
+            else
+                innerElement.append(item);
+        }
 
-        let readOnly = !this._property.editable;
+        let readOnly = !this._isEditable();
         let swatch = new WI.InlineSwatch(type, valueObject, readOnly);
 
         swatch.addEventListener(WI.InlineSwatch.Event.ValueChanged, (event) => {
@@ -349,16 +506,53 @@ WI.SpreadsheetStyleProperty = class SpreadsheetStyleProperty extends WI.Object
 
             innerElement.textContent = value;
             this._handleValueChange();
+
+            if (type === WI.InlineSwatch.Type.Variable)
+                this._renderValue(this._property.rawValue);
         }, this);
 
-        tokenElement.append(swatch.element, innerElement);
+        if (type === WI.InlineSwatch.Type.Variable) {
+            swatch.value = () => {
+                return this._property.ownerStyle.nodeStyles.computedStyle.resolveVariableValue(innerElement.textContent);
+            };
+        }
+
+        if (this._delegate && typeof this._delegate.stylePropertyInlineSwatchActivated === "function") {
+            swatch.addEventListener(WI.InlineSwatch.Event.Activated, () => {
+                this._delegate.stylePropertyInlineSwatchActivated();
+            });
+        }
+
+        if (this._delegate && typeof this._delegate.stylePropertyInlineSwatchDeactivated === "function") {
+            swatch.addEventListener(WI.InlineSwatch.Event.Deactivated, () => {
+                this._delegate.stylePropertyInlineSwatchDeactivated();
+            });
+        }
 
-        // Prevent the value from editing when clicking on the swatch.
-        swatch.element.addEventListener("mousedown", (event) => { event.stop(); });
+        tokenElement.append(swatch.element, innerElement);
 
         return tokenElement;
     }
 
+    _replaceSpecialTokens(tokens)
+    {
+        // FIXME: <https://webkit.org/b/178636> Web Inspector: Styles: Make inline widgets work with CSS functions (var(), calc(), etc.)
+
+        tokens = this._addVariableTokens(tokens);
+
+        if (this._property.variable || WI.CSSKeywordCompletions.isColorAwareProperty(this._property.name)) {
+            tokens = this._addGradientTokens(tokens);
+            tokens = this._addColorTokens(tokens);
+        }
+
+        if (this._property.variable || WI.CSSKeywordCompletions.isTimingFunctionAwareProperty(this._property.name)) {
+            tokens = this._addTimingFunctionTokens(tokens, "cubic-bezier");
+            tokens = this._addTimingFunctionTokens(tokens, "spring");
+        }
+
+        return tokens;
+    }
+
     _addGradientTokens(tokens)
     {
         let gradientRegex = /^(repeating-)?(linear|radial)-gradient$/i;
@@ -384,7 +578,7 @@ WI.SpreadsheetStyleProperty = class SpreadsheetStyleProperty extends WI.Object
                 let text = rawTokens.map((token) => token.value).join("");
                 let gradient = WI.Gradient.fromString(text);
                 if (gradient)
-                    newTokens.push(this._createInlineSwatch(WI.InlineSwatch.Type.Gradient, text, gradient));
+                    newTokens.push(this._createInlineSwatch(WI.InlineSwatch.Type.Gradient, rawTokens, gradient));
                 else
                     newTokens.push(...rawTokens);
 
@@ -403,7 +597,7 @@ WI.SpreadsheetStyleProperty = class SpreadsheetStyleProperty extends WI.Object
         let pushPossibleColorToken = (text, ...rawTokens) => {
             let color = WI.Color.fromString(text);
             if (color)
-                newTokens.push(this._createInlineSwatch(WI.InlineSwatch.Type.Color, text, color));
+                newTokens.push(this._createInlineSwatch(WI.InlineSwatch.Type.Color, rawTokens, color));
             else
                 newTokens.push(...rawTokens);
         };
@@ -418,7 +612,7 @@ WI.SpreadsheetStyleProperty = class SpreadsheetStyleProperty extends WI.Object
             } else if (WI.Color.FunctionNames.has(token.value) && token.type && (token.type.includes("atom") || token.type.includes("keyword"))) {
                 // Color Function start
                 colorFunctionStartIndex = i;
-            } else if (isNaN(colorFunctionStartIndex) && token.type && token.type.includes("keyword")) {
+            } else if (isNaN(colorFunctionStartIndex) && token.type && (token.type.includes("atom") || token.type.includes("keyword"))) {
                 // Color keyword
                 pushPossibleColorToken(token.value, token);
             } else if (!isNaN(colorFunctionStartIndex)) {
@@ -470,7 +664,7 @@ WI.SpreadsheetStyleProperty = class SpreadsheetStyleProperty extends WI.Object
                 }
 
                 if (valueObject)
-                    newTokens.push(this._createInlineSwatch(inlineSwatchType, text, valueObject));
+                    newTokens.push(this._createInlineSwatch(inlineSwatchType, rawTokens, valueObject));
                 else
                     newTokens.push(...rawTokens);
 
@@ -491,8 +685,10 @@ WI.SpreadsheetStyleProperty = class SpreadsheetStyleProperty extends WI.Object
         for (let i = 0; i < tokens.length; i++) {
             let token = tokens[i];
             if (token.value === "var" && token.type && token.type.includes("atom")) {
-                startIndex = i;
-                openParenthesis = 0;
+                if (isNaN(startIndex)) {
+                    startIndex = i;
+                    openParenthesis = 0;
+                }
             } else if (token.value === "(" && !isNaN(startIndex))
                 ++openParenthesis;
             else if (token.value === ")" && !isNaN(startIndex)) {
@@ -501,14 +697,22 @@ WI.SpreadsheetStyleProperty = class SpreadsheetStyleProperty extends WI.Object
                     continue;
 
                 let rawTokens = tokens.slice(startIndex, i + 1);
-                let tokenValues = rawTokens.map((token) => token.value);
-                let variableName = tokenValues.find((value, i) => value.startsWith("--") && /\bvariable-2\b/.test(rawTokens[i].type));
-
-                const dontCreateIfMissing = true;
-                let variableProperty = this._property.ownerStyle.nodeStyles.computedStyle.propertyForName(variableName, dontCreateIfMissing);
-                if (variableProperty) {
-                    let valueObject = variableProperty.value.trim();
-                    newTokens.push(this._createInlineSwatch(WI.InlineSwatch.Type.Variable, tokenValues.join(""), valueObject));
+                let variableNameIndex = rawTokens.findIndex((token) => token.value.startsWith("--") && /\bvariable-2\b/.test(token.type));
+                if (variableNameIndex !== -1) {
+                    let contents = [];
+                    let fallbackStartIndex = rawTokens.findIndex((value, i) => i > variableNameIndex + 1 && /\bm-css\b/.test(value.type));
+                    if (fallbackStartIndex !== -1) {
+                        contents = contents.concat(rawTokens.slice(0, fallbackStartIndex));
+                        contents = contents.concat(this._replaceSpecialTokens(rawTokens.slice(fallbackStartIndex, i)));
+                    } else
+                        contents = contents.concat(rawTokens.slice(0, i));
+                    contents.push(token);
+
+                    let text = rawTokens.reduce((accumulator, token) => accumulator + token.value, "");
+                    if (this._property.ownerStyle.nodeStyles.computedStyle.resolveVariableValue(text))
+                        newTokens.push(this._createInlineSwatch(WI.InlineSwatch.Type.Variable, contents));
+                    else
+                        newTokens = newTokens.concat(contents);
                 } else {
                     this._hasInvalidVariableValue = true;
                     newTokens.push(...rawTokens);
@@ -542,9 +746,31 @@ WI.SpreadsheetStyleProperty = class SpreadsheetStyleProperty extends WI.Object
         this._valueTextField.startEditing();
     }
 
-    _nameCompletionDataProvider(prefix)
+    _handleNamePaste(event)
+    {
+        let text = event.clipboardData.getData("text/plain");
+        if (!text || !text.includes(":"))
+            return;
+
+        event.preventDefault();
+
+        this.remove(text);
+
+        if (this._delegate.spreadsheetStylePropertyAddBlankPropertySoon) {
+            this._delegate.spreadsheetStylePropertyAddBlankPropertySoon(this, {
+                index: parseInt(this._element.dataset.propertyIndex) + 1,
+            });
+        }
+    }
+
+    _nameCompletionDataProvider(prefix, options = {})
     {
-        return WI.CSSCompletions.cssNameCompletions.startsWith(prefix);
+        let completions;
+        if (!prefix && options.allowEmptyPrefix)
+            completions = WI.CSSCompletions.cssNameCompletions.values;
+        else
+            completions = WI.CSSCompletions.cssNameCompletions.startsWith(prefix);
+        return {prefix, completions};
     }
 
     _handleValueBeforeInput(event)
@@ -557,20 +783,8 @@ WI.SpreadsheetStyleProperty = class SpreadsheetStyleProperty extends WI.Object
         if (!selection.rangeCount || selection.getRangeAt(0).endOffset !== text.length)
             return;
 
-        // Find the first and last index (if any) of a quote character to ensure that the string
-        // doesn't contain unbalanced quotes. If so, then there's no way that the semicolon could be
-        // part of a string within the value, so we can assume that it's the property "terminator".
-        const quoteRegex = /["']/g;
-        let start = -1;
-        let end = text.length;
-        let match = null;
-        while (match = quoteRegex.exec(text)) {
-            if (start < 0)
-                start = match.index;
-            end = match.index + 1;
-        }
-
-        if (start !== -1 && !text.substring(start, end).hasMatchingEscapedQuotes())
+        let unbalancedCharacters = WI.CSSCompletions.completeUnbalancedValue(text);
+        if (unbalancedCharacters)
             return;
 
         event.preventDefault();
@@ -580,8 +794,19 @@ WI.SpreadsheetStyleProperty = class SpreadsheetStyleProperty extends WI.Object
 
     _valueCompletionDataProvider(prefix)
     {
+        // For "border: 1px so|", we want to suggest "solid" based on "so" prefix.
+        let match = prefix.match(/[a-z0-9()-]+$/i);
+
+        // Clicking on the value of `height: 100%` shouldn't show any completions.
+        if (!match && prefix)
+            return {completions: [], prefix: ""};
+
+        prefix = match ? match[0] : "";
         let propertyName = this._nameElement.textContent.trim();
-        return WI.CSSKeywordCompletions.forProperty(propertyName).startsWith(prefix);
+        return {
+            prefix,
+            completions: WI.CSSKeywordCompletions.forProperty(propertyName).startsWith(prefix)
+        };
     }
 
     _setupJumpToSymbol(element)
@@ -612,8 +837,53 @@ WI.SpreadsheetStyleProperty = class SpreadsheetStyleProperty extends WI.Object
             WI.showSourceCodeLocation(sourceCode.createSourceCodeLocation(range.startLine, range.startColumn), options);
         });
     }
+
+    _handleLinkContextMenu(token, event)
+    {
+        let contextMenu = WI.ContextMenu.createFromEvent(event);
+
+        let resolveURL = (url) => {
+            let ownerStyle = this._property.ownerStyle;
+            if (!ownerStyle)
+                return url;
+
+            let ownerStyleSheet = ownerStyle.ownerStyleSheet;
+            if (!ownerStyleSheet) {
+                let ownerRule = ownerStyle.ownerRule;
+                if (ownerRule)
+                    ownerStyleSheet = ownerRule.ownerStyleSheet;
+            }
+            if (ownerStyleSheet) {
+                if (ownerStyleSheet.url)
+                    return absoluteURL(url, ownerStyleSheet.url);
+
+                let parentFrame = ownerStyleSheet.parentFrame;
+                if (parentFrame)
+                    return absoluteURL(url, parentFrame.url);
+            }
+
+            let node = ownerStyle.node;
+            if (!node) {
+                let nodeStyles = ownerStyle.nodeStyles;
+                if (!nodeStyles) {
+                    let ownerRule = ownerStyle.ownerRule;
+                    if (ownerRule)
+                        nodeStyles = ownerRule.nodeStyles;
+                }
+                if (nodeStyles)
+                    node = nodeStyles.node;
+            }
+            if (node) {
+                let ownerDocument = node.ownerDocument;
+                if (ownerDocument)
+                    return absoluteURL(url, node.ownerDocument.documentURL);
+            }
+
+            return url;
+        };
+
+        WI.appendContextMenuItemsForURL(contextMenu, resolveURL(token.value));
+    }
 };
 
 WI.SpreadsheetStyleProperty.StyleClassName = "property";
-
-WI.SpreadsheetStyleProperty.CommitCoalesceDelay = 250;