tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

webconsole-ui.js (24320B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 "use strict";
      6 
      7 const EventEmitter = require("resource://devtools/shared/event-emitter.js");
      8 const KeyShortcuts = require("resource://devtools/client/shared/key-shortcuts.js");
      9 const {
     10  l10n,
     11 } = require("resource://devtools/client/webconsole/utils/messages.js");
     12 
     13 const { BrowserLoader } = ChromeUtils.importESModule(
     14  "resource://devtools/shared/loader/browser-loader.sys.mjs"
     15 );
     16 const {
     17  getAdHocFrontOrPrimitiveGrip,
     18 } = require("resource://devtools/client/fronts/object.js");
     19 
     20 const {
     21  PREFS,
     22  FILTERS,
     23 } = require("resource://devtools/client/webconsole/constants.js");
     24 
     25 const FirefoxDataProvider = require("resource://devtools/client/netmonitor/src/connector/firefox-data-provider.js");
     26 
     27 const lazy = {};
     28 ChromeUtils.defineESModuleGetters(lazy, {
     29  AppConstants: "resource://gre/modules/AppConstants.sys.mjs",
     30 });
     31 
     32 loader.lazyRequireGetter(
     33  this,
     34  "START_IGNORE_ACTION",
     35  "resource://devtools/client/shared/redux/middleware/ignore.js",
     36  true
     37 );
     38 const ZoomKeys = require("resource://devtools/client/shared/zoom-keys.js");
     39 loader.lazyRequireGetter(
     40  this,
     41  "TRACER_LOG_METHODS",
     42  "resource://devtools/shared/specs/tracer.js",
     43  true
     44 );
     45 
     46 const PREF_SIDEBAR_ENABLED = "devtools.webconsole.sidebarToggle";
     47 const PREF_BROWSERTOOLBOX_SCOPE = "devtools.browsertoolbox.scope";
     48 
     49 /**
     50 * A WebConsoleUI instance is an interactive console initialized *per target*
     51 * that displays console log data as well as provides an interactive terminal to
     52 * manipulate the target's document content.
     53 *
     54 * The WebConsoleUI is responsible for the actual Web Console UI
     55 * implementation.
     56 */
     57 class WebConsoleUI {
     58  /**
     59   * @param {WebConsole} hud: The WebConsole owner object.
     60   */
     61  constructor(hud) {
     62    this.hud = hud;
     63    this.hudId = this.hud.hudId;
     64    this.isBrowserConsole = this.hud.isBrowserConsole;
     65 
     66    this.isBrowserToolboxConsole =
     67      this.hud.commands.descriptorFront.isBrowserProcessDescriptor &&
     68      !this.isBrowserConsole;
     69 
     70    this.window = this.hud.iframeWindow;
     71 
     72    this._onPanelSelected = this._onPanelSelected.bind(this);
     73    this._onChangeSplitConsoleState =
     74      this._onChangeSplitConsoleState.bind(this);
     75    this._onTargetAvailable = this._onTargetAvailable.bind(this);
     76    this._onTargetDestroyed = this._onTargetDestroyed.bind(this);
     77    this._onResourceAvailable = this._onResourceAvailable.bind(this);
     78    this._onNetworkResourceUpdated = this._onNetworkResourceUpdated.bind(this);
     79    this._onScopePrefChanged = this._onScopePrefChanged.bind(this);
     80    this._onShowConsoleEvaluation = this._onShowConsoleEvaluation.bind(this);
     81 
     82    if (this.isBrowserConsole) {
     83      Services.prefs.addObserver(
     84        PREF_BROWSERTOOLBOX_SCOPE,
     85        this._onScopePrefChanged
     86      );
     87    }
     88 
     89    EventEmitter.decorate(this);
     90  }
     91 
     92  /**
     93   * Initialize the WebConsoleUI instance.
     94   *
     95   * @return {object}
     96   *         A promise object that resolves once the frame is ready to use.
     97   */
     98  init() {
     99    if (this._initializer) {
    100      return this._initializer;
    101    }
    102 
    103    this._initializer = (async () => {
    104      this._initUI();
    105 
    106      if (this.isBrowserConsole) {
    107        // Bug 1605763:
    108        // TargetCommand.startListening will start fetching additional targets
    109        // and may overload the Browser Console with loads of targets and resources.
    110        // We can call it from here, as `_attachTargets` is called after the UI is initialized.
    111        // Bug 1642599:
    112        // TargetCommand.startListening has to be called before:
    113        // - `_attachTargets`, in order to set TargetCommand.watcherFront which is used by ResourceWatcher.watchResources.
    114        // - `ConsoleCommands`, in order to set TargetCommand.targetFront which is wrapped by hud.currentTarget
    115        await this.hud.commands.targetCommand.startListening();
    116        if (this._destroyed) {
    117          return;
    118        }
    119      }
    120 
    121      await this.wrapper.init();
    122      if (this._destroyed) {
    123        return;
    124      }
    125 
    126      // Bug 1605763: It's important to call _attachTargets once the UI is initialized, as
    127      // it may overload the Browser Console with many updates.
    128      // It is also important to do it only after the wrapper is initialized,
    129      // otherwise its `store` will be null while we already call a few dispatch methods
    130      // from onResourceAvailable
    131      await this._attachTargets();
    132      if (this._destroyed) {
    133        return;
    134      }
    135 
    136      // `_attachTargets` will process resources and throttle some actions
    137      // Wait for these actions to be dispatched before reporting that the
    138      // console is initialized. Otherwise `showToolbox` will resolve before
    139      // all already existing console messages are displayed.
    140      await this.wrapper.waitAsyncDispatches();
    141      this._initNotifications();
    142    })();
    143 
    144    return this._initializer;
    145  }
    146 
    147  destroy() {
    148    if (this._destroyed) {
    149      return;
    150    }
    151 
    152    this._destroyed = true;
    153 
    154    this.React = this.ReactDOM = this.FrameView = null;
    155 
    156    if (this.wrapper) {
    157      this.wrapper.getStore()?.dispatch(START_IGNORE_ACTION);
    158      this.wrapper.destroy();
    159    }
    160 
    161    if (this.jsterm) {
    162      this.jsterm.destroy();
    163      this.jsterm = null;
    164    }
    165 
    166    const { toolbox } = this.hud;
    167    if (toolbox) {
    168      toolbox.off("webconsole-selected", this._onPanelSelected);
    169      toolbox.off("split-console", this._onChangeSplitConsoleState);
    170      toolbox.off("select", this._onChangeSplitConsoleState);
    171      toolbox.off(
    172        "show-original-variable-mapping-warnings",
    173        this._onShowConsoleEvaluation
    174      );
    175    }
    176 
    177    if (this.isBrowserConsole) {
    178      Services.prefs.removeObserver(
    179        PREF_BROWSERTOOLBOX_SCOPE,
    180        this._onScopePrefChanged
    181      );
    182    }
    183 
    184    // Stop listening for targets
    185    this.hud.commands.targetCommand.unwatchTargets({
    186      types: this.hud.commands.targetCommand.ALL_TYPES,
    187      onAvailable: this._onTargetAvailable,
    188      onDestroyed: this._onTargetDestroyed,
    189    });
    190 
    191    const resourceCommand = this.hud.resourceCommand;
    192    if (this._watchedResources) {
    193      resourceCommand.unwatchResources(this._watchedResources, {
    194        onAvailable: this._onResourceAvailable,
    195      });
    196    }
    197 
    198    this.stopWatchingNetworkResources();
    199 
    200    if (this.networkDataProvider) {
    201      this.networkDataProvider.destroy();
    202      this.networkDataProvider = null;
    203    }
    204 
    205    // Nullify `hud` last as it nullify also target which is used on destroy
    206    this.window = this.hud = this.wrapper = null;
    207  }
    208 
    209  /**
    210   * Clear the Web Console output.
    211   *
    212   * This method emits the "messages-cleared" notification.
    213   *
    214   * @param {boolean} clearStorage
    215   *        True if you want to clear the console messages storage associated to
    216   *        this Web Console.
    217   * @param {object} event
    218   *        If the event exists, calls preventDefault on it.
    219   */
    220  async clearOutput(clearStorage, event) {
    221    if (event) {
    222      event.preventDefault();
    223    }
    224    if (this.wrapper) {
    225      this.wrapper.dispatchMessagesClear();
    226    }
    227 
    228    if (clearStorage) {
    229      await this.clearMessagesCache();
    230    }
    231    this.emitForTests("messages-cleared");
    232  }
    233 
    234  async clearMessagesCache() {
    235    if (this._destroyed) {
    236      return;
    237    }
    238 
    239    // This can be called during console destruction and getAllFronts would reject in such case.
    240    try {
    241      const consoleFronts = await this.hud.commands.targetCommand.getAllFronts(
    242        this.hud.commands.targetCommand.ALL_TYPES,
    243        "console"
    244      );
    245      const promises = [];
    246      for (const consoleFront of consoleFronts) {
    247        promises.push(consoleFront.clearMessagesCacheAsync());
    248      }
    249      await Promise.all(promises);
    250      this.emitForTests("messages-cache-cleared");
    251    } catch (e) {
    252      console.warn("Exception in clearMessagesCache", e);
    253    }
    254  }
    255 
    256  /**
    257   * Remove all of the private messages from the Web Console output.
    258   *
    259   * This method emits the "private-messages-cleared" notification.
    260   */
    261  clearPrivateMessages() {
    262    if (this._destroyed) {
    263      return;
    264    }
    265 
    266    this.wrapper.dispatchPrivateMessagesClear();
    267    this.emitForTests("private-messages-cleared");
    268  }
    269 
    270  inspectObjectActor(objectActor) {
    271    const { targetFront } = this.hud.commands.targetCommand;
    272    this.wrapper.dispatchMessageAdd(
    273      {
    274        helperResult: {
    275          type: "inspectObject",
    276          object:
    277            objectActor && objectActor.getGrip
    278              ? objectActor
    279              : getAdHocFrontOrPrimitiveGrip(objectActor, targetFront),
    280        },
    281      },
    282      true
    283    );
    284    return this.wrapper;
    285  }
    286 
    287  disableAllNetworkMessages() {
    288    if (this._destroyed) {
    289      return;
    290    }
    291    this.wrapper.dispatchNetworkMessagesDisable();
    292  }
    293 
    294  getPanelWindow() {
    295    return this.window;
    296  }
    297 
    298  logWarningAboutReplacedAPI() {
    299    return this.hud.currentTarget.logWarningInPage(
    300      l10n.getStr("ConsoleAPIDisabled"),
    301      "ConsoleAPIDisabled"
    302    );
    303  }
    304 
    305  /**
    306   * Connect to the server using the remote debugging protocol.
    307   *
    308   * @private
    309   * @return {object}
    310   *         A promise object that is resolved/reject based on the proxies connections.
    311   */
    312  async _attachTargets() {
    313    const { commands, resourceCommand } = this.hud;
    314    this.networkDataProvider = new FirefoxDataProvider({
    315      commands,
    316      actions: {
    317        updateRequest: (id, data) =>
    318          this.wrapper.batchedRequestUpdates({ id, data }),
    319      },
    320      owner: this,
    321    });
    322 
    323    // Listen for all target types, including:
    324    // - frames, in order to get the parent process target
    325    // which is considered as a frame rather than a process.
    326    // - workers, for similar reason. When we open a toolbox
    327    // for just a worker, the top level target is a worker target.
    328    // - processes, as we want to spawn additional proxies for them.
    329    await commands.targetCommand.watchTargets({
    330      types: this.hud.commands.targetCommand.ALL_TYPES,
    331      onAvailable: this._onTargetAvailable,
    332      onDestroyed: this._onTargetDestroyed,
    333    });
    334 
    335    this._watchedResources = [
    336      resourceCommand.TYPES.CONSOLE_MESSAGE,
    337      resourceCommand.TYPES.ERROR_MESSAGE,
    338      resourceCommand.TYPES.PLATFORM_MESSAGE,
    339      resourceCommand.TYPES.DOCUMENT_EVENT,
    340      resourceCommand.TYPES.LAST_PRIVATE_CONTEXT_EXIT,
    341      resourceCommand.TYPES.JSTRACER_TRACE,
    342      resourceCommand.TYPES.JSTRACER_STATE,
    343    ];
    344 
    345    // CSS Warnings are only enabled when the user explicitely requested to show them
    346    // as it can slow down page load.
    347    const shouldShowCssWarnings = this.wrapper.getFilterState(FILTERS.CSS);
    348    if (shouldShowCssWarnings) {
    349      this._watchedResources.push(resourceCommand.TYPES.CSS_MESSAGE);
    350    }
    351 
    352    await resourceCommand.watchResources(this._watchedResources, {
    353      onAvailable: this._onResourceAvailable,
    354    });
    355 
    356    if (this.isBrowserConsole || this.isBrowserToolboxConsole) {
    357      const shouldEnableNetworkMonitoring = Services.prefs.getBoolPref(
    358        PREFS.UI.ENABLE_NETWORK_MONITORING
    359      );
    360      if (shouldEnableNetworkMonitoring) {
    361        await this.startWatchingNetworkResources();
    362      } else {
    363        await this.stopWatchingNetworkResources();
    364      }
    365    } else {
    366      // We should always watch for network resources in the webconsole
    367      await this.startWatchingNetworkResources();
    368    }
    369  }
    370 
    371  async startWatchingNetworkResources() {
    372    const { commands, resourceCommand } = this.hud;
    373    await resourceCommand.watchResources(
    374      [
    375        resourceCommand.TYPES.NETWORK_EVENT,
    376        resourceCommand.TYPES.NETWORK_EVENT_STACKTRACE,
    377      ],
    378      {
    379        onAvailable: this._onResourceAvailable,
    380        onUpdated: this._onNetworkResourceUpdated,
    381      }
    382    );
    383 
    384    // When opening a worker toolbox from about:debugging,
    385    // we do not instantiate any Watcher actor yet and would throw here.
    386    // But even once we do, we wouldn't support network inspection anyway.
    387    if (commands.targetCommand.hasTargetWatcherSupport()) {
    388      const networkFront = await commands.watcherFront.getNetworkParentActor();
    389      // There is no way to view response bodies from the Browser Console, so do
    390      // not waste the memory.
    391      const saveBodies =
    392        !this.isBrowserConsole &&
    393        Services.prefs.getBoolPref(
    394          "devtools.netmonitor.saveRequestAndResponseBodies"
    395        );
    396      await networkFront.setSaveRequestAndResponseBodies(saveBodies);
    397    }
    398  }
    399 
    400  async stopWatchingNetworkResources() {
    401    if (this._destroyed) {
    402      return;
    403    }
    404 
    405    await this.hud.resourceCommand.unwatchResources(
    406      [
    407        this.hud.resourceCommand.TYPES.NETWORK_EVENT,
    408        this.hud.resourceCommand.TYPES.NETWORK_EVENT_STACKTRACE,
    409      ],
    410      {
    411        onAvailable: this._onResourceAvailable,
    412        onUpdated: this._onNetworkResourceUpdated,
    413      }
    414    );
    415  }
    416 
    417  handleDocumentEvent(resource) {
    418    // Only consider top level document, and ignore remote iframes top document
    419    if (!resource.targetFront.isTopLevel) {
    420      return;
    421    }
    422 
    423    if (resource.name == "will-navigate") {
    424      this.handleWillNavigate({
    425        timeStamp: resource.time,
    426        url: resource.newURI,
    427      });
    428    } else if (resource.name == "dom-complete") {
    429      this.handleNavigated({
    430        hasNativeConsoleAPI: resource.hasNativeConsoleAPI,
    431      });
    432    }
    433    // For now, ignore all other DOCUMENT_EVENT's.
    434  }
    435 
    436  /**
    437   * Handler for when the page is done loading.
    438   *
    439   * @param {boolean} hasNativeConsoleAPI
    440   *        True if the `console` object is the native one and hasn't been overloaded by a custom
    441   *        object by the page itself.
    442   */
    443  async handleNavigated({ hasNativeConsoleAPI }) {
    444    // Updates instant evaluation on page navigation
    445    this.wrapper.dispatchUpdateInstantEvaluationResultForCurrentExpression();
    446 
    447    // Wait for completion of any async dispatch before notifying that the console
    448    // is fully updated after a page reload
    449    await this.wrapper.waitAsyncDispatches();
    450 
    451    if (!hasNativeConsoleAPI) {
    452      this.logWarningAboutReplacedAPI();
    453    }
    454 
    455    this.emit("reloaded");
    456  }
    457 
    458  handleWillNavigate({ timeStamp, url }) {
    459    this.wrapper.dispatchTabWillNavigate({ timeStamp, url });
    460  }
    461 
    462  /**
    463   * Called when the CSS Warning filter is enabled, in order to start observing for them in the backend.
    464   */
    465  async watchCssMessages() {
    466    const { resourceCommand } = this.hud;
    467    if (this._watchedResources.includes(resourceCommand.TYPES.CSS_MESSAGE)) {
    468      return;
    469    }
    470    await resourceCommand.watchResources([resourceCommand.TYPES.CSS_MESSAGE], {
    471      onAvailable: this._onResourceAvailable,
    472    });
    473    this._watchedResources.push(resourceCommand.TYPES.CSS_MESSAGE);
    474  }
    475 
    476  // eslint-disable-next-line complexity
    477  _onResourceAvailable(resources) {
    478    if (this._destroyed) {
    479      return;
    480    }
    481 
    482    const { logMethod } = this.hud.commands.tracerCommand.getTracingOptions();
    483 
    484    const messages = [];
    485    for (const resource of resources) {
    486      const { TYPES } = this.hud.resourceCommand;
    487      if (resource.resourceType === TYPES.DOCUMENT_EVENT) {
    488        this.handleDocumentEvent(resource);
    489        continue;
    490      }
    491      if (resource.resourceType == TYPES.LAST_PRIVATE_CONTEXT_EXIT) {
    492        // Private messages only need to be removed from the output in Browser Console/Browser Toolbox
    493        // (but in theory this resource should only be send from parent process watchers)
    494        if (this.isBrowserConsole || this.isBrowserToolboxConsole) {
    495          this.clearPrivateMessages();
    496        }
    497        continue;
    498      }
    499      // Ignore messages forwarded from content processes if we're in fission browser toolbox.
    500      if (
    501        !this.wrapper ||
    502        ((resource.resourceType === TYPES.ERROR_MESSAGE ||
    503          resource.resourceType === TYPES.CSS_MESSAGE) &&
    504          resource.pageError?.isForwardedFromContentProcess &&
    505          (this.isBrowserToolboxConsole || this.isBrowserConsole))
    506      ) {
    507        continue;
    508      }
    509 
    510      // Don't show messages emitted from a private window before the Browser Console was
    511      // opened to avoid leaking data from past usage of the browser (e.g. content message
    512      // from now closed private tabs)
    513      if (
    514        (this.isBrowserToolboxConsole || this.isBrowserConsole) &&
    515        resource.isAlreadyExistingResource &&
    516        (resource.pageError?.private || resource.private)
    517      ) {
    518        continue;
    519      }
    520 
    521      if (
    522        resource.resourceType === TYPES.JSTRACER_TRACE &&
    523        logMethod != TRACER_LOG_METHODS.CONSOLE
    524      ) {
    525        continue;
    526      }
    527      if (resource.resourceType === TYPES.NETWORK_EVENT_STACKTRACE) {
    528        this.networkDataProvider?.onStackTraceAvailable(resource);
    529        continue;
    530      }
    531 
    532      if (resource.resourceType === TYPES.NETWORK_EVENT) {
    533        this.networkDataProvider?.onNetworkResourceAvailable(resource);
    534      }
    535      messages.push(resource);
    536    }
    537    this.wrapper.dispatchMessagesAdd(messages);
    538  }
    539 
    540  _onNetworkResourceUpdated(updates) {
    541    if (this._destroyed) {
    542      return;
    543    }
    544 
    545    const messageUpdates = [];
    546    for (const { resource, update } of updates) {
    547      if (
    548        resource.resourceType == this.hud.resourceCommand.TYPES.NETWORK_EVENT
    549      ) {
    550        this.networkDataProvider?.onNetworkResourceUpdated(resource, update);
    551        messageUpdates.push(resource);
    552      }
    553    }
    554    this.wrapper.dispatchMessagesUpdate(messageUpdates);
    555  }
    556 
    557  /**
    558   * Called any time a new target is available.
    559   * i.e. it was already existing or has just been created.
    560   *
    561   * @private
    562   */
    563  async _onTargetAvailable() {
    564    // onTargetAvailable is a mandatory argument for watchTargets,
    565    // we still define it solely for being able to use onTargetDestroyed.
    566  }
    567 
    568  _onTargetDestroyed({ targetFront, isModeSwitching }) {
    569    // Don't try to do anything if the WebConsole is being destroyed
    570    if (this._destroyed) {
    571      return;
    572    }
    573 
    574    // We only want to remove messages from a target destroyed when we're switching mode
    575    // in the Browser Console/Browser Toolbox Console.
    576    // For regular cases, we want to keep the message history (the output will still be
    577    // cleared when the top level target navigates, if "Persist Logs" isn't true, via handleWillNavigate)
    578    if (isModeSwitching) {
    579      this.wrapper.dispatchTargetMessagesRemove(targetFront);
    580    }
    581  }
    582 
    583  _initUI() {
    584    this.document = this.window.document;
    585    this.rootElement = this.document.documentElement;
    586 
    587    this.outputNode = this.document.getElementById("app-wrapper");
    588 
    589    const { toolbox } = this.hud;
    590 
    591    // Initialize module loader and load all the WebConsoleWrapper. The entire code-base
    592    // doesn't need any extra privileges and runs entirely in content scope.
    593    const browserLoader = BrowserLoader({
    594      baseURI: "resource://devtools/client/webconsole/",
    595      window: this.window,
    596    });
    597    // Expose `require` for the CustomFormatter ESM in order to allow it to load
    598    // ObjectInspector, which are still CommonJS modules, via the same BrowserLoader instance.
    599    this.window.browserLoaderRequire = browserLoader.require;
    600    const WebConsoleWrapper = browserLoader.require(
    601      "resource://devtools/client/webconsole/webconsole-wrapper.js"
    602    );
    603 
    604    this.wrapper = new WebConsoleWrapper(
    605      this.outputNode,
    606      this,
    607      toolbox,
    608      this.document
    609    );
    610 
    611    this._initShortcuts();
    612    this._initOutputSyntaxHighlighting();
    613 
    614    if (toolbox) {
    615      toolbox.on("webconsole-selected", this._onPanelSelected);
    616      toolbox.on("split-console", this._onChangeSplitConsoleState);
    617      toolbox.on("select", this._onChangeSplitConsoleState);
    618    }
    619  }
    620 
    621  _initOutputSyntaxHighlighting() {
    622    // Given a DOM node, we syntax highlight identically to how the input field
    623    // looks. See https://codemirror.net/demo/runmode.html;
    624    const syntaxHighlightNode = node => {
    625      const editor = this.jsterm && this.jsterm.editor;
    626      if (node && editor) {
    627        node.classList.add("cm-s-mozilla");
    628        editor.CodeMirror.runMode(
    629          node.textContent,
    630          "application/javascript",
    631          node
    632        );
    633      }
    634    };
    635 
    636    // Use a Custom Element to handle syntax highlighting to avoid
    637    // dealing with refs or innerHTML from React.
    638    const win = this.window;
    639    win.customElements.define(
    640      "syntax-highlighted",
    641      class extends win.HTMLElement {
    642        connectedCallback() {
    643          if (!this.connected) {
    644            this.connected = true;
    645            syntaxHighlightNode(this);
    646 
    647            // Highlight Again when the innerText changes
    648            // We remove the listener before running codemirror mode and add
    649            // it again to capture text changes
    650            this.observer = new win.MutationObserver((mutations, observer) => {
    651              observer.disconnect();
    652              syntaxHighlightNode(this);
    653              observer.observe(this, { childList: true });
    654            });
    655 
    656            this.observer.observe(this, { childList: true });
    657          }
    658        }
    659      }
    660    );
    661  }
    662 
    663  _initNotifications() {
    664    if (this.hud.toolbox) {
    665      this.wrapper.toggleOriginalVariableMappingEvaluationNotification(
    666        !!this.hud.toolbox
    667          .getPanel("jsdebugger")
    668          ?.shouldShowOriginalVariableMappingWarnings()
    669      );
    670      this.hud.toolbox.on(
    671        "show-original-variable-mapping-warnings",
    672        this._onShowConsoleEvaluation
    673      );
    674    }
    675  }
    676 
    677  _initShortcuts() {
    678    const shortcuts = new KeyShortcuts({
    679      window: this.window,
    680    });
    681 
    682    for (const clearShortcut of this.getClearKeyShortcuts()) {
    683      shortcuts.on(clearShortcut, event => this.clearOutput(true, event));
    684    }
    685 
    686    if (this.isBrowserConsole) {
    687      // Make sure keyboard shortcuts work immediately after opening
    688      // the Browser Console (Bug 1461366).
    689      this.window.focus();
    690      shortcuts.on(
    691        l10n.getStr("webconsole.close.key"),
    692        this.window.close.bind(this.window)
    693      );
    694 
    695      ZoomKeys.register(this.window, shortcuts);
    696 
    697      /* This is the same as DevelopmentHelpers.quickRestart, but it runs in all
    698       * builds (even official). This allows a user to do a restart + session restore
    699       * with Ctrl+Shift+J (open Browser Console) and then Ctrl+Alt+R (restart).
    700       */
    701      shortcuts.on("CmdOrCtrl+Alt+R", () => {
    702        this.hud.commands.targetCommand.reloadTopLevelTarget();
    703      });
    704    } else if (Services.prefs.getBoolPref(PREF_SIDEBAR_ENABLED)) {
    705      shortcuts.on("Esc", () => {
    706        this.wrapper.dispatchSidebarClose();
    707        if (this.jsterm) {
    708          this.jsterm.focus();
    709        }
    710      });
    711    }
    712  }
    713 
    714  /**
    715   * Returns system-specific key shortcuts for clearing the console.
    716   *
    717   * @return {string[]}
    718   *         An array of key shortcut strings.
    719   */
    720  getClearKeyShortcuts() {
    721    if (lazy.AppConstants.platform === "macosx") {
    722      return [
    723        l10n.getStr("webconsole.clear.alternativeKeyOSX"),
    724        l10n.getStr("webconsole.clear.keyOSX"),
    725      ];
    726    }
    727 
    728    return [l10n.getStr("webconsole.clear.key")];
    729  }
    730 
    731  /**
    732   * Sets the focus to JavaScript input field when the web console tab is
    733   * selected or when there is a split console present.
    734   *
    735   * @private
    736   */
    737  _onPanelSelected() {
    738    // We can only focus when we have the jsterm reference. This is fine because if the
    739    // jsterm is not mounted yet, it will be focused in JSTerm's componentDidMount.
    740    if (this.jsterm) {
    741      this.jsterm.focus();
    742    }
    743  }
    744 
    745  _onChangeSplitConsoleState() {
    746    this.wrapper.dispatchSplitConsoleCloseButtonToggle();
    747  }
    748 
    749  _onScopePrefChanged() {
    750    if (this.isBrowserConsole) {
    751      this.hud.updateWindowTitle();
    752    }
    753  }
    754 
    755  _onShowConsoleEvaluation(isOriginalVariableMappingEnabled) {
    756    this.wrapper.toggleOriginalVariableMappingEvaluationNotification(
    757      isOriginalVariableMappingEnabled
    758    );
    759  }
    760 
    761  getInputCursor() {
    762    return this.jsterm && this.jsterm.getSelectionStart();
    763  }
    764 
    765  getJsTermTooltipAnchor() {
    766    return this.outputNode.querySelector(".CodeMirror-cursor");
    767  }
    768 
    769  attachRef(id, node) {
    770    this[id] = node;
    771  }
    772 
    773  getSelectedNodeActorID() {
    774    const inspectorSelection = this.hud.getInspectorSelection();
    775    return inspectorSelection?.nodeFront?.actorID;
    776  }
    777 }
    778 
    779 exports.WebConsoleUI = WebConsoleUI;