Bug 964939 - Allow clicking on autocomplete items for CSS source editor. r=pbrosset
authorBrian Grinstead <bgrinstead@mozilla.com>
Tue, 17 Mar 2015 11:42:00 -0400
changeset 234225 2f64916c94aa4b783e1c4abb2e5e5baf5b27637c
parent 234224 930a64f79bcdaae9f316101eef5dcd68a79ad408
child 234226 839427d11a62b13084f7ef6efe913021c78d759b
push id11828
push userryanvm@gmail.com
push dateWed, 18 Mar 2015 15:24:53 +0000
treeherderfx-team@4b5850a205d0 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerspbrosset
bugs964939
milestone39.0a1
Bug 964939 - Allow clicking on autocomplete items for CSS source editor. r=pbrosset
browser/devtools/shared/autocomplete-popup.js
browser/devtools/sourceeditor/autocomplete.js
browser/devtools/sourceeditor/test/browser.ini
browser/devtools/sourceeditor/test/browser_editor_autocomplete_events.js
browser/devtools/sourceeditor/test/head.js
--- a/browser/devtools/shared/autocomplete-popup.js
+++ b/browser/devtools/shared/autocomplete-popup.js
@@ -5,16 +5,17 @@
 
 "use strict";
 
 const {Cc, Ci, Cu} = require("chrome");
 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 
 loader.lazyImporter(this, "Services", "resource://gre/modules/Services.jsm");
 loader.lazyImporter(this, "gDevTools", "resource:///modules/devtools/gDevTools.jsm");
+const events  = require("devtools/toolkit/event-emitter");
 
 /**
  * Autocomplete popup UI implementation.
  *
  * @constructor
  * @param nsIDOMDocument aDocument
  *        The document you want the popup attached to.
  * @param Object aOptions
@@ -104,16 +105,18 @@ function AutocompletePopup(aDocument, aO
   if (this.onClick) {
     this._list.addEventListener("click", this.onClick, false);
   }
 
   if (this.onKeypress) {
     this._list.addEventListener("keypress", this.onKeypress, false);
   }
   this._itemIdCounter = 0;
+
+  events.decorate(this);
 }
 exports.AutocompletePopup = AutocompletePopup;
 
 AutocompletePopup.prototype = {
   _document: null,
   _panel: null,
   _list: null,
   __scrollbarWidth: null,
@@ -139,16 +142,18 @@ AutocompletePopup.prototype = {
   {
     this.__maxLabelLength = -1;
     this._updateSize();
     this._panel.openPopup(aAnchor, this.position, aXOffset, aYOffset);
 
     if (this.autoSelect) {
       this.selectFirstItem();
     }
+
+    this.emit("popup-opened");
   },
 
   /**
    * Hide the autocomplete popup panel.
    */
   hidePopup: function AP_hidePopup()
   {
     // Return accessibility focus to the input.
--- a/browser/devtools/sourceeditor/autocomplete.js
+++ b/browser/devtools/sourceeditor/autocomplete.js
@@ -99,21 +99,40 @@ function initializeAutoCompletion(ctx, o
     });
 
     // TODO: Integrate tern autocompletion with this autocomplete API.
     return;
   } else if (ed.config.mode == Editor.modes.css) {
     completer = new cssAutoCompleter({walker: options.walker});
   }
 
