Wed, 31 Dec 2014 06:09:35 +0100
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); |