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