Bug 1431949 - Show variable values in the CSS variable autocomplete popup. r=jdescottes
authorSarah Childs <sarahchilds19@gmail.com>
Tue, 03 Apr 2018 20:32:22 -0400
changeset 411587 3f6eb728f242
parent 411586 cc66bed8784f
child 411588 86b13fe023ca
push id101694
push usergabriel.luong@gmail.com
push dateWed, 04 Apr 2018 00:32:40 +0000
treeherdermozilla-inbound@3f6eb728f242 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjdescottes
bugs1431949
milestone61.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1431949 - Show variable values in the CSS variable autocomplete popup. r=jdescottes
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,20 +249,26 @@ 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;
   },
 
   /**
@@ -417,39 +423,54 @@ 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
@@ -1327,17 +1327,20 @@ InplaceEditor.prototype = {
         // Check if the next character is a valid word character, no suggestion should be
         // 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 +1348,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 +1414,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;
@@ -1513,16 +1518,27 @@ InplaceEditor.prototype = {
   /**
    * 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();
   },
+
+  /**
+  * Returns the variable's value for the given CSS variable name.
+  *
+  * @param {String} varName
+  *        The variable name to retrieve the value of
+  * @return {String} the variable value to the given CSS variable name
+  */
+  _getCSSVariableValue: function(varName) {
+    return this.cssVariables.get(varName);
+  },
 };
 
 /**
  * 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_inplace-editor_autocomplete_css_variable.js
+++ b/devtools/client/shared/test/browser_inplace-editor_autocomplete_css_variable.js
@@ -16,69 +16,72 @@ loadHelperScript("helper_inplace_editor.
 // 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 CSS_VARIABLES = [
+  ["--abc", "blue"],
+  ["--def", "red"],
+  ["--ghi", "green"],
+  ["--jkl", "yellow"]
 ];
 
 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");
+  await addTab("data:text/html;charset=utf-8,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 post label for the selected suggestion
  * @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,24 @@ 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) {
+    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 */