Bug 1292592 - Convert sourceeditor to use key shortcuts module;r=ochameau draft
authorFred Lin <gasolin@mozilla.com>
Fri, 23 Sep 2016 15:12:17 +0800
changeset 420488 c09f121df4198266e10edc3b979dc2ea66237673
parent 420487 32d3ce69e604298d4fc9ae560af66252e013b453
child 532822 f9049712262f9ca2d548dcb5f6b4f1b50aba2c00
push id31211
push userbmo:gasolin@mozilla.com
push dateTue, 04 Oct 2016 07:17:08 +0000
reviewersochameau
bugs1292592
milestone52.0a1
Bug 1292592 - Convert sourceeditor to use key shortcuts module;r=ochameau MozReview-Commit-ID: 2hFtanHPuyr
devtools/client/framework/test/shared-head.js
devtools/client/locales/en-US/sourceeditor.properties
devtools/client/scratchpad/test/browser_scratchpad_goto_line_ui.js
devtools/client/sourceeditor/editor.js
devtools/client/sourceeditor/test/browser.ini
devtools/client/sourceeditor/test/browser_css_autocompletion.js
devtools/client/sourceeditor/test/browser_editor_autocomplete_events.js
devtools/client/sourceeditor/test/browser_editor_find_again.js
devtools/client/sourceeditor/test/head.js
--- a/devtools/client/framework/test/shared-head.js
+++ b/devtools/client/framework/test/shared-head.js
@@ -196,25 +196,28 @@ function synthesizeKeyFromKeyTag(key) {
  * @param {String} key
  * @param {DOMWindow} target
  *        Optional window where to fire the key event
  */
 function synthesizeKeyShortcut(key, target) {
   // parseElectronKey requires any window, just to access `KeyboardEvent`
   let window = Services.appShell.hiddenDOMWindow;
   let shortcut = KeyShortcuts.parseElectronKey(window, key);
-
-  info("Synthesizing key shortcut: " + key);
-  EventUtils.synthesizeKey(shortcut.key || "", {
-    keyCode: shortcut.keyCode,
+  let keyEvent = {
     altKey: shortcut.alt,
     ctrlKey: shortcut.ctrl,
     metaKey: shortcut.meta,
     shiftKey: shortcut.shift
-  }, target);
+  };
+  if (shortcut.keyCode) {
+    keyEvent.keyCode = shortcut.keyCode;
+  }
+
+  info("Synthesizing key shortcut: " + key);
+  EventUtils.synthesizeKey(shortcut.key || "", keyEvent, target);
 }
 
 /**
  * Wait for eventName on target to be delivered a number of times.
  *
  * @param {Object} target
  *        An observable object that either supports on/off or
  *        addEventListener/removeEventListener
--- a/devtools/client/locales/en-US/sourceeditor.properties
+++ b/devtools/client/locales/en-US/sourceeditor.properties
@@ -103,17 +103,37 @@ moveLineDown.commandkey=Alt-Down
 autocompletion.commandkey=Space
 
 # LOCALIZATION NOTE  (showInformation2.commandkey): This is the combination of
 # keys used to display more information, like type inference.
 # Do not localize "Shift", "Ctrl", "Space", or change the format of the string.
 # These are key identifiers, not messages displayed to the user.
 showInformation2.commandkey=Shift-Ctrl-Space
 
-# LOCALIZATION NOTE  (find.commandkey): This is the key to use in
-# conjunction with accel (Command on Mac or Ctrl on other platforms) to find
-# the typed search
-find.commandkey=F
+# LOCALIZATION NOTE  (find.key):
+# Key shortcut used to find the typed search
+# Do not localize "CmdOrCtrl", "F", or change the format of the string. These are
+# key identifiers, not messages displayed to the user.
+find.key=CmdOrCtrl+F
+
+# LOCALIZATION NOTE (replaceAll.key):
+# Key shortcut used to replace the content of the editor
+# Do not localize "Shift", "CmdOrCtrl", "F", or change the format of the string. These are
+# key identifiers, not messages displayed to the user.
+replaceAll.key=Shift+CmdOrCtrl+F
 
-# LOCALIZATION NOTE  (findAgain.commandkey): This is the key to use in
-# conjunction with accel (Command on Mac or Ctrl on other platforms) to find
-# again the typed search
-findAgain.commandkey=G
+# LOCALIZATION NOTE (replaceAllMac.key):
+# Key shortcut used to replace the content of the editor on Mac
+# Do not localize "Alt", "CmdOrCtrl", "F", or change the format of the string. These are
+# key identifiers, not messages displayed to the user.
+replaceAllMac.key=Alt+CmdOrCtrl+F
+
+# LOCALIZATION NOTE  (findNext.key):
+# Key shortcut used to find again the typed search
+# Do not localize "CmdOrCtrl", "G", or change the format of the string. These are
+# key identifiers, not messages displayed to the user.
+findNext.key=CmdOrCtrl+G
+
+# LOCALIZATION NOTE (findPrev.key):
+# Key shortcut used to find the previous typed search
+# Do not localize "Shift", "CmdOrCtrl", "G", or change the format of the string. These are
+# key identifiers, not messages displayed to the user.
+findPrev.key=Shift+CmdOrCtrl+G
--- a/devtools/client/scratchpad/test/browser_scratchpad_goto_line_ui.js
+++ b/devtools/client/scratchpad/test/browser_scratchpad_goto_line_ui.js
@@ -30,14 +30,14 @@ function runTests(aWindow, aScratchpad)
     cb(desiredValue);
   };
 
   desiredValue = 3;
   EventUtils.synthesizeKey("J", {accelKey: true}, aWindow);
   is(editor.getCursor().line, 2, "line is correct");
 
   desiredValue = 2;
-  aWindow.goDoCommand("cmd_gotoLine");
+  EventUtils.synthesizeKey("J", {accelKey: true}, aWindow);
   is(editor.getCursor().line, 1, "line is correct (again)");
 
   editor.openDialog = oldPrompt;
   finish();
 }
--- a/devtools/client/sourceeditor/editor.js
+++ b/devtools/client/sourceeditor/editor.js
@@ -29,16 +29,17 @@ const MAX_VERTICAL_OFFSET = 3;
 const RE_SCRATCHPAD_ERROR = /(?:@Scratchpad\/\d+:|\()(\d+):?(\d+)?(?:\)|\n)/;
 const RE_JUMP_TO_LINE = /^(\d+):?(\d+)?/;
 
 const Services = require("Services");
 const promise = require("promise");
 const events = require("devtools/shared/event-emitter");
 const { PrefObserver } = require("devtools/client/styleeditor/utils");
 const { getClientCssProperties } = require("devtools/shared/fronts/css-properties");
+const {KeyShortcuts} = require("devtools/client/shared/key-shortcuts");
 
 const {LocalizationHelper} = require("devtools/shared/l10n");
 const L10N = new LocalizationHelper("devtools/locale/sourceeditor.properties");
 
 const { OS } = Services.appinfo;
 
 // CM_STYLES, CM_SCRIPTS and CM_IFRAME represent the HTML,
 // JavaScript and CSS that is injected into an iframe in
@@ -376,75 +377,16 @@ Editor.prototype = {
       if (typeof popup == "string") {
         popup = doc.getElementById(this.config.contextMenu);
       }
 
       this.emit("popupOpen", ev, popup);
       popup.openPopupAtScreen(ev.screenX, ev.screenY, true);
     }, false);
 
-    // Intercept the find and find again keystroke on CodeMirror, to avoid
-    // the browser's search
-
-    let findKey = L10N.getStr("find.commandkey");
-    let findAgainKey = L10N.getStr("findAgain.commandkey");
-    let [accel, modifier] = OS === "Darwin"
-        ? ["metaKey", "altKey"]
-        : ["ctrlKey", "shiftKey"];
-
-    cm.getWrapperElement().addEventListener("keydown", ev => {
-      let key = ev.key.toUpperCase();
-      let node = ev.originalTarget;
-      let isInput = node.tagName === "INPUT";
-      let isSearchInput = isInput && node.type === "search";
-
-      // replace box is a different input instance than search, and it is
-      // located in a code mirror dialog
-      let isDialogInput = isInput &&
-          node.parentNode &&
-          node.parentNode.classList.contains("CodeMirror-dialog");
-
-      if (!ev[accel] || !(isSearchInput || isDialogInput)) {
-        return;
-      }
-
-      if (key === findKey) {
-        ev.preventDefault();
-
-        if (isSearchInput || ev[modifier]) {
-          node.select();
-        }
-      } else if (key === findAgainKey) {
-        ev.preventDefault();
-
-        if (!isSearchInput) {
-          return;
-        }
-
-        let query = node.value;
-
-        // If there isn't a search state, or the text in the input does not
-        // match with the current search state, we need to create a new one
-        if (!cm.state.search || cm.state.search.query !== query) {
-          cm.state.search = {
-            posFrom: null,
-            posTo: null,
-            overlay: null,
-            query
-          };
-        }
-
-        if (ev.shiftKey) {
-          cm.execCommand("findPrev");
-        } else {
-          cm.execCommand("findNext");
-        }
-      }
-    });
-
     cm.on("focus", () => this.emit("focus"));
     cm.on("scroll", () => this.emit("scroll"));
     cm.on("change", () => {
       this.emit("change");
       if (!this._lastDirty) {
         this._lastDirty = true;
         this.emit("dirty-change");
       }
@@ -463,21 +405,17 @@ Editor.prototype = {
 
       this.emit("gutterClick", line, ev.button);
     });
 
     win.CodeMirror.defineExtension("l10n", (name) => {
       return L10N.getStr(name);
     });
 
-    try {
-      cm.getInputField().controllers.insertControllerAt(0, controller(this));
-    } catch (e) {
-      console.warn("controller command is only supported in XUL");
-    }
+    this._initShortcuts(win);
 
     editors.set(this, cm);
 
     this.reloadPreferences = this.reloadPreferences.bind(this);
     this._prefObserver = new PrefObserver("devtools.editor.");
     this._prefObserver.on(TAB_SIZE, this.reloadPreferences);
     this._prefObserver.on(EXPAND_TAB, this.reloadPreferences);
     this._prefObserver.on(KEYMAP, this.reloadPreferences);
@@ -995,18 +933,17 @@ Editor.prototype = {
   /**
    * This method opens an in-editor dialog asking for a line to
    * jump to. Once given, it changes cursor to that line.
    */
   jumpToLine: function () {
     let doc = editors.get(this).getWrapperElement().ownerDocument;
     let div = doc.createElement("div");
     let inp = doc.createElement("input");
-    let txt =
-      doc.createTextNode(L10N.getStr("gotoLineCmd.promptTitle"));
+    let txt = doc.createTextNode(L10N.getStr("gotoLineCmd.promptTitle"));
 
     inp.type = "text";
     inp.style.width = "10em";
     inp.style.marginInlineStart = "1em";
 
     div.appendChild(txt);
     div.appendChild(inp);
 
@@ -1091,16 +1028,75 @@ Editor.prototype = {
     // value and maintain the selection of the text.
     cm.replaceRange(value, { line: start.line, ch: 0 },
       { line: end.line + 1, ch: cm.getLine(end.line + 1).length});
     cm.setSelection({ line: start.line + 1, ch: start.ch },
       { line: end.line + 1, ch: end.ch });
   },
 
   /**
+   * Intercept CodeMirror's Find and replace key shortcut to select the search input
+   */
+  findOrReplace: function (node, isReplaceAll) {
+    let cm = editors.get(this);
+    let isInput = node.tagName === "INPUT";
+    let isSearchInput = isInput && node.type === "search";
+    // replace box is a different input instance than search, and it is
+    // located in a code mirror dialog
+    let isDialogInput = isInput &&
+        node.parentNode &&
+        node.parentNode.classList.contains("CodeMirror-dialog");
+    if (!(isSearchInput || isDialogInput)) {
+      return;
+    }
+
+    if (isSearchInput || isReplaceAll) {
+      // select the search input
+      // it's the precise reason why we reimplement these key shortcuts
+      node.select();
+    }
+
+    // need to call it since we prevent the propagation of the event and
+    // cancel codemirror's key handling
+    cm.execCommand("find");
+  },
+
+  /**
+   * Intercept CodeMirror's findNext and findPrev key shortcut to allow
+   * immediately search for next occurance after typing a word to search.
+   */
+  findNextOrPrev: function (node, isFindPrev) {
+    let cm = editors.get(this);
+    let isInput = node.tagName === "INPUT";
+    let isSearchInput = isInput && node.type === "search";
+    if (!isSearchInput) {
+      return;
+    }
+    let query = node.value;
+    // cm.state.search allows to automatically start searching for the next occurance
+    // it's the precise reason why we reimplement these key shortcuts
+    if (!cm.state.search || cm.state.search.query !== query) {
+      cm.state.search = {
+        posFrom: null,
+        posTo: null,
+        overlay: null,
+        query
+      };
+    }
+
+    // need to call it since we prevent the propagation of the event and
+    // cancel codemirror's key handling
+    if (isFindPrev) {
+      cm.execCommand("findPrev");
+    } else {
+      cm.execCommand("findNext");
+    }
+  },
+
+  /**
    * Returns current font size for the editor area, in pixels.
    */
   getFontSize: function () {
     let cm = editors.get(this);
     let el = cm.getWrapperElement();
     let win = el.ownerDocument.defaultView;
 
     return parseInt(win.getComputedStyle(el).getPropertyValue("font-size"), 10);
@@ -1258,16 +1254,87 @@ Editor.prototype = {
       if (foldGutterIndex !== -1) {
         let gutters = this.config.gutters.slice();
         gutters.splice(foldGutterIndex, 1);
         this.setOption("gutters", gutters);
       }
 
       this.setOption("foldGutter", false);
     }
+  },
+
+  /**
+   * Register all key shortcuts.
+   */
+  _initShortcuts: function (win) {
+    let shortcuts = new KeyShortcuts({
+      window: win
+    });
+    this._onShortcut = this._onShortcut.bind(this);
+    let keys = [
+      "find.key",
+      "findNext.key",
+      "findPrev.key"
+    ];
+
+    if (OS === "Darwin") {
+      keys.push("replaceAllMac.key");
+    } else {
+      keys.push("replaceAll.key");
+    }
+    // Process generic keys:
+    keys.forEach(name => {
+      let key = L10N.getStr(name);
+      shortcuts.on(key, (_, event) => this._onShortcut(name, event));
+    });
+  },
+    /**
+   * Key shortcut listener.
+   */
+  _onShortcut: function (name, event) {
+    if (!this._isInputOrTextarea(event.target)) {
+      return;
+    }
+    let cm = editors.get(this);
+    let node = event.originalTarget;
+
+    switch (name) {
+      // replaceAll.key is Alt + find.key
+      case "replaceAllMac.key":
+        this.findOrReplace(node, true);
+        break;
+      // replaceAll.key is Shift + find.key
+      case "replaceAll.key":
+        this.findOrReplace(node, true);
+        break;
+      case "find.key":
+        this.findOrReplace(node, false);
+        break;
+      // findPrev.key is Shift + findNext.key
+      case "findPrev.key":
+        this.findNextOrPrev(node, true);
+        break;
+      case "findNext.key":
+        this.findNextOrPrev(node, false);
+        break;
+      default:
+        console.error("Unexpected editor key shortcut", name);
+        return;
+    }
+    // Prevent default for this action
+    event.stopPropagation();
+    event.preventDefault();
+  },
+
+  /**
+   * Check if a node is an input or textarea
+   */
+  _isInputOrTextarea: function (element) {
+    let name = element.tagName.toLowerCase();
+    return name === "input" || name === "textarea";
   }
 };
 
 // Since Editor is a thin layer over CodeMirror some methods
 // are mapped directly—without any changes.
 
 CM_MAPPING.forEach(name => {
   Editor.prototype[name] = function (...args) {
@@ -1337,78 +1404,9 @@ function getCSSKeywords(cssProperties) {
 
   return {
     propertyKeywords: keySet(propertyKeywords),
     colorKeywords: colorKeywords,
     valueKeywords: valueKeywords
   };
 }
 
-/**
- * Returns a controller object that can be used for
- * editor-specific commands such as find, jump to line,
- * copy/paste, etc.
- */
-function controller(ed) {
-  return {
-    supportsCommand: function (cmd) {
-      switch (cmd) {
-        case "cmd_find":
-        case "cmd_findAgain":
-        case "cmd_findPrevious":
-        case "cmd_gotoLine":
-        case "cmd_undo":
-        case "cmd_redo":
-        case "cmd_delete":
-        case "cmd_selectAll":
-          return true;
-      }
-
-      return false;
-    },
-
-    isCommandEnabled: function (cmd) {
-      let cm = editors.get(ed);
-
-      switch (cmd) {
-        case "cmd_find":
-        case "cmd_gotoLine":
-        case "cmd_selectAll":
-          return true;
-        case "cmd_findAgain":
-          return cm.state.search != null && cm.state.search.query != null;
-        case "cmd_undo":
-          return ed.canUndo();
-        case "cmd_redo":
-          return ed.canRedo();
-        case "cmd_delete":
-          return ed.somethingSelected();
-      }
-
-      return false;
-    },
-
-    doCommand: function (cmd) {
-      let cm = editors.get(ed);
-      let map = {
-        "cmd_selectAll": "selectAll",
-        "cmd_find": "find",
-        "cmd_undo": "undo",
-        "cmd_redo": "redo",
-        "cmd_delete": "delCharAfter",
-        "cmd_findAgain": "findNext"
-      };
-
-      if (map[cmd]) {
-        cm.execCommand(map[cmd]);
-        return;
-      }
-
-      if (cmd == "cmd_gotoLine") {
-        ed.jumpToLine();
-      }
-    },
-
-    onEvent: function () {}
-  };
-}
-
 module.exports = Editor;
--- a/devtools/client/sourceeditor/test/browser.ini
+++ b/devtools/client/sourceeditor/test/browser.ini
@@ -18,16 +18,17 @@ support-files =
   codemirror/mode/javascript/test.js
   css_statemachine_testcases.css
   css_statemachine_tests.json
   css_autocompletion_tests.json
   head.js
   helper_codemirror_runner.js
   cm_mode_ruby.js
   cm_script_injection_test.js
+  !/devtools/client/framework/test/shared-head.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_find_again.js]
 [browser_editor_goto_line.js]
--- a/devtools/client/sourceeditor/test/browser_css_autocompletion.js
+++ b/devtools/client/sourceeditor/test/browser_css_autocompletion.js
@@ -1,18 +1,16 @@
 /* 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 CSSCompleter = require("devtools/client/sourceeditor/css-autocompleter");
 const {InspectorFront} = require("devtools/shared/fronts/inspector");
-const {TargetFactory} = require("devtools/client/framework/target");
-const { Cc, Ci } = require("chrome");
 
 const CSS_URI = "http://mochi.test:8888/browser/devtools/client/sourceeditor" +
                 "/test/css_statemachine_testcases.css";
 const TESTS_URI = "http://mochi.test:8888/browser/devtools/client" +
                   "/sourceeditor/test/css_autocompletion_tests.json";
 
 const source = read(CSS_URI);
 const tests = eval(read(TESTS_URI));
--- a/devtools/client/sourceeditor/test/browser_editor_autocomplete_events.js
+++ b/devtools/client/sourceeditor/test/browser_editor_autocomplete_events.js
@@ -1,24 +1,21 @@
 /* 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/shared/fronts/inspector");
-const {TargetFactory} = require("devtools/client/framework/target");
 const AUTOCOMPLETION_PREF = "devtools.editor.autocomplete";
 const TEST_URI = "data:text/html;charset=UTF-8,<html><body><bar></bar>" +
                  "<div id='baz'></div><body></html>";
 
-const wait = (delay) => new Promise(resolve => setTimeout(resolve, delay));
-
 add_task(function* () {
-  yield promiseTab(TEST_URI);
+  yield addTab(TEST_URI);
   yield runTests();
 });
 
 function* runTests() {
   let target = TargetFactory.forTab(gBrowser.selectedTab);
   yield target.makeRemote();
   let inspector = InspectorFront(target.client, target.form);
   let walker = yield inspector.getWalker();
--- a/devtools/client/sourceeditor/test/browser_editor_find_again.js
+++ b/devtools/client/sourceeditor/test/browser_editor_find_again.js
@@ -3,22 +3,25 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 const {LocalizationHelper} = require("devtools/shared/l10n");
 const L10N = new LocalizationHelper("devtools/locale/sourceeditor.properties");
 
-const FIND_KEY = L10N.getStr("find.commandkey");
-const FINDAGAIN_KEY = L10N.getStr("findAgain.commandkey");
-
 const { OS } = Services.appinfo;
 
 // On linux, getting immediately the selection's range here fails, returning
+const FIND_KEY = L10N.getStr("find.key");
+const FINDNEXT_KEY = L10N.getStr("findNext.key");
+const FINDPREV_KEY = L10N.getStr("findPrev.key");
+// the replace's key with the appropriate modifiers based on OS
+const REPLACE_KEY = OS == "Darwin" ? L10N.getStr("replaceAllMac.key") : L10N.getStr("replaceAll.key");
+
 // values like it's not selected – even if the selection is visible.
 // For the record, setting the selection's range immediately doesn't have
 // any effect.
 // It's like the <input> is not ready yet.
 // Therefore, we trigger the UI focus event to the <input>, waiting for the
 // response.
 // Using a timeout could also work, but that is more precise, ensuring also
 // the execution of the listeners added to the <input>'s focus.
@@ -36,34 +39,37 @@ function openSearchBox(ed) {
   let edWin = edDoc.defaultView;
 
   let input = edDoc.querySelector("input[type=search]");
   ok(!input, "search box closed");
 
   // The editor needs the focus to properly receive the `synthesizeKey`
   ed.focus();
 
-  EventUtils.synthesizeKey(FINDAGAIN_KEY, { accelKey: true }, edWin);
-
+  synthesizeKeyShortcut(FINDNEXT_KEY, edWin);
   input = edDoc.querySelector("input[type=search]");
   ok(input, "find again command key opens the search box");
 }
 
-function testFindAgain(ed, inputLine, expectCursor, shiftKey = false) {
+function testFindAgain(ed, inputLine, expectCursor, isFindPrev = false) {
   let edDoc = ed.container.contentDocument;
   let edWin = edDoc.defaultView;
 
   let input = edDoc.querySelector("input[type=search]");
   input.value = inputLine;
 
   // Ensure the input has the focus before send the key – necessary on Linux,
   // it seems that during the tests can be lost
   input.focus();
 
-  EventUtils.synthesizeKey(FINDAGAIN_KEY, { accelKey: true, shiftKey }, edWin);
+  if (isFindPrev) {
+    synthesizeKeyShortcut(FINDPREV_KEY, edWin);
+  } else {
+    synthesizeKeyShortcut(FINDNEXT_KEY, edWin);
+  }
 
   ch(ed.getCursor(), expectCursor,
     "find: " + inputLine + " expects cursor: " + expectCursor.toSource());
 }
 
 const testSearchBoxTextIsSelected = Task.async(function* (ed) {
   let edDoc = ed.container.contentDocument;
   let edWin = edDoc.defaultView;
@@ -77,32 +83,32 @@ const testSearchBoxTextIsSelected = Task
 
   // Close search box
   EventUtils.synthesizeKey("VK_ESCAPE", {}, edWin);
 
   input = edDoc.querySelector("input[type=search]");
   ok(!input, "search box is closed");
 
   // Re-open the search box
-  EventUtils.synthesizeKey(FIND_KEY, { accelKey: true }, edWin);
+  synthesizeKeyShortcut(FIND_KEY, edWin);
 
   input = edDoc.querySelector("input[type=search]");
   ok(input, "find command key opens the search box");
 
   yield dispatchAndWaitForFocus(input);
 
   let { selectionStart, selectionEnd, value } = input;
 
   ok(selectionStart === 0 && selectionEnd === value.length,
     "search box's text is selected when re-opened");
 
   // Removing selection
   input.setSelectionRange(0, 0);
 
-  EventUtils.synthesizeKey(FIND_KEY, { accelKey: true }, edWin);
+  synthesizeKeyShortcut(FIND_KEY, edWin);
 
   ({ selectionStart, selectionEnd } = input);
 
   ok(selectionStart === 0 && selectionEnd === value.length,
     "search box's text is selected when find key is pressed");
 
   // Close search box
   EventUtils.synthesizeKey("VK_ESCAPE", {}, edWin);
@@ -113,21 +119,17 @@ const testReplaceBoxTextIsSelected = Tas
   let edWin = edDoc.defaultView;
 
   let input = edDoc.querySelector(".CodeMirror-dialog > input");
   ok(!input, "dialog box with replace is closed");
 
   // The editor needs the focus to properly receive the `synthesizeKey`
   ed.focus();
 
-  // Send the replace's key with the appropriate modifiers based on OS
-  let [altKey, shiftKey] = OS === "Darwin" ? [true, false] : [false, true];
-
-  EventUtils.synthesizeKey(FIND_KEY,
-    { accelKey: true, altKey, shiftKey }, edWin);
+  synthesizeKeyShortcut(REPLACE_KEY, edWin);
 
   input = edDoc.querySelector(".CodeMirror-dialog > input");
   ok(input, "dialog box with replace is opened");
 
   input.value = "line 5";
 
   // Ensure the input has the focus before send the key – necessary on Linux,
   // it seems that during the tests can be lost
@@ -135,18 +137,17 @@ const testReplaceBoxTextIsSelected = Tas
 
   yield dispatchAndWaitForFocus(input);
 
   let { selectionStart, selectionEnd, value } = input;
 
   ok(!(selectionStart === 0 && selectionEnd === value.length),
     "Text in dialog box is not selected");
 
-  EventUtils.synthesizeKey(FIND_KEY,
-    { accelKey: true, altKey, shiftKey }, edWin);
+  synthesizeKeyShortcut(REPLACE_KEY, edWin);
 
   ({ selectionStart, selectionEnd } = input);
 
   ok(selectionStart === 0 && selectionEnd === value.length,
     "dialog box's text is selected when replace key is pressed");
 
   // Close dialog box
   EventUtils.synthesizeKey("VK_ESCAPE", {}, edWin);
--- a/devtools/client/sourceeditor/test/head.js
+++ b/devtools/client/sourceeditor/test/head.js
@@ -1,49 +1,28 @@
 /* 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 ../../framework/test/shared-head.js */
 "use strict";
 
-const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+// shared-head.js handles imports, constants, and utility functions
+Services.scriptloader.loadSubScript(
+  "chrome://mochitests/content/browser/devtools/client/framework/test/shared-head.js",
+  this);
+
 const { NetUtil } = require("resource://gre/modules/NetUtil.jsm");
 const Editor = require("devtools/client/sourceeditor/editor");
-const promise = require("promise");
-const flags = require("devtools/shared/flags");
 const {getClientCssProperties} = require("devtools/shared/fronts/css-properties");
 
 flags.testing = true;
 SimpleTest.registerCleanupFunction(() => {
   flags.testing = false;
 });
 
-/**
- * Open a new tab at a URL and call a callback on load
- */
-function addTab(url, callback) {
-  waitForExplicitFinish();
-
-  gBrowser.selectedTab = gBrowser.addTab(url);
-  let tab = gBrowser.selectedTab;
-  let browser = gBrowser.getBrowserForTab(tab);
-
-  return BrowserTestUtils.browserLoaded(browser).then(function () {
-    if (typeof(callback) == "function") {
-      callback(browser, tab, browser.contentDocument);
-    }
-    return tab;
-  });
-}
-
-function promiseTab(url) {
-  return new Promise(resolve =>
-    addTab(url, resolve));
-}
-
 function promiseWaitForFocus() {
   return new Promise(resolve =>
     waitForFocus(resolve));
 }
 
 function setup(cb, additionalOpts = {}) {
   cb = cb || function () {};
   let def = promise.defer();