+  function insertSelectedPopupItem() {
+    let autocompleteState = autocompleteMap.get(ed);
+    if (!popup || !popup.isOpen || !autocompleteState) {
+      return;
+    }
+
+    if (!autocompleteState.suggestionInsertedOnce && popup.selectedItem) {
+      autocompleteMap.get(ed).insertingSuggestion = true;
+      let {label, preLabel, text} = popup.selectedItem;
+      let cur = ed.getCursor();
+      ed.replaceText(text.slice(preLabel.length), cur, cur);
+    }
+
+    popup.hidePopup();
+    ed.emit("popup-hidden"); // This event is used in tests.
+    return true;
+  }
+
   let popup = new AutocompletePopup(win.parent.document, {
     position: "after_start",
     fixedWidth: true,
     theme: "auto",
-    autoSelect: true
+    autoSelect: true,
+    onClick: insertSelectedPopupItem
   });
 
   let cycle = (reverse) => {
     if (popup && popup.isOpen) {
       cycleSuggestions(ed, reverse == true);
       return;
     }
 
@@ -121,30 +140,18 @@ function initializeAutoCompletion(ctx, o
   };
 
   let keyMap = {
     "Tab": cycle,
     "Down": cycle,
     "Shift-Tab": cycle.bind(null, true),
     "Up": cycle.bind(null, true),
     "Enter": () => {
-      if (popup && popup.isOpen) {
-        if (!autocompleteMap.get(ed).suggestionInsertedOnce) {
-          autocompleteMap.get(ed).insertingSuggestion = true;
-          let {label, preLabel, text} = popup.getItemAtIndex(0);
-          let cur = ed.getCursor();
-          ed.replaceText(text.slice(preLabel.length), cur, cur);
-        }
-        popup.hidePopup();
-        // This event is used in tests
-        ed.emit("popup-hidden");
-        return;
-      }
-
-      return CodeMirror.Pass;
+      let wasHandled = insertSelectedPopupItem();
+      return wasHandled ? true : CodeMirror.Pass;
     }
   };
   let autoCompleteCallback = autoComplete.bind(null, ctx);
   let keypressCallback = onEditorKeypress.bind(null, ctx);
   keyMap[autocompleteKey] = autoCompleteCallback;
   cm.addKeyMap(keyMap);
 
   cm.on("keydown", keypressCallback);
--- a/browser/devtools/sourceeditor/test/browser.ini
+++ b/browser/devtools/sourceeditor/test/browser.ini
@@ -16,16 +16,17 @@ support-files =
   css_statemachine_testcases.css
   css_statemachine_tests.json
   css_autocompletion_tests.json
   vimemacs.html
   head.js
   helper_codemirror_runner.js
 
 [browser_editor_autocomplete_basic.js]
+[browser_editor_autocomplete_events.js]
 [browser_editor_autocomplete_js.js]
 [browser_editor_basic.js]
 [browser_editor_cursor.js]
 [browser_editor_goto_line.js]
 [browser_editor_history.js]
 [browser_editor_markers.js]
 [browser_editor_movelines.js]
 [browser_editor_prefs.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/sourceeditor/test/browser_editor_autocomplete_events.js
@@ -0,0 +1,62 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const {InspectorFront} = require("devtools/server/actors/inspector");
+const AUTOCOMPLETION_PREF = "devtools.editor.autocomplete";
+const TEST_URI = "data:text/html;charset=UTF-8,<html><body><b1></b1><b2></b2><body></html>";
+
+add_task(function*() {
+  yield promiseTab(TEST_URI);
+  yield runTests();
+});
+
+function* runTests() {
+  let target = devtools.TargetFactory.forTab(gBrowser.selectedTab);
+  yield target.makeRemote();
+  let inspector = InspectorFront(target.client, target.form);
+  let walker = yield inspector.getWalker();
+  let {ed, win, edWin} = yield setup(null, {
+    autocomplete: true,
+    mode: Editor.modes.css,
+    autocompleteOpts: {walker: walker}
+  });
+  yield testMouse(ed, edWin);
+  yield testKeyboard(ed, edWin);
+  teardown(ed, win);
+}
+
+function* testKeyboard(ed, win) {
+  ed.focus();
+  ed.setText("b");
+  ed.setCursor({line: 1, ch: 1});
+
+  let popupOpened = ed.getAutocompletionPopup().once("popup-opened");
+
+  let autocompleteKey = Editor.keyFor("autocompletion", { noaccel: true }).toUpperCase();
+  EventUtils.synthesizeKey("VK_" + autocompleteKey, { ctrlKey: true }, win);
+
+  info ("Waiting for popup to be opened");
+  yield popupOpened;
+
+  EventUtils.synthesizeKey("VK_RETURN", { }, win);
+  is (ed.getText(), "b1", "Editor text has been updated");
+}
+
+function* testMouse(ed, win) {
+  ed.focus();
+  ed.setText("b");
+  ed.setCursor({line: 1, ch: 1});
+
+  let popupOpened = ed.getAutocompletionPopup().once("popup-opened");
+
+  let autocompleteKey = Editor.keyFor("autocompletion", { noaccel: true }).toUpperCase();
+  EventUtils.synthesizeKey("VK_" + autocompleteKey, { ctrlKey: true }, win);
+
+  info ("Waiting for popup to be opened");
+  yield popupOpened;
+  ed.getAutocompletionPopup()._list.firstChild.click();
+  is (ed.getText(), "b1", "Editor text has been updated");
+}
--- a/browser/devtools/sourceeditor/test/head.js
+++ b/browser/devtools/sourceeditor/test/head.js
@@ -9,52 +9,95 @@ const { require } = devtools;
 const Editor  = require("devtools/sourceeditor/editor");
 const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
 
 gDevTools.testing = true;
 SimpleTest.registerCleanupFunction(() => {
   gDevTools.testing = false;
 });
 
-function setup(cb) {
+/**
+ * Open a new tab at a URL and call a callback on load
+ */
+function addTab(aURL, aCallback) {
+  waitForExplicitFinish();
+
+  gBrowser.selectedTab = gBrowser.addTab();
+  content.location = aURL;
+
+  let tab = gBrowser.selectedTab;
+  let browser = gBrowser.getBrowserForTab(tab);
+
+  function onTabLoad() {
+    browser.removeEventListener("load", onTabLoad, true);
+    aCallback(browser, tab, browser.contentDocument);
+  }
+
+  browser.addEventListener("load", onTabLoad, true);
+}
+
+function promiseTab(aURL) {
+  return new Promise(resolve =>
+    addTab(aURL, resolve));
+}
+
+function setup(cb, additionalOpts = {}) {
+  cb = cb || function() {};
+  let def = promise.defer();
   const opt = "chrome,titlebar,toolbar,centerscreen,resizable,dialog=no";
-  const url = "data:text/xml;charset=UTF-8,<?xml version='1.0'?>" +
+  const url = "data:application/vnd.mozilla.xul+xml;charset=UTF-8,<?xml version='1.0'?>" +
     "<?xml-stylesheet href='chrome://global/skin/global.css'?>" +
     "<window xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'" +
     " title='Editor' width='600' height='500'><box flex='1'/></window>";
 
   let win = Services.ww.openWindow(null, url, "_blank", opt, null);
+  let opts = {
+    value: "Hello.",
+    lineNumbers: true,
+    foldGutter: true,
+    gutters: [ "CodeMirror-linenumbers", "breakpoints", "CodeMirror-foldgutter" ]
+  }
+  for (let o in additionalOpts) {
+    opts[o] = additionalOpts[o];
+  }
 
   win.addEventListener("load", function onLoad() {
     win.removeEventListener("load", onLoad, false);
 
     waitForFocus(function () {
       let box = win.document.querySelector("box");
-      let editor = new Editor({
-        value: "Hello.",
-        lineNumbers: true,
-        foldGutter: true,
-        gutters: [ "CodeMirror-linenumbers", "breakpoints", "CodeMirror-foldgutter" ]
-      });
+      let editor = new Editor(opts);
 
       editor.appendTo(box)
-        .then(() => cb(editor, win))
-        .then(null, (err) => ok(false, err.message));
+        .then(() => {
+          def.resolve({
+            ed: editor,
+            win: win,
+            edWin: editor.container.contentWindow.wrappedJSObject
+          });
+          cb(editor, win);
+        }, err => ok(false, err.message));
     }, win);
   }, false);
+
+  return def.promise;
 }
 
 function ch(exp, act, label) {
   is(exp.line, act.line, label + " (line)");
   is(exp.ch, act.ch, label + " (ch)");
 }
 
 function teardown(ed, win) {
   ed.destroy();
   win.close();
+
+  while (gBrowser.tabs.length > 1) {
+    gBrowser.removeCurrentTab();
+  }
   finish();
 }
 
 /**
  * Some tests may need to import one or more of the test helper scripts.
  * A test helper script is simply a js file that contains common test code that
  * is either not common-enough to be in head.js, or that is located in a separate
  * directory.