Bug 1422635: Implement CSS variable autocompletion. r=jdescottes
Initial support for CSS variable autocompletion in ruleview.
MozReview-Commit-ID: AlblDmyW4Iq
--- a/devtools/client/inspector/rules/views/text-property-editor.js
+++ b/devtools/client/inspector/rules/views/text-property-editor.js
@@ -295,16 +295,17 @@ TextPropertyEditor.prototype = {
advanceChars: advanceValidate,
contentType: InplaceEditor.CONTENT_TYPES.CSS_VALUE,
property: this.prop,
defaultIncrement: this.prop.name === "opacity" ? 0.1 : 1,
popup: this.popup,
multiline: true,
maxWidth: () => this.container.getBoundingClientRect().width,
cssProperties: this.cssProperties,
+ cssVariables: this.rule.elementStyle.variables,
});
this.ruleView.highlighters.on("hover-shape-point", this._onHoverShapePoint);
}
},
/**
* Get the path from which to resolve requests for this
--- a/devtools/client/shared/inplace-editor.js
+++ b/devtools/client/shared/inplace-editor.js
@@ -117,16 +117,17 @@ function isKeyIn(key, ...keys) {
* available if multiline is true. If a function is provided, it will be
* called when replacing the element by the inplace input.
* {Boolean} trimOutput: Should the returned string be trimmed?
* defaults to true
* {Boolean} preserveTextStyles: If true, do not copy text-related styles
* from `element` to the new input.
* defaults to false
* {Object} cssProperties: An instance of CSSProperties.
+ * {Object} cssVariables: A Map object containing all CSS variables.
* {Number} defaultIncrement: The value by which the input is incremented
* or decremented by default (0.1 for properties like opacity and 1 by default)
*/
function editableField(options) {
return editableItem(options, function (element, event) {
if (!options.element.inplaceEditor) {
new InplaceEditor(options, event);
}
@@ -219,16 +220,17 @@ function getInplaceEditorForSpan(span) {
exports.getInplaceEditorForSpan = getInplaceEditorForSpan;
function InplaceEditor(options, event) {
this.elt = options.element;
let doc = this.elt.ownerDocument;
this.doc = doc;
this.elt.inplaceEditor = this;
this.cssProperties = options.cssProperties;
+ this.cssVariables = options.cssVariables || new Map();
this.change = options.change;
this.done = options.done;
this.contextMenu = options.contextMenu;
this.defaultIncrement = options.defaultIncrement || 1;
this.destroy = options.destroy;
this.initial = options.initial ? options.initial : this.elt.textContent;
this.multiline = options.multiline || false;
this.maxWidth = options.maxWidth;
@@ -1328,18 +1330,26 @@ InplaceEditor.prototype = {
// Get the last query to be completed before the caret.
let match = /([^\s,.\/]+$)/.exec(query);
if (match) {
startCheckQuery = match[0];
} else {
startCheckQuery = "";
}
- list = ["!important",
- ...this._getCSSValuesForPropertyName(this.property.name)];
+ // Check if the query to be completed is a CSS variable.
+ let varMatch = /^var\(([^\s]+$)/.exec(startCheckQuery);
+
+ if (varMatch && varMatch.length == 2) {
+ startCheckQuery = varMatch[1];
+ list = this._getCSSVariableNames();
+ } else {
+ list = ["!important",
+ ...this._getCSSValuesForPropertyName(this.property.name)];
+ }
if (query == "") {
// Do not suggest '!important' without any manually typed character.
list.splice(0, 1);
}
} else if (this.contentType == CONTENT_TYPES.CSS_MIXED &&
/^\s*style\s*=/.test(query)) {
// Check if the style attribute is closed before the selection.
@@ -1485,16 +1495,25 @@ InplaceEditor.prototype = {
* mocked suggestion lists.
*
* @param {String} propertyName
* @return {Array} array of CSS property values (Strings)
*/
_getCSSValuesForPropertyName: function (propertyName) {
return this.cssProperties.getValues(propertyName);
},
+
+ /**
+ * Returns the list of all CSS variables to use for the autocompletion.
+ *
+ * @return {Array} array of CSS variable names (Strings)
+ */
+ _getCSSVariableNames: function () {
+ return Array.from(this.cssVariables.keys()).sort();
+ },
};
/**
* Copy text-related styles from one element to another.
*/
function copyTextStyles(from, to) {
let win = from.ownerDocument.defaultView;
let style = win.getComputedStyle(from);
--- a/devtools/client/shared/test/browser.ini
+++ b/devtools/client/shared/test/browser.ini
@@ -143,16 +143,17 @@ skip-if = e10s # Bug 1221911, bug 122228
[browser_html_tooltip_variable-height.js]
[browser_html_tooltip_width-auto.js]
[browser_html_tooltip_xul-wrapper.js]
[browser_inplace-editor-01.js]
[browser_inplace-editor-02.js]
[browser_inplace-editor_autocomplete_01.js]
[browser_inplace-editor_autocomplete_02.js]
[browser_inplace-editor_autocomplete_offset.js]
+[browser_inplace-editor_autocomplete_css_variable.js]
[browser_inplace-editor_maxwidth.js]
[browser_keycodes.js]
[browser_key_shortcuts.js]
[browser_layoutHelpers.js]
skip-if = e10s # Layouthelpers test should not run in a content page.
[browser_layoutHelpers-getBoxQuads.js]
skip-if = e10s # Layouthelpers test should not run in a content page.
[browser_num-l10n.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/test/browser_inplace-editor_autocomplete_css_variable.js
@@ -0,0 +1,84 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_inplace_editor.js */
+
+"use strict";
+
+const AutocompletePopup = require("devtools/client/shared/autocomplete-popup");
+const { InplaceEditor } = require("devtools/client/shared/inplace-editor");
+loadHelperScript("helper_inplace_editor.js");
+
+// Test the inplace-editor autocomplete popup for variable suggestions.
+// Using a mocked list of CSS variables to avoid test failures linked to
+// engine changes (new property, removed property, ...).
+// Also using a mocked list of CSS properties to avoid autocompletion when
+// typing in "var"
+
+// format :
+// [
+// what key to press,
+// expected input box value after keypress,
+// selected suggestion index (-1 if popup is hidden),
+// number of suggestions in the popup (0 if popup is hidden),
+// ]
+const testData = [
+ ["v", "v", -1, 0],
+ ["a", "va", -1, 0],
+ ["r", "var", -1, 0],
+ ["(", "var(", -1, 0],
+ ["-", "var(--abc", 0, 2],
+ ["VK_BACK_SPACE", "var(-", -1, 0],
+ ["-", "var(--abc", 0, 2],
+ ["VK_DOWN", "var(--def", 1, 2],
+ ["VK_DOWN", "var(--abc", 0, 2],
+ ["VK_LEFT", "var(--abc", -1, 0],
+];
+
+const mockGetCSSValuesForPropertyName = function (propertyName) {
+ return [];
+};
+
+const mockGetCSSVariableNames = function () {
+ return [
+ "--abc",
+ "--def",
+ ]
+};
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," +
+ "inplace editor CSS variable autocomplete");
+ let [host, win, doc] = yield createHost();
+
+ let xulDocument = win.top.document;
+ let popup = new AutocompletePopup(xulDocument, { autoSelect: true });
+
+ yield new Promise(resolve => {
+ createInplaceEditorAndClick({
+ start: runAutocompletionTest,
+ contentType: InplaceEditor.CONTENT_TYPES.CSS_VALUE,
+ property: {
+ name: "color"
+ },
+ done: resolve,
+ popup: popup
+ }, doc);
+ });
+
+ popup.destroy();
+ host.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+let runAutocompletionTest = Task.async(function* (editor) {
+ info("Starting to test for css variable completion");
+ editor._getCSSValuesForPropertyName = mockGetCSSValuesForPropertyName;
+ editor._getCSSVariableNames = mockGetCSSVariableNames;
+
+ for (let data of testData) {
+ yield testCompletion(data, editor);
+ }
+
+ EventUtils.synthesizeKey("VK_RETURN", {}, editor.input.defaultView);
+});