diff --git a/client/apf/elements/codeeditor.js b/client/apf/elements/codeeditor.js new file mode 100644 index 00000000000..e27a425ceb3 --- /dev/null +++ b/client/apf/elements/codeeditor.js @@ -0,0 +1,684 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + * + */ + +// #ifdef __AMLCODEEDITOR || __INC_ALL + +/** + * Element allowing the user to type code. + * + * @constructor + * @define codeeditor + * @addnode elements + * + * @inherits apf.StandardBinding + * + * @author Ruben Daniels (ruben AT ajax DOT org) + * @author Fabian Jakobs (fabian AT ajax DOT org) + * @version %I%, %G% + * @since 0.1 + */ + +define(function(require, exports, module) { + +var Editor = require("ace/editor"); +var EditSession = require("ace/edit_session"); +var VirtualRenderer = require("ace/virtual_renderer"); +var UndoManager = require("ace/undomanager"); +var Range = require("ace/range"); +require("ace/lib/fixoldbrowsers"); + +Editor = Editor.Editor; +EditSession = EditSession.EditSession; +VirtualRenderer = VirtualRenderer.VirtualRenderer; +UndoManager = UndoManager.UndoManager; +Range = Range.Range; + +apf.codeeditor = module.exports = function(struct, tagName) { + this.$init(tagName || "codeeditor", apf.NODE_VISIBLE, struct); + + this.documents = []; + this.$cache = {}; + + //this.setProperty("overwrite", false); + this.setProperty("line", 1); + this.setProperty("col", 1); +}; + +(function() { + this.implement( + //#ifdef __WITH_DATAACTION + apf.DataAction + //#endif + ); + + this.$focussable = true; // This object can get the focus + this.$childProperty = "value"; + this.$isTextInput = true; + + this.value = ""; + this.multiline = true; + this.caching = true; + + this.$booleanProperties["activeline"] = true; + this.$booleanProperties["caching"] = true; + this.$booleanProperties["readonly"] = true; + this.$booleanProperties["activeline"] = true; + this.$booleanProperties["showinvisibles"] = true; + this.$booleanProperties["showprintmargin"] = true; + this.$booleanProperties["overwrite"] = true; + this.$booleanProperties["softtabs"] = true; + this.$booleanProperties["gutter"] = true; + this.$booleanProperties["highlightselectedword"] = true; + this.$booleanProperties["autohidehorscrollbar"] = true; + this.$booleanProperties["behaviors"] = true; + this.$booleanProperties["folding"] = true; + + this.$supportedProperties.push("value", "syntax", "activeline", "selectstyle", + "caching", "readonly", "showinvisibles", "showprintmargin", "printmargincolumn", + "overwrite", "tabsize", "softtabs", "debugger", "model-breakpoints", "scrollspeed", + "theme", "gutter", "highlightselectedword", "autohidehorscrollbar", + "behaviors", "folding"); + + this.$getCacheKey = function(value) { + if (typeof value == "string") { + var key = this.xmlRoot + ? this.xmlRoot.getAttribute(apf.xmldb.xmlIdTag) + : value; + } + else if (value.nodeType) { + var key = value.getAttribute(apf.xmldb.xmlIdTag); + } + + return key; + }; + + this.clearCacheItem = function(xmlNode) { + if (!this.caching) + return; + + var key = this.$getCacheKey(xmlNode); + if (key) + delete this.$cache[key]; + }; + + this.addEventListener("unloadmodel", function() { + this.syncValue(); + }); + + /** + * @attribute {String} value the text of this element + * @todo apf3.0 check use of this.$propHandlers["value"].call + */ + this.$propHandlers["value"] = function(value){ //@todo apf3.0 add support for the range object as a value + var doc, key, + _self = this; + + if (this.caching) + key = this.$getCacheKey(value); + + //Assuming document + if (value instanceof EditSession) + doc = value; + + if (!doc && key) + doc = this.$cache[key]; + + if (!doc) { + if (value.nodeType) { + apf.xmldb.addNodeListener(value.nodeType == 1 + ? value : value.parentNode, this); + } + + doc = new EditSession(typeof value == "string" + ? value + : (value.nodeType > 1 && value.nodeType < 5 //@todo replace this by a proper function + ? value.nodeValue + : value.firstChild && value.firstChild.nodeValue || "")); + + doc.cacheId = key; + doc.setUndoManager(new UndoManager()); + + if (key) + this.$cache[key] = doc; + } + //@todo value can also be an xml node and should be updated in a similar fashion as above + else if (typeof value == "string" && !doc.hasValue) { + //@todo big hack! + doc.setValue(value); + this.$editor.moveCursorTo(0, 0); + doc.hasValue = true; + } + + _self.$getMode(_self.syntax, function(mode) { + doc.setMode(mode); + }); + + doc.setTabSize(parseInt(_self.tabsize, 10)); + doc.setUseSoftTabs(_self.softtabs); + doc.setUseWrapMode(_self.wrapmode); + doc.setWrapLimitRange(_self.wraplimitmin, _self.wraplimitmax); + doc.setFoldStyle(_self.folding ? "markbegin" : "manual"); + + _self.$removeDocListeners && _self.$removeDocListeners(); + _self.$removeDocListeners = _self.$addDocListeners(doc); + + _self.$editor.setShowPrintMargin(_self.showprintmargin); + + // remove existing markers + _self.$clearMarker(); + + _self.$editor.setSession(doc); + + _self.$updateMarker(); + _self.$updateBreakpoints(doc); + }; + + this.$addDocListeners = function(doc) { + var _self = this; + var onCursorChange = function() { + var cursor = doc.getSelection().getCursor(); + _self.setProperty("line", cursor.row+1); + _self.setProperty("col", cursor.column+1); + }; + + doc.getSelection().addEventListener("changeCursor", onCursorChange); + + onCursorChange(); + + return function() { + doc.getSelection().removeEventListener("changeCursor", onCursorChange); + }; + }; + + this.$clearMarker = function () { + if (this.$marker) { + this.$editor.renderer.removeGutterDecoration(this.$lastRow[0], this.$lastRow[1]); + this.$editor.getSession().removeMarker(this.$marker); + this.$marker = null; + } + }; + + /** + * Indicates whether we are going to set a marker + */ + this.$updateMarkerPrerequisite = function () { + if (!this.$debugger) { + return; + } + var frame = this.$debugger.activeframe; + if (!frame) { + return; + } + + // when running node with 'debugbrk' it will auto break on the first line of executable code + // we don't want to really break here so we put this: + if (frame.getAttribute("name") === "anonymous(exports, require, module, __filename, __dirname)" + && frame.getAttribute("index") === "0" && frame.getAttribute("line") === "0") { + + var fileNameNode = frame.selectSingleNode("//frame/vars/item[@name='__filename']"); + var fileName = fileNameNode ? fileNameNode.getAttribute("value") : ""; + var model = this["model-breakpoints"].data; + + // is there a breakpoint on the exact same line and file? then continue + if (fileName && model && model.selectSingleNode("//breakpoints/breakpoint[@script='" + fileName + "' and @line=0]")) { + return frame; + } + + return; + } + + return frame; + }; + + this.$updateMarker = function () { + this.$clearMarker(); + + var frame = this.$updateMarkerPrerequisite(); + if (!frame) { + return; + } + + var script = this.xmlRoot; + if (script.getAttribute("scriptid") !== frame.getAttribute("scriptid")) { + return; + } + + var head = this.$debugger.$mdlStack.queryNode("frame[1]"); + var isTop = frame == head; + var lineOffset = parseInt(script.getAttribute("lineoffset") || "0", 10); + var row = parseInt(frame.getAttribute("line"), 10) - lineOffset; + var range = new Range(row, 0, row + 1, 0); + this.$marker = this.$editor.getSession().addMarker(range, isTop ? "ace_step" : "ace_stack", "line"); + var type = isTop ? "arrow" : "stack"; + this.$lastRow = [row, type]; + this.$editor.renderer.addGutterDecoration(row, type); + this.$editor.gotoLine(row + 1, parseInt(frame.getAttribute("column"), 10)); + }; + + this.$updateBreakpoints = function(doc) { + doc = doc || this.$editor.getSession(); + + doc.setBreakpoints([]); + if (!this.$breakpoints) + return; + + if (this.xmlRoot) { + var scriptName = this.xmlRoot.getAttribute("scriptname"); + if (!scriptName) + return; + + var breakpoints = this.$breakpoints.queryNodes("//breakpoint[@script='" + scriptName + "']"); + + var rows = []; + for (var i=0; i"); + } + model.load("" + xml.join("") + ""); + }); + }); + }; + + this.attach = function(tabId, callback) { + var dbg; + + if (dbg = this.$debuggers[tabId]) + return callback(null, dbg) + + var self = this; + this.$connect(function() { + self.$v8ds.attach(tabId, function() { + dbg = new APFV8Debugger(new V8Debugger(tabId, self.$v8ds), this); + self.$debuggers[tabId] = dbg; + callback(null, dbg); + }); + }); + }; + + this.detach = function(dbg, callback) { + var self = this; + for (var id in this.$debuggers) { + if (this.$debuggers[id] == dbg) { + this.$v8ds.detach(id, function(err) { + delete self.$debuggers[id]; + dbg.dispatchEvent("detach"); + callback && callback(err); + }); + break; + } + } + }; + + this.disconnect = function(callback) { + var debuggers = []; + for (var id in this.$debuggers) { + debuggers.push(id); + } + + var self = this; + var detachNext = function() { + if (debuggers.length) { + var id = debuggers.shift(); + var dbg = self.$debuggers[id] + self.$v8ds.detach(id, function() { + detachNext(); + dbg.dispatchEvent("detach"); + }); + } else { + self.$socket.close(); + self.$debuggers = {}; + self.dispatchEvent("disconnect", {}); + callback && callback(); + } + } + + detachNext(); + }; + +}).call(ChromeDebugHost.prototype = new apf.Class()); + +}); +// #endif \ No newline at end of file diff --git a/client/apf/elements/dbg/v8debugger.js b/client/apf/elements/dbg/v8debugger.js new file mode 100644 index 00000000000..1868c5c8e28 --- /dev/null +++ b/client/apf/elements/dbg/v8debugger.js @@ -0,0 +1,529 @@ +// #ifdef __AMLDEBUGGER || __INC_ALL +if (apf.hasRequireJS) define("apf/elements/dbg/v8debugger", + ["module", "debug/Breakpoint"], + function(module, Breakpoint) { + +var V8Debugger = module.exports = function(dbg, host) { + this.$init(); + + this.$debugger = dbg; + this.$host = host; + + this.$breakpoints = {}; + + var _self = this; + dbg.addEventListener("changeRunning", function(e) { + _self.dispatchEvent("changeRunning", e); + if (dbg.isRunning()) { + _self.setFrame(null); + } + }); + dbg.addEventListener("break", function(e) { + _self.dispatchEvent("break", e); + }); + dbg.addEventListener("afterCompile", function(e) { + _self.dispatchEvent("afterCompile", {script: apf.getXml(_self.$getScriptXml(e.data.script))}); + }); + + this.setFrame(null); +}; + +(function() { + var hasChildren = { + "object": 8, + "function": 4 + }; + + this.stripPrefix = ""; + + this.setStrip = function(stripPrefix) { + this.stripPrefix = stripPrefix; + }; + + this.$strip = function(str) { + if (!this.stripPrefix) + return str; + + return str.indexOf(this.stripPrefix) === 0 + ? str.slice(this.stripPrefix.length) + : str; + }; + + this.isRunning = function() { + return this.$debugger.isRunning(); + }; + + this.scripts = function(model, callback) { + var _self = this; + this.$debugger.scripts(4, null, false, function(scripts) { + var xml = []; + for (var i = 0, l = scripts.length; i < l; i++) { + var script = scripts[i]; + if (script.name && script.name.indexOf("chrome-extension://") === 0) + continue; + xml.push(_self.$getScriptXml(script)); + } + model.load("" + xml.join("") + ""); + callback(); + }); + }; + + this.$getScriptXml = function(script) { + return [ + "" + ].join(""); + }; + + function getId(frame){ + return (frame.func.name || frame.func.inferredName || (frame.line + frame.position)); + } + + this.$isEqual = function(xmlFrameSet, frameSet){ + if (xmlFrameSet.length != frameSet.length) + return false; + + var xmlFirst = xmlFrameSet[0]; + var first = frameSet[0]; + if (xmlFirst.getAttribute("scriptid") != first.func.scriptId) + return false; + if (xmlFirst.getAttribute("id") != getId(first)) + return false; + //if (xmlFirst.selectNodes("vars/item").length != (1 + first.arguments.length + first.locals.length)) + //return false; + + //@todo check for ref?? might fail for 2 functions in the same file with the same name in a different context + return true; + }; + + /** + * Assumptions: + * - .index stays the same + * - sequence in the array stays the same + * - ref stays the same when stepping in the same context + */ + this.$updateFrame = function(xmlFrame, frame){ + //With code insertion, line/column might change?? + xmlFrame.setAttribute("line", frame.line); + xmlFrame.setAttribute("column", frame.column); + + var i, j, l; + var vars = xmlFrame.selectNodes("vars/item"); + var fVars = frame.arguments; + for (i = 1, j = 0, l = fVars.length; j < l; j++) { //i = 1 to skin this + if (fVars[j].name) + this.$updateVar(vars[i++], fVars[j]); + } + fVars = frame.locals; + for (j = 0, l = frame.locals.length; j < l; j++) { + if (fVars[j].name !== ".arguments") + this.$updateVar(vars[i++], fVars[j]); + } + + //@todo not caring about globals/scopes right now + }; + + this.$updateVar = function(xmlVar, fVar){ + xmlVar.setAttribute("value", this.$valueString(fVar.value)); + xmlVar.setAttribute("type", fVar.value.type); + xmlVar.setAttribute("ref", fVar.value.ref); + apf.xmldb.setAttribute(xmlVar, "children", hasChildren[fVar.value.type] ? "true" : "false"); + }; + + this.$buildFrame = function(frame, ref, xml){ + var script = ref(frame.script.ref); + xml.push( + "" + ); + xml.push(""); + + var receiver = { + name: "this", + value: frame.receiver + }; + xml.push(this.$serializeVariable(receiver)); + + var j, l; + for (j = 0, l = frame.arguments.length; j < l; j++) { + if (frame.arguments[j].name) + xml.push(this.$serializeVariable(frame.arguments[j])); + } + for (j = 0, l = frame.locals.length; j < l; j++) { + if (frame.locals[j].name !== ".arguments") + xml.push(this.$serializeVariable(frame.locals[j])); + } + xml.push(""); + xml.push(""); + + xml.push(""); + var scopes = frame.scopes; + for (j = 0, l = scopes.length; j < l; j++) { + var scope = scopes[j]; + xml.push(""); + } + xml.push(""); + + xml.push(""); + }; + + this.backtrace = function(model, callback) { + var _self = this; + this.$debugger.backtrace(null, null, null, true, function(body, refs) { + function ref(id) { + for (var i=0; i" + xml.join("") + ""); + _self.setFrame(model.data.firstChild); + } + callback(); + }); + }; + + this.loadScript = function(script, callback) { + var id = script.getAttribute("scriptid"); + var _self = this; + this.$debugger.scripts(4, [id], true, function(scripts) { + if (!scripts.length) + return; + var script = scripts[0]; + callback(script.source); + }); + }; + + this.loadObjects = function(item, callback) { + var ref = item.getAttribute("ref"); + var _self = this; + this.$debugger.lookup([ref], false, function(body) { + var refs = []; + var props = body[ref].properties; + for (var i = 0, l = props.length; i < l; i++) + refs.push(props[i].ref); + + _self.$debugger.lookup(refs, false, function(body) { + var xml = [""]; + for (var i = 0, l = props.length; i < l; i++) { + props[i].value = body[props[i].ref]; + xml.push(_self.$serializeVariable(props[i])); + } + xml.push(""); + callback(xml.join("")); + }); + }); + }; + + this.loadFrame = function(frame, callback) { + //var xml = "" + var scopes = frame.getElementsByTagName("scope"); + + var frameIndex = parseInt(frame.getAttribute("index"), 10); + + var _self = this; + var processed = 0; + var expected = 0; + var xml = [""]; + + for (var i = 0, l = scopes.length; i < l; i++) { + var scope = scopes[i]; + var type = parseInt(scope.getAttribute("type"), 10); + + // ignore local and global scope + if (type > 1) { + expected += 1; + var index = parseInt(scope.getAttribute("index"), 10); + this.$debugger.scope(index, frameIndex, true, function(body) { + var props = body.object.properties; + for (j = 0, l2 = props.length; j < l2; j++) + xml.push(_self.$serializeVariable(props[j])); + processed += 1; + if (processed == expected) { + xml.push(""); + callback(xml.join("")); + } + }); + } + } + if (expected === 0) + return callback(""); + }; + + this.setFrame = function(frame) { + this.$activeFrame = frame; + this.dispatchEvent("changeFrame", {data: frame}); + }; + + + this.getActiveFrame = function() { + return this.$activeFrame; + }; + + this.setBreakpoints = function(model, callback) { + var _self = this; + + var breakpoints = model.queryNodes("breakpoint"); + _self.$debugger.listbreakpoints(function(v8Breakpoints) { + if (v8Breakpoints.breakpoints) { + var bp; + for (var bId in _self.$breakpoints) { + bp = _self.$breakpoints[bId]; + bp.destroy(); + if (bp.xml) { + apf.xmldb.removeNode(bp.xml); + delete bp.xml; + } + } + _self.$breakpoints = {}; + + for (var i = 0, l = v8Breakpoints.breakpoints.length; i < l; i++) { + if (v8Breakpoints.breakpoints[i].type == "scriptId") + continue; + + var breakpoint = Breakpoint.fromJson(v8Breakpoints.breakpoints[i], _self.$debugger); + var id = breakpoint.source + "|" + breakpoint.line; + + _self.$breakpoints[id] = breakpoint; + + model.removeXml("breakpoint[@script='" + breakpoint.source + "' and @line='" + breakpoint.line + "']"); + breakpoint.xml = model.appendXml(_self.$getBreakpointXml(breakpoint, 0)); + } + } + + var modelBps = model.queryNodes("breakpoint") || []; + apf.asyncForEach(Array.prototype.slice.call(modelBps, 0), function(modelBp, next) { + var script = modelBp.getAttribute("script"); + var line = modelBp.getAttribute("line"); + var id = script + "|" + line; + var bp = _self.$breakpoints[id]; + + if (modelBp.parentNode) + apf.xmldb.removeNode(modelBp); + if (!bp) { + bp = _self.$addBreakpoint({ + id: id, + name: script, + row: line, + col: modelBp.getAttribute("column"), + lineOffset: 0, + scriptId: null, + data: { + condition: modelBp.getAttribute("condition"), + ignoreCount: parseInt(modelBp.getAttribute("ignorecount") || 0, 10), + enabled: (modelBp.getAttribute("enabled") == "true") + } + }, model, next); + } + else { + model.appendXml(_self.$getBreakpointXml(bp, 0)); + next(); + } + }, callback); + }); + }; + + this.toggleBreakpoint = function(script, relativeRow, model) { + var name = script.getAttribute("scriptname"); + var lineOffset = parseInt(script.getAttribute("lineoffset") || "0", 10); + var row = lineOffset + relativeRow; + var id = name + "|" + row; + var breakpoint = this.$breakpoints[id]; + var _self = this; + + if (breakpoint) { + try { + breakpoint.clear(function() { + _self.$removeBreakpoint(breakpoint, model); + }); + } + catch(ex) { + // aie! failed, remove it to be sure. + _self.$removeBreakpoint(breakpoint, model); + } + } + else { + breakpoint = this.$addBreakpoint({ + id: id, + name: name, + row: row, + col: 0, + lineOffset: lineOffset, + scriptId: script.getAttribute("scriptid") + }, model); + } + }; + + this.$removeBreakpoint = function(bp, model) { + if (bp.$id) { + var node, + xpath = "breakpoint[@id=" + bp.$id + "]"; + while (node = model.queryNode(xpath)) + apf.xmldb.removeNode(node); + } + delete this.$breakpoints[bp.id]; + }; + + this.$addBreakpoint = function(options, model, callback) { + //id, name, row, col, lineOffset, scriptId, dbg, data + var bp = this.$breakpoints[options.id] = new Breakpoint(options.name, options.row, options.col, options.dbg); + bp.id = options.id; + if (options.data) + apf.extend(bp, options.data); + var _self = this; + bp.attach(this.$debugger, function() { + // TODO: error handling + model.appendXml(_self.$getBreakpointXml(bp, options.lineOffset, options.scriptId)); + callback && callback(); + }); + return bp; + }; + + this.$getBreakpointXml = function(breakpoint, lineOffset, scriptId) { + var xml = []; + xml.push( + "" + ); + + return xml.join(""); + }; + + this.continueScript = function(callback) { + this.$debugger.continueScript(null, null, callback); + }; + + this.stepInto = function(callback) { + this.$debugger.continueScript("in", 1, callback); + }; + + this.stepNext = function(callback) { + this.$debugger.continueScript("next", 1, callback); + }; + + this.stepOut = function(callback) { + this.$debugger.continueScript("out", 1, callback); + }; + + this.suspend = function() { + this.$debugger.suspend(); + }; + + this.changeLive = function(scriptId, newSource, previewOnly, callback) { + this.$debugger.changelive(scriptId, newSource, previewOnly, callback); + }; + + this.evaluate = function(expression, frame, global, disableBreak, callback){ + this.$debugger.evaluate(expression, frame, global, disableBreak, function(body, refs, error){ + var str = []; + var name = expression.trim(); + if (error) { + str.push(""); + } + else { + str.push( + "" + ); + } + callback(apf.getXml(str.join("")), body, refs, error); + }); + }; + + this.$valueString = function(value) { + switch (value.type) { + case "undefined": + case "null": + return value.type; + + case "boolean": + case "number": + case "string": + return value.value + ""; + + case "object": + return "[" + value.className + "]"; + + case "function": + return "function " + value.inferredName + "()"; + + default: + return value.type; + }; + }; + + this.$frameToString = function(frame) { + var str = []; + var args = frame.arguments; + var argsStr = []; + + str.push(frame.func.name || frame.func.inferredName || "anonymous", "("); + for (var i = 0, l = args.length; i < l; i++) { + var arg = args[i]; + if (!arg.name) + continue; + argsStr.push(arg.name); + } + str.push(argsStr.join(", "), ")"); + return str.join(""); + }; + + this.$serializeVariable = function(item, name) { + var str = [ + "" + ]; + return str.join(""); + }; + +}).call(V8Debugger.prototype = new apf.Class()); + +}); +// #endif diff --git a/client/apf/elements/dbg/v8debughost.js b/client/apf/elements/dbg/v8debughost.js new file mode 100644 index 00000000000..5236565bc0f --- /dev/null +++ b/client/apf/elements/dbg/v8debughost.js @@ -0,0 +1,89 @@ +// #ifdef __AMLDEBUGGER || __INC_ALL +if (apf.hasRequireJS) define("apf/elements/dbg/v8debughost", + ["module", + "debug/StandaloneV8DebuggerService", + "debug/V8Debugger", + "apf/elements/dbg/v8debugger"], + function(module, StandaloneV8DebuggerService, V8Debugger, APFV8Debugger) { + +var V8DebugHost = module.exports = function(hostname, port, o3obj) { + this.$hostname = hostname; + this.$port = port; + this.$o3obj = o3obj; + + this.$debugger = null; + + this.$init(); +}; + +(function() { + + this.$connect = function(callback) { + var self = this; + + if (this.state == "connected") { + return callback.call(this); + } else { + this.addEventListener("connect", function() { + self.removeEventListener("connect", arguments.callee); + callback.call(self); + }); + } + if (this.state == "connecting") + return; + + this.state = "connecting"; + + var socket = this.$socket = new O3Socket(this.$hostname, this.$port, this.$o3obj); + this.$v8ds = new StandaloneV8DebuggerService(socket); + + this.state = "connected"; + this.dispatchEvent("connect"); + + window.onunload = this.disconnect.bind(this); + callback.call(this); + }; + + this.loadTabs = function(model) { + model.load("V8"); + }; + + this.attach = function(tabId, callback) { + var dbg = this.$debugger; + + if (dbg) + return callback(null, dbg) + + var self = this; + this.$connect(function() { + self.$v8ds.attach(0, function() { + dbg = new APFV8Debugger(new V8Debugger(0, self.$v8ds), this); + self.$debugger = dbg; + callback(null, dbg); + }); + }); + }; + + this.detach = function(dbg, callback) { + if (!dbg || this.$debugger !== dbg) + return callback(); + + this.$debugger = null; + + var self = this; + this.$v8ds.detach(0, function(err) { + dbg.dispatchEvent("detach"); + self.$socket.close(); + self.dispatchEvent("disconnect", {}); + callback && callback(err); + }); + }; + + this.disconnect = function(callback) { + this.detach(this.$debugger, callback); + }; + +}).call(V8DebugHost.prototype = new apf.Class()); + +}); +// #endif \ No newline at end of file diff --git a/client/apf/elements/dbg/v8websocketdebughost.js b/client/apf/elements/dbg/v8websocketdebughost.js new file mode 100644 index 00000000000..a9189ead9fc --- /dev/null +++ b/client/apf/elements/dbg/v8websocketdebughost.js @@ -0,0 +1,68 @@ +// #ifdef __AMLDEBUGGER || __INC_ALL +if (apf.hasRequireJS) define("apf/elements/dbg/v8websocketdebughost", + ["module", + "debug/WSV8DebuggerService", + "debug/V8Debugger", + "apf/elements/dbg/v8debugger"], + function(module, WSV8DebuggerService, V8Debugger, APFV8Debugger) { + +var V8WebSocketDebugHost = module.exports = function(socket) { + this.$socket = socket; + this.$debugger = null; + + this.$init(); +}; + +(function() { + + this.$connect = function(callback) { + if (this.state != "connected") + this.$v8ds = new WSV8DebuggerService(this.$socket); + + this.state = "connected"; + this.dispatchEvent("connect"); + callback.call(this); + }; + + this.loadTabs = function(model) { + model.load("V8"); + }; + + this.attach = function(tabId, callback) { + var dbg = this.$debugger; + + if (dbg) + return callback(null, dbg) + + var self = this; + this.$connect(function() { + self.$v8ds.attach(0, function() { + dbg = new APFV8Debugger(new V8Debugger(0, self.$v8ds), this); + self.$debugger = dbg; + callback(null, dbg); + }); + }); + }; + + this.detach = function(dbg, callback) { + if (!dbg || this.$debugger !== dbg) + return callback(); + + this.$debugger = null; + + var self = this; + this.$v8ds.detach(0, function(err) { + dbg.dispatchEvent("detach"); + self.dispatchEvent("disconnect", {}); + callback && callback(err); + }); + }; + + this.disconnect = function(callback) { + this.detach(this.$debugger, callback); + }; + +}).call(V8WebSocketDebugHost.prototype = new apf.Class()); + +}); +// #endif diff --git a/client/apf/elements/debugger.js b/client/apf/elements/debugger.js new file mode 100644 index 00000000000..9ec8a596e59 --- /dev/null +++ b/client/apf/elements/debugger.js @@ -0,0 +1,338 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + * + */ + +// #ifdef __AMLDEBUGGER || __INC_ALL +if (apf.hasRequireJS) define("apf/elements/debugger", + ["module"], function(module) { + +apf.dbg = module.exports = function(struct, tagName){ + this.$init(tagName || "debugger", apf.NODE_HIDDEN, struct); +}; + +(function(){ + + this.$host = null; + this.$debugger = null; + + this.$supportedProperties.push("state-running", "state-attached", + "model-sources", "model-stacks", "model-breakpoints", "activeframe"); + + this.$createModelPropHandler = function(name, xml, callback) { + return function(value) { + if (!value) return; + //#ifdef __WITH_NAMESERVER + this[name] = apf.setReference(value, + apf.nameserver.register("model", value, new apf.model())); + + // set the root node for this model + this[name].id = this[name].name = value; + this[name].load(xml); + //#endif + } + }; + + this.$createStatePropHandler = function(name) { + return function(value) { + if (!value) return; + //#ifdef __WITH_NAMESERVER + this[name] = apf.setReference(value, + apf.nameserver.register("state", value, new apf.state())); + + // set the root node for this model + this[name].id = this[name].name = value; + this[name].deactivate(); + //#endif + } + }; + + this.$propHandlers["model-sources"] = this.$createModelPropHandler("$mdlSources", ""); + this.$propHandlers["model-stack"] = this.$createModelPropHandler("$mdlStack", ""); + this.$propHandlers["model-breakpoints"] = this.$createModelPropHandler("$mdlBreakpoints", ""); + + this.$propHandlers["state-running"] = this.$createStatePropHandler("$stRunning"); + this.$propHandlers["state-attached"] = this.$createStatePropHandler("$stAttached"); + + this.$propHandlers["activeframe"] = function(value) { + if (this.$debugger) { + this.$ignoreFrameEvent = true; + this.$debugger.setFrame(value); + this.$ignoreFrameEvent = false; + } + this.dispatchEvent("changeframe", {data: value}); + }; + + this.attach = function(host, tab) { + var _self = this; + + host.$attach(this, tab, function(err, dbgImpl) { + _self.$host = host; + _self.$debugger = dbgImpl; + + _self.$loadSources(function() { + dbgImpl.setBreakpoints(_self.$mdlBreakpoints, function() { + var backtraceModel = new apf.model(); + backtraceModel.load(""); + + _self.$debugger.backtrace(backtraceModel, function() { + var frame = backtraceModel.queryNode("frame[1]"); + + if (!_self.$allowAttaching(frame)) { + _self.$debugger.continueScript(); + } + else { + _self.$mdlStack.load(backtraceModel.data); + } + + dbgImpl.addEventListener("afterCompile", _self.$onAfterCompile.bind(_self)); + + _self.$stAttached.activate(); + _self.$stRunning.setProperty("active", dbgImpl.isRunning()); + + dbgImpl.addEventListener("changeRunning", _self.$onChangeRunning.bind(_self)); + dbgImpl.addEventListener("break", _self.$onBreak.bind(_self)); + dbgImpl.addEventListener("detach", _self.$onDetach.bind(_self)); + dbgImpl.addEventListener("changeFrame", _self.$onChangeFrame.bind(_self)); + }); + }); + }); + }); + }; + + this.$allowAttaching = function (frame) { + var _self = this; + + if (frame) { + var scriptId = frame.getAttribute("scriptid"); + var scriptName = _self.$mdlSources.queryValue("file[@scriptid='" + scriptId + "']/@scriptname"); + + if (scriptName) { + var line = frame.getAttribute("line"); + var bp = _self.$mdlBreakpoints.queryNode("breakpoint[@script='" + scriptName + "' and @line='" + line + "']"); + } + if (!scriptName || !bp) { + return false; + } + + return true; + } + + return false; + }; + + this.$onChangeRunning = function() { + var isRunning = this.$debugger && this.$debugger.isRunning(); + if (this.$stRunning.active && !isRunning) + this.$onBreak(); + + this.$stRunning.setProperty("active", isRunning); + + //if (isRunning) + //this.$mdlStack.load(""); + }; + + this.$onBreak = function() { + var _self = this; + if (!this.$debugger || this.$debugger.isRunning()) + return; + + this.$debugger.backtrace(this.$mdlStack, function() { + _self.dispatchEvent("break"); + }); + }; + + this.$onAfterCompile = function(e) { + var id = e.script.getAttribute("id"); + var oldNode = this.$mdlSources.queryNode("//file[@id='" + id + "']"); + if (oldNode) + this.$mdlSources.removeXml(oldNode); + this.$mdlSources.appendXml(e.script); + }; + + this.$onDetach = function() { + if (this.$debugger) { + this.$debugger.destroy(); + this.$debugger = null; + } + + this.$host = null; + + this.$mdlSources.load(""); + this.$mdlStack.load(""); + this.$stAttached.deactivate(); + this.setProperty("activeframe", null); + }; + + this.$onChangeFrame = function() { + if (!this.$ignoreFrameEvent) { + this.setProperty("activeframe", this.$debugger.getActiveFrame()); + } + }; + + this.changeFrame = function(frame) { + this.$debugger.setFrame(frame); + }; + + this.detach = function(callback) { + this.continueScript(); + if (this.$host) + this.$host.$detach(this.$debugger, callback); + else + this.$onDetach(); + }; + + this.$loadSources = function(callback) { + this.$debugger.scripts(this.$mdlSources, callback); + }; + + this.loadScript = function(script, callback) { + this.$debugger.loadScript(script, callback); + }; + + this.loadObjects = function(item, callback) { + this.$debugger.loadObjects(item, callback); + }; + + this.loadFrame = function(frame, callback) { + this.$debugger.loadFrame(frame, callback); + }; + + this.toggleBreakpoint = function(script, row) { + var model = this.$mdlBreakpoints; + if (this.$debugger) { + this.$debugger.toggleBreakpoint(script, row, model); + } + else { + var scriptName = script.getAttribute("scriptname"); + var bp = model.queryNode("breakpoint[@script='" + scriptName + "' and @line='" + row + "']"); + if (bp) { + apf.xmldb.removeNode(bp); + } + else { + // filename is something like blah/blah/workspace/realdir/file + // we are only interested in the part after workspace for display purposes + var tofind = "/workspace/"; + var path = script.getAttribute("path"); + var displayText = path; + if (path.indexOf(tofind) > -1) { + displayText = path.substring(path.indexOf(tofind) + tofind.length); + } + + var bp = apf.n("") + .attr("script", scriptName) + .attr("line", row) + .attr("text", displayText + ":" + (parseInt(row, 10) + 1)) + .attr("lineoffset", 0) + .node(); + model.appendXml(bp); + } + } + }; + + this.continueScript = function(callback) { + this.dispatchEvent("beforecontinue"); + + if (this.$debugger) + this.$debugger.continueScript(callback); + else + callback && callback(); + }; + + this.stepInto = function() { + this.dispatchEvent("beforecontinue"); + + this.$debugger && this.$debugger.stepInto(); + }; + + this.stepNext = function() { + this.dispatchEvent("beforecontinue"); + + this.$debugger && this.$debugger.stepNext(); + }; + + this.stepOut = function() { + this.dispatchEvent("beforecontinue"); + + this.$debugger && this.$debugger.stepOut(); + }; + + this.suspend = function() { + this.$debugger && this.$debugger.suspend(); + }; + + this.evaluate = function(expression, frame, global, disableBreak, callback){ + this.$debugger && this.$debugger.evaluate(expression, frame, global, disableBreak, callback); + }; + + this.changeLive = function(scriptId, newSource, previewOnly, callback) { + this.$debugger && this.$debugger.changeLive(scriptId, newSource, previewOnly, callback); + }; + +}).call(apf.dbg.prototype = new apf.AmlElement()); + +apf.aml.setElement("debugger", apf.dbg); + + +window.adbg = { + exec : function(method, args, callback, options) { + if (method == "loadScript") { + var dbg = args[0]; + var script = args[1]; + dbg.loadScript(script, function(source) { + if (options && options.callback) { + options.callback(apf.escapeXML(source), apf.SUCCESS); + } else { +// callback("" + apf.escapeXML(source) + "", apf.SUCCESS); + //TODO: ugly text() bug workaround + callback("", "]] >") + "]]>", apf.SUCCESS); + } + }); + } + else if (method == "loadObjects") { + var dbg = args[0]; + var item = args[1]; + + dbg.loadObjects(item, function(xml) { + if (options && options.callback) { + options.callback(xml, apf.SUCCESS); + } else { + callback(xml, apf.SUCCESS); + } + }); + } + else if (method == "loadFrame") { + var dbg = args[0]; + var frame = args[1]; + + dbg.loadFrame(frame, function(xml) { + if (options && options.callback) { + options.callback(xml, apf.SUCCESS); + } else { + callback(xml, apf.SUCCESS); + } + }); + } + } + }; + +(apf.$asyncObjects || (apf.$asyncObjects = {}))["adbg"] = 1; + +}); +// #endif diff --git a/client/apf/elements/debughost.js b/client/apf/elements/debughost.js new file mode 100644 index 00000000000..0d0b4cb05e2 --- /dev/null +++ b/client/apf/elements/debughost.js @@ -0,0 +1,146 @@ +// #ifdef __AMLDEBUGGER || __INC_ALL +if (apf.hasRequireJS) define("apf/elements/debughost", + ["module", + "apf/elements/dbg/chromedebughost", + "apf/elements/dbg/v8debughost", + "apf/elements/dbg/v8websocketdebughost"], + function(module, ChromeDebugHost, V8DebugHost, V8WebSocketDebugHost) { + +apf.debughost = module.exports = function(struct, tagName){ + this.$init(tagName || "debughost", apf.NODE_HIDDEN, struct); +}; + +(function(){ + + this.port = 9222; + this.server = "localhost"; + this.type = "chrome"; + this.autoinit = false; + this.$modelTabs = null; + this.$stateConnected = null; + + this.$host = null; + + this.$booleanProperties["autostart"] = true; + + this.$supportedProperties.push("port", "server", "type", "autoinit", + "model-tabs", "state-connected", "strip"); + + this.$propHandlers["model-tabs"] = function(value) { + if (!value) return; + //#ifdef __WITH_NAMESERVER + this.$modelTabs = apf.nameserver.get("model", value) || + apf.setReference(value, apf.nameserver.register("model", value, new apf.model())); + + // set the root node for this model + this.$modelTabs.id = this.$modelTabs.name = value; + this.$modelTabs.load(""); + //#endif + }; + + this.$propHandlers["state-connected"] = function(value) { + if (!value) return; + //#ifdef __WITH_NAMESERVER + this.$stateConnected = apf.nameserver.get("state", value) || + apf.setReference(value, apf.nameserver.register("state", value, new apf.state())); + + // set the root node for this model + this.$stateConnected.id = this.$stateConnected.name = value; + this.$stateConnected.deactivate(); + //#endif + }; + + this.init = function() { + if (this.$host) + return; + + if (this.type == "chrome" || this.type == "v8" || this.type == "v8-ws") { + if (!apf.debughost.$o3obj && this.type !== "v8-ws") { + apf.debughost.$o3obj = window.o3Obj || o3.create("8A66ECAC-63FD-4AFA-9D42-3034D18C88F4", { + oninstallprompt: function() { alert("can't find o3 plugin"); }, + product: "O3Demo" + }); + } + + if (this.type == "chrome") { + this.$host = new ChromeDebugHost(this.server, this.port, apf.debughost.$o3obj); + } + else if (this.type == "v8") { + this.$host = new V8DebugHost(this.server, this.port, apf.debughost.$o3obj); + } + else if (this.type == "v8-ws") { + var socket = this.dispatchEvent("socketfind"); + if (!socket) + throw new Error("no socket found!") + this.$host = new V8WebSocketDebugHost(socket); + } + else if (this.type == "chrome-ws") { + var socket = this.dispatchEvent("socketfind"); + if (!socket) + throw new Error("no socket found!") + this.$host = new ChromeDebugHost(null, null, null, socket); + } + + var self = this; + this.$host.addEventListener("connect", function() { + self.dispatchEvent("connect"); + self.$stateConnected.activate(); + }); + this.$host.addEventListener("disconnect", function() { + self.dispatchEvent("disconnect"); + self.$stateConnected.deactivate(); + }); + + this.loadTabs(); + } + }; + + this.loadTabs = function() { + if (!this.$host) + this.init(); + + var self = this; + this.$host.loadTabs(this.$modelTabs, function() { + self.$dispatchEvent("tabsloaded"); + }); + } + + this.addEventListener("DOMNodeInsertedIntoDocument", function(e) { + if (this.autoinit) + this.init(); + }); + + this.$attach = function(dbg, tab, callback) { + if (!this.$host) + this.init(); + + var id = tab ? tab.getAttribute("id") : null; + + var _self = this; + this.$host.attach(id, function(err, dbg) { + dbg.setStrip(_self.strip || ""); + callback(err, dbg); + }); + }; + + this.$detach = function(dbgImpl, callback) { + if (!this.$host) + return; + + this.$host.detach(dbgImpl, callback); + }; + + this.disconnect = function() { + if (!this.$host) + return; + + this.$host.disconnect(); + this.$host = null; + }; + +}).call(apf.debughost.prototype = new apf.AmlElement()); + +apf.aml.setElement("debughost", apf.debughost); + +}); +// #endif diff --git a/client/ext/code/code.js b/client/ext/code/code.js index c75858f0d36..9712be95b5f 100644 --- a/client/ext/code/code.js +++ b/client/ext/code/code.js @@ -7,6 +7,8 @@ define(function(require, exports, module) { +require("apf/elements/codeeditor"); + var ide = require("core/ide"); var ext = require("core/ext"); var EditSession = require("ace/edit_session").EditSession; diff --git a/client/ext/debugger/debugger.js b/client/ext/debugger/debugger.js index 7e4099ecda2..1c345b576da 100644 --- a/client/ext/debugger/debugger.js +++ b/client/ext/debugger/debugger.js @@ -7,6 +7,8 @@ define(function(require, exports, module) { +require("apf/elements/codeeditor"); + var ide = require("core/ide"); var ext = require("core/ext"); var editors = require("ext/editors/editors"); diff --git a/client/ext/jslanguage/narcissus_jshint.js b/client/ext/jslanguage/narcissus_jshint.js index 8de6bb0c1c5..5c7f34896f8 100644 --- a/client/ext/jslanguage/narcissus_jshint.js +++ b/client/ext/jslanguage/narcissus_jshint.js @@ -49,7 +49,10 @@ handler.analyze = function(doc) { lint(value, { undef: false, onevar: false, - passfail: false + passfail: false, + devel: true, + browser: true, + node: true }); lint.errors.forEach(function(warning) { if (!warning) diff --git a/client/ext/jslanguage/scope_analyzer.js b/client/ext/jslanguage/scope_analyzer.js index fc6d3331ccf..2e0eb2caaf2 100644 --- a/client/ext/jslanguage/scope_analyzer.js +++ b/client/ext/jslanguage/scope_analyzer.js @@ -18,6 +18,262 @@ var baseLanguageHandler = require('ext/language/base_handler'); require('treehugger/traverse'); var handler = module.exports = Object.create(baseLanguageHandler); +// Based on https://github.com/jshint/jshint/blob/master/jshint.js#L331 +var GLOBALS = { + // Literals + "true" : true, + "false" : true, + "undefined" : true, + "null" : true, + "this" : true, + "arguments" : true, + // Browser + ArrayBuffer : true, + ArrayBufferView : true, + Audio : true, + addEventListener : true, + applicationCache : true, + blur : true, + clearInterval : true, + clearTimeout : true, + close : true, + closed : true, + DataView : true, + defaultStatus : true, + document : true, + event : true, + FileReader : true, + Float32Array : true, + Float64Array : true, + FormData : true, + getComputedStyle : true, + HTMLElement : true, + HTMLAnchorElement : true, + HTMLBaseElement : true, + HTMLBlockquoteElement : true, + HTMLBodyElement : true, + HTMLBRElement : true, + HTMLButtonElement : true, + HTMLCanvasElement : true, + HTMLDirectoryElement : true, + HTMLDivElement : true, + HTMLDListElement : true, + HTMLFieldSetElement : true, + HTMLFontElement : true, + HTMLFormElement : true, + HTMLFrameElement : true, + HTMLFrameSetElement : true, + HTMLHeadElement : true, + HTMLHeadingElement : true, + HTMLHRElement : true, + HTMLHtmlElement : true, + HTMLIFrameElement : true, + HTMLImageElement : true, + HTMLInputElement : true, + HTMLIsIndexElement : true, + HTMLLabelElement : true, + HTMLLayerElement : true, + HTMLLegendElement : true, + HTMLLIElement : true, + HTMLLinkElement : true, + HTMLMapElement : true, + HTMLMenuElement : true, + HTMLMetaElement : true, + HTMLModElement : true, + HTMLObjectElement : true, + HTMLOListElement : true, + HTMLOptGroupElement : true, + HTMLOptionElement : true, + HTMLParagraphElement : true, + HTMLParamElement : true, + HTMLPreElement : true, + HTMLQuoteElement : true, + HTMLScriptElement : true, + HTMLSelectElement : true, + HTMLStyleElement : true, + HTMLTableCaptionElement : true, + HTMLTableCellElement : true, + HTMLTableColElement : true, + HTMLTableElement : true, + HTMLTableRowElement : true, + HTMLTableSectionElement : true, + HTMLTextAreaElement : true, + HTMLTitleElement : true, + HTMLUListElement : true, + HTMLVideoElement : true, + Int16Array : true, + Int32Array : true, + Int8Array : true, + Image : true, + localStorage : true, + location : true, + navigator : true, + open : true, + openDatabase : true, + Option : true, + parent : true, + print : true, + removeEventListener : true, + resizeBy : true, + resizeTo : true, + screen : true, + scroll : true, + scrollBy : true, + scrollTo : true, + sessionStorage : true, + setInterval : true, + setTimeout : true, + SharedWorker : true, + Uint16Array : true, + Uint32Array : true, + Uint8Array : true, + WebSocket : true, + window : true, + Worker : true, + XMLHttpRequest : true, + XPathEvaluator : true, + XPathException : true, + XPathExpression : true, + XPathNamespace : true, + XPathNSResolver : true, + XPathResult : true, + // Devel + alert : true, + confirm : true, + console : true, + Debug : true, + opera : true, + prompt : true, + // Frameworks + jQuery : true, + "$" : true, + "$$" : true, + goog : true, + dojo : true, + dojox : true, + dijit : true, + apf : true, + // mootools + Assets : true, + Browser : true, + Chain : true, + Class : true, + Color : true, + Cookie : true, + Core : true, + Document : true, + DomReady : true, + DOMReady : true, + Drag : true, + Element : true, + Elements : true, + Event : true, + Events : true, + Fx : true, + Group : true, + Hash : true, + HtmlTable : true, + Iframe : true, + IframeShim : true, + InputValidator : true, + instanceOf : true, + Keyboard : true, + Locale : true, + Mask : true, + MooTools : true, + Native : true, + Options : true, + OverText : true, + Request : true, + Scroller : true, + Slick : true, + Slider : true, + Sortables : true, + Spinner : true, + Swiff : true, + Tips : true, + Type : true, + typeOf : true, + URI : true, + Window : true, + // prototype.js + '$A' : true, + '$F' : true, + '$H' : true, + '$R' : true, + '$break' : true, + '$continue' : true, + '$w' : true, + Abstract : true, + Ajax : true, + Enumerable : true, + Field : true, + Form : true, + Insertion : true, + ObjectRange : true, + PeriodicalExecuter : true, + Position : true, + Prototype : true, + Selector : true, + Template : true, + Toggle : true, + Try : true, + Autocompleter : true, + Builder : true, + Control : true, + Draggable : true, + Draggables : true, + Droppables : true, + Effect : true, + Sortable : true, + SortableObserver : true, + Sound : true, + Scriptaculous : true, + // require.js + define : true, + // node.js + __filename : true, + __dirname : true, + Buffer : true, + exports : true, + GLOBAL : true, + global : true, + module : true, + process : true, + require : true, + // Standard + Array : true, + Boolean : true, + Date : true, + decodeURI : true, + decodeURIComponent : true, + encodeURI : true, + encodeURIComponent : true, + Error : true, + 'eval' : true, + EvalError : true, + Function : true, + hasOwnProperty : true, + isFinite : true, + isNaN : true, + JSON : true, + Math : true, + Number : true, + Object : true, + parseInt : true, + parseFloat : true, + RangeError : true, + ReferenceError : true, + RegExp : true, + String : true, + SyntaxError : true, + TypeError : true, + URIError : true, + // non-standard + escape : true, + unescape : true +}; + handler.handlesLanguage = function(language) { return language === 'javascript'; }; @@ -76,76 +332,96 @@ handler.analyze = function(doc, ast) { function scopeAnalyzer(scope, node, parentLocalVars) { preDeclareHoisted(scope, node); var localVariables = parentLocalVars || []; - node.traverseTopDown( - 'VarDecl(x)', function(b) { - localVariables.push(scope[b.x.value]); - }, - 'VarDeclInit(x, _)', function(b) { - localVariables.push(scope[b.x.value]); - }, - 'Assign(Var(x), _)', function(b, node) { - if(!scope[b.x.value]) { + function analyze(scope, node) { + node.traverseTopDown( + 'VarDecl(x)', function(b) { + localVariables.push(scope[b.x.value]); + }, + 'VarDeclInit(x, _)', function(b) { + localVariables.push(scope[b.x.value]); + }, + 'Assign(Var(x), e)', function(b, node) { + if(!scope[b.x.value]) { + markers.push({ + pos: node[0].getPos(), + type: 'warning', + message: 'Assigning to undeclared variable.' + }); + } + else { + scope[b.x.value].addUse(node); + } + analyze(scope, b.e); + return this; + }, + 'ForIn(Var(x), e, stats)', function(b, node) { + if(!scope[b.x.value]) { + markers.push({ + pos: node[0].getPos(), + type: 'warning', + message: 'Using undeclared variable as iterator variable.' + }); + } + else { + scope[b.x.value].addUse(node); + } + analyze(scope, b.e); + analyze(scope, b.stats); + return this; + }, + 'Var(x)', function(b, node) { + node.setAnnotation("scope", scope); + if(scope[b.x.value]) { + scope[b.x.value].addUse(node); + } else if(handler.isFeatureEnabled("undeclaredVars") && !GLOBALS[b.x.value]) { + markers.push({ + pos: this.getPos(), + type: 'warning', + message: "Undeclared variable." + }); + } + return node; + }, + 'Function(x, fargs, body)', function(b, node) { + node.setAnnotation("scope", scope); + + var newScope = Object.create(scope); + newScope['this'] = new Variable(); + b.fargs.forEach(function(farg) { + farg.setAnnotation("scope", newScope); + newScope[farg[0].value] = new Variable(farg); + if (handler.isFeatureEnabled("unusedFunctionArgs")) + localVariables.push(newScope[farg[0].value]); + }); + scopeAnalyzer(newScope, b.body); + return node; + }, + 'Catch(x, body)', function(b, node) { + var oldVar = scope[b.x.value]; + // Temporarily override + scope[b.x.value] = new Variable(b.x); + scopeAnalyzer(scope, b.body, localVariables); + // Put back + scope[b.x.value] = oldVar; + return node; + }, + 'PropAccess(_, "lenght")', function(b, node) { markers.push({ - pos: node[0].getPos(), + pos: node.getPos(), type: 'warning', - message: 'Assigning to undeclared variable.' + message: "Did you mean 'length'?" }); - } - }, - 'ForIn(Var(x), _, _)', function(b, node) { - if(!scope[b.x.value]) { + }, + 'Call(Var("parseInt"), [_])', function() { markers.push({ - pos: node[0].getPos(), + pos: this[0].getPos(), type: 'warning', - message: 'Using undeclared variable as iterator variable.' + message: "Missing radix argument." }); } - }, - 'Var(x)', function(b, node) { - node.setAnnotation("scope", scope); - if(scope[b.x.value]) { - scope[b.x.value].addUse(node); - } - return node; - }, - 'Function(x, fargs, body)', function(b, node) { - node.setAnnotation("scope", scope); - - var newScope = Object.create(scope); - newScope['this'] = new Variable(); - b.fargs.forEach(function(farg) { - farg.setAnnotation("scope", newScope); - newScope[farg[0].value] = new Variable(farg); - if (handler.isFeatureEnabled("unusedFunctionArgs")) - localVariables.push(newScope[farg[0].value]); - }); - scopeAnalyzer(newScope, b.body); - return node; - }, - 'Catch(x, body)', function(b, node) { - var oldVar = scope[b.x.value]; - // Temporarily override - scope[b.x.value] = new Variable(b.x); - scopeAnalyzer(scope, b.body, localVariables); - // Put back - scope[b.x.value] = oldVar; - return node; - }, - 'PropAccess(_, "lenght")', function(b, node) { - markers.push({ - pos: node.getPos(), - type: 'warning', - message: "Did you mean 'length'?" - }); - }, - 'Call(Var("parseInt"), [_])', function() { - markers.push({ - pos: this[0].getPos(), - type: 'warning', - message: "Missing radix argument." - }); - } - ); + ); + } + analyze(scope, node); if(!parentLocalVars) { for (var i = 0; i < localVariables.length; i++) { if (localVariables[i].uses.length === 0) { diff --git a/client/ext/language/language.js b/client/ext/language/language.js index bbdab5f211b..0c8d1f26add 100644 --- a/client/ext/language/language.js +++ b/client/ext/language/language.js @@ -9,6 +9,7 @@ define(function(require, exports, module) { var ext = require("core/ext"); var ide = require("core/ide"); var editors = require("ext/editors/editors"); +var noderunner = require("ext/noderunner/noderunner"); var WorkerClient = require("ace/worker/worker_client").WorkerClient; var complete = require('ext/language/complete'); @@ -28,7 +29,7 @@ module.exports = ext.register("ext/language/language", { name : "Javascript Outline", dev : "Ajax.org", type : ext.GENERAL, - deps : [editors], + deps : [editors, noderunner], nodes : [], alone : true, markup : markup, @@ -96,6 +97,7 @@ module.exports = ext.register("ext/language/language", { this.setJSHint(); this.setInstanceHighlight(); this.setUnusedFunctionArgs(); + this.setUndeclaredVars(); this.editor.on("changeSession", function(event) { // Time out a litle, to let the page path be updated @@ -122,10 +124,10 @@ module.exports = ext.register("ext/language/language", { }, setPath: function() { - var currentPath = tabEditors.getPage().getAttribute("id"); // Currently no code editor active - if(!editors.currentEditor.ceEditor) + if(!editors.currentEditor.ceEditor || !tabEditors.getPage()) return; + var currentPath = tabEditors.getPage().getAttribute("id"); this.worker.call("switchFile", [currentPath, editors.currentEditor.ceEditor.syntax, this.editor.getSession().getValue(), this.editor.getCursorPosition()]); }, @@ -147,7 +149,7 @@ module.exports = ext.register("ext/language/language", { this.worker.emit("cursormove", {data: cursorPos}); }, - setUnusedFunctionArgs: function(yeah) { + setUnusedFunctionArgs: function() { if(extSettings.model.queryValue("language/@unusedFunctionArgs") != "false") this.worker.call("enableFeature", ["unusedFunctionArgs"]); else @@ -155,6 +157,14 @@ module.exports = ext.register("ext/language/language", { this.setPath(); }, + setUndeclaredVars: function() { + if(extSettings.model.queryValue("language/@undeclaredVars") != "false") + this.worker.call("enableFeature", ["undeclaredVars"]); + else + this.worker.call("disableFeature", ["undeclaredVars"]); + this.setPath(); + }, + /** * Method attached to key combo for complete */ diff --git a/client/ext/language/liveinspect.js b/client/ext/language/liveinspect.js index 5037912f3c6..84e74a3eea5 100644 --- a/client/ext/language/liveinspect.js +++ b/client/ext/language/liveinspect.js @@ -18,9 +18,21 @@ module.exports = (function () { // get respective HTML elements windowHtml = winLiveInspect.$ext; datagridHtml = dgLiveInspect.$ext; + winLiveInspect.addEventListener("prop.visible", function(e) { + // don't track when hiding the window + if (!e.value) + return; + ide.dispatchEvent("track_action", { + type: "live inspect code", + expression: currentExpression || "no expression available yet." + }); + }); }; var hook = function(_ext, worker) { + if (typeof stRunning === "undefined") + return; + ext.initExtension(this); // listen to the worker's response @@ -55,17 +67,17 @@ module.exports = (function () { stDebugProcessRunning.addEventListener("prop.active", checkDebuggerActive); // when hovering over the inspector window we should ignore all further listeners - datagridHtml.addEventListener("mouseover", function () { + apf.addListener(datagridHtml, "mouseover", function() { if (activeTimeout) { clearTimeout(activeTimeout); } }); // we should track mouse movement over the whole window - document.addEventListener("mousemove", onDocumentMouseMove) + apf.addListener(document, "mousemove", onDocumentMouseMove); - // yes, this is superhacky but the editor function in APF is crazy - datagridHtml.addEventListener("dblclick", initializeEditor); + // yes, this is superhacky but the editor function in APF is crazy + apf.addListener(datagridHtml, "dblclick", initializeEditor); // when collapsing or expanding the datagrid we want to resize dgLiveInspect.addEventListener("expand", resizeWindow); @@ -171,10 +183,10 @@ module.exports = (function () { }; // when blurring, update - edit.addEventListener("blur", onBlur); + apf.addListener(edit, "blur", onBlur); // on keydown, same same - edit.addEventListener("keydown", function (ev) { + apf.addListener(edit, "keydown", function(ev) { if (ev.keyCode === 27 || ev.keyCode === 13) { // tab or enter return onBlur.call(this); } @@ -235,23 +247,30 @@ module.exports = (function () { return; } - // see whether we hover over the editor - if (ceEditor) { - // calculate position - var ele = ceEditor.$ext; - var position = apf.getAbsolutePosition(ele, document.body); - var left = position[0]; - var top = position[1]; - - // x boundaries - if (ev.pageX >= left && ev.pageX <= (left + ele.offsetWidth)) { - // y boundaries - if (ev.pageY >= top && ev.pageY <= (top + ele.offsetHeight)) { - // we are in the editor, so return; this will be handled - return; - } - } - } + // see whether we hover over the editor or the quickwatch window + var mouseMoveAllowed = false; + + var eles = [ ceEditor, winLiveInspect ]; + // only the visible ones + eles.filter(function (ele) { return ele.visible; }) + .map(function (ele) { return ele.$ext; }) // then get the HTML counterpart + .forEach(function (ele) { + // then detect real position + var position = apf.getAbsolutePosition(ele, document.body); + var left = position[0]; + var top = position[1]; + + // x boundaries + if (ev.pageX >= left && ev.pageX <= (left + ele.offsetWidth)) { + // y boundaries + if (ev.pageY >= top && ev.pageY <= (top + ele.offsetHeight)) { + // we are in the editor, so return; this will be handled + mouseMoveAllowed = true; + } + } + }); + + if (mouseMoveAllowed) return; // not in the editor? if (winLiveInspect.visible) { diff --git a/client/ext/language/settings.xml b/client/ext/language/settings.xml index ccae5ca4a63..bba5a7dd031 100644 --- a/client/ext/language/settings.xml +++ b/client/ext/language/settings.xml @@ -8,6 +8,9 @@ + diff --git a/client/ext/noderunner/noderunner.js b/client/ext/noderunner/noderunner.js index 06d885252fa..b07904fa7ee 100644 --- a/client/ext/noderunner/noderunner.js +++ b/client/ext/noderunner/noderunner.js @@ -7,6 +7,9 @@ define(function(require, exports, module) { +require("apf/elements/debugger"); +require("apf/elements/debughost"); + var ide = require("core/ide"); var ext = require("core/ext"); var util = require("core/util"); diff --git a/client/ext/openfiles/openfiles.js b/client/ext/openfiles/openfiles.js index 0f1f877455a..62575ba2a12 100644 --- a/client/ext/openfiles/openfiles.js +++ b/client/ext/openfiles/openfiles.js @@ -96,7 +96,7 @@ module.exports = ext.register("ext/openfiles/openfiles", { tabEditors.addEventListener("afterswitch", function(e){ var page = e.nextPage; - if (page) { + if (page && page.$model.data) { var node = _self.model.queryNode("//node()[@path='" + page.$model.data.getAttribute("path") + "']"); if (node) lstOpenFiles.select(node); diff --git a/client/ext/save/save.js b/client/ext/save/save.js index e6041841063..991ac66de87 100644 --- a/client/ext/save/save.js +++ b/client/ext/save/save.js @@ -42,8 +42,9 @@ module.exports = ext.register("ext/save/save", { var at = e.page.$at; if (!at.undo_ptr) at.undo_ptr = at.$undostack[0]; - if (at.undo_ptr && at.$undostack[at.$undostack.length-1] !== at.undo_ptr - || !at.undo_ptr && e.page.$doc.getNode().getAttribute("changed") == 1 + var node = e.page.$doc.getNode(); + if (node && at.undo_ptr && at.$undostack[at.$undostack.length-1] !== at.undo_ptr + || !at.undo_ptr && node.getAttribute("changed") == 1 && e.page.$doc.getValue()) { ext.initExtension(_self); @@ -273,7 +274,11 @@ module.exports = ext.register("ext/save/save", { panel.setAttribute("caption", "Saved file " + path); ide.dispatchEvent("afterfilesave", {node: node, doc: doc, value: value}); - ide.dispatchEvent("track_action", {type: "save as filetype", fileType: node.getAttribute("name").split(".").pop()}); + ide.dispatchEvent("track_action", { + type: "save as filetype", + fileType: node.getAttribute("name").split(".").pop(), + success: state != apf.SUCCESS ? "false" : "true" + }); apf.xmldb.removeAttribute(node, "saving"); apf.xmldb.removeAttribute(node, "new"); diff --git a/client/ext/settings/settings.js b/client/ext/settings/settings.js index cdd34b827fe..1bde1da1e4f 100644 --- a/client/ext/settings/settings.js +++ b/client/ext/settings/settings.js @@ -39,11 +39,16 @@ module.exports = ext.register("ext/settings/settings", { }, saveToFile : function() { + var data = this.model.data && apf.xmldb.cleanXml(this.model.data.xml) || ""; ide.send(JSON.stringify({ command: "settings", action: "set", - settings: this.model.data && apf.xmldb.cleanXml(this.model.data.xml) || "" + settings: data })); + ide.dispatchEvent("track_action", { + type: "save settings", + settings: data + }); }, saveSettingsPanel: function() { diff --git a/client/ext/tree/tree.js b/client/ext/tree/tree.js index 06abda67c26..62160c2a4f0 100644 --- a/client/ext/tree/tree.js +++ b/client/ext/tree/tree.js @@ -188,7 +188,11 @@ module.exports = ext.register("ext/tree/tree", { }) } })); - davProject.setAttribute("showhidden", "[{require('ext/settings/settings').model}::auto/tree/@showhidden]"); + + ide.addEventListener("loadsettings", function(e) { + var model = e.model; + (davProject.realWebdav || davProject).setAttribute("showhidden", model.queryValue('auto/tree/@showhidden')); + }); mnuView.appendChild(new apf.divider()); diff --git a/client/style/skins.xml b/client/style/skins.xml index 4ea688e1687..714b4e2306b 100644 --- a/client/style/skins.xml +++ b/client/style/skins.xml @@ -2048,11 +2048,9 @@ } .ace_gutter { - position: absolute; overflow-x: scroll; overflow-y: hidden; height: 100%; - width: 60px !important; -moz-user-select: -moz-none; -khtml-user-select: none; -webkit-user-select: none; diff --git a/server/cloud9/ide.js b/server/cloud9/ide.js index 1c55ea3148a..3814ceeeb73 100644 --- a/server/cloud9/ide.js +++ b/server/cloud9/ide.js @@ -28,7 +28,6 @@ var Ide = module.exports = function(options, httpServer, exts, socket) { paths: { "ace": staticUrl + "/support/ace/lib/ace", "debug": staticUrl + "/support/lib-v8debug/lib/v8debug", - "apf": staticUrl + "/support/apf", "treehugger": staticUrl + "/support/treehugger/lib/treehugger" }, waitSeconds: 30 diff --git a/support/ace b/support/ace index e1b21b0dc80..b2162f04083 160000 --- a/support/ace +++ b/support/ace @@ -1 +1 @@ -Subproject commit e1b21b0dc80edfaab789ffd7e81396521cf5e81b +Subproject commit b2162f040835834b145b947c47b6abbb826511b3 diff --git a/support/apf b/support/apf index e68111ecae1..1f3030695d0 160000 --- a/support/apf +++ b/support/apf @@ -1 +1 @@ -Subproject commit e68111ecae1918704cf302356acc32068c6f4e9d +Subproject commit 1f3030695d05bd5418a9368d0c5b9eb4cfdb2b0e