michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0:
michael@0: "use strict";
michael@0:
michael@0: const { Cc, Ci, Cu } = require("chrome");
michael@0: const gcli = require("gcli/index");
michael@0:
michael@0: loader.lazyImporter(this, "gDevTools", "resource:///modules/devtools/gDevTools.jsm");
michael@0:
michael@0: /**
michael@0: * The commands and converters that are exported to GCLI
michael@0: */
michael@0: exports.items = [];
michael@0:
michael@0: /**
michael@0: * Utility to get access to the current breakpoint list.
michael@0: *
michael@0: * @param DebuggerPanel dbg
michael@0: * The debugger panel.
michael@0: * @return array
michael@0: * An array of objects, one for each breakpoint, where each breakpoint
michael@0: * object has the following properties:
michael@0: * - url: the URL of the source file.
michael@0: * - label: a unique string identifier designed to be user visible.
michael@0: * - lineNumber: the line number of the breakpoint in the source file.
michael@0: * - lineText: the text of the line at the breakpoint.
michael@0: * - truncatedLineText: lineText truncated to MAX_LINE_TEXT_LENGTH.
michael@0: */
michael@0: function getAllBreakpoints(dbg) {
michael@0: let breakpoints = [];
michael@0: let sources = dbg._view.Sources;
michael@0: let { trimUrlLength: trim } = dbg.panelWin.SourceUtils;
michael@0:
michael@0: for (let source of sources) {
michael@0: for (let { attachment: breakpoint } of source) {
michael@0: breakpoints.push({
michael@0: url: source.value,
michael@0: label: source.attachment.label + ":" + breakpoint.line,
michael@0: lineNumber: breakpoint.line,
michael@0: lineText: breakpoint.text,
michael@0: truncatedLineText: trim(breakpoint.text, MAX_LINE_TEXT_LENGTH, "end")
michael@0: });
michael@0: }
michael@0: }
michael@0:
michael@0: return breakpoints;
michael@0: }
michael@0:
michael@0: /**
michael@0: * 'break' command
michael@0: */
michael@0: exports.items.push({
michael@0: name: "break",
michael@0: description: gcli.lookup("breakDesc"),
michael@0: manual: gcli.lookup("breakManual")
michael@0: });
michael@0:
michael@0: /**
michael@0: * 'break list' command
michael@0: */
michael@0: exports.items.push({
michael@0: name: "break list",
michael@0: description: gcli.lookup("breaklistDesc"),
michael@0: returnType: "breakpoints",
michael@0: exec: function(args, context) {
michael@0: let dbg = getPanel(context, "jsdebugger", { ensureOpened: true });
michael@0: return dbg.then(getAllBreakpoints);
michael@0: }
michael@0: });
michael@0:
michael@0: exports.items.push({
michael@0: item: "converter",
michael@0: from: "breakpoints",
michael@0: to: "view",
michael@0: exec: function(breakpoints, context) {
michael@0: let dbg = getPanel(context, "jsdebugger");
michael@0: if (dbg && breakpoints.length) {
michael@0: return context.createView({
michael@0: html: breakListHtml,
michael@0: data: {
michael@0: breakpoints: breakpoints,
michael@0: onclick: context.update,
michael@0: ondblclick: context.updateExec
michael@0: }
michael@0: });
michael@0: } else {
michael@0: return context.createView({
michael@0: html: "
${message}
",
michael@0: data: { message: gcli.lookup("breaklistNone") }
michael@0: });
michael@0: }
michael@0: }
michael@0: });
michael@0:
michael@0: var breakListHtml = "" +
michael@0: "" +
michael@0: " " +
michael@0: " Source | " +
michael@0: " Line | " +
michael@0: " Actions | " +
michael@0: " " +
michael@0: " " +
michael@0: " " +
michael@0: " ${breakpoint.label} | " +
michael@0: " " +
michael@0: " ${breakpoint.truncatedLineText}" +
michael@0: " | " +
michael@0: " " +
michael@0: " " +
michael@0: " " + gcli.lookup("breaklistOutRemove") + "" +
michael@0: " | " +
michael@0: "
" +
michael@0: " " +
michael@0: "
" +
michael@0: "";
michael@0:
michael@0: var MAX_LINE_TEXT_LENGTH = 30;
michael@0: var MAX_LABEL_LENGTH = 20;
michael@0:
michael@0: /**
michael@0: * 'break add' command
michael@0: */
michael@0: exports.items.push({
michael@0: name: "break add",
michael@0: description: gcli.lookup("breakaddDesc"),
michael@0: manual: gcli.lookup("breakaddManual")
michael@0: });
michael@0:
michael@0: /**
michael@0: * 'break add line' command
michael@0: */
michael@0: exports.items.push({
michael@0: name: "break add line",
michael@0: description: gcli.lookup("breakaddlineDesc"),
michael@0: params: [
michael@0: {
michael@0: name: "file",
michael@0: type: {
michael@0: name: "selection",
michael@0: data: function(context) {
michael@0: let dbg = getPanel(context, "jsdebugger");
michael@0: if (dbg) {
michael@0: return dbg._view.Sources.values;
michael@0: }
michael@0: return [];
michael@0: }
michael@0: },
michael@0: description: gcli.lookup("breakaddlineFileDesc")
michael@0: },
michael@0: {
michael@0: name: "line",
michael@0: type: { name: "number", min: 1, step: 10 },
michael@0: description: gcli.lookup("breakaddlineLineDesc")
michael@0: }
michael@0: ],
michael@0: returnType: "string",
michael@0: exec: function(args, context) {
michael@0: let dbg = getPanel(context, "jsdebugger");
michael@0: if (!dbg) {
michael@0: return gcli.lookup("debuggerStopped");
michael@0: }
michael@0:
michael@0: let deferred = context.defer();
michael@0: let position = { url: args.file, line: args.line };
michael@0:
michael@0: dbg.addBreakpoint(position).then(() => {
michael@0: deferred.resolve(gcli.lookup("breakaddAdded"));
michael@0: }, aError => {
michael@0: deferred.resolve(gcli.lookupFormat("breakaddFailed", [aError]));
michael@0: });
michael@0:
michael@0: return deferred.promise;
michael@0: }
michael@0: });
michael@0:
michael@0: /**
michael@0: * 'break del' command
michael@0: */
michael@0: exports.items.push({
michael@0: name: "break del",
michael@0: description: gcli.lookup("breakdelDesc"),
michael@0: params: [
michael@0: {
michael@0: name: "breakpoint",
michael@0: type: {
michael@0: name: "selection",
michael@0: lookup: function(context) {
michael@0: let dbg = getPanel(context, "jsdebugger");
michael@0: if (!dbg) {
michael@0: return [];
michael@0: }
michael@0: return getAllBreakpoints(dbg).map(breakpoint => ({
michael@0: name: breakpoint.label,
michael@0: value: breakpoint,
michael@0: description: breakpoint.truncatedLineText
michael@0: }));
michael@0: }
michael@0: },
michael@0: description: gcli.lookup("breakdelBreakidDesc")
michael@0: }
michael@0: ],
michael@0: returnType: "string",
michael@0: exec: function(args, context) {
michael@0: let dbg = getPanel(context, "jsdebugger");
michael@0: if (!dbg) {
michael@0: return gcli.lookup("debuggerStopped");
michael@0: }
michael@0:
michael@0: let deferred = context.defer();
michael@0: let position = { url: args.breakpoint.url, line: args.breakpoint.lineNumber };
michael@0:
michael@0: dbg.removeBreakpoint(position).then(() => {
michael@0: deferred.resolve(gcli.lookup("breakdelRemoved"));
michael@0: }, () => {
michael@0: deferred.resolve(gcli.lookup("breakNotFound"));
michael@0: });
michael@0:
michael@0: return deferred.promise;
michael@0: }
michael@0: });
michael@0:
michael@0: /**
michael@0: * 'dbg' command
michael@0: */
michael@0: exports.items.push({
michael@0: name: "dbg",
michael@0: description: gcli.lookup("dbgDesc"),
michael@0: manual: gcli.lookup("dbgManual")
michael@0: });
michael@0:
michael@0: /**
michael@0: * 'dbg open' command
michael@0: */
michael@0: exports.items.push({
michael@0: name: "dbg open",
michael@0: description: gcli.lookup("dbgOpen"),
michael@0: params: [],
michael@0: exec: function(args, context) {
michael@0: let target = context.environment.target;
michael@0: return gDevTools.showToolbox(target, "jsdebugger").then(() => null);
michael@0: }
michael@0: });
michael@0:
michael@0: /**
michael@0: * 'dbg close' command
michael@0: */
michael@0: exports.items.push({
michael@0: name: "dbg close",
michael@0: description: gcli.lookup("dbgClose"),
michael@0: params: [],
michael@0: exec: function(args, context) {
michael@0: if (!getPanel(context, "jsdebugger")) {
michael@0: return;
michael@0: }
michael@0: let target = context.environment.target;
michael@0: return gDevTools.closeToolbox(target).then(() => null);
michael@0: }
michael@0: });
michael@0:
michael@0: /**
michael@0: * 'dbg interrupt' command
michael@0: */
michael@0: exports.items.push({
michael@0: name: "dbg interrupt",
michael@0: description: gcli.lookup("dbgInterrupt"),
michael@0: params: [],
michael@0: exec: function(args, context) {
michael@0: let dbg = getPanel(context, "jsdebugger");
michael@0: if (!dbg) {
michael@0: return gcli.lookup("debuggerStopped");
michael@0: }
michael@0:
michael@0: let controller = dbg._controller;
michael@0: let thread = controller.activeThread;
michael@0: if (!thread.paused) {
michael@0: thread.interrupt();
michael@0: }
michael@0: }
michael@0: });
michael@0:
michael@0: /**
michael@0: * 'dbg continue' command
michael@0: */
michael@0: exports.items.push({
michael@0: name: "dbg continue",
michael@0: description: gcli.lookup("dbgContinue"),
michael@0: params: [],
michael@0: exec: function(args, context) {
michael@0: let dbg = getPanel(context, "jsdebugger");
michael@0: if (!dbg) {
michael@0: return gcli.lookup("debuggerStopped");
michael@0: }
michael@0:
michael@0: let controller = dbg._controller;
michael@0: let thread = controller.activeThread;
michael@0: if (thread.paused) {
michael@0: thread.resume();
michael@0: }
michael@0: }
michael@0: });
michael@0:
michael@0: /**
michael@0: * 'dbg step' command
michael@0: */
michael@0: exports.items.push({
michael@0: name: "dbg step",
michael@0: description: gcli.lookup("dbgStepDesc"),
michael@0: manual: gcli.lookup("dbgStepManual")
michael@0: });
michael@0:
michael@0: /**
michael@0: * 'dbg step over' command
michael@0: */
michael@0: exports.items.push({
michael@0: name: "dbg step over",
michael@0: description: gcli.lookup("dbgStepOverDesc"),
michael@0: params: [],
michael@0: exec: function(args, context) {
michael@0: let dbg = getPanel(context, "jsdebugger");
michael@0: if (!dbg) {
michael@0: return gcli.lookup("debuggerStopped");
michael@0: }
michael@0:
michael@0: let controller = dbg._controller;
michael@0: let thread = controller.activeThread;
michael@0: if (thread.paused) {
michael@0: thread.stepOver();
michael@0: }
michael@0: }
michael@0: });
michael@0:
michael@0: /**
michael@0: * 'dbg step in' command
michael@0: */
michael@0: exports.items.push({
michael@0: name: 'dbg step in',
michael@0: description: gcli.lookup("dbgStepInDesc"),
michael@0: params: [],
michael@0: exec: function(args, context) {
michael@0: let dbg = getPanel(context, "jsdebugger");
michael@0: if (!dbg) {
michael@0: return gcli.lookup("debuggerStopped");
michael@0: }
michael@0:
michael@0: let controller = dbg._controller;
michael@0: let thread = controller.activeThread;
michael@0: if (thread.paused) {
michael@0: thread.stepIn();
michael@0: }
michael@0: }
michael@0: });
michael@0:
michael@0: /**
michael@0: * 'dbg step over' command
michael@0: */
michael@0: exports.items.push({
michael@0: name: 'dbg step out',
michael@0: description: gcli.lookup("dbgStepOutDesc"),
michael@0: params: [],
michael@0: exec: function(args, context) {
michael@0: let dbg = getPanel(context, "jsdebugger");
michael@0: if (!dbg) {
michael@0: return gcli.lookup("debuggerStopped");
michael@0: }
michael@0:
michael@0: let controller = dbg._controller;
michael@0: let thread = controller.activeThread;
michael@0: if (thread.paused) {
michael@0: thread.stepOut();
michael@0: }
michael@0: }
michael@0: });
michael@0:
michael@0: /**
michael@0: * 'dbg list' command
michael@0: */
michael@0: exports.items.push({
michael@0: name: "dbg list",
michael@0: description: gcli.lookup("dbgListSourcesDesc"),
michael@0: params: [],
michael@0: returnType: "dom",
michael@0: exec: function(args, context) {
michael@0: let dbg = getPanel(context, "jsdebugger");
michael@0: if (!dbg) {
michael@0: return gcli.lookup("debuggerClosed");
michael@0: }
michael@0:
michael@0: let sources = dbg._view.Sources.values;
michael@0: let doc = context.environment.chromeDocument;
michael@0: let div = createXHTMLElement(doc, "div");
michael@0: let ol = createXHTMLElement(doc, "ol");
michael@0:
michael@0: sources.forEach(source => {
michael@0: let li = createXHTMLElement(doc, "li");
michael@0: li.textContent = source;
michael@0: ol.appendChild(li);
michael@0: });
michael@0: div.appendChild(ol);
michael@0:
michael@0: return div;
michael@0: }
michael@0: });
michael@0:
michael@0: /**
michael@0: * Define the 'dbg blackbox' and 'dbg unblackbox' commands.
michael@0: */
michael@0: [
michael@0: {
michael@0: name: "blackbox",
michael@0: clientMethod: "blackBox",
michael@0: l10nPrefix: "dbgBlackBox"
michael@0: },
michael@0: {
michael@0: name: "unblackbox",
michael@0: clientMethod: "unblackBox",
michael@0: l10nPrefix: "dbgUnBlackBox"
michael@0: }
michael@0: ].forEach(function(cmd) {
michael@0: const lookup = function(id) {
michael@0: return gcli.lookup(cmd.l10nPrefix + id);
michael@0: };
michael@0:
michael@0: exports.items.push({
michael@0: name: "dbg " + cmd.name,
michael@0: description: lookup("Desc"),
michael@0: params: [
michael@0: {
michael@0: name: "source",
michael@0: type: {
michael@0: name: "selection",
michael@0: data: function(context) {
michael@0: let dbg = getPanel(context, "jsdebugger");
michael@0: if (dbg) {
michael@0: return dbg._view.Sources.values;
michael@0: }
michael@0: return [];
michael@0: }
michael@0: },
michael@0: description: lookup("SourceDesc"),
michael@0: defaultValue: null
michael@0: },
michael@0: {
michael@0: name: "glob",
michael@0: type: "string",
michael@0: description: lookup("GlobDesc"),
michael@0: defaultValue: null
michael@0: },
michael@0: {
michael@0: name: "invert",
michael@0: type: "boolean",
michael@0: description: lookup("InvertDesc")
michael@0: }
michael@0: ],
michael@0: returnType: "dom",
michael@0: exec: function(args, context) {
michael@0: const dbg = getPanel(context, "jsdebugger");
michael@0: const doc = context.environment.chromeDocument;
michael@0: if (!dbg) {
michael@0: throw new Error(gcli.lookup("debuggerClosed"));
michael@0: }
michael@0:
michael@0: const { promise, resolve, reject } = context.defer();
michael@0: const { activeThread } = dbg._controller;
michael@0: const globRegExp = args.glob ? globToRegExp(args.glob) : null;
michael@0:
michael@0: // Filter the sources down to those that we will need to black box.
michael@0:
michael@0: function shouldBlackBox(source) {
michael@0: var value = globRegExp && globRegExp.test(source.url)
michael@0: || args.source && source.url == args.source;
michael@0: return args.invert ? !value : value;
michael@0: }
michael@0:
michael@0: const toBlackBox = [s.attachment.source
michael@0: for (s of dbg._view.Sources.items)
michael@0: if (shouldBlackBox(s.attachment.source))];
michael@0:
michael@0: // If we aren't black boxing any sources, bail out now.
michael@0:
michael@0: if (toBlackBox.length === 0) {
michael@0: const empty = createXHTMLElement(doc, "div");
michael@0: empty.textContent = lookup("EmptyDesc");
michael@0: return void resolve(empty);
michael@0: }
michael@0:
michael@0: // Send the black box request to each source we are black boxing. As we
michael@0: // get responses, accumulate the results in `blackBoxed`.
michael@0:
michael@0: const blackBoxed = [];
michael@0:
michael@0: for (let source of toBlackBox) {
michael@0: activeThread.source(source)[cmd.clientMethod](function({ error }) {
michael@0: if (error) {
michael@0: blackBoxed.push(lookup("ErrorDesc") + " " + source.url);
michael@0: } else {
michael@0: blackBoxed.push(source.url);
michael@0: }
michael@0:
michael@0: if (toBlackBox.length === blackBoxed.length) {
michael@0: displayResults();
michael@0: }
michael@0: });
michael@0: }
michael@0:
michael@0: // List the results for the user.
michael@0:
michael@0: function displayResults() {
michael@0: const results = doc.createElement("div");
michael@0: results.textContent = lookup("NonEmptyDesc");
michael@0:
michael@0: const list = createXHTMLElement(doc, "ul");
michael@0: results.appendChild(list);
michael@0:
michael@0: for (let result of blackBoxed) {
michael@0: const item = createXHTMLElement(doc, "li");
michael@0: item.textContent = result;
michael@0: list.appendChild(item);
michael@0: }
michael@0: resolve(results);
michael@0: }
michael@0:
michael@0: return promise;
michael@0: }
michael@0: });
michael@0: });
michael@0:
michael@0: /**
michael@0: * A helper to create xhtml namespaced elements.
michael@0: */
michael@0: function createXHTMLElement(document, tagname) {
michael@0: return document.createElementNS("http://www.w3.org/1999/xhtml", tagname);
michael@0: }
michael@0:
michael@0: /**
michael@0: * A helper to go from a command context to a debugger panel.
michael@0: */
michael@0: function getPanel(context, id, options = {}) {
michael@0: if (!context) {
michael@0: return undefined;
michael@0: }
michael@0:
michael@0: let target = context.environment.target;
michael@0:
michael@0: if (options.ensureOpened) {
michael@0: return gDevTools.showToolbox(target, id).then(toolbox => {
michael@0: return toolbox.getPanel(id);
michael@0: });
michael@0: } else {
michael@0: let toolbox = gDevTools.getToolbox(target);
michael@0: if (toolbox) {
michael@0: return toolbox.getPanel(id);
michael@0: } else {
michael@0: return undefined;
michael@0: }
michael@0: }
michael@0: }
michael@0:
michael@0: /**
michael@0: * Converts a glob to a regular expression.
michael@0: */
michael@0: function globToRegExp(glob) {
michael@0: const reStr = glob
michael@0: // Escape existing regular expression syntax.
michael@0: .replace(/\\/g, "\\\\")
michael@0: .replace(/\//g, "\\/")
michael@0: .replace(/\^/g, "\\^")
michael@0: .replace(/\$/g, "\\$")
michael@0: .replace(/\+/g, "\\+")
michael@0: .replace(/\?/g, "\\?")
michael@0: .replace(/\./g, "\\.")
michael@0: .replace(/\(/g, "\\(")
michael@0: .replace(/\)/g, "\\)")
michael@0: .replace(/\=/g, "\\=")
michael@0: .replace(/\!/g, "\\!")
michael@0: .replace(/\|/g, "\\|")
michael@0: .replace(/\{/g, "\\{")
michael@0: .replace(/\}/g, "\\}")
michael@0: .replace(/\,/g, "\\,")
michael@0: .replace(/\[/g, "\\[")
michael@0: .replace(/\]/g, "\\]")
michael@0: .replace(/\-/g, "\\-")
michael@0: // Turn * into the match everything wildcard.
michael@0: .replace(/\*/g, ".*")
michael@0: return new RegExp("^" + reStr + "$");
michael@0: }