browser/devtools/debugger/debugger-commands.js

changeset 0
6474c204b198
equal deleted inserted replaced
-1:000000000000 0:e706f8abaae2
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 }

mercurial