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