c9-core/plugins/c9.ide.run.debug/debuggers/sourcemap.js

409 wiersze
16 KiB
JavaScript

define(function(require, exports, module) {
main.consumes = [
"Plugin", "settings", "debugger", "preferences", "fs", "tabManager"
];
main.provides = ["sourcemap"];
return main;
/*
Source Map Support:
- Add "Sourcemaps: Enable/Disable/Auto" in settings
- (Default:auto) detects based on filename
- [Hook debugger plugin] Setting a breakpoint via Ace
- Record that it's a sourcemap and the sourcemap path
- [Hook debugger plugin] Setting a breakpoint through the API
- [Hook debugger plugin] Make sure init waits until done
- Fetch generated file
- Check if generated file has sourcemap
- [Hook debugger plugin] Getting a breakpoint from the server (or using the api)
- Fetch source in background
- If sourcemap is detected update breakpoints
- [Hook debugger plugin] On breaking (breakpoint, debugger, stepping)
- Keep cache of known sourcemap states (none/map_contents)
- Deal with loading latency
- [Hook fs&debugger plugin] If not in cache load source (which we're doing anyway)
- If sourcemap is detected
- load sourcemap
- add sourcemap to cache
- calculate original file
- load original file
- update all breakpoints set on this file
- update all frames in this file
- Else if in cache, load original or do nothing
- [Hook debugger plugin] When loading the frames, check if they belong to a known sourcemapped file
- if so, translate the coords
- Check out https://github.com/evanw/node-source-map-support for node
*/
function main(options, imports, register) {
var Plugin = imports.Plugin;
var settings = imports.settings;
var debug = imports.debugger;
var prefs = imports.preferences;
var fs = imports.fs;
var tabs = imports.tabManager;
// Source Map Parser
var SourceMapConsumer = require('lib/source-map/lib/source-map/source-map-consumer').SourceMapConsumer;
var basename = require("path").basename;
var dirname = require("path").dirname;
/***** Initialization *****/
var plugin = new Plugin("Ajax.org", main.consumes);
var emit = plugin.getEmitter();
var KNOWN_MAP_TYPES = ["ts", "coffee"];
var generated = {};
var originals = {};
var maps = {};
var fetching = 0;
var loaded = false;
function load() {
if (loaded) return false;
loaded = true;
// - Add "Sourcemaps: Enable/Disable/Auto" in settings
// - (Default:auto) detects based on filename
prefs.add({
"Project": {
"Run & Debug": {
"Source Maps": {
type: "dropdown",
path: "project/debug/@sourcemaps",
width: 300,
items: [
{ caption: "Auto (check based on extension - .ts and .coffee)", value: "auto" },
{ caption: "Enabled (always check)", value: "true" },
{ caption: "Disabled (never check)", value: "false" }
],
position: 50
}
}
}
}, plugin);
settings.on("read", function() {
settings.setDefaults("project/debug", [["sourcemaps", "auto"]]);
}, plugin);
// - [Hook debugger plugin] Setting a breakpoint via Ace
// - Record that it's a sourcemap and the sourcemap path
// - [Hook debugger plugin] Setting a breakpoint through the API
// - Fetch generated file
// - Check if generated file has sourcemap
// - [Hook debugger plugin] Getting a breakpoint from the server (or using the api) - also fires breakpoint.update
// - Fetch source in background
// - If sourcemap is detected update breakpoints
debug.on("breakpointsUpdate", function(e) {
if (e.action == "add") {
var bp = e.breakpoint;
if (!isEnabled(bp.path))
return;
// check if we don't already know that it has a sourcemap
if (!bp.sourcemap || e.force) {
fetching++;
var getSourcemap = bp.serverOnly
? getSourcemapFromGenerated
: getSourcemapFromOriginal;
getSourcemap(bp.path, function(err, map) {
if (!err) {
if (!map) {
bp.sourcemap = -1;
return;
}
if (bp.actual) {
bp.actual = map.originalPositionFor({
line: bp.actual.line + 1,
column: bp.actual.column + 1
});
makeZeroBased(bp.actual);
}
else {
bp.sourcemap = map.generatedPositionFor({
line: bp.line + 1,
column: bp.column + 1,
source: basename(bp.path)
});
makeZeroBased(bp.sourcemap);
bp.sourcemap.source =
dirname(bp.path) + map.file;
}
}
fetching--;
if (!fetching)
emit("fetchingDone");
});
}
}
}, plugin);
// - [Hook debugger plugin] Make sure init waits until done
debug.on("beforeAttach", function(e) {
// Wait until all breakpoints have been checked
if (fetching) {
plugin.once("fetchingDone", function() {
debug.debug(e.runner, e.callback);
});
return false;
}
}, plugin);
// - [Hook debugger plugin] On breaking (breakpoint, debugger, stepping)
// - Keep cache of known sourcemap states (none/map_contents)
// - Deal with loading latency
// - [Hook debugger plugin] If not in cache load source (which we're doing anyway)
// - If sourcemap is detected
// - load sourcemap
// - add sourcemap to cache
// - calculate original file
// - load original file
// - update all breakpoints set on this file
// - update all frames in this file
debug.on("beforeOpen", function(e) {
if (e.generated)
return;
// Fetch the map, based on the generated file
getSourcemapFromGenerated(e.state.path, function(err, map, source) {
var jump = e.state.document.ace.jump;
if (map) {
if (!jump)
jump = { row: 0, column: 0 };
var mapping = map.originalPositionFor({
line: jump.row + 1,
column: jump.column + 1
});
// Set path, line, column
var path = dirname(e.state.path) + mapping.source; //@todo is this the correct path
e.state.path = path;
e.state.document.title = mapping.source;
jump.row = mapping.line - 1;
jump.column = mapping.column - 1;
delete e.state.value;
updateFrames();
}
else if (source) {
e.state.value = source;
}
tabs.open(e.state, function(err, tab, done) {
if (err || !done)
return e.callback(err, tab);
tabs.focusTab(tab);
fetchSource(e.state.path, function(err, value) {
if (err) return;
tab.document.value = value;
if (tab.isActive() && jump) {
tab.document.editor
.scrollTo(jump.row, jump.column, jump.select);
}
done();
e.callback(null, tab);
});
});
});
return false;
}, plugin);
// Update new frames with cached data
debug.on("framesLoad", function(e) {
var frames = e.frames;
if (!frames) return;
frames.forEach(function(frame) {
if (!frame.sourcemap && typeof generated[frame.path] == "string") {
var map = maps[generated[frame.path]];
var mapping = map.originalPositionFor({
line: frame.line + 1,
column: frame.column + 1
});
frame.line = mapping.line - 1;
frame.column = mapping.column - 1;
frame.path = dirname(frame.path) + mapping.source;
if (mapping.name)
frame.name = mapping.name;
frame.sourcemap = true;
}
// debug.updateFrame(frame, true);
});
}, plugin);
// Premature optimization
debug.on("attach", function(e) {
// Store cache in settings for accidental refreshes
settings.setJson("user/debug/sourcemaps", [generated, originals]);
}, plugin);
debug.on("detach", function(e) {
// Stop keeping cache in settings
settings.setJson("user/debug/sourcemaps", []);
}, plugin);
// - Else if in cache, load original or do nothing
// - [Hook debugger plugin] When loading the frames, check if they belong to a known sourcemapped file
// - if so, translate the coords
// @todo frames, variables, scopes
}
function makeZeroBased(obj) {
if (obj.line) obj.line--;
if (obj.column) obj.column--;
}
/***** Methods *****/
function isEnabled(path) {
var enabled = settings.get("project/debug/@sourcemaps");
return enabled == "auto"
&& KNOWN_MAP_TYPES.indexOf(fs.getExtension(path)) > -1
|| enabled == "true";
}
function fetchSource(path, callback) {
if (debug.state !== "disconnected") {
var sources = debug.sources || [];
for (var i = 0, l = sources.length; i < l; i++) {
if (sources[i].path == path) {
debug.getSource(sources[i], callback);
return;
}
}
}
fs.readFile(path, "utf8", callback);
}
function fetchMap(path, callback) {
if (maps[path])
return callback(null, maps[path]);
fetchSource(path, function(err, source) {
if (err) return callback(err);
// Create Map
var map = new SourceMapConsumer(source);
// Store paths in cache
map._sources._array.forEach(function(p) {
originals[dirname(path) + p] = path;
});
generated[dirname(path) + map.file] = path;
// Cache Map
maps[path] = map;
callback(null, map);
});
}
function getMapPath(source) {
var match = source.match(/\/\/\@ sourceMappingURL\=(.*)/);
return match ? match[1].trim() : false;
}
function detectMap(path, source, callback) {
// Find the path of the map file if any
var mapPath = getMapPath(source);
if (!mapPath) {
generated[path] = -1;
return callback(null, false, source);
}
if (mapPath.charAt(0) != "/")
mapPath = dirname(path) + mapPath;
// Fetch the map itself
fetchMap(mapPath, callback);
}
// The difficult thing with this function is that we need to somehow
// guess the map file path of this file, unless we already know it
// lets try to get as much info from map files, otherwise we'll guess
// the map file path
//
// @todo there's probably lots of room for improvement. Think about
// hooking into runner information, or having a settings file, or
// even doing a source in all files.
function getSourcemapFromOriginal(path, callback) {
var mapPath = originals[path]
|| path.substr(0, path.lastIndexOf(".")) + ".js.map";
return fetchMap(mapPath, callback);
}
function getSourcemapFromGenerated(path, callback) {
// We know there is no source map
if (generated[path] == -1)
return callback(null, false);
// We know there is a source map
if (generated[path])
return fetchMap(generated[path], callback);
// Fetch the source of the generated file
fetchSource(path, function(err, source) {
if (err) return callback(err);
detectMap(path, source, callback);
});
}
//@todo
function updateFrames() {
}
/***** Lifecycle *****/
plugin.on("load", function() {
load();
});
plugin.on("enable", function() {
});
plugin.on("disable", function() {
});
plugin.on("unload", function() {
loaded = false;
});
/***** Register and define API *****/
/**
* Adds source map support to the {@link debugger Cloud9 Debugger}.
**/
plugin.freezePublicAPI({
});
register(null, {
sourcemap: plugin
});
}
});