Bug 1422635: Implement CSS variable autocompletion. r=jdescottes draft
authorRajdeep Nanua <rajdeep.nanua@mail.utoronto.ca>
Tue, 12 Dec 2017 01:47:59 -0500
changeset 723375 e2f03a2bd0c418d6a3faf99936142e479a37324b
parent 687940 d58424c244c38f88357a26fb61c333d3c6e552d7
child 746846 f5edf4bee0bbcb76377c0e9c14f90bc80d4bd6b1
push id96416
push userbmo:rajdeep.nanua@mail.utoronto.ca
push dateTue, 23 Jan 2018 03:23:23 +0000
reviewersjdescottes
bugs1422635
milestone58.0a1
Bug 1422635: Implement CSS variable autocompletion. r=jdescottes Initial support for CSS variable autocompletion in ruleview. MozReview-Commit-ID: AlblDmyW4Iq
devtools/client/inspector/rules/views/text-property-editor.js
devtools/client/shared/inplace-editor.js
devtools/client/shared/test/browser.ini
devtools/client/shared/test/browser_inplace-editor_autocomplete_css_variable.js
--- 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);
+});