summaryrefslogtreecommitdiffstats
path: root/devtools/client/scratchpad/scratchpad.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/scratchpad/scratchpad.js')
-rw-r--r--devtools/client/scratchpad/scratchpad.js2480
1 files changed, 2480 insertions, 0 deletions
diff --git a/devtools/client/scratchpad/scratchpad.js b/devtools/client/scratchpad/scratchpad.js
new file mode 100644
index 000000000..306b635df
--- /dev/null
+++ b/devtools/client/scratchpad/scratchpad.js
@@ -0,0 +1,2480 @@
+/* vim:set ts=2 sw=2 sts=2 et:
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * Original version history can be found here:
+ * https://github.com/mozilla/workspace
+ *
+ * Copied and relicensed from the Public Domain.
+ * See bug 653934 for details.
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=653934
+ */
+
+"use strict";
+
+var Cu = Components.utils;
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+
+const SCRATCHPAD_CONTEXT_CONTENT = 1;
+const SCRATCHPAD_CONTEXT_BROWSER = 2;
+const BUTTON_POSITION_SAVE = 0;
+const BUTTON_POSITION_CANCEL = 1;
+const BUTTON_POSITION_DONT_SAVE = 2;
+const BUTTON_POSITION_REVERT = 0;
+const EVAL_FUNCTION_TIMEOUT = 1000; // milliseconds
+
+const MAXIMUM_FONT_SIZE = 96;
+const MINIMUM_FONT_SIZE = 6;
+const NORMAL_FONT_SIZE = 12;
+
+const SCRATCHPAD_L10N = "chrome://devtools/locale/scratchpad.properties";
+const DEVTOOLS_CHROME_ENABLED = "devtools.chrome.enabled";
+const PREF_RECENT_FILES_MAX = "devtools.scratchpad.recentFilesMax";
+const SHOW_LINE_NUMBERS = "devtools.scratchpad.lineNumbers";
+const WRAP_TEXT = "devtools.scratchpad.wrapText";
+const SHOW_TRAILING_SPACE = "devtools.scratchpad.showTrailingSpace";
+const EDITOR_FONT_SIZE = "devtools.scratchpad.editorFontSize";
+const ENABLE_AUTOCOMPLETION = "devtools.scratchpad.enableAutocompletion";
+const TAB_SIZE = "devtools.editor.tabsize";
+const FALLBACK_CHARSET_LIST = "intl.fallbackCharsetList.ISO-8859-1";
+
+const VARIABLES_VIEW_URL = "chrome://devtools/content/shared/widgets/VariablesView.xul";
+
+const {require, loader} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+
+const Editor = require("devtools/client/sourceeditor/editor");
+const TargetFactory = require("devtools/client/framework/target").TargetFactory;
+const EventEmitter = require("devtools/shared/event-emitter");
+const {DevToolsWorker} = require("devtools/shared/worker/worker");
+const DevToolsUtils = require("devtools/shared/DevToolsUtils");
+const flags = require("devtools/shared/flags");
+const promise = require("promise");
+const Services = require("Services");
+const {gDevTools} = require("devtools/client/framework/devtools");
+const {Heritage} = require("devtools/client/shared/widgets/view-helpers");
+
+const {XPCOMUtils} = require("resource://gre/modules/XPCOMUtils.jsm");
+const {NetUtil} = require("resource://gre/modules/NetUtil.jsm");
+const {ScratchpadManager} = require("resource://devtools/client/scratchpad/scratchpad-manager.jsm");
+const {addDebuggerToGlobal} = require("resource://gre/modules/jsdebugger.jsm");
+const {OS} = require("resource://gre/modules/osfile.jsm");
+const {Reflect} = require("resource://gre/modules/reflect.jsm");
+
+XPCOMUtils.defineConstant(this, "SCRATCHPAD_CONTEXT_CONTENT", SCRATCHPAD_CONTEXT_CONTENT);
+XPCOMUtils.defineConstant(this, "SCRATCHPAD_CONTEXT_BROWSER", SCRATCHPAD_CONTEXT_BROWSER);
+XPCOMUtils.defineConstant(this, "BUTTON_POSITION_SAVE", BUTTON_POSITION_SAVE);
+XPCOMUtils.defineConstant(this, "BUTTON_POSITION_CANCEL", BUTTON_POSITION_CANCEL);
+XPCOMUtils.defineConstant(this, "BUTTON_POSITION_DONT_SAVE", BUTTON_POSITION_DONT_SAVE);
+XPCOMUtils.defineConstant(this, "BUTTON_POSITION_REVERT", BUTTON_POSITION_REVERT);
+
+XPCOMUtils.defineLazyModuleGetter(this, "VariablesView",
+ "resource://devtools/client/shared/widgets/VariablesView.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "VariablesViewController",
+ "resource://devtools/client/shared/widgets/VariablesViewController.jsm");
+
+loader.lazyRequireGetter(this, "DebuggerServer", "devtools/server/main", true);
+
+loader.lazyRequireGetter(this, "DebuggerClient", "devtools/shared/client/main", true);
+loader.lazyRequireGetter(this, "EnvironmentClient", "devtools/shared/client/main", true);
+loader.lazyRequireGetter(this, "ObjectClient", "devtools/shared/client/main", true);
+loader.lazyRequireGetter(this, "HUDService", "devtools/client/webconsole/hudservice");
+
+XPCOMUtils.defineLazyGetter(this, "REMOTE_TIMEOUT", () =>
+ Services.prefs.getIntPref("devtools.debugger.remote-timeout"));
+
+XPCOMUtils.defineLazyModuleGetter(this, "ShortcutUtils",
+ "resource://gre/modules/ShortcutUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Reflect",
+ "resource://gre/modules/reflect.jsm");
+
+var WebConsoleUtils = require("devtools/client/webconsole/utils").Utils;
+
+/**
+ * The scratchpad object handles the Scratchpad window functionality.
+ */
+var Scratchpad = {
+ _instanceId: null,
+ _initialWindowTitle: document.title,
+ _dirty: false,
+
+ /**
+ * Check if provided string is a mode-line and, if it is, return an
+ * object with its values.
+ *
+ * @param string aLine
+ * @return string
+ */
+ _scanModeLine: function SP__scanModeLine(aLine = "")
+ {
+ aLine = aLine.trim();
+
+ let obj = {};
+ let ch1 = aLine.charAt(0);
+ let ch2 = aLine.charAt(1);
+
+ if (ch1 !== "/" || (ch2 !== "*" && ch2 !== "/")) {
+ return obj;
+ }
+
+ aLine = aLine
+ .replace(/^\/\//, "")
+ .replace(/^\/\*/, "")
+ .replace(/\*\/$/, "");
+
+ aLine.split(",").forEach(pair => {
+ let [key, val] = pair.split(":");
+
+ if (key && val) {
+ obj[key.trim()] = val.trim();
+ }
+ });
+
+ return obj;
+ },
+
+ /**
+ * Add the event listeners for popupshowing events.
+ */
+ _setupPopupShowingListeners: function SP_setupPopupShowing() {
+ let elementIDs = ["sp-menu_editpopup", "scratchpad-text-popup"];
+
+ for (let elementID of elementIDs) {
+ let elem = document.getElementById(elementID);
+ if (elem) {
+ elem.addEventListener("popupshowing", function () {
+ goUpdateGlobalEditMenuItems();
+ let commands = ["cmd_undo", "cmd_redo", "cmd_delete", "cmd_findAgain"];
+ commands.forEach(goUpdateCommand);
+ });
+ }
+ }
+ },
+
+ /**
+ * Add the event event listeners for command events.
+ */
+ _setupCommandListeners: function SP_setupCommands() {
+ let commands = {
+ "cmd_find": () => {
+ goDoCommand("cmd_find");
+ },
+ "cmd_findAgain": () => {
+ goDoCommand("cmd_findAgain");
+ },
+ "cmd_gotoLine": () => {
+ goDoCommand("cmd_gotoLine");
+ },
+ "sp-cmd-newWindow": () => {
+ Scratchpad.openScratchpad();
+ },
+ "sp-cmd-openFile": () => {
+ Scratchpad.openFile();
+ },
+ "sp-cmd-clearRecentFiles": () => {
+ Scratchpad.clearRecentFiles();
+ },
+ "sp-cmd-save": () => {
+ Scratchpad.saveFile();
+ },
+ "sp-cmd-saveas": () => {
+ Scratchpad.saveFileAs();
+ },
+ "sp-cmd-revert": () => {
+ Scratchpad.promptRevert();
+ },
+ "sp-cmd-close": () => {
+ Scratchpad.close();
+ },
+ "sp-cmd-run": () => {
+ Scratchpad.run();
+ },
+ "sp-cmd-inspect": () => {
+ Scratchpad.inspect();
+ },
+ "sp-cmd-display": () => {
+ Scratchpad.display();
+ },
+ "sp-cmd-pprint": () => {
+ Scratchpad.prettyPrint();
+ },
+ "sp-cmd-contentContext": () => {
+ Scratchpad.setContentContext();
+ },
+ "sp-cmd-browserContext": () => {
+ Scratchpad.setBrowserContext();
+ },
+ "sp-cmd-reloadAndRun": () => {
+ Scratchpad.reloadAndRun();
+ },
+ "sp-cmd-evalFunction": () => {
+ Scratchpad.evalTopLevelFunction();
+ },
+ "sp-cmd-errorConsole": () => {
+ Scratchpad.openErrorConsole();
+ },
+ "sp-cmd-webConsole": () => {
+ Scratchpad.openWebConsole();
+ },
+ "sp-cmd-documentationLink": () => {
+ Scratchpad.openDocumentationPage();
+ },
+ "sp-cmd-hideSidebar": () => {
+ Scratchpad.sidebar.hide();
+ },
+ "sp-cmd-line-numbers": () => {
+ Scratchpad.toggleEditorOption("lineNumbers", SHOW_LINE_NUMBERS);
+ },
+ "sp-cmd-wrap-text": () => {
+ Scratchpad.toggleEditorOption("lineWrapping", WRAP_TEXT);
+ },
+ "sp-cmd-highlight-trailing-space": () => {
+ Scratchpad.toggleEditorOption("showTrailingSpace", SHOW_TRAILING_SPACE);
+ },
+ "sp-cmd-larger-font": () => {
+ Scratchpad.increaseFontSize();
+ },
+ "sp-cmd-smaller-font": () => {
+ Scratchpad.decreaseFontSize();
+ },
+ "sp-cmd-normal-font": () => {
+ Scratchpad.normalFontSize();
+ },
+ };
+
+ for (let command in commands) {
+ let elem = document.getElementById(command);
+ if (elem) {
+ elem.addEventListener("command", commands[command]);
+ }
+ }
+ },
+
+ /**
+ * Check or uncheck view menu items according to stored preferences.
+ */
+ _updateViewMenuItems: function SP_updateViewMenuItems() {
+ this._updateViewMenuItem(SHOW_LINE_NUMBERS, "sp-menu-line-numbers");
+ this._updateViewMenuItem(WRAP_TEXT, "sp-menu-word-wrap");
+ this._updateViewMenuItem(SHOW_TRAILING_SPACE, "sp-menu-highlight-trailing-space");
+ this._updateViewFontMenuItem(MINIMUM_FONT_SIZE, "sp-cmd-smaller-font");
+ this._updateViewFontMenuItem(MAXIMUM_FONT_SIZE, "sp-cmd-larger-font");
+ },
+
+ /**
+ * Check or uncheck view menu item according to stored preferences.
+ */
+ _updateViewMenuItem: function SP_updateViewMenuItem(preferenceName, menuId) {
+ let checked = Services.prefs.getBoolPref(preferenceName);
+ if (checked) {
+ document.getElementById(menuId).setAttribute("checked", true);
+ } else {
+ document.getElementById(menuId).removeAttribute("checked");
+ }
+ },
+
+ /**
+ * Disable view menu item if the stored font size is equals to the given one.
+ */
+ _updateViewFontMenuItem: function SP_updateViewFontMenuItem(fontSize, commandId) {
+ let prefFontSize = Services.prefs.getIntPref(EDITOR_FONT_SIZE);
+ if (prefFontSize === fontSize) {
+ document.getElementById(commandId).setAttribute("disabled", true);
+ }
+ },
+
+ /**
+ * The script execution context. This tells Scratchpad in which context the
+ * script shall execute.
+ *
+ * Possible values:
+ * - SCRATCHPAD_CONTEXT_CONTENT to execute code in the context of the current
+ * tab content window object.
+ * - SCRATCHPAD_CONTEXT_BROWSER to execute code in the context of the
+ * currently active chrome window object.
+ */
+ executionContext: SCRATCHPAD_CONTEXT_CONTENT,
+
+ /**
+ * Tells if this Scratchpad is initialized and ready for use.
+ * @boolean
+ * @see addObserver
+ */
+ initialized: false,
+
+ /**
+ * Returns the 'dirty' state of this Scratchpad.
+ */
+ get dirty()
+ {
+ let clean = this.editor && this.editor.isClean();
+ return this._dirty || !clean;
+ },
+
+ /**
+ * Sets the 'dirty' state of this Scratchpad.
+ */
+ set dirty(aValue)
+ {
+ this._dirty = aValue;
+ if (!aValue && this.editor)
+ this.editor.setClean();
+ this._updateTitle();
+ },
+
+ /**
+ * Retrieve the xul:notificationbox DOM element. It notifies the user when
+ * the current code execution context is SCRATCHPAD_CONTEXT_BROWSER.
+ */
+ get notificationBox()
+ {
+ return document.getElementById("scratchpad-notificationbox");
+ },
+
+ /**
+ * Hide the menu bar.
+ */
+ hideMenu: function SP_hideMenu()
+ {
+ document.getElementById("sp-menubar").style.display = "none";
+ },
+
+ /**
+ * Show the menu bar.
+ */
+ showMenu: function SP_showMenu()
+ {
+ document.getElementById("sp-menubar").style.display = "";
+ },
+
+ /**
+ * Get the editor content, in the given range. If no range is given you get
+ * the entire editor content.
+ *
+ * @param number [aStart=0]
+ * Optional, start from the given offset.
+ * @param number [aEnd=content char count]
+ * Optional, end offset for the text you want. If this parameter is not
+ * given, then the text returned goes until the end of the editor
+ * content.
+ * @return string
+ * The text in the given range.
+ */
+ getText: function SP_getText(aStart, aEnd)
+ {
+ var value = this.editor.getText();
+ return value.slice(aStart || 0, aEnd || value.length);
+ },
+
+ /**
+ * Set the filename in the scratchpad UI and object
+ *
+ * @param string aFilename
+ * The new filename
+ */
+ setFilename: function SP_setFilename(aFilename)
+ {
+ this.filename = aFilename;
+ this._updateTitle();
+ },
+
+ /**
+ * Update the Scratchpad window title based on the current state.
+ * @private
+ */
+ _updateTitle: function SP__updateTitle()
+ {
+ let title = this.filename || this._initialWindowTitle;
+
+ if (this.dirty)
+ title = "*" + title;
+
+ document.title = title;
+ },
+
+ /**
+ * Get the current state of the scratchpad. Called by the
+ * Scratchpad Manager for session storing.
+ *
+ * @return object
+ * An object with 3 properties: filename, text, and
+ * executionContext.
+ */
+ getState: function SP_getState()
+ {
+ return {
+ filename: this.filename,
+ text: this.getText(),
+ executionContext: this.executionContext,
+ saved: !this.dirty
+ };
+ },
+
+ /**
+ * Set the filename and execution context using the given state. Called
+ * when scratchpad is being restored from a previous session.
+ *
+ * @param object aState
+ * An object with filename and executionContext properties.
+ */
+ setState: function SP_setState(aState)
+ {
+ if (aState.filename)
+ this.setFilename(aState.filename);
+
+ this.dirty = !aState.saved;
+
+ if (aState.executionContext == SCRATCHPAD_CONTEXT_BROWSER)
+ this.setBrowserContext();
+ else
+ this.setContentContext();
+ },
+
+ /**
+ * Get the most recent main chrome browser window
+ */
+ get browserWindow()
+ {
+ return Services.wm.getMostRecentWindow(gDevTools.chromeWindowType);
+ },
+
+ /**
+ * Get the gBrowser object of the most recent browser window.
+ */
+ get gBrowser()
+ {
+ let recentWin = this.browserWindow;
+ return recentWin ? recentWin.gBrowser : null;
+ },
+
+ /**
+ * Unique name for the current Scratchpad instance. Used to distinguish
+ * Scratchpad windows between each other. See bug 661762.
+ */
+ get uniqueName()
+ {
+ return "Scratchpad/" + this._instanceId;
+ },
+
+
+ /**
+ * Sidebar that contains the VariablesView for object inspection.
+ */
+ get sidebar()
+ {
+ if (!this._sidebar) {
+ this._sidebar = new ScratchpadSidebar(this);
+ }
+ return this._sidebar;
+ },
+
+ /**
+ * Replaces context of an editor with provided value (a string).
+ * Note: this method is simply a shortcut to editor.setText.
+ */
+ setText: function SP_setText(value)
+ {
+ return this.editor.setText(value);
+ },
+
+ /**
+ * Evaluate a string in the currently desired context, that is either the
+ * chrome window or the tab content window object.
+ *
+ * @param string aString
+ * The script you want to evaluate.
+ * @return Promise
+ * The promise for the script evaluation result.
+ */
+ evaluate: function SP_evaluate(aString)
+ {
+ let connection;
+ if (this.target) {
+ connection = ScratchpadTarget.consoleFor(this.target);
+ }
+ else if (this.executionContext == SCRATCHPAD_CONTEXT_CONTENT) {
+ connection = ScratchpadTab.consoleFor(this.gBrowser.selectedTab);
+ }
+ else {
+ connection = ScratchpadWindow.consoleFor(this.browserWindow);
+ }
+
+ let evalOptions = { url: this.uniqueName };
+
+ return connection.then(({ debuggerClient, webConsoleClient }) => {
+ let deferred = promise.defer();
+
+ webConsoleClient.evaluateJSAsync(aString, aResponse => {
+ this.debuggerClient = debuggerClient;
+ this.webConsoleClient = webConsoleClient;
+ if (aResponse.error) {
+ deferred.reject(aResponse);
+ }
+ else if (aResponse.exception !== null) {
+ deferred.resolve([aString, aResponse]);
+ }
+ else {
+ deferred.resolve([aString, undefined, aResponse.result]);
+ }
+ }, evalOptions);
+
+ return deferred.promise;
+ });
+ },
+
+ /**
+ * Execute the selected text (if any) or the entire editor content in the
+ * current context.
+ *
+ * @return Promise
+ * The promise for the script evaluation result.
+ */
+ execute: function SP_execute()
+ {
+ WebConsoleUtils.usageCount++;
+ let selection = this.editor.getSelection() || this.getText();
+ return this.evaluate(selection);
+ },
+
+ /**
+ * Execute the selected text (if any) or the entire editor content in the
+ * current context.
+ *
+ * @return Promise
+ * The promise for the script evaluation result.
+ */
+ run: function SP_run()
+ {
+ let deferred = promise.defer();
+ let reject = aReason => deferred.reject(aReason);
+
+ this.execute().then(([aString, aError, aResult]) => {
+ let resolve = () => deferred.resolve([aString, aError, aResult]);
+
+ if (aError) {
+ this.writeAsErrorComment(aError).then(resolve, reject);
+ }
+ else {
+ this.editor.dropSelection();
+ resolve();
+ }
+ }, reject);
+
+ return deferred.promise;
+ },
+
+ /**
+ * Execute the selected text (if any) or the entire editor content in the
+ * current context. The resulting object is inspected up in the sidebar.
+ *
+ * @return Promise
+ * The promise for the script evaluation result.
+ */
+ inspect: function SP_inspect()
+ {
+ let deferred = promise.defer();
+ let reject = aReason => deferred.reject(aReason);
+
+ this.execute().then(([aString, aError, aResult]) => {
+ let resolve = () => deferred.resolve([aString, aError, aResult]);
+
+ if (aError) {
+ this.writeAsErrorComment(aError).then(resolve, reject);
+ }
+ else {
+ this.editor.dropSelection();
+ this.sidebar.open(aString, aResult).then(resolve, reject);
+ }
+ }, reject);
+
+ return deferred.promise;
+ },
+
+ /**
+ * Reload the current page and execute the entire editor content when
+ * the page finishes loading. Note that this operation should be available
+ * only in the content context.
+ *
+ * @return Promise
+ * The promise for the script evaluation result.
+ */
+ reloadAndRun: function SP_reloadAndRun()
+ {
+ let deferred = promise.defer();
+
+ if (this.executionContext !== SCRATCHPAD_CONTEXT_CONTENT) {
+ console.error(this.strings.
+ GetStringFromName("scratchpadContext.invalid"));
+ return;
+ }
+
+ let target = TargetFactory.forTab(this.gBrowser.selectedTab);
+ target.once("navigate", () => {
+ this.run().then(results => deferred.resolve(results));
+ });
+ target.makeRemote().then(() => target.activeTab.reload());
+
+ return deferred.promise;
+ },
+
+ /**
+ * Execute the selected text (if any) or the entire editor content in the
+ * current context. The evaluation result is inserted into the editor after
+ * the selected text, or at the end of the editor content if there is no
+ * selected text.
+ *
+ * @return Promise
+ * The promise for the script evaluation result.
+ */
+ display: function SP_display()
+ {
+ let deferred = promise.defer();
+ let reject = aReason => deferred.reject(aReason);
+
+ this.execute().then(([aString, aError, aResult]) => {
+ let resolve = () => deferred.resolve([aString, aError, aResult]);
+
+ if (aError) {
+ this.writeAsErrorComment(aError).then(resolve, reject);
+ }
+ else if (VariablesView.isPrimitive({ value: aResult })) {
+ this._writePrimitiveAsComment(aResult).then(resolve, reject);
+ }
+ else {
+ let objectClient = new ObjectClient(this.debuggerClient, aResult);
+ objectClient.getDisplayString(aResponse => {
+ if (aResponse.error) {
+ reportError("display", aResponse);
+ reject(aResponse);
+ }
+ else {
+ this.writeAsComment(aResponse.displayString);
+ resolve();
+ }
+ });
+ }
+ }, reject);
+
+ return deferred.promise;
+ },
+
+ _prettyPrintWorker: null,
+
+ /**
+ * Get or create the worker that handles pretty printing.
+ */
+ get prettyPrintWorker() {
+ if (!this._prettyPrintWorker) {
+ this._prettyPrintWorker = new DevToolsWorker(
+ "resource://devtools/server/actors/pretty-print-worker.js",
+ { name: "pretty-print",
+ verbose: flags.wantLogging }
+ );
+ }
+ return this._prettyPrintWorker;
+ },
+
+ /**
+ * Pretty print the source text inside the scratchpad.
+ *
+ * @return Promise
+ * A promise resolved with the pretty printed code, or rejected with
+ * an error.
+ */
+ prettyPrint: function SP_prettyPrint() {
+ const uglyText = this.getText();
+ const tabsize = Services.prefs.getIntPref(TAB_SIZE);
+
+ return this.prettyPrintWorker.performTask("pretty-print", {
+ url: "(scratchpad)",
+ indent: tabsize,
+ source: uglyText
+ }).then(data => {
+ this.editor.setText(data.code);
+ }).then(null, error => {
+ this.writeAsErrorComment({ exception: error });
+ throw error;
+ });
+ },
+
+ /**
+ * Parse the text and return an AST. If we can't parse it, write an error
+ * comment and return false.
+ */
+ _parseText: function SP__parseText(aText) {
+ try {
+ return Reflect.parse(aText);
+ } catch (e) {
+ this.writeAsErrorComment({ exception: DevToolsUtils.safeErrorString(e) });
+ return false;
+ }
+ },
+
+ /**
+ * Determine if the given AST node location contains the given cursor
+ * position.
+ *
+ * @returns Boolean
+ */
+ _containsCursor: function (aLoc, aCursorPos) {
+ // Our line numbers are 1-based, while CodeMirror's are 0-based.
+ const lineNumber = aCursorPos.line + 1;
+ const columnNumber = aCursorPos.ch;
+
+ if (aLoc.start.line <= lineNumber && aLoc.end.line >= lineNumber) {
+ if (aLoc.start.line === aLoc.end.line) {
+ return aLoc.start.column <= columnNumber
+ && aLoc.end.column >= columnNumber;
+ }
+
+ if (aLoc.start.line == lineNumber) {
+ return columnNumber >= aLoc.start.column;
+ }
+
+ if (aLoc.end.line == lineNumber) {
+ return columnNumber <= aLoc.end.column;
+ }
+
+ return true;
+ }
+
+ return false;
+ },
+
+ /**
+ * Find the top level function AST node that the cursor is within.
+ *
+ * @returns Object|null
+ */
+ _findTopLevelFunction: function SP__findTopLevelFunction(aAst, aCursorPos) {
+ for (let statement of aAst.body) {
+ switch (statement.type) {
+ case "FunctionDeclaration":
+ if (this._containsCursor(statement.loc, aCursorPos)) {
+ return statement;
+ }
+ break;
+
+ case "VariableDeclaration":
+ for (let decl of statement.declarations) {
+ if (!decl.init) {
+ continue;
+ }
+ if ((decl.init.type == "FunctionExpression"
+ || decl.init.type == "ArrowFunctionExpression")
+ && this._containsCursor(decl.loc, aCursorPos)) {
+ return decl;
+ }
+ }
+ break;
+ }
+ }
+
+ return null;
+ },
+
+ /**
+ * Get the source text associated with the given function statement.
+ *
+ * @param Object aFunction
+ * @param String aFullText
+ * @returns String
+ */
+ _getFunctionText: function SP__getFunctionText(aFunction, aFullText) {
+ let functionText = "";
+ // Initially set to 0, but incremented first thing in the loop below because
+ // line numbers are 1 based, not 0 based.
+ let lineNumber = 0;
+ const { start, end } = aFunction.loc;
+ const singleLine = start.line === end.line;
+
+ for (let line of aFullText.split(/\n/g)) {
+ lineNumber++;
+
+ if (singleLine && start.line === lineNumber) {
+ functionText = line.slice(start.column, end.column);
+ break;
+ }
+
+ if (start.line === lineNumber) {
+ functionText += line.slice(start.column) + "\n";
+ continue;
+ }
+
+ if (end.line === lineNumber) {
+ functionText += line.slice(0, end.column);
+ break;
+ }
+
+ if (start.line < lineNumber && end.line > lineNumber) {
+ functionText += line + "\n";
+ }
+ }
+
+ return functionText;
+ },
+
+ /**
+ * Evaluate the top level function that the cursor is resting in.
+ *
+ * @returns Promise [text, error, result]
+ */
+ evalTopLevelFunction: function SP_evalTopLevelFunction() {
+ const text = this.getText();
+ const ast = this._parseText(text);
+ if (!ast) {
+ return promise.resolve([text, undefined, undefined]);
+ }
+
+ const cursorPos = this.editor.getCursor();
+ const funcStatement = this._findTopLevelFunction(ast, cursorPos);
+ if (!funcStatement) {
+ return promise.resolve([text, undefined, undefined]);
+ }
+
+ let functionText = this._getFunctionText(funcStatement, text);
+
+ // TODO: This is a work around for bug 940086. It should be removed when
+ // that is fixed.
+ if (funcStatement.type == "FunctionDeclaration"
+ && !functionText.startsWith("function ")) {
+ functionText = "function " + functionText;
+ funcStatement.loc.start.column -= 9;
+ }
+
+ // The decrement by one is because our line numbers are 1-based, while
+ // CodeMirror's are 0-based.
+ const from = {
+ line: funcStatement.loc.start.line - 1,
+ ch: funcStatement.loc.start.column
+ };
+ const to = {
+ line: funcStatement.loc.end.line - 1,
+ ch: funcStatement.loc.end.column
+ };
+
+ const marker = this.editor.markText(from, to, "eval-text");
+ setTimeout(() => marker.clear(), EVAL_FUNCTION_TIMEOUT);
+
+ return this.evaluate(functionText);
+ },
+
+ /**
+ * Writes out a primitive value as a comment. This handles values which are
+ * to be printed directly (number, string) as well as grips to values
+ * (null, undefined, longString).
+ *
+ * @param any aValue
+ * The value to print.
+ * @return Promise
+ * The promise that resolves after the value has been printed.
+ */
+ _writePrimitiveAsComment: function SP__writePrimitiveAsComment(aValue)
+ {
+ let deferred = promise.defer();
+
+ if (aValue.type == "longString") {
+ let client = this.webConsoleClient;
+ client.longString(aValue).substring(0, aValue.length, aResponse => {
+ if (aResponse.error) {
+ reportError("display", aResponse);
+ deferred.reject(aResponse);
+ }
+ else {
+ deferred.resolve(aResponse.substring);
+ }
+ });
+ }
+ else {
+ deferred.resolve(aValue.type || aValue);
+ }
+
+ return deferred.promise.then(aComment => {
+ this.writeAsComment(aComment);
+ });
+ },
+
+ /**
+ * Write out a value at the next line from the current insertion point.
+ * The comment block will always be preceded by a newline character.
+ * @param object aValue
+ * The Object to write out as a string
+ */
+ writeAsComment: function SP_writeAsComment(aValue)
+ {
+ let value = "\n/*\n" + aValue + "\n*/";
+
+ if (this.editor.somethingSelected()) {
+ let from = this.editor.getCursor("end");
+ this.editor.replaceSelection(this.editor.getSelection() + value);
+ let to = this.editor.getPosition(this.editor.getOffset(from) + value.length);
+ this.editor.setSelection(from, to);
+ return;
+ }
+
+ let text = this.editor.getText();
+ this.editor.setText(text + value);
+
+ let [ from, to ] = this.editor.getPosition(text.length, (text + value).length);
+ this.editor.setSelection(from, to);
+ },
+
+ /**
+ * Write out an error at the current insertion point as a block comment
+ * @param object aValue
+ * The error object to write out the message and stack trace. It must
+ * contain an |exception| property with the actual error thrown, but it
+ * will often be the entire response of an evaluateJS request.
+ * @return Promise
+ * The promise that indicates when writing the comment completes.
+ */
+ writeAsErrorComment: function SP_writeAsErrorComment(aError)
+ {
+ let deferred = promise.defer();
+
+ if (VariablesView.isPrimitive({ value: aError.exception })) {
+ let error = aError.exception;
+ let type = error.type;
+ if (type == "undefined" ||
+ type == "null" ||
+ type == "Infinity" ||
+ type == "-Infinity" ||
+ type == "NaN" ||
+ type == "-0") {
+ deferred.resolve(type);
+ }
+ else if (type == "longString") {
+ deferred.resolve(error.initial + "\u2026");
+ }
+ else {
+ deferred.resolve(error);
+ }
+ } else if ("preview" in aError.exception) {
+ let error = aError.exception;
+ let stack = this._constructErrorStack(error.preview);
+ if (typeof aError.exceptionMessage == "string") {
+ deferred.resolve(aError.exceptionMessage + stack);
+ } else {
+ deferred.resolve(stack);
+ }
+ } else {
+ // If there is no preview information, we need to ask the server for more.
+ let objectClient = new ObjectClient(this.debuggerClient, aError.exception);
+ objectClient.getPrototypeAndProperties(aResponse => {
+ if (aResponse.error) {
+ deferred.reject(aResponse);
+ return;
+ }
+
+ let { ownProperties, safeGetterValues } = aResponse;
+ let error = Object.create(null);
+
+ // Combine all the property descriptor/getter values into one object.
+ for (let key of Object.keys(safeGetterValues)) {
+ error[key] = safeGetterValues[key].getterValue;
+ }
+
+ for (let key of Object.keys(ownProperties)) {
+ error[key] = ownProperties[key].value;
+ }
+
+ let stack = this._constructErrorStack(error);
+
+ if (typeof error.message == "string") {
+ deferred.resolve(error.message + stack);
+ }
+ else {
+ objectClient.getDisplayString(aResponse => {
+ if (aResponse.error) {
+ deferred.reject(aResponse);
+ }
+ else if (typeof aResponse.displayString == "string") {
+ deferred.resolve(aResponse.displayString + stack);
+ }
+ else {
+ deferred.resolve(stack);
+ }
+ });
+ }
+ });
+ }
+
+ return deferred.promise.then(aMessage => {
+ console.error(aMessage);
+ this.writeAsComment("Exception: " + aMessage);
+ });
+ },
+
+ /**
+ * Assembles the best possible stack from the properties of the provided
+ * error.
+ */
+ _constructErrorStack(error) {
+ let stack;
+ if (typeof error.stack == "string" && error.stack) {
+ stack = error.stack;
+ } else if (typeof error.fileName == "string") {
+ stack = "@" + error.fileName;
+ if (typeof error.lineNumber == "number") {
+ stack += ":" + error.lineNumber;
+ }
+ } else if (typeof error.filename == "string") {
+ stack = "@" + error.filename;
+ if (typeof error.lineNumber == "number") {
+ stack += ":" + error.lineNumber;
+ if (typeof error.columnNumber == "number") {
+ stack += ":" + error.columnNumber;
+ }
+ }
+ } else if (typeof error.lineNumber == "number") {
+ stack = "@" + error.lineNumber;
+ if (typeof error.columnNumber == "number") {
+ stack += ":" + error.columnNumber;
+ }
+ }
+
+ return stack ? "\n" + stack.replace(/\n$/, "") : "";
+ },
+
+ // Menu Operations
+
+ /**
+ * Open a new Scratchpad window.
+ *
+ * @return nsIWindow
+ */
+ openScratchpad: function SP_openScratchpad()
+ {
+ return ScratchpadManager.openScratchpad();
+ },
+
+ /**
+ * Export the textbox content to a file.
+ *
+ * @param nsILocalFile aFile
+ * The file where you want to save the textbox content.
+ * @param boolean aNoConfirmation
+ * If the file already exists, ask for confirmation?
+ * @param boolean aSilentError
+ * True if you do not want to display an error when file save fails,
+ * false otherwise.
+ * @param function aCallback
+ * Optional function you want to call when file save completes. It will
+ * get the following arguments:
+ * 1) the nsresult status code for the export operation.
+ */
+ exportToFile: function SP_exportToFile(aFile, aNoConfirmation, aSilentError,
+ aCallback)
+ {
+ if (!aNoConfirmation && aFile.exists() &&
+ !window.confirm(this.strings.
+ GetStringFromName("export.fileOverwriteConfirmation"))) {
+ return;
+ }
+
+ let encoder = new TextEncoder();
+ let buffer = encoder.encode(this.getText());
+ let writePromise = OS.File.writeAtomic(aFile.path, buffer, {tmpPath: aFile.path + ".tmp"});
+ writePromise.then(value => {
+ if (aCallback) {
+ aCallback.call(this, Components.results.NS_OK);
+ }
+ }, reason => {
+ if (!aSilentError) {
+ window.alert(this.strings.GetStringFromName("saveFile.failed"));
+ }
+ if (aCallback) {
+ aCallback.call(this, Components.results.NS_ERROR_UNEXPECTED);
+ }
+ });
+
+ },
+
+ /**
+ * Get a list of applicable charsets.
+ * The best charset, defaulting to "UTF-8"
+ *
+ * @param string aBestCharset
+ * @return array of strings
+ */
+ _getApplicableCharsets: function SP__getApplicableCharsets(aBestCharset = "UTF-8") {
+ let charsets = Services.prefs.getCharPref(
+ FALLBACK_CHARSET_LIST).split(",").filter(function (value) {
+ return value.length;
+ });
+ charsets.unshift(aBestCharset);
+ return charsets;
+ },
+
+ /**
+ * Get content converted to unicode, using a list of input charset to try.
+ *
+ * @param string aContent
+ * @param array of string aCharsetArray
+ * @return string
+ */
+ _getUnicodeContent: function SP__getUnicodeContent(aContent, aCharsetArray) {
+ let content = null,
+ converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].createInstance(Ci.nsIScriptableUnicodeConverter),
+ success = aCharsetArray.some(charset => {
+ try {
+ converter.charset = charset;
+ content = converter.ConvertToUnicode(aContent);
+ return true;
+ } catch (e) {
+ this.notificationBox.appendNotification(
+ this.strings.formatStringFromName("importFromFile.convert.failed",
+ [ charset ], 1),
+ "file-import-convert-failed",
+ null,
+ this.notificationBox.PRIORITY_WARNING_HIGH,
+ null);
+ }
+ });
+ return content;
+ },
+
+ /**
+ * Read the content of a file and put it into the textbox.
+ *
+ * @param nsILocalFile aFile
+ * The file you want to save the textbox content into.
+ * @param boolean aSilentError
+ * True if you do not want to display an error when file load fails,
+ * false otherwise.
+ * @param function aCallback
+ * Optional function you want to call when file load completes. It will
+ * get the following arguments:
+ * 1) the nsresult status code for the import operation.
+ * 2) the data that was read from the file, if any.
+ */
+ importFromFile: function SP_importFromFile(aFile, aSilentError, aCallback)
+ {
+ // Prevent file type detection.
+ let channel = NetUtil.newChannel({
+ uri: NetUtil.newURI(aFile),
+ loadingNode: window.document,
+ securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_INHERITS,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER});
+ channel.contentType = "application/javascript";
+
+ this.notificationBox.removeAllNotifications(false);
+
+ NetUtil.asyncFetch(channel, (aInputStream, aStatus) => {
+ let content = null;
+
+ if (Components.isSuccessCode(aStatus)) {
+ let charsets = this._getApplicableCharsets();
+ content = NetUtil.readInputStreamToString(aInputStream,
+ aInputStream.available());
+ content = this._getUnicodeContent(content, charsets);
+ if (!content) {
+ let message = this.strings.formatStringFromName(
+ "importFromFile.convert.failed",
+ [ charsets.join(", ") ],
+ 1);
+ this.notificationBox.appendNotification(
+ message,
+ "file-import-convert-failed",
+ null,
+ this.notificationBox.PRIORITY_CRITICAL_MEDIUM,
+ null);
+ if (aCallback) {
+ aCallback.call(this, aStatus, content);
+ }
+ return;
+ }
+ // Check to see if the first line is a mode-line comment.
+ let line = content.split("\n")[0];
+ let modeline = this._scanModeLine(line);
+ let chrome = Services.prefs.getBoolPref(DEVTOOLS_CHROME_ENABLED);
+
+ if (chrome && modeline["-sp-context"] === "browser") {
+ this.setBrowserContext();
+ }
+
+ this.editor.setText(content);
+ this.editor.clearHistory();
+ this.dirty = false;
+ document.getElementById("sp-cmd-revert").setAttribute("disabled", true);
+ }
+ else if (!aSilentError) {
+ window.alert(this.strings.GetStringFromName("openFile.failed"));
+ }
+ this.setFilename(aFile.path);
+ this.setRecentFile(aFile);
+ if (aCallback) {
+ aCallback.call(this, aStatus, content);
+ }
+ });
+ },
+
+ /**
+ * Open a file to edit in the Scratchpad.
+ *
+ * @param integer aIndex
+ * Optional integer: clicked menuitem in the 'Open Recent'-menu.
+ */
+ openFile: function SP_openFile(aIndex)
+ {
+ let promptCallback = aFile => {
+ this.promptSave((aCloseFile, aSaved, aStatus) => {
+ let shouldOpen = aCloseFile;
+ if (aSaved && !Components.isSuccessCode(aStatus)) {
+ shouldOpen = false;
+ }
+
+ if (shouldOpen) {
+ let file;
+ if (aFile) {
+ file = aFile;
+ } else {
+ file = Components.classes["@mozilla.org/file/local;1"].
+ createInstance(Components.interfaces.nsILocalFile);
+ let filePath = this.getRecentFiles()[aIndex];
+ file.initWithPath(filePath);
+ }
+
+ if (!file.exists()) {
+ this.notificationBox.appendNotification(
+ this.strings.GetStringFromName("fileNoLongerExists.notification"),
+ "file-no-longer-exists",
+ null,
+ this.notificationBox.PRIORITY_WARNING_HIGH,
+ null);
+
+ this.clearFiles(aIndex, 1);
+ return;
+ }
+
+ this.importFromFile(file, false);
+ }
+ });
+ };
+
+ if (aIndex > -1) {
+ promptCallback();
+ } else {
+ let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+ fp.init(window, this.strings.GetStringFromName("openFile.title"),
+ Ci.nsIFilePicker.modeOpen);
+ fp.defaultString = "";
+ fp.appendFilter("JavaScript Files", "*.js; *.jsm; *.json");
+ fp.appendFilter("All Files", "*.*");
+ fp.open(aResult => {
+ if (aResult != Ci.nsIFilePicker.returnCancel) {
+ promptCallback(fp.file);
+ }
+ });
+ }
+ },
+
+ /**
+ * Get recent files.
+ *
+ * @return Array
+ * File paths.
+ */
+ getRecentFiles: function SP_getRecentFiles()
+ {
+ let branch = Services.prefs.getBranch("devtools.scratchpad.");
+ let filePaths = [];
+
+ // WARNING: Do not use getCharPref here, it doesn't play nicely with
+ // Unicode strings.
+
+ if (branch.prefHasUserValue("recentFilePaths")) {
+ let data = branch.getComplexValue("recentFilePaths",
+ Ci.nsISupportsString).data;
+ filePaths = JSON.parse(data);
+ }
+
+ return filePaths;
+ },
+
+ /**
+ * Save a recent file in a JSON parsable string.
+ *
+ * @param nsILocalFile aFile
+ * The nsILocalFile we want to save as a recent file.
+ */
+ setRecentFile: function SP_setRecentFile(aFile)
+ {
+ let maxRecent = Services.prefs.getIntPref(PREF_RECENT_FILES_MAX);
+ if (maxRecent < 1) {
+ return;
+ }
+
+ let filePaths = this.getRecentFiles();
+ let filesCount = filePaths.length;
+ let pathIndex = filePaths.indexOf(aFile.path);
+
+ // We are already storing this file in the list of recent files.
+ if (pathIndex > -1) {
+ // If it's already the most recent file, we don't have to do anything.
+ if (pathIndex === (filesCount - 1)) {
+ // Updating the menu to clear the disabled state from the wrong menuitem
+ // in rare cases when two or more Scratchpad windows are open and the
+ // same file has been opened in two or more windows.
+ this.populateRecentFilesMenu();
+ return;
+ }
+
+ // It is not the most recent file. Remove it from the list, we add it as
+ // the most recent farther down.
+ filePaths.splice(pathIndex, 1);
+ }
+ // If we are not storing the file and the 'recent files'-list is full,
+ // remove the oldest file from the list.
+ else if (filesCount === maxRecent) {
+ filePaths.shift();
+ }
+
+ filePaths.push(aFile.path);
+
+ // WARNING: Do not use setCharPref here, it doesn't play nicely with
+ // Unicode strings.
+
+ let str = Cc["@mozilla.org/supports-string;1"]
+ .createInstance(Ci.nsISupportsString);
+ str.data = JSON.stringify(filePaths);
+
+ let branch = Services.prefs.getBranch("devtools.scratchpad.");
+ branch.setComplexValue("recentFilePaths",
+ Ci.nsISupportsString, str);
+ },
+
+ /**
+ * Populates the 'Open Recent'-menu.
+ */
+ populateRecentFilesMenu: function SP_populateRecentFilesMenu()
+ {
+ let maxRecent = Services.prefs.getIntPref(PREF_RECENT_FILES_MAX);
+ let recentFilesMenu = document.getElementById("sp-open_recent-menu");
+
+ if (maxRecent < 1) {
+ recentFilesMenu.setAttribute("hidden", true);
+ return;
+ }
+
+ let recentFilesPopup = recentFilesMenu.firstChild;
+ let filePaths = this.getRecentFiles();
+ let filename = this.getState().filename;
+
+ recentFilesMenu.setAttribute("disabled", true);
+ while (recentFilesPopup.hasChildNodes()) {
+ recentFilesPopup.removeChild(recentFilesPopup.firstChild);
+ }
+
+ if (filePaths.length > 0) {
+ recentFilesMenu.removeAttribute("disabled");
+
+ // Print out menuitems with the most recent file first.
+ for (let i = filePaths.length - 1; i >= 0; --i) {
+ let menuitem = document.createElement("menuitem");
+ menuitem.setAttribute("type", "radio");
+ menuitem.setAttribute("label", filePaths[i]);
+
+ if (filePaths[i] === filename) {
+ menuitem.setAttribute("checked", true);
+ menuitem.setAttribute("disabled", true);
+ }
+
+ menuitem.addEventListener("command", Scratchpad.openFile.bind(Scratchpad, i));
+ recentFilesPopup.appendChild(menuitem);
+ }
+
+ recentFilesPopup.appendChild(document.createElement("menuseparator"));
+ let clearItems = document.createElement("menuitem");
+ clearItems.setAttribute("id", "sp-menu-clear_recent");
+ clearItems.setAttribute("label",
+ this.strings.
+ GetStringFromName("clearRecentMenuItems.label"));
+ clearItems.setAttribute("command", "sp-cmd-clearRecentFiles");
+ recentFilesPopup.appendChild(clearItems);
+ }
+ },
+
+ /**
+ * Clear a range of files from the list.
+ *
+ * @param integer aIndex
+ * Index of file in menu to remove.
+ * @param integer aLength
+ * Number of files from the index 'aIndex' to remove.
+ */
+ clearFiles: function SP_clearFile(aIndex, aLength)
+ {
+ let filePaths = this.getRecentFiles();
+ filePaths.splice(aIndex, aLength);
+
+ // WARNING: Do not use setCharPref here, it doesn't play nicely with
+ // Unicode strings.
+
+ let str = Cc["@mozilla.org/supports-string;1"]
+ .createInstance(Ci.nsISupportsString);
+ str.data = JSON.stringify(filePaths);
+
+ let branch = Services.prefs.getBranch("devtools.scratchpad.");
+ branch.setComplexValue("recentFilePaths",
+ Ci.nsISupportsString, str);
+ },
+
+ /**
+ * Clear all recent files.
+ */
+ clearRecentFiles: function SP_clearRecentFiles()
+ {
+ Services.prefs.clearUserPref("devtools.scratchpad.recentFilePaths");
+ },
+
+ /**
+ * Handle changes to the 'PREF_RECENT_FILES_MAX'-preference.
+ */
+ handleRecentFileMaxChange: function SP_handleRecentFileMaxChange()
+ {
+ let maxRecent = Services.prefs.getIntPref(PREF_RECENT_FILES_MAX);
+ let menu = document.getElementById("sp-open_recent-menu");
+
+ // Hide the menu if the 'PREF_RECENT_FILES_MAX'-pref is set to zero or less.
+ if (maxRecent < 1) {
+ menu.setAttribute("hidden", true);
+ } else {
+ if (menu.hasAttribute("hidden")) {
+ if (!menu.firstChild.hasChildNodes()) {
+ this.populateRecentFilesMenu();
+ }
+
+ menu.removeAttribute("hidden");
+ }
+
+ let filePaths = this.getRecentFiles();
+ if (maxRecent < filePaths.length) {
+ let diff = filePaths.length - maxRecent;
+ this.clearFiles(0, diff);
+ }
+ }
+ },
+ /**
+ * Save the textbox content to the currently open file.
+ *
+ * @param function aCallback
+ * Optional function you want to call when file is saved
+ */
+ saveFile: function SP_saveFile(aCallback)
+ {
+ if (!this.filename) {
+ return this.saveFileAs(aCallback);
+ }
+
+ let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
+ file.initWithPath(this.filename);
+
+ this.exportToFile(file, true, false, aStatus => {
+ if (Components.isSuccessCode(aStatus)) {
+ this.dirty = false;
+ document.getElementById("sp-cmd-revert").setAttribute("disabled", true);
+ this.setRecentFile(file);
+ }
+ if (aCallback) {
+ aCallback(aStatus);
+ }
+ });
+ },
+
+ /**
+ * Save the textbox content to a new file.
+ *
+ * @param function aCallback
+ * Optional function you want to call when file is saved
+ */
+ saveFileAs: function SP_saveFileAs(aCallback)
+ {
+ let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+ let fpCallback = aResult => {
+ if (aResult != Ci.nsIFilePicker.returnCancel) {
+ this.setFilename(fp.file.path);
+ this.exportToFile(fp.file, true, false, aStatus => {
+ if (Components.isSuccessCode(aStatus)) {
+ this.dirty = false;
+ this.setRecentFile(fp.file);
+ }
+ if (aCallback) {
+ aCallback(aStatus);
+ }
+ });
+ }
+ };
+
+ fp.init(window, this.strings.GetStringFromName("saveFileAs"),
+ Ci.nsIFilePicker.modeSave);
+ fp.defaultString = "scratchpad.js";
+ fp.appendFilter("JavaScript Files", "*.js; *.jsm; *.json");
+ fp.appendFilter("All Files", "*.*");
+ fp.open(fpCallback);
+ },
+
+ /**
+ * Restore content from saved version of current file.
+ *
+ * @param function aCallback
+ * Optional function you want to call when file is saved
+ */
+ revertFile: function SP_revertFile(aCallback)
+ {
+ let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
+ file.initWithPath(this.filename);
+
+ if (!file.exists()) {
+ return;
+ }
+
+ this.importFromFile(file, false, (aStatus, aContent) => {
+ if (aCallback) {
+ aCallback(aStatus);
+ }
+ });
+ },
+
+ /**
+ * Prompt to revert scratchpad if it has unsaved changes.
+ *
+ * @param function aCallback
+ * Optional function you want to call when file is saved. The callback
+ * receives three arguments:
+ * - aRevert (boolean) - tells if the file has been reverted.
+ * - status (number) - the file revert status result (if the file was
+ * saved).
+ */
+ promptRevert: function SP_promptRervert(aCallback)
+ {
+ if (this.filename) {
+ let ps = Services.prompt;
+ let flags = ps.BUTTON_POS_0 * ps.BUTTON_TITLE_REVERT +
+ ps.BUTTON_POS_1 * ps.BUTTON_TITLE_CANCEL;
+
+ let button = ps.confirmEx(window,
+ this.strings.GetStringFromName("confirmRevert.title"),
+ this.strings.GetStringFromName("confirmRevert"),
+ flags, null, null, null, null, {});
+ if (button == BUTTON_POSITION_CANCEL) {
+ if (aCallback) {
+ aCallback(false);
+ }
+
+ return;
+ }
+ if (button == BUTTON_POSITION_REVERT) {
+ this.revertFile(aStatus => {
+ if (aCallback) {
+ aCallback(true, aStatus);
+ }
+ });
+
+ return;
+ }
+ }
+ if (aCallback) {
+ aCallback(false);
+ }
+ },
+
+ /**
+ * Open the Error Console.
+ */
+ openErrorConsole: function SP_openErrorConsole()
+ {
+ HUDService.toggleBrowserConsole();
+ },
+
+ /**
+ * Open the Web Console.
+ */
+ openWebConsole: function SP_openWebConsole()
+ {
+ let target = TargetFactory.forTab(this.gBrowser.selectedTab);
+ gDevTools.showToolbox(target, "webconsole");
+ this.browserWindow.focus();
+ },
+
+ /**
+ * Set the current execution context to be the active tab content window.
+ */
+ setContentContext: function SP_setContentContext()
+ {
+ if (this.executionContext == SCRATCHPAD_CONTEXT_CONTENT) {
+ return;
+ }
+
+ let content = document.getElementById("sp-menu-content");
+ document.getElementById("sp-menu-browser").removeAttribute("checked");
+ document.getElementById("sp-cmd-reloadAndRun").removeAttribute("disabled");
+ content.setAttribute("checked", true);
+ this.executionContext = SCRATCHPAD_CONTEXT_CONTENT;
+ this.notificationBox.removeAllNotifications(false);
+ },
+
+ /**
+ * Set the current execution context to be the most recent chrome window.
+ */
+ setBrowserContext: function SP_setBrowserContext()
+ {
+ if (this.executionContext == SCRATCHPAD_CONTEXT_BROWSER) {
+ return;
+ }
+
+ let browser = document.getElementById("sp-menu-browser");
+ let reloadAndRun = document.getElementById("sp-cmd-reloadAndRun");
+
+ document.getElementById("sp-menu-content").removeAttribute("checked");
+ reloadAndRun.setAttribute("disabled", true);
+ browser.setAttribute("checked", true);
+
+ this.executionContext = SCRATCHPAD_CONTEXT_BROWSER;
+ this.notificationBox.appendNotification(
+ this.strings.GetStringFromName("browserContext.notification"),
+ SCRATCHPAD_CONTEXT_BROWSER,
+ null,
+ this.notificationBox.PRIORITY_WARNING_HIGH,
+ null);
+ },
+
+ /**
+ * Gets the ID of the inner window of the given DOM window object.
+ *
+ * @param nsIDOMWindow aWindow
+ * @return integer
+ * the inner window ID
+ */
+ getInnerWindowId: function SP_getInnerWindowId(aWindow)
+ {
+ return aWindow.QueryInterface(Ci.nsIInterfaceRequestor).
+ getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID;
+ },
+
+ updateStatusBar: function SP_updateStatusBar(aEventType)
+ {
+ var statusBarField = document.getElementById("statusbar-line-col");
+ let { line, ch } = this.editor.getCursor();
+ statusBarField.textContent = this.strings.formatStringFromName(
+ "scratchpad.statusBarLineCol", [ line + 1, ch + 1], 2);
+ },
+
+ /**
+ * The Scratchpad window load event handler. This method
+ * initializes the Scratchpad window and source editor.
+ *
+ * @param nsIDOMEvent aEvent
+ */
+ onLoad: function SP_onLoad(aEvent)
+ {
+ if (aEvent.target != document) {
+ return;
+ }
+
+ let chrome = Services.prefs.getBoolPref(DEVTOOLS_CHROME_ENABLED);
+ if (chrome) {
+ let environmentMenu = document.getElementById("sp-environment-menu");
+ let errorConsoleCommand = document.getElementById("sp-cmd-errorConsole");
+ let chromeContextCommand = document.getElementById("sp-cmd-browserContext");
+ environmentMenu.removeAttribute("hidden");
+ chromeContextCommand.removeAttribute("disabled");
+ errorConsoleCommand.removeAttribute("disabled");
+ }
+
+ let initialText = this.strings.formatStringFromName(
+ "scratchpadIntro1",
+ [ShortcutUtils.prettifyShortcut(document.getElementById("sp-key-run"), true),
+ ShortcutUtils.prettifyShortcut(document.getElementById("sp-key-inspect"), true),
+ ShortcutUtils.prettifyShortcut(document.getElementById("sp-key-display"), true)],
+ 3);
+
+ let args = window.arguments;
+ let state = null;
+
+ if (args && args[0] instanceof Ci.nsIDialogParamBlock) {
+ args = args[0];
+ this._instanceId = args.GetString(0);
+
+ state = args.GetString(1) || null;
+ if (state) {
+ state = JSON.parse(state);
+ this.setState(state);
+ if ("text" in state) {
+ initialText = state.text;
+ }
+ }
+ } else {
+ this._instanceId = ScratchpadManager.createUid();
+ }
+
+ let config = {
+ mode: Editor.modes.js,
+ value: initialText,
+ lineNumbers: Services.prefs.getBoolPref(SHOW_LINE_NUMBERS),
+ contextMenu: "scratchpad-text-popup",
+ showTrailingSpace: Services.prefs.getBoolPref(SHOW_TRAILING_SPACE),
+ autocomplete: Services.prefs.getBoolPref(ENABLE_AUTOCOMPLETION),
+ lineWrapping: Services.prefs.getBoolPref(WRAP_TEXT),
+ };
+
+ this.editor = new Editor(config);
+ let editorElement = document.querySelector("#scratchpad-editor");
+ this.editor.appendTo(editorElement).then(() => {
+ var lines = initialText.split("\n");
+
+ this.editor.setFontSize(Services.prefs.getIntPref(EDITOR_FONT_SIZE));
+
+ this.editor.on("change", this._onChanged);
+ // Keep a reference to the bound version for use in onUnload.
+ this.updateStatusBar = Scratchpad.updateStatusBar.bind(this);
+ this.editor.on("cursorActivity", this.updateStatusBar);
+ let okstring = this.strings.GetStringFromName("selfxss.okstring");
+ let msg = this.strings.formatStringFromName("selfxss.msg", [okstring], 1);
+ this._onPaste = WebConsoleUtils.pasteHandlerGen(this.editor.container.contentDocument.body,
+ document.querySelector("#scratchpad-notificationbox"),
+ msg, okstring);
+ editorElement.addEventListener("paste", this._onPaste, true);
+ editorElement.addEventListener("drop", this._onPaste);
+ this.editor.on("saveRequested", () => this.saveFile());
+ this.editor.focus();
+ this.editor.setCursor({ line: lines.length, ch: lines.pop().length });
+
+ if (state)
+ this.dirty = !state.saved;
+
+ this.initialized = true;
+ this._triggerObservers("Ready");
+ this.populateRecentFilesMenu();
+ PreferenceObserver.init();
+ CloseObserver.init();
+ }).then(null, (err) => console.error(err));
+ this._setupCommandListeners();
+ this._updateViewMenuItems();
+ this._setupPopupShowingListeners();
+ },
+
+ /**
+ * The Source Editor "change" event handler. This function updates the
+ * Scratchpad window title to show an asterisk when there are unsaved changes.
+ *
+ * @private
+ */
+ _onChanged: function SP__onChanged()
+ {
+ Scratchpad._updateTitle();
+
+ if (Scratchpad.filename) {
+ if (Scratchpad.dirty)
+ document.getElementById("sp-cmd-revert").removeAttribute("disabled");
+ else
+ document.getElementById("sp-cmd-revert").setAttribute("disabled", true);
+ }
+ },
+
+ /**
+ * Undo the last action of the user.
+ */
+ undo: function SP_undo()
+ {
+ this.editor.undo();
+ },
+
+ /**
+ * Redo the previously undone action.
+ */
+ redo: function SP_redo()
+ {
+ this.editor.redo();
+ },
+
+ /**
+ * The Scratchpad window unload event handler. This method unloads/destroys
+ * the source editor.
+ *
+ * @param nsIDOMEvent aEvent
+ */
+ onUnload: function SP_onUnload(aEvent)
+ {
+ if (aEvent.target != document) {
+ return;
+ }
+
+ // This event is created only after user uses 'reload and run' feature.
+ if (this._reloadAndRunEvent && this.gBrowser) {
+ this.gBrowser.selectedBrowser.removeEventListener("load",
+ this._reloadAndRunEvent, true);
+ }
+
+ PreferenceObserver.uninit();
+ CloseObserver.uninit();
+ if (this._onPaste) {
+ let editorElement = document.querySelector("#scratchpad-editor");
+ editorElement.removeEventListener("paste", this._onPaste, true);
+ editorElement.removeEventListener("drop", this._onPaste);
+ this._onPaste = null;
+ }
+ this.editor.off("change", this._onChanged);
+ this.editor.off("cursorActivity", this.updateStatusBar);
+ this.editor.destroy();
+ this.editor = null;
+
+ if (this._sidebar) {
+ this._sidebar.destroy();
+ this._sidebar = null;
+ }
+
+ if (this._prettyPrintWorker) {
+ this._prettyPrintWorker.destroy();
+ this._prettyPrintWorker = null;
+ }
+
+ scratchpadTargets = null;
+ this.webConsoleClient = null;
+ this.debuggerClient = null;
+ this.initialized = false;
+ },
+
+ /**
+ * Prompt to save scratchpad if it has unsaved changes.
+ *
+ * @param function aCallback
+ * Optional function you want to call when file is saved. The callback
+ * receives three arguments:
+ * - toClose (boolean) - tells if the window should be closed.
+ * - saved (boolen) - tells if the file has been saved.
+ * - status (number) - the file save status result (if the file was
+ * saved).
+ * @return boolean
+ * Whether the window should be closed
+ */
+ promptSave: function SP_promptSave(aCallback)
+ {
+ if (this.dirty) {
+ let ps = Services.prompt;
+ let flags = ps.BUTTON_POS_0 * ps.BUTTON_TITLE_SAVE +
+ ps.BUTTON_POS_1 * ps.BUTTON_TITLE_CANCEL +
+ ps.BUTTON_POS_2 * ps.BUTTON_TITLE_DONT_SAVE;
+
+ let button = ps.confirmEx(window,
+ this.strings.GetStringFromName("confirmClose.title"),
+ this.strings.GetStringFromName("confirmClose"),
+ flags, null, null, null, null, {});
+
+ if (button == BUTTON_POSITION_CANCEL) {
+ if (aCallback) {
+ aCallback(false, false);
+ }
+ return false;
+ }
+
+ if (button == BUTTON_POSITION_SAVE) {
+ this.saveFile(aStatus => {
+ if (aCallback) {
+ aCallback(true, true, aStatus);
+ }
+ });
+ return true;
+ }
+ }
+
+ if (aCallback) {
+ aCallback(true, false);
+ }
+ return true;
+ },
+
+ /**
+ * Handler for window close event. Prompts to save scratchpad if
+ * there are unsaved changes.
+ *
+ * @param nsIDOMEvent aEvent
+ * @param function aCallback
+ * Optional function you want to call when file is saved/closed.
+ * Used mainly for tests.
+ */
+ onClose: function SP_onClose(aEvent, aCallback)
+ {
+ aEvent.preventDefault();
+ this.close(aCallback);
+ },
+
+ /**
+ * Close the scratchpad window. Prompts before closing if the scratchpad
+ * has unsaved changes.
+ *
+ * @param function aCallback
+ * Optional function you want to call when file is saved
+ */
+ close: function SP_close(aCallback)
+ {
+ let shouldClose;
+
+ this.promptSave((aShouldClose, aSaved, aStatus) => {
+ shouldClose = aShouldClose;
+ if (aSaved && !Components.isSuccessCode(aStatus)) {
+ shouldClose = false;
+ }
+
+ if (shouldClose) {
+ window.close();
+ }
+
+ if (aCallback) {
+ aCallback(shouldClose);
+ }
+ });
+
+ return shouldClose;
+ },
+
+ /**
+ * Toggle a editor's boolean option.
+ */
+ toggleEditorOption: function SP_toggleEditorOption(optionName, optionPreference)
+ {
+ let newOptionValue = !this.editor.getOption(optionName);
+ this.editor.setOption(optionName, newOptionValue);
+ Services.prefs.setBoolPref(optionPreference, newOptionValue);
+ },
+
+ /**
+ * Increase the editor's font size by 1 px.
+ */
+ increaseFontSize: function SP_increaseFontSize()
+ {
+ let size = this.editor.getFontSize();
+
+ if (size < MAXIMUM_FONT_SIZE) {
+ let newFontSize = size + 1;
+ this.editor.setFontSize(newFontSize);
+ Services.prefs.setIntPref(EDITOR_FONT_SIZE, newFontSize);
+
+ if (newFontSize === MAXIMUM_FONT_SIZE) {
+ document.getElementById("sp-cmd-larger-font").setAttribute("disabled", true);
+ }
+
+ document.getElementById("sp-cmd-smaller-font").removeAttribute("disabled");
+ }
+ },
+
+ /**
+ * Decrease the editor's font size by 1 px.
+ */
+ decreaseFontSize: function SP_decreaseFontSize()
+ {
+ let size = this.editor.getFontSize();
+
+ if (size > MINIMUM_FONT_SIZE) {
+ let newFontSize = size - 1;
+ this.editor.setFontSize(newFontSize);
+ Services.prefs.setIntPref(EDITOR_FONT_SIZE, newFontSize);
+
+ if (newFontSize === MINIMUM_FONT_SIZE) {
+ document.getElementById("sp-cmd-smaller-font").setAttribute("disabled", true);
+ }
+ }
+
+ document.getElementById("sp-cmd-larger-font").removeAttribute("disabled");
+ },
+
+ /**
+ * Restore the editor's original font size.
+ */
+ normalFontSize: function SP_normalFontSize()
+ {
+ this.editor.setFontSize(NORMAL_FONT_SIZE);
+ Services.prefs.setIntPref(EDITOR_FONT_SIZE, NORMAL_FONT_SIZE);
+
+ document.getElementById("sp-cmd-larger-font").removeAttribute("disabled");
+ document.getElementById("sp-cmd-smaller-font").removeAttribute("disabled");
+ },
+
+ _observers: [],
+
+ /**
+ * Add an observer for Scratchpad events.
+ *
+ * The observer implements IScratchpadObserver := {
+ * onReady: Called when the Scratchpad and its Editor are ready.
+ * Arguments: (Scratchpad aScratchpad)
+ * }
+ *
+ * All observer handlers are optional.
+ *
+ * @param IScratchpadObserver aObserver
+ * @see removeObserver
+ */
+ addObserver: function SP_addObserver(aObserver)
+ {
+ this._observers.push(aObserver);
+ },
+
+ /**
+ * Remove an observer for Scratchpad events.
+ *
+ * @param IScratchpadObserver aObserver
+ * @see addObserver
+ */
+ removeObserver: function SP_removeObserver(aObserver)
+ {
+ let index = this._observers.indexOf(aObserver);
+ if (index != -1) {
+ this._observers.splice(index, 1);
+ }
+ },
+
+ /**
+ * Trigger named handlers in Scratchpad observers.
+ *
+ * @param string aName
+ * Name of the handler to trigger.
+ * @param Array aArgs
+ * Optional array of arguments to pass to the observer(s).
+ * @see addObserver
+ */
+ _triggerObservers: function SP_triggerObservers(aName, aArgs)
+ {
+ // insert this Scratchpad instance as the first argument
+ if (!aArgs) {
+ aArgs = [this];
+ } else {
+ aArgs.unshift(this);
+ }
+
+ // trigger all observers that implement this named handler
+ for (let i = 0; i < this._observers.length; ++i) {
+ let observer = this._observers[i];
+ let handler = observer["on" + aName];
+ if (handler) {
+ handler.apply(observer, aArgs);
+ }
+ }
+ },
+
+ /**
+ * Opens the MDN documentation page for Scratchpad.
+ */
+ openDocumentationPage: function SP_openDocumentationPage()
+ {
+ let url = this.strings.GetStringFromName("help.openDocumentationPage");
+ this.browserWindow.openUILinkIn(url,"tab");
+ this.browserWindow.focus();
+ },
+};
+
+
+/**
+ * Represents the DebuggerClient connection to a specific tab as used by the
+ * Scratchpad.
+ *
+ * @param object aTab
+ * The tab to connect to.
+ */
+function ScratchpadTab(aTab)
+{
+ this._tab = aTab;
+}
+
+var scratchpadTargets = new WeakMap();
+
+/**
+ * Returns the object containing the DebuggerClient and WebConsoleClient for a
+ * given tab or window.
+ *
+ * @param object aSubject
+ * The tab or window to obtain the connection for.
+ * @return Promise
+ * The promise for the connection information.
+ */
+ScratchpadTab.consoleFor = function consoleFor(aSubject)
+{
+ if (!scratchpadTargets.has(aSubject)) {
+ scratchpadTargets.set(aSubject, new this(aSubject));
+ }
+ return scratchpadTargets.get(aSubject).connect(aSubject);
+};
+
+
+ScratchpadTab.prototype = {
+ /**
+ * The promise for the connection.
+ */
+ _connector: null,
+
+ /**
+ * Initialize a debugger client and connect it to the debugger server.
+ *
+ * @param object aSubject
+ * The tab or window to obtain the connection for.
+ * @return Promise
+ * The promise for the result of connecting to this tab or window.
+ */
+ connect: function ST_connect(aSubject)
+ {
+ if (this._connector) {
+ return this._connector;
+ }
+
+ let deferred = promise.defer();
+ this._connector = deferred.promise;
+
+ let connectTimer = setTimeout(() => {
+ deferred.reject({
+ error: "timeout",
+ message: Scratchpad.strings.GetStringFromName("connectionTimeout"),
+ });
+ }, REMOTE_TIMEOUT);
+
+ deferred.promise.then(() => clearTimeout(connectTimer));
+
+ this._attach(aSubject).then(aTarget => {
+ let consoleActor = aTarget.form.consoleActor;
+ let client = aTarget.client;
+ client.attachConsole(consoleActor, [], (aResponse, aWebConsoleClient) => {
+ if (aResponse.error) {
+ reportError("attachConsole", aResponse);
+ deferred.reject(aResponse);
+ }
+ else {
+ deferred.resolve({
+ webConsoleClient: aWebConsoleClient,
+ debuggerClient: client
+ });
+ }
+ });
+ });
+
+ return deferred.promise;
+ },
+
+ /**
+ * Attach to this tab.
+ *
+ * @param object aSubject
+ * The tab or window to obtain the connection for.
+ * @return Promise
+ * The promise for the TabTarget for this tab.
+ */
+ _attach: function ST__attach(aSubject)
+ {
+ let target = TargetFactory.forTab(this._tab);
+ target.once("close", () => {
+ if (scratchpadTargets) {
+ scratchpadTargets.delete(aSubject);
+ }
+ });
+ return target.makeRemote().then(() => target);
+ },
+};
+
+
+/**
+ * Represents the DebuggerClient connection to a specific window as used by the
+ * Scratchpad.
+ */
+function ScratchpadWindow() {}
+
+ScratchpadWindow.consoleFor = ScratchpadTab.consoleFor;
+
+ScratchpadWindow.prototype = Heritage.extend(ScratchpadTab.prototype, {
+ /**
+ * Attach to this window.
+ *
+ * @return Promise
+ * The promise for the target for this window.
+ */
+ _attach: function SW__attach()
+ {
+ if (!DebuggerServer.initialized) {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+ }
+ DebuggerServer.allowChromeProcess = true;
+
+ let client = new DebuggerClient(DebuggerServer.connectPipe());
+ return client.connect()
+ .then(() => client.getProcess())
+ .then(aResponse => {
+ return { form: aResponse.form, client: client };
+ });
+ }
+});
+
+
+function ScratchpadTarget(aTarget)
+{
+ this._target = aTarget;
+}
+
+ScratchpadTarget.consoleFor = ScratchpadTab.consoleFor;
+
+ScratchpadTarget.prototype = Heritage.extend(ScratchpadTab.prototype, {
+ _attach: function ST__attach()
+ {
+ if (this._target.isRemote) {
+ return promise.resolve(this._target);
+ }
+ return this._target.makeRemote().then(() => this._target);
+ }
+});
+
+
+/**
+ * Encapsulates management of the sidebar containing the VariablesView for
+ * object inspection.
+ */
+function ScratchpadSidebar(aScratchpad)
+{
+ // Make sure to decorate this object. ToolSidebar requires the parent
+ // panel to support event (emit) API.
+ EventEmitter.decorate(this);
+
+ let ToolSidebar = require("devtools/client/framework/sidebar").ToolSidebar;
+ let tabbox = document.querySelector("#scratchpad-sidebar");
+ this._sidebar = new ToolSidebar(tabbox, this, "scratchpad");
+ this._scratchpad = aScratchpad;
+}
+
+ScratchpadSidebar.prototype = {
+ /*
+ * The ToolSidebar for this sidebar.
+ */
+ _sidebar: null,
+
+ /*
+ * The VariablesView for this sidebar.
+ */
+ variablesView: null,
+
+ /*
+ * Whether the sidebar is currently shown.
+ */
+ visible: false,
+
+ /**
+ * Open the sidebar, if not open already, and populate it with the properties
+ * of the given object.
+ *
+ * @param string aString
+ * The string that was evaluated.
+ * @param object aObject
+ * The object to inspect, which is the aEvalString evaluation result.
+ * @return Promise
+ * A promise that will resolve once the sidebar is open.
+ */
+ open: function SS_open(aEvalString, aObject)
+ {
+ this.show();
+
+ let deferred = promise.defer();
+
+ let onTabReady = () => {
+ if (this.variablesView) {
+ this.variablesView.controller.releaseActors();
+ }
+ else {
+ let window = this._sidebar.getWindowForTab("variablesview");
+ let container = window.document.querySelector("#variables");
+
+ this.variablesView = new VariablesView(container, {
+ searchEnabled: true,
+ searchPlaceholder: this._scratchpad.strings
+ .GetStringFromName("propertiesFilterPlaceholder")
+ });
+
+ VariablesViewController.attach(this.variablesView, {
+ getEnvironmentClient: aGrip => {
+ return new EnvironmentClient(this._scratchpad.debuggerClient, aGrip);
+ },
+ getObjectClient: aGrip => {
+ return new ObjectClient(this._scratchpad.debuggerClient, aGrip);
+ },
+ getLongStringClient: aActor => {
+ return this._scratchpad.webConsoleClient.longString(aActor);
+ },
+ releaseActor: aActor => {
+ this._scratchpad.debuggerClient.release(aActor);
+ }
+ });
+ }
+ this._update(aObject).then(() => deferred.resolve());
+ };
+
+ if (this._sidebar.getCurrentTabID() == "variablesview") {
+ onTabReady();
+ }
+ else {
+ this._sidebar.once("variablesview-ready", onTabReady);
+ this._sidebar.addTab("variablesview", VARIABLES_VIEW_URL, {selected: true});
+ }
+
+ return deferred.promise;
+ },
+
+ /**
+ * Show the sidebar.
+ */
+ show: function SS_show()
+ {
+ if (!this.visible) {
+ this.visible = true;
+ this._sidebar.show();
+ }
+ },
+
+ /**
+ * Hide the sidebar.
+ */
+ hide: function SS_hide()
+ {
+ if (this.visible) {
+ this.visible = false;
+ this._sidebar.hide();
+ }
+ },
+
+ /**
+ * Destroy the sidebar.
+ *
+ * @return Promise
+ * The promise that resolves when the sidebar is destroyed.
+ */
+ destroy: function SS_destroy()
+ {
+ if (this.variablesView) {
+ this.variablesView.controller.releaseActors();
+ this.variablesView = null;
+ }
+ return this._sidebar.destroy();
+ },
+
+ /**
+ * Update the object currently inspected by the sidebar.
+ *
+ * @param any aValue
+ * The JS value to inspect in the sidebar.
+ * @return Promise
+ * A promise that resolves when the update completes.
+ */
+ _update: function SS__update(aValue)
+ {
+ let options, onlyEnumVisible;
+ if (VariablesView.isPrimitive({ value: aValue })) {
+ options = { rawObject: { value: aValue } };
+ onlyEnumVisible = true;
+ } else {
+ options = { objectActor: aValue };
+ onlyEnumVisible = false;
+ }
+ let view = this.variablesView;
+ view.onlyEnumVisible = onlyEnumVisible;
+ view.empty();
+ return view.controller.setSingleVariable(options).expanded;
+ }
+};
+
+
+/**
+ * Report an error coming over the remote debugger protocol.
+ *
+ * @param string aAction
+ * The name of the action or method that failed.
+ * @param object aResponse
+ * The response packet that contains the error.
+ */
+function reportError(aAction, aResponse)
+{
+ console.error(aAction + " failed: " + aResponse.error + " " +
+ aResponse.message);
+}
+
+
+/**
+ * The PreferenceObserver listens for preference changes while Scratchpad is
+ * running.
+ */
+var PreferenceObserver = {
+ _initialized: false,
+
+ init: function PO_init()
+ {
+ if (this._initialized) {
+ return;
+ }
+
+ this.branch = Services.prefs.getBranch("devtools.scratchpad.");
+ this.branch.addObserver("", this, false);
+ this._initialized = true;
+ },
+
+ observe: function PO_observe(aMessage, aTopic, aData)
+ {
+ if (aTopic != "nsPref:changed") {
+ return;
+ }
+
+ if (aData == "recentFilesMax") {
+ Scratchpad.handleRecentFileMaxChange();
+ }
+ else if (aData == "recentFilePaths") {
+ Scratchpad.populateRecentFilesMenu();
+ }
+ },
+
+ uninit: function PO_uninit() {
+ if (!this.branch) {
+ return;
+ }
+
+ this.branch.removeObserver("", this);
+ this.branch = null;
+ }
+};
+
+
+/**
+ * The CloseObserver listens for the last browser window closing and attempts to
+ * close the Scratchpad.
+ */
+var CloseObserver = {
+ init: function CO_init()
+ {
+ Services.obs.addObserver(this, "browser-lastwindow-close-requested", false);
+ },
+
+ observe: function CO_observe(aSubject)
+ {
+ if (Scratchpad.close()) {
+ this.uninit();
+ }
+ else {
+ aSubject.QueryInterface(Ci.nsISupportsPRBool);
+ aSubject.data = true;
+ }
+ },
+
+ uninit: function CO_uninit()
+ {
+ // Will throw exception if removeObserver is called twice.
+ if (this._uninited) {
+ return;
+ }
+
+ this._uninited = true;
+ Services.obs.removeObserver(this, "browser-lastwindow-close-requested",
+ false);
+ },
+};
+
+XPCOMUtils.defineLazyGetter(Scratchpad, "strings", function () {
+ return Services.strings.createBundle(SCRATCHPAD_L10N);
+});
+
+addEventListener("load", Scratchpad.onLoad.bind(Scratchpad), false);
+addEventListener("unload", Scratchpad.onUnload.bind(Scratchpad), false);
+addEventListener("close", Scratchpad.onClose.bind(Scratchpad), false);