|
1 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
2 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 |
|
5 "use strict"; |
|
6 |
|
7 const { Cc, Ci, Cu } = require("chrome"); |
|
8 const gcli = require("gcli/index"); |
|
9 |
|
10 loader.lazyImporter(this, "gDevTools", "resource:///modules/devtools/gDevTools.jsm"); |
|
11 |
|
12 /** |
|
13 * The commands and converters that are exported to GCLI |
|
14 */ |
|
15 exports.items = []; |
|
16 |
|
17 /** |
|
18 * Utility to get access to the current breakpoint list. |
|
19 * |
|
20 * @param DebuggerPanel dbg |
|
21 * The debugger panel. |
|
22 * @return array |
|
23 * An array of objects, one for each breakpoint, where each breakpoint |
|
24 * object has the following properties: |
|
25 * - url: the URL of the source file. |
|
26 * - label: a unique string identifier designed to be user visible. |
|
27 * - lineNumber: the line number of the breakpoint in the source file. |
|
28 * - lineText: the text of the line at the breakpoint. |
|
29 * - truncatedLineText: lineText truncated to MAX_LINE_TEXT_LENGTH. |
|
30 */ |
|
31 function getAllBreakpoints(dbg) { |
|
32 let breakpoints = []; |
|
33 let sources = dbg._view.Sources; |
|
34 let { trimUrlLength: trim } = dbg.panelWin.SourceUtils; |
|
35 |
|
36 for (let source of sources) { |
|
37 for (let { attachment: breakpoint } of source) { |
|
38 breakpoints.push({ |
|
39 url: source.value, |
|
40 label: source.attachment.label + ":" + breakpoint.line, |
|
41 lineNumber: breakpoint.line, |
|
42 lineText: breakpoint.text, |
|
43 truncatedLineText: trim(breakpoint.text, MAX_LINE_TEXT_LENGTH, "end") |
|
44 }); |
|
45 } |
|
46 } |
|
47 |
|
48 return breakpoints; |
|
49 } |
|
50 |
|
51 /** |
|
52 * 'break' command |
|
53 */ |
|
54 exports.items.push({ |
|
55 name: "break", |
|
56 description: gcli.lookup("breakDesc"), |
|
57 manual: gcli.lookup("breakManual") |
|
58 }); |
|
59 |
|
60 /** |
|
61 * 'break list' command |
|
62 */ |
|
63 exports.items.push({ |
|
64 name: "break list", |
|
65 description: gcli.lookup("breaklistDesc"), |
|
66 returnType: "breakpoints", |
|
67 exec: function(args, context) { |
|
68 let dbg = getPanel(context, "jsdebugger", { ensureOpened: true }); |
|
69 return dbg.then(getAllBreakpoints); |
|
70 } |
|
71 }); |
|
72 |
|
73 exports.items.push({ |
|
74 item: "converter", |
|
75 from: "breakpoints", |
|
76 to: "view", |
|
77 exec: function(breakpoints, context) { |
|
78 let dbg = getPanel(context, "jsdebugger"); |
|
79 if (dbg && breakpoints.length) { |
|
80 return context.createView({ |
|
81 html: breakListHtml, |
|
82 data: { |
|
83 breakpoints: breakpoints, |
|
84 onclick: context.update, |
|
85 ondblclick: context.updateExec |
|
86 } |
|
87 }); |
|
88 } else { |
|
89 return context.createView({ |
|
90 html: "<p>${message}</p>", |
|
91 data: { message: gcli.lookup("breaklistNone") } |
|
92 }); |
|
93 } |
|
94 } |
|
95 }); |
|
96 |
|
97 var breakListHtml = "" + |
|
98 "<table>" + |
|
99 " <thead>" + |
|
100 " <th>Source</th>" + |
|
101 " <th>Line</th>" + |
|
102 " <th>Actions</th>" + |
|
103 " </thead>" + |
|
104 " <tbody>" + |
|
105 " <tr foreach='breakpoint in ${breakpoints}'>" + |
|
106 " <td class='gcli-breakpoint-label'>${breakpoint.label}</td>" + |
|
107 " <td class='gcli-breakpoint-lineText'>" + |
|
108 " ${breakpoint.truncatedLineText}" + |
|
109 " </td>" + |
|
110 " <td>" + |
|
111 " <span class='gcli-out-shortcut'" + |
|
112 " data-command='break del ${breakpoint.label}'" + |
|
113 " onclick='${onclick}'" + |
|
114 " ondblclick='${ondblclick}'>" + |
|
115 " " + gcli.lookup("breaklistOutRemove") + "</span>" + |
|
116 " </td>" + |
|
117 " </tr>" + |
|
118 " </tbody>" + |
|
119 "</table>" + |
|
120 ""; |
|
121 |
|
122 var MAX_LINE_TEXT_LENGTH = 30; |
|
123 var MAX_LABEL_LENGTH = 20; |
|
124 |
|
125 /** |
|
126 * 'break add' command |
|
127 */ |
|
128 exports.items.push({ |
|
129 name: "break add", |
|
130 description: gcli.lookup("breakaddDesc"), |
|
131 manual: gcli.lookup("breakaddManual") |
|
132 }); |
|
133 |
|
134 /** |
|
135 * 'break add line' command |
|
136 */ |
|
137 exports.items.push({ |
|
138 name: "break add line", |
|
139 description: gcli.lookup("breakaddlineDesc"), |
|
140 params: [ |
|
141 { |
|
142 name: "file", |
|
143 type: { |
|
144 name: "selection", |
|
145 data: function(context) { |
|
146 let dbg = getPanel(context, "jsdebugger"); |
|
147 if (dbg) { |
|
148 return dbg._view.Sources.values; |
|
149 } |
|
150 return []; |
|
151 } |
|
152 }, |
|
153 description: gcli.lookup("breakaddlineFileDesc") |
|
154 }, |
|
155 { |
|
156 name: "line", |
|
157 type: { name: "number", min: 1, step: 10 }, |
|
158 description: gcli.lookup("breakaddlineLineDesc") |
|
159 } |
|
160 ], |
|
161 returnType: "string", |
|
162 exec: function(args, context) { |
|
163 let dbg = getPanel(context, "jsdebugger"); |
|
164 if (!dbg) { |
|
165 return gcli.lookup("debuggerStopped"); |
|
166 } |
|
167 |
|
168 let deferred = context.defer(); |
|
169 let position = { url: args.file, line: args.line }; |
|
170 |
|
171 dbg.addBreakpoint(position).then(() => { |
|
172 deferred.resolve(gcli.lookup("breakaddAdded")); |
|
173 }, aError => { |
|
174 deferred.resolve(gcli.lookupFormat("breakaddFailed", [aError])); |
|
175 }); |
|
176 |
|
177 return deferred.promise; |
|
178 } |
|
179 }); |
|
180 |
|
181 /** |
|
182 * 'break del' command |
|
183 */ |
|
184 exports.items.push({ |
|
185 name: "break del", |
|
186 description: gcli.lookup("breakdelDesc"), |
|
187 params: [ |
|
188 { |
|
189 name: "breakpoint", |
|
190 type: { |
|
191 name: "selection", |
|
192 lookup: function(context) { |
|
193 let dbg = getPanel(context, "jsdebugger"); |
|
194 if (!dbg) { |
|
195 return []; |
|
196 } |
|
197 return getAllBreakpoints(dbg).map(breakpoint => ({ |
|
198 name: breakpoint.label, |
|
199 value: breakpoint, |
|
200 description: breakpoint.truncatedLineText |
|
201 })); |
|
202 } |
|
203 }, |
|
204 description: gcli.lookup("breakdelBreakidDesc") |
|
205 } |
|
206 ], |
|
207 returnType: "string", |
|
208 exec: function(args, context) { |
|
209 let dbg = getPanel(context, "jsdebugger"); |
|
210 if (!dbg) { |
|
211 return gcli.lookup("debuggerStopped"); |
|
212 } |
|
213 |
|
214 let deferred = context.defer(); |
|
215 let position = { url: args.breakpoint.url, line: args.breakpoint.lineNumber }; |
|
216 |
|
217 dbg.removeBreakpoint(position).then(() => { |
|
218 deferred.resolve(gcli.lookup("breakdelRemoved")); |
|
219 }, () => { |
|
220 deferred.resolve(gcli.lookup("breakNotFound")); |
|
221 }); |
|
222 |
|
223 return deferred.promise; |
|
224 } |
|
225 }); |
|
226 |
|
227 /** |
|
228 * 'dbg' command |
|
229 */ |
|
230 exports.items.push({ |
|
231 name: "dbg", |
|
232 description: gcli.lookup("dbgDesc"), |
|
233 manual: gcli.lookup("dbgManual") |
|
234 }); |
|
235 |
|
236 /** |
|
237 * 'dbg open' command |
|
238 */ |
|
239 exports.items.push({ |
|
240 name: "dbg open", |
|
241 description: gcli.lookup("dbgOpen"), |
|
242 params: [], |
|
243 exec: function(args, context) { |
|
244 let target = context.environment.target; |
|
245 return gDevTools.showToolbox(target, "jsdebugger").then(() => null); |
|
246 } |
|
247 }); |
|
248 |
|
249 /** |
|
250 * 'dbg close' command |
|
251 */ |
|
252 exports.items.push({ |
|
253 name: "dbg close", |
|
254 description: gcli.lookup("dbgClose"), |
|
255 params: [], |
|
256 exec: function(args, context) { |
|
257 if (!getPanel(context, "jsdebugger")) { |
|
258 return; |
|
259 } |
|
260 let target = context.environment.target; |
|
261 return gDevTools.closeToolbox(target).then(() => null); |
|
262 } |
|
263 }); |
|
264 |
|
265 /** |
|
266 * 'dbg interrupt' command |
|
267 */ |
|
268 exports.items.push({ |
|
269 name: "dbg interrupt", |
|
270 description: gcli.lookup("dbgInterrupt"), |
|
271 params: [], |
|
272 exec: function(args, context) { |
|
273 let dbg = getPanel(context, "jsdebugger"); |
|
274 if (!dbg) { |
|
275 return gcli.lookup("debuggerStopped"); |
|
276 } |
|
277 |
|
278 let controller = dbg._controller; |
|
279 let thread = controller.activeThread; |
|
280 if (!thread.paused) { |
|
281 thread.interrupt(); |
|
282 } |
|
283 } |
|
284 }); |
|
285 |
|
286 /** |
|
287 * 'dbg continue' command |
|
288 */ |
|
289 exports.items.push({ |
|
290 name: "dbg continue", |
|
291 description: gcli.lookup("dbgContinue"), |
|
292 params: [], |
|
293 exec: function(args, context) { |
|
294 let dbg = getPanel(context, "jsdebugger"); |
|
295 if (!dbg) { |
|
296 return gcli.lookup("debuggerStopped"); |
|
297 } |
|
298 |
|
299 let controller = dbg._controller; |
|
300 let thread = controller.activeThread; |
|
301 if (thread.paused) { |
|
302 thread.resume(); |
|
303 } |
|
304 } |
|
305 }); |
|
306 |
|
307 /** |
|
308 * 'dbg step' command |
|
309 */ |
|
310 exports.items.push({ |
|
311 name: "dbg step", |
|
312 description: gcli.lookup("dbgStepDesc"), |
|
313 manual: gcli.lookup("dbgStepManual") |
|
314 }); |
|
315 |
|
316 /** |
|
317 * 'dbg step over' command |
|
318 */ |
|
319 exports.items.push({ |
|
320 name: "dbg step over", |
|
321 description: gcli.lookup("dbgStepOverDesc"), |
|
322 params: [], |
|
323 exec: function(args, context) { |
|
324 let dbg = getPanel(context, "jsdebugger"); |
|
325 if (!dbg) { |
|
326 return gcli.lookup("debuggerStopped"); |
|
327 } |
|
328 |
|
329 let controller = dbg._controller; |
|
330 let thread = controller.activeThread; |
|
331 if (thread.paused) { |
|
332 thread.stepOver(); |
|
333 } |
|
334 } |
|
335 }); |
|
336 |
|
337 /** |
|
338 * 'dbg step in' command |
|
339 */ |
|
340 exports.items.push({ |
|
341 name: 'dbg step in', |
|
342 description: gcli.lookup("dbgStepInDesc"), |
|
343 params: [], |
|
344 exec: function(args, context) { |
|
345 let dbg = getPanel(context, "jsdebugger"); |
|
346 if (!dbg) { |
|
347 return gcli.lookup("debuggerStopped"); |
|
348 } |
|
349 |
|
350 let controller = dbg._controller; |
|
351 let thread = controller.activeThread; |
|
352 if (thread.paused) { |
|
353 thread.stepIn(); |
|
354 } |
|
355 } |
|
356 }); |
|
357 |
|
358 /** |
|
359 * 'dbg step over' command |
|
360 */ |
|
361 exports.items.push({ |
|
362 name: 'dbg step out', |
|
363 description: gcli.lookup("dbgStepOutDesc"), |
|
364 params: [], |
|
365 exec: function(args, context) { |
|
366 let dbg = getPanel(context, "jsdebugger"); |
|
367 if (!dbg) { |
|
368 return gcli.lookup("debuggerStopped"); |
|
369 } |
|
370 |
|
371 let controller = dbg._controller; |
|
372 let thread = controller.activeThread; |
|
373 if (thread.paused) { |
|
374 thread.stepOut(); |
|
375 } |
|
376 } |
|
377 }); |
|
378 |
|
379 /** |
|
380 * 'dbg list' command |
|
381 */ |
|
382 exports.items.push({ |
|
383 name: "dbg list", |
|
384 description: gcli.lookup("dbgListSourcesDesc"), |
|
385 params: [], |
|
386 returnType: "dom", |
|
387 exec: function(args, context) { |
|
388 let dbg = getPanel(context, "jsdebugger"); |
|
389 if (!dbg) { |
|
390 return gcli.lookup("debuggerClosed"); |
|
391 } |
|
392 |
|
393 let sources = dbg._view.Sources.values; |
|
394 let doc = context.environment.chromeDocument; |
|
395 let div = createXHTMLElement(doc, "div"); |
|
396 let ol = createXHTMLElement(doc, "ol"); |
|
397 |
|
398 sources.forEach(source => { |
|
399 let li = createXHTMLElement(doc, "li"); |
|
400 li.textContent = source; |
|
401 ol.appendChild(li); |
|
402 }); |
|
403 div.appendChild(ol); |
|
404 |
|
405 return div; |
|
406 } |
|
407 }); |
|
408 |
|
409 /** |
|
410 * Define the 'dbg blackbox' and 'dbg unblackbox' commands. |
|
411 */ |
|
412 [ |
|
413 { |
|
414 name: "blackbox", |
|
415 clientMethod: "blackBox", |
|
416 l10nPrefix: "dbgBlackBox" |
|
417 }, |
|
418 { |
|
419 name: "unblackbox", |
|
420 clientMethod: "unblackBox", |
|
421 l10nPrefix: "dbgUnBlackBox" |
|
422 } |
|
423 ].forEach(function(cmd) { |
|
424 const lookup = function(id) { |
|
425 return gcli.lookup(cmd.l10nPrefix + id); |
|
426 }; |
|
427 |
|
428 exports.items.push({ |
|
429 name: "dbg " + cmd.name, |
|
430 description: lookup("Desc"), |
|
431 params: [ |
|
432 { |
|
433 name: "source", |
|
434 type: { |
|
435 name: "selection", |
|
436 data: function(context) { |
|
437 let dbg = getPanel(context, "jsdebugger"); |
|
438 if (dbg) { |
|
439 return dbg._view.Sources.values; |
|
440 } |
|
441 return []; |
|
442 } |
|
443 }, |
|
444 description: lookup("SourceDesc"), |
|
445 defaultValue: null |
|
446 }, |
|
447 { |
|
448 name: "glob", |
|
449 type: "string", |
|
450 description: lookup("GlobDesc"), |
|
451 defaultValue: null |
|
452 }, |
|
453 { |
|
454 name: "invert", |
|
455 type: "boolean", |
|
456 description: lookup("InvertDesc") |
|
457 } |
|
458 ], |
|
459 returnType: "dom", |
|
460 exec: function(args, context) { |
|
461 const dbg = getPanel(context, "jsdebugger"); |
|
462 const doc = context.environment.chromeDocument; |
|
463 if (!dbg) { |
|
464 throw new Error(gcli.lookup("debuggerClosed")); |
|
465 } |
|
466 |
|
467 const { promise, resolve, reject } = context.defer(); |
|
468 const { activeThread } = dbg._controller; |
|
469 const globRegExp = args.glob ? globToRegExp(args.glob) : null; |
|
470 |
|
471 // Filter the sources down to those that we will need to black box. |
|
472 |
|
473 function shouldBlackBox(source) { |
|
474 var value = globRegExp && globRegExp.test(source.url) |
|
475 || args.source && source.url == args.source; |
|
476 return args.invert ? !value : value; |
|
477 } |
|
478 |
|
479 const toBlackBox = [s.attachment.source |
|
480 for (s of dbg._view.Sources.items) |
|
481 if (shouldBlackBox(s.attachment.source))]; |
|
482 |
|
483 // If we aren't black boxing any sources, bail out now. |
|
484 |
|
485 if (toBlackBox.length === 0) { |
|
486 const empty = createXHTMLElement(doc, "div"); |
|
487 empty.textContent = lookup("EmptyDesc"); |
|
488 return void resolve(empty); |
|
489 } |
|
490 |
|
491 // Send the black box request to each source we are black boxing. As we |
|
492 // get responses, accumulate the results in `blackBoxed`. |
|
493 |
|
494 const blackBoxed = []; |
|
495 |
|
496 for (let source of toBlackBox) { |
|
497 activeThread.source(source)[cmd.clientMethod](function({ error }) { |
|
498 if (error) { |
|
499 blackBoxed.push(lookup("ErrorDesc") + " " + source.url); |
|
500 } else { |
|
501 blackBoxed.push(source.url); |
|
502 } |
|
503 |
|
504 if (toBlackBox.length === blackBoxed.length) { |
|
505 displayResults(); |
|
506 } |
|
507 }); |
|
508 } |
|
509 |
|
510 // List the results for the user. |
|
511 |
|
512 function displayResults() { |
|
513 const results = doc.createElement("div"); |
|
514 results.textContent = lookup("NonEmptyDesc"); |
|
515 |
|
516 const list = createXHTMLElement(doc, "ul"); |
|
517 results.appendChild(list); |
|
518 |
|
519 for (let result of blackBoxed) { |
|
520 const item = createXHTMLElement(doc, "li"); |
|
521 item.textContent = result; |
|
522 list.appendChild(item); |
|
523 } |
|
524 resolve(results); |
|
525 } |
|
526 |
|
527 return promise; |
|
528 } |
|
529 }); |
|
530 }); |
|
531 |
|
532 /** |
|
533 * A helper to create xhtml namespaced elements. |
|
534 */ |
|
535 function createXHTMLElement(document, tagname) { |
|
536 return document.createElementNS("http://www.w3.org/1999/xhtml", tagname); |
|
537 } |
|
538 |
|
539 /** |
|
540 * A helper to go from a command context to a debugger panel. |
|
541 */ |
|
542 function getPanel(context, id, options = {}) { |
|
543 if (!context) { |
|
544 return undefined; |
|
545 } |
|
546 |
|
547 let target = context.environment.target; |
|
548 |
|
549 if (options.ensureOpened) { |
|
550 return gDevTools.showToolbox(target, id).then(toolbox => { |
|
551 return toolbox.getPanel(id); |
|
552 }); |
|
553 } else { |
|
554 let toolbox = gDevTools.getToolbox(target); |
|
555 if (toolbox) { |
|
556 return toolbox.getPanel(id); |
|
557 } else { |
|
558 return undefined; |
|
559 } |
|
560 } |
|
561 } |
|
562 |
|
563 /** |
|
564 * Converts a glob to a regular expression. |
|
565 */ |
|
566 function globToRegExp(glob) { |
|
567 const reStr = glob |
|
568 // Escape existing regular expression syntax. |
|
569 .replace(/\\/g, "\\\\") |
|
570 .replace(/\//g, "\\/") |
|
571 .replace(/\^/g, "\\^") |
|
572 .replace(/\$/g, "\\$") |
|
573 .replace(/\+/g, "\\+") |
|
574 .replace(/\?/g, "\\?") |
|
575 .replace(/\./g, "\\.") |
|
576 .replace(/\(/g, "\\(") |
|
577 .replace(/\)/g, "\\)") |
|
578 .replace(/\=/g, "\\=") |
|
579 .replace(/\!/g, "\\!") |
|
580 .replace(/\|/g, "\\|") |
|
581 .replace(/\{/g, "\\{") |
|
582 .replace(/\}/g, "\\}") |
|
583 .replace(/\,/g, "\\,") |
|
584 .replace(/\[/g, "\\[") |
|
585 .replace(/\]/g, "\\]") |
|
586 .replace(/\-/g, "\\-") |
|
587 // Turn * into the match everything wildcard. |
|
588 .replace(/\*/g, ".*") |
|
589 return new RegExp("^" + reStr + "$"); |
|
590 } |