|
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 /* |
|
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 */ |
|
14 |
|
15 "use strict"; |
|
16 |
|
17 const Cu = Components.utils; |
|
18 const Cc = Components.classes; |
|
19 const Ci = Components.interfaces; |
|
20 |
|
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 |
|
28 |
|
29 const MAXIMUM_FONT_SIZE = 96; |
|
30 const MINIMUM_FONT_SIZE = 6; |
|
31 const NORMAL_FONT_SIZE = 12; |
|
32 |
|
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"; |
|
38 |
|
39 const VARIABLES_VIEW_URL = "chrome://browser/content/devtools/widgets/VariablesView.xul"; |
|
40 |
|
41 const require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require; |
|
42 |
|
43 const Telemetry = require("devtools/shared/telemetry"); |
|
44 const Editor = require("devtools/sourceeditor/editor"); |
|
45 const TargetFactory = require("devtools/framework/target").TargetFactory; |
|
46 |
|
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"); |
|
58 |
|
59 XPCOMUtils.defineLazyModuleGetter(this, "VariablesView", |
|
60 "resource:///modules/devtools/VariablesView.jsm"); |
|
61 |
|
62 XPCOMUtils.defineLazyModuleGetter(this, "VariablesViewController", |
|
63 "resource:///modules/devtools/VariablesViewController.jsm"); |
|
64 |
|
65 XPCOMUtils.defineLazyModuleGetter(this, "EnvironmentClient", |
|
66 "resource://gre/modules/devtools/dbg-client.jsm"); |
|
67 |
|
68 XPCOMUtils.defineLazyModuleGetter(this, "ObjectClient", |
|
69 "resource://gre/modules/devtools/dbg-client.jsm"); |
|
70 |
|
71 XPCOMUtils.defineLazyModuleGetter(this, "WebConsoleUtils", |
|
72 "resource://gre/modules/devtools/WebConsoleUtils.jsm"); |
|
73 |
|
74 XPCOMUtils.defineLazyModuleGetter(this, "DebuggerServer", |
|
75 "resource://gre/modules/devtools/dbg-server.jsm"); |
|
76 |
|
77 XPCOMUtils.defineLazyModuleGetter(this, "DebuggerClient", |
|
78 "resource://gre/modules/devtools/dbg-client.jsm"); |
|
79 |
|
80 XPCOMUtils.defineLazyGetter(this, "REMOTE_TIMEOUT", () => |
|
81 Services.prefs.getIntPref("devtools.debugger.remote-timeout")); |
|
82 |
|
83 XPCOMUtils.defineLazyModuleGetter(this, "ShortcutUtils", |
|
84 "resource://gre/modules/ShortcutUtils.jsm"); |
|
85 |
|
86 XPCOMUtils.defineLazyModuleGetter(this, "Reflect", |
|
87 "resource://gre/modules/reflect.jsm"); |
|
88 |
|
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"); |
|
93 |
|
94 /** |
|
95 * The scratchpad object handles the Scratchpad window functionality. |
|
96 */ |
|
97 var Scratchpad = { |
|
98 _instanceId: null, |
|
99 _initialWindowTitle: document.title, |
|
100 _dirty: false, |
|
101 |
|
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(); |
|
112 |
|
113 let obj = {}; |
|
114 let ch1 = aLine.charAt(0); |
|
115 let ch2 = aLine.charAt(1); |
|
116 |
|
117 if (ch1 !== "/" || (ch2 !== "*" && ch2 !== "/")) { |
|
118 return obj; |
|
119 } |
|
120 |
|
121 aLine = aLine |
|
122 .replace(/^\/\//, "") |
|
123 .replace(/^\/\*/, "") |
|
124 .replace(/\*\/$/, ""); |
|
125 |
|
126 aLine.split(",").forEach(pair => { |
|
127 let [key, val] = pair.split(":"); |
|
128 |
|
129 if (key && val) { |
|
130 obj[key.trim()] = val.trim(); |
|
131 } |
|
132 }); |
|
133 |
|
134 return obj; |
|
135 }, |
|
136 |
|
137 /** |
|
138 * Add the event listeners for popupshowing events. |
|
139 */ |
|
140 _setupPopupShowingListeners: function SP_setupPopupShowing() { |
|
141 let elementIDs = ['sp-menu_editpopup', 'scratchpad-text-popup']; |
|
142 |
|
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 }, |
|
154 |
|
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 } |
|
239 |
|
240 for (let command in commands) { |
|
241 let elem = document.getElementById(command); |
|
242 if (elem) { |
|
243 elem.addEventListener("command", commands[command]); |
|
244 } |
|
245 } |
|
246 }, |
|
247 |
|
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, |
|
259 |
|
260 /** |
|
261 * Tells if this Scratchpad is initialized and ready for use. |
|
262 * @boolean |
|
263 * @see addObserver |
|
264 */ |
|
265 initialized: false, |
|
266 |
|
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 }, |
|
275 |
|
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 }, |
|
286 |
|
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 }, |
|
295 |
|
296 /** |
|
297 * Hide the menu bar. |
|
298 */ |
|
299 hideMenu: function SP_hideMenu() |
|
300 { |
|
301 document.getElementById("sp-menubar").style.display = "none"; |
|
302 }, |
|
303 |
|
304 /** |
|
305 * Show the menu bar. |
|
306 */ |
|
307 showMenu: function SP_showMenu() |
|
308 { |
|
309 document.getElementById("sp-menubar").style.display = ""; |
|
310 }, |
|
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 /** |
|
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 }, |
|
342 |
|
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; |
|
350 |
|
351 if (this.dirty) |
|
352 title = "*" + title; |
|
353 |
|
354 document.title = title; |
|
355 }, |
|
356 |
|
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 }, |
|
374 |
|
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); |
|
386 |
|
387 this.dirty = !aState.saved; |
|
388 |
|
389 if (aState.executionContext == SCRATCHPAD_CONTEXT_BROWSER) |
|
390 this.setBrowserContext(); |
|
391 else |
|
392 this.setContentContext(); |
|
393 }, |
|
394 |
|
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 }, |
|
402 |
|
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 }, |
|
411 |
|
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 }, |
|
420 |
|
421 |
|
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 }, |
|
432 |
|
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 }, |
|
441 |
|
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 } |
|
463 |
|
464 let evalOptions = { url: this.uniqueName }; |
|
465 |
|
466 return connection.then(({ debuggerClient, webConsoleClient }) => { |
|
467 let deferred = promise.defer(); |
|
468 |
|
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); |
|
482 |
|
483 return deferred.promise; |
|
484 }); |
|
485 }, |
|
486 |
|
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 }, |
|
499 |
|
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); |
|
511 |
|
512 this.execute().then(([aString, aError, aResult]) => { |
|
513 let resolve = () => deferred.resolve([aString, aError, aResult]); |
|
514 |
|
515 if (aError) { |
|
516 this.writeAsErrorComment(aError.exception).then(resolve, reject); |
|
517 } |
|
518 else { |
|
519 this.editor.dropSelection(); |
|
520 resolve(); |
|
521 } |
|
522 }, reject); |
|
523 |
|
524 return deferred.promise; |
|
525 }, |
|
526 |
|
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); |
|
539 |
|
540 this.execute().then(([aString, aError, aResult]) => { |
|
541 let resolve = () => deferred.resolve([aString, aError, aResult]); |
|
542 |
|
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); |
|
554 |
|
555 return deferred.promise; |
|
556 }, |
|
557 |
|
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(); |
|
569 |
|
570 if (this.executionContext !== SCRATCHPAD_CONTEXT_CONTENT) { |
|
571 Cu.reportError(this.strings. |
|
572 GetStringFromName("scratchpadContext.invalid")); |
|
573 return; |
|
574 } |
|
575 |
|
576 let browser = this.gBrowser.selectedBrowser; |
|
577 |
|
578 this._reloadAndRunEvent = evt => { |
|
579 if (evt.target !== browser.contentDocument) { |
|
580 return; |
|
581 } |
|
582 |
|
583 browser.removeEventListener("load", this._reloadAndRunEvent, true); |
|
584 |
|
585 this.run().then(aResults => deferred.resolve(aResults)); |
|
586 }; |
|
587 |
|
588 browser.addEventListener("load", this._reloadAndRunEvent, true); |
|
589 browser.contentWindow.location.reload(); |
|
590 |
|
591 return deferred.promise; |
|
592 }, |
|
593 |
|
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); |
|
607 |
|
608 this.execute().then(([aString, aError, aResult]) => { |
|
609 let resolve = () => deferred.resolve([aString, aError, aResult]); |
|
610 |
|
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); |
|
631 |
|
632 return deferred.promise; |
|
633 }, |
|
634 |
|
635 _prettyPrintWorker: null, |
|
636 |
|
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"); |
|
644 |
|
645 this._prettyPrintWorker.addEventListener("error", ({ message, filename, lineno }) => { |
|
646 DevToolsUtils.reportException(message + " @ " + filename + ":" + lineno); |
|
647 }, false); |
|
648 } |
|
649 return this._prettyPrintWorker; |
|
650 }, |
|
651 |
|
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(); |
|
664 |
|
665 const onReply = ({ data }) => { |
|
666 if (data.id !== id) { |
|
667 return; |
|
668 } |
|
669 this.prettyPrintWorker.removeEventListener("message", onReply, false); |
|
670 |
|
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 }; |
|
680 |
|
681 this.prettyPrintWorker.addEventListener("message", onReply, false); |
|
682 this.prettyPrintWorker.postMessage({ |
|
683 id: id, |
|
684 url: "(scratchpad)", |
|
685 indent: tabsize, |
|
686 source: uglyText |
|
687 }); |
|
688 |
|
689 return deferred.promise; |
|
690 }, |
|
691 |
|
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 }, |
|
704 |
|
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; |
|
715 |
|
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 } |
|
721 |
|
722 if (aLoc.start.line == lineNumber) { |
|
723 return columnNumber >= aLoc.start.column; |
|
724 } |
|
725 |
|
726 if (aLoc.end.line == lineNumber) { |
|
727 return columnNumber <= aLoc.end.column; |
|
728 } |
|
729 |
|
730 return true; |
|
731 } |
|
732 |
|
733 return false; |
|
734 }, |
|
735 |
|
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; |
|
749 |
|
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 } |
|
764 |
|
765 return null; |
|
766 }, |
|
767 |
|
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; |
|
782 |
|
783 for (let line of aFullText.split(/\n/g)) { |
|
784 lineNumber++; |
|
785 |
|
786 if (singleLine && start.line === lineNumber) { |
|
787 functionText = line.slice(start.column, end.column); |
|
788 break; |
|
789 } |
|
790 |
|
791 if (start.line === lineNumber) { |
|
792 functionText += line.slice(start.column) + "\n"; |
|
793 continue; |
|
794 } |
|
795 |
|
796 if (end.line === lineNumber) { |
|
797 functionText += line.slice(0, end.column); |
|
798 break; |
|
799 } |
|
800 |
|
801 if (start.line < lineNumber && end.line > lineNumber) { |
|
802 functionText += line + "\n"; |
|
803 } |
|
804 } |
|
805 |
|
806 return functionText; |
|
807 }, |
|
808 |
|
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 } |
|
820 |
|
821 const cursorPos = this.editor.getCursor(); |
|
822 const funcStatement = this._findTopLevelFunction(ast, cursorPos); |
|
823 if (!funcStatement) { |
|
824 return promise.resolve([text, undefined, undefined]); |
|
825 } |
|
826 |
|
827 let functionText = this._getFunctionText(funcStatement, text); |
|
828 |
|
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 } |
|
836 |
|
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 }; |
|
847 |
|
848 const marker = this.editor.markText(from, to, "eval-text"); |
|
849 setTimeout(() => marker.clear(), EVAL_FUNCTION_TIMEOUT); |
|
850 |
|
851 return this.evaluate(functionText); |
|
852 }, |
|
853 |
|
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(); |
|
867 |
|
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 } |
|
883 |
|
884 return deferred.promise.then(aComment => { |
|
885 this.writeAsComment(aComment); |
|
886 }); |
|
887 }, |
|
888 |
|
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*/"; |
|
898 |
|
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 } |
|
906 |
|
907 let text = this.editor.getText(); |
|
908 this.editor.setText(text + value); |
|
909 |
|
910 let [ from, to ] = this.editor.getPosition(text.length, (text + value).length); |
|
911 this.editor.setSelection(from, to); |
|
912 }, |
|
913 |
|
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(); |
|
924 |
|
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 } |
|
949 |
|
950 let { ownProperties, safeGetterValues } = aResponse; |
|
951 let error = Object.create(null); |
|
952 |
|
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 } |
|
957 |
|
958 for (let key of Object.keys(ownProperties)) { |
|
959 error[key] = ownProperties[key].value; |
|
960 } |
|
961 |
|
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 } |
|
976 |
|
977 stack = stack ? "\n" + stack.replace(/\n$/, "") : ""; |
|
978 |
|
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 } |
|
997 |
|
998 return deferred.promise.then(aMessage => { |
|
999 console.error(aMessage); |
|
1000 this.writeAsComment("Exception: " + aMessage); |
|
1001 }); |
|
1002 }, |
|
1003 |
|
1004 // Menu Operations |
|
1005 |
|
1006 /** |
|
1007 * Open a new Scratchpad window. |
|
1008 * |
|
1009 * @return nsIWindow |
|
1010 */ |
|
1011 openScratchpad: function SP_openScratchpad() |
|
1012 { |
|
1013 return ScratchpadManager.openScratchpad(); |
|
1014 }, |
|
1015 |
|
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 } |
|
1039 |
|
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 }); |
|
1055 |
|
1056 }, |
|
1057 |
|
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"; |
|
1077 |
|
1078 NetUtil.asyncFetch(channel, (aInputStream, aStatus) => { |
|
1079 let content = null; |
|
1080 |
|
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); |
|
1088 |
|
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); |
|
1093 |
|
1094 if (chrome && modeline["-sp-context"] === "browser") { |
|
1095 this.setBrowserContext(); |
|
1096 } |
|
1097 |
|
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 } |
|
1106 |
|
1107 if (aCallback) { |
|
1108 aCallback.call(this, aStatus, content); |
|
1109 } |
|
1110 }); |
|
1111 }, |
|
1112 |
|
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 } |
|
1127 |
|
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 } |
|
1138 |
|
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); |
|
1146 |
|
1147 this.clearFiles(aIndex, 1); |
|
1148 return; |
|
1149 } |
|
1150 |
|
1151 this.setFilename(file.path); |
|
1152 this.importFromFile(file, false); |
|
1153 this.setRecentFile(file); |
|
1154 } |
|
1155 }); |
|
1156 }; |
|
1157 |
|
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 }, |
|
1174 |
|
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 = []; |
|
1185 |
|
1186 // WARNING: Do not use getCharPref here, it doesn't play nicely with |
|
1187 // Unicode strings. |
|
1188 |
|
1189 if (branch.prefHasUserValue("recentFilePaths")) { |
|
1190 let data = branch.getComplexValue("recentFilePaths", |
|
1191 Ci.nsISupportsString).data; |
|
1192 filePaths = JSON.parse(data); |
|
1193 } |
|
1194 |
|
1195 return filePaths; |
|
1196 }, |
|
1197 |
|
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 } |
|
1210 |
|
1211 let filePaths = this.getRecentFiles(); |
|
1212 let filesCount = filePaths.length; |
|
1213 let pathIndex = filePaths.indexOf(aFile.path); |
|
1214 |
|
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 } |
|
1225 |
|
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 } |
|
1235 |
|
1236 filePaths.push(aFile.path); |
|
1237 |
|
1238 // WARNING: Do not use setCharPref here, it doesn't play nicely with |
|
1239 // Unicode strings. |
|
1240 |
|
1241 let str = Cc["@mozilla.org/supports-string;1"] |
|
1242 .createInstance(Ci.nsISupportsString); |
|
1243 str.data = JSON.stringify(filePaths); |
|
1244 |
|
1245 let branch = Services.prefs.getBranch("devtools.scratchpad."); |
|
1246 branch.setComplexValue("recentFilePaths", |
|
1247 Ci.nsISupportsString, str); |
|
1248 }, |
|
1249 |
|
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"); |
|
1257 |
|
1258 if (maxRecent < 1) { |
|
1259 recentFilesMenu.setAttribute("hidden", true); |
|
1260 return; |
|
1261 } |
|
1262 |
|
1263 let recentFilesPopup = recentFilesMenu.firstChild; |
|
1264 let filePaths = this.getRecentFiles(); |
|
1265 let filename = this.getState().filename; |
|
1266 |
|
1267 recentFilesMenu.setAttribute("disabled", true); |
|
1268 while (recentFilesPopup.hasChildNodes()) { |
|
1269 recentFilesPopup.removeChild(recentFilesPopup.firstChild); |
|
1270 } |
|
1271 |
|
1272 if (filePaths.length > 0) { |
|
1273 recentFilesMenu.removeAttribute("disabled"); |
|
1274 |
|
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]); |
|
1280 |
|
1281 if (filePaths[i] === filename) { |
|
1282 menuitem.setAttribute("checked", true); |
|
1283 menuitem.setAttribute("disabled", true); |
|
1284 } |
|
1285 |
|
1286 menuitem.addEventListener("command", Scratchpad.openFile.bind(Scratchpad, i)); |
|
1287 recentFilesPopup.appendChild(menuitem); |
|
1288 } |
|
1289 |
|
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 }, |
|
1300 |
|
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); |
|
1313 |
|
1314 // WARNING: Do not use setCharPref here, it doesn't play nicely with |
|
1315 // Unicode strings. |
|
1316 |
|
1317 let str = Cc["@mozilla.org/supports-string;1"] |
|
1318 .createInstance(Ci.nsISupportsString); |
|
1319 str.data = JSON.stringify(filePaths); |
|
1320 |
|
1321 let branch = Services.prefs.getBranch("devtools.scratchpad."); |
|
1322 branch.setComplexValue("recentFilePaths", |
|
1323 Ci.nsISupportsString, str); |
|
1324 }, |
|
1325 |
|
1326 /** |
|
1327 * Clear all recent files. |
|
1328 */ |
|
1329 clearRecentFiles: function SP_clearRecentFiles() |
|
1330 { |
|
1331 Services.prefs.clearUserPref("devtools.scratchpad.recentFilePaths"); |
|
1332 }, |
|
1333 |
|
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"); |
|
1341 |
|
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 } |
|
1350 |
|
1351 menu.removeAttribute("hidden"); |
|
1352 } |
|
1353 |
|
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 } |
|
1372 |
|
1373 let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile); |
|
1374 file.initWithPath(this.filename); |
|
1375 |
|
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 }, |
|
1387 |
|
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 }; |
|
1411 |
|
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 }, |
|
1419 |
|
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); |
|
1430 |
|
1431 if (!file.exists()) { |
|
1432 return; |
|
1433 } |
|
1434 |
|
1435 this.importFromFile(file, false, (aStatus, aContent) => { |
|
1436 if (aCallback) { |
|
1437 aCallback(aStatus); |
|
1438 } |
|
1439 }); |
|
1440 }, |
|
1441 |
|
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; |
|
1458 |
|
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 } |
|
1467 |
|
1468 return; |
|
1469 } |
|
1470 if (button == BUTTON_POSITION_REVERT) { |
|
1471 this.revertFile(aStatus => { |
|
1472 if (aCallback) { |
|
1473 aCallback(true, aStatus); |
|
1474 } |
|
1475 }); |
|
1476 |
|
1477 return; |
|
1478 } |
|
1479 } |
|
1480 if (aCallback) { |
|
1481 aCallback(false); |
|
1482 } |
|
1483 }, |
|
1484 |
|
1485 /** |
|
1486 * Open the Error Console. |
|
1487 */ |
|
1488 openErrorConsole: function SP_openErrorConsole() |
|
1489 { |
|
1490 this.browserWindow.HUDService.toggleBrowserConsole(); |
|
1491 }, |
|
1492 |
|
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 }, |
|
1502 |
|
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 } |
|
1511 |
|
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 }, |
|
1519 |
|
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 } |
|
1528 |
|
1529 let browser = document.getElementById("sp-menu-browser"); |
|
1530 let reloadAndRun = document.getElementById("sp-cmd-reloadAndRun"); |
|
1531 |
|
1532 document.getElementById("sp-menu-content").removeAttribute("checked"); |
|
1533 reloadAndRun.setAttribute("disabled", true); |
|
1534 browser.setAttribute("checked", true); |
|
1535 |
|
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 }, |
|
1544 |
|
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 }, |
|
1557 |
|
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 } |
|
1569 |
|
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 } |
|
1579 |
|
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); |
|
1586 |
|
1587 let args = window.arguments; |
|
1588 let state = null; |
|
1589 |
|
1590 if (args && args[0] instanceof Ci.nsIDialogParamBlock) { |
|
1591 args = args[0]; |
|
1592 this._instanceId = args.GetString(0); |
|
1593 |
|
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 } |
|
1603 |
|
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 }; |
|
1612 |
|
1613 this.editor = new Editor(config); |
|
1614 this.editor.appendTo(document.querySelector("#scratchpad-editor")).then(() => { |
|
1615 var lines = initialText.split("\n"); |
|
1616 |
|
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 }); |
|
1621 |
|
1622 if (state) |
|
1623 this.dirty = !state.saved; |
|
1624 |
|
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 }, |
|
1634 |
|
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(); |
|
1644 |
|
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 }, |
|
1652 |
|
1653 /** |
|
1654 * Undo the last action of the user. |
|
1655 */ |
|
1656 undo: function SP_undo() |
|
1657 { |
|
1658 this.editor.undo(); |
|
1659 }, |
|
1660 |
|
1661 /** |
|
1662 * Redo the previously undone action. |
|
1663 */ |
|
1664 redo: function SP_redo() |
|
1665 { |
|
1666 this.editor.redo(); |
|
1667 }, |
|
1668 |
|
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 } |
|
1680 |
|
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 } |
|
1686 |
|
1687 PreferenceObserver.uninit(); |
|
1688 CloseObserver.uninit(); |
|
1689 |
|
1690 this.editor.off("change", this._onChanged); |
|
1691 this.editor.destroy(); |
|
1692 this.editor = null; |
|
1693 |
|
1694 if (this._sidebar) { |
|
1695 this._sidebar.destroy(); |
|
1696 this._sidebar = null; |
|
1697 } |
|
1698 |
|
1699 if (this._prettyPrintWorker) { |
|
1700 this._prettyPrintWorker.terminate(); |
|
1701 this._prettyPrintWorker = null; |
|
1702 } |
|
1703 |
|
1704 scratchpadTargets = null; |
|
1705 this.webConsoleClient = null; |
|
1706 this.debuggerClient = null; |
|
1707 this.initialized = false; |
|
1708 }, |
|
1709 |
|
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; |
|
1730 |
|
1731 let button = ps.confirmEx(window, |
|
1732 this.strings.GetStringFromName("confirmClose.title"), |
|
1733 this.strings.GetStringFromName("confirmClose"), |
|
1734 flags, null, null, null, null, {}); |
|
1735 |
|
1736 if (button == BUTTON_POSITION_CANCEL) { |
|
1737 if (aCallback) { |
|
1738 aCallback(false, false); |
|
1739 } |
|
1740 return false; |
|
1741 } |
|
1742 |
|
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 } |
|
1752 |
|
1753 if (aCallback) { |
|
1754 aCallback(true, false); |
|
1755 } |
|
1756 return true; |
|
1757 }, |
|
1758 |
|
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 }, |
|
1773 |
|
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; |
|
1784 |
|
1785 this.promptSave((aShouldClose, aSaved, aStatus) => { |
|
1786 shouldClose = aShouldClose; |
|
1787 if (aSaved && !Components.isSuccessCode(aStatus)) { |
|
1788 shouldClose = false; |
|
1789 } |
|
1790 |
|
1791 if (shouldClose) { |
|
1792 telemetry.toolClosed("scratchpad"); |
|
1793 window.close(); |
|
1794 } |
|
1795 |
|
1796 if (aCallback) { |
|
1797 aCallback(shouldClose); |
|
1798 } |
|
1799 }); |
|
1800 |
|
1801 return shouldClose; |
|
1802 }, |
|
1803 |
|
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 }, |
|
1812 |
|
1813 /** |
|
1814 * Increase the editor's font size by 1 px. |
|
1815 */ |
|
1816 increaseFontSize: function SP_increaseFontSize() |
|
1817 { |
|
1818 let size = this.editor.getFontSize(); |
|
1819 |
|
1820 if (size < MAXIMUM_FONT_SIZE) { |
|
1821 this.editor.setFontSize(size + 1); |
|
1822 } |
|
1823 }, |
|
1824 |
|
1825 /** |
|
1826 * Decrease the editor's font size by 1 px. |
|
1827 */ |
|
1828 decreaseFontSize: function SP_decreaseFontSize() |
|
1829 { |
|
1830 let size = this.editor.getFontSize(); |
|
1831 |
|
1832 if (size > MINIMUM_FONT_SIZE) { |
|
1833 this.editor.setFontSize(size - 1); |
|
1834 } |
|
1835 }, |
|
1836 |
|
1837 /** |
|
1838 * Restore the editor's original font size. |
|
1839 */ |
|
1840 normalFontSize: function SP_normalFontSize() |
|
1841 { |
|
1842 this.editor.setFontSize(NORMAL_FONT_SIZE); |
|
1843 }, |
|
1844 |
|
1845 _observers: [], |
|
1846 |
|
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 }, |
|
1864 |
|
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 }, |
|
1878 |
|
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 } |
|
1896 |
|
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 }, |
|
1906 |
|
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 }; |
|
1918 |
|
1919 |
|
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 } |
|
1931 |
|
1932 let scratchpadTargets = new WeakMap(); |
|
1933 |
|
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 }; |
|
1950 |
|
1951 |
|
1952 ScratchpadTab.prototype = { |
|
1953 /** |
|
1954 * The promise for the connection. |
|
1955 */ |
|
1956 _connector: null, |
|
1957 |
|
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 } |
|
1969 |
|
1970 let deferred = promise.defer(); |
|
1971 this._connector = deferred.promise; |
|
1972 |
|
1973 let connectTimer = setTimeout(() => { |
|
1974 deferred.reject({ |
|
1975 error: "timeout", |
|
1976 message: Scratchpad.strings.GetStringFromName("connectionTimeout"), |
|
1977 }); |
|
1978 }, REMOTE_TIMEOUT); |
|
1979 |
|
1980 deferred.promise.then(() => clearTimeout(connectTimer)); |
|
1981 |
|
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 }); |
|
1998 |
|
1999 return deferred.promise; |
|
2000 }, |
|
2001 |
|
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 }; |
|
2014 |
|
2015 |
|
2016 /** |
|
2017 * Represents the DebuggerClient connection to a specific window as used by the |
|
2018 * Scratchpad. |
|
2019 */ |
|
2020 function ScratchpadWindow() {} |
|
2021 |
|
2022 ScratchpadWindow.consoleFor = ScratchpadTab.consoleFor; |
|
2023 |
|
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(); |
|
2034 |
|
2035 if (!DebuggerServer.initialized) { |
|
2036 DebuggerServer.init(); |
|
2037 DebuggerServer.addBrowserActors(); |
|
2038 } |
|
2039 |
|
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 }); |
|
2052 |
|
2053 return deferred.promise; |
|
2054 } |
|
2055 }); |
|
2056 |
|
2057 |
|
2058 function ScratchpadTarget(aTarget) |
|
2059 { |
|
2060 this._target = aTarget; |
|
2061 } |
|
2062 |
|
2063 ScratchpadTarget.consoleFor = ScratchpadTab.consoleFor; |
|
2064 |
|
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 }); |
|
2074 |
|
2075 |
|
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 } |
|
2087 |
|
2088 ScratchpadSidebar.prototype = { |
|
2089 /* |
|
2090 * The ToolSidebar for this sidebar. |
|
2091 */ |
|
2092 _sidebar: null, |
|
2093 |
|
2094 /* |
|
2095 * The VariablesView for this sidebar. |
|
2096 */ |
|
2097 variablesView: null, |
|
2098 |
|
2099 /* |
|
2100 * Whether the sidebar is currently shown. |
|
2101 */ |
|
2102 visible: false, |
|
2103 |
|
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(); |
|
2118 |
|
2119 let deferred = promise.defer(); |
|
2120 |
|
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"); |
|
2128 |
|
2129 this.variablesView = new VariablesView(container, { |
|
2130 searchEnabled: true, |
|
2131 searchPlaceholder: this._scratchpad.strings |
|
2132 .GetStringFromName("propertiesFilterPlaceholder") |
|
2133 }); |
|
2134 |
|
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 }; |
|
2152 |
|
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 } |
|
2160 |
|
2161 return deferred.promise; |
|
2162 }, |
|
2163 |
|
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 }, |
|
2174 |
|
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 }, |
|
2185 |
|
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 }, |
|
2200 |
|
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 }; |
|
2217 |
|
2218 |
|
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 } |
|
2232 |
|
2233 |
|
2234 /** |
|
2235 * The PreferenceObserver listens for preference changes while Scratchpad is |
|
2236 * running. |
|
2237 */ |
|
2238 var PreferenceObserver = { |
|
2239 _initialized: false, |
|
2240 |
|
2241 init: function PO_init() |
|
2242 { |
|
2243 if (this._initialized) { |
|
2244 return; |
|
2245 } |
|
2246 |
|
2247 this.branch = Services.prefs.getBranch("devtools.scratchpad."); |
|
2248 this.branch.addObserver("", this, false); |
|
2249 this._initialized = true; |
|
2250 }, |
|
2251 |
|
2252 observe: function PO_observe(aMessage, aTopic, aData) |
|
2253 { |
|
2254 if (aTopic != "nsPref:changed") { |
|
2255 return; |
|
2256 } |
|
2257 |
|
2258 if (aData == "recentFilesMax") { |
|
2259 Scratchpad.handleRecentFileMaxChange(); |
|
2260 } |
|
2261 else if (aData == "recentFilePaths") { |
|
2262 Scratchpad.populateRecentFilesMenu(); |
|
2263 } |
|
2264 }, |
|
2265 |
|
2266 uninit: function PO_uninit () { |
|
2267 if (!this.branch) { |
|
2268 return; |
|
2269 } |
|
2270 |
|
2271 this.branch.removeObserver("", this); |
|
2272 this.branch = null; |
|
2273 } |
|
2274 }; |
|
2275 |
|
2276 |
|
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 }, |
|
2286 |
|
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 }, |
|
2297 |
|
2298 uninit: function CO_uninit() |
|
2299 { |
|
2300 Services.obs.removeObserver(this, "browser-lastwindow-close-requested", |
|
2301 false); |
|
2302 }, |
|
2303 }; |
|
2304 |
|
2305 XPCOMUtils.defineLazyGetter(Scratchpad, "strings", function () { |
|
2306 return Services.strings.createBundle(SCRATCHPAD_L10N); |
|
2307 }); |
|
2308 |
|
2309 addEventListener("load", Scratchpad.onLoad.bind(Scratchpad), false); |
|
2310 addEventListener("unload", Scratchpad.onUnload.bind(Scratchpad), false); |
|
2311 addEventListener("close", Scratchpad.onClose.bind(Scratchpad), false); |