browser/devtools/scratchpad/scratchpad.js

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/browser/devtools/scratchpad/scratchpad.js	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,2311 @@
     1.4 +/* vim:set ts=2 sw=2 sts=2 et:
     1.5 + * This Source Code Form is subject to the terms of the Mozilla Public
     1.6 + * License, v. 2.0. If a copy of the MPL was not distributed with this
     1.7 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.8 +
     1.9 +/*
    1.10 + * Original version history can be found here:
    1.11 + * https://github.com/mozilla/workspace
    1.12 + *
    1.13 + * Copied and relicensed from the Public Domain.
    1.14 + * See bug 653934 for details.
    1.15 + * https://bugzilla.mozilla.org/show_bug.cgi?id=653934
    1.16 + */
    1.17 +
    1.18 +"use strict";
    1.19 +
    1.20 +const Cu = Components.utils;
    1.21 +const Cc = Components.classes;
    1.22 +const Ci = Components.interfaces;
    1.23 +
    1.24 +const SCRATCHPAD_CONTEXT_CONTENT = 1;
    1.25 +const SCRATCHPAD_CONTEXT_BROWSER = 2;
    1.26 +const BUTTON_POSITION_SAVE       = 0;
    1.27 +const BUTTON_POSITION_CANCEL     = 1;
    1.28 +const BUTTON_POSITION_DONT_SAVE  = 2;
    1.29 +const BUTTON_POSITION_REVERT     = 0;
    1.30 +const EVAL_FUNCTION_TIMEOUT      = 1000; // milliseconds
    1.31 +
    1.32 +const MAXIMUM_FONT_SIZE = 96;
    1.33 +const MINIMUM_FONT_SIZE = 6;
    1.34 +const NORMAL_FONT_SIZE = 12;
    1.35 +
    1.36 +const SCRATCHPAD_L10N = "chrome://browser/locale/devtools/scratchpad.properties";
    1.37 +const DEVTOOLS_CHROME_ENABLED = "devtools.chrome.enabled";
    1.38 +const PREF_RECENT_FILES_MAX = "devtools.scratchpad.recentFilesMax";
    1.39 +const SHOW_TRAILING_SPACE = "devtools.scratchpad.showTrailingSpace";
    1.40 +const ENABLE_CODE_FOLDING = "devtools.scratchpad.enableCodeFolding";
    1.41 +
    1.42 +const VARIABLES_VIEW_URL = "chrome://browser/content/devtools/widgets/VariablesView.xul";
    1.43 +
    1.44 +const require   = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
    1.45 +
    1.46 +const Telemetry = require("devtools/shared/telemetry");
    1.47 +const Editor    = require("devtools/sourceeditor/editor");
    1.48 +const TargetFactory = require("devtools/framework/target").TargetFactory;
    1.49 +
    1.50 +const { Promise: promise } = Cu.import("resource://gre/modules/Promise.jsm", {});
    1.51 +Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    1.52 +Cu.import("resource://gre/modules/Services.jsm");
    1.53 +Cu.import("resource://gre/modules/NetUtil.jsm");
    1.54 +Cu.import("resource:///modules/devtools/scratchpad-manager.jsm");
    1.55 +Cu.import("resource://gre/modules/jsdebugger.jsm");
    1.56 +Cu.import("resource:///modules/devtools/gDevTools.jsm");
    1.57 +Cu.import("resource://gre/modules/osfile.jsm");
    1.58 +Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
    1.59 +Cu.import("resource://gre/modules/reflect.jsm");
    1.60 +Cu.import("resource://gre/modules/devtools/DevToolsUtils.jsm");
    1.61 +
    1.62 +XPCOMUtils.defineLazyModuleGetter(this, "VariablesView",
    1.63 +  "resource:///modules/devtools/VariablesView.jsm");
    1.64 +
    1.65 +XPCOMUtils.defineLazyModuleGetter(this, "VariablesViewController",
    1.66 +  "resource:///modules/devtools/VariablesViewController.jsm");
    1.67 +
    1.68 +XPCOMUtils.defineLazyModuleGetter(this, "EnvironmentClient",
    1.69 +  "resource://gre/modules/devtools/dbg-client.jsm");
    1.70 +
    1.71 +XPCOMUtils.defineLazyModuleGetter(this, "ObjectClient",
    1.72 +  "resource://gre/modules/devtools/dbg-client.jsm");
    1.73 +
    1.74 +XPCOMUtils.defineLazyModuleGetter(this, "WebConsoleUtils",
    1.75 +  "resource://gre/modules/devtools/WebConsoleUtils.jsm");
    1.76 +
    1.77 +XPCOMUtils.defineLazyModuleGetter(this, "DebuggerServer",
    1.78 +  "resource://gre/modules/devtools/dbg-server.jsm");
    1.79 +
    1.80 +XPCOMUtils.defineLazyModuleGetter(this, "DebuggerClient",
    1.81 +  "resource://gre/modules/devtools/dbg-client.jsm");
    1.82 +
    1.83 +XPCOMUtils.defineLazyGetter(this, "REMOTE_TIMEOUT", () =>
    1.84 +  Services.prefs.getIntPref("devtools.debugger.remote-timeout"));
    1.85 +
    1.86 +XPCOMUtils.defineLazyModuleGetter(this, "ShortcutUtils",
    1.87 +  "resource://gre/modules/ShortcutUtils.jsm");
    1.88 +
    1.89 +XPCOMUtils.defineLazyModuleGetter(this, "Reflect",
    1.90 +  "resource://gre/modules/reflect.jsm");
    1.91 +
    1.92 +// Because we have no constructor / destructor where we can log metrics we need
    1.93 +// to do so here.
    1.94 +let telemetry = new Telemetry();
    1.95 +telemetry.toolOpened("scratchpad");
    1.96 +
    1.97 +/**
    1.98 + * The scratchpad object handles the Scratchpad window functionality.
    1.99 + */
   1.100 +var Scratchpad = {
   1.101 +  _instanceId: null,
   1.102 +  _initialWindowTitle: document.title,
   1.103 +  _dirty: false,
   1.104 +
   1.105 +  /**
   1.106 +   * Check if provided string is a mode-line and, if it is, return an
   1.107 +   * object with its values.
   1.108 +   *
   1.109 +   * @param string aLine
   1.110 +   * @return string
   1.111 +   */
   1.112 +  _scanModeLine: function SP__scanModeLine(aLine="")
   1.113 +  {
   1.114 +    aLine = aLine.trim();
   1.115 +
   1.116 +    let obj = {};
   1.117 +    let ch1 = aLine.charAt(0);
   1.118 +    let ch2 = aLine.charAt(1);
   1.119 +
   1.120 +    if (ch1 !== "/" || (ch2 !== "*" && ch2 !== "/")) {
   1.121 +      return obj;
   1.122 +    }
   1.123 +
   1.124 +    aLine = aLine
   1.125 +      .replace(/^\/\//, "")
   1.126 +      .replace(/^\/\*/, "")
   1.127 +      .replace(/\*\/$/, "");
   1.128 +
   1.129 +    aLine.split(",").forEach(pair => {
   1.130 +      let [key, val] = pair.split(":");
   1.131 +
   1.132 +      if (key && val) {
   1.133 +        obj[key.trim()] = val.trim();
   1.134 +      }
   1.135 +    });
   1.136 +
   1.137 +    return obj;
   1.138 +  },
   1.139 +
   1.140 +  /**
   1.141 +   * Add the event listeners for popupshowing events.
   1.142 +   */
   1.143 +  _setupPopupShowingListeners: function SP_setupPopupShowing() {
   1.144 +    let elementIDs = ['sp-menu_editpopup', 'scratchpad-text-popup'];
   1.145 +
   1.146 +    for (let elementID of elementIDs) {
   1.147 +      let elem = document.getElementById(elementID);
   1.148 +      if (elem) {
   1.149 +        elem.addEventListener("popupshowing", function () {
   1.150 +          goUpdateGlobalEditMenuItems();
   1.151 +          let commands = ['cmd_undo', 'cmd_redo', 'cmd_delete', 'cmd_findAgain'];
   1.152 +          commands.forEach(goUpdateCommand);
   1.153 +        });
   1.154 +      }
   1.155 +    }
   1.156 +  },
   1.157 +
   1.158 +  /**
   1.159 +   * Add the event event listeners for command events.
   1.160 +   */
   1.161 +  _setupCommandListeners: function SP_setupCommands() {
   1.162 +    let commands = {
   1.163 +      "cmd_gotoLine": () => {
   1.164 +        goDoCommand('cmd_gotoLine');
   1.165 +      },
   1.166 +      "sp-cmd-newWindow": () => {
   1.167 +        Scratchpad.openScratchpad();
   1.168 +      },
   1.169 +      "sp-cmd-openFile": () => {
   1.170 +        Scratchpad.openFile();
   1.171 +      },
   1.172 +      "sp-cmd-clearRecentFiles": () => {
   1.173 +        Scratchpad.clearRecentFiles();
   1.174 +      },
   1.175 +      "sp-cmd-save": () => {
   1.176 +        Scratchpad.saveFile();
   1.177 +      },
   1.178 +      "sp-cmd-saveas": () => {
   1.179 +        Scratchpad.saveFileAs();
   1.180 +      },
   1.181 +      "sp-cmd-revert": () => {
   1.182 +        Scratchpad.promptRevert();
   1.183 +      },
   1.184 +      "sp-cmd-close": () => {
   1.185 +        Scratchpad.close();
   1.186 +      },
   1.187 +      "sp-cmd-run": () => {
   1.188 +        Scratchpad.run();
   1.189 +      },
   1.190 +      "sp-cmd-inspect": () => {
   1.191 +        Scratchpad.inspect();
   1.192 +      },
   1.193 +      "sp-cmd-display": () => {
   1.194 +        Scratchpad.display();
   1.195 +      },
   1.196 +      "sp-cmd-pprint": () => {
   1.197 +        Scratchpad.prettyPrint();
   1.198 +      },
   1.199 +      "sp-cmd-contentContext": () => {
   1.200 +        Scratchpad.setContentContext();
   1.201 +      },
   1.202 +      "sp-cmd-browserContext": () => {
   1.203 +        Scratchpad.setBrowserContext();
   1.204 +      },
   1.205 +      "sp-cmd-reloadAndRun": () => {
   1.206 +        Scratchpad.reloadAndRun();
   1.207 +      },
   1.208 +      "sp-cmd-evalFunction": () => {
   1.209 +        Scratchpad.evalTopLevelFunction();
   1.210 +      },
   1.211 +      "sp-cmd-errorConsole": () => {
   1.212 +        Scratchpad.openErrorConsole();
   1.213 +      },
   1.214 +      "sp-cmd-webConsole": () => {
   1.215 +        Scratchpad.openWebConsole();
   1.216 +      },
   1.217 +      "sp-cmd-documentationLink": () => {
   1.218 +        Scratchpad.openDocumentationPage();
   1.219 +      },
   1.220 +      "sp-cmd-hideSidebar": () => {
   1.221 +        Scratchpad.sidebar.hide();
   1.222 +      },
   1.223 +      "sp-cmd-line-numbers": () => {
   1.224 +        Scratchpad.toggleEditorOption('lineNumbers');
   1.225 +      },
   1.226 +      "sp-cmd-wrap-text": () => {
   1.227 +        Scratchpad.toggleEditorOption('lineWrapping');
   1.228 +      },
   1.229 +      "sp-cmd-highlight-trailing-space": () => {
   1.230 +        Scratchpad.toggleEditorOption('showTrailingSpace');
   1.231 +      },
   1.232 +      "sp-cmd-larger-font": () => {
   1.233 +        Scratchpad.increaseFontSize();
   1.234 +      },
   1.235 +      "sp-cmd-smaller-font": () => {
   1.236 +        Scratchpad.decreaseFontSize();
   1.237 +      },
   1.238 +      "sp-cmd-normal-font": () => {
   1.239 +        Scratchpad.normalFontSize();
   1.240 +      },
   1.241 +    }
   1.242 +
   1.243 +    for (let command in commands) {
   1.244 +      let elem = document.getElementById(command);
   1.245 +      if (elem) {
   1.246 +        elem.addEventListener("command", commands[command]);
   1.247 +      }
   1.248 +    }
   1.249 +  },
   1.250 +
   1.251 +  /**
   1.252 +   * The script execution context. This tells Scratchpad in which context the
   1.253 +   * script shall execute.
   1.254 +   *
   1.255 +   * Possible values:
   1.256 +   *   - SCRATCHPAD_CONTEXT_CONTENT to execute code in the context of the current
   1.257 +   *   tab content window object.
   1.258 +   *   - SCRATCHPAD_CONTEXT_BROWSER to execute code in the context of the
   1.259 +   *   currently active chrome window object.
   1.260 +   */
   1.261 +  executionContext: SCRATCHPAD_CONTEXT_CONTENT,
   1.262 +
   1.263 +  /**
   1.264 +   * Tells if this Scratchpad is initialized and ready for use.
   1.265 +   * @boolean
   1.266 +   * @see addObserver
   1.267 +   */
   1.268 +  initialized: false,
   1.269 +
   1.270 +  /**
   1.271 +   * Returns the 'dirty' state of this Scratchpad.
   1.272 +   */
   1.273 +  get dirty()
   1.274 +  {
   1.275 +    let clean = this.editor && this.editor.isClean();
   1.276 +    return this._dirty || !clean;
   1.277 +  },
   1.278 +
   1.279 +  /**
   1.280 +   * Sets the 'dirty' state of this Scratchpad.
   1.281 +   */
   1.282 +  set dirty(aValue)
   1.283 +  {
   1.284 +    this._dirty = aValue;
   1.285 +    if (!aValue && this.editor)
   1.286 +      this.editor.setClean();
   1.287 +    this._updateTitle();
   1.288 +  },
   1.289 +
   1.290 +  /**
   1.291 +   * Retrieve the xul:notificationbox DOM element. It notifies the user when
   1.292 +   * the current code execution context is SCRATCHPAD_CONTEXT_BROWSER.
   1.293 +   */
   1.294 +  get notificationBox()
   1.295 +  {
   1.296 +    return document.getElementById("scratchpad-notificationbox");
   1.297 +  },
   1.298 +
   1.299 +  /**
   1.300 +   * Hide the menu bar.
   1.301 +   */
   1.302 +  hideMenu: function SP_hideMenu()
   1.303 +  {
   1.304 +    document.getElementById("sp-menubar").style.display = "none";
   1.305 +  },
   1.306 +
   1.307 +  /**
   1.308 +   * Show the menu bar.
   1.309 +   */
   1.310 +  showMenu: function SP_showMenu()
   1.311 +  {
   1.312 +    document.getElementById("sp-menubar").style.display = "";
   1.313 +  },
   1.314 +
   1.315 +  /**
   1.316 +   * Get the editor content, in the given range. If no range is given you get
   1.317 +   * the entire editor content.
   1.318 +   *
   1.319 +   * @param number [aStart=0]
   1.320 +   *        Optional, start from the given offset.
   1.321 +   * @param number [aEnd=content char count]
   1.322 +   *        Optional, end offset for the text you want. If this parameter is not
   1.323 +   *        given, then the text returned goes until the end of the editor
   1.324 +   *        content.
   1.325 +   * @return string
   1.326 +   *         The text in the given range.
   1.327 +   */
   1.328 +  getText: function SP_getText(aStart, aEnd)
   1.329 +  {
   1.330 +    var value = this.editor.getText();
   1.331 +    return value.slice(aStart || 0, aEnd || value.length);
   1.332 +  },
   1.333 +
   1.334 +  /**
   1.335 +   * Set the filename in the scratchpad UI and object
   1.336 +   *
   1.337 +   * @param string aFilename
   1.338 +   *        The new filename
   1.339 +   */
   1.340 +  setFilename: function SP_setFilename(aFilename)
   1.341 +  {
   1.342 +    this.filename = aFilename;
   1.343 +    this._updateTitle();
   1.344 +  },
   1.345 +
   1.346 +  /**
   1.347 +   * Update the Scratchpad window title based on the current state.
   1.348 +   * @private
   1.349 +   */
   1.350 +  _updateTitle: function SP__updateTitle()
   1.351 +  {
   1.352 +    let title = this.filename || this._initialWindowTitle;
   1.353 +
   1.354 +    if (this.dirty)
   1.355 +      title = "*" + title;
   1.356 +
   1.357 +    document.title = title;
   1.358 +  },
   1.359 +
   1.360 +  /**
   1.361 +   * Get the current state of the scratchpad. Called by the
   1.362 +   * Scratchpad Manager for session storing.
   1.363 +   *
   1.364 +   * @return object
   1.365 +   *        An object with 3 properties: filename, text, and
   1.366 +   *        executionContext.
   1.367 +   */
   1.368 +  getState: function SP_getState()
   1.369 +  {
   1.370 +    return {
   1.371 +      filename: this.filename,
   1.372 +      text: this.getText(),
   1.373 +      executionContext: this.executionContext,
   1.374 +      saved: !this.dirty
   1.375 +    };
   1.376 +  },
   1.377 +
   1.378 +  /**
   1.379 +   * Set the filename and execution context using the given state. Called
   1.380 +   * when scratchpad is being restored from a previous session.
   1.381 +   *
   1.382 +   * @param object aState
   1.383 +   *        An object with filename and executionContext properties.
   1.384 +   */
   1.385 +  setState: function SP_setState(aState)
   1.386 +  {
   1.387 +    if (aState.filename)
   1.388 +      this.setFilename(aState.filename);
   1.389 +
   1.390 +    this.dirty = !aState.saved;
   1.391 +
   1.392 +    if (aState.executionContext == SCRATCHPAD_CONTEXT_BROWSER)
   1.393 +      this.setBrowserContext();
   1.394 +    else
   1.395 +      this.setContentContext();
   1.396 +  },
   1.397 +
   1.398 +  /**
   1.399 +   * Get the most recent chrome window of type navigator:browser.
   1.400 +   */
   1.401 +  get browserWindow()
   1.402 +  {
   1.403 +    return Services.wm.getMostRecentWindow("navigator:browser");
   1.404 +  },
   1.405 +
   1.406 +  /**
   1.407 +   * Get the gBrowser object of the most recent browser window.
   1.408 +   */
   1.409 +  get gBrowser()
   1.410 +  {
   1.411 +    let recentWin = this.browserWindow;
   1.412 +    return recentWin ? recentWin.gBrowser : null;
   1.413 +  },
   1.414 +
   1.415 +  /**
   1.416 +   * Unique name for the current Scratchpad instance. Used to distinguish
   1.417 +   * Scratchpad windows between each other. See bug 661762.
   1.418 +   */
   1.419 +  get uniqueName()
   1.420 +  {
   1.421 +    return "Scratchpad/" + this._instanceId;
   1.422 +  },
   1.423 +
   1.424 +
   1.425 +  /**
   1.426 +   * Sidebar that contains the VariablesView for object inspection.
   1.427 +   */
   1.428 +  get sidebar()
   1.429 +  {
   1.430 +    if (!this._sidebar) {
   1.431 +      this._sidebar = new ScratchpadSidebar(this);
   1.432 +    }
   1.433 +    return this._sidebar;
   1.434 +  },
   1.435 +
   1.436 +  /**
   1.437 +   * Replaces context of an editor with provided value (a string).
   1.438 +   * Note: this method is simply a shortcut to editor.setText.
   1.439 +   */
   1.440 +  setText: function SP_setText(value)
   1.441 +  {
   1.442 +    return this.editor.setText(value);
   1.443 +  },
   1.444 +
   1.445 +  /**
   1.446 +   * Evaluate a string in the currently desired context, that is either the
   1.447 +   * chrome window or the tab content window object.
   1.448 +   *
   1.449 +   * @param string aString
   1.450 +   *        The script you want to evaluate.
   1.451 +   * @return Promise
   1.452 +   *         The promise for the script evaluation result.
   1.453 +   */
   1.454 +  evaluate: function SP_evaluate(aString)
   1.455 +  {
   1.456 +    let connection;
   1.457 +    if (this.target) {
   1.458 +      connection = ScratchpadTarget.consoleFor(this.target);
   1.459 +    }
   1.460 +    else if (this.executionContext == SCRATCHPAD_CONTEXT_CONTENT) {
   1.461 +      connection = ScratchpadTab.consoleFor(this.gBrowser.selectedTab);
   1.462 +    }
   1.463 +    else {
   1.464 +      connection = ScratchpadWindow.consoleFor(this.browserWindow);
   1.465 +    }
   1.466 +
   1.467 +    let evalOptions = { url: this.uniqueName };
   1.468 +
   1.469 +    return connection.then(({ debuggerClient, webConsoleClient }) => {
   1.470 +      let deferred = promise.defer();
   1.471 +
   1.472 +      webConsoleClient.evaluateJS(aString, aResponse => {
   1.473 +        this.debuggerClient = debuggerClient;
   1.474 +        this.webConsoleClient = webConsoleClient;
   1.475 +        if (aResponse.error) {
   1.476 +          deferred.reject(aResponse);
   1.477 +        }
   1.478 +        else if (aResponse.exception !== null) {
   1.479 +          deferred.resolve([aString, aResponse]);
   1.480 +        }
   1.481 +        else {
   1.482 +          deferred.resolve([aString, undefined, aResponse.result]);
   1.483 +        }
   1.484 +      }, evalOptions);
   1.485 +
   1.486 +      return deferred.promise;
   1.487 +    });
   1.488 +   },
   1.489 +
   1.490 +  /**
   1.491 +   * Execute the selected text (if any) or the entire editor content in the
   1.492 +   * current context.
   1.493 +   *
   1.494 +   * @return Promise
   1.495 +   *         The promise for the script evaluation result.
   1.496 +   */
   1.497 +  execute: function SP_execute()
   1.498 +  {
   1.499 +    let selection = this.editor.getSelection() || this.getText();
   1.500 +    return this.evaluate(selection);
   1.501 +  },
   1.502 +
   1.503 +  /**
   1.504 +   * Execute the selected text (if any) or the entire editor content in the
   1.505 +   * current context.
   1.506 +   *
   1.507 +   * @return Promise
   1.508 +   *         The promise for the script evaluation result.
   1.509 +   */
   1.510 +  run: function SP_run()
   1.511 +  {
   1.512 +    let deferred = promise.defer();
   1.513 +    let reject = aReason => deferred.reject(aReason);
   1.514 +
   1.515 +    this.execute().then(([aString, aError, aResult]) => {
   1.516 +      let resolve = () => deferred.resolve([aString, aError, aResult]);
   1.517 +
   1.518 +      if (aError) {
   1.519 +        this.writeAsErrorComment(aError.exception).then(resolve, reject);
   1.520 +      }
   1.521 +      else {
   1.522 +        this.editor.dropSelection();
   1.523 +        resolve();
   1.524 +      }
   1.525 +    }, reject);
   1.526 +
   1.527 +    return deferred.promise;
   1.528 +  },
   1.529 +
   1.530 +  /**
   1.531 +   * Execute the selected text (if any) or the entire editor content in the
   1.532 +   * current context. If the result is primitive then it is written as a
   1.533 +   * comment. Otherwise, the resulting object is inspected up in the sidebar.
   1.534 +   *
   1.535 +   * @return Promise
   1.536 +   *         The promise for the script evaluation result.
   1.537 +   */
   1.538 +  inspect: function SP_inspect()
   1.539 +  {
   1.540 +    let deferred = promise.defer();
   1.541 +    let reject = aReason => deferred.reject(aReason);
   1.542 +
   1.543 +    this.execute().then(([aString, aError, aResult]) => {
   1.544 +      let resolve = () => deferred.resolve([aString, aError, aResult]);
   1.545 +
   1.546 +      if (aError) {
   1.547 +        this.writeAsErrorComment(aError.exception).then(resolve, reject);
   1.548 +      }
   1.549 +      else if (VariablesView.isPrimitive({ value: aResult })) {
   1.550 +        this._writePrimitiveAsComment(aResult).then(resolve, reject);
   1.551 +      }
   1.552 +      else {
   1.553 +        this.editor.dropSelection();
   1.554 +        this.sidebar.open(aString, aResult).then(resolve, reject);
   1.555 +      }
   1.556 +    }, reject);
   1.557 +
   1.558 +    return deferred.promise;
   1.559 +  },
   1.560 +
   1.561 +  /**
   1.562 +   * Reload the current page and execute the entire editor content when
   1.563 +   * the page finishes loading. Note that this operation should be available
   1.564 +   * only in the content context.
   1.565 +   *
   1.566 +   * @return Promise
   1.567 +   *         The promise for the script evaluation result.
   1.568 +   */
   1.569 +  reloadAndRun: function SP_reloadAndRun()
   1.570 +  {
   1.571 +    let deferred = promise.defer();
   1.572 +
   1.573 +    if (this.executionContext !== SCRATCHPAD_CONTEXT_CONTENT) {
   1.574 +      Cu.reportError(this.strings.
   1.575 +          GetStringFromName("scratchpadContext.invalid"));
   1.576 +      return;
   1.577 +    }
   1.578 +
   1.579 +    let browser = this.gBrowser.selectedBrowser;
   1.580 +
   1.581 +    this._reloadAndRunEvent = evt => {
   1.582 +      if (evt.target !== browser.contentDocument) {
   1.583 +        return;
   1.584 +      }
   1.585 +
   1.586 +      browser.removeEventListener("load", this._reloadAndRunEvent, true);
   1.587 +
   1.588 +      this.run().then(aResults => deferred.resolve(aResults));
   1.589 +    };
   1.590 +
   1.591 +    browser.addEventListener("load", this._reloadAndRunEvent, true);
   1.592 +    browser.contentWindow.location.reload();
   1.593 +
   1.594 +    return deferred.promise;
   1.595 +  },
   1.596 +
   1.597 +  /**
   1.598 +   * Execute the selected text (if any) or the entire editor content in the
   1.599 +   * current context. The evaluation result is inserted into the editor after
   1.600 +   * the selected text, or at the end of the editor content if there is no
   1.601 +   * selected text.
   1.602 +   *
   1.603 +   * @return Promise
   1.604 +   *         The promise for the script evaluation result.
   1.605 +   */
   1.606 +  display: function SP_display()
   1.607 +  {
   1.608 +    let deferred = promise.defer();
   1.609 +    let reject = aReason => deferred.reject(aReason);
   1.610 +
   1.611 +    this.execute().then(([aString, aError, aResult]) => {
   1.612 +      let resolve = () => deferred.resolve([aString, aError, aResult]);
   1.613 +
   1.614 +      if (aError) {
   1.615 +        this.writeAsErrorComment(aError.exception).then(resolve, reject);
   1.616 +      }
   1.617 +      else if (VariablesView.isPrimitive({ value: aResult })) {
   1.618 +        this._writePrimitiveAsComment(aResult).then(resolve, reject);
   1.619 +      }
   1.620 +      else {
   1.621 +        let objectClient = new ObjectClient(this.debuggerClient, aResult);
   1.622 +        objectClient.getDisplayString(aResponse => {
   1.623 +          if (aResponse.error) {
   1.624 +            reportError("display", aResponse);
   1.625 +            reject(aResponse);
   1.626 +          }
   1.627 +          else {
   1.628 +            this.writeAsComment(aResponse.displayString);
   1.629 +            resolve();
   1.630 +          }
   1.631 +        });
   1.632 +      }
   1.633 +    }, reject);
   1.634 +
   1.635 +    return deferred.promise;
   1.636 +  },
   1.637 +
   1.638 +  _prettyPrintWorker: null,
   1.639 +
   1.640 +  /**
   1.641 +   * Get or create the worker that handles pretty printing.
   1.642 +   */
   1.643 +  get prettyPrintWorker() {
   1.644 +    if (!this._prettyPrintWorker) {
   1.645 +      this._prettyPrintWorker = new ChromeWorker(
   1.646 +        "resource://gre/modules/devtools/server/actors/pretty-print-worker.js");
   1.647 +
   1.648 +      this._prettyPrintWorker.addEventListener("error", ({ message, filename, lineno }) => {
   1.649 +        DevToolsUtils.reportException(message + " @ " + filename + ":" + lineno);
   1.650 +      }, false);
   1.651 +    }
   1.652 +    return this._prettyPrintWorker;
   1.653 +  },
   1.654 +
   1.655 +  /**
   1.656 +   * Pretty print the source text inside the scratchpad.
   1.657 +   *
   1.658 +   * @return Promise
   1.659 +   *         A promise resolved with the pretty printed code, or rejected with
   1.660 +   *         an error.
   1.661 +   */
   1.662 +  prettyPrint: function SP_prettyPrint() {
   1.663 +    const uglyText = this.getText();
   1.664 +    const tabsize = Services.prefs.getIntPref("devtools.editor.tabsize");
   1.665 +    const id = Math.random();
   1.666 +    const deferred = promise.defer();
   1.667 +
   1.668 +    const onReply = ({ data }) => {
   1.669 +      if (data.id !== id) {
   1.670 +        return;
   1.671 +      }
   1.672 +      this.prettyPrintWorker.removeEventListener("message", onReply, false);
   1.673 +
   1.674 +      if (data.error) {
   1.675 +        let errorString = DevToolsUtils.safeErrorString(data.error);
   1.676 +        this.writeAsErrorComment(errorString);
   1.677 +        deferred.reject(errorString);
   1.678 +      } else {
   1.679 +        this.editor.setText(data.code);
   1.680 +        deferred.resolve(data.code);
   1.681 +      }
   1.682 +    };
   1.683 +
   1.684 +    this.prettyPrintWorker.addEventListener("message", onReply, false);
   1.685 +    this.prettyPrintWorker.postMessage({
   1.686 +      id: id,
   1.687 +      url: "(scratchpad)",
   1.688 +      indent: tabsize,
   1.689 +      source: uglyText
   1.690 +    });
   1.691 +
   1.692 +    return deferred.promise;
   1.693 +  },
   1.694 +
   1.695 +  /**
   1.696 +   * Parse the text and return an AST. If we can't parse it, write an error
   1.697 +   * comment and return false.
   1.698 +   */
   1.699 +  _parseText: function SP__parseText(aText) {
   1.700 +    try {
   1.701 +      return Reflect.parse(aText);
   1.702 +    } catch (e) {
   1.703 +      this.writeAsErrorComment(DevToolsUtils.safeErrorString(e));
   1.704 +      return false;
   1.705 +    }
   1.706 +  },
   1.707 +
   1.708 +  /**
   1.709 +   * Determine if the given AST node location contains the given cursor
   1.710 +   * position.
   1.711 +   *
   1.712 +   * @returns Boolean
   1.713 +   */
   1.714 +  _containsCursor: function (aLoc, aCursorPos) {
   1.715 +    // Our line numbers are 1-based, while CodeMirror's are 0-based.
   1.716 +    const lineNumber = aCursorPos.line + 1;
   1.717 +    const columnNumber = aCursorPos.ch;
   1.718 +
   1.719 +    if (aLoc.start.line <= lineNumber && aLoc.end.line >= lineNumber) {
   1.720 +      if (aLoc.start.line === aLoc.end.line) {
   1.721 +        return aLoc.start.column <= columnNumber
   1.722 +          && aLoc.end.column >= columnNumber;
   1.723 +      }
   1.724 +
   1.725 +      if (aLoc.start.line == lineNumber) {
   1.726 +        return columnNumber >= aLoc.start.column;
   1.727 +      }
   1.728 +
   1.729 +      if (aLoc.end.line == lineNumber) {
   1.730 +        return columnNumber <= aLoc.end.column;
   1.731 +      }
   1.732 +
   1.733 +      return true;
   1.734 +    }
   1.735 +
   1.736 +    return false;
   1.737 +  },
   1.738 +
   1.739 +  /**
   1.740 +   * Find the top level function AST node that the cursor is within.
   1.741 +   *
   1.742 +   * @returns Object|null
   1.743 +   */
   1.744 +  _findTopLevelFunction: function SP__findTopLevelFunction(aAst, aCursorPos) {
   1.745 +    for (let statement of aAst.body) {
   1.746 +      switch (statement.type) {
   1.747 +      case "FunctionDeclaration":
   1.748 +        if (this._containsCursor(statement.loc, aCursorPos)) {
   1.749 +          return statement;
   1.750 +        }
   1.751 +        break;
   1.752 +
   1.753 +      case "VariableDeclaration":
   1.754 +        for (let decl of statement.declarations) {
   1.755 +          if (!decl.init) {
   1.756 +            continue;
   1.757 +          }
   1.758 +          if ((decl.init.type == "FunctionExpression"
   1.759 +               || decl.init.type == "ArrowExpression")
   1.760 +              && this._containsCursor(decl.loc, aCursorPos)) {
   1.761 +            return decl;
   1.762 +          }
   1.763 +        }
   1.764 +        break;
   1.765 +      }
   1.766 +    }
   1.767 +
   1.768 +    return null;
   1.769 +  },
   1.770 +
   1.771 +  /**
   1.772 +   * Get the source text associated with the given function statement.
   1.773 +   *
   1.774 +   * @param Object aFunction
   1.775 +   * @param String aFullText
   1.776 +   * @returns String
   1.777 +   */
   1.778 +  _getFunctionText: function SP__getFunctionText(aFunction, aFullText) {
   1.779 +    let functionText = "";
   1.780 +    // Initially set to 0, but incremented first thing in the loop below because
   1.781 +    // line numbers are 1 based, not 0 based.
   1.782 +    let lineNumber = 0;
   1.783 +    const { start, end } = aFunction.loc;
   1.784 +    const singleLine = start.line === end.line;
   1.785 +
   1.786 +    for (let line of aFullText.split(/\n/g)) {
   1.787 +      lineNumber++;
   1.788 +
   1.789 +      if (singleLine && start.line === lineNumber) {
   1.790 +        functionText = line.slice(start.column, end.column);
   1.791 +        break;
   1.792 +      }
   1.793 +
   1.794 +      if (start.line === lineNumber) {
   1.795 +        functionText += line.slice(start.column) + "\n";
   1.796 +        continue;
   1.797 +      }
   1.798 +
   1.799 +      if (end.line === lineNumber) {
   1.800 +        functionText += line.slice(0, end.column);
   1.801 +        break;
   1.802 +      }
   1.803 +
   1.804 +      if (start.line < lineNumber && end.line > lineNumber) {
   1.805 +        functionText += line + "\n";
   1.806 +      }
   1.807 +    }
   1.808 +
   1.809 +    return functionText;
   1.810 +  },
   1.811 +
   1.812 +  /**
   1.813 +   * Evaluate the top level function that the cursor is resting in.
   1.814 +   *
   1.815 +   * @returns Promise [text, error, result]
   1.816 +   */
   1.817 +  evalTopLevelFunction: function SP_evalTopLevelFunction() {
   1.818 +    const text = this.getText();
   1.819 +    const ast = this._parseText(text);
   1.820 +    if (!ast) {
   1.821 +      return promise.resolve([text, undefined, undefined]);
   1.822 +    }
   1.823 +
   1.824 +    const cursorPos = this.editor.getCursor();
   1.825 +    const funcStatement = this._findTopLevelFunction(ast, cursorPos);
   1.826 +    if (!funcStatement) {
   1.827 +      return promise.resolve([text, undefined, undefined]);
   1.828 +    }
   1.829 +
   1.830 +    let functionText = this._getFunctionText(funcStatement, text);
   1.831 +
   1.832 +    // TODO: This is a work around for bug 940086. It should be removed when
   1.833 +    // that is fixed.
   1.834 +    if (funcStatement.type == "FunctionDeclaration"
   1.835 +        && !functionText.startsWith("function ")) {
   1.836 +      functionText = "function " + functionText;
   1.837 +      funcStatement.loc.start.column -= 9;
   1.838 +    }
   1.839 +
   1.840 +    // The decrement by one is because our line numbers are 1-based, while
   1.841 +    // CodeMirror's are 0-based.
   1.842 +    const from = {
   1.843 +      line: funcStatement.loc.start.line - 1,
   1.844 +      ch: funcStatement.loc.start.column
   1.845 +    };
   1.846 +    const to = {
   1.847 +      line: funcStatement.loc.end.line - 1,
   1.848 +      ch: funcStatement.loc.end.column
   1.849 +    };
   1.850 +
   1.851 +    const marker = this.editor.markText(from, to, "eval-text");
   1.852 +    setTimeout(() => marker.clear(), EVAL_FUNCTION_TIMEOUT);
   1.853 +
   1.854 +    return this.evaluate(functionText);
   1.855 +  },
   1.856 +
   1.857 +  /**
   1.858 +   * Writes out a primitive value as a comment. This handles values which are
   1.859 +   * to be printed directly (number, string) as well as grips to values
   1.860 +   * (null, undefined, longString).
   1.861 +   *
   1.862 +   * @param any aValue
   1.863 +   *        The value to print.
   1.864 +   * @return Promise
   1.865 +   *         The promise that resolves after the value has been printed.
   1.866 +   */
   1.867 +  _writePrimitiveAsComment: function SP__writePrimitiveAsComment(aValue)
   1.868 +  {
   1.869 +    let deferred = promise.defer();
   1.870 +
   1.871 +    if (aValue.type == "longString") {
   1.872 +      let client = this.webConsoleClient;
   1.873 +      client.longString(aValue).substring(0, aValue.length, aResponse => {
   1.874 +        if (aResponse.error) {
   1.875 +          reportError("display", aResponse);
   1.876 +          deferred.reject(aResponse);
   1.877 +        }
   1.878 +        else {
   1.879 +          deferred.resolve(aResponse.substring);
   1.880 +        }
   1.881 +      });
   1.882 +    }
   1.883 +    else {
   1.884 +      deferred.resolve(aValue.type || aValue);
   1.885 +    }
   1.886 +
   1.887 +    return deferred.promise.then(aComment => {
   1.888 +      this.writeAsComment(aComment);
   1.889 +    });
   1.890 +  },
   1.891 +
   1.892 +  /**
   1.893 +   * Write out a value at the next line from the current insertion point.
   1.894 +   * The comment block will always be preceded by a newline character.
   1.895 +   * @param object aValue
   1.896 +   *        The Object to write out as a string
   1.897 +   */
   1.898 +  writeAsComment: function SP_writeAsComment(aValue)
   1.899 +  {
   1.900 +    let value = "\n/*\n" + aValue + "\n*/";
   1.901 +
   1.902 +    if (this.editor.somethingSelected()) {
   1.903 +      let from = this.editor.getCursor("end");
   1.904 +      this.editor.replaceSelection(this.editor.getSelection() + value);
   1.905 +      let to = this.editor.getPosition(this.editor.getOffset(from) + value.length);
   1.906 +      this.editor.setSelection(from, to);
   1.907 +      return;
   1.908 +    }
   1.909 +
   1.910 +    let text = this.editor.getText();
   1.911 +    this.editor.setText(text + value);
   1.912 +
   1.913 +    let [ from, to ] = this.editor.getPosition(text.length, (text + value).length);
   1.914 +    this.editor.setSelection(from, to);
   1.915 +  },
   1.916 +
   1.917 +  /**
   1.918 +   * Write out an error at the current insertion point as a block comment
   1.919 +   * @param object aValue
   1.920 +   *        The Error object to write out the message and stack trace
   1.921 +   * @return Promise
   1.922 +   *         The promise that indicates when writing the comment completes.
   1.923 +   */
   1.924 +  writeAsErrorComment: function SP_writeAsErrorComment(aError)
   1.925 +  {
   1.926 +    let deferred = promise.defer();
   1.927 +
   1.928 +    if (VariablesView.isPrimitive({ value: aError })) {
   1.929 +      let type = aError.type;
   1.930 +      if (type == "undefined" ||
   1.931 +          type == "null" ||
   1.932 +          type == "Infinity" ||
   1.933 +          type == "-Infinity" ||
   1.934 +          type == "NaN" ||
   1.935 +          type == "-0") {
   1.936 +        deferred.resolve(type);
   1.937 +      }
   1.938 +      else if (type == "longString") {
   1.939 +        deferred.resolve(aError.initial + "\u2026");
   1.940 +      }
   1.941 +      else {
   1.942 +        deferred.resolve(aError);
   1.943 +      }
   1.944 +    }
   1.945 +    else {
   1.946 +      let objectClient = new ObjectClient(this.debuggerClient, aError);
   1.947 +      objectClient.getPrototypeAndProperties(aResponse => {
   1.948 +        if (aResponse.error) {
   1.949 +          deferred.reject(aResponse);
   1.950 +          return;
   1.951 +        }
   1.952 +
   1.953 +        let { ownProperties, safeGetterValues } = aResponse;
   1.954 +        let error = Object.create(null);
   1.955 +
   1.956 +        // Combine all the property descriptor/getter values into one object.
   1.957 +        for (let key of Object.keys(safeGetterValues)) {
   1.958 +          error[key] = safeGetterValues[key].getterValue;
   1.959 +        }
   1.960 +
   1.961 +        for (let key of Object.keys(ownProperties)) {
   1.962 +          error[key] = ownProperties[key].value;
   1.963 +        }
   1.964 +
   1.965 +        // Assemble the best possible stack we can given the properties we have.
   1.966 +        let stack;
   1.967 +        if (typeof error.stack == "string" && error.stack) {
   1.968 +          stack = error.stack;
   1.969 +        }
   1.970 +        else if (typeof error.fileName == "string") {
   1.971 +          stack = "@" + error.fileName;
   1.972 +          if (typeof error.lineNumber == "number") {
   1.973 +            stack += ":" + error.lineNumber;
   1.974 +          }
   1.975 +        }
   1.976 +        else if (typeof error.lineNumber == "number") {
   1.977 +          stack = "@" + error.lineNumber;
   1.978 +        }
   1.979 +
   1.980 +        stack = stack ? "\n" + stack.replace(/\n$/, "") : "";
   1.981 +
   1.982 +        if (typeof error.message == "string") {
   1.983 +          deferred.resolve(error.message + stack);
   1.984 +        }
   1.985 +        else {
   1.986 +          objectClient.getDisplayString(aResponse => {
   1.987 +            if (aResponse.error) {
   1.988 +              deferred.reject(aResponse);
   1.989 +            }
   1.990 +            else if (typeof aResponse.displayString == "string") {
   1.991 +              deferred.resolve(aResponse.displayString + stack);
   1.992 +            }
   1.993 +            else {
   1.994 +              deferred.resolve(stack);
   1.995 +            }
   1.996 +          });
   1.997 +        }
   1.998 +      });
   1.999 +    }
  1.1000 +
  1.1001 +    return deferred.promise.then(aMessage => {
  1.1002 +      console.error(aMessage);
  1.1003 +      this.writeAsComment("Exception: " + aMessage);
  1.1004 +    });
  1.1005 +  },
  1.1006 +
  1.1007 +  // Menu Operations
  1.1008 +
  1.1009 +  /**
  1.1010 +   * Open a new Scratchpad window.
  1.1011 +   *
  1.1012 +   * @return nsIWindow
  1.1013 +   */
  1.1014 +  openScratchpad: function SP_openScratchpad()
  1.1015 +  {
  1.1016 +    return ScratchpadManager.openScratchpad();
  1.1017 +  },
  1.1018 +
  1.1019 +  /**
  1.1020 +   * Export the textbox content to a file.
  1.1021 +   *
  1.1022 +   * @param nsILocalFile aFile
  1.1023 +   *        The file where you want to save the textbox content.
  1.1024 +   * @param boolean aNoConfirmation
  1.1025 +   *        If the file already exists, ask for confirmation?
  1.1026 +   * @param boolean aSilentError
  1.1027 +   *        True if you do not want to display an error when file save fails,
  1.1028 +   *        false otherwise.
  1.1029 +   * @param function aCallback
  1.1030 +   *        Optional function you want to call when file save completes. It will
  1.1031 +   *        get the following arguments:
  1.1032 +   *        1) the nsresult status code for the export operation.
  1.1033 +   */
  1.1034 +  exportToFile: function SP_exportToFile(aFile, aNoConfirmation, aSilentError,
  1.1035 +                                         aCallback)
  1.1036 +  {
  1.1037 +    if (!aNoConfirmation && aFile.exists() &&
  1.1038 +        !window.confirm(this.strings.
  1.1039 +                        GetStringFromName("export.fileOverwriteConfirmation"))) {
  1.1040 +      return;
  1.1041 +    }
  1.1042 +
  1.1043 +    let encoder = new TextEncoder();
  1.1044 +    let buffer = encoder.encode(this.getText());
  1.1045 +    let writePromise = OS.File.writeAtomic(aFile.path, buffer,{tmpPath: aFile.path + ".tmp"});
  1.1046 +    writePromise.then(value => {
  1.1047 +      if (aCallback) {
  1.1048 +        aCallback.call(this, Components.results.NS_OK);
  1.1049 +      }
  1.1050 +    }, reason => {
  1.1051 +      if (!aSilentError) {
  1.1052 +        window.alert(this.strings.GetStringFromName("saveFile.failed"));
  1.1053 +      }
  1.1054 +      if (aCallback) {
  1.1055 +        aCallback.call(this, Components.results.NS_ERROR_UNEXPECTED);
  1.1056 +      }
  1.1057 +    });
  1.1058 +
  1.1059 +  },
  1.1060 +
  1.1061 +  /**
  1.1062 +   * Read the content of a file and put it into the textbox.
  1.1063 +   *
  1.1064 +   * @param nsILocalFile aFile
  1.1065 +   *        The file you want to save the textbox content into.
  1.1066 +   * @param boolean aSilentError
  1.1067 +   *        True if you do not want to display an error when file load fails,
  1.1068 +   *        false otherwise.
  1.1069 +   * @param function aCallback
  1.1070 +   *        Optional function you want to call when file load completes. It will
  1.1071 +   *        get the following arguments:
  1.1072 +   *        1) the nsresult status code for the import operation.
  1.1073 +   *        2) the data that was read from the file, if any.
  1.1074 +   */
  1.1075 +  importFromFile: function SP_importFromFile(aFile, aSilentError, aCallback)
  1.1076 +  {
  1.1077 +    // Prevent file type detection.
  1.1078 +    let channel = NetUtil.newChannel(aFile);
  1.1079 +    channel.contentType = "application/javascript";
  1.1080 +
  1.1081 +    NetUtil.asyncFetch(channel, (aInputStream, aStatus) => {
  1.1082 +      let content = null;
  1.1083 +
  1.1084 +      if (Components.isSuccessCode(aStatus)) {
  1.1085 +        let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
  1.1086 +                        createInstance(Ci.nsIScriptableUnicodeConverter);
  1.1087 +        converter.charset = "UTF-8";
  1.1088 +        content = NetUtil.readInputStreamToString(aInputStream,
  1.1089 +                                                  aInputStream.available());
  1.1090 +        content = converter.ConvertToUnicode(content);
  1.1091 +
  1.1092 +        // Check to see if the first line is a mode-line comment.
  1.1093 +        let line = content.split("\n")[0];
  1.1094 +        let modeline = this._scanModeLine(line);
  1.1095 +        let chrome = Services.prefs.getBoolPref(DEVTOOLS_CHROME_ENABLED);
  1.1096 +
  1.1097 +        if (chrome && modeline["-sp-context"] === "browser") {
  1.1098 +          this.setBrowserContext();
  1.1099 +        }
  1.1100 +
  1.1101 +        this.editor.setText(content);
  1.1102 +        this.editor.clearHistory();
  1.1103 +        this.dirty = false;
  1.1104 +        document.getElementById("sp-cmd-revert").setAttribute("disabled", true);
  1.1105 +      }
  1.1106 +      else if (!aSilentError) {
  1.1107 +        window.alert(this.strings.GetStringFromName("openFile.failed"));
  1.1108 +      }
  1.1109 +
  1.1110 +      if (aCallback) {
  1.1111 +        aCallback.call(this, aStatus, content);
  1.1112 +      }
  1.1113 +    });
  1.1114 +  },
  1.1115 +
  1.1116 +  /**
  1.1117 +   * Open a file to edit in the Scratchpad.
  1.1118 +   *
  1.1119 +   * @param integer aIndex
  1.1120 +   *        Optional integer: clicked menuitem in the 'Open Recent'-menu.
  1.1121 +   */
  1.1122 +  openFile: function SP_openFile(aIndex)
  1.1123 +  {
  1.1124 +    let promptCallback = aFile => {
  1.1125 +      this.promptSave((aCloseFile, aSaved, aStatus) => {
  1.1126 +        let shouldOpen = aCloseFile;
  1.1127 +        if (aSaved && !Components.isSuccessCode(aStatus)) {
  1.1128 +          shouldOpen = false;
  1.1129 +        }
  1.1130 +
  1.1131 +        if (shouldOpen) {
  1.1132 +          let file;
  1.1133 +          if (aFile) {
  1.1134 +            file = aFile;
  1.1135 +          } else {
  1.1136 +            file = Components.classes["@mozilla.org/file/local;1"].
  1.1137 +                   createInstance(Components.interfaces.nsILocalFile);
  1.1138 +            let filePath = this.getRecentFiles()[aIndex];
  1.1139 +            file.initWithPath(filePath);
  1.1140 +          }
  1.1141 +
  1.1142 +          if (!file.exists()) {
  1.1143 +            this.notificationBox.appendNotification(
  1.1144 +              this.strings.GetStringFromName("fileNoLongerExists.notification"),
  1.1145 +              "file-no-longer-exists",
  1.1146 +              null,
  1.1147 +              this.notificationBox.PRIORITY_WARNING_HIGH,
  1.1148 +              null);
  1.1149 +
  1.1150 +            this.clearFiles(aIndex, 1);
  1.1151 +            return;
  1.1152 +          }
  1.1153 +
  1.1154 +          this.setFilename(file.path);
  1.1155 +          this.importFromFile(file, false);
  1.1156 +          this.setRecentFile(file);
  1.1157 +        }
  1.1158 +      });
  1.1159 +    };
  1.1160 +
  1.1161 +    if (aIndex > -1) {
  1.1162 +      promptCallback();
  1.1163 +    } else {
  1.1164 +      let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
  1.1165 +      fp.init(window, this.strings.GetStringFromName("openFile.title"),
  1.1166 +              Ci.nsIFilePicker.modeOpen);
  1.1167 +      fp.defaultString = "";
  1.1168 +      fp.appendFilter("JavaScript Files", "*.js; *.jsm; *.json");
  1.1169 +      fp.appendFilter("All Files", "*.*");
  1.1170 +      fp.open(aResult => {
  1.1171 +        if (aResult != Ci.nsIFilePicker.returnCancel) {
  1.1172 +          promptCallback(fp.file);
  1.1173 +        }
  1.1174 +      });
  1.1175 +    }
  1.1176 +  },
  1.1177 +
  1.1178 +  /**
  1.1179 +   * Get recent files.
  1.1180 +   *
  1.1181 +   * @return Array
  1.1182 +   *         File paths.
  1.1183 +   */
  1.1184 +  getRecentFiles: function SP_getRecentFiles()
  1.1185 +  {
  1.1186 +    let branch = Services.prefs.getBranch("devtools.scratchpad.");
  1.1187 +    let filePaths = [];
  1.1188 +
  1.1189 +    // WARNING: Do not use getCharPref here, it doesn't play nicely with
  1.1190 +    // Unicode strings.
  1.1191 +
  1.1192 +    if (branch.prefHasUserValue("recentFilePaths")) {
  1.1193 +      let data = branch.getComplexValue("recentFilePaths",
  1.1194 +        Ci.nsISupportsString).data;
  1.1195 +      filePaths = JSON.parse(data);
  1.1196 +    }
  1.1197 +
  1.1198 +    return filePaths;
  1.1199 +  },
  1.1200 +
  1.1201 +  /**
  1.1202 +   * Save a recent file in a JSON parsable string.
  1.1203 +   *
  1.1204 +   * @param nsILocalFile aFile
  1.1205 +   *        The nsILocalFile we want to save as a recent file.
  1.1206 +   */
  1.1207 +  setRecentFile: function SP_setRecentFile(aFile)
  1.1208 +  {
  1.1209 +    let maxRecent = Services.prefs.getIntPref(PREF_RECENT_FILES_MAX);
  1.1210 +    if (maxRecent < 1) {
  1.1211 +      return;
  1.1212 +    }
  1.1213 +
  1.1214 +    let filePaths = this.getRecentFiles();
  1.1215 +    let filesCount = filePaths.length;
  1.1216 +    let pathIndex = filePaths.indexOf(aFile.path);
  1.1217 +
  1.1218 +    // We are already storing this file in the list of recent files.
  1.1219 +    if (pathIndex > -1) {
  1.1220 +      // If it's already the most recent file, we don't have to do anything.
  1.1221 +      if (pathIndex === (filesCount - 1)) {
  1.1222 +        // Updating the menu to clear the disabled state from the wrong menuitem
  1.1223 +        // in rare cases when two or more Scratchpad windows are open and the
  1.1224 +        // same file has been opened in two or more windows.
  1.1225 +        this.populateRecentFilesMenu();
  1.1226 +        return;
  1.1227 +      }
  1.1228 +
  1.1229 +      // It is not the most recent file. Remove it from the list, we add it as
  1.1230 +      // the most recent farther down.
  1.1231 +      filePaths.splice(pathIndex, 1);
  1.1232 +    }
  1.1233 +    // If we are not storing the file and the 'recent files'-list is full,
  1.1234 +    // remove the oldest file from the list.
  1.1235 +    else if (filesCount === maxRecent) {
  1.1236 +      filePaths.shift();
  1.1237 +    }
  1.1238 +
  1.1239 +    filePaths.push(aFile.path);
  1.1240 +
  1.1241 +    // WARNING: Do not use setCharPref here, it doesn't play nicely with
  1.1242 +    // Unicode strings.
  1.1243 +
  1.1244 +    let str = Cc["@mozilla.org/supports-string;1"]
  1.1245 +      .createInstance(Ci.nsISupportsString);
  1.1246 +    str.data = JSON.stringify(filePaths);
  1.1247 +
  1.1248 +    let branch = Services.prefs.getBranch("devtools.scratchpad.");
  1.1249 +    branch.setComplexValue("recentFilePaths",
  1.1250 +      Ci.nsISupportsString, str);
  1.1251 +  },
  1.1252 +
  1.1253 +  /**
  1.1254 +   * Populates the 'Open Recent'-menu.
  1.1255 +   */
  1.1256 +  populateRecentFilesMenu: function SP_populateRecentFilesMenu()
  1.1257 +  {
  1.1258 +    let maxRecent = Services.prefs.getIntPref(PREF_RECENT_FILES_MAX);
  1.1259 +    let recentFilesMenu = document.getElementById("sp-open_recent-menu");
  1.1260 +
  1.1261 +    if (maxRecent < 1) {
  1.1262 +      recentFilesMenu.setAttribute("hidden", true);
  1.1263 +      return;
  1.1264 +    }
  1.1265 +
  1.1266 +    let recentFilesPopup = recentFilesMenu.firstChild;
  1.1267 +    let filePaths = this.getRecentFiles();
  1.1268 +    let filename = this.getState().filename;
  1.1269 +
  1.1270 +    recentFilesMenu.setAttribute("disabled", true);
  1.1271 +    while (recentFilesPopup.hasChildNodes()) {
  1.1272 +      recentFilesPopup.removeChild(recentFilesPopup.firstChild);
  1.1273 +    }
  1.1274 +
  1.1275 +    if (filePaths.length > 0) {
  1.1276 +      recentFilesMenu.removeAttribute("disabled");
  1.1277 +
  1.1278 +      // Print out menuitems with the most recent file first.
  1.1279 +      for (let i = filePaths.length - 1; i >= 0; --i) {
  1.1280 +        let menuitem = document.createElement("menuitem");
  1.1281 +        menuitem.setAttribute("type", "radio");
  1.1282 +        menuitem.setAttribute("label", filePaths[i]);
  1.1283 +
  1.1284 +        if (filePaths[i] === filename) {
  1.1285 +          menuitem.setAttribute("checked", true);
  1.1286 +          menuitem.setAttribute("disabled", true);
  1.1287 +        }
  1.1288 +
  1.1289 +        menuitem.addEventListener("command", Scratchpad.openFile.bind(Scratchpad, i));
  1.1290 +        recentFilesPopup.appendChild(menuitem);
  1.1291 +      }
  1.1292 +
  1.1293 +      recentFilesPopup.appendChild(document.createElement("menuseparator"));
  1.1294 +      let clearItems = document.createElement("menuitem");
  1.1295 +      clearItems.setAttribute("id", "sp-menu-clear_recent");
  1.1296 +      clearItems.setAttribute("label",
  1.1297 +                              this.strings.
  1.1298 +                              GetStringFromName("clearRecentMenuItems.label"));
  1.1299 +      clearItems.setAttribute("command", "sp-cmd-clearRecentFiles");
  1.1300 +      recentFilesPopup.appendChild(clearItems);
  1.1301 +    }
  1.1302 +  },
  1.1303 +
  1.1304 +  /**
  1.1305 +   * Clear a range of files from the list.
  1.1306 +   *
  1.1307 +   * @param integer aIndex
  1.1308 +   *        Index of file in menu to remove.
  1.1309 +   * @param integer aLength
  1.1310 +   *        Number of files from the index 'aIndex' to remove.
  1.1311 +   */
  1.1312 +  clearFiles: function SP_clearFile(aIndex, aLength)
  1.1313 +  {
  1.1314 +    let filePaths = this.getRecentFiles();
  1.1315 +    filePaths.splice(aIndex, aLength);
  1.1316 +
  1.1317 +    // WARNING: Do not use setCharPref here, it doesn't play nicely with
  1.1318 +    // Unicode strings.
  1.1319 +
  1.1320 +    let str = Cc["@mozilla.org/supports-string;1"]
  1.1321 +      .createInstance(Ci.nsISupportsString);
  1.1322 +    str.data = JSON.stringify(filePaths);
  1.1323 +
  1.1324 +    let branch = Services.prefs.getBranch("devtools.scratchpad.");
  1.1325 +    branch.setComplexValue("recentFilePaths",
  1.1326 +      Ci.nsISupportsString, str);
  1.1327 +  },
  1.1328 +
  1.1329 +  /**
  1.1330 +   * Clear all recent files.
  1.1331 +   */
  1.1332 +  clearRecentFiles: function SP_clearRecentFiles()
  1.1333 +  {
  1.1334 +    Services.prefs.clearUserPref("devtools.scratchpad.recentFilePaths");
  1.1335 +  },
  1.1336 +
  1.1337 +  /**
  1.1338 +   * Handle changes to the 'PREF_RECENT_FILES_MAX'-preference.
  1.1339 +   */
  1.1340 +  handleRecentFileMaxChange: function SP_handleRecentFileMaxChange()
  1.1341 +  {
  1.1342 +    let maxRecent = Services.prefs.getIntPref(PREF_RECENT_FILES_MAX);
  1.1343 +    let menu = document.getElementById("sp-open_recent-menu");
  1.1344 +
  1.1345 +    // Hide the menu if the 'PREF_RECENT_FILES_MAX'-pref is set to zero or less.
  1.1346 +    if (maxRecent < 1) {
  1.1347 +      menu.setAttribute("hidden", true);
  1.1348 +    } else {
  1.1349 +      if (menu.hasAttribute("hidden")) {
  1.1350 +        if (!menu.firstChild.hasChildNodes()) {
  1.1351 +          this.populateRecentFilesMenu();
  1.1352 +        }
  1.1353 +
  1.1354 +        menu.removeAttribute("hidden");
  1.1355 +      }
  1.1356 +
  1.1357 +      let filePaths = this.getRecentFiles();
  1.1358 +      if (maxRecent < filePaths.length) {
  1.1359 +        let diff = filePaths.length - maxRecent;
  1.1360 +        this.clearFiles(0, diff);
  1.1361 +      }
  1.1362 +    }
  1.1363 +  },
  1.1364 +  /**
  1.1365 +   * Save the textbox content to the currently open file.
  1.1366 +   *
  1.1367 +   * @param function aCallback
  1.1368 +   *        Optional function you want to call when file is saved
  1.1369 +   */
  1.1370 +  saveFile: function SP_saveFile(aCallback)
  1.1371 +  {
  1.1372 +    if (!this.filename) {
  1.1373 +      return this.saveFileAs(aCallback);
  1.1374 +    }
  1.1375 +
  1.1376 +    let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
  1.1377 +    file.initWithPath(this.filename);
  1.1378 +
  1.1379 +    this.exportToFile(file, true, false, aStatus => {
  1.1380 +      if (Components.isSuccessCode(aStatus)) {
  1.1381 +        this.dirty = false;
  1.1382 +        document.getElementById("sp-cmd-revert").setAttribute("disabled", true);
  1.1383 +        this.setRecentFile(file);
  1.1384 +      }
  1.1385 +      if (aCallback) {
  1.1386 +        aCallback(aStatus);
  1.1387 +      }
  1.1388 +    });
  1.1389 +  },
  1.1390 +
  1.1391 +  /**
  1.1392 +   * Save the textbox content to a new file.
  1.1393 +   *
  1.1394 +   * @param function aCallback
  1.1395 +   *        Optional function you want to call when file is saved
  1.1396 +   */
  1.1397 +  saveFileAs: function SP_saveFileAs(aCallback)
  1.1398 +  {
  1.1399 +    let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
  1.1400 +    let fpCallback = aResult => {
  1.1401 +      if (aResult != Ci.nsIFilePicker.returnCancel) {
  1.1402 +        this.setFilename(fp.file.path);
  1.1403 +        this.exportToFile(fp.file, true, false, aStatus => {
  1.1404 +          if (Components.isSuccessCode(aStatus)) {
  1.1405 +            this.dirty = false;
  1.1406 +            this.setRecentFile(fp.file);
  1.1407 +          }
  1.1408 +          if (aCallback) {
  1.1409 +            aCallback(aStatus);
  1.1410 +          }
  1.1411 +        });
  1.1412 +      }
  1.1413 +    };
  1.1414 +
  1.1415 +    fp.init(window, this.strings.GetStringFromName("saveFileAs"),
  1.1416 +            Ci.nsIFilePicker.modeSave);
  1.1417 +    fp.defaultString = "scratchpad.js";
  1.1418 +    fp.appendFilter("JavaScript Files", "*.js; *.jsm; *.json");
  1.1419 +    fp.appendFilter("All Files", "*.*");
  1.1420 +    fp.open(fpCallback);
  1.1421 +  },
  1.1422 +
  1.1423 +  /**
  1.1424 +   * Restore content from saved version of current file.
  1.1425 +   *
  1.1426 +   * @param function aCallback
  1.1427 +   *        Optional function you want to call when file is saved
  1.1428 +   */
  1.1429 +  revertFile: function SP_revertFile(aCallback)
  1.1430 +  {
  1.1431 +    let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
  1.1432 +    file.initWithPath(this.filename);
  1.1433 +
  1.1434 +    if (!file.exists()) {
  1.1435 +      return;
  1.1436 +    }
  1.1437 +
  1.1438 +    this.importFromFile(file, false, (aStatus, aContent) => {
  1.1439 +      if (aCallback) {
  1.1440 +        aCallback(aStatus);
  1.1441 +      }
  1.1442 +    });
  1.1443 +  },
  1.1444 +
  1.1445 +  /**
  1.1446 +   * Prompt to revert scratchpad if it has unsaved changes.
  1.1447 +   *
  1.1448 +   * @param function aCallback
  1.1449 +   *        Optional function you want to call when file is saved. The callback
  1.1450 +   *        receives three arguments:
  1.1451 +   *          - aRevert (boolean) - tells if the file has been reverted.
  1.1452 +   *          - status (number) - the file revert status result (if the file was
  1.1453 +   *          saved).
  1.1454 +   */
  1.1455 +  promptRevert: function SP_promptRervert(aCallback)
  1.1456 +  {
  1.1457 +    if (this.filename) {
  1.1458 +      let ps = Services.prompt;
  1.1459 +      let flags = ps.BUTTON_POS_0 * ps.BUTTON_TITLE_REVERT +
  1.1460 +                  ps.BUTTON_POS_1 * ps.BUTTON_TITLE_CANCEL;
  1.1461 +
  1.1462 +      let button = ps.confirmEx(window,
  1.1463 +                          this.strings.GetStringFromName("confirmRevert.title"),
  1.1464 +                          this.strings.GetStringFromName("confirmRevert"),
  1.1465 +                          flags, null, null, null, null, {});
  1.1466 +      if (button == BUTTON_POSITION_CANCEL) {
  1.1467 +        if (aCallback) {
  1.1468 +          aCallback(false);
  1.1469 +        }
  1.1470 +
  1.1471 +        return;
  1.1472 +      }
  1.1473 +      if (button == BUTTON_POSITION_REVERT) {
  1.1474 +        this.revertFile(aStatus => {
  1.1475 +          if (aCallback) {
  1.1476 +            aCallback(true, aStatus);
  1.1477 +          }
  1.1478 +        });
  1.1479 +
  1.1480 +        return;
  1.1481 +      }
  1.1482 +    }
  1.1483 +    if (aCallback) {
  1.1484 +      aCallback(false);
  1.1485 +    }
  1.1486 +  },
  1.1487 +
  1.1488 +  /**
  1.1489 +   * Open the Error Console.
  1.1490 +   */
  1.1491 +  openErrorConsole: function SP_openErrorConsole()
  1.1492 +  {
  1.1493 +    this.browserWindow.HUDService.toggleBrowserConsole();
  1.1494 +  },
  1.1495 +
  1.1496 +  /**
  1.1497 +   * Open the Web Console.
  1.1498 +   */
  1.1499 +  openWebConsole: function SP_openWebConsole()
  1.1500 +  {
  1.1501 +    let target = TargetFactory.forTab(this.gBrowser.selectedTab);
  1.1502 +    gDevTools.showToolbox(target, "webconsole");
  1.1503 +    this.browserWindow.focus();
  1.1504 +  },
  1.1505 +
  1.1506 +  /**
  1.1507 +   * Set the current execution context to be the active tab content window.
  1.1508 +   */
  1.1509 +  setContentContext: function SP_setContentContext()
  1.1510 +  {
  1.1511 +    if (this.executionContext == SCRATCHPAD_CONTEXT_CONTENT) {
  1.1512 +      return;
  1.1513 +    }
  1.1514 +
  1.1515 +    let content = document.getElementById("sp-menu-content");
  1.1516 +    document.getElementById("sp-menu-browser").removeAttribute("checked");
  1.1517 +    document.getElementById("sp-cmd-reloadAndRun").removeAttribute("disabled");
  1.1518 +    content.setAttribute("checked", true);
  1.1519 +    this.executionContext = SCRATCHPAD_CONTEXT_CONTENT;
  1.1520 +    this.notificationBox.removeAllNotifications(false);
  1.1521 +  },
  1.1522 +
  1.1523 +  /**
  1.1524 +   * Set the current execution context to be the most recent chrome window.
  1.1525 +   */
  1.1526 +  setBrowserContext: function SP_setBrowserContext()
  1.1527 +  {
  1.1528 +    if (this.executionContext == SCRATCHPAD_CONTEXT_BROWSER) {
  1.1529 +      return;
  1.1530 +    }
  1.1531 +
  1.1532 +    let browser = document.getElementById("sp-menu-browser");
  1.1533 +    let reloadAndRun = document.getElementById("sp-cmd-reloadAndRun");
  1.1534 +
  1.1535 +    document.getElementById("sp-menu-content").removeAttribute("checked");
  1.1536 +    reloadAndRun.setAttribute("disabled", true);
  1.1537 +    browser.setAttribute("checked", true);
  1.1538 +
  1.1539 +    this.executionContext = SCRATCHPAD_CONTEXT_BROWSER;
  1.1540 +    this.notificationBox.appendNotification(
  1.1541 +      this.strings.GetStringFromName("browserContext.notification"),
  1.1542 +      SCRATCHPAD_CONTEXT_BROWSER,
  1.1543 +      null,
  1.1544 +      this.notificationBox.PRIORITY_WARNING_HIGH,
  1.1545 +      null);
  1.1546 +  },
  1.1547 +
  1.1548 +  /**
  1.1549 +   * Gets the ID of the inner window of the given DOM window object.
  1.1550 +   *
  1.1551 +   * @param nsIDOMWindow aWindow
  1.1552 +   * @return integer
  1.1553 +   *         the inner window ID
  1.1554 +   */
  1.1555 +  getInnerWindowId: function SP_getInnerWindowId(aWindow)
  1.1556 +  {
  1.1557 +    return aWindow.QueryInterface(Ci.nsIInterfaceRequestor).
  1.1558 +           getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID;
  1.1559 +  },
  1.1560 +
  1.1561 +  /**
  1.1562 +   * The Scratchpad window load event handler. This method
  1.1563 +   * initializes the Scratchpad window and source editor.
  1.1564 +   *
  1.1565 +   * @param nsIDOMEvent aEvent
  1.1566 +   */
  1.1567 +  onLoad: function SP_onLoad(aEvent)
  1.1568 +  {
  1.1569 +    if (aEvent.target != document) {
  1.1570 +      return;
  1.1571 +    }
  1.1572 +
  1.1573 +    let chrome = Services.prefs.getBoolPref(DEVTOOLS_CHROME_ENABLED);
  1.1574 +    if (chrome) {
  1.1575 +      let environmentMenu = document.getElementById("sp-environment-menu");
  1.1576 +      let errorConsoleCommand = document.getElementById("sp-cmd-errorConsole");
  1.1577 +      let chromeContextCommand = document.getElementById("sp-cmd-browserContext");
  1.1578 +      environmentMenu.removeAttribute("hidden");
  1.1579 +      chromeContextCommand.removeAttribute("disabled");
  1.1580 +      errorConsoleCommand.removeAttribute("disabled");
  1.1581 +    }
  1.1582 +
  1.1583 +    let initialText = this.strings.formatStringFromName(
  1.1584 +      "scratchpadIntro1",
  1.1585 +      [ShortcutUtils.prettifyShortcut(document.getElementById("sp-key-run"), true),
  1.1586 +       ShortcutUtils.prettifyShortcut(document.getElementById("sp-key-inspect"), true),
  1.1587 +       ShortcutUtils.prettifyShortcut(document.getElementById("sp-key-display"), true)],
  1.1588 +      3);
  1.1589 +
  1.1590 +    let args = window.arguments;
  1.1591 +    let state = null;
  1.1592 +
  1.1593 +    if (args && args[0] instanceof Ci.nsIDialogParamBlock) {
  1.1594 +      args = args[0];
  1.1595 +      this._instanceId = args.GetString(0);
  1.1596 +
  1.1597 +      state = args.GetString(1) || null;
  1.1598 +      if (state) {
  1.1599 +        state = JSON.parse(state);
  1.1600 +        this.setState(state);
  1.1601 +        initialText = state.text;
  1.1602 +      }
  1.1603 +    } else {
  1.1604 +      this._instanceId = ScratchpadManager.createUid();
  1.1605 +    }
  1.1606 +
  1.1607 +    let config = {
  1.1608 +      mode: Editor.modes.js,
  1.1609 +      value: initialText,
  1.1610 +      lineNumbers: true,
  1.1611 +      showTrailingSpace: Services.prefs.getBoolPref(SHOW_TRAILING_SPACE),
  1.1612 +      enableCodeFolding: Services.prefs.getBoolPref(ENABLE_CODE_FOLDING),
  1.1613 +      contextMenu: "scratchpad-text-popup"
  1.1614 +    };
  1.1615 +
  1.1616 +    this.editor = new Editor(config);
  1.1617 +    this.editor.appendTo(document.querySelector("#scratchpad-editor")).then(() => {
  1.1618 +      var lines = initialText.split("\n");
  1.1619 +
  1.1620 +      this.editor.on("change", this._onChanged);
  1.1621 +      this.editor.on("save", () => this.saveFile());
  1.1622 +      this.editor.focus();
  1.1623 +      this.editor.setCursor({ line: lines.length, ch: lines.pop().length });
  1.1624 +
  1.1625 +      if (state)
  1.1626 +        this.dirty = !state.saved;
  1.1627 +
  1.1628 +      this.initialized = true;
  1.1629 +      this._triggerObservers("Ready");
  1.1630 +      this.populateRecentFilesMenu();
  1.1631 +      PreferenceObserver.init();
  1.1632 +      CloseObserver.init();
  1.1633 +    }).then(null, (err) => console.log(err.message));
  1.1634 +    this._setupCommandListeners();
  1.1635 +    this._setupPopupShowingListeners();
  1.1636 +  },
  1.1637 +
  1.1638 +  /**
  1.1639 +   * The Source Editor "change" event handler. This function updates the
  1.1640 +   * Scratchpad window title to show an asterisk when there are unsaved changes.
  1.1641 +   *
  1.1642 +   * @private
  1.1643 +   */
  1.1644 +  _onChanged: function SP__onChanged()
  1.1645 +  {
  1.1646 +    Scratchpad._updateTitle();
  1.1647 +
  1.1648 +    if (Scratchpad.filename) {
  1.1649 +      if (Scratchpad.dirty)
  1.1650 +        document.getElementById("sp-cmd-revert").removeAttribute("disabled");
  1.1651 +      else
  1.1652 +        document.getElementById("sp-cmd-revert").setAttribute("disabled", true);
  1.1653 +    }
  1.1654 +  },
  1.1655 +
  1.1656 +  /**
  1.1657 +   * Undo the last action of the user.
  1.1658 +   */
  1.1659 +  undo: function SP_undo()
  1.1660 +  {
  1.1661 +    this.editor.undo();
  1.1662 +  },
  1.1663 +
  1.1664 +  /**
  1.1665 +   * Redo the previously undone action.
  1.1666 +   */
  1.1667 +  redo: function SP_redo()
  1.1668 +  {
  1.1669 +    this.editor.redo();
  1.1670 +  },
  1.1671 +
  1.1672 +  /**
  1.1673 +   * The Scratchpad window unload event handler. This method unloads/destroys
  1.1674 +   * the source editor.
  1.1675 +   *
  1.1676 +   * @param nsIDOMEvent aEvent
  1.1677 +   */
  1.1678 +  onUnload: function SP_onUnload(aEvent)
  1.1679 +  {
  1.1680 +    if (aEvent.target != document) {
  1.1681 +      return;
  1.1682 +    }
  1.1683 +
  1.1684 +    // This event is created only after user uses 'reload and run' feature.
  1.1685 +    if (this._reloadAndRunEvent && this.gBrowser) {
  1.1686 +      this.gBrowser.selectedBrowser.removeEventListener("load",
  1.1687 +          this._reloadAndRunEvent, true);
  1.1688 +    }
  1.1689 +
  1.1690 +    PreferenceObserver.uninit();
  1.1691 +    CloseObserver.uninit();
  1.1692 +
  1.1693 +    this.editor.off("change", this._onChanged);
  1.1694 +    this.editor.destroy();
  1.1695 +    this.editor = null;
  1.1696 +
  1.1697 +    if (this._sidebar) {
  1.1698 +      this._sidebar.destroy();
  1.1699 +      this._sidebar = null;
  1.1700 +    }
  1.1701 +
  1.1702 +    if (this._prettyPrintWorker) {
  1.1703 +      this._prettyPrintWorker.terminate();
  1.1704 +      this._prettyPrintWorker = null;
  1.1705 +    }
  1.1706 +
  1.1707 +    scratchpadTargets = null;
  1.1708 +    this.webConsoleClient = null;
  1.1709 +    this.debuggerClient = null;
  1.1710 +    this.initialized = false;
  1.1711 +  },
  1.1712 +
  1.1713 +  /**
  1.1714 +   * Prompt to save scratchpad if it has unsaved changes.
  1.1715 +   *
  1.1716 +   * @param function aCallback
  1.1717 +   *        Optional function you want to call when file is saved. The callback
  1.1718 +   *        receives three arguments:
  1.1719 +   *          - toClose (boolean) - tells if the window should be closed.
  1.1720 +   *          - saved (boolen) - tells if the file has been saved.
  1.1721 +   *          - status (number) - the file save status result (if the file was
  1.1722 +   *          saved).
  1.1723 +   * @return boolean
  1.1724 +   *         Whether the window should be closed
  1.1725 +   */
  1.1726 +  promptSave: function SP_promptSave(aCallback)
  1.1727 +  {
  1.1728 +    if (this.dirty) {
  1.1729 +      let ps = Services.prompt;
  1.1730 +      let flags = ps.BUTTON_POS_0 * ps.BUTTON_TITLE_SAVE +
  1.1731 +                  ps.BUTTON_POS_1 * ps.BUTTON_TITLE_CANCEL +
  1.1732 +                  ps.BUTTON_POS_2 * ps.BUTTON_TITLE_DONT_SAVE;
  1.1733 +
  1.1734 +      let button = ps.confirmEx(window,
  1.1735 +                          this.strings.GetStringFromName("confirmClose.title"),
  1.1736 +                          this.strings.GetStringFromName("confirmClose"),
  1.1737 +                          flags, null, null, null, null, {});
  1.1738 +
  1.1739 +      if (button == BUTTON_POSITION_CANCEL) {
  1.1740 +        if (aCallback) {
  1.1741 +          aCallback(false, false);
  1.1742 +        }
  1.1743 +        return false;
  1.1744 +      }
  1.1745 +
  1.1746 +      if (button == BUTTON_POSITION_SAVE) {
  1.1747 +        this.saveFile(aStatus => {
  1.1748 +          if (aCallback) {
  1.1749 +            aCallback(true, true, aStatus);
  1.1750 +          }
  1.1751 +        });
  1.1752 +        return true;
  1.1753 +      }
  1.1754 +    }
  1.1755 +
  1.1756 +    if (aCallback) {
  1.1757 +      aCallback(true, false);
  1.1758 +    }
  1.1759 +    return true;
  1.1760 +  },
  1.1761 +
  1.1762 +  /**
  1.1763 +   * Handler for window close event. Prompts to save scratchpad if
  1.1764 +   * there are unsaved changes.
  1.1765 +   *
  1.1766 +   * @param nsIDOMEvent aEvent
  1.1767 +   * @param function aCallback
  1.1768 +   *        Optional function you want to call when file is saved/closed.
  1.1769 +   *        Used mainly for tests.
  1.1770 +   */
  1.1771 +  onClose: function SP_onClose(aEvent, aCallback)
  1.1772 +  {
  1.1773 +    aEvent.preventDefault();
  1.1774 +    this.close(aCallback);
  1.1775 +  },
  1.1776 +
  1.1777 +  /**
  1.1778 +   * Close the scratchpad window. Prompts before closing if the scratchpad
  1.1779 +   * has unsaved changes.
  1.1780 +   *
  1.1781 +   * @param function aCallback
  1.1782 +   *        Optional function you want to call when file is saved
  1.1783 +   */
  1.1784 +  close: function SP_close(aCallback)
  1.1785 +  {
  1.1786 +    let shouldClose;
  1.1787 +
  1.1788 +    this.promptSave((aShouldClose, aSaved, aStatus) => {
  1.1789 +       shouldClose = aShouldClose;
  1.1790 +      if (aSaved && !Components.isSuccessCode(aStatus)) {
  1.1791 +        shouldClose = false;
  1.1792 +      }
  1.1793 +
  1.1794 +      if (shouldClose) {
  1.1795 +        telemetry.toolClosed("scratchpad");
  1.1796 +        window.close();
  1.1797 +      }
  1.1798 +
  1.1799 +      if (aCallback) {
  1.1800 +        aCallback(shouldClose);
  1.1801 +      }
  1.1802 +    });
  1.1803 +
  1.1804 +    return shouldClose;
  1.1805 +  },
  1.1806 +
  1.1807 +  /**
  1.1808 +   * Toggle a editor's boolean option.
  1.1809 +   */
  1.1810 +  toggleEditorOption: function SP_toggleEditorOption(optionName)
  1.1811 +  {
  1.1812 +    let newOptionValue = !this.editor.getOption(optionName);
  1.1813 +    this.editor.setOption(optionName, newOptionValue);
  1.1814 +  },
  1.1815 +
  1.1816 +  /**
  1.1817 +   * Increase the editor's font size by 1 px.
  1.1818 +   */
  1.1819 +  increaseFontSize: function SP_increaseFontSize()
  1.1820 +  {
  1.1821 +    let size = this.editor.getFontSize();
  1.1822 +
  1.1823 +    if (size < MAXIMUM_FONT_SIZE) {
  1.1824 +      this.editor.setFontSize(size + 1);
  1.1825 +    }
  1.1826 +  },
  1.1827 +
  1.1828 +  /**
  1.1829 +   * Decrease the editor's font size by 1 px.
  1.1830 +   */
  1.1831 +  decreaseFontSize: function SP_decreaseFontSize()
  1.1832 +  {
  1.1833 +    let size = this.editor.getFontSize();
  1.1834 +
  1.1835 +    if (size > MINIMUM_FONT_SIZE) {
  1.1836 +      this.editor.setFontSize(size - 1);
  1.1837 +    }
  1.1838 +  },
  1.1839 +
  1.1840 +  /**
  1.1841 +   * Restore the editor's original font size.
  1.1842 +   */
  1.1843 +  normalFontSize: function SP_normalFontSize()
  1.1844 +  {
  1.1845 +    this.editor.setFontSize(NORMAL_FONT_SIZE);
  1.1846 +  },
  1.1847 +
  1.1848 +  _observers: [],
  1.1849 +
  1.1850 +  /**
  1.1851 +   * Add an observer for Scratchpad events.
  1.1852 +   *
  1.1853 +   * The observer implements IScratchpadObserver := {
  1.1854 +   *   onReady:      Called when the Scratchpad and its Editor are ready.
  1.1855 +   *                 Arguments: (Scratchpad aScratchpad)
  1.1856 +   * }
  1.1857 +   *
  1.1858 +   * All observer handlers are optional.
  1.1859 +   *
  1.1860 +   * @param IScratchpadObserver aObserver
  1.1861 +   * @see removeObserver
  1.1862 +   */
  1.1863 +  addObserver: function SP_addObserver(aObserver)
  1.1864 +  {
  1.1865 +    this._observers.push(aObserver);
  1.1866 +  },
  1.1867 +
  1.1868 +  /**
  1.1869 +   * Remove an observer for Scratchpad events.
  1.1870 +   *
  1.1871 +   * @param IScratchpadObserver aObserver
  1.1872 +   * @see addObserver
  1.1873 +   */
  1.1874 +  removeObserver: function SP_removeObserver(aObserver)
  1.1875 +  {
  1.1876 +    let index = this._observers.indexOf(aObserver);
  1.1877 +    if (index != -1) {
  1.1878 +      this._observers.splice(index, 1);
  1.1879 +    }
  1.1880 +  },
  1.1881 +
  1.1882 +  /**
  1.1883 +   * Trigger named handlers in Scratchpad observers.
  1.1884 +   *
  1.1885 +   * @param string aName
  1.1886 +   *        Name of the handler to trigger.
  1.1887 +   * @param Array aArgs
  1.1888 +   *        Optional array of arguments to pass to the observer(s).
  1.1889 +   * @see addObserver
  1.1890 +   */
  1.1891 +  _triggerObservers: function SP_triggerObservers(aName, aArgs)
  1.1892 +  {
  1.1893 +    // insert this Scratchpad instance as the first argument
  1.1894 +    if (!aArgs) {
  1.1895 +      aArgs = [this];
  1.1896 +    } else {
  1.1897 +      aArgs.unshift(this);
  1.1898 +    }
  1.1899 +
  1.1900 +    // trigger all observers that implement this named handler
  1.1901 +    for (let i = 0; i < this._observers.length; ++i) {
  1.1902 +      let observer = this._observers[i];
  1.1903 +      let handler = observer["on" + aName];
  1.1904 +      if (handler) {
  1.1905 +        handler.apply(observer, aArgs);
  1.1906 +      }
  1.1907 +    }
  1.1908 +  },
  1.1909 +
  1.1910 +  /**
  1.1911 +   * Opens the MDN documentation page for Scratchpad.
  1.1912 +   */
  1.1913 +  openDocumentationPage: function SP_openDocumentationPage()
  1.1914 +  {
  1.1915 +    let url = this.strings.GetStringFromName("help.openDocumentationPage");
  1.1916 +    let newTab = this.gBrowser.addTab(url);
  1.1917 +    this.browserWindow.focus();
  1.1918 +    this.gBrowser.selectedTab = newTab;
  1.1919 +  },
  1.1920 +};
  1.1921 +
  1.1922 +
  1.1923 +/**
  1.1924 + * Represents the DebuggerClient connection to a specific tab as used by the
  1.1925 + * Scratchpad.
  1.1926 + *
  1.1927 + * @param object aTab
  1.1928 + *              The tab to connect to.
  1.1929 + */
  1.1930 +function ScratchpadTab(aTab)
  1.1931 +{
  1.1932 +  this._tab = aTab;
  1.1933 +}
  1.1934 +
  1.1935 +let scratchpadTargets = new WeakMap();
  1.1936 +
  1.1937 +/**
  1.1938 + * Returns the object containing the DebuggerClient and WebConsoleClient for a
  1.1939 + * given tab or window.
  1.1940 + *
  1.1941 + * @param object aSubject
  1.1942 + *        The tab or window to obtain the connection for.
  1.1943 + * @return Promise
  1.1944 + *         The promise for the connection information.
  1.1945 + */
  1.1946 +ScratchpadTab.consoleFor = function consoleFor(aSubject)
  1.1947 +{
  1.1948 +  if (!scratchpadTargets.has(aSubject)) {
  1.1949 +    scratchpadTargets.set(aSubject, new this(aSubject));
  1.1950 +  }
  1.1951 +  return scratchpadTargets.get(aSubject).connect();
  1.1952 +};
  1.1953 +
  1.1954 +
  1.1955 +ScratchpadTab.prototype = {
  1.1956 +  /**
  1.1957 +   * The promise for the connection.
  1.1958 +   */
  1.1959 +  _connector: null,
  1.1960 +
  1.1961 +  /**
  1.1962 +   * Initialize a debugger client and connect it to the debugger server.
  1.1963 +   *
  1.1964 +   * @return Promise
  1.1965 +   *         The promise for the result of connecting to this tab or window.
  1.1966 +   */
  1.1967 +  connect: function ST_connect()
  1.1968 +  {
  1.1969 +    if (this._connector) {
  1.1970 +      return this._connector;
  1.1971 +    }
  1.1972 +
  1.1973 +    let deferred = promise.defer();
  1.1974 +    this._connector = deferred.promise;
  1.1975 +
  1.1976 +    let connectTimer = setTimeout(() => {
  1.1977 +      deferred.reject({
  1.1978 +        error: "timeout",
  1.1979 +        message: Scratchpad.strings.GetStringFromName("connectionTimeout"),
  1.1980 +      });
  1.1981 +    }, REMOTE_TIMEOUT);
  1.1982 +
  1.1983 +    deferred.promise.then(() => clearTimeout(connectTimer));
  1.1984 +
  1.1985 +    this._attach().then(aTarget => {
  1.1986 +      let consoleActor = aTarget.form.consoleActor;
  1.1987 +      let client = aTarget.client;
  1.1988 +      client.attachConsole(consoleActor, [], (aResponse, aWebConsoleClient) => {
  1.1989 +        if (aResponse.error) {
  1.1990 +          reportError("attachConsole", aResponse);
  1.1991 +          deferred.reject(aResponse);
  1.1992 +        }
  1.1993 +        else {
  1.1994 +          deferred.resolve({
  1.1995 +            webConsoleClient: aWebConsoleClient,
  1.1996 +            debuggerClient: client
  1.1997 +          });
  1.1998 +        }
  1.1999 +      });
  1.2000 +    });
  1.2001 +
  1.2002 +    return deferred.promise;
  1.2003 +  },
  1.2004 +
  1.2005 +  /**
  1.2006 +   * Attach to this tab.
  1.2007 +   *
  1.2008 +   * @return Promise
  1.2009 +   *         The promise for the TabTarget for this tab.
  1.2010 +   */
  1.2011 +  _attach: function ST__attach()
  1.2012 +  {
  1.2013 +    let target = TargetFactory.forTab(this._tab);
  1.2014 +    return target.makeRemote().then(() => target);
  1.2015 +  },
  1.2016 +};
  1.2017 +
  1.2018 +
  1.2019 +/**
  1.2020 + * Represents the DebuggerClient connection to a specific window as used by the
  1.2021 + * Scratchpad.
  1.2022 + */
  1.2023 +function ScratchpadWindow() {}
  1.2024 +
  1.2025 +ScratchpadWindow.consoleFor = ScratchpadTab.consoleFor;
  1.2026 +
  1.2027 +ScratchpadWindow.prototype = Heritage.extend(ScratchpadTab.prototype, {
  1.2028 +  /**
  1.2029 +   * Attach to this window.
  1.2030 +   *
  1.2031 +   * @return Promise
  1.2032 +   *         The promise for the target for this window.
  1.2033 +   */
  1.2034 +  _attach: function SW__attach()
  1.2035 +  {
  1.2036 +    let deferred = promise.defer();
  1.2037 +
  1.2038 +    if (!DebuggerServer.initialized) {
  1.2039 +      DebuggerServer.init();
  1.2040 +      DebuggerServer.addBrowserActors();
  1.2041 +    }
  1.2042 +
  1.2043 +    let client = new DebuggerClient(DebuggerServer.connectPipe());
  1.2044 +    client.connect(() => {
  1.2045 +      client.listTabs(aResponse => {
  1.2046 +        if (aResponse.error) {
  1.2047 +          reportError("listTabs", aResponse);
  1.2048 +          deferred.reject(aResponse);
  1.2049 +        }
  1.2050 +        else {
  1.2051 +          deferred.resolve({ form: aResponse, client: client });
  1.2052 +        }
  1.2053 +      });
  1.2054 +    });
  1.2055 +
  1.2056 +    return deferred.promise;
  1.2057 +  }
  1.2058 +});
  1.2059 +
  1.2060 +
  1.2061 +function ScratchpadTarget(aTarget)
  1.2062 +{
  1.2063 +  this._target = aTarget;
  1.2064 +}
  1.2065 +
  1.2066 +ScratchpadTarget.consoleFor = ScratchpadTab.consoleFor;
  1.2067 +
  1.2068 +ScratchpadTarget.prototype = Heritage.extend(ScratchpadTab.prototype, {
  1.2069 +  _attach: function ST__attach()
  1.2070 +  {
  1.2071 +    if (this._target.isRemote) {
  1.2072 +      return promise.resolve(this._target);
  1.2073 +    }
  1.2074 +    return this._target.makeRemote().then(() => this._target);
  1.2075 +  }
  1.2076 +});
  1.2077 +
  1.2078 +
  1.2079 +/**
  1.2080 + * Encapsulates management of the sidebar containing the VariablesView for
  1.2081 + * object inspection.
  1.2082 + */
  1.2083 +function ScratchpadSidebar(aScratchpad)
  1.2084 +{
  1.2085 +  let ToolSidebar = require("devtools/framework/sidebar").ToolSidebar;
  1.2086 +  let tabbox = document.querySelector("#scratchpad-sidebar");
  1.2087 +  this._sidebar = new ToolSidebar(tabbox, this, "scratchpad");
  1.2088 +  this._scratchpad = aScratchpad;
  1.2089 +}
  1.2090 +
  1.2091 +ScratchpadSidebar.prototype = {
  1.2092 +  /*
  1.2093 +   * The ToolSidebar for this sidebar.
  1.2094 +   */
  1.2095 +  _sidebar: null,
  1.2096 +
  1.2097 +  /*
  1.2098 +   * The VariablesView for this sidebar.
  1.2099 +   */
  1.2100 +  variablesView: null,
  1.2101 +
  1.2102 +  /*
  1.2103 +   * Whether the sidebar is currently shown.
  1.2104 +   */
  1.2105 +  visible: false,
  1.2106 +
  1.2107 +  /**
  1.2108 +   * Open the sidebar, if not open already, and populate it with the properties
  1.2109 +   * of the given object.
  1.2110 +   *
  1.2111 +   * @param string aString
  1.2112 +   *        The string that was evaluated.
  1.2113 +   * @param object aObject
  1.2114 +   *        The object to inspect, which is the aEvalString evaluation result.
  1.2115 +   * @return Promise
  1.2116 +   *         A promise that will resolve once the sidebar is open.
  1.2117 +   */
  1.2118 +  open: function SS_open(aEvalString, aObject)
  1.2119 +  {
  1.2120 +    this.show();
  1.2121 +
  1.2122 +    let deferred = promise.defer();
  1.2123 +
  1.2124 +    let onTabReady = () => {
  1.2125 +      if (this.variablesView) {
  1.2126 +        this.variablesView.controller.releaseActors();
  1.2127 +      }
  1.2128 +      else {
  1.2129 +        let window = this._sidebar.getWindowForTab("variablesview");
  1.2130 +        let container = window.document.querySelector("#variables");
  1.2131 +
  1.2132 +        this.variablesView = new VariablesView(container, {
  1.2133 +          searchEnabled: true,
  1.2134 +          searchPlaceholder: this._scratchpad.strings
  1.2135 +                             .GetStringFromName("propertiesFilterPlaceholder")
  1.2136 +        });
  1.2137 +
  1.2138 +        VariablesViewController.attach(this.variablesView, {
  1.2139 +          getEnvironmentClient: aGrip => {
  1.2140 +            return new EnvironmentClient(this._scratchpad.debuggerClient, aGrip);
  1.2141 +          },
  1.2142 +          getObjectClient: aGrip => {
  1.2143 +            return new ObjectClient(this._scratchpad.debuggerClient, aGrip);
  1.2144 +          },
  1.2145 +          getLongStringClient: aActor => {
  1.2146 +            return this._scratchpad.webConsoleClient.longString(aActor);
  1.2147 +          },
  1.2148 +          releaseActor: aActor => {
  1.2149 +            this._scratchpad.debuggerClient.release(aActor);
  1.2150 +          }
  1.2151 +        });
  1.2152 +      }
  1.2153 +      this._update(aObject).then(() => deferred.resolve());
  1.2154 +    };
  1.2155 +
  1.2156 +    if (this._sidebar.getCurrentTabID() == "variablesview") {
  1.2157 +      onTabReady();
  1.2158 +    }
  1.2159 +    else {
  1.2160 +      this._sidebar.once("variablesview-ready", onTabReady);
  1.2161 +      this._sidebar.addTab("variablesview", VARIABLES_VIEW_URL, true);
  1.2162 +    }
  1.2163 +
  1.2164 +    return deferred.promise;
  1.2165 +  },
  1.2166 +
  1.2167 +  /**
  1.2168 +   * Show the sidebar.
  1.2169 +   */
  1.2170 +  show: function SS_show()
  1.2171 +  {
  1.2172 +    if (!this.visible) {
  1.2173 +      this.visible = true;
  1.2174 +      this._sidebar.show();
  1.2175 +    }
  1.2176 +  },
  1.2177 +
  1.2178 +  /**
  1.2179 +   * Hide the sidebar.
  1.2180 +   */
  1.2181 +  hide: function SS_hide()
  1.2182 +  {
  1.2183 +    if (this.visible) {
  1.2184 +      this.visible = false;
  1.2185 +      this._sidebar.hide();
  1.2186 +    }
  1.2187 +  },
  1.2188 +
  1.2189 +  /**
  1.2190 +   * Destroy the sidebar.
  1.2191 +   *
  1.2192 +   * @return Promise
  1.2193 +   *         The promise that resolves when the sidebar is destroyed.
  1.2194 +   */
  1.2195 +  destroy: function SS_destroy()
  1.2196 +  {
  1.2197 +    if (this.variablesView) {
  1.2198 +      this.variablesView.controller.releaseActors();
  1.2199 +      this.variablesView = null;
  1.2200 +    }
  1.2201 +    return this._sidebar.destroy();
  1.2202 +  },
  1.2203 +
  1.2204 +  /**
  1.2205 +   * Update the object currently inspected by the sidebar.
  1.2206 +   *
  1.2207 +   * @param object aObject
  1.2208 +   *        The object to inspect in the sidebar.
  1.2209 +   * @return Promise
  1.2210 +   *         A promise that resolves when the update completes.
  1.2211 +   */
  1.2212 +  _update: function SS__update(aObject)
  1.2213 +  {
  1.2214 +    let options = { objectActor: aObject };
  1.2215 +    let view = this.variablesView;
  1.2216 +    view.empty();
  1.2217 +    return view.controller.setSingleVariable(options).expanded;
  1.2218 +  }
  1.2219 +};
  1.2220 +
  1.2221 +
  1.2222 +/**
  1.2223 + * Report an error coming over the remote debugger protocol.
  1.2224 + *
  1.2225 + * @param string aAction
  1.2226 + *        The name of the action or method that failed.
  1.2227 + * @param object aResponse
  1.2228 + *        The response packet that contains the error.
  1.2229 + */
  1.2230 +function reportError(aAction, aResponse)
  1.2231 +{
  1.2232 +  Cu.reportError(aAction + " failed: " + aResponse.error + " " +
  1.2233 +                 aResponse.message);
  1.2234 +}
  1.2235 +
  1.2236 +
  1.2237 +/**
  1.2238 + * The PreferenceObserver listens for preference changes while Scratchpad is
  1.2239 + * running.
  1.2240 + */
  1.2241 +var PreferenceObserver = {
  1.2242 +  _initialized: false,
  1.2243 +
  1.2244 +  init: function PO_init()
  1.2245 +  {
  1.2246 +    if (this._initialized) {
  1.2247 +      return;
  1.2248 +    }
  1.2249 +
  1.2250 +    this.branch = Services.prefs.getBranch("devtools.scratchpad.");
  1.2251 +    this.branch.addObserver("", this, false);
  1.2252 +    this._initialized = true;
  1.2253 +  },
  1.2254 +
  1.2255 +  observe: function PO_observe(aMessage, aTopic, aData)
  1.2256 +  {
  1.2257 +    if (aTopic != "nsPref:changed") {
  1.2258 +      return;
  1.2259 +    }
  1.2260 +
  1.2261 +    if (aData == "recentFilesMax") {
  1.2262 +      Scratchpad.handleRecentFileMaxChange();
  1.2263 +    }
  1.2264 +    else if (aData == "recentFilePaths") {
  1.2265 +      Scratchpad.populateRecentFilesMenu();
  1.2266 +    }
  1.2267 +  },
  1.2268 +
  1.2269 +  uninit: function PO_uninit () {
  1.2270 +    if (!this.branch) {
  1.2271 +      return;
  1.2272 +    }
  1.2273 +
  1.2274 +    this.branch.removeObserver("", this);
  1.2275 +    this.branch = null;
  1.2276 +  }
  1.2277 +};
  1.2278 +
  1.2279 +
  1.2280 +/**
  1.2281 + * The CloseObserver listens for the last browser window closing and attempts to
  1.2282 + * close the Scratchpad.
  1.2283 + */
  1.2284 +var CloseObserver = {
  1.2285 +  init: function CO_init()
  1.2286 +  {
  1.2287 +    Services.obs.addObserver(this, "browser-lastwindow-close-requested", false);
  1.2288 +  },
  1.2289 +
  1.2290 +  observe: function CO_observe(aSubject)
  1.2291 +  {
  1.2292 +    if (Scratchpad.close()) {
  1.2293 +      this.uninit();
  1.2294 +    }
  1.2295 +    else {
  1.2296 +      aSubject.QueryInterface(Ci.nsISupportsPRBool);
  1.2297 +      aSubject.data = true;
  1.2298 +    }
  1.2299 +  },
  1.2300 +
  1.2301 +  uninit: function CO_uninit()
  1.2302 +  {
  1.2303 +    Services.obs.removeObserver(this, "browser-lastwindow-close-requested",
  1.2304 +                                false);
  1.2305 +  },
  1.2306 +};
  1.2307 +
  1.2308 +XPCOMUtils.defineLazyGetter(Scratchpad, "strings", function () {
  1.2309 +  return Services.strings.createBundle(SCRATCHPAD_L10N);
  1.2310 +});
  1.2311 +
  1.2312 +addEventListener("load", Scratchpad.onLoad.bind(Scratchpad), false);
  1.2313 +addEventListener("unload", Scratchpad.onUnload.bind(Scratchpad), false);
  1.2314 +addEventListener("close", Scratchpad.onClose.bind(Scratchpad), false);

mercurial