scratchpad.js 71 KB

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