Bug 896181 - Autocomplete CSS properties and values in markup view, r=mratcliffe
authorGirish Sharma <scrapmachines@gmail.com>
Fri, 02 Aug 2013 16:05:50 +0530
changeset 153482 2a567b5e955d02d831a99ddc6be14ae4d145d60d
parent 153481 489187b2309d9b8b301c89ca9c72dfb709c101a0
child 153483 fee8d3cd387d77545ad64d8600122f633ac1af84
push id2859
push userakeybl@mozilla.com
push dateMon, 16 Sep 2013 19:14:59 +0000
treeherdermozilla-beta@87d3c51cd2bf [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmratcliffe
bugs896181
milestone25.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 896181 - Autocomplete CSS properties and values in markup view, r=mratcliffe
browser/devtools/markupview/markup-view.js
browser/devtools/markupview/test/Makefile.in
browser/devtools/markupview/test/browser_bug896181_css_mixed_completion_new_attribute.js
browser/devtools/shared/inplace-editor.js
browser/devtools/styleinspector/test/browser_bug893965_css_property_completion_existing_property.js
browser/devtools/styleinspector/test/browser_bug893965_css_property_completion_new_property.js
--- a/browser/devtools/markupview/markup-view.js
+++ b/browser/devtools/markupview/markup-view.js
@@ -15,16 +15,20 @@ const DEFAULT_MAX_CHILDREN = 100;
 let {UndoStack} = require("devtools/shared/undo");
 let EventEmitter = require("devtools/shared/event-emitter");
 let {editableField, InplaceEditor} = require("devtools/shared/inplace-editor");
 let promise = require("sdk/core/promise");
 
 Cu.import("resource:///modules/devtools/LayoutHelpers.jsm");
 Cu.import("resource://gre/modules/devtools/Templater.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+XPCOMUtils.defineLazyGetter(this, "AutocompletePopup", function() {
+  return Cu.import("resource:///modules/devtools/AutocompletePopup.jsm", {}).AutocompletePopup;
+});
 
 /**
  * Vocabulary for the purposes of this file:
  *
  * MarkupContainer - the structure that holds an editor and its
  *  immediate children in the markup panel.
  * Node - A content node.
  * object.elt - A UI element in the markup panel.
@@ -48,16 +52,24 @@ function MarkupView(aInspector, aFrame, 
   this._elt = this.doc.querySelector("#root");
 
   try {
     this.maxChildren = Services.prefs.getIntPref("devtools.markup.pagesize");
   } catch(ex) {
     this.maxChildren = DEFAULT_MAX_CHILDREN;
   }
 
+  // Creating the popup to be used to show CSS suggestions.
+  let options = {
+    fixedWidth: true,
+    autoSelect: true,
+    theme: "auto"
+  };
+  this.popup = new AutocompletePopup(this.doc.defaultView.parent.document, options);
+
   this.undo = new UndoStack();
   this.undo.installController(aControllerWindow);
 
   this._containers = new WeakMap();
 
   this._boundMutationObserver = this._mutationObserver.bind(this);
   this.walker.on("mutations", this._boundMutationObserver)
 
@@ -671,16 +683,19 @@ MarkupView.prototype = {
   /**
    * Tear down the markup panel.
    */
   destroy: function MT_destroy()
   {
     this.undo.destroy();
     delete this.undo;
 
+    this.popup.destroy();
+    delete this.popup;
+
     this._frame.removeEventListener("focus", this._boundFocus, false);
     delete this._boundFocus;
 
     if (this._boundUpdatePreview) {
       this._frame.contentWindow.removeEventListener("scroll", this._boundUpdatePreview, true);
       delete this._boundUpdatePreview;
     }
 
@@ -1125,16 +1140,18 @@ function ElementEditor(aContainer, aNode
     });
   }
 
   // Make the new attribute space editable.
   editableField({
     element: this.newAttr,
     trigger: "dblclick",
     stopOnReturn: true,
+    contentType: InplaceEditor.CONTENT_TYPES.CSS_MIXED,
+    popup: this.markup.popup,
     done: (aVal, aCommit) => {
       if (!aCommit) {
         return;
       }
 
       try {
         let doMods = this._startModifyingAttributes();
         let undoMods = this._startModifyingAttributes();
@@ -1217,16 +1234,18 @@ ElementEditor.prototype = {
       this.attrList.insertBefore(attr, before);
 
       // Make the attribute editable.
       editableField({
         element: inner,
         trigger: "dblclick",
         stopOnReturn: true,
         selectAll: false,
+        contentType: InplaceEditor.CONTENT_TYPES.CSS_MIXED,
+        popup: this.markup.popup,
         start: (aEditor, aEvent) => {
           // If the editing was started inside the name or value areas,
           // select accordingly.
           if (aEvent && aEvent.target === name) {
             aEditor.input.setSelectionRange(0, name.textContent.length);
           } else if (aEvent && aEvent.target === val) {
             let length = val.textContent.length;
             let editorLength = aEditor.input.value.length;
--- a/browser/devtools/markupview/test/Makefile.in
+++ b/browser/devtools/markupview/test/Makefile.in
@@ -14,12 +14,13 @@ MOCHITEST_BROWSER_FILES := \
 		browser_inspector_markup_navigation.html \
 		browser_inspector_markup_navigation.js \
 		browser_inspector_markup_mutation.html \
 		browser_inspector_markup_mutation.js \
 		browser_inspector_markup_edit.html \
     browser_inspector_markup_edit.js \
     browser_inspector_markup_subset.html \
     browser_inspector_markup_subset.js \
+    browser_bug896181_css_mixed_completion_new_attribute.js \
 		head.js \
 		$(NULL)
 
 include $(topsrcdir)/config/rules.mk
new file mode 100644
--- /dev/null
+++ b/browser/devtools/markupview/test/browser_bug896181_css_mixed_completion_new_attribute.js
@@ -0,0 +1,167 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test CSS state is correctly determined and the corresponding suggestions are
+// displayed. i.e. CSS property suggestions are shown when cursor is like:
+// ```style="di|"``` where | is teh cursor; And CSS value suggestion is
+// displayed when the cursor is like: ```style="display:n|"``` properly. No
+// suggestions should ever appear when the attribute is not a style attribute.
+// The correctness and cycling of the suggestions is covered in the ruleview
+// tests.
+
+function test() {
+  let inspector;
+  let {
+    getInplaceEditorForSpan: inplaceEditor
+  } = devtools.require("devtools/shared/inplace-editor");
+
+  waitForExplicitFinish();
+
+  // Will hold the doc we're viewing
+  let doc;
+
+  // Holds the MarkupTool object we're testing.
+  let markup;
+  let editor;
+  let state;
+  // format :
+  //  [
+  //    what key to press,
+  //    expected input box value after keypress,
+  //    expected input.selectionStart,
+  //    expected input.selectionEnd,
+  //    is popup expected to be open ?
+  //  ]
+  let testData = [
+    ['s', 's', 1, 1, false],
+    ['t', 'st', 2, 2, false],
+    ['y', 'sty', 3, 3, false],
+    ['l', 'styl', 4, 4, false],
+    ['e', 'style', 5, 5, false],
+    ['=', 'style=', 6, 6, false],
+    ['"', 'style="', 7, 7, false],
+    ['d', 'style="direction', 8, 16, true],
+    ['VK_DOWN', 'style="display', 8, 14, true],
+    ['VK_RIGHT', 'style="display', 14, 14, false],
+    [':', 'style="display:', 15, 15, false],
+    ['n', 'style="display:none', 16, 19, false],
+    ['VK_BACK_SPACE', 'style="display:n', 16, 16, false],
+    ['VK_BACK_SPACE', 'style="display:', 15, 15, false],
+    [' ', 'style="display: ', 16, 16, false],
+    [' ', 'style="display:  ', 17, 17, false],
+    ['i', 'style="display:  inherit', 18, 24, true],
+    ['VK_RIGHT', 'style="display:  inherit', 24, 24, false],
+    [';', 'style="display:  inherit;', 25, 25, false],
+    [' ', 'style="display:  inherit; ', 26, 26, false],
+    [' ', 'style="display:  inherit;  ', 27, 27, false],
+    ['VK_LEFT', 'style="display:  inherit;  ', 26, 26, false],
+    ['c', 'style="display:  inherit; caption-side ', 27, 38, true],
+    ['o', 'style="display:  inherit; color ', 28, 31, true],
+    ['VK_RIGHT', 'style="display:  inherit; color ', 31, 31, false],
+    [' ', 'style="display:  inherit; color  ', 32, 32, false],
+    ['c', 'style="display:  inherit; color c ', 33, 33, false],
+    ['VK_BACK_SPACE', 'style="display:  inherit; color  ', 32, 32, false],
+    [':', 'style="display:  inherit; color : ', 33, 33, false],
+    ['c', 'style="display:  inherit; color :cadetblue ', 34, 42, true],
+    ['VK_DOWN', 'style="display:  inherit; color :chartreuse ', 34, 43, true],
+    ['VK_RETURN', 'style="display:  inherit; color :chartreuse"', -1, -1, false]
+  ];
+
+  function startTests() {
+    markup = inspector.markup;
+    markup.expandAll().then(() => {
+      let node = getContainerForRawNode(markup, doc.querySelector("#node14")).editor;
+      let attr = node.newAttr;
+      attr.focus();
+      EventUtils.sendKey("return", inspector.panelWin);
+      editor = inplaceEditor(attr);
+      checkStateAndMoveOn(0);
+    });
+  }
+
+  function checkStateAndMoveOn(index) {
+    if (index == testData.length) {
+      finishUp();
+      return;
+    }
+
+    let [key] = testData[index];
+    state = index;
+
+    info("pressing key " + key + " to get result: [" + testData[index].slice(1) +
+         "] for state " + state);
+    if (/(down|left|right|back_space|return)/ig.test(key)) {
+      info("added event listener for down|left|right|back_space|return keys");
+      editor.input.addEventListener("keypress", function onKeypress() {
+        if (editor.input) {
+          editor.input.removeEventListener("keypress", onKeypress);
+        }
+        info("inside event listener");
+        checkState();
+      }) 
+    }
+    else {
+      editor.once("after-suggest", checkState);
+    }
+    EventUtils.synthesizeKey(key, {}, inspector.panelWin);
+  }
+
+  function checkState() {
+    executeSoon(() => {
+      info("After keypress for state " + state);
+      let [key, completion, selStart, selEnd, popupOpen] = testData[state];
+      if (selEnd != -1) {
+        is(editor.input.value, completion,
+           "Correct value is autocompleted for state " + state);
+        is(editor.input.selectionStart, selStart,
+           "Selection is starting at the right location for state " + state);
+        is(editor.input.selectionEnd, selEnd,
+           "Selection is ending at the right location for state " + state);
+        if (popupOpen) {
+          ok(editor.popup._panel.state == "open" ||
+             editor.popup._panel.state == "showing",
+             "Popup is open for state " + state);
+        }
+        else {
+          ok(editor.popup._panel.state != "open" &&
+             editor.popup._panel.state != "showing",
+             "Popup is closed for state " + state);
+        }
+      }
+      else {
+        let editor = getContainerForRawNode(markup, doc.querySelector("#node14")).editor;
+        let attr = editor.attrs["style"].querySelector(".editable");
+        is(attr.textContent, completion,
+           "Correct value is persisted after pressing Enter for state " + state);
+      }
+      checkStateAndMoveOn(state + 1);
+    });
+  }
+
+  // Create the helper tab for parsing...
+  gBrowser.selectedTab = gBrowser.addTab();
+  gBrowser.selectedBrowser.addEventListener("load", function onload() {
+    gBrowser.selectedBrowser.removeEventListener("load", onload, true);
+    doc = content.document;
+    waitForFocus(setupTest, content);
+  }, true);
+  content.location = "http://mochi.test:8888/browser/browser/devtools/markupview/test/browser_inspector_markup_edit.html";
+
+  function setupTest() {
+    var target = TargetFactory.forTab(gBrowser.selectedTab);
+    gDevTools.showToolbox(target, "inspector").then(function(toolbox) {
+      inspector = toolbox.getCurrentPanel();
+      startTests();
+    });
+  }
+
+  function finishUp() {
+    while (markup.undo.canUndo()) {
+      markup.undo.undo();
+    }
+    doc = inspector = null;
+    gBrowser.removeCurrentTab();
+    finish();
+  }
+}
--- a/browser/devtools/shared/inplace-editor.js
+++ b/browser/devtools/shared/inplace-editor.js
@@ -754,17 +754,23 @@ InplaceEditor.prototype = {
     } else if (increment && this.popup && this.popup.isOpen) {
       cycling = true;
       prevent = true;
       if (increment > 0) {
         this.popup.selectPreviousItem();
       } else {
         this.popup.selectNextItem();
       }
-      this.input.value = this.popup.selectedItem.label;
+      let input = this.input;
+      let pre = input.value.slice(0, input.selectionStart);
+      let post = input.value.slice(input.selectionEnd, input.value.length);
+      let item = this.popup.selectedItem;
+      let toComplete = item.label.slice(item.preLabel.length);
+      input.value = pre + toComplete + post;
+      input.setSelectionRange(pre.length, pre.length + toComplete.length);
       this._updateSize();
       // This emit is mainly for the purpose of making the test flow simpler.
       this.emit("after-suggest");
     }
 
     if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_BACK_SPACE ||
         aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_DELETE ||
         aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_LEFT ||
@@ -893,55 +899,80 @@ InplaceEditor.prototype = {
       }
 
       let input = this.input;
       // Input can be null in cases when you intantaneously switch out of it.
       if (!input) {
         return;
       }
       let query = input.value.slice(0, input.selectionStart);
+      let startCheckQuery = query;
       if (!query) {
         return;
       }
 
       let list = [];
       if (this.contentType == CONTENT_TYPES.CSS_PROPERTY) {
         list = CSSPropertyList;
       } else if (this.contentType == CONTENT_TYPES.CSS_VALUE) {
-        list = domUtils.getCSSValuesForProperty(this.property.name).sort();
+        list = domUtils.getCSSValuesForProperty(this.property.name);
+      } else if (this.contentType == CONTENT_TYPES.CSS_MIXED &&
+                 /^\s*style\s*=/.test(query)) {
+        // Detecting if cursor is at property or value;
+        let match = query.match(/([:;"'=]?)\s*([^"';:= ]+)$/);
+        if (match && match.length == 3) {
+          if (match[1] == ":") { // We are in CSS value completion
+            let propertyName =
+              query.match(/[;"'=]\s*([^"';:= ]+)\s*:\s*[^"';:= ]+$/)[1];
+            list = domUtils.getCSSValuesForProperty(propertyName);
+            startCheckQuery = match[2];
+          } else if (match[1]) { // We are in CSS property name completion
+            list = CSSPropertyList;
+            startCheckQuery = match[2];
+          }
+          if (!startCheckQuery) {
+            // This emit is mainly to make the test flow simpler.
+            this.emit("after-suggest", "nothing to autocomplete");
+            return;
+          }
+        }
       }
 
       list.some(item => {
-        if (item.startsWith(query)) {
-          input.value = item;
-          input.setSelectionRange(query.length, item.length);
+        if (item.startsWith(startCheckQuery)) {
+          input.value = query + item.slice(startCheckQuery.length) +
+                        input.value.slice(query.length);
+          input.setSelectionRange(query.length, query.length + item.length -
+                                                startCheckQuery.length);
           this._updateSize();
           return true;
         }
       });
 
       if (!this.popup) {
+        // This emit is mainly to make the test flow simpler.
+        this.emit("after-suggest", "no popup");
         return;
       }
       let finalList = [];
       let length = list.length;
       for (let i = 0, count = 0; i < length && count < MAX_POPUP_ENTRIES; i++) {
-        if (list[i].startsWith(query)) {
+        if (list[i].startsWith(startCheckQuery)) {
           count++;
           finalList.push({
-            preLabel: query,
+            preLabel: startCheckQuery,
             label: list[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 (list[i][0] > query[0]) {
+        else if (list[i][0] > startCheckQuery[0]) {
           // We have crossed all possible matches alphabetically.
           break;
         }
       }
 
       if (finalList.length > 1) {
         this.popup.setItems(finalList);
         this.popup.openPopup(this.input);
--- a/browser/devtools/styleinspector/test/browser_bug893965_css_property_completion_existing_property.js
+++ b/browser/devtools/styleinspector/test/browser_bug893965_css_property_completion_existing_property.js
@@ -34,21 +34,16 @@ let testData = [
   ["d", "direction", 0, 3],
   ["VK_DOWN", "display", 1, 3],
   ["VK_DOWN", "dominant-baseline", 2, 3],
   ["VK_DOWN", "direction", 0, 3],
   ["VK_DOWN", "display", 1, 3],
   ["VK_UP", "direction", 0, 3],
   ["VK_UP", "dominant-baseline", 2, 3],
   ["VK_UP", "display", 1, 3],
-  ["VK_BACK_SPACE", "displa", -1, 0],
-  ["VK_BACK_SPACE", "displ", -1, 0],
-  ["VK_BACK_SPACE", "disp", -1, 0],
-  ["VK_BACK_SPACE", "dis", -1, 0],
-  ["VK_BACK_SPACE", "di", -1, 0],
   ["VK_BACK_SPACE", "d", -1, 0],
   ["i", "direction", 0, 2],
   ["s", "display", -1, 0],
   ["VK_BACK_SPACE", "dis", -1, 0],
   ["VK_BACK_SPACE", "di", -1, 0],
   ["VK_BACK_SPACE", "d", -1, 0],
   ["VK_BACK_SPACE", "", -1, 0],
   ["f", "fill", 0, MAX_ENTRIES],
--- a/browser/devtools/styleinspector/test/browser_bug893965_css_property_completion_new_property.js
+++ b/browser/devtools/styleinspector/test/browser_bug893965_css_property_completion_new_property.js
@@ -22,21 +22,16 @@ let testData = [
   ["d", "direction", 0, 3],
   ["VK_DOWN", "display", 1, 3],
   ["VK_DOWN", "dominant-baseline", 2, 3],
   ["VK_DOWN", "direction", 0, 3],
   ["VK_DOWN", "display", 1, 3],
   ["VK_UP", "direction", 0, 3],
   ["VK_UP", "dominant-baseline", 2, 3],
   ["VK_UP", "display", 1, 3],
-  ["VK_BACK_SPACE", "displa", -1, 0],
-  ["VK_BACK_SPACE", "displ", -1, 0],
-  ["VK_BACK_SPACE", "disp", -1, 0],
-  ["VK_BACK_SPACE", "dis", -1, 0],
-  ["VK_BACK_SPACE", "di", -1, 0],
   ["VK_BACK_SPACE", "d", -1, 0],
   ["i", "direction", 0, 2],
   ["s", "display", -1, 0],
   ["VK_BACK_SPACE", "dis", -1, 0],
   ["VK_BACK_SPACE", "di", -1, 0],
   ["VK_BACK_SPACE", "d", -1, 0],
   ["VK_BACK_SPACE", "", -1, 0],
   ["f", "fill", 0, MAX_ENTRIES],