diff -r 000000000000 -r 6474c204b198 browser/devtools/debugger/debugger-commands.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/browser/devtools/debugger/debugger-commands.js Wed Dec 31 06:09:35 2014 +0100 @@ -0,0 +1,590 @@ +/* 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.value, + label: source.attachment.label + ":" + breakpoint.line, + lineNumber: breakpoint.line, + lineText: breakpoint.text, + truncatedLineText: trim(breakpoint.text, MAX_LINE_TEXT_LENGTH, "end") + }); + } + } + + return breakpoints; +} + +/** + * '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: "

${message}

", + data: { message: gcli.lookup("breaklistNone") } + }); + } + } +}); + +var breakListHtml = "" + + "" + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + "
SourceLineActions
${breakpoint.label}" + + " ${breakpoint.truncatedLineText}" + + " " + + " " + + " " + gcli.lookup("breaklistOutRemove") + "" + + "
" + + ""; + +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", + data: function(context) { + let dbg = getPanel(context, "jsdebugger"); + if (dbg) { + return dbg._view.Sources.values; + } + return []; + } + }, + 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 position = { url: args.file, 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 deferred = context.defer(); + let position = { url: args.breakpoint.url, 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 = dbg._view.Sources.values; + 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; + 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", + data: function(context) { + let dbg = getPanel(context, "jsdebugger"); + if (dbg) { + return dbg._view.Sources.values; + } + return []; + } + }, + 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.url == 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 + "$"); +}