browser/devtools/scratchpad/scratchpad.js

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

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

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

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

mercurial