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 }); } });