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