Bug 1431949 - Show variable values in the CSS variable autocomplete popup. r=gl draft
authorJulian Descottes <jdescottes@mozilla.com>
Tue, 03 Apr 2018 10:34:14 +0200
changeset 776537 7eb1d554882b521521e6549523ab5db452617a4a
parent 776477 d75d996016dcf325c2db2ed8a47af512d07ffacd
child 776538 0108824983d3b1e810a1443ab7ef6895e7e6b343
child 776549 447ed7688da5263c3bb5daa0bd029511e81531fd
push id104900
push userjdescottes@mozilla.com
push dateTue, 03 Apr 2018 09:15:05 +0000
reviewersgl
bugs1431949
milestone61.0a1
Bug 1431949 - Show variable values in the CSS variable autocomplete popup. r=gl MozReview-Commit-ID: GclXYtx37kD
devtools/client/shared/autocomplete-popup.js
devtools/client/shared/inplace-editor.js
devtools/client/shared/test/browser_inplace-editor_autocomplete_css_variable.js
devtools/client/shared/test/helper_inplace_editor.js
devtools/client/themes/common.css
--- a/devtools/client/shared/autocomplete-popup.js
+++ b/devtools/client/shared/autocomplete-popup.js
@@ -249,35 +249,38 @@ AutocompletePopup.prototype = {
   __maxLabelLength: -1,
 
   get _maxLabelLength() {
     if (this.__maxLabelLength !== -1) {
       return this.__maxLabelLength;
     }
 
     let max = 0;
-    for (let {label, count} of this.items) {
+
+    for (let {label, postLabel, count} of this.items) {
       if (count) {
         label += count + "";
       }
+      if (postLabel) {
+        label += postLabel;
+      }
       max = Math.max(label.length, max);
     }
 
     this.__maxLabelLength = max;
     return this.__maxLabelLength;
   },
 
   /**
    * Update the panel size to fit the content.
    */
   _updateSize: function() {
     if (!this._tooltip) {
       return;
     }
-
     this._list.style.width = (this._maxLabelLength + 3) + "ch";
     let selectedItem = this.selectedItem;
     if (selectedItem) {
       this._scrollElementIntoViewIfNeeded(this.elements.get(selectedItem));
     }
   },
 
   _scrollElementIntoViewIfNeeded: function(element) {
@@ -417,39 +420,49 @@ AutocompletePopup.prototype = {
    *        The item object can have the following properties:
    *        - label {String} Property which is used as the displayed value.
    *        - preLabel {String} [Optional] The String that will be displayed
    *                   before the label indicating that this is the already
    *                   present text in the input box, and label is the text
    *                   that will be auto completed. When this property is
    *                   present, |preLabel.length| starting characters will be
    *                   removed from label.
+   *        - postLabel {String} [Optional] The string that will be displayed
+   *                  after the label. Currently used to display the value of
+   *                  a desired variable.
    *        - count {Number} [Optional] The number to represent the count of
    *                autocompleted label.
    */
   appendItem: function(item) {
     let listItem = this._document.createElementNS(HTML_NS, "li");
     // Items must have an id for accessibility.
     listItem.setAttribute("id", "autocomplete-item-" + itemIdCounter++);
     listItem.className = "autocomplete-item";
     listItem.setAttribute("data-index", this.items.length);
     if (this.direction) {
       listItem.setAttribute("dir", this.direction);
     }
+
     let label = this._document.createElementNS(HTML_NS, "span");
     label.textContent = item.label;
     label.className = "autocomplete-value";
     if (item.preLabel) {
       let preDesc = this._document.createElementNS(HTML_NS, "span");
       preDesc.textContent = item.preLabel;
       preDesc.className = "initial-value";
       listItem.appendChild(preDesc);
       label.textContent = item.label.slice(item.preLabel.length);
     }
     listItem.appendChild(label);
+    if (item.postLabel) {
+      let postDesc = this._document.createElementNS(HTML_NS, "span");
+      postDesc.textContent = item.postLabel;
+      postDesc.className = "autocomplete-postlabel";
+      listItem.appendChild(postDesc);
+    }
     if (item.count && item.count > 1) {
       let countDesc = this._document.createElementNS(HTML_NS, "span");
       countDesc.textContent = item.count;
       countDesc.setAttribute("flex", "1");
       countDesc.className = "autocomplete-count";
       listItem.appendChild(countDesc);
     }
 
--- a/devtools/client/shared/inplace-editor.js
+++ b/devtools/client/shared/inplace-editor.js
@@ -1328,16 +1328,17 @@ InplaceEditor.prototype = {
         // provided when preceeding a word.
         if (/[\w-]/.test(nextChar)) {
           // This emit is mainly to make the test flow simpler.
           this.emit("after-suggest", "nothing to autocomplete");
           return;
         }
       }
       let list = [];
+      let postLabelValues = [];
       if (this.contentType == CONTENT_TYPES.CSS_PROPERTY) {
         list = this._getCSSPropertyList();
       } else if (this.contentType == CONTENT_TYPES.CSS_VALUE) {
         // Get the last query to be completed before the caret.
         let match = /([^\s,.\/]+$)/.exec(query);
         if (match) {
           startCheckQuery = match[0];
         } else {
@@ -1345,16 +1346,17 @@ InplaceEditor.prototype = {
         }
 
         // 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();
+          postLabelValues = list.map(varName => this._getCSSVariableValue(varName));
         } else {
           list = ["!important",
                   ...this._getCSSValuesForPropertyName(this.property.name)];
         }
 
         if (query == "") {
           // Do not suggest '!important' without any manually typed character.
           list.splice(0, 1);
@@ -1410,17 +1412,18 @@ InplaceEditor.prototype = {
 
       let finalList = [];
       let length = list.length;
       for (let i = 0, count = 0; i < length && count < MAX_POPUP_ENTRIES; i++) {
         if (startCheckQuery != null && list[i].startsWith(startCheckQuery)) {
           count++;
           finalList.push({
             preLabel: startCheckQuery,
-            label: list[i]
+            label: list[i],
+            postLabel: (postLabelValues[i] ? postLabelValues[i] : "")
           });
         } else if (count > 0) {
           // Since count was incremented, we had already crossed the entries
           // which would have started with query, assuming that list is sorted.
           break;
         } else if (startCheckQuery != null && list[i][0] > startCheckQuery[0]) {
           // We have crossed all possible matches alphabetically.
           break;
@@ -1506,16 +1509,25 @@ InplaceEditor.prototype = {
    * @param {String} propertyName
    * @return {Array} array of CSS property values (Strings)
    */
   _getCSSValuesForPropertyName: function(propertyName) {
     return this.cssProperties.getValues(propertyName);
   },
 
   /**
+  * Returns the variable's value for the given CSS variable name.
+  *
+  * @return {String} varName - the variable to retrieve the value of
+  */
+  _getCSSVariableValue: function(varName) {
+    return this.cssVariables.get(varName);
+  },
+
+  /**
    * 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();
   },
 };
--- a/devtools/client/shared/test/browser_inplace-editor_autocomplete_css_variable.js
+++ b/devtools/client/shared/test/browser_inplace-editor_autocomplete_css_variable.js
@@ -3,82 +3,85 @@
    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");
+const CSS_VARIABLES = [
+  ["--abc", "blue"],
+  ["--def", "red"],
+  ["--ghi", "green"],
+  ["--jkl", "yellow"]
+];
 
 // 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),
+//    expected post label corresponding with the input box value,
 //  ]
 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],
+  ["v", "v", -1, 0, null],
+  ["a", "va", -1, 0, null],
+  ["r", "var", -1, 0, null],
+  ["(", "var(", -1, 0, null],
+  ["-", "var(--abc", 0, 4, "blue"],
+  ["VK_BACK_SPACE", "var(-", -1, 0, null],
+  ["-", "var(--abc", 0, 4, "blue"],
+  ["VK_DOWN", "var(--def", 1, 4, "red"],
+  ["VK_DOWN", "var(--ghi", 2, 4, "green"],
+  ["VK_DOWN", "var(--jkl", 3, 4, "yellow"],
+  ["VK_DOWN", "var(--abc", 0, 4, "blue"],
+  ["VK_DOWN", "var(--def", 1, 4, "red"],
+  ["VK_LEFT", "var(--def", -1, 0, null],
 ];
 
 const mockGetCSSValuesForPropertyName = function(propertyName) {
   return [];
 };
 
-const mockGetCSSVariableNames = function() {
-  return [
-    "--abc",
-    "--def",
-  ];
-};
-
 add_task(async function() {
   await addTab("data:text/html;charset=utf-8," +
-    "inplace editor CSS variable autocomplete");
+         "inplace editor CSS variable autocomplete");
   let [host, win, doc] = await createHost();
 
   let xulDocument = win.top.document;
   let popup = new AutocompletePopup(xulDocument, { autoSelect: true });
 
   await new Promise(resolve => {
     createInplaceEditorAndClick({
       start: runAutocompletionTest,
       contentType: InplaceEditor.CONTENT_TYPES.CSS_VALUE,
       property: {
         name: "color"
       },
+      cssVariables: new Map(CSS_VARIABLES),
       done: resolve,
       popup: popup
     }, doc);
   });
 
   popup.destroy();
   host.destroy();
   gBrowser.removeCurrentTab();
 });
 
 let runAutocompletionTest = async function(editor) {
   info("Starting to test for css variable completion");
   editor._getCSSValuesForPropertyName = mockGetCSSValuesForPropertyName;
-  editor._getCSSVariableNames = mockGetCSSVariableNames;
 
   for (let data of testData) {
     await testCompletion(data, editor);
   }
 
   EventUtils.synthesizeKey("VK_RETURN", {}, editor.input.defaultView);
 };
--- a/devtools/client/shared/test/helper_inplace_editor.js
+++ b/devtools/client/shared/test/helper_inplace_editor.js
@@ -67,20 +67,21 @@ function createSpan(doc) {
  * Test helper simulating a key event in an InplaceEditor and checking that the
  * autocompletion works as expected.
  *
  * @param {Array} testData
  *        - {String} key, the key to send
  *        - {String} completion, the expected value of the auto-completion
  *        - {Number} index, the index of the selected suggestion in the popup
  *        - {Number} total, the total number of suggestions in the popup
+ *        - {String} postLabel, the expected value of the tooltip
  * @param {InplaceEditor} editor
  *        The InplaceEditor instance being tested
  */
-async function testCompletion([key, completion, index, total], editor) {
+async function testCompletion([key, completion, index, total, postLabel], editor) {
   info("Pressing key " + key);
   info("Expecting " + completion);
 
   let onVisibilityChange = null;
   let open = total > 0;
   if (editor.popup.isOpen != open) {
     onVisibilityChange = editor.popup.once(open ? "popup-opened" : "popup-closed");
   }
@@ -100,16 +101,22 @@ async function testCompletion([key, comp
   await onSuggest;
   await onVisibilityChange;
   await waitForTime(5);
 
   info("Checking the state");
   if (completion !== null) {
     is(editor.input.value, completion, "Correct value is autocompleted");
   }
+  if (postLabel !== null) {
+    let selectedItem = editor.popup.getItems()[index];
+    let selectedElement = editor.popup.elements.get(selectedItem);
+    ok(selectedElement.textContent.includes(postLabel),
+      "Selected popup element contains the expected post-label");
+  }
   if (total === 0) {
     ok(!(editor.popup && editor.popup.isOpen), "Popup is closed");
   } else {
     ok(editor.popup.isOpen, "Popup is open");
     is(editor.popup.getItems().length, total, "Number of suggestions match");
     is(editor.popup.selectedIndex, index, "Expected item is selected");
   }
 }
--- a/devtools/client/themes/common.css
+++ b/devtools/client/themes/common.css
@@ -96,16 +96,22 @@ html|button, html|select {
   white-space: pre;
   overflow: hidden;
 }
 
 .devtools-autocomplete-listbox .autocomplete-item > .initial-value,
 .devtools-autocomplete-listbox .autocomplete-item > .autocomplete-value {
   margin: 0;
   padding: 0;
+  float: left;
+}
+
+.devtools-autocomplete-listbox .autocomplete-item > .autocomplete-postlabel {
+  font-style: italic;
+  float: right;
 }
 
 .devtools-autocomplete-listbox .autocomplete-item > .autocomplete-count {
   text-align: end;
 }
 
 /* Rest of the dark and light theme */