mirror of
https://github.com/roytam1/UXP.git
synced 2026-05-26 05:46:58 +00:00
d388e478a0
Vim control lines were re-introduced or not entirely cleaned up. This nukes them again. Removing from the rest of js, caps, chrome, config, devtools, docshell, image, intl. More to come.
2483 lines
71 KiB
JavaScript
2483 lines
71 KiB
JavaScript
/* 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", true);
|
|
|
|
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 });
|
|
|
|
// Add the commands controller for the source-editor.
|
|
this.editor.insertCommandsController();
|
|
|
|
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);
|