browser/devtools/scratchpad/scratchpad.js

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

michael@0 1 /* vim:set ts=2 sw=2 sts=2 et:
michael@0 2 * This Source Code Form is subject to the terms of the Mozilla Public
michael@0 3 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 5
michael@0 6 /*
michael@0 7 * Original version history can be found here:
michael@0 8 * https://github.com/mozilla/workspace
michael@0 9 *
michael@0 10 * Copied and relicensed from the Public Domain.
michael@0 11 * See bug 653934 for details.
michael@0 12 * https://bugzilla.mozilla.org/show_bug.cgi?id=653934
michael@0 13 */
michael@0 14
michael@0 15 "use strict";
michael@0 16
michael@0 17 const Cu = Components.utils;
michael@0 18 const Cc = Components.classes;
michael@0 19 const Ci = Components.interfaces;
michael@0 20
michael@0 21 const SCRATCHPAD_CONTEXT_CONTENT = 1;
michael@0 22 const SCRATCHPAD_CONTEXT_BROWSER = 2;
michael@0 23 const BUTTON_POSITION_SAVE = 0;
michael@0 24 const BUTTON_POSITION_CANCEL = 1;
michael@0 25 const BUTTON_POSITION_DONT_SAVE = 2;
michael@0 26 const BUTTON_POSITION_REVERT = 0;
michael@0 27 const EVAL_FUNCTION_TIMEOUT = 1000; // milliseconds
michael@0 28
michael@0 29 const MAXIMUM_FONT_SIZE = 96;
michael@0 30 const MINIMUM_FONT_SIZE = 6;
michael@0 31 const NORMAL_FONT_SIZE = 12;
michael@0 32
michael@0 33 const SCRATCHPAD_L10N = "chrome://browser/locale/devtools/scratchpad.properties";
michael@0 34 const DEVTOOLS_CHROME_ENABLED = "devtools.chrome.enabled";
michael@0 35 const PREF_RECENT_FILES_MAX = "devtools.scratchpad.recentFilesMax";
michael@0 36 const SHOW_TRAILING_SPACE = "devtools.scratchpad.showTrailingSpace";
michael@0 37 const ENABLE_CODE_FOLDING = "devtools.scratchpad.enableCodeFolding";
michael@0 38
michael@0 39 const VARIABLES_VIEW_URL = "chrome://browser/content/devtools/widgets/VariablesView.xul";
michael@0 40
michael@0 41 const require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
michael@0 42
michael@0 43 const Telemetry = require("devtools/shared/telemetry");
michael@0 44 const Editor = require("devtools/sourceeditor/editor");
michael@0 45 const TargetFactory = require("devtools/framework/target").TargetFactory;
michael@0 46
michael@0 47 const { Promise: promise } = Cu.import("resource://gre/modules/Promise.jsm", {});
michael@0 48 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
michael@0 49 Cu.import("resource://gre/modules/Services.jsm");
michael@0 50 Cu.import("resource://gre/modules/NetUtil.jsm");
michael@0 51 Cu.import("resource:///modules/devtools/scratchpad-manager.jsm");
michael@0 52 Cu.import("resource://gre/modules/jsdebugger.jsm");
michael@0 53 Cu.import("resource:///modules/devtools/gDevTools.jsm");
michael@0 54 Cu.import("resource://gre/modules/osfile.jsm");
michael@0 55 Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
michael@0 56 Cu.import("resource://gre/modules/reflect.jsm");
michael@0 57 Cu.import("resource://gre/modules/devtools/DevToolsUtils.jsm");
michael@0 58
michael@0 59 XPCOMUtils.defineLazyModuleGetter(this, "VariablesView",
michael@0 60 "resource:///modules/devtools/VariablesView.jsm");
michael@0 61
michael@0 62 XPCOMUtils.defineLazyModuleGetter(this, "VariablesViewController",
michael@0 63 "resource:///modules/devtools/VariablesViewController.jsm");
michael@0 64
michael@0 65 XPCOMUtils.defineLazyModuleGetter(this, "EnvironmentClient",
michael@0 66 "resource://gre/modules/devtools/dbg-client.jsm");
michael@0 67
michael@0 68 XPCOMUtils.defineLazyModuleGetter(this, "ObjectClient",
michael@0 69 "resource://gre/modules/devtools/dbg-client.jsm");
michael@0 70
michael@0 71 XPCOMUtils.defineLazyModuleGetter(this, "WebConsoleUtils",
michael@0 72 "resource://gre/modules/devtools/WebConsoleUtils.jsm");
michael@0 73
michael@0 74 XPCOMUtils.defineLazyModuleGetter(this, "DebuggerServer",
michael@0 75 "resource://gre/modules/devtools/dbg-server.jsm");
michael@0 76
michael@0 77 XPCOMUtils.defineLazyModuleGetter(this, "DebuggerClient",
michael@0 78 "resource://gre/modules/devtools/dbg-client.jsm");
michael@0 79
michael@0 80 XPCOMUtils.defineLazyGetter(this, "REMOTE_TIMEOUT", () =>
michael@0 81 Services.prefs.getIntPref("devtools.debugger.remote-timeout"));
michael@0 82
michael@0 83 XPCOMUtils.defineLazyModuleGetter(this, "ShortcutUtils",
michael@0 84 "resource://gre/modules/ShortcutUtils.jsm");
michael@0 85
michael@0 86 XPCOMUtils.defineLazyModuleGetter(this, "Reflect",
michael@0 87 "resource://gre/modules/reflect.jsm");
michael@0 88
michael@0 89 // Because we have no constructor / destructor where we can log metrics we need
michael@0 90 // to do so here.
michael@0 91 let telemetry = new Telemetry();
michael@0 92 telemetry.toolOpened("scratchpad");
michael@0 93
michael@0 94 /**
michael@0 95 * The scratchpad object handles the Scratchpad window functionality.
michael@0 96 */
michael@0 97 var Scratchpad = {
michael@0 98 _instanceId: null,
michael@0 99 _initialWindowTitle: document.title,
michael@0 100 _dirty: false,
michael@0 101
michael@0 102 /**
michael@0 103 * Check if provided string is a mode-line and, if it is, return an
michael@0 104 * object with its values.
michael@0 105 *
michael@0 106 * @param string aLine
michael@0 107 * @return string
michael@0 108 */
michael@0 109 _scanModeLine: function SP__scanModeLine(aLine="")
michael@0 110 {
michael@0 111 aLine = aLine.trim();
michael@0 112
michael@0 113 let obj = {};
michael@0 114 let ch1 = aLine.charAt(0);
michael@0 115 let ch2 = aLine.charAt(1);
michael@0 116
michael@0 117 if (ch1 !== "/" || (ch2 !== "*" && ch2 !== "/")) {
michael@0 118 return obj;
michael@0 119 }
michael@0 120
michael@0 121 aLine = aLine
michael@0 122 .replace(/^\/\//, "")
michael@0 123 .replace(/^\/\*/, "")
michael@0 124 .replace(/\*\/$/, "");
michael@0 125
michael@0 126 aLine.split(",").forEach(pair => {
michael@0 127 let [key, val] = pair.split(":");
michael@0 128
michael@0 129 if (key && val) {
michael@0 130 obj[key.trim()] = val.trim();
michael@0 131 }
michael@0 132 });
michael@0 133
michael@0 134 return obj;
michael@0 135 },
michael@0 136
michael@0 137 /**
michael@0 138 * Add the event listeners for popupshowing events.
michael@0 139 */
michael@0 140 _setupPopupShowingListeners: function SP_setupPopupShowing() {
michael@0 141 let elementIDs = ['sp-menu_editpopup', 'scratchpad-text-popup'];
michael@0 142
michael@0 143 for (let elementID of elementIDs) {
michael@0 144 let elem = document.getElementById(elementID);
michael@0 145 if (elem) {
michael@0 146 elem.addEventListener("popupshowing", function () {
michael@0 147 goUpdateGlobalEditMenuItems();
michael@0 148 let commands = ['cmd_undo', 'cmd_redo', 'cmd_delete', 'cmd_findAgain'];
michael@0 149 commands.forEach(goUpdateCommand);
michael@0 150 });
michael@0 151 }
michael@0 152 }
michael@0 153 },
michael@0 154
michael@0 155 /**
michael@0 156 * Add the event event listeners for command events.
michael@0 157 */
michael@0 158 _setupCommandListeners: function SP_setupCommands() {
michael@0 159 let commands = {
michael@0 160 "cmd_gotoLine": () => {
michael@0 161 goDoCommand('cmd_gotoLine');
michael@0 162 },
michael@0 163 "sp-cmd-newWindow": () => {
michael@0 164 Scratchpad.openScratchpad();
michael@0 165 },
michael@0 166 "sp-cmd-openFile": () => {
michael@0 167 Scratchpad.openFile();
michael@0 168 },
michael@0 169 "sp-cmd-clearRecentFiles": () => {
michael@0 170 Scratchpad.clearRecentFiles();
michael@0 171 },
michael@0 172 "sp-cmd-save": () => {
michael@0 173 Scratchpad.saveFile();
michael@0 174 },
michael@0 175 "sp-cmd-saveas": () => {
michael@0 176 Scratchpad.saveFileAs();
michael@0 177 },
michael@0 178 "sp-cmd-revert": () => {
michael@0 179 Scratchpad.promptRevert();
michael@0 180 },
michael@0 181 "sp-cmd-close": () => {
michael@0 182 Scratchpad.close();
michael@0 183 },
michael@0 184 "sp-cmd-run": () => {
michael@0 185 Scratchpad.run();
michael@0 186 },
michael@0 187 "sp-cmd-inspect": () => {
michael@0 188 Scratchpad.inspect();
michael@0 189 },
michael@0 190 "sp-cmd-display": () => {
michael@0 191 Scratchpad.display();
michael@0 192 },
michael@0 193 "sp-cmd-pprint": () => {
michael@0 194 Scratchpad.prettyPrint();
michael@0 195 },
michael@0 196 "sp-cmd-contentContext": () => {
michael@0 197 Scratchpad.setContentContext();
michael@0 198 },
michael@0 199 "sp-cmd-browserContext": () => {
michael@0 200 Scratchpad.setBrowserContext();
michael@0 201 },
michael@0 202 "sp-cmd-reloadAndRun": () => {
michael@0 203 Scratchpad.reloadAndRun();
michael@0 204 },
michael@0 205 "sp-cmd-evalFunction": () => {
michael@0 206 Scratchpad.evalTopLevelFunction();
michael@0 207 },
michael@0 208 "sp-cmd-errorConsole": () => {
michael@0 209 Scratchpad.openErrorConsole();
michael@0 210 },
michael@0 211 "sp-cmd-webConsole": () => {
michael@0 212 Scratchpad.openWebConsole();
michael@0 213 },
michael@0 214 "sp-cmd-documentationLink": () => {
michael@0 215 Scratchpad.openDocumentationPage();
michael@0 216 },
michael@0 217 "sp-cmd-hideSidebar": () => {
michael@0 218 Scratchpad.sidebar.hide();
michael@0 219 },
michael@0 220 "sp-cmd-line-numbers": () => {
michael@0 221 Scratchpad.toggleEditorOption('lineNumbers');
michael@0 222 },
michael@0 223 "sp-cmd-wrap-text": () => {
michael@0 224 Scratchpad.toggleEditorOption('lineWrapping');
michael@0 225 },
michael@0 226 "sp-cmd-highlight-trailing-space": () => {
michael@0 227 Scratchpad.toggleEditorOption('showTrailingSpace');
michael@0 228 },
michael@0 229 "sp-cmd-larger-font": () => {
michael@0 230 Scratchpad.increaseFontSize();
michael@0 231 },
michael@0 232 "sp-cmd-smaller-font": () => {
michael@0 233 Scratchpad.decreaseFontSize();
michael@0 234 },
michael@0 235 "sp-cmd-normal-font": () => {
michael@0 236 Scratchpad.normalFontSize();
michael@0 237 },
michael@0 238 }
michael@0 239
michael@0 240 for (let command in commands) {
michael@0 241 let elem = document.getElementById(command);
michael@0 242 if (elem) {
michael@0 243 elem.addEventListener("command", commands[command]);
michael@0 244 }
michael@0 245 }
michael@0 246 },
michael@0 247
michael@0 248 /**
michael@0 249 * The script execution context. This tells Scratchpad in which context the
michael@0 250 * script shall execute.
michael@0 251 *
michael@0 252 * Possible values:
michael@0 253 * - SCRATCHPAD_CONTEXT_CONTENT to execute code in the context of the current
michael@0 254 * tab content window object.
michael@0 255 * - SCRATCHPAD_CONTEXT_BROWSER to execute code in the context of the
michael@0 256 * currently active chrome window object.
michael@0 257 */
michael@0 258 executionContext: SCRATCHPAD_CONTEXT_CONTENT,
michael@0 259
michael@0 260 /**
michael@0 261 * Tells if this Scratchpad is initialized and ready for use.
michael@0 262 * @boolean
michael@0 263 * @see addObserver
michael@0 264 */
michael@0 265 initialized: false,
michael@0 266
michael@0 267 /**
michael@0 268 * Returns the 'dirty' state of this Scratchpad.
michael@0 269 */
michael@0 270 get dirty()
michael@0 271 {
michael@0 272 let clean = this.editor && this.editor.isClean();
michael@0 273 return this._dirty || !clean;
michael@0 274 },
michael@0 275
michael@0 276 /**
michael@0 277 * Sets the 'dirty' state of this Scratchpad.
michael@0 278 */
michael@0 279 set dirty(aValue)
michael@0 280 {
michael@0 281 this._dirty = aValue;
michael@0 282 if (!aValue && this.editor)
michael@0 283 this.editor.setClean();
michael@0 284 this._updateTitle();
michael@0 285 },
michael@0 286
michael@0 287 /**
michael@0 288 * Retrieve the xul:notificationbox DOM element. It notifies the user when
michael@0 289 * the current code execution context is SCRATCHPAD_CONTEXT_BROWSER.
michael@0 290 */
michael@0 291 get notificationBox()
michael@0 292 {
michael@0 293 return document.getElementById("scratchpad-notificationbox");
michael@0 294 },
michael@0 295
michael@0 296 /**
michael@0 297 * Hide the menu bar.
michael@0 298 */
michael@0 299 hideMenu: function SP_hideMenu()
michael@0 300 {
michael@0 301 document.getElementById("sp-menubar").style.display = "none";
michael@0 302 },
michael@0 303
michael@0 304 /**
michael@0 305 * Show the menu bar.
michael@0 306 */
michael@0 307 showMenu: function SP_showMenu()
michael@0 308 {
michael@0 309 document.getElementById("sp-menubar").style.display = "";
michael@0 310 },
michael@0 311
michael@0 312 /**
michael@0 313 * Get the editor content, in the given range. If no range is given you get
michael@0 314 * the entire editor content.
michael@0 315 *
michael@0 316 * @param number [aStart=0]
michael@0 317 * Optional, start from the given offset.
michael@0 318 * @param number [aEnd=content char count]
michael@0 319 * Optional, end offset for the text you want. If this parameter is not
michael@0 320 * given, then the text returned goes until the end of the editor
michael@0 321 * content.
michael@0 322 * @return string
michael@0 323 * The text in the given range.
michael@0 324 */
michael@0 325 getText: function SP_getText(aStart, aEnd)
michael@0 326 {
michael@0 327 var value = this.editor.getText();
michael@0 328 return value.slice(aStart || 0, aEnd || value.length);
michael@0 329 },
michael@0 330
michael@0 331 /**
michael@0 332 * Set the filename in the scratchpad UI and object
michael@0 333 *
michael@0 334 * @param string aFilename
michael@0 335 * The new filename
michael@0 336 */
michael@0 337 setFilename: function SP_setFilename(aFilename)
michael@0 338 {
michael@0 339 this.filename = aFilename;
michael@0 340 this._updateTitle();
michael@0 341 },
michael@0 342
michael@0 343 /**
michael@0 344 * Update the Scratchpad window title based on the current state.
michael@0 345 * @private
michael@0 346 */
michael@0 347 _updateTitle: function SP__updateTitle()
michael@0 348 {
michael@0 349 let title = this.filename || this._initialWindowTitle;
michael@0 350
michael@0 351 if (this.dirty)
michael@0 352 title = "*" + title;
michael@0 353
michael@0 354 document.title = title;
michael@0 355 },
michael@0 356
michael@0 357 /**
michael@0 358 * Get the current state of the scratchpad. Called by the
michael@0 359 * Scratchpad Manager for session storing.
michael@0 360 *
michael@0 361 * @return object
michael@0 362 * An object with 3 properties: filename, text, and
michael@0 363 * executionContext.
michael@0 364 */
michael@0 365 getState: function SP_getState()
michael@0 366 {
michael@0 367 return {
michael@0 368 filename: this.filename,
michael@0 369 text: this.getText(),
michael@0 370 executionContext: this.executionContext,
michael@0 371 saved: !this.dirty
michael@0 372 };
michael@0 373 },
michael@0 374
michael@0 375 /**
michael@0 376 * Set the filename and execution context using the given state. Called
michael@0 377 * when scratchpad is being restored from a previous session.
michael@0 378 *
michael@0 379 * @param object aState
michael@0 380 * An object with filename and executionContext properties.
michael@0 381 */
michael@0 382 setState: function SP_setState(aState)
michael@0 383 {
michael@0 384 if (aState.filename)
michael@0 385 this.setFilename(aState.filename);
michael@0 386
michael@0 387 this.dirty = !aState.saved;
michael@0 388
michael@0 389 if (aState.executionContext == SCRATCHPAD_CONTEXT_BROWSER)
michael@0 390 this.setBrowserContext();
michael@0 391 else
michael@0 392 this.setContentContext();
michael@0 393 },
michael@0 394
michael@0 395 /**
michael@0 396 * Get the most recent chrome window of type navigator:browser.
michael@0 397 */
michael@0 398 get browserWindow()
michael@0 399 {
michael@0 400 return Services.wm.getMostRecentWindow("navigator:browser");
michael@0 401 },
michael@0 402
michael@0 403 /**
michael@0 404 * Get the gBrowser object of the most recent browser window.
michael@0 405 */
michael@0 406 get gBrowser()
michael@0 407 {
michael@0 408 let recentWin = this.browserWindow;
michael@0 409 return recentWin ? recentWin.gBrowser : null;
michael@0 410 },
michael@0 411
michael@0 412 /**
michael@0 413 * Unique name for the current Scratchpad instance. Used to distinguish
michael@0 414 * Scratchpad windows between each other. See bug 661762.
michael@0 415 */
michael@0 416 get uniqueName()
michael@0 417 {
michael@0 418 return "Scratchpad/" + this._instanceId;
michael@0 419 },
michael@0 420
michael@0 421
michael@0 422 /**
michael@0 423 * Sidebar that contains the VariablesView for object inspection.
michael@0 424 */
michael@0 425 get sidebar()
michael@0 426 {
michael@0 427 if (!this._sidebar) {
michael@0 428 this._sidebar = new ScratchpadSidebar(this);
michael@0 429 }
michael@0 430 return this._sidebar;
michael@0 431 },
michael@0 432
michael@0 433 /**
michael@0 434 * Replaces context of an editor with provided value (a string).
michael@0 435 * Note: this method is simply a shortcut to editor.setText.
michael@0 436 */
michael@0 437 setText: function SP_setText(value)
michael@0 438 {
michael@0 439 return this.editor.setText(value);
michael@0 440 },
michael@0 441
michael@0 442 /**
michael@0 443 * Evaluate a string in the currently desired context, that is either the
michael@0 444 * chrome window or the tab content window object.
michael@0 445 *
michael@0 446 * @param string aString
michael@0 447 * The script you want to evaluate.
michael@0 448 * @return Promise
michael@0 449 * The promise for the script evaluation result.
michael@0 450 */
michael@0 451 evaluate: function SP_evaluate(aString)
michael@0 452 {
michael@0 453 let connection;
michael@0 454 if (this.target) {
michael@0 455 connection = ScratchpadTarget.consoleFor(this.target);
michael@0 456 }
michael@0 457 else if (this.executionContext == SCRATCHPAD_CONTEXT_CONTENT) {
michael@0 458 connection = ScratchpadTab.consoleFor(this.gBrowser.selectedTab);
michael@0 459 }
michael@0 460 else {
michael@0 461 connection = ScratchpadWindow.consoleFor(this.browserWindow);
michael@0 462 }
michael@0 463
michael@0 464 let evalOptions = { url: this.uniqueName };
michael@0 465
michael@0 466 return connection.then(({ debuggerClient, webConsoleClient }) => {
michael@0 467 let deferred = promise.defer();
michael@0 468
michael@0 469 webConsoleClient.evaluateJS(aString, aResponse => {
michael@0 470 this.debuggerClient = debuggerClient;
michael@0 471 this.webConsoleClient = webConsoleClient;
michael@0 472 if (aResponse.error) {
michael@0 473 deferred.reject(aResponse);
michael@0 474 }
michael@0 475 else if (aResponse.exception !== null) {
michael@0 476 deferred.resolve([aString, aResponse]);
michael@0 477 }
michael@0 478 else {
michael@0 479 deferred.resolve([aString, undefined, aResponse.result]);
michael@0 480 }
michael@0 481 }, evalOptions);
michael@0 482
michael@0 483 return deferred.promise;
michael@0 484 });
michael@0 485 },
michael@0 486
michael@0 487 /**
michael@0 488 * Execute the selected text (if any) or the entire editor content in the
michael@0 489 * current context.
michael@0 490 *
michael@0 491 * @return Promise
michael@0 492 * The promise for the script evaluation result.
michael@0 493 */
michael@0 494 execute: function SP_execute()
michael@0 495 {
michael@0 496 let selection = this.editor.getSelection() || this.getText();
michael@0 497 return this.evaluate(selection);
michael@0 498 },
michael@0 499
michael@0 500 /**
michael@0 501 * Execute the selected text (if any) or the entire editor content in the
michael@0 502 * current context.
michael@0 503 *
michael@0 504 * @return Promise
michael@0 505 * The promise for the script evaluation result.
michael@0 506 */
michael@0 507 run: function SP_run()
michael@0 508 {
michael@0 509 let deferred = promise.defer();
michael@0 510 let reject = aReason => deferred.reject(aReason);
michael@0 511
michael@0 512 this.execute().then(([aString, aError, aResult]) => {
michael@0 513 let resolve = () => deferred.resolve([aString, aError, aResult]);
michael@0 514
michael@0 515 if (aError) {
michael@0 516 this.writeAsErrorComment(aError.exception).then(resolve, reject);
michael@0 517 }
michael@0 518 else {
michael@0 519 this.editor.dropSelection();
michael@0 520 resolve();
michael@0 521 }
michael@0 522 }, reject);
michael@0 523
michael@0 524 return deferred.promise;
michael@0 525 },
michael@0 526
michael@0 527 /**
michael@0 528 * Execute the selected text (if any) or the entire editor content in the
michael@0 529 * current context. If the result is primitive then it is written as a
michael@0 530 * comment. Otherwise, the resulting object is inspected up in the sidebar.
michael@0 531 *
michael@0 532 * @return Promise
michael@0 533 * The promise for the script evaluation result.
michael@0 534 */
michael@0 535 inspect: function SP_inspect()
michael@0 536 {
michael@0 537 let deferred = promise.defer();
michael@0 538 let reject = aReason => deferred.reject(aReason);
michael@0 539
michael@0 540 this.execute().then(([aString, aError, aResult]) => {
michael@0 541 let resolve = () => deferred.resolve([aString, aError, aResult]);
michael@0 542
michael@0 543 if (aError) {
michael@0 544 this.writeAsErrorComment(aError.exception).then(resolve, reject);
michael@0 545 }
michael@0 546 else if (VariablesView.isPrimitive({ value: aResult })) {
michael@0 547 this._writePrimitiveAsComment(aResult).then(resolve, reject);
michael@0 548 }
michael@0 549 else {
michael@0 550 this.editor.dropSelection();
michael@0 551 this.sidebar.open(aString, aResult).then(resolve, reject);
michael@0 552 }
michael@0 553 }, reject);
michael@0 554
michael@0 555 return deferred.promise;
michael@0 556 },
michael@0 557
michael@0 558 /**
michael@0 559 * Reload the current page and execute the entire editor content when
michael@0 560 * the page finishes loading. Note that this operation should be available
michael@0 561 * only in the content context.
michael@0 562 *
michael@0 563 * @return Promise
michael@0 564 * The promise for the script evaluation result.
michael@0 565 */
michael@0 566 reloadAndRun: function SP_reloadAndRun()
michael@0 567 {
michael@0 568 let deferred = promise.defer();
michael@0 569
michael@0 570 if (this.executionContext !== SCRATCHPAD_CONTEXT_CONTENT) {
michael@0 571 Cu.reportError(this.strings.
michael@0 572 GetStringFromName("scratchpadContext.invalid"));
michael@0 573 return;
michael@0 574 }
michael@0 575
michael@0 576 let browser = this.gBrowser.selectedBrowser;
michael@0 577
michael@0 578 this._reloadAndRunEvent = evt => {
michael@0 579 if (evt.target !== browser.contentDocument) {
michael@0 580 return;
michael@0 581 }
michael@0 582
michael@0 583 browser.removeEventListener("load", this._reloadAndRunEvent, true);
michael@0 584
michael@0 585 this.run().then(aResults => deferred.resolve(aResults));
michael@0 586 };
michael@0 587
michael@0 588 browser.addEventListener("load", this._reloadAndRunEvent, true);
michael@0 589 browser.contentWindow.location.reload();
michael@0 590
michael@0 591 return deferred.promise;
michael@0 592 },
michael@0 593
michael@0 594 /**
michael@0 595 * Execute the selected text (if any) or the entire editor content in the
michael@0 596 * current context. The evaluation result is inserted into the editor after
michael@0 597 * the selected text, or at the end of the editor content if there is no
michael@0 598 * selected text.
michael@0 599 *
michael@0 600 * @return Promise
michael@0 601 * The promise for the script evaluation result.
michael@0 602 */
michael@0 603 display: function SP_display()
michael@0 604 {
michael@0 605 let deferred = promise.defer();
michael@0 606 let reject = aReason => deferred.reject(aReason);
michael@0 607
michael@0 608 this.execute().then(([aString, aError, aResult]) => {
michael@0 609 let resolve = () => deferred.resolve([aString, aError, aResult]);
michael@0 610
michael@0 611 if (aError) {
michael@0 612 this.writeAsErrorComment(aError.exception).then(resolve, reject);
michael@0 613 }
michael@0 614 else if (VariablesView.isPrimitive({ value: aResult })) {
michael@0 615 this._writePrimitiveAsComment(aResult).then(resolve, reject);
michael@0 616 }
michael@0 617 else {
michael@0 618 let objectClient = new ObjectClient(this.debuggerClient, aResult);
michael@0 619 objectClient.getDisplayString(aResponse => {
michael@0 620 if (aResponse.error) {
michael@0 621 reportError("display", aResponse);
michael@0 622 reject(aResponse);
michael@0 623 }
michael@0 624 else {
michael@0 625 this.writeAsComment(aResponse.displayString);
michael@0 626 resolve();
michael@0 627 }
michael@0 628 });
michael@0 629 }
michael@0 630 }, reject);
michael@0 631
michael@0 632 return deferred.promise;
michael@0 633 },
michael@0 634
michael@0 635 _prettyPrintWorker: null,
michael@0 636
michael@0 637 /**
michael@0 638 * Get or create the worker that handles pretty printing.
michael@0 639 */
michael@0 640 get prettyPrintWorker() {
michael@0 641 if (!this._prettyPrintWorker) {
michael@0 642 this._prettyPrintWorker = new ChromeWorker(
michael@0 643 "resource://gre/modules/devtools/server/actors/pretty-print-worker.js");
michael@0 644
michael@0 645 this._prettyPrintWorker.addEventListener("error", ({ message, filename, lineno }) => {
michael@0 646 DevToolsUtils.reportException(message + " @ " + filename + ":" + lineno);
michael@0 647 }, false);
michael@0 648 }
michael@0 649 return this._prettyPrintWorker;
michael@0 650 },
michael@0 651
michael@0 652 /**
michael@0 653 * Pretty print the source text inside the scratchpad.
michael@0 654 *
michael@0 655 * @return Promise
michael@0 656 * A promise resolved with the pretty printed code, or rejected with
michael@0 657 * an error.
michael@0 658 */
michael@0 659 prettyPrint: function SP_prettyPrint() {
michael@0 660 const uglyText = this.getText();
michael@0 661 const tabsize = Services.prefs.getIntPref("devtools.editor.tabsize");
michael@0 662 const id = Math.random();
michael@0 663 const deferred = promise.defer();
michael@0 664
michael@0 665 const onReply = ({ data }) => {
michael@0 666 if (data.id !== id) {
michael@0 667 return;
michael@0 668 }
michael@0 669 this.prettyPrintWorker.removeEventListener("message", onReply, false);
michael@0 670
michael@0 671 if (data.error) {
michael@0 672 let errorString = DevToolsUtils.safeErrorString(data.error);
michael@0 673 this.writeAsErrorComment(errorString);
michael@0 674 deferred.reject(errorString);
michael@0 675 } else {
michael@0 676 this.editor.setText(data.code);
michael@0 677 deferred.resolve(data.code);
michael@0 678 }
michael@0 679 };
michael@0 680
michael@0 681 this.prettyPrintWorker.addEventListener("message", onReply, false);
michael@0 682 this.prettyPrintWorker.postMessage({
michael@0 683 id: id,
michael@0 684 url: "(scratchpad)",
michael@0 685 indent: tabsize,
michael@0 686 source: uglyText
michael@0 687 });
michael@0 688
michael@0 689 return deferred.promise;
michael@0 690 },
michael@0 691
michael@0 692 /**
michael@0 693 * Parse the text and return an AST. If we can't parse it, write an error
michael@0 694 * comment and return false.
michael@0 695 */
michael@0 696 _parseText: function SP__parseText(aText) {
michael@0 697 try {
michael@0 698 return Reflect.parse(aText);
michael@0 699 } catch (e) {
michael@0 700 this.writeAsErrorComment(DevToolsUtils.safeErrorString(e));
michael@0 701 return false;
michael@0 702 }
michael@0 703 },
michael@0 704
michael@0 705 /**
michael@0 706 * Determine if the given AST node location contains the given cursor
michael@0 707 * position.
michael@0 708 *
michael@0 709 * @returns Boolean
michael@0 710 */
michael@0 711 _containsCursor: function (aLoc, aCursorPos) {
michael@0 712 // Our line numbers are 1-based, while CodeMirror's are 0-based.
michael@0 713 const lineNumber = aCursorPos.line + 1;
michael@0 714 const columnNumber = aCursorPos.ch;
michael@0 715
michael@0 716 if (aLoc.start.line <= lineNumber && aLoc.end.line >= lineNumber) {
michael@0 717 if (aLoc.start.line === aLoc.end.line) {
michael@0 718 return aLoc.start.column <= columnNumber
michael@0 719 && aLoc.end.column >= columnNumber;
michael@0 720 }
michael@0 721
michael@0 722 if (aLoc.start.line == lineNumber) {
michael@0 723 return columnNumber >= aLoc.start.column;
michael@0 724 }
michael@0 725
michael@0 726 if (aLoc.end.line == lineNumber) {
michael@0 727 return columnNumber <= aLoc.end.column;
michael@0 728 }
michael@0 729
michael@0 730 return true;
michael@0 731 }
michael@0 732
michael@0 733 return false;
michael@0 734 },
michael@0 735
michael@0 736 /**
michael@0 737 * Find the top level function AST node that the cursor is within.
michael@0 738 *
michael@0 739 * @returns Object|null
michael@0 740 */
michael@0 741 _findTopLevelFunction: function SP__findTopLevelFunction(aAst, aCursorPos) {
michael@0 742 for (let statement of aAst.body) {
michael@0 743 switch (statement.type) {
michael@0 744 case "FunctionDeclaration":
michael@0 745 if (this._containsCursor(statement.loc, aCursorPos)) {
michael@0 746 return statement;
michael@0 747 }
michael@0 748 break;
michael@0 749
michael@0 750 case "VariableDeclaration":
michael@0 751 for (let decl of statement.declarations) {
michael@0 752 if (!decl.init) {
michael@0 753 continue;
michael@0 754 }
michael@0 755 if ((decl.init.type == "FunctionExpression"
michael@0 756 || decl.init.type == "ArrowExpression")
michael@0 757 && this._containsCursor(decl.loc, aCursorPos)) {
michael@0 758 return decl;
michael@0 759 }
michael@0 760 }
michael@0 761 break;
michael@0 762 }
michael@0 763 }
michael@0 764
michael@0 765 return null;
michael@0 766 },
michael@0 767
michael@0 768 /**
michael@0 769 * Get the source text associated with the given function statement.
michael@0 770 *
michael@0 771 * @param Object aFunction
michael@0 772 * @param String aFullText
michael@0 773 * @returns String
michael@0 774 */
michael@0 775 _getFunctionText: function SP__getFunctionText(aFunction, aFullText) {
michael@0 776 let functionText = "";
michael@0 777 // Initially set to 0, but incremented first thing in the loop below because
michael@0 778 // line numbers are 1 based, not 0 based.
michael@0 779 let lineNumber = 0;
michael@0 780 const { start, end } = aFunction.loc;
michael@0 781 const singleLine = start.line === end.line;
michael@0 782
michael@0 783 for (let line of aFullText.split(/\n/g)) {
michael@0 784 lineNumber++;
michael@0 785
michael@0 786 if (singleLine && start.line === lineNumber) {
michael@0 787 functionText = line.slice(start.column, end.column);
michael@0 788 break;
michael@0 789 }
michael@0 790
michael@0 791 if (start.line === lineNumber) {
michael@0 792 functionText += line.slice(start.column) + "\n";
michael@0 793 continue;
michael@0 794 }
michael@0 795
michael@0 796 if (end.line === lineNumber) {
michael@0 797 functionText += line.slice(0, end.column);
michael@0 798 break;
michael@0 799 }
michael@0 800
michael@0 801 if (start.line < lineNumber && end.line > lineNumber) {
michael@0 802 functionText += line + "\n";
michael@0 803 }
michael@0 804 }
michael@0 805
michael@0 806 return functionText;
michael@0 807 },
michael@0 808
michael@0 809 /**
michael@0 810 * Evaluate the top level function that the cursor is resting in.
michael@0 811 *
michael@0 812 * @returns Promise [text, error, result]
michael@0 813 */
michael@0 814 evalTopLevelFunction: function SP_evalTopLevelFunction() {
michael@0 815 const text = this.getText();
michael@0 816 const ast = this._parseText(text);
michael@0 817 if (!ast) {
michael@0 818 return promise.resolve([text, undefined, undefined]);
michael@0 819 }
michael@0 820
michael@0 821 const cursorPos = this.editor.getCursor();
michael@0 822 const funcStatement = this._findTopLevelFunction(ast, cursorPos);
michael@0 823 if (!funcStatement) {
michael@0 824 return promise.resolve([text, undefined, undefined]);
michael@0 825 }
michael@0 826
michael@0 827 let functionText = this._getFunctionText(funcStatement, text);
michael@0 828
michael@0 829 // TODO: This is a work around for bug 940086. It should be removed when
michael@0 830 // that is fixed.
michael@0 831 if (funcStatement.type == "FunctionDeclaration"
michael@0 832 && !functionText.startsWith("function ")) {
michael@0 833 functionText = "function " + functionText;
michael@0 834 funcStatement.loc.start.column -= 9;
michael@0 835 }
michael@0 836
michael@0 837 // The decrement by one is because our line numbers are 1-based, while
michael@0 838 // CodeMirror's are 0-based.
michael@0 839 const from = {
michael@0 840 line: funcStatement.loc.start.line - 1,
michael@0 841 ch: funcStatement.loc.start.column
michael@0 842 };
michael@0 843 const to = {
michael@0 844 line: funcStatement.loc.end.line - 1,
michael@0 845 ch: funcStatement.loc.end.column
michael@0 846 };
michael@0 847
michael@0 848 const marker = this.editor.markText(from, to, "eval-text");
michael@0 849 setTimeout(() => marker.clear(), EVAL_FUNCTION_TIMEOUT);
michael@0 850
michael@0 851 return this.evaluate(functionText);
michael@0 852 },
michael@0 853
michael@0 854 /**
michael@0 855 * Writes out a primitive value as a comment. This handles values which are
michael@0 856 * to be printed directly (number, string) as well as grips to values
michael@0 857 * (null, undefined, longString).
michael@0 858 *
michael@0 859 * @param any aValue
michael@0 860 * The value to print.
michael@0 861 * @return Promise
michael@0 862 * The promise that resolves after the value has been printed.
michael@0 863 */
michael@0 864 _writePrimitiveAsComment: function SP__writePrimitiveAsComment(aValue)
michael@0 865 {
michael@0 866 let deferred = promise.defer();
michael@0 867
michael@0 868 if (aValue.type == "longString") {
michael@0 869 let client = this.webConsoleClient;
michael@0 870 client.longString(aValue).substring(0, aValue.length, aResponse => {
michael@0 871 if (aResponse.error) {
michael@0 872 reportError("display", aResponse);
michael@0 873 deferred.reject(aResponse);
michael@0 874 }
michael@0 875 else {
michael@0 876 deferred.resolve(aResponse.substring);
michael@0 877 }
michael@0 878 });
michael@0 879 }
michael@0 880 else {
michael@0 881 deferred.resolve(aValue.type || aValue);
michael@0 882 }
michael@0 883
michael@0 884 return deferred.promise.then(aComment => {
michael@0 885 this.writeAsComment(aComment);
michael@0 886 });
michael@0 887 },
michael@0 888
michael@0 889 /**
michael@0 890 * Write out a value at the next line from the current insertion point.
michael@0 891 * The comment block will always be preceded by a newline character.
michael@0 892 * @param object aValue
michael@0 893 * The Object to write out as a string
michael@0 894 */
michael@0 895 writeAsComment: function SP_writeAsComment(aValue)
michael@0 896 {
michael@0 897 let value = "\n/*\n" + aValue + "\n*/";
michael@0 898
michael@0 899 if (this.editor.somethingSelected()) {
michael@0 900 let from = this.editor.getCursor("end");
michael@0 901 this.editor.replaceSelection(this.editor.getSelection() + value);
michael@0 902 let to = this.editor.getPosition(this.editor.getOffset(from) + value.length);
michael@0 903 this.editor.setSelection(from, to);
michael@0 904 return;
michael@0 905 }
michael@0 906
michael@0 907 let text = this.editor.getText();
michael@0 908 this.editor.setText(text + value);
michael@0 909
michael@0 910 let [ from, to ] = this.editor.getPosition(text.length, (text + value).length);
michael@0 911 this.editor.setSelection(from, to);
michael@0 912 },
michael@0 913
michael@0 914 /**
michael@0 915 * Write out an error at the current insertion point as a block comment
michael@0 916 * @param object aValue
michael@0 917 * The Error object to write out the message and stack trace
michael@0 918 * @return Promise
michael@0 919 * The promise that indicates when writing the comment completes.
michael@0 920 */
michael@0 921 writeAsErrorComment: function SP_writeAsErrorComment(aError)
michael@0 922 {
michael@0 923 let deferred = promise.defer();
michael@0 924
michael@0 925 if (VariablesView.isPrimitive({ value: aError })) {
michael@0 926 let type = aError.type;
michael@0 927 if (type == "undefined" ||
michael@0 928 type == "null" ||
michael@0 929 type == "Infinity" ||
michael@0 930 type == "-Infinity" ||
michael@0 931 type == "NaN" ||
michael@0 932 type == "-0") {
michael@0 933 deferred.resolve(type);
michael@0 934 }
michael@0 935 else if (type == "longString") {
michael@0 936 deferred.resolve(aError.initial + "\u2026");
michael@0 937 }
michael@0 938 else {
michael@0 939 deferred.resolve(aError);
michael@0 940 }
michael@0 941 }
michael@0 942 else {
michael@0 943 let objectClient = new ObjectClient(this.debuggerClient, aError);
michael@0 944 objectClient.getPrototypeAndProperties(aResponse => {
michael@0 945 if (aResponse.error) {
michael@0 946 deferred.reject(aResponse);
michael@0 947 return;
michael@0 948 }
michael@0 949
michael@0 950 let { ownProperties, safeGetterValues } = aResponse;
michael@0 951 let error = Object.create(null);
michael@0 952
michael@0 953 // Combine all the property descriptor/getter values into one object.
michael@0 954 for (let key of Object.keys(safeGetterValues)) {
michael@0 955 error[key] = safeGetterValues[key].getterValue;
michael@0 956 }
michael@0 957
michael@0 958 for (let key of Object.keys(ownProperties)) {
michael@0 959 error[key] = ownProperties[key].value;
michael@0 960 }
michael@0 961
michael@0 962 // Assemble the best possible stack we can given the properties we have.
michael@0 963 let stack;
michael@0 964 if (typeof error.stack == "string" && error.stack) {
michael@0 965 stack = error.stack;
michael@0 966 }
michael@0 967 else if (typeof error.fileName == "string") {
michael@0 968 stack = "@" + error.fileName;
michael@0 969 if (typeof error.lineNumber == "number") {
michael@0 970 stack += ":" + error.lineNumber;
michael@0 971 }
michael@0 972 }
michael@0 973 else if (typeof error.lineNumber == "number") {
michael@0 974 stack = "@" + error.lineNumber;
michael@0 975 }
michael@0 976
michael@0 977 stack = stack ? "\n" + stack.replace(/\n$/, "") : "";
michael@0 978
michael@0 979 if (typeof error.message == "string") {
michael@0 980 deferred.resolve(error.message + stack);
michael@0 981 }
michael@0 982 else {
michael@0 983 objectClient.getDisplayString(aResponse => {
michael@0 984 if (aResponse.error) {
michael@0 985 deferred.reject(aResponse);
michael@0 986 }
michael@0 987 else if (typeof aResponse.displayString == "string") {
michael@0 988 deferred.resolve(aResponse.displayString + stack);
michael@0 989 }
michael@0 990 else {
michael@0 991 deferred.resolve(stack);
michael@0 992 }
michael@0 993 });
michael@0 994 }
michael@0 995 });
michael@0 996 }
michael@0 997
michael@0 998 return deferred.promise.then(aMessage => {
michael@0 999 console.error(aMessage);
michael@0 1000 this.writeAsComment("Exception: " + aMessage);
michael@0 1001 });
michael@0 1002 },
michael@0 1003
michael@0 1004 // Menu Operations
michael@0 1005
michael@0 1006 /**
michael@0 1007 * Open a new Scratchpad window.
michael@0 1008 *
michael@0 1009 * @return nsIWindow
michael@0 1010 */
michael@0 1011 openScratchpad: function SP_openScratchpad()
michael@0 1012 {
michael@0 1013 return ScratchpadManager.openScratchpad();
michael@0 1014 },
michael@0 1015
michael@0 1016 /**
michael@0 1017 * Export the textbox content to a file.
michael@0 1018 *
michael@0 1019 * @param nsILocalFile aFile
michael@0 1020 * The file where you want to save the textbox content.
michael@0 1021 * @param boolean aNoConfirmation
michael@0 1022 * If the file already exists, ask for confirmation?
michael@0 1023 * @param boolean aSilentError
michael@0 1024 * True if you do not want to display an error when file save fails,
michael@0 1025 * false otherwise.
michael@0 1026 * @param function aCallback
michael@0 1027 * Optional function you want to call when file save completes. It will
michael@0 1028 * get the following arguments:
michael@0 1029 * 1) the nsresult status code for the export operation.
michael@0 1030 */
michael@0 1031 exportToFile: function SP_exportToFile(aFile, aNoConfirmation, aSilentError,
michael@0 1032 aCallback)
michael@0 1033 {
michael@0 1034 if (!aNoConfirmation && aFile.exists() &&
michael@0 1035 !window.confirm(this.strings.
michael@0 1036 GetStringFromName("export.fileOverwriteConfirmation"))) {
michael@0 1037 return;
michael@0 1038 }
michael@0 1039
michael@0 1040 let encoder = new TextEncoder();
michael@0 1041 let buffer = encoder.encode(this.getText());
michael@0 1042 let writePromise = OS.File.writeAtomic(aFile.path, buffer,{tmpPath: aFile.path + ".tmp"});
michael@0 1043 writePromise.then(value => {
michael@0 1044 if (aCallback) {
michael@0 1045 aCallback.call(this, Components.results.NS_OK);
michael@0 1046 }
michael@0 1047 }, reason => {
michael@0 1048 if (!aSilentError) {
michael@0 1049 window.alert(this.strings.GetStringFromName("saveFile.failed"));
michael@0 1050 }
michael@0 1051 if (aCallback) {
michael@0 1052 aCallback.call(this, Components.results.NS_ERROR_UNEXPECTED);
michael@0 1053 }
michael@0 1054 });
michael@0 1055
michael@0 1056 },
michael@0 1057
michael@0 1058 /**
michael@0 1059 * Read the content of a file and put it into the textbox.
michael@0 1060 *
michael@0 1061 * @param nsILocalFile aFile
michael@0 1062 * The file you want to save the textbox content into.
michael@0 1063 * @param boolean aSilentError
michael@0 1064 * True if you do not want to display an error when file load fails,
michael@0 1065 * false otherwise.
michael@0 1066 * @param function aCallback
michael@0 1067 * Optional function you want to call when file load completes. It will
michael@0 1068 * get the following arguments:
michael@0 1069 * 1) the nsresult status code for the import operation.
michael@0 1070 * 2) the data that was read from the file, if any.
michael@0 1071 */
michael@0 1072 importFromFile: function SP_importFromFile(aFile, aSilentError, aCallback)
michael@0 1073 {
michael@0 1074 // Prevent file type detection.
michael@0 1075 let channel = NetUtil.newChannel(aFile);
michael@0 1076 channel.contentType = "application/javascript";
michael@0 1077
michael@0 1078 NetUtil.asyncFetch(channel, (aInputStream, aStatus) => {
michael@0 1079 let content = null;
michael@0 1080
michael@0 1081 if (Components.isSuccessCode(aStatus)) {
michael@0 1082 let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
michael@0 1083 createInstance(Ci.nsIScriptableUnicodeConverter);
michael@0 1084 converter.charset = "UTF-8";
michael@0 1085 content = NetUtil.readInputStreamToString(aInputStream,
michael@0 1086 aInputStream.available());
michael@0 1087 content = converter.ConvertToUnicode(content);
michael@0 1088
michael@0 1089 // Check to see if the first line is a mode-line comment.
michael@0 1090 let line = content.split("\n")[0];
michael@0 1091 let modeline = this._scanModeLine(line);
michael@0 1092 let chrome = Services.prefs.getBoolPref(DEVTOOLS_CHROME_ENABLED);
michael@0 1093
michael@0 1094 if (chrome && modeline["-sp-context"] === "browser") {
michael@0 1095 this.setBrowserContext();
michael@0 1096 }
michael@0 1097
michael@0 1098 this.editor.setText(content);
michael@0 1099 this.editor.clearHistory();
michael@0 1100 this.dirty = false;
michael@0 1101 document.getElementById("sp-cmd-revert").setAttribute("disabled", true);
michael@0 1102 }
michael@0 1103 else if (!aSilentError) {
michael@0 1104 window.alert(this.strings.GetStringFromName("openFile.failed"));
michael@0 1105 }
michael@0 1106
michael@0 1107 if (aCallback) {
michael@0 1108 aCallback.call(this, aStatus, content);
michael@0 1109 }
michael@0 1110 });
michael@0 1111 },
michael@0 1112
michael@0 1113 /**
michael@0 1114 * Open a file to edit in the Scratchpad.
michael@0 1115 *
michael@0 1116 * @param integer aIndex
michael@0 1117 * Optional integer: clicked menuitem in the 'Open Recent'-menu.
michael@0 1118 */
michael@0 1119 openFile: function SP_openFile(aIndex)
michael@0 1120 {
michael@0 1121 let promptCallback = aFile => {
michael@0 1122 this.promptSave((aCloseFile, aSaved, aStatus) => {
michael@0 1123 let shouldOpen = aCloseFile;
michael@0 1124 if (aSaved && !Components.isSuccessCode(aStatus)) {
michael@0 1125 shouldOpen = false;
michael@0 1126 }
michael@0 1127
michael@0 1128 if (shouldOpen) {
michael@0 1129 let file;
michael@0 1130 if (aFile) {
michael@0 1131 file = aFile;
michael@0 1132 } else {
michael@0 1133 file = Components.classes["@mozilla.org/file/local;1"].
michael@0 1134 createInstance(Components.interfaces.nsILocalFile);
michael@0 1135 let filePath = this.getRecentFiles()[aIndex];
michael@0 1136 file.initWithPath(filePath);
michael@0 1137 }
michael@0 1138
michael@0 1139 if (!file.exists()) {
michael@0 1140 this.notificationBox.appendNotification(
michael@0 1141 this.strings.GetStringFromName("fileNoLongerExists.notification"),
michael@0 1142 "file-no-longer-exists",
michael@0 1143 null,
michael@0 1144 this.notificationBox.PRIORITY_WARNING_HIGH,
michael@0 1145 null);
michael@0 1146
michael@0 1147 this.clearFiles(aIndex, 1);
michael@0 1148 return;
michael@0 1149 }
michael@0 1150
michael@0 1151 this.setFilename(file.path);
michael@0 1152 this.importFromFile(file, false);
michael@0 1153 this.setRecentFile(file);
michael@0 1154 }
michael@0 1155 });
michael@0 1156 };
michael@0 1157
michael@0 1158 if (aIndex > -1) {
michael@0 1159 promptCallback();
michael@0 1160 } else {
michael@0 1161 let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
michael@0 1162 fp.init(window, this.strings.GetStringFromName("openFile.title"),
michael@0 1163 Ci.nsIFilePicker.modeOpen);
michael@0 1164 fp.defaultString = "";
michael@0 1165 fp.appendFilter("JavaScript Files", "*.js; *.jsm; *.json");
michael@0 1166 fp.appendFilter("All Files", "*.*");
michael@0 1167 fp.open(aResult => {
michael@0 1168 if (aResult != Ci.nsIFilePicker.returnCancel) {
michael@0 1169 promptCallback(fp.file);
michael@0 1170 }
michael@0 1171 });
michael@0 1172 }
michael@0 1173 },
michael@0 1174
michael@0 1175 /**
michael@0 1176 * Get recent files.
michael@0 1177 *
michael@0 1178 * @return Array
michael@0 1179 * File paths.
michael@0 1180 */
michael@0 1181 getRecentFiles: function SP_getRecentFiles()
michael@0 1182 {
michael@0 1183 let branch = Services.prefs.getBranch("devtools.scratchpad.");
michael@0 1184 let filePaths = [];
michael@0 1185
michael@0 1186 // WARNING: Do not use getCharPref here, it doesn't play nicely with
michael@0 1187 // Unicode strings.
michael@0 1188
michael@0 1189 if (branch.prefHasUserValue("recentFilePaths")) {
michael@0 1190 let data = branch.getComplexValue("recentFilePaths",
michael@0 1191 Ci.nsISupportsString).data;
michael@0 1192 filePaths = JSON.parse(data);
michael@0 1193 }
michael@0 1194
michael@0 1195 return filePaths;
michael@0 1196 },
michael@0 1197
michael@0 1198 /**
michael@0 1199 * Save a recent file in a JSON parsable string.
michael@0 1200 *
michael@0 1201 * @param nsILocalFile aFile
michael@0 1202 * The nsILocalFile we want to save as a recent file.
michael@0 1203 */
michael@0 1204 setRecentFile: function SP_setRecentFile(aFile)
michael@0 1205 {
michael@0 1206 let maxRecent = Services.prefs.getIntPref(PREF_RECENT_FILES_MAX);
michael@0 1207 if (maxRecent < 1) {
michael@0 1208 return;
michael@0 1209 }
michael@0 1210
michael@0 1211 let filePaths = this.getRecentFiles();
michael@0 1212 let filesCount = filePaths.length;
michael@0 1213 let pathIndex = filePaths.indexOf(aFile.path);
michael@0 1214
michael@0 1215 // We are already storing this file in the list of recent files.
michael@0 1216 if (pathIndex > -1) {
michael@0 1217 // If it's already the most recent file, we don't have to do anything.
michael@0 1218 if (pathIndex === (filesCount - 1)) {
michael@0 1219 // Updating the menu to clear the disabled state from the wrong menuitem
michael@0 1220 // in rare cases when two or more Scratchpad windows are open and the
michael@0 1221 // same file has been opened in two or more windows.
michael@0 1222 this.populateRecentFilesMenu();
michael@0 1223 return;
michael@0 1224 }
michael@0 1225
michael@0 1226 // It is not the most recent file. Remove it from the list, we add it as
michael@0 1227 // the most recent farther down.
michael@0 1228 filePaths.splice(pathIndex, 1);
michael@0 1229 }
michael@0 1230 // If we are not storing the file and the 'recent files'-list is full,
michael@0 1231 // remove the oldest file from the list.
michael@0 1232 else if (filesCount === maxRecent) {
michael@0 1233 filePaths.shift();
michael@0 1234 }
michael@0 1235
michael@0 1236 filePaths.push(aFile.path);
michael@0 1237
michael@0 1238 // WARNING: Do not use setCharPref here, it doesn't play nicely with
michael@0 1239 // Unicode strings.
michael@0 1240
michael@0 1241 let str = Cc["@mozilla.org/supports-string;1"]
michael@0 1242 .createInstance(Ci.nsISupportsString);
michael@0 1243 str.data = JSON.stringify(filePaths);
michael@0 1244
michael@0 1245 let branch = Services.prefs.getBranch("devtools.scratchpad.");
michael@0 1246 branch.setComplexValue("recentFilePaths",
michael@0 1247 Ci.nsISupportsString, str);
michael@0 1248 },
michael@0 1249
michael@0 1250 /**
michael@0 1251 * Populates the 'Open Recent'-menu.
michael@0 1252 */
michael@0 1253 populateRecentFilesMenu: function SP_populateRecentFilesMenu()
michael@0 1254 {
michael@0 1255 let maxRecent = Services.prefs.getIntPref(PREF_RECENT_FILES_MAX);
michael@0 1256 let recentFilesMenu = document.getElementById("sp-open_recent-menu");
michael@0 1257
michael@0 1258 if (maxRecent < 1) {
michael@0 1259 recentFilesMenu.setAttribute("hidden", true);
michael@0 1260 return;
michael@0 1261 }
michael@0 1262
michael@0 1263 let recentFilesPopup = recentFilesMenu.firstChild;
michael@0 1264 let filePaths = this.getRecentFiles();
michael@0 1265 let filename = this.getState().filename;
michael@0 1266
michael@0 1267 recentFilesMenu.setAttribute("disabled", true);
michael@0 1268 while (recentFilesPopup.hasChildNodes()) {
michael@0 1269 recentFilesPopup.removeChild(recentFilesPopup.firstChild);
michael@0 1270 }
michael@0 1271
michael@0 1272 if (filePaths.length > 0) {
michael@0 1273 recentFilesMenu.removeAttribute("disabled");
michael@0 1274
michael@0 1275 // Print out menuitems with the most recent file first.
michael@0 1276 for (let i = filePaths.length - 1; i >= 0; --i) {
michael@0 1277 let menuitem = document.createElement("menuitem");
michael@0 1278 menuitem.setAttribute("type", "radio");
michael@0 1279 menuitem.setAttribute("label", filePaths[i]);
michael@0 1280
michael@0 1281 if (filePaths[i] === filename) {
michael@0 1282 menuitem.setAttribute("checked", true);
michael@0 1283 menuitem.setAttribute("disabled", true);
michael@0 1284 }
michael@0 1285
michael@0 1286 menuitem.addEventListener("command", Scratchpad.openFile.bind(Scratchpad, i));
michael@0 1287 recentFilesPopup.appendChild(menuitem);
michael@0 1288 }
michael@0 1289
michael@0 1290 recentFilesPopup.appendChild(document.createElement("menuseparator"));
michael@0 1291 let clearItems = document.createElement("menuitem");
michael@0 1292 clearItems.setAttribute("id", "sp-menu-clear_recent");
michael@0 1293 clearItems.setAttribute("label",
michael@0 1294 this.strings.
michael@0 1295 GetStringFromName("clearRecentMenuItems.label"));
michael@0 1296 clearItems.setAttribute("command", "sp-cmd-clearRecentFiles");
michael@0 1297 recentFilesPopup.appendChild(clearItems);
michael@0 1298 }
michael@0 1299 },
michael@0 1300
michael@0 1301 /**
michael@0 1302 * Clear a range of files from the list.
michael@0 1303 *
michael@0 1304 * @param integer aIndex
michael@0 1305 * Index of file in menu to remove.
michael@0 1306 * @param integer aLength
michael@0 1307 * Number of files from the index 'aIndex' to remove.
michael@0 1308 */
michael@0 1309 clearFiles: function SP_clearFile(aIndex, aLength)
michael@0 1310 {
michael@0 1311 let filePaths = this.getRecentFiles();
michael@0 1312 filePaths.splice(aIndex, aLength);
michael@0 1313
michael@0 1314 // WARNING: Do not use setCharPref here, it doesn't play nicely with
michael@0 1315 // Unicode strings.
michael@0 1316
michael@0 1317 let str = Cc["@mozilla.org/supports-string;1"]
michael@0 1318 .createInstance(Ci.nsISupportsString);
michael@0 1319 str.data = JSON.stringify(filePaths);
michael@0 1320
michael@0 1321 let branch = Services.prefs.getBranch("devtools.scratchpad.");
michael@0 1322 branch.setComplexValue("recentFilePaths",
michael@0 1323 Ci.nsISupportsString, str);
michael@0 1324 },
michael@0 1325
michael@0 1326 /**
michael@0 1327 * Clear all recent files.
michael@0 1328 */
michael@0 1329 clearRecentFiles: function SP_clearRecentFiles()
michael@0 1330 {
michael@0 1331 Services.prefs.clearUserPref("devtools.scratchpad.recentFilePaths");
michael@0 1332 },
michael@0 1333
michael@0 1334 /**
michael@0 1335 * Handle changes to the 'PREF_RECENT_FILES_MAX'-preference.
michael@0 1336 */
michael@0 1337 handleRecentFileMaxChange: function SP_handleRecentFileMaxChange()
michael@0 1338 {
michael@0 1339 let maxRecent = Services.prefs.getIntPref(PREF_RECENT_FILES_MAX);
michael@0 1340 let menu = document.getElementById("sp-open_recent-menu");
michael@0 1341
michael@0 1342 // Hide the menu if the 'PREF_RECENT_FILES_MAX'-pref is set to zero or less.
michael@0 1343 if (maxRecent < 1) {
michael@0 1344 menu.setAttribute("hidden", true);
michael@0 1345 } else {
michael@0 1346 if (menu.hasAttribute("hidden")) {
michael@0 1347 if (!menu.firstChild.hasChildNodes()) {
michael@0 1348 this.populateRecentFilesMenu();
michael@0 1349 }
michael@0 1350
michael@0 1351 menu.removeAttribute("hidden");
michael@0 1352 }
michael@0 1353
michael@0 1354 let filePaths = this.getRecentFiles();
michael@0 1355 if (maxRecent < filePaths.length) {
michael@0 1356 let diff = filePaths.length - maxRecent;
michael@0 1357 this.clearFiles(0, diff);
michael@0 1358 }
michael@0 1359 }
michael@0 1360 },
michael@0 1361 /**
michael@0 1362 * Save the textbox content to the currently open file.
michael@0 1363 *
michael@0 1364 * @param function aCallback
michael@0 1365 * Optional function you want to call when file is saved
michael@0 1366 */
michael@0 1367 saveFile: function SP_saveFile(aCallback)
michael@0 1368 {
michael@0 1369 if (!this.filename) {
michael@0 1370 return this.saveFileAs(aCallback);
michael@0 1371 }
michael@0 1372
michael@0 1373 let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
michael@0 1374 file.initWithPath(this.filename);
michael@0 1375
michael@0 1376 this.exportToFile(file, true, false, aStatus => {
michael@0 1377 if (Components.isSuccessCode(aStatus)) {
michael@0 1378 this.dirty = false;
michael@0 1379 document.getElementById("sp-cmd-revert").setAttribute("disabled", true);
michael@0 1380 this.setRecentFile(file);
michael@0 1381 }
michael@0 1382 if (aCallback) {
michael@0 1383 aCallback(aStatus);
michael@0 1384 }
michael@0 1385 });
michael@0 1386 },
michael@0 1387
michael@0 1388 /**
michael@0 1389 * Save the textbox content to a new file.
michael@0 1390 *
michael@0 1391 * @param function aCallback
michael@0 1392 * Optional function you want to call when file is saved
michael@0 1393 */
michael@0 1394 saveFileAs: function SP_saveFileAs(aCallback)
michael@0 1395 {
michael@0 1396 let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
michael@0 1397 let fpCallback = aResult => {
michael@0 1398 if (aResult != Ci.nsIFilePicker.returnCancel) {
michael@0 1399 this.setFilename(fp.file.path);
michael@0 1400 this.exportToFile(fp.file, true, false, aStatus => {
michael@0 1401 if (Components.isSuccessCode(aStatus)) {
michael@0 1402 this.dirty = false;
michael@0 1403 this.setRecentFile(fp.file);
michael@0 1404 }
michael@0 1405 if (aCallback) {
michael@0 1406 aCallback(aStatus);
michael@0 1407 }
michael@0 1408 });
michael@0 1409 }
michael@0 1410 };
michael@0 1411
michael@0 1412 fp.init(window, this.strings.GetStringFromName("saveFileAs"),
michael@0 1413 Ci.nsIFilePicker.modeSave);
michael@0 1414 fp.defaultString = "scratchpad.js";
michael@0 1415 fp.appendFilter("JavaScript Files", "*.js; *.jsm; *.json");
michael@0 1416 fp.appendFilter("All Files", "*.*");
michael@0 1417 fp.open(fpCallback);
michael@0 1418 },
michael@0 1419
michael@0 1420 /**
michael@0 1421 * Restore content from saved version of current file.
michael@0 1422 *
michael@0 1423 * @param function aCallback
michael@0 1424 * Optional function you want to call when file is saved
michael@0 1425 */
michael@0 1426 revertFile: function SP_revertFile(aCallback)
michael@0 1427 {
michael@0 1428 let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
michael@0 1429 file.initWithPath(this.filename);
michael@0 1430
michael@0 1431 if (!file.exists()) {
michael@0 1432 return;
michael@0 1433 }
michael@0 1434
michael@0 1435 this.importFromFile(file, false, (aStatus, aContent) => {
michael@0 1436 if (aCallback) {
michael@0 1437 aCallback(aStatus);
michael@0 1438 }
michael@0 1439 });
michael@0 1440 },
michael@0 1441
michael@0 1442 /**
michael@0 1443 * Prompt to revert scratchpad if it has unsaved changes.
michael@0 1444 *
michael@0 1445 * @param function aCallback
michael@0 1446 * Optional function you want to call when file is saved. The callback
michael@0 1447 * receives three arguments:
michael@0 1448 * - aRevert (boolean) - tells if the file has been reverted.
michael@0 1449 * - status (number) - the file revert status result (if the file was
michael@0 1450 * saved).
michael@0 1451 */
michael@0 1452 promptRevert: function SP_promptRervert(aCallback)
michael@0 1453 {
michael@0 1454 if (this.filename) {
michael@0 1455 let ps = Services.prompt;
michael@0 1456 let flags = ps.BUTTON_POS_0 * ps.BUTTON_TITLE_REVERT +
michael@0 1457 ps.BUTTON_POS_1 * ps.BUTTON_TITLE_CANCEL;
michael@0 1458
michael@0 1459 let button = ps.confirmEx(window,
michael@0 1460 this.strings.GetStringFromName("confirmRevert.title"),
michael@0 1461 this.strings.GetStringFromName("confirmRevert"),
michael@0 1462 flags, null, null, null, null, {});
michael@0 1463 if (button == BUTTON_POSITION_CANCEL) {
michael@0 1464 if (aCallback) {
michael@0 1465 aCallback(false);
michael@0 1466 }
michael@0 1467
michael@0 1468 return;
michael@0 1469 }
michael@0 1470 if (button == BUTTON_POSITION_REVERT) {
michael@0 1471 this.revertFile(aStatus => {
michael@0 1472 if (aCallback) {
michael@0 1473 aCallback(true, aStatus);
michael@0 1474 }
michael@0 1475 });
michael@0 1476
michael@0 1477 return;
michael@0 1478 }
michael@0 1479 }
michael@0 1480 if (aCallback) {
michael@0 1481 aCallback(false);
michael@0 1482 }
michael@0 1483 },
michael@0 1484
michael@0 1485 /**
michael@0 1486 * Open the Error Console.
michael@0 1487 */
michael@0 1488 openErrorConsole: function SP_openErrorConsole()
michael@0 1489 {
michael@0 1490 this.browserWindow.HUDService.toggleBrowserConsole();
michael@0 1491 },
michael@0 1492
michael@0 1493 /**
michael@0 1494 * Open the Web Console.
michael@0 1495 */
michael@0 1496 openWebConsole: function SP_openWebConsole()
michael@0 1497 {
michael@0 1498 let target = TargetFactory.forTab(this.gBrowser.selectedTab);
michael@0 1499 gDevTools.showToolbox(target, "webconsole");
michael@0 1500 this.browserWindow.focus();
michael@0 1501 },
michael@0 1502
michael@0 1503 /**
michael@0 1504 * Set the current execution context to be the active tab content window.
michael@0 1505 */
michael@0 1506 setContentContext: function SP_setContentContext()
michael@0 1507 {
michael@0 1508 if (this.executionContext == SCRATCHPAD_CONTEXT_CONTENT) {
michael@0 1509 return;
michael@0 1510 }
michael@0 1511
michael@0 1512 let content = document.getElementById("sp-menu-content");
michael@0 1513 document.getElementById("sp-menu-browser").removeAttribute("checked");
michael@0 1514 document.getElementById("sp-cmd-reloadAndRun").removeAttribute("disabled");
michael@0 1515 content.setAttribute("checked", true);
michael@0 1516 this.executionContext = SCRATCHPAD_CONTEXT_CONTENT;
michael@0 1517 this.notificationBox.removeAllNotifications(false);
michael@0 1518 },
michael@0 1519
michael@0 1520 /**
michael@0 1521 * Set the current execution context to be the most recent chrome window.
michael@0 1522 */
michael@0 1523 setBrowserContext: function SP_setBrowserContext()
michael@0 1524 {
michael@0 1525 if (this.executionContext == SCRATCHPAD_CONTEXT_BROWSER) {
michael@0 1526 return;
michael@0 1527 }
michael@0 1528
michael@0 1529 let browser = document.getElementById("sp-menu-browser");
michael@0 1530 let reloadAndRun = document.getElementById("sp-cmd-reloadAndRun");
michael@0 1531
michael@0 1532 document.getElementById("sp-menu-content").removeAttribute("checked");
michael@0 1533 reloadAndRun.setAttribute("disabled", true);
michael@0 1534 browser.setAttribute("checked", true);
michael@0 1535
michael@0 1536 this.executionContext = SCRATCHPAD_CONTEXT_BROWSER;
michael@0 1537 this.notificationBox.appendNotification(
michael@0 1538 this.strings.GetStringFromName("browserContext.notification"),
michael@0 1539 SCRATCHPAD_CONTEXT_BROWSER,
michael@0 1540 null,
michael@0 1541 this.notificationBox.PRIORITY_WARNING_HIGH,
michael@0 1542 null);
michael@0 1543 },
michael@0 1544
michael@0 1545 /**
michael@0 1546 * Gets the ID of the inner window of the given DOM window object.
michael@0 1547 *
michael@0 1548 * @param nsIDOMWindow aWindow
michael@0 1549 * @return integer
michael@0 1550 * the inner window ID
michael@0 1551 */
michael@0 1552 getInnerWindowId: function SP_getInnerWindowId(aWindow)
michael@0 1553 {
michael@0 1554 return aWindow.QueryInterface(Ci.nsIInterfaceRequestor).
michael@0 1555 getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID;
michael@0 1556 },
michael@0 1557
michael@0 1558 /**
michael@0 1559 * The Scratchpad window load event handler. This method
michael@0 1560 * initializes the Scratchpad window and source editor.
michael@0 1561 *
michael@0 1562 * @param nsIDOMEvent aEvent
michael@0 1563 */
michael@0 1564 onLoad: function SP_onLoad(aEvent)
michael@0 1565 {
michael@0 1566 if (aEvent.target != document) {
michael@0 1567 return;
michael@0 1568 }
michael@0 1569
michael@0 1570 let chrome = Services.prefs.getBoolPref(DEVTOOLS_CHROME_ENABLED);
michael@0 1571 if (chrome) {
michael@0 1572 let environmentMenu = document.getElementById("sp-environment-menu");
michael@0 1573 let errorConsoleCommand = document.getElementById("sp-cmd-errorConsole");
michael@0 1574 let chromeContextCommand = document.getElementById("sp-cmd-browserContext");
michael@0 1575 environmentMenu.removeAttribute("hidden");
michael@0 1576 chromeContextCommand.removeAttribute("disabled");
michael@0 1577 errorConsoleCommand.removeAttribute("disabled");
michael@0 1578 }
michael@0 1579
michael@0 1580 let initialText = this.strings.formatStringFromName(
michael@0 1581 "scratchpadIntro1",
michael@0 1582 [ShortcutUtils.prettifyShortcut(document.getElementById("sp-key-run"), true),
michael@0 1583 ShortcutUtils.prettifyShortcut(document.getElementById("sp-key-inspect"), true),
michael@0 1584 ShortcutUtils.prettifyShortcut(document.getElementById("sp-key-display"), true)],
michael@0 1585 3);
michael@0 1586
michael@0 1587 let args = window.arguments;
michael@0 1588 let state = null;
michael@0 1589
michael@0 1590 if (args && args[0] instanceof Ci.nsIDialogParamBlock) {
michael@0 1591 args = args[0];
michael@0 1592 this._instanceId = args.GetString(0);
michael@0 1593
michael@0 1594 state = args.GetString(1) || null;
michael@0 1595 if (state) {
michael@0 1596 state = JSON.parse(state);
michael@0 1597 this.setState(state);
michael@0 1598 initialText = state.text;
michael@0 1599 }
michael@0 1600 } else {
michael@0 1601 this._instanceId = ScratchpadManager.createUid();
michael@0 1602 }
michael@0 1603
michael@0 1604 let config = {
michael@0 1605 mode: Editor.modes.js,
michael@0 1606 value: initialText,
michael@0 1607 lineNumbers: true,
michael@0 1608 showTrailingSpace: Services.prefs.getBoolPref(SHOW_TRAILING_SPACE),
michael@0 1609 enableCodeFolding: Services.prefs.getBoolPref(ENABLE_CODE_FOLDING),
michael@0 1610 contextMenu: "scratchpad-text-popup"
michael@0 1611 };
michael@0 1612
michael@0 1613 this.editor = new Editor(config);
michael@0 1614 this.editor.appendTo(document.querySelector("#scratchpad-editor")).then(() => {
michael@0 1615 var lines = initialText.split("\n");
michael@0 1616
michael@0 1617 this.editor.on("change", this._onChanged);
michael@0 1618 this.editor.on("save", () => this.saveFile());
michael@0 1619 this.editor.focus();
michael@0 1620 this.editor.setCursor({ line: lines.length, ch: lines.pop().length });
michael@0 1621
michael@0 1622 if (state)
michael@0 1623 this.dirty = !state.saved;
michael@0 1624
michael@0 1625 this.initialized = true;
michael@0 1626 this._triggerObservers("Ready");
michael@0 1627 this.populateRecentFilesMenu();
michael@0 1628 PreferenceObserver.init();
michael@0 1629 CloseObserver.init();
michael@0 1630 }).then(null, (err) => console.log(err.message));
michael@0 1631 this._setupCommandListeners();
michael@0 1632 this._setupPopupShowingListeners();
michael@0 1633 },
michael@0 1634
michael@0 1635 /**
michael@0 1636 * The Source Editor "change" event handler. This function updates the
michael@0 1637 * Scratchpad window title to show an asterisk when there are unsaved changes.
michael@0 1638 *
michael@0 1639 * @private
michael@0 1640 */
michael@0 1641 _onChanged: function SP__onChanged()
michael@0 1642 {
michael@0 1643 Scratchpad._updateTitle();
michael@0 1644
michael@0 1645 if (Scratchpad.filename) {
michael@0 1646 if (Scratchpad.dirty)
michael@0 1647 document.getElementById("sp-cmd-revert").removeAttribute("disabled");
michael@0 1648 else
michael@0 1649 document.getElementById("sp-cmd-revert").setAttribute("disabled", true);
michael@0 1650 }
michael@0 1651 },
michael@0 1652
michael@0 1653 /**
michael@0 1654 * Undo the last action of the user.
michael@0 1655 */
michael@0 1656 undo: function SP_undo()
michael@0 1657 {
michael@0 1658 this.editor.undo();
michael@0 1659 },
michael@0 1660
michael@0 1661 /**
michael@0 1662 * Redo the previously undone action.
michael@0 1663 */
michael@0 1664 redo: function SP_redo()
michael@0 1665 {
michael@0 1666 this.editor.redo();
michael@0 1667 },
michael@0 1668
michael@0 1669 /**
michael@0 1670 * The Scratchpad window unload event handler. This method unloads/destroys
michael@0 1671 * the source editor.
michael@0 1672 *
michael@0 1673 * @param nsIDOMEvent aEvent
michael@0 1674 */
michael@0 1675 onUnload: function SP_onUnload(aEvent)
michael@0 1676 {
michael@0 1677 if (aEvent.target != document) {
michael@0 1678 return;
michael@0 1679 }
michael@0 1680
michael@0 1681 // This event is created only after user uses 'reload and run' feature.
michael@0 1682 if (this._reloadAndRunEvent && this.gBrowser) {
michael@0 1683 this.gBrowser.selectedBrowser.removeEventListener("load",
michael@0 1684 this._reloadAndRunEvent, true);
michael@0 1685 }
michael@0 1686
michael@0 1687 PreferenceObserver.uninit();
michael@0 1688 CloseObserver.uninit();
michael@0 1689
michael@0 1690 this.editor.off("change", this._onChanged);
michael@0 1691 this.editor.destroy();
michael@0 1692 this.editor = null;
michael@0 1693
michael@0 1694 if (this._sidebar) {
michael@0 1695 this._sidebar.destroy();
michael@0 1696 this._sidebar = null;
michael@0 1697 }
michael@0 1698
michael@0 1699 if (this._prettyPrintWorker) {
michael@0 1700 this._prettyPrintWorker.terminate();
michael@0 1701 this._prettyPrintWorker = null;
michael@0 1702 }
michael@0 1703
michael@0 1704 scratchpadTargets = null;
michael@0 1705 this.webConsoleClient = null;
michael@0 1706 this.debuggerClient = null;
michael@0 1707 this.initialized = false;
michael@0 1708 },
michael@0 1709
michael@0 1710 /**
michael@0 1711 * Prompt to save scratchpad if it has unsaved changes.
michael@0 1712 *
michael@0 1713 * @param function aCallback
michael@0 1714 * Optional function you want to call when file is saved. The callback
michael@0 1715 * receives three arguments:
michael@0 1716 * - toClose (boolean) - tells if the window should be closed.
michael@0 1717 * - saved (boolen) - tells if the file has been saved.
michael@0 1718 * - status (number) - the file save status result (if the file was
michael@0 1719 * saved).
michael@0 1720 * @return boolean
michael@0 1721 * Whether the window should be closed
michael@0 1722 */
michael@0 1723 promptSave: function SP_promptSave(aCallback)
michael@0 1724 {
michael@0 1725 if (this.dirty) {
michael@0 1726 let ps = Services.prompt;
michael@0 1727 let flags = ps.BUTTON_POS_0 * ps.BUTTON_TITLE_SAVE +
michael@0 1728 ps.BUTTON_POS_1 * ps.BUTTON_TITLE_CANCEL +
michael@0 1729 ps.BUTTON_POS_2 * ps.BUTTON_TITLE_DONT_SAVE;
michael@0 1730
michael@0 1731 let button = ps.confirmEx(window,
michael@0 1732 this.strings.GetStringFromName("confirmClose.title"),
michael@0 1733 this.strings.GetStringFromName("confirmClose"),
michael@0 1734 flags, null, null, null, null, {});
michael@0 1735
michael@0 1736 if (button == BUTTON_POSITION_CANCEL) {
michael@0 1737 if (aCallback) {
michael@0 1738 aCallback(false, false);
michael@0 1739 }
michael@0 1740 return false;
michael@0 1741 }
michael@0 1742
michael@0 1743 if (button == BUTTON_POSITION_SAVE) {
michael@0 1744 this.saveFile(aStatus => {
michael@0 1745 if (aCallback) {
michael@0 1746 aCallback(true, true, aStatus);
michael@0 1747 }
michael@0 1748 });
michael@0 1749 return true;
michael@0 1750 }
michael@0 1751 }
michael@0 1752
michael@0 1753 if (aCallback) {
michael@0 1754 aCallback(true, false);
michael@0 1755 }
michael@0 1756 return true;
michael@0 1757 },
michael@0 1758
michael@0 1759 /**
michael@0 1760 * Handler for window close event. Prompts to save scratchpad if
michael@0 1761 * there are unsaved changes.
michael@0 1762 *
michael@0 1763 * @param nsIDOMEvent aEvent
michael@0 1764 * @param function aCallback
michael@0 1765 * Optional function you want to call when file is saved/closed.
michael@0 1766 * Used mainly for tests.
michael@0 1767 */
michael@0 1768 onClose: function SP_onClose(aEvent, aCallback)
michael@0 1769 {
michael@0 1770 aEvent.preventDefault();
michael@0 1771 this.close(aCallback);
michael@0 1772 },
michael@0 1773
michael@0 1774 /**
michael@0 1775 * Close the scratchpad window. Prompts before closing if the scratchpad
michael@0 1776 * has unsaved changes.
michael@0 1777 *
michael@0 1778 * @param function aCallback
michael@0 1779 * Optional function you want to call when file is saved
michael@0 1780 */
michael@0 1781 close: function SP_close(aCallback)
michael@0 1782 {
michael@0 1783 let shouldClose;
michael@0 1784
michael@0 1785 this.promptSave((aShouldClose, aSaved, aStatus) => {
michael@0 1786 shouldClose = aShouldClose;
michael@0 1787 if (aSaved && !Components.isSuccessCode(aStatus)) {
michael@0 1788 shouldClose = false;
michael@0 1789 }
michael@0 1790
michael@0 1791 if (shouldClose) {
michael@0 1792 telemetry.toolClosed("scratchpad");
michael@0 1793 window.close();
michael@0 1794 }
michael@0 1795
michael@0 1796 if (aCallback) {
michael@0 1797 aCallback(shouldClose);
michael@0 1798 }
michael@0 1799 });
michael@0 1800
michael@0 1801 return shouldClose;
michael@0 1802 },
michael@0 1803
michael@0 1804 /**
michael@0 1805 * Toggle a editor's boolean option.
michael@0 1806 */
michael@0 1807 toggleEditorOption: function SP_toggleEditorOption(optionName)
michael@0 1808 {
michael@0 1809 let newOptionValue = !this.editor.getOption(optionName);
michael@0 1810 this.editor.setOption(optionName, newOptionValue);
michael@0 1811 },
michael@0 1812
michael@0 1813 /**
michael@0 1814 * Increase the editor's font size by 1 px.
michael@0 1815 */
michael@0 1816 increaseFontSize: function SP_increaseFontSize()
michael@0 1817 {
michael@0 1818 let size = this.editor.getFontSize();
michael@0 1819
michael@0 1820 if (size < MAXIMUM_FONT_SIZE) {
michael@0 1821 this.editor.setFontSize(size + 1);
michael@0 1822 }
michael@0 1823 },
michael@0 1824
michael@0 1825 /**
michael@0 1826 * Decrease the editor's font size by 1 px.
michael@0 1827 */
michael@0 1828 decreaseFontSize: function SP_decreaseFontSize()
michael@0 1829 {
michael@0 1830 let size = this.editor.getFontSize();
michael@0 1831
michael@0 1832 if (size > MINIMUM_FONT_SIZE) {
michael@0 1833 this.editor.setFontSize(size - 1);
michael@0 1834 }
michael@0 1835 },
michael@0 1836
michael@0 1837 /**
michael@0 1838 * Restore the editor's original font size.
michael@0 1839 */
michael@0 1840 normalFontSize: function SP_normalFontSize()
michael@0 1841 {
michael@0 1842 this.editor.setFontSize(NORMAL_FONT_SIZE);
michael@0 1843 },
michael@0 1844
michael@0 1845 _observers: [],
michael@0 1846
michael@0 1847 /**
michael@0 1848 * Add an observer for Scratchpad events.
michael@0 1849 *
michael@0 1850 * The observer implements IScratchpadObserver := {
michael@0 1851 * onReady: Called when the Scratchpad and its Editor are ready.
michael@0 1852 * Arguments: (Scratchpad aScratchpad)
michael@0 1853 * }
michael@0 1854 *
michael@0 1855 * All observer handlers are optional.
michael@0 1856 *
michael@0 1857 * @param IScratchpadObserver aObserver
michael@0 1858 * @see removeObserver
michael@0 1859 */
michael@0 1860 addObserver: function SP_addObserver(aObserver)
michael@0 1861 {
michael@0 1862 this._observers.push(aObserver);
michael@0 1863 },
michael@0 1864
michael@0 1865 /**
michael@0 1866 * Remove an observer for Scratchpad events.
michael@0 1867 *
michael@0 1868 * @param IScratchpadObserver aObserver
michael@0 1869 * @see addObserver
michael@0 1870 */
michael@0 1871 removeObserver: function SP_removeObserver(aObserver)
michael@0 1872 {
michael@0 1873 let index = this._observers.indexOf(aObserver);
michael@0 1874 if (index != -1) {
michael@0 1875 this._observers.splice(index, 1);
michael@0 1876 }
michael@0 1877 },
michael@0 1878
michael@0 1879 /**
michael@0 1880 * Trigger named handlers in Scratchpad observers.
michael@0 1881 *
michael@0 1882 * @param string aName
michael@0 1883 * Name of the handler to trigger.
michael@0 1884 * @param Array aArgs
michael@0 1885 * Optional array of arguments to pass to the observer(s).
michael@0 1886 * @see addObserver
michael@0 1887 */
michael@0 1888 _triggerObservers: function SP_triggerObservers(aName, aArgs)
michael@0 1889 {
michael@0 1890 // insert this Scratchpad instance as the first argument
michael@0 1891 if (!aArgs) {
michael@0 1892 aArgs = [this];
michael@0 1893 } else {
michael@0 1894 aArgs.unshift(this);
michael@0 1895 }
michael@0 1896
michael@0 1897 // trigger all observers that implement this named handler
michael@0 1898 for (let i = 0; i < this._observers.length; ++i) {
michael@0 1899 let observer = this._observers[i];
michael@0 1900 let handler = observer["on" + aName];
michael@0 1901 if (handler) {
michael@0 1902 handler.apply(observer, aArgs);
michael@0 1903 }
michael@0 1904 }
michael@0 1905 },
michael@0 1906
michael@0 1907 /**
michael@0 1908 * Opens the MDN documentation page for Scratchpad.
michael@0 1909 */
michael@0 1910 openDocumentationPage: function SP_openDocumentationPage()
michael@0 1911 {
michael@0 1912 let url = this.strings.GetStringFromName("help.openDocumentationPage");
michael@0 1913 let newTab = this.gBrowser.addTab(url);
michael@0 1914 this.browserWindow.focus();
michael@0 1915 this.gBrowser.selectedTab = newTab;
michael@0 1916 },
michael@0 1917 };
michael@0 1918
michael@0 1919
michael@0 1920 /**
michael@0 1921 * Represents the DebuggerClient connection to a specific tab as used by the
michael@0 1922 * Scratchpad.
michael@0 1923 *
michael@0 1924 * @param object aTab
michael@0 1925 * The tab to connect to.
michael@0 1926 */
michael@0 1927 function ScratchpadTab(aTab)
michael@0 1928 {
michael@0 1929 this._tab = aTab;
michael@0 1930 }
michael@0 1931
michael@0 1932 let scratchpadTargets = new WeakMap();
michael@0 1933
michael@0 1934 /**
michael@0 1935 * Returns the object containing the DebuggerClient and WebConsoleClient for a
michael@0 1936 * given tab or window.
michael@0 1937 *
michael@0 1938 * @param object aSubject
michael@0 1939 * The tab or window to obtain the connection for.
michael@0 1940 * @return Promise
michael@0 1941 * The promise for the connection information.
michael@0 1942 */
michael@0 1943 ScratchpadTab.consoleFor = function consoleFor(aSubject)
michael@0 1944 {
michael@0 1945 if (!scratchpadTargets.has(aSubject)) {
michael@0 1946 scratchpadTargets.set(aSubject, new this(aSubject));
michael@0 1947 }
michael@0 1948 return scratchpadTargets.get(aSubject).connect();
michael@0 1949 };
michael@0 1950
michael@0 1951
michael@0 1952 ScratchpadTab.prototype = {
michael@0 1953 /**
michael@0 1954 * The promise for the connection.
michael@0 1955 */
michael@0 1956 _connector: null,
michael@0 1957
michael@0 1958 /**
michael@0 1959 * Initialize a debugger client and connect it to the debugger server.
michael@0 1960 *
michael@0 1961 * @return Promise
michael@0 1962 * The promise for the result of connecting to this tab or window.
michael@0 1963 */
michael@0 1964 connect: function ST_connect()
michael@0 1965 {
michael@0 1966 if (this._connector) {
michael@0 1967 return this._connector;
michael@0 1968 }
michael@0 1969
michael@0 1970 let deferred = promise.defer();
michael@0 1971 this._connector = deferred.promise;
michael@0 1972
michael@0 1973 let connectTimer = setTimeout(() => {
michael@0 1974 deferred.reject({
michael@0 1975 error: "timeout",
michael@0 1976 message: Scratchpad.strings.GetStringFromName("connectionTimeout"),
michael@0 1977 });
michael@0 1978 }, REMOTE_TIMEOUT);
michael@0 1979
michael@0 1980 deferred.promise.then(() => clearTimeout(connectTimer));
michael@0 1981
michael@0 1982 this._attach().then(aTarget => {
michael@0 1983 let consoleActor = aTarget.form.consoleActor;
michael@0 1984 let client = aTarget.client;
michael@0 1985 client.attachConsole(consoleActor, [], (aResponse, aWebConsoleClient) => {
michael@0 1986 if (aResponse.error) {
michael@0 1987 reportError("attachConsole", aResponse);
michael@0 1988 deferred.reject(aResponse);
michael@0 1989 }
michael@0 1990 else {
michael@0 1991 deferred.resolve({
michael@0 1992 webConsoleClient: aWebConsoleClient,
michael@0 1993 debuggerClient: client
michael@0 1994 });
michael@0 1995 }
michael@0 1996 });
michael@0 1997 });
michael@0 1998
michael@0 1999 return deferred.promise;
michael@0 2000 },
michael@0 2001
michael@0 2002 /**
michael@0 2003 * Attach to this tab.
michael@0 2004 *
michael@0 2005 * @return Promise
michael@0 2006 * The promise for the TabTarget for this tab.
michael@0 2007 */
michael@0 2008 _attach: function ST__attach()
michael@0 2009 {
michael@0 2010 let target = TargetFactory.forTab(this._tab);
michael@0 2011 return target.makeRemote().then(() => target);
michael@0 2012 },
michael@0 2013 };
michael@0 2014
michael@0 2015
michael@0 2016 /**
michael@0 2017 * Represents the DebuggerClient connection to a specific window as used by the
michael@0 2018 * Scratchpad.
michael@0 2019 */
michael@0 2020 function ScratchpadWindow() {}
michael@0 2021
michael@0 2022 ScratchpadWindow.consoleFor = ScratchpadTab.consoleFor;
michael@0 2023
michael@0 2024 ScratchpadWindow.prototype = Heritage.extend(ScratchpadTab.prototype, {
michael@0 2025 /**
michael@0 2026 * Attach to this window.
michael@0 2027 *
michael@0 2028 * @return Promise
michael@0 2029 * The promise for the target for this window.
michael@0 2030 */
michael@0 2031 _attach: function SW__attach()
michael@0 2032 {
michael@0 2033 let deferred = promise.defer();
michael@0 2034
michael@0 2035 if (!DebuggerServer.initialized) {
michael@0 2036 DebuggerServer.init();
michael@0 2037 DebuggerServer.addBrowserActors();
michael@0 2038 }
michael@0 2039
michael@0 2040 let client = new DebuggerClient(DebuggerServer.connectPipe());
michael@0 2041 client.connect(() => {
michael@0 2042 client.listTabs(aResponse => {
michael@0 2043 if (aResponse.error) {
michael@0 2044 reportError("listTabs", aResponse);
michael@0 2045 deferred.reject(aResponse);
michael@0 2046 }
michael@0 2047 else {
michael@0 2048 deferred.resolve({ form: aResponse, client: client });
michael@0 2049 }
michael@0 2050 });
michael@0 2051 });
michael@0 2052
michael@0 2053 return deferred.promise;
michael@0 2054 }
michael@0 2055 });
michael@0 2056
michael@0 2057
michael@0 2058 function ScratchpadTarget(aTarget)
michael@0 2059 {
michael@0 2060 this._target = aTarget;
michael@0 2061 }
michael@0 2062
michael@0 2063 ScratchpadTarget.consoleFor = ScratchpadTab.consoleFor;
michael@0 2064
michael@0 2065 ScratchpadTarget.prototype = Heritage.extend(ScratchpadTab.prototype, {
michael@0 2066 _attach: function ST__attach()
michael@0 2067 {
michael@0 2068 if (this._target.isRemote) {
michael@0 2069 return promise.resolve(this._target);
michael@0 2070 }
michael@0 2071 return this._target.makeRemote().then(() => this._target);
michael@0 2072 }
michael@0 2073 });
michael@0 2074
michael@0 2075
michael@0 2076 /**
michael@0 2077 * Encapsulates management of the sidebar containing the VariablesView for
michael@0 2078 * object inspection.
michael@0 2079 */
michael@0 2080 function ScratchpadSidebar(aScratchpad)
michael@0 2081 {
michael@0 2082 let ToolSidebar = require("devtools/framework/sidebar").ToolSidebar;
michael@0 2083 let tabbox = document.querySelector("#scratchpad-sidebar");
michael@0 2084 this._sidebar = new ToolSidebar(tabbox, this, "scratchpad");
michael@0 2085 this._scratchpad = aScratchpad;
michael@0 2086 }
michael@0 2087
michael@0 2088 ScratchpadSidebar.prototype = {
michael@0 2089 /*
michael@0 2090 * The ToolSidebar for this sidebar.
michael@0 2091 */
michael@0 2092 _sidebar: null,
michael@0 2093
michael@0 2094 /*
michael@0 2095 * The VariablesView for this sidebar.
michael@0 2096 */
michael@0 2097 variablesView: null,
michael@0 2098
michael@0 2099 /*
michael@0 2100 * Whether the sidebar is currently shown.
michael@0 2101 */
michael@0 2102 visible: false,
michael@0 2103
michael@0 2104 /**
michael@0 2105 * Open the sidebar, if not open already, and populate it with the properties
michael@0 2106 * of the given object.
michael@0 2107 *
michael@0 2108 * @param string aString
michael@0 2109 * The string that was evaluated.
michael@0 2110 * @param object aObject
michael@0 2111 * The object to inspect, which is the aEvalString evaluation result.
michael@0 2112 * @return Promise
michael@0 2113 * A promise that will resolve once the sidebar is open.
michael@0 2114 */
michael@0 2115 open: function SS_open(aEvalString, aObject)
michael@0 2116 {
michael@0 2117 this.show();
michael@0 2118
michael@0 2119 let deferred = promise.defer();
michael@0 2120
michael@0 2121 let onTabReady = () => {
michael@0 2122 if (this.variablesView) {
michael@0 2123 this.variablesView.controller.releaseActors();
michael@0 2124 }
michael@0 2125 else {
michael@0 2126 let window = this._sidebar.getWindowForTab("variablesview");
michael@0 2127 let container = window.document.querySelector("#variables");
michael@0 2128
michael@0 2129 this.variablesView = new VariablesView(container, {
michael@0 2130 searchEnabled: true,
michael@0 2131 searchPlaceholder: this._scratchpad.strings
michael@0 2132 .GetStringFromName("propertiesFilterPlaceholder")
michael@0 2133 });
michael@0 2134
michael@0 2135 VariablesViewController.attach(this.variablesView, {
michael@0 2136 getEnvironmentClient: aGrip => {
michael@0 2137 return new EnvironmentClient(this._scratchpad.debuggerClient, aGrip);
michael@0 2138 },
michael@0 2139 getObjectClient: aGrip => {
michael@0 2140 return new ObjectClient(this._scratchpad.debuggerClient, aGrip);
michael@0 2141 },
michael@0 2142 getLongStringClient: aActor => {
michael@0 2143 return this._scratchpad.webConsoleClient.longString(aActor);
michael@0 2144 },
michael@0 2145 releaseActor: aActor => {
michael@0 2146 this._scratchpad.debuggerClient.release(aActor);
michael@0 2147 }
michael@0 2148 });
michael@0 2149 }
michael@0 2150 this._update(aObject).then(() => deferred.resolve());
michael@0 2151 };
michael@0 2152
michael@0 2153 if (this._sidebar.getCurrentTabID() == "variablesview") {
michael@0 2154 onTabReady();
michael@0 2155 }
michael@0 2156 else {
michael@0 2157 this._sidebar.once("variablesview-ready", onTabReady);
michael@0 2158 this._sidebar.addTab("variablesview", VARIABLES_VIEW_URL, true);
michael@0 2159 }
michael@0 2160
michael@0 2161 return deferred.promise;
michael@0 2162 },
michael@0 2163
michael@0 2164 /**
michael@0 2165 * Show the sidebar.
michael@0 2166 */
michael@0 2167 show: function SS_show()
michael@0 2168 {
michael@0 2169 if (!this.visible) {
michael@0 2170 this.visible = true;
michael@0 2171 this._sidebar.show();
michael@0 2172 }
michael@0 2173 },
michael@0 2174
michael@0 2175 /**
michael@0 2176 * Hide the sidebar.
michael@0 2177 */
michael@0 2178 hide: function SS_hide()
michael@0 2179 {
michael@0 2180 if (this.visible) {
michael@0 2181 this.visible = false;
michael@0 2182 this._sidebar.hide();
michael@0 2183 }
michael@0 2184 },
michael@0 2185
michael@0 2186 /**
michael@0 2187 * Destroy the sidebar.
michael@0 2188 *
michael@0 2189 * @return Promise
michael@0 2190 * The promise that resolves when the sidebar is destroyed.
michael@0 2191 */
michael@0 2192 destroy: function SS_destroy()
michael@0 2193 {
michael@0 2194 if (this.variablesView) {
michael@0 2195 this.variablesView.controller.releaseActors();
michael@0 2196 this.variablesView = null;
michael@0 2197 }
michael@0 2198 return this._sidebar.destroy();
michael@0 2199 },
michael@0 2200
michael@0 2201 /**
michael@0 2202 * Update the object currently inspected by the sidebar.
michael@0 2203 *
michael@0 2204 * @param object aObject
michael@0 2205 * The object to inspect in the sidebar.
michael@0 2206 * @return Promise
michael@0 2207 * A promise that resolves when the update completes.
michael@0 2208 */
michael@0 2209 _update: function SS__update(aObject)
michael@0 2210 {
michael@0 2211 let options = { objectActor: aObject };
michael@0 2212 let view = this.variablesView;
michael@0 2213 view.empty();
michael@0 2214 return view.controller.setSingleVariable(options).expanded;
michael@0 2215 }
michael@0 2216 };
michael@0 2217
michael@0 2218
michael@0 2219 /**
michael@0 2220 * Report an error coming over the remote debugger protocol.
michael@0 2221 *
michael@0 2222 * @param string aAction
michael@0 2223 * The name of the action or method that failed.
michael@0 2224 * @param object aResponse
michael@0 2225 * The response packet that contains the error.
michael@0 2226 */
michael@0 2227 function reportError(aAction, aResponse)
michael@0 2228 {
michael@0 2229 Cu.reportError(aAction + " failed: " + aResponse.error + " " +
michael@0 2230 aResponse.message);
michael@0 2231 }
michael@0 2232
michael@0 2233
michael@0 2234 /**
michael@0 2235 * The PreferenceObserver listens for preference changes while Scratchpad is
michael@0 2236 * running.
michael@0 2237 */
michael@0 2238 var PreferenceObserver = {
michael@0 2239 _initialized: false,
michael@0 2240
michael@0 2241 init: function PO_init()
michael@0 2242 {
michael@0 2243 if (this._initialized) {
michael@0 2244 return;
michael@0 2245 }
michael@0 2246
michael@0 2247 this.branch = Services.prefs.getBranch("devtools.scratchpad.");
michael@0 2248 this.branch.addObserver("", this, false);
michael@0 2249 this._initialized = true;
michael@0 2250 },
michael@0 2251
michael@0 2252 observe: function PO_observe(aMessage, aTopic, aData)
michael@0 2253 {
michael@0 2254 if (aTopic != "nsPref:changed") {
michael@0 2255 return;
michael@0 2256 }
michael@0 2257
michael@0 2258 if (aData == "recentFilesMax") {
michael@0 2259 Scratchpad.handleRecentFileMaxChange();
michael@0 2260 }
michael@0 2261 else if (aData == "recentFilePaths") {
michael@0 2262 Scratchpad.populateRecentFilesMenu();
michael@0 2263 }
michael@0 2264 },
michael@0 2265
michael@0 2266 uninit: function PO_uninit () {
michael@0 2267 if (!this.branch) {
michael@0 2268 return;
michael@0 2269 }
michael@0 2270
michael@0 2271 this.branch.removeObserver("", this);
michael@0 2272 this.branch = null;
michael@0 2273 }
michael@0 2274 };
michael@0 2275
michael@0 2276
michael@0 2277 /**
michael@0 2278 * The CloseObserver listens for the last browser window closing and attempts to
michael@0 2279 * close the Scratchpad.
michael@0 2280 */
michael@0 2281 var CloseObserver = {
michael@0 2282 init: function CO_init()
michael@0 2283 {
michael@0 2284 Services.obs.addObserver(this, "browser-lastwindow-close-requested", false);
michael@0 2285 },
michael@0 2286
michael@0 2287 observe: function CO_observe(aSubject)
michael@0 2288 {
michael@0 2289 if (Scratchpad.close()) {
michael@0 2290 this.uninit();
michael@0 2291 }
michael@0 2292 else {
michael@0 2293 aSubject.QueryInterface(Ci.nsISupportsPRBool);
michael@0 2294 aSubject.data = true;
michael@0 2295 }
michael@0 2296 },
michael@0 2297
michael@0 2298 uninit: function CO_uninit()
michael@0 2299 {
michael@0 2300 Services.obs.removeObserver(this, "browser-lastwindow-close-requested",
michael@0 2301 false);
michael@0 2302 },
michael@0 2303 };
michael@0 2304
michael@0 2305 XPCOMUtils.defineLazyGetter(Scratchpad, "strings", function () {
michael@0 2306 return Services.strings.createBundle(SCRATCHPAD_L10N);
michael@0 2307 });
michael@0 2308
michael@0 2309 addEventListener("load", Scratchpad.onLoad.bind(Scratchpad), false);
michael@0 2310 addEventListener("unload", Scratchpad.onUnload.bind(Scratchpad), false);
michael@0 2311 addEventListener("close", Scratchpad.onClose.bind(Scratchpad), false);

mercurial