kopia lustrzana https://github.com/c9/core
668 wiersze
24 KiB
JavaScript
668 wiersze
24 KiB
JavaScript
define(function(require, exports, module) {
|
|
main.consumes = [
|
|
"DebugPanel", "util", "ui", "tabManager", "debugger", "save", "panels",
|
|
"Menu", "MenuItem", "dialog.error", "layout"
|
|
];
|
|
main.provides = ["callstack"];
|
|
return main;
|
|
|
|
function main(options, imports, register) {
|
|
var util = imports.util;
|
|
var DebugPanel = imports.DebugPanel;
|
|
var ui = imports.ui;
|
|
var save = imports.save;
|
|
var layout = imports.layout;
|
|
var debug = imports.debugger;
|
|
var tabs = imports.tabManager;
|
|
var panels = imports.panels;
|
|
var Menu = imports.Menu;
|
|
var MenuItem = imports.MenuItem;
|
|
var showError = imports["dialog.error"].show;
|
|
|
|
var Range = require("ace/range").Range;
|
|
var markup = require("text!./callstack.xml");
|
|
|
|
var Tree = require("ace_tree/tree");
|
|
var TreeData = require("ace_tree/data_provider");
|
|
var LineWidgets = require("ace/line_widgets").LineWidgets;
|
|
|
|
/***** Initialization *****/
|
|
|
|
var plugin = new DebugPanel("Ajax.org", main.consumes, {
|
|
caption: "Call Stack",
|
|
index: 200
|
|
});
|
|
var emit = plugin.getEmitter();
|
|
|
|
var datagrid, modelSources, modelFrames; // UI Elements
|
|
var sources = [];
|
|
var frames = [];
|
|
|
|
var activeFrame, dbg, menu, button, lastException;
|
|
|
|
var loaded = false;
|
|
function load() {
|
|
if (loaded) return false;
|
|
loaded = true;
|
|
|
|
modelSources = new TreeData();
|
|
modelSources.$sortNodes = false;
|
|
|
|
modelFrames = new TreeData();
|
|
modelFrames.emptyMessage = "No call stack to display";
|
|
modelFrames.$sortNodes = false;
|
|
|
|
modelFrames.$sorted = false;
|
|
modelFrames.columns = [{
|
|
caption: "Function",
|
|
value: "name",
|
|
width: "60%",
|
|
icon: "debugger/stckframe_obj.gif"
|
|
}, {
|
|
caption: "File",
|
|
getText: function(node) {
|
|
var path = node.path;
|
|
if (typeof path != "string")
|
|
return "";
|
|
|
|
if (path.charAt(0) === "/")
|
|
path = path.substr(1);
|
|
return path + " :" + (node.line + 1) + ":" + (node.column + 1);
|
|
},
|
|
width: "40%"
|
|
}];
|
|
|
|
// Set and clear the dbg variable
|
|
debug.on("attach", function(e) {
|
|
dbg = e.implementation;
|
|
|
|
if (button)
|
|
button.setAttribute("disabled", !dbg.features.scripts);
|
|
});
|
|
debug.on("detach", function(e) {
|
|
dbg = null;
|
|
});
|
|
debug.on("stateChange", function(e) {
|
|
if (!plugin.enabled && e.action == "enable" && activeFrame)
|
|
debug.activeFrame = activeFrame;
|
|
|
|
plugin[e.action]();
|
|
|
|
if (e.action == "disable" && e.state != "away")
|
|
clearFrames();
|
|
});
|
|
|
|
debug.on("framesLoad", function(e) {
|
|
function setFrames(frames, frame, force) {
|
|
// Load frames into the callstack and if the frames
|
|
// are completely reloaded, set active frame
|
|
var top = debug.findTopFrame(frames);
|
|
if (loadFrames(frames, top, false, force) && (force
|
|
|| !activeFrame || activeFrame == frame
|
|
|| activeFrame == top)) {
|
|
|
|
// Set the active frame
|
|
activeFrame = top;
|
|
emit("frameActivate", { frame: activeFrame });
|
|
debug.activeFrame = activeFrame;
|
|
|
|
e.frame = activeFrame;
|
|
emit("framesLoad", e);
|
|
}
|
|
}
|
|
|
|
// Load frames
|
|
if (e.frames)
|
|
return setFrames(e.frames, e.frame, true);
|
|
|
|
// If we don't have the frames yet, lets fetch them
|
|
dbg.getFrames(function(err, frames) {
|
|
setFrames(frames, e.frame);
|
|
});
|
|
|
|
// If we're most likely in the current frame, lets update
|
|
// The callstack and show it in the editor
|
|
var frame = debug.findTopFrame(frames);
|
|
if (frame && e.frame.path == frame.path
|
|
&& e.frame.sourceId == frame.sourceId) {
|
|
frame.line = e.frame.line;
|
|
frame.column = e.frame.column;
|
|
|
|
setFrames(frames, frame, true);
|
|
}
|
|
// Otherwise set the current frame as the active one, until
|
|
// we have fetched all the frames
|
|
else {
|
|
setFrames([e.frame], e.frame, true);
|
|
}
|
|
});
|
|
|
|
debug.on("break", function(e) {
|
|
if (e.exception && e.frame) {
|
|
lastException = e;
|
|
} else if (lastException) {
|
|
if (!e.frame || frameId(e.frame) != frameId(lastException.frame))
|
|
lastException = null;
|
|
}
|
|
// Show the frame in the editor
|
|
debug.showDebugFrame(activeFrame);
|
|
});
|
|
|
|
debug.on("frameActivate", function(e) {
|
|
// This is disabled, because frames should be kept around a bit
|
|
// in order to update them, for a better UX experience
|
|
//callstack.activeFrame = e.frame;
|
|
updateMarker(e.frame, true);
|
|
});
|
|
|
|
// Loading new sources
|
|
debug.on("sources", function(e) {
|
|
loadSources(e.sources);
|
|
}, plugin);
|
|
|
|
// Adding single new sources when they are compiles
|
|
debug.on("sourcesCompile", function(e) {
|
|
addSource(e.source);
|
|
}, plugin);
|
|
|
|
// Set script source when a file is saved
|
|
save.on("afterSave", function(e) {
|
|
if (debug.state == "disconnected")
|
|
return;
|
|
|
|
var script = findSourceByPath(e.path);
|
|
if (!script)
|
|
return;
|
|
|
|
if (!dbg.features.liveUpdate || debug.disabledFeatures.liveUpdate)
|
|
return;
|
|
|
|
var value = e.document.value, lastError;
|
|
dbg.setScriptSource(script, value, false, function(err) {
|
|
if (err) {
|
|
if (lastError != err.message) {
|
|
lastError = err.message;
|
|
showError(err.message);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// @todo update the UI
|
|
});
|
|
}, plugin);
|
|
}
|
|
|
|
var drawn = false;
|
|
function draw(options) {
|
|
if (drawn) return;
|
|
drawn = true;
|
|
|
|
// Create UI elements
|
|
ui.insertMarkup(options.aml, markup, plugin);
|
|
|
|
var datagridEl = plugin.getElement("datagrid");
|
|
datagrid = new Tree(datagridEl.$ext);
|
|
datagrid.renderer.setTheme({ cssClass: "blackdg" });
|
|
datagrid.setOption("maxLines", 200);
|
|
|
|
datagrid.setDataProvider(modelFrames);
|
|
panels.on("afterAnimate", function(e) {
|
|
if (panels.isActive("debugger"))
|
|
datagrid && datagrid.resize();
|
|
});
|
|
|
|
// Update markers when a document becomes available
|
|
tabs.on("tabAfterActivateSync", function(e) {
|
|
updateMarker(activeFrame);
|
|
}, plugin);
|
|
tabs.on("open", function wait(e) {
|
|
if (activeFrame)
|
|
updateMarker(activeFrame);
|
|
}, plugin);
|
|
|
|
// stack view
|
|
datagrid.on("userSelect", function(e) {
|
|
var frame = datagrid.selection.getCursor();
|
|
setActiveFrame(frame, true);
|
|
});
|
|
|
|
var contextMenu = new Menu({
|
|
items: [
|
|
new MenuItem({ value: "restart", caption: "Restart Frame" }),
|
|
// new MenuItem({ value: "edit2", caption: "Edit Watch Value" })
|
|
]
|
|
}, plugin);
|
|
contextMenu.on("itemclick", function(e) {
|
|
if (e.value == "restart")
|
|
dbg.restartFrame(activeFrame, function() {});
|
|
});
|
|
contextMenu.on("show", function(e) {
|
|
var selected = datagrid.selection.getCursor();
|
|
contextMenu.items[0].disabled = selected && dbg ? false : true;
|
|
});
|
|
|
|
datagridEl.setAttribute("contextmenu", contextMenu.aml);
|
|
|
|
var hbox = debug.getElement("hbox");
|
|
menu = hbox.ownerDocument.documentElement.appendChild(new ui.menu({
|
|
style: "top: 56px;"
|
|
+ "left: 803px;"
|
|
+ "width: 350px;"
|
|
+ "opacity: 1;"
|
|
+ "border: 0px;"
|
|
+ "padding: 0px;"
|
|
+ "background-color: transparent;"
|
|
+ "margin: -3px 0px 0px;"
|
|
+ "box-shadow: none;",
|
|
childNodes: [
|
|
]
|
|
}));
|
|
button = hbox.appendChild(new ui.button({
|
|
id: "btnScripts",
|
|
tooltip: "Available internal and external scripts",
|
|
icon: "scripts.png",
|
|
right: "0",
|
|
top: "0",
|
|
class: "scripts",
|
|
skin: "c9-menu-btn",
|
|
disabled: !dbg || !dbg.features.scripts
|
|
}));
|
|
plugin.addElement(menu, button);
|
|
|
|
menu.on("prop.visible", function(e) {
|
|
if (!e.value || menu.reopen)
|
|
return;
|
|
|
|
list.resize();
|
|
menu.resize();
|
|
});
|
|
|
|
menu.resize = function() {
|
|
if (!menu.visible) return;
|
|
|
|
list.renderer.setOption("maxLines", Math.floor(window.innerHeight / 28 * 3 / 4));
|
|
setTimeout(function() {
|
|
if (menu.opener) {
|
|
menu.reopen = true;
|
|
menu.$ext.style.overflowY = "";
|
|
menu.display(null, null, true, menu.opener);
|
|
menu.$ext.style.overflowY = "";
|
|
menu.reopen = false;
|
|
}
|
|
}, 10);
|
|
};
|
|
|
|
// Load the scripts in the sources dropdown
|
|
var list = new Tree();
|
|
menu.$ext.appendChild(list.container);
|
|
list.setDataProvider(modelSources);
|
|
list.renderer.setTheme({ cssClass: "blackdg" });
|
|
list.on("click", function(e) {
|
|
var selected = list.selection.getCursor();
|
|
debug.openFile({
|
|
scriptId: selected.id,
|
|
path: selected.path,
|
|
generated: true
|
|
});
|
|
menu.hide();
|
|
}, plugin);
|
|
list.renderer.setScrollMargin(10, 10);
|
|
list.container.className = "ace_tree c9menu list_dark";
|
|
list.container.style.width = "inherit";
|
|
// Set context menu to the button
|
|
button.setAttribute("submenu", menu);
|
|
|
|
layout.on("eachTheme", function(e) {
|
|
var height = parseInt(ui.getStyleRule(".blackdg .row", "height"), 10) || 24;
|
|
// modelFrames.rowHeightInner = height - 1;
|
|
modelFrames.rowHeight = height;
|
|
modelSources.rowHeight = height;
|
|
|
|
if (e.changed) datagrid.resize(true);
|
|
});
|
|
|
|
}
|
|
|
|
function setActiveFrame(frame, fromDG) {
|
|
activeFrame = frame;
|
|
if (!frames.length) return;
|
|
|
|
if (!fromDG && datagrid) {
|
|
// Select the frame in the UI
|
|
if (!frame) {
|
|
modelFrames.setRoot({});
|
|
frames = [];
|
|
}
|
|
else {
|
|
datagrid.select(frame);
|
|
}
|
|
}
|
|
|
|
// Highlight frame in Ace and Open the file
|
|
if (frame) {
|
|
debug.showDebugFrame(frame, function() {
|
|
updateMarker(frame);
|
|
});
|
|
}
|
|
|
|
emit("frameActivate", { frame: activeFrame });
|
|
debug.activeFrame = activeFrame;
|
|
}
|
|
|
|
/***** Helper Functions *****/
|
|
|
|
function addMarker(session, type, row) {
|
|
var marker = session.addMarker(new Range(row, 0, row, 1), "ace_" + type, "fullLine");
|
|
session.addGutterDecoration(row, type);
|
|
session["$" + type + "Marker"] = { lineMarker: marker, row: row };
|
|
}
|
|
|
|
function removeMarker(session, type) {
|
|
var markerName = "$" + type + "Marker";
|
|
session.removeMarker(session[markerName].lineMarker);
|
|
session.removeGutterDecoration(session[markerName].row, type);
|
|
session[markerName] = null;
|
|
}
|
|
|
|
function removeMarkerFromSession(session) {
|
|
session.$stackMarker && removeMarker(session, "stack");
|
|
session.$stepMarker && removeMarker(session, "step");
|
|
session.$exceptionWidget && session.$exceptionWidget.destroy();
|
|
}
|
|
|
|
function addExceptionWidget(editor, ev) {
|
|
var session = editor.session;
|
|
if (!session.widgetManager) {
|
|
session.widgetManager = new LineWidgets(session);
|
|
session.widgetManager.attach(editor);
|
|
}
|
|
|
|
var oldWidget = session.$exceptionWidget;
|
|
if (oldWidget)
|
|
oldWidget.destroy();
|
|
|
|
var row = ev.frame.line;
|
|
var column = ev.frame.column || 0;
|
|
var w = {
|
|
row: row,
|
|
fixedWidth: true,
|
|
coverGutter: true,
|
|
el: document.createElement("div"),
|
|
type: "debuggerException"
|
|
};
|
|
var el = w.el.appendChild(document.createElement("div"));
|
|
var arrow = w.el.appendChild(document.createElement("div"));
|
|
arrow.className = "error_widget_arrow ace_error";
|
|
|
|
var left = editor.renderer.$cursorLayer
|
|
.getPixelPosition({ row: row, column: column }).left;
|
|
arrow.style.left = left + editor.renderer.gutterWidth - 5 + "px";
|
|
|
|
w.el.className = "error_widget_wrapper";
|
|
el.className = "error_widget ace_error";
|
|
el.textContent = ev.exception.value;
|
|
el.appendChild(document.createElement("div"));
|
|
|
|
w.destroy = function() {
|
|
session.$exceptionWidget = null;
|
|
session.off("change", w.destroy);
|
|
session.widgetManager.removeLineWidget(w);
|
|
};
|
|
session.$exceptionWidget = w;
|
|
session.on("change", w.destroy);
|
|
|
|
editor.session.widgetManager.addLineWidget(w);
|
|
|
|
w.el.onmousedown = function(e) {
|
|
e.stopPropagation();
|
|
};
|
|
|
|
// TODO add buttons to: close, disable break on exception, not break on current line
|
|
|
|
editor.renderer.scrollCursorIntoView(null, 0.5, { bottom: w.el.offsetHeight });
|
|
}
|
|
|
|
function updateMarker(frame, scrollToLine) {
|
|
// Remove from all active sessions, when there is no active frame.
|
|
if (!frame) {
|
|
tabs.getPanes().forEach(function(pane) {
|
|
var tab = pane.getTab();
|
|
if (tab && tab.editor && tab.editor.type == "ace") {
|
|
var session = tab.document.getSession().session;
|
|
removeMarkerFromSession(session);
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Otherwise find the active session and set the marker
|
|
var tab = frame && tabs.findTab(frame.path);
|
|
var editor = tab && tab.isActive() && tab.editor;
|
|
if (!editor || editor.type != "ace")
|
|
return;
|
|
|
|
var session = tab.document.getSession().session;
|
|
removeMarkerFromSession(session);
|
|
|
|
if (!frame)
|
|
return;
|
|
|
|
var path = tab.path;
|
|
var framePath = frame.path;
|
|
var row = frame.line;
|
|
|
|
if (frame.istop) {
|
|
if (path == framePath || path == "/" + framePath) {
|
|
if (row >= session.getLength())
|
|
row = session.getLength() - 1;
|
|
|
|
addMarker(session, "step", row);
|
|
|
|
if (scrollToLine) {
|
|
var ace = tab.editor.ace;
|
|
var renderer = ace.renderer;
|
|
if (row < renderer.getFirstFullyVisibleRow()
|
|
|| row > renderer.getLastFullyVisibleRow()) {
|
|
ace.scrollToLine(row, true, true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
if (path == framePath || path == "/" + framePath)
|
|
addMarker(session, "stack", row);
|
|
|
|
var topFrame = debug.findTopFrame(frames);
|
|
if (path == topFrame.path)
|
|
addMarker(session, "step", topFrame.line);
|
|
}
|
|
|
|
if (lastException && frameId(frame) == frameId(lastException.frame)) {
|
|
addExceptionWidget(editor.ace, lastException);
|
|
}
|
|
}
|
|
|
|
/***** Methods *****/
|
|
|
|
function findSourceByPath(path) {
|
|
for (var i = 0, l = sources.length, source; i < l; i++) {
|
|
if ((source = sources[i]).path == path)
|
|
return source;
|
|
}
|
|
}
|
|
|
|
function findSource(id) {
|
|
if (typeof id == "object") {
|
|
id = parseInt(id.getAttribute("id"), 10);
|
|
}
|
|
|
|
for (var i = 0, l = sources.length, source; i < l; i++) {
|
|
if ((source = sources[i]).id == id)
|
|
return source;
|
|
}
|
|
}
|
|
|
|
function findFrame(index) {
|
|
if (typeof index == "object") {
|
|
index = parseInt(index.getAttribute("index"), 10);
|
|
}
|
|
|
|
for (var i = 0, l = frames.length, frame; i < l; i++) {
|
|
if ((frame = frames[i]).index == index)
|
|
return frame;
|
|
}
|
|
}
|
|
|
|
function frameId(frame) {
|
|
return [frame.path, frame.line, frame.column, frame.sourceId].join(":");
|
|
}
|
|
/**
|
|
* Assumptions:
|
|
* - .index stays the same
|
|
* - sequence in the array stays the same
|
|
* - ref stays the same when stepping in the same context
|
|
*/
|
|
|
|
function updateFrame(frame, noRecur) {
|
|
modelFrames._signal("change", frame);
|
|
if (noRecur)
|
|
return;
|
|
|
|
// Updating the scopes of a frame
|
|
if (frame.variables) {
|
|
emit("scopeUpdate", {
|
|
scope: frame,
|
|
variables: frame.variables
|
|
});
|
|
}
|
|
else {
|
|
dbg.getScope(activeFrame, frame, function(err, vars) {
|
|
if (err) return console.error(err);
|
|
|
|
emit("scopeUpdate", {
|
|
scope: frame,
|
|
variables: vars
|
|
});
|
|
});
|
|
}
|
|
|
|
// Update scopes if already loaded
|
|
frame.scopes && frame.scopes.forEach(function(scope) {
|
|
if (scope.variables)
|
|
emit("scopeUpdate", { scope: scope });
|
|
});
|
|
}
|
|
|
|
function loadFrames(input, top, noRecur, force) {
|
|
frames = input;
|
|
modelFrames.setRoot(frames);
|
|
|
|
if (activeFrame && frames.indexOf(activeFrame) > -1)
|
|
setActiveFrame(activeFrame);
|
|
else
|
|
setActiveFrame(top);
|
|
|
|
for (var i = 0, l = input.length; i < l; i++)
|
|
updateFrame(input[i], noRecur);
|
|
|
|
return true;
|
|
}
|
|
|
|
function loadSources(input) {
|
|
sources = input;
|
|
modelSources.setRoot(sources);
|
|
}
|
|
|
|
function clearFrames() {
|
|
setActiveFrame(null);
|
|
}
|
|
|
|
function addSource(source) {
|
|
sources.push(source);
|
|
modelSources.setRoot(sources);
|
|
}
|
|
|
|
function updateAll() {
|
|
modelFrames.setRoot(frames);
|
|
}
|
|
|
|
/***** Lifecycle *****/
|
|
|
|
plugin.on("load", function() {
|
|
load();
|
|
plugin.once("draw", draw);
|
|
});
|
|
plugin.on("enable", function() {
|
|
if (drawn) {
|
|
menu.enable();
|
|
button.setAttribute("disabled", dbg && !dbg.features.scripts);
|
|
datagrid.enable();
|
|
}
|
|
});
|
|
plugin.on("disable", function() {
|
|
if (drawn) {
|
|
menu.disable();
|
|
button.disable();
|
|
datagrid.disable();
|
|
}
|
|
});
|
|
plugin.on("unload", function() {
|
|
loaded = false;
|
|
drawn = false;
|
|
});
|
|
|
|
/***** Register and define API *****/
|
|
|
|
/**
|
|
* The call stack panel for the {@link debugger Cloud9 debugger}.
|
|
*
|
|
* This panel allows a user to inspect the call stack and jump to the
|
|
* different items in the stack.
|
|
*
|
|
* @singleton
|
|
* @extends DebugPanel
|
|
**/
|
|
plugin.freezePublicAPI({
|
|
/**
|
|
* When the debugger has hit a breakpoint or an exception, it breaks
|
|
* and shows the active frame in the callstack panel. The active
|
|
* frame represents the scope at which the debugger is stopped.
|
|
* @property {debugger.Frame} activeFrame
|
|
*/
|
|
get activeFrame() { return activeFrame; },
|
|
set activeFrame(frame) { setActiveFrame(frame); },
|
|
/**
|
|
* A list of sources that are available from the debugger. These
|
|
* can be files that are loaded in the runtime as well as code that
|
|
* is injected by a script or by the runtime itself.
|
|
* @property {debugger.Source[]} sources
|
|
* @readonly
|
|
*/
|
|
get sources() { return sources; },
|
|
/**
|
|
* A list (or stack) of frames that make up the call stack. The
|
|
* frames are in order and the index 0 contains the frame where
|
|
* the debugger is breaked on.
|
|
* @property {debugger.Frame[]} frames
|
|
* @readonly
|
|
*/
|
|
get frames() { return frames; },
|
|
|
|
/**
|
|
* Updates all frames in the call stack UI.
|
|
*/
|
|
updateAll: updateAll,
|
|
|
|
/**
|
|
* Updates a specific frame in the call stack UI
|
|
* @param {debugger.Frame} frame The frame to update.
|
|
*/
|
|
updateFrame: updateFrame
|
|
});
|
|
|
|
register(null, {
|
|
callstack: plugin
|
|
});
|
|
}
|
|
});
|