Bug 1342928 - Keep the commands / buttons state in sync; r=jwalker draft
authorMatteo Ferretti <mferretti@mozilla.com>
Wed, 15 Mar 2017 18:05:05 +0100
changeset 499904 93a7d714c37edbf6076df252d2390b3762eea87a
parent 499202 8c704b823d38f479ad228c585439ae41100be166
child 549513 60a08b3516dde49d8ba0d0a9476a20898b54f8cf
push id49585
push userbmo:zer0@mozilla.com
push dateThu, 16 Mar 2017 12:11:02 +0000
reviewersjwalker
bugs1342928, 1320149
milestone55.0a1
Bug 1342928 - Keep the commands / buttons state in sync; r=jwalker This was a regression given by bug 1320149; in order to keep the performance gain I created a lightweight object (`CommandState`) that is required from both gcli's command and toolbox so that the last one doesn't need to be strong coupled with the first one. Also fixing an inconsistency of buttons' state when we enter/exit in/from RDM. MozReview-Commit-ID: 3NcTt6i4ezx
devtools/client/definitions.js
devtools/client/framework/toolbox.js
devtools/client/responsive.html/browser/swap.js
devtools/shared/event-emitter.js
devtools/shared/gcli/command-state.js
devtools/shared/gcli/commands/measure.js
devtools/shared/gcli/commands/paintflashing.js
devtools/shared/gcli/commands/rulers.js
devtools/shared/gcli/moz.build
--- a/devtools/client/definitions.js
+++ b/devtools/client/definitions.js
@@ -21,16 +21,17 @@ loader.lazyGetter(this, "MemoryPanel", (
 loader.lazyGetter(this, "PerformancePanel", () => require("devtools/client/performance/panel").PerformancePanel);
 loader.lazyGetter(this, "NetMonitorPanel", () => require("devtools/client/netmonitor/panel").NetMonitorPanel);
 loader.lazyGetter(this, "StoragePanel", () => require("devtools/client/storage/panel").StoragePanel);
 loader.lazyGetter(this, "ScratchpadPanel", () => require("devtools/client/scratchpad/scratchpad-panel").ScratchpadPanel);
 loader.lazyGetter(this, "DomPanel", () => require("devtools/client/dom/dom-panel").DomPanel);
 
 // Other dependencies
 loader.lazyRequireGetter(this, "CommandUtils", "devtools/client/shared/developer-toolbar", true);
+loader.lazyRequireGetter(this, "CommandState", "devtools/shared/gcli/command-state", true);
 loader.lazyImporter(this, "ResponsiveUIManager", "resource://devtools/client/responsivedesign/responsivedesign.jsm");
 loader.lazyImporter(this, "ScratchpadManager", "resource://devtools/client/scratchpad/scratchpad-manager.jsm");
 
 const {LocalizationHelper} = require("devtools/shared/l10n");
 const L10N = new LocalizationHelper("devtools/client/locales/startup.properties");
 
 var Tools = {};
 exports.Tools = Tools;
@@ -492,17 +493,25 @@ exports.ToolboxButtons = [
     }
   },
   { id: "command-button-paintflashing",
     description: l10n("toolbox.buttons.paintflashing"),
     isTargetSupported: target => target.isLocalTab,
     onClick(event, toolbox) {
       CommandUtils.executeOnTarget(toolbox.target, "paintflashing toggle");
     },
-    autoToggle: true
+    isChecked(toolbox) {
+      return CommandState.isEnabledForTarget(toolbox.target, "paintflashing");
+    },
+    setup(toolbox, onChange) {
+      CommandState.on("changed", onChange);
+    },
+    teardown(toolbox, onChange) {
+      CommandState.off("changed", onChange);
+    }
   },
   { id: "command-button-scratchpad",
     description: l10n("toolbox.buttons.scratchpad"),
     isTargetSupported: target => target.isLocalTab,
     onClick(event, toolbox) {
       ScratchpadManager.openScratchpad();
     }
   },
@@ -547,25 +556,41 @@ exports.ToolboxButtons = [
     }
   },
   { id: "command-button-rulers",
     description: l10n("toolbox.buttons.rulers"),
     isTargetSupported: target => target.isLocalTab,
     onClick(event, toolbox) {
       CommandUtils.executeOnTarget(toolbox.target, "rulers");
     },
-    autoToggle: true
+    isChecked(toolbox) {
+      return CommandState.isEnabledForTarget(toolbox.target, "rulers");
+    },
+    setup(toolbox, onChange) {
+      CommandState.on("changed", onChange);
+    },
+    teardown(toolbox, onChange) {
+      CommandState.off("changed", onChange);
+    }
   },
   { id: "command-button-measure",
     description: l10n("toolbox.buttons.measure"),
     isTargetSupported: target => target.isLocalTab,
     onClick(event, toolbox) {
       CommandUtils.executeOnTarget(toolbox.target, "measure");
     },
-    autoToggle: true
+    isChecked(toolbox) {
+      return CommandState.isEnabledForTarget(toolbox.target, "measure");
+    },
+    setup(toolbox, onChange) {
+      CommandState.on("changed", onChange);
+    },
+    teardown(toolbox, onChange) {
+      CommandState.off("changed", onChange);
+    }
   },
 ];
 
 /**
  * Lookup l10n string from a string bundle.
  *
  * @param {string} name
  *        The key to lookup.
--- a/devtools/client/framework/toolbox.js
+++ b/devtools/client/framework/toolbox.js
@@ -569,35 +569,30 @@ Toolbox.prototype = {
    *                      memory leaks. The same arguments than `setup` function are
    *                      passed to `teardown`.
    * @property {Function} isTargetSupported - Function to automatically enable/disable
    *                      the button based on the target. If the target don't support
    *                      the button feature, this method should return false.
    * @property {Function} isChecked - Optional function called to known if the button
    *                      is toggled or not. The function should return true when
    *                      the button should be displayed as toggled on.
-   * @property {Boolean}  autoToggle - If true, the checked state is going to be
-   *                      automatically toggled on click.
    */
   _createButtonState: function (options) {
     let isCheckedValue = false;
     const { id, className, description, onClick, isInStartContainer, setup, teardown,
-            isTargetSupported, isChecked, autoToggle } = options;
+            isTargetSupported, isChecked } = options;
     const toolbox = this;
     const button = {
       id,
       className,
       description,
       onClick(event) {
         if (typeof onClick == "function") {
           onClick(event, toolbox);
         }
-        if (autoToggle) {
-          button.isChecked = !button.isChecked;
-        }
       },
       isTargetSupported,
       get isChecked() {
         if (typeof isChecked == "function") {
           return isChecked(toolbox);
         }
         return isCheckedValue;
       },
--- a/devtools/client/responsive.html/browser/swap.js
+++ b/devtools/client/responsive.html/browser/swap.js
@@ -3,16 +3,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const { Ci } = require("chrome");
 const promise = require("promise");
 const { Task } = require("devtools/shared/task");
 const { tunnelToInnerBrowser } = require("./tunnel");
+const { CommandState } = require("devtools/shared/gcli/command-state");
 
 /**
  * Swap page content from an existing tab into a new browser within a container
  * page.  Page state is preserved by using `swapFrameLoaders`, just like when
  * you move a tab to a new window.  This provides a seamless transition for the
  * user since the page is not reloaded.
  *
  * See /devtools/docs/responsive-design-mode.md for a high level overview of how
@@ -135,16 +136,21 @@ function swapToInnerBrowser({ tab, conta
         }
       }
 
       // Force the browser UI to match the new state of the tab and browser.
       thawNavigationState(tab);
       gBrowser.setTabTitle(tab);
       gBrowser.updateCurrentBrowser(true);
 
+      // Since bug 1341756 when we're entering in RDM we're clearing any highlighters;
+      // so we have to update the command's state too.
+      CommandState.disableForTarget({tab}, "measure");
+      CommandState.disableForTarget({tab}, "rulers");
+
       // Show the browser content again now that the move is done.
       tab.linkedBrowser.style.visibility = "";
     }),
 
     stop() {
       // Hide the browser content temporarily while things move around to avoid displaying
       // strange intermediate states.
       tab.linkedBrowser.style.visibility = "hidden";
@@ -204,16 +210,21 @@ function swapToInnerBrowser({ tab, conta
 
       // The focus manager seems to get a little dizzy after all this swapping.  If a
       // content element had been focused inside the viewport before stopping, it will
       // have lost focus.  Activate the frame to restore expected focus.
       tab.linkedBrowser.frameLoader.activateRemoteFrame();
 
       delete tab.isResponsiveDesignMode;
 
+      // Since bug 1341756 when we're exiting from RDM we're clearing any highlighters;
+      // so we have to update the command's state too.
+      CommandState.disableForTarget({tab}, "measure");
+      CommandState.disableForTarget({tab}, "rulers");
+
       // Show the browser content again now that the move is done.
       tab.linkedBrowser.style.visibility = "";
     },
 
   };
 }
 
 /**
--- a/devtools/shared/event-emitter.js
+++ b/devtools/shared/event-emitter.js
@@ -79,23 +79,26 @@
   }
 
   /**
    * Decorate an object with event emitter functionality.
    *
    * @param Object objectToDecorate
    *        Bind all public methods of EventEmitter to
    *        the objectToDecorate object.
+   * @return Object the object given.
    */
   EventEmitter.decorate = function (objectToDecorate) {
     let emitter = new EventEmitter();
     objectToDecorate.on = emitter.on.bind(emitter);
     objectToDecorate.off = emitter.off.bind(emitter);
     objectToDecorate.once = emitter.once.bind(emitter);
     objectToDecorate.emit = emitter.emit.bind(emitter);
+
+    return objectToDecorate;
   };
 
   EventEmitter.prototype = {
     /**
      * Connect a listener.
      *
      * @param string event
      *        The event name to which we're connecting.
new file mode 100644
--- /dev/null
+++ b/devtools/shared/gcli/command-state.js
@@ -0,0 +1,80 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const EventEmitter = require("devtools/shared/event-emitter");
+
+loader.lazyRequireGetter(this, "getBrowserForTab", "sdk/tabs/utils", true);
+
+const getTargetId = ({tab}) => getBrowserForTab(tab).outerWindowID;
+const enabledCommands = new Map();
+
+/**
+ * The `CommandState` is a singleton that provides utility methods to keep the commands'
+ * state in sync between the toolbox, the toolbar and the content.
+ */
+const CommandState = EventEmitter.decorate({
+  /**
+   * Returns if a command is enabled on a given target.
+   *
+   * @param {Object} target
+   *                  The target object must have a tab's reference.
+   * @param {String} command
+  *                  The command's name used in gcli.
+   * @returns {Boolean} returns `false` if the command is not enabled for the target
+   *                    given, or if the target given hasn't a tab; `true` otherwise.
+   */
+  isEnabledForTarget(target, command) {
+    if (!target.tab || !enabledCommands.has(command)) {
+      return false;
+    }
+
+    return enabledCommands.get(command).has(getTargetId(target));
+  },
+
+  /**
+   * Enables a command on a given target.
+   * Emits a "changed" event to notify potential observers about the new commands state.
+   *
+   * @param {Object} target
+   *                  The target object must have a tab's reference.
+   * @param {String} command
+  *                  The command's name used in gcli.
+   */
+  enableForTarget(target, command) {
+    if (!target.tab) {
+      return;
+    }
+
+    if (!enabledCommands.has(command)) {
+      enabledCommands.set(command, new Set());
+    }
+
+    enabledCommands.get(command).add(getTargetId(target));
+
+    CommandState.emit("changed", {target, command});
+  },
+
+  /**
+   * Disabled a command on a given target.
+   * Emits a "changed" event to notify potential observers about the new commands state.
+   *
+   * @param {Object} target
+   *                  The target object must have a tab's reference.
+   * @param {String} command
+  *                  The command's name used in gcli.
+   */
+  disableForTarget(target, command) {
+    if (!target.tab || !enabledCommands.has(command)) {
+      return;
+    }
+
+    enabledCommands.get(command).delete(getTargetId(target));
+
+    CommandState.emit("changed", {target, command});
+  },
+});
+exports.CommandState = CommandState;
+
--- a/devtools/shared/gcli/commands/measure.js
+++ b/devtools/shared/gcli/commands/measure.js
@@ -1,94 +1,80 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
  /* globals getOuterId, getBrowserForTab */
 
 "use strict";
 
-const EventEmitter = require("devtools/shared/event-emitter");
-const eventEmitter = new EventEmitter();
 const events = require("sdk/event/core");
 
-loader.lazyRequireGetter(this, "getOuterId", "sdk/window/utils", true);
-loader.lazyRequireGetter(this, "getBrowserForTab", "sdk/tabs/utils", true);
+loader.lazyRequireGetter(this, "CommandState",
+  "devtools/shared/gcli/command-state", true);
 
 const l10n = require("gcli/l10n");
 require("devtools/server/actors/inspector");
 const { MeasuringToolHighlighter, HighlighterEnvironment } =
   require("devtools/server/actors/highlighters");
 
 const highlighters = new WeakMap();
-const visibleHighlighters = new Set();
-
-const isCheckedFor = (tab) =>
-  tab ? visibleHighlighters.has(getBrowserForTab(tab).outerWindowID) : false;
 
 exports.items = [
   // The client measure command is used to maintain the toolbar button state
   // only and redirects to the server command to actually toggle the measuring
   // tool (see `measure_server` below).
   {
     name: "measure",
     runAt: "client",
     description: l10n.lookup("measureDesc"),
     manual: l10n.lookup("measureManual"),
     buttonId: "command-button-measure",
     buttonClass: "command-button command-button-invertable",
     tooltipText: l10n.lookup("measureTooltip"),
     state: {
-      isChecked: ({_tab}) => isCheckedFor(_tab),
-      onChange: (target, handler) => eventEmitter.on("changed", handler),
-      offChange: (target, handler) => eventEmitter.off("changed", handler)
+      isChecked: (target) => CommandState.isEnabledForTarget(target, "measure"),
+      onChange: (target, handler) => CommandState.on("changed", handler),
+      offChange: (target, handler) => CommandState.off("changed", handler)
     },
     exec: function* (args, context) {
       let { target } = context.environment;
 
       // Pipe the call to the server command.
       let response = yield context.updateExec("measure_server");
-      let { visible, id } = response.data;
+      let isEnabled = response.data;
 
-      if (visible) {
-        visibleHighlighters.add(id);
+      if (isEnabled) {
+        CommandState.enableForTarget(target, "measure");
       } else {
-        visibleHighlighters.delete(id);
+        CommandState.disableForTarget(target, "measure");
       }
 
-      eventEmitter.emit("changed", { target });
-
       // Toggle off the button when the page navigates because the measuring
       // tool is removed automatically by the MeasuringToolHighlighter on the
       // server then.
-      let onNavigate = () => {
-        visibleHighlighters.delete(id);
-        eventEmitter.emit("changed", { target });
-      };
-      target.off("will-navigate", onNavigate);
-      target.once("will-navigate", onNavigate);
+      target.once("will-navigate", () => CommandState.disableForTarget(target, "measure"));
     }
   },
   // The server measure command is hidden by default, it's just used by the
   // client command.
   {
     name: "measure_server",
     runAt: "server",
     hidden: true,
     returnType: "highlighterVisibility",
     exec: function (args, context) {
       let env = context.environment;
       let { document } = env;
-      let id = getOuterId(env.window);
 
       // Calling the command again after the measuring tool has been shown once,
       // hides it.
       if (highlighters.has(document)) {
         let { highlighter } = highlighters.get(document);
         highlighter.destroy();
-        return {visible: false, id};
+        return false;
       }
 
       // Otherwise, display the measuring tool.
       let environment = new HighlighterEnvironment();
       environment.initFromWindow(env.window);
       let highlighter = new MeasuringToolHighlighter(environment);
 
       // Store the instance of the measuring tool highlighter for this document
@@ -101,12 +87,12 @@ exports.items = [
         if (highlighters.has(document)) {
           let { environment: toDestroy } = highlighters.get(document);
           toDestroy.destroy();
           highlighters.delete(document);
         }
       });
 
       highlighter.show();
-      return {visible: true, id};
+      return true;
     }
   }
 ];
--- a/devtools/shared/gcli/commands/paintflashing.js
+++ b/devtools/shared/gcli/commands/paintflashing.js
@@ -1,59 +1,41 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const { Ci } = require("chrome");
-loader.lazyRequireGetter(this, "getOuterId", "sdk/window/utils", true);
-loader.lazyRequireGetter(this, "getBrowserForTab", "sdk/tabs/utils", true);
+
+loader.lazyRequireGetter(this, "CommandState",
+  "devtools/shared/gcli/command-state", true);
 
 var telemetry;
 try {
   const Telemetry = require("devtools/client/shared/telemetry");
   telemetry = new Telemetry();
 } catch (e) {
   // DevTools Telemetry module only available in Firefox
 }
 
-const EventEmitter = require("devtools/shared/event-emitter");
-const eventEmitter = new EventEmitter();
-
-// exports the event emitter to help test know when this command is toggled
-exports.eventEmitter = eventEmitter;
-
 const gcli = require("gcli/index");
 const l10n = require("gcli/l10n");
 
-const enabledPaintFlashing = new Set();
-
-const isCheckedFor = (tab) =>
-  tab ? enabledPaintFlashing.has(getBrowserForTab(tab).outerWindowID) : false;
-
 /**
  * Fire events and telemetry when paintFlashing happens
  */
-function onPaintFlashingChanged(target, state) {
-  const { flashing, id } = state;
-
+function onPaintFlashingChanged(target, flashing) {
   if (flashing) {
-    enabledPaintFlashing.add(id);
+    CommandState.enableForTarget(target, "paintflashing");
   } else {
-    enabledPaintFlashing.delete(id);
+    CommandState.disableForTarget(target, "paintflashing");
   }
 
-  eventEmitter.emit("changed", { target: target });
-  function fireChange() {
-    eventEmitter.emit("changed", { target: target });
-  }
-
-  target.off("navigate", fireChange);
-  target.once("navigate", fireChange);
+  target.once("will-navigate", () => CommandState.disableForTarget(target, "paintflashing"));
 
   if (!telemetry) {
     return;
   }
   if (flashing) {
     telemetry.toolOpened("paintflashing");
   } else {
     telemetry.toolClosed("paintflashing");
@@ -160,19 +142,19 @@ exports.items = [
   {
     item: "command",
     runAt: "client",
     name: "paintflashing toggle",
     hidden: true,
     buttonId: "command-button-paintflashing",
     buttonClass: "command-button command-button-invertable",
     state: {
-      isChecked: ({_tab}) => isCheckedFor(_tab),
-      onChange: (_, handler) => eventEmitter.on("changed", handler),
-      offChange: (_, handler) => eventEmitter.off("changed", handler),
+      isChecked: (target) => CommandState.isEnabledForTarget(target, "paintflashing"),
+      onChange: (_, handler) => CommandState.on("changed", handler),
+      offChange: (_, handler) => CommandState.off("changed", handler),
     },
     tooltipText: l10n.lookup("paintflashingTooltip"),
     description: l10n.lookup("paintflashingToggleDesc"),
     manual: l10n.lookup("paintflashingManual"),
     exec: function* (args, context) {
       const output = yield context.updateExec("paintflashing_server --state toggle");
 
       onPaintFlashingChanged(context.environment.target, output.data);
@@ -190,15 +172,13 @@ exports.items = [
           name: "selection",
           data: [ "on", "off", "toggle", "query" ]
         }
       },
     ],
     returnType: "paintFlashingState",
     exec: function (args, context) {
       let { window } = context.environment;
-      let id = getOuterId(window);
-      let flashing = setPaintFlashing(window, args.state);
 
-      return { flashing, id };
+      return setPaintFlashing(window, args.state);
     }
   }
 ];
--- a/devtools/shared/gcli/commands/rulers.js
+++ b/devtools/shared/gcli/commands/rulers.js
@@ -1,92 +1,79 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-/* globals getBrowserForTab */
 
 "use strict";
 
-const EventEmitter = require("devtools/shared/event-emitter");
-const eventEmitter = new EventEmitter();
 const events = require("sdk/event/core");
+
 loader.lazyRequireGetter(this, "getOuterId", "sdk/window/utils", true);
-loader.lazyRequireGetter(this, "getBrowserForTab", "sdk/tabs/utils", true);
+loader.lazyRequireGetter(this, "CommandState",
+  "devtools/shared/gcli/command-state", true);
 
 const l10n = require("gcli/l10n");
 require("devtools/server/actors/inspector");
 const { RulersHighlighter, HighlighterEnvironment } =
   require("devtools/server/actors/highlighters");
 
 const highlighters = new WeakMap();
-const visibleHighlighters = new Set();
-
-const isCheckedFor = (tab) =>
-  tab ? visibleHighlighters.has(getBrowserForTab(tab).outerWindowID) : false;
 
 exports.items = [
   // The client rulers command is used to maintain the toolbar button state only
   // and redirects to the server command to actually toggle the rulers (see
   // rulers_server below).
   {
     name: "rulers",
     runAt: "client",
     description: l10n.lookup("rulersDesc"),
     manual: l10n.lookup("rulersManual"),
     buttonId: "command-button-rulers",
     buttonClass: "command-button command-button-invertable",
     tooltipText: l10n.lookup("rulersTooltip"),
     state: {
-      isChecked: ({_tab}) => isCheckedFor(_tab),
-      onChange: (target, handler) => eventEmitter.on("changed", handler),
-      offChange: (target, handler) => eventEmitter.off("changed", handler)
+      isChecked: (target) => CommandState.isEnabledForTarget(target, "rulers"),
+      onChange: (target, handler) => CommandState.on("changed", handler),
+      offChange: (target, handler) => CommandState.off("changed", handler)
     },
     exec: function* (args, context) {
       let { target } = context.environment;
 
       // Pipe the call to the server command.
       let response = yield context.updateExec("rulers_server");
-      let { visible, id } = response.data;
+      let isEnabled = response.data;
 
-      if (visible) {
-        visibleHighlighters.add(id);
+      if (isEnabled) {
+        CommandState.enableForTarget(target, "rulers");
       } else {
-        visibleHighlighters.delete(id);
+        CommandState.disableForTarget(target, "rulers");
       }
 
-      eventEmitter.emit("changed", { target });
-
       // Toggle off the button when the page navigates because the rulers are
       // removed automatically by the RulersHighlighter on the server then.
-      let onNavigate = () => {
-        visibleHighlighters.delete(id);
-        eventEmitter.emit("changed", { target });
-      };
-      target.off("will-navigate", onNavigate);
-      target.once("will-navigate", onNavigate);
+      target.once("will-navigate", () => CommandState.disableForTarget(target, "rulers"));
     }
   },
   // The server rulers command is hidden by default, it's just used by the
   // client command.
   {
     name: "rulers_server",
     runAt: "server",
     hidden: true,
     returnType: "highlighterVisibility",
     exec: function (args, context) {
       let env = context.environment;
       let { document } = env;
-      let id = getOuterId(env.window);
 
       // Calling the command again after the rulers have been shown once hides
       // them.
       if (highlighters.has(document)) {
         let { highlighter } = highlighters.get(document);
         highlighter.destroy();
-        return {visible: false, id};
+        return false;
       }
 
       // Otherwise, display the rulers.
       let environment = new HighlighterEnvironment();
       environment.initFromWindow(env.window);
       let highlighter = new RulersHighlighter(environment);
 
       // Store the instance of the rulers highlighter for this document so we
@@ -99,12 +86,12 @@ exports.items = [
         if (highlighters.has(document)) {
           let { environment: toDestroy } = highlighters.get(document);
           toDestroy.destroy();
           highlighters.delete(document);
         }
       });
 
       highlighter.show();
-      return {visible: true, id};
+      return true;
     }
   }
 ];
--- a/devtools/shared/gcli/moz.build
+++ b/devtools/shared/gcli/moz.build
@@ -14,10 +14,11 @@ DIRS += [
     'source/lib/gcli/languages',
     'source/lib/gcli/mozui',
     'source/lib/gcli/types',
     'source/lib/gcli/ui',
     'source/lib/gcli/util',
 ]
 
 DevToolsModules(
+    'command-state.js',
     'templater.js'
 )