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