Bug 762761 - add prettyPrint request to the remote debugging protocol server/client; r=past

This commit is contained in:
Nick Fitzgerald 2013-09-11 10:15:51 -07:00
parent 1d64f9bf07
commit 71d9295729
10 changed files with 395 additions and 48 deletions

View File

@ -2010,32 +2010,50 @@ SourceClient.prototype = {
to: this._form.actor,
type: "source"
};
this._client.request(packet, function (aResponse) {
this._client.request(packet, aResponse => {
this._onSourceResponse(aResponse, aCallback)
});
},
/**
* Pretty print this source's text.
*/
prettyPrint: function SC_prettyPrint(aIndent, aCallback) {
const packet = {
to: this._form.actor,
type: "prettyPrint",
indent: aIndent
};
this._client.request(packet, aResponse => {
this._onSourceResponse(aResponse, aCallback);
});
},
_onSourceResponse: function SC__onSourceResponse(aResponse, aCallback) {
if (aResponse.error) {
aCallback(aResponse);
return;
}
if (typeof aResponse.source === "string") {
aCallback(aResponse);
return;
}
let { contentType, source } = aResponse;
let longString = this._client.activeThread.threadLongString(
source);
longString.substring(0, longString.length, function (aResponse) {
if (aResponse.error) {
aCallback(aResponse);
return;
}
if (typeof aResponse.source === "string") {
aCallback(aResponse);
return;
}
let { contentType, source } = aResponse;
let longString = this._client.activeThread.threadLongString(
source);
longString.substring(0, longString.length, function (aResponse) {
if (aResponse.error) {
aCallback(aResponse);
return;
}
aCallback({
source: aResponse.substring,
contentType: contentType
});
aCallback({
source: aResponse.substring,
contentType: contentType
});
}.bind(this));
});
}
};

View File

@ -2298,11 +2298,19 @@ PauseScopedActor.prototype = {
* The current thread actor.
* @param aSourceMap SourceMapConsumer
* Optional. The source map that introduced this source, if available.
* @param aGeneratedSource String
* Optional, passed in when aSourceMap is also passed in. The generated
* source url that introduced this source.
*/
function SourceActor(aUrl, aThreadActor, aSourceMap=null) {
function SourceActor(aUrl, aThreadActor, aSourceMap=null, aGeneratedSource=null) {
this._threadActor = aThreadActor;
this._url = aUrl;
this._sourceMap = aSourceMap;
this._generatedSource = aGeneratedSource;
this.onSource = this.onSource.bind(this);
this._invertSourceMap = this._invertSourceMap.bind(this);
this._saveMap = this._saveMap.bind(this);
}
SourceActor.prototype = {
@ -2321,33 +2329,35 @@ SourceActor.prototype = {
};
},
disconnect: function LSA_disconnect() {
disconnect: function SA_disconnect() {
if (this.registeredPool && this.registeredPool.sourceActors) {
delete this.registeredPool.sourceActors[this.actorID];
}
},
/**
* Handler for the "source" packet.
*/
onSource: function SA_onSource(aRequest) {
_getSourceText: function SA__getSourceText() {
let sourceContent = null;
if (this._sourceMap) {
sourceContent = this._sourceMap.sourceContentFor(this._url);
}
if (sourceContent) {
return {
from: this.actorID,
source: this.threadActor.createValueGrip(
sourceContent, this.threadActor.threadLifetimePool)
};
return resolve({
content: sourceContent
});
}
// XXX bug 865252: Don't load from the cache if this is a source mapped
// source because we can't guarantee that the cache has the most up to date
// content for this source like we can if it isn't source mapped.
return fetch(this._url, { loadFromCache: !this._sourceMap })
return fetch(this._url, { loadFromCache: !this._sourceMap });
},
/**
* Handler for the "source" packet.
*/
onSource: function SA_onSource(aRequest) {
return this._getSourceText()
.then(({ content, contentType }) => {
return {
from: this.actorID,
@ -2367,6 +2377,109 @@ SourceActor.prototype = {
});
},
/**
* Handler for the "prettyPrint" packet.
*/
onPrettyPrint: function ({ indent }) {
return this._getSourceText()
.then(this._parseAST)
.then(this._generatePrettyCodeAndMap(indent))
.then(this._invertSourceMap)
.then(this._saveMap)
.then(this.onSource)
.then(null, error => ({
from: this.actorID,
error: "prettyPrintError",
message: DevToolsUtils.safeErrorString(error)
}));
},
/**
* Parse the source content into an AST.
*/
_parseAST: function SA__parseAST({ content}) {
return Reflect.parse(content);
},
/**
* Take the number of spaces to indent and return a function that takes an AST
* and generates code and a source map from the ugly code to the pretty code.
*/
_generatePrettyCodeAndMap: function SA__generatePrettyCodeAndMap(aNumSpaces) {
let indent = "";
for (let i = 0; i < aNumSpaces; i++) {
indent += " ";
}
return aAST => escodegen.generate(aAST, {
format: {
indent: {
style: indent
}
},
sourceMap: this._url,
sourceMapWithCode: true
});
},
/**
* Invert a source map. So if a source map maps from a to b, return a new
* source map from b to a. We need to do this because the source map we get
* from _generatePrettyCodeAndMap goes the opposite way we want it to for
* debugging.
*/
_invertSourceMap: function SA__invertSourceMap({ code, map }) {
const smc = new SourceMapConsumer(map.toJSON());
const invertedMap = new SourceMapGenerator({
file: this._url
});
smc.eachMapping(m => {
if (!m.originalLine || !m.originalColumn) {
return;
}
const invertedMapping = {
source: m.source,
name: m.name,
original: {
line: m.generatedLine,
column: m.generatedColumn
},
generated: {
line: m.originalLine,
column: m.originalColumn
}
};
invertedMap.addMapping(invertedMapping);
});
invertedMap.setSourceContent(this._url, code);
return {
code: code,
map: new SourceMapConsumer(invertedMap.toJSON())
};
},
/**
* Save the source map back to our thread's ThreadSources object so that
* stepping, breakpoints, debugger statements, etc can use it. If we are
* pretty printing a source mapped source, we need to compose the existing
* source map with our new one.
*/
_saveMap: function SA__saveMap({ map }) {
if (this._sourceMap) {
// Compose the source maps
this._sourceMap = SourceMapGenerator.fromSourceMap(this._sourceMap);
this._sourceMap.applySourceMap(map, this._url);
this._sourceMap = new SourceMapConsumer(this._sourceMap.toJSON());
this._threadActor.sources.saveSourceMap(this._sourceMap,
this._generatedSource);
} else {
this._sourceMap = map;
this._threadActor.sources.saveSourceMap(this._sourceMap, this._url);
}
},
/**
* Handler for the "blackbox" packet.
*/
@ -2397,7 +2510,8 @@ SourceActor.prototype = {
SourceActor.prototype.requestTypes = {
"source": SourceActor.prototype.onSource,
"blackbox": SourceActor.prototype.onBlackBox,
"unblackbox": SourceActor.prototype.onUnblackBox
"unblackbox": SourceActor.prototype.onUnblackBox,
"prettyPrint": SourceActor.prototype.onPrettyPrint
};
@ -3486,9 +3600,12 @@ ThreadSources.prototype = {
* The source URL.
* @param optional SourceMapConsumer aSourceMap
* The source map that introduced this source, if any.
* @param optional String aGeneratedSource
* The generated source url that introduced this source via source map,
* if any.
* @returns a SourceActor representing the source at aURL or null.
*/
source: function TS_source(aURL, aSourceMap=null) {
source: function TS_source(aURL, aSourceMap=null, aGeneratedSource=null) {
if (!this._allow(aURL)) {
return null;
}
@ -3497,7 +3614,7 @@ ThreadSources.prototype = {
return this._sourceActors[aURL];
}
let actor = new SourceActor(aURL, this._thread, aSourceMap);
let actor = new SourceActor(aURL, this._thread, aSourceMap, aGeneratedSource);
this._thread.threadLifetimePool.addActor(actor);
this._sourceActors[aURL] = actor;
try {
@ -3524,7 +3641,7 @@ ThreadSources.prototype = {
return this.sourceMap(aScript)
.then((aSourceMap) => {
return [
this.source(s, aSourceMap) for (s of aSourceMap.sources)
this.source(s, aSourceMap, aScript.url) for (s of aSourceMap.sources)
];
})
.then(null, (e) => {
@ -3546,17 +3663,24 @@ ThreadSources.prototype = {
dbg_assert(aScript.sourceMapURL, "Script should have a sourceMapURL");
let sourceMapURL = this._normalize(aScript.sourceMapURL, aScript.url);
let map = this._fetchSourceMap(sourceMapURL, aScript.url)
.then((aSourceMap) => {
for (let s of aSourceMap.sources) {
this._generatedUrlsByOriginalUrl[s] = aScript.url;
this._sourceMapsByOriginalSource[s] = resolve(aSourceMap);
}
return aSourceMap;
});
.then(aSourceMap => this.saveSourceMap(aSourceMap, aScript.url));
this._sourceMapsByGeneratedSource[aScript.url] = map;
return map;
},
/**
* Save the given source map so that we can use it to query source locations
* down the line.
*/
saveSourceMap: function TS_saveSourceMap(aSourceMap, aGeneratedSource) {
this._sourceMapsByGeneratedSource[aGeneratedSource] = resolve(aSourceMap);
for (let s of aSourceMap.sources) {
this._generatedUrlsByOriginalUrl[s] = aGeneratedSource;
this._sourceMapsByOriginalSource[s] = resolve(aSourceMap);
}
return aSourceMap;
},
/**
* Return a promise of a SourceMapConsumer for the source map located at
* |aAbsSourceMapURL|, which must be absolute. If there is already such a

View File

@ -44,6 +44,8 @@ if (this.require) {
const DBG_STRINGS_URI = "chrome://global/locale/devtools/debugger.properties";
const nsFile = CC("@mozilla.org/file/local;1", "nsIFile", "initWithPath");
Cu.import("resource://gre/modules/reflect.jsm");
Cu.import("resource://gre/modules/devtools/DevToolsUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
let wantLogging = Services.prefs.getBoolPref("devtools.debugger.log");
@ -72,6 +74,7 @@ loadSubScript.call(this, "resource://gre/modules/commonjs/sdk/core/promise.js");
this.require = loaderRequire;
Cu.import("resource://gre/modules/devtools/SourceMap.jsm");
const escodegen = localRequire("escodegen/escodegen");
loadSubScript.call(this, "resource://gre/modules/devtools/DevToolsUtils.js");

View File

@ -15,11 +15,22 @@ Services.prefs.setBoolPref("devtools.debugger.log", true);
// Enable remote debugging for the relevant tests.
Services.prefs.setBoolPref("devtools.debugger.remote-enabled", true);
Cu.import("resource://gre/modules/devtools/dbg-server.jsm");
Cu.import("resource://gre/modules/devtools/dbg-client.jsm");
Cu.import("resource://gre/modules/devtools/Loader.jsm");
Cu.import("resource://gre/modules/devtools/DevToolsUtils.jsm");
function tryImport(url) {
try {
Cu.import(url);
} catch (e) {
dump("Error importing " + url + "\n");
dump(DevToolsUtils.safeErrorString(e) + "\n");
throw e;
}
}
tryImport("resource://gre/modules/devtools/dbg-server.jsm");
tryImport("resource://gre/modules/devtools/dbg-client.jsm");
tryImport("resource://gre/modules/devtools/Loader.jsm");
function testExceptionHook(ex) {
try {
do_report_unexpected_exception(ex);

View File

@ -0,0 +1,42 @@
/* -*- Mode: javascript; js-indent-level: 2; -*- */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
var gDebuggee;
var gClient;
var gThreadClient;
// Test basic pretty printing functionality
function run_test() {
initTestDebuggerServer();
gDebuggee = addTestGlobal("test-pretty-print");
gClient = new DebuggerClient(DebuggerServer.connectPipe());
gClient.connect(function() {
attachTestTabAndResume(gClient, "test-pretty-print", function(aResponse, aTabClient, aThreadClient) {
gThreadClient = aThreadClient;
evalCode();
});
});
do_test_pending();
}
function evalCode() {
gClient.addOneTimeListener("newSource", prettyPrintSource);
const code = "" + function main() { let a = 1 + 3; let b = a++; return b + a; };
Cu.evalInSandbox(
code,
gDebuggee,
"1.8",
"data:text/javascript," + code);
}
function prettyPrintSource(event, { source }) {
gThreadClient.source(source).prettyPrint(4, testPrettyPrinted);
}
function testPrettyPrinted({ error, source}) {
do_check_true(!error);
do_check_true(source.contains("\n "));
finishClient(gClient);
}

View File

@ -0,0 +1,67 @@
/* -*- Mode: javascript; js-indent-level: 2; -*- */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
var gDebuggee;
var gClient;
var gThreadClient;
var gSource;
// Test stepping through pretty printed sources.
function run_test() {
initTestDebuggerServer();
gDebuggee = addTestGlobal("test-pretty-print");
gClient = new DebuggerClient(DebuggerServer.connectPipe());
gClient.connect(function() {
attachTestTabAndResume(gClient, "test-pretty-print", function(aResponse, aTabClient, aThreadClient) {
gThreadClient = aThreadClient;
evalCode();
});
});
do_test_pending();
}
const CODE = "" + function main() { debugger; return 10; };
const CODE_URL = "data:text/javascript," + CODE;
function evalCode() {
gClient.addOneTimeListener("newSource", prettyPrintSource);
Cu.evalInSandbox(
CODE,
gDebuggee,
"1.8",
CODE_URL,
1
);
}
function prettyPrintSource(event, { source }) {
gSource = source;
gThreadClient.source(gSource).prettyPrint(2, runCode);
}
function runCode({ error }) {
do_check_true(!error);
gClient.addOneTimeListener("paused", testDbgStatement);
gDebuggee.main();
}
function testDbgStatement(event, { frame }) {
const { url, line, column } = frame.where;
do_check_eq(url, CODE_URL);
do_check_eq(line, 2);
do_check_eq(column, 2);
testStepping();
}
function testStepping() {
gClient.addOneTimeListener("paused", (event, { frame }) => {
const { url, line, column } = frame.where;
do_check_eq(url, CODE_URL);
do_check_eq(line, 3);
do_check_eq(column, 2);
finishClient(gClient);
});
gThreadClient.stepIn();
}

View File

@ -0,0 +1,74 @@
/* -*- Mode: javascript; js-indent-level: 2; -*- */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
// Test pretty printing source mapped sources.
var gDebuggee;
var gClient;
var gThreadClient;
var gSource;
Components.utils.import('resource:///modules/devtools/SourceMap.jsm');
function run_test() {
initTestDebuggerServer();
gDebuggee = addTestGlobal("test-pretty-print");
gClient = new DebuggerClient(DebuggerServer.connectPipe());
gClient.connect(function() {
attachTestTabAndResume(gClient, "test-pretty-print", function(aResponse, aTabClient, aThreadClient) {
gThreadClient = aThreadClient;
evalCode();
});
});
do_test_pending();
}
const dataUrl = s => "data:text/javascript," + s;
const A = "function a(){b()}";
const A_URL = dataUrl(A);
const B = "function b(){debugger}";
const B_URL = dataUrl(B);
function evalCode() {
let { code, map } = (new SourceNode(null, null, null, [
new SourceNode(1, 0, A_URL, A),
B.split("").map((ch, i) => new SourceNode(1, i, B_URL, ch))
])).toStringWithSourceMap({
file: "abc.js"
});
code += "//# sourceMappingURL=data:text/json;base64," + btoa(map.toString());
gClient.addListener("newSource", waitForB);
Components.utils.evalInSandbox(code, gDebuggee, "1.8",
"http://example.com/foo.js", 1);
}
function waitForB(event, { source }) {
if (source.url !== B_URL) {
return;
}
gClient.removeListener("newSource", waitForB);
prettyPrint(source);
}
function prettyPrint(source) {
gThreadClient.source(source).prettyPrint(2, runCode);
}
function runCode({ error }) {
do_check_true(!error);
gClient.addOneTimeListener("paused", testDbgStatement);
gDebuggee.a();
}
function testDbgStatement(event, { frame, why }) {
do_check_eq(why.type, "debuggerStatement");
const { url, line, column } = frame.where;
do_check_eq(url, B_URL);
do_check_eq(line, 2);
do_check_eq(column, 2);
finishClient(gClient);
}

View File

@ -153,6 +153,9 @@ reason = bug 820380
[test_longstringactor.js]
[test_longstringgrips-01.js]
[test_longstringgrips-02.js]
[test_pretty_print-01.js]
[test_pretty_print-02.js]
[test_pretty_print-03.js]
[test_source-01.js]
skip-if = toolkit == "gonk"
reason = bug 820380

View File

@ -490,6 +490,7 @@ define('source-map/util', ['require', 'exports', 'module' , ], function(require,
exports.getArg = getArg;
var urlRegexp = /([\w+\-.]+):\/\/((\w+:\w+)@)?([\w.]+)?(:(\d+))?(\S+)?/;
var dataUrlRegexp = /^data:.+\,.+/;
function urlParse(aUrl) {
var match = aUrl.match(urlRegexp);
@ -527,7 +528,7 @@ define('source-map/util', ['require', 'exports', 'module' , ], function(require,
function join(aRoot, aPath) {
var url;
if (aPath.match(urlRegexp)) {
if (aPath.match(urlRegexp) || aPath.match(dataUrlRegexp)) {
return aPath;
}
@ -1082,11 +1083,13 @@ define('source-map/source-map-generator', ['require', 'exports', 'module' , 'so
if (!aSourceFile) {
aSourceFile = aSourceMapConsumer.file;
}
var sourceRoot = this._sourceRoot;
// Make "aSourceFile" relative if an absolute Url is passed.
if (sourceRoot) {
aSourceFile = util.relative(sourceRoot, aSourceFile);
}
// Applying the SourceMap can add and remove items from the sources and
// the names array.
var newSources = new ArraySet();
@ -1100,6 +1103,7 @@ define('source-map/source-map-generator', ['require', 'exports', 'module' , 'so
line: mapping.original.line,
column: mapping.original.column
});
if (original.source !== null) {
// Copy mapping
if (sourceRoot) {

View File

@ -259,6 +259,7 @@ define('lib/source-map/util', ['require', 'exports', 'module' , ], function(requ
exports.getArg = getArg;
var urlRegexp = /([\w+\-.]+):\/\/((\w+:\w+)@)?([\w.]+)?(:(\d+))?(\S+)?/;
var dataUrlRegexp = /^data:.+\,.+/;
function urlParse(aUrl) {
var match = aUrl.match(urlRegexp);
@ -296,7 +297,7 @@ define('lib/source-map/util', ['require', 'exports', 'module' , ], function(requ
function join(aRoot, aPath) {
var url;
if (aPath.match(urlRegexp)) {
if (aPath.match(urlRegexp) || aPath.match(dataUrlRegexp)) {
return aPath;
}