define(function(require, exports, module) { main.consumes = [ "DebugPanel", "settings", "ui", "util", "debugger", "ace", "commands", "menus", "Menu", "MenuItem", "Divider", "panels", "layout" ]; main.provides = ["watches"]; return main; function main(options, imports, register) { var DebugPanel = imports.DebugPanel; var settings = imports.settings; var ui = imports.ui; var debug = imports.debugger; var util = imports.util; var layout = imports.layout; var menus = imports.menus; var commands = imports.commands; var ace = imports.ace; var Menu = imports.Menu; var MenuItem = imports.MenuItem; var Divider = imports.Divider; var panels = imports.panels; var keys = require("ace/lib/keys"); var markup = require("text!./watches.xml"); var Variable = require("./data/variable"); var Tree = require("ace_tree/tree"); var TreeData = require("./variablesdp"); var TreeEditor = require("ace_tree/edit"); /***** Initialization *****/ var plugin = new DebugPanel("Ajax.org", main.consumes, { caption: "Watch Expressions", index: 100 }); var emit = plugin.getEmitter(); var count = 0; var watches = []; var dirty = false; var dbg, model, datagrid, errorWatch; var loaded = false; function load() { if (loaded) return false; loaded = true; model = new TreeData(); model.emptyMessage = "Type an expression here..."; model.$sortNodes = false; model.loadChildren = function(node, callback) { if (node.isNew) return callback(true); emit("expand", { variable: node, expand: callback }); }; model.columns = [{ caption: "Expression", match: "name", value: "name", width: "60%", icon: true, type: "tree", editor: "textbox" }, { caption: "Value", value: "value", width: "40%", editor: "textbox" }, { caption: "Type", value: "type", width: "55" }]; // Set and clear the dbg variable debug.on("attach", function(e) { dbg = e.implementation; updateAll(); }); debug.on("detach", function(e) { dbg = null; }); debug.on("stateChange", function(e) { if (errorWatch) removeWatch(errorWatch); plugin[e.action](); if (e.action == "enable") updateAll(); }); debug.on("framesLoad", function(e) { // Update Watchers updateAll(); }); debug.on("exception", function(e) { if (errorWatch) removeWatch(errorWatch); errorWatch = e.exception; addWatch(errorWatch); model.expand(errorWatch); }); plugin.on("expand", function(e) { if (e.variable.parent == model.root) return e.expand && e.expand(e.variable.error); dbg.getProperties(e.variable, function(err, properties) { updateVariable(e.variable, properties); e.expand && e.expand(); }); }); // Add Watch hook into ace commands.addCommand({ name: "addwatchfromselection", bindKey: { mac: "Command-Shift-C", win: "Ctrl-Shift-C" }, hint: "Add the selection as a watch expression", isAvailable: function(editor) { var ace = dbg && editor && editor.ace; return ace && !ace.selection.isEmpty(); }, exec: function(editor) { if (!editor.ace.selection.isEmpty()) addWatch(editor.ace.getCopyText()); } }, plugin); // right click context item in ace ace.getElement("menu", function(menu) { menus.addItemToMenu(menu, new ui.item({ caption: "Add As Watch Expression", command: "addwatchfromselection" }), 600, plugin); }); // restore the variables from the IDE settings settings.on("read", function (e) { (settings.getJson("state/watches") || []).forEach(function(name) { watches.push(new Variable({ name: name, ref: "fromsettings" + count++ })); }); reloadModel(); if (dbg) updateAll(); }); settings.on("write", function (e) { if (dirty) { settings.setJson("state/watches", watches.filter(function(w) { return w !== errorWatch; }).map(function(w) { return w.name; })); dirty = false; } }); } var drawn; 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.setTheme({ cssClass: "blackdg" }); datagrid.setOption("maxLines", 200); layout.on("eachTheme", function(e) { var height = parseInt(ui.getStyleRule(".blackdg .row", "height"), 10) || 24; // model.rowHeightInner = height - 1; model.rowHeight = height; if (e.changed) datagrid.resize(true); }); datagrid.setDataProvider(model); datagrid.edit = new TreeEditor(datagrid); panels.on("afterAnimate", function(e) { if (panels.isActive("debugger")) datagrid && datagrid.resize(); }); reloadModel(); var contextMenu = new Menu({ items: [ new MenuItem({ value: "edit1", caption: "Edit Watch Expression" }), new MenuItem({ value: "edit2", caption: "Edit Watch Value" }), new Divider(), new MenuItem({ value: "remove", caption: "Remove Watch Expression" }), ] }, plugin); contextMenu.on("itemclick", function(e) { if (e.value == "edit1") datagrid.edit.startRename(null, 0); else if (e.value == "edit2") datagrid.edit.startRename(null, 1); else if (e.value == "remove") datagrid.execCommand("delete"); }); contextMenu.on("show", function(e) { var selected = datagrid.selection.getCursor(); var isNew = selected && selected.isNew; var isProp = selected.parent != model.root; contextMenu.items[0].disabled = !selected || isProp; contextMenu.items[1].disabled = !selected || !!isNew; contextMenu.items[3].disabled = !selected || !!isNew || isProp; }); datagridEl.setAttribute("contextmenu", contextMenu.aml); datagrid.on("delete", function(e) { var nodes = datagrid.selection.getSelectedNodes(); nodes.forEach(function (node) { var idx = watches.indexOf(node); if (idx != -1) { model._signal("remove", node); watches.splice(idx, 1); } }); reloadModel(); dirty = true; settings.save(); }); var justEdited = false; datagrid.on("rename", function(e) { var node = e.node; var name = e.value; var value = node.value; var isNew = node.isNew; var column = e.column; var parents = []; var variable, oldValue; // Delete a watch by removing the expression if (!name) { datagrid.execCommand("delete"); return; } // If we've filled a new watch remove the new attribute if (isNew) { variable = new Variable({ name: name, value: value, ref: node.ref }); watches.push(variable); } else { // variable = node; if (watches.indexOf(node) != -1) variable = node; else if (node.ref) variable = findVariable(node.ref, parents); if (column.value == "value") { oldValue = variable.value; value = variable.value = name; } else { variable.name = name; isNew = true; } if (variable.error) { isNew = true; variable.ref = null; } } dirty = true; settings.save(); reloadModel(); setWatch(variable, value, isNew, oldValue, node, parents); }); datagrid.on("beforeRename", function(e) { // Don't allow setting the value of new variables if (e.column.caption == "Value" && (e.node.ref + "").substr(0, 3) == "new") { datagrid.edit.startRename(null, 0); return e.preventDefault(); } // When editing a property name, always force editing the value if (e.column.caption == "Expression" && e.node.parent != model.root) { datagrid.edit.startRename(null, 1); return e.preventDefault(); } if (e.column.caption != "Expression" && dbg && !dbg.features.updateWatchedVariables) return e.preventDefault(); }); datagrid.on("rename", function(e) { justEdited = true; setTimeout(function() { justEdited = false; }, 500); }); datagrid.textInput.getElement().addEventListener("keydown", function(e) { var cursor = datagrid.selection.getCursor(); var key = keys[e.keyCode] || ""; if (key.length == 1 || key.substr(0, 3) == "num" && cursor && !justEdited) datagrid.edit.startRename(cursor, 0); }, true); datagrid.textInput.getElement().addEventListener("keyup", function(e) { var cursor = datagrid.selection.getCursor(); if (e.keyCode == 13 && cursor && !justEdited) datagrid.edit.startRename(cursor, 0); }, true); } /***** Methods *****/ function addWatch(expression) { var variable; if (expression instanceof Variable) { variable = expression; } else { variable = new Variable({ name: expression, value: "", ref: "" }); setWatch(variable, null, true, null, {}, []); } watches.push(variable); reloadModel(); dirty = true; settings.save(); } function removeWatch(variable) { watches.splice(watches.indexOf(variable), 1); reloadModel(); dirty = true; settings.save(); } function setWatch(variable, value, isNew, oldValue, node, parents) { if (!dbg) return; // We've apparently already disconnected. variable.status = "pending"; // Editing watches in the current or global frame // Execute expression if (isNew) { dbg.evaluate(variable.name, debug.activeFrame, !debug.activeFrame, true, function(err, serverVariable) { if (err) { variable.json = { name: variable.name, value: err.message, error: true }; updateVariable(variable, [], node, true); return; } variable.json = serverVariable.json; updateVariable(variable, variable.properties || [], node); }); } // Set new value of a property else { dbg.setVariable(variable, value, debug.activeFrame, function(err) { if (err) { variable.value = oldValue; updateVariable(variable, [], node, true); return; } // Reload properties of the variable dbg.getProperties(variable, function(err, properties) { updateVariable(variable, properties, node); }); }); } emit("setWatch", { name: node.name, value: value, node: node, isNew: isNew, variable: variable, parents: parents }); } function updateAll() { watches.forEach(function(variable) { setWatch(variable, undefined, true, null, variable, []); }); } function findVariable(ref, parents) { if (typeof ref == "object") ref = ref.getAttribute("ref"); var result; for (var i = 0, l = watches.length; i < l; i++) { if (watches[i].ref == ref) return watches[i]; result = watches[i].findVariable(ref, null, parents); if (result) return result; } } function updateVariable(variable, properties, node, error) { // Pass node for recursive trees variable.error = error; if (!variable.parent || variable.parent == model.root) reloadModel(); else model.updateNode(variable); } function reloadModel() { model.newWatchNode = model.newWatchNode || { name: model.emptyMessage, className: "newwatch", fullWidth: true, isNew: true, }; model.setRoot({ items: [].concat(watches, model.newWatchNode), $sorted: true }); } /***** Lifecycle *****/ plugin.on("load", function() { load(); plugin.once("draw", draw); }); plugin.on("enable", function() { }); plugin.on("disable", function() { }); plugin.on("unload", function() { loaded = false; drawn = false; }); /***** Register and define API *****/ /** * The watch expression panel for the {@link debugger Cloud9 debugger}. * * This panel allows a user to add small expressions that are evaluated * continuously, displaying the result of the expression in the UI. This * allows a user to monitor what is going on while stepping through the * code. * * @singleton * @extends DebugPanel **/ plugin.freezePublicAPI({ /** * A list of variables that are watched. * @param {debugger.Variable[]} watches The list of variables watched. */ get watches() { return watches; }, /** * Re-evaluate all watch expressions. */ updateAll: updateAll }); register(null, { watches: plugin }); } });