define(function(require, exports, module) { main.consumes = [ "TestPanel", "ui", "Tree", "settings", "panels", "commands", "test", "Menu", "MenuItem", "Divider", "tabManager", "save", "preferences", "fs", "run.gui", "layout", "c9", "Form" ]; main.provides = ["test.all"]; return main; function main(options, imports, register) { var TestPanel = imports.TestPanel; var settings = imports.settings; var panels = imports.panels; var ui = imports.ui; var Tree = imports.Tree; var test = imports.test; var commands = imports.commands; var fs = imports.fs; var c9 = imports.c9; var Form = imports.Form; var Menu = imports.Menu; var MenuItem = imports.MenuItem; var Divider = imports.Divider; var tabManager = imports.tabManager; var save = imports.save; var layout = imports.layout; var prefs = imports.preferences; var runGui = imports["run.gui"]; var Node = test.Node; var async = require("async"); var basename = require("path").basename; var dirname = require("path").dirname; var escapeHTML = require("ace/lib/lang").escapeHTML; var LineWidgets = require("ace/line_widgets").LineWidgets; var dom = require("ace/lib/dom"); // var Range = require("../range").Range; /***** Initialization *****/ var plugin = new TestPanel("Ajax.org", main.consumes, { caption: "All Tests", index: 200, // showTitle: true, style: "flex:1;-webkit-flex:1" }); var emit = plugin.getEmitter(); var tree, stopping, menuContext, running, boxFilter, menuInlineContext; var wsNode = new Node({ label: "workspace", isOpen: true, className: "heading", status: "loaded", noSelect: true, $sorted: true, }); var rmtNode = new Node({ label: "remote", isOpen: true, className: "heading", status: "loaded", noSelect: true, $sorted: true }); var rootNode = new Node({ label: "root", tree: tree, items: [wsNode] }); function load() { if (test.inactive) return; panels.on("afterAnimate", function() { if (panels.isActive("test")) tree && tree.resize(); }, plugin); test.on("ready", function() { if (!test.config.excluded) test.config.excluded = {}; if (!test.config.skipped) test.config.skipped = {}; }, plugin); test.on("updateConfig", function() { async.each(test.runners, function(runner, cb) { runner.update(cb); }, function() { refresh(); }); }, plugin); settings.on("read", function() { settings.setDefaults("user/test", [ ["inlineresults", true], ["runonsave", true] ]); }, plugin); prefs.add({ "Test": { position: 2000, "Test Runner": { position: 100, "Run Tests On Save": { type: "checkbox", position: 50, setting: "user/test/@runonsave" }, "Show Inline Test Results": { type: "checkbox", position: 100, setting: "user/test/@inlineresults" }, "Exclude These Files": { name: "txtTestExclude", type: "textarea-row", fixedFont: true, width: 600, height: 200, rowheight: 250, position: 1000 }, } } }, plugin); plugin.getElement("txtTestExclude", function(txtTestExclude) { var ta = txtTestExclude.lastChild; ta.on("blur", function(e) { test.config.excluded = {}; ta.value.split("\n").forEach(function(rawLine) { var path = rawLine.split("#")[0].trim(); test.config.excluded[path] = rawLine; }); test.saveConfig(function() { // Trigger a refetch for all runners test.refresh(); }); }); var update = function() { var str = []; for (var path in test.config.excluded) { str.push(test.config.excluded[path]); } ta.setValue(str.join("\n")); }; test.on("ready", update, plugin); test.on("updateConfig", update, plugin); }, plugin); // Save hooks save.on("afterSave", function(e) { var runner = isTest(e.path, e.value); if (!runner) return; // Notify runners of change event and refresh tree var runonsave = settings.getBool("user/test/@runonsave"); runner.fileChange({ path: e.path, value: e.value, runonsave: runonsave, run: function(fileNode) { // Re-run test on save if (runonsave) { var cmd = fileNode.coverage ? "runtestwithcoverage" : "runtest"; fileNode.fixParents(); commands.exec(cmd, null, { nodes: [fileNode]}); } }, refresh: function() { tree && tree.refresh(); } }); }, plugin); // Run Button Hook runGui.on("updateRunButton", function(e) { if (!isTest(e.path)) return; var btnRun = e.button; btnRun.enable(); btnRun.setAttribute("command", "runfocussedtest"); btnRun.setAttribute("caption", "Run Test"); btnRun.setAttribute("tooltip", "Run Test" + basename(e.path)); return false; }, plugin); // Initiate test runners test.on("register", function(e) { init(e.runner); }, plugin); test.on("unregister", function(e) { deinit(e.runner); }, plugin); test.on("update", function() { test.runners.forEach(function(runner) { updateStatus(runner.root, "loading"); runner.update(); }); }, plugin); test.on("resize", function() { tree && tree.resize(); }, plugin); // This is global to protect from error states plugin.on("stop", function() { progress.stop = []; running = false; runGui.transformButton(); }); var label, form; test.on("showRunMenu", function(e) { if (!label) { label = new ui.label({ caption: "general", class: "runner-form-header" }); var runner = e.runners[0] || 0; form = new Form({ colwidth: 180, rowheight: 45, style: "width:320px", form: [ { title: "Parallel Test Execution", type: "checked-spinner", name: "parallel", min: 1, max: 999, defaultCheckboxValue: test.config.parallel !== undefined ? test.config.parallel : (runner.defaultParallel || false), defaultValue: test.config.parallelConcurrency !== undefined ? test.config.parallelConcurrency : (runner.defaultParallelConcurrency || 6), onchange: function(e) { test.config[e.type == "checkbox" ? "parallel" : "parallelConcurrency"] = e.value; test.saveConfig(function() {}); } } ] }, plugin); } e.menu.appendChild(label); form.attachTo(e.menu); return false; }); // Initialize All Runners test.runners.forEach(init); // Set the all panel as the focussed panel test.focussedPanel = plugin; } var drawn = false; function draw(opts) { if (drawn) return; drawn = true; // Insert CSS ui.insertCss(require("text!./style.css"), options.staticPrefix, plugin); // Tree tree = new Tree({ container: opts.html, scrollMargin: [10, 10], theme: "filetree", emptyMessage: "No tests found", getCaptionHTML: function(node) { if (node.type == "file") { var path = dirname(node.label); if (path == ".") return escapeHTML(node.label); return escapeHTML(basename(path) + "/" + basename(node.label)) + " - " + escapeHTML(dirname(path)) + ""; } else if (node.type == "testset") { return escapeHTML(node.label); // "" + escapeHTML(node.label) + ""; } else if (node.kind == "it") { return "it " + escapeHTML(node.label); } else if (node.type == "runner") { return escapeHTML(node.label) + " (" + (!node.items.length && node.status == "loading" ? "loading" : node.items.length) + ")"; } return escapeHTML(node.label); }, getIconHTML: function(node) { var icon = "default"; if (node.status === "loading") icon = "loading"; else if (node.status === "running") icon = "test-in-progress"; else if (node.passed === 1) icon = "test-passed"; else if (node.passed === 0) icon = "test-failed"; else if (node.passed === 2) icon = "test-error"; else if (node.passed === 3) icon = "test-terminated"; else if (node.skip) icon = "test-ignored"; else if (node.type == "testset") icon = "test-set"; else if (node.type == "file") icon = "test-file"; else if (node.type == "runner") icon = "test-file"; else if (node.type == "prepare") icon = "test-prepare"; else if (node.type == "test") icon = "test-notran"; return ""; }, getClassName: function(node) { return (node.className || "") + (node.status == "loading" ? " loading" : "") + (node.status == "running" ? " loading" : ""); // TODO different running icon }, getRowIndent: function(node) { return node.$depth ? node.$depth - 1 : 0; }, hasChildren: function(node) { return node.status === "pending" || node.items && node.items.length; }, loadChildren: function(node, callback) { populate(node, callback); }, sort: function(children) { if (!children.length || children[0].type != "file") return; var compare = tree.model.alphanumCompare; return children.sort(function(a, b) { // TODO index sorting // if (aIsSpecial && bIsSpecial) return a.index - b.index; return compare(a.path + "", b.path + ""); }); } }, plugin); // TODO generalize this tree.renderer.scrollBarV.$minWidth = 10; tree.container.style.position = "absolute"; tree.container.style.left = "0"; tree.container.style.top = "0"; tree.container.style.right = "0"; tree.container.style.bottom = "0"; tree.container.style.height = ""; tree.setRoot(rootNode); tree.commands.bindKey("Space", function(e) { openTestFile(); }); tree.commands.bindKey("Enter", function(e) { commands.exec("runtest"); }); tree.commands.bindKey("Shift-Enter", function(e) { commands.exec("runtestwithcoverage"); }); tree.on("focus", function() { test.focussedPanel = plugin; }); tree.on("select", function() { openTestFile([tree.selectedNode], true); }); tree.on("afterChoose", function() { commands.exec("runtest"); }); layout.on("eachTheme", function(e) { var height = parseInt(ui.getStyleRule(".filetree .tree-row", "height"), 10) || 22; tree.rowHeightInner = height; tree.rowHeight = height; if (e.changed && tree) tree.resize(true); }); // Hook clear test.on("clear", function() { clear(); }, plugin); // Hook opening of known files tabManager.on("open", function(e) { var node, tab = e.tab; if (rootNode.findAllNodes("file").some(function(n) { node = n; return n.path == tab.path; })) { decorate(node, tab); } }, plugin); // Filter var toolbar = test.getElement("toolbar"); ui.insertByIndex(toolbar, new ui.filler(), 900, plugin); boxFilter = ui.insertByIndex(toolbar, new apf.codebox({ "initial-message": "Filter Tests", "clearbutton": true, "focusselect": true, "singleline": true, "width": 100, "style": "flex:10; max-width:150px" // "style": "float:right;margin:1px 2px" }), 1000, plugin); boxFilter.ace.on("input", function() { tree.filterKeyword = boxFilter.ace.getValue(); }); // Menu menuContext = new Menu({ items: [ new MenuItem({ position: 100, command: "runtest", caption: "Run", class: "strong", hotkey: "Enter" }), new MenuItem({ position: 200, command: "runtestwithcoverage", caption: "Run with Code Coverage", hotkey: "Shift-Enter" }), new Divider({ position: 300 }), new MenuItem({ position: 400, caption: "Open Test File", onclick: openTestFile, hotkey: "Space" }), new MenuItem({ position: 500, caption: "Open Raw Test Output", command: "opentestoutput" }), new Divider({ position: 600 }), new MenuItem({ position: 700, caption: "Skip", command: "skiptest" }), new MenuItem({ position: 800, caption: "Remove", command: "removetest" }) ]}, plugin); opts.aml.setAttribute("contextmenu", menuContext.aml); menuInlineContext = new Menu({ items: [ new MenuItem({ caption: "Show Inline Test Results", checked: "user/test/@inlineresults", type: "check", position: 100 }), new Divider(), new MenuItem({ caption: "Open Raw Test Output", onclick: function() { var path = tabManager.focussedTab.path; var test = findTest(path); if (test) commands.exec("opentestoutput", null, { nodes: [test]}); }, position: 300 }), new MenuItem({ caption: "Clear Test Results In This File", onclick: function() { var editor = tabManager.focussedTab.editor; if (editor.ace) clearDecoration(editor.ace.session); }, position: 400 }), ]}, plugin); settings.on("read", function() { test.settingsMenu.append(new MenuItem({ caption: "Show Inline Test Results", checked: "user/test/@inlineresults", type: "check", position: 100 })); }, plugin); settings.on("user/test/@inlineresults", function(value) { rootNode.findAllNodes("file").forEach(function(fileNode) { if (fileNode.passed === undefined) return; var tab = tabManager.findTab(fileNode.path); if (tab) decorate(fileNode, tab); }); }, plugin); tree.resize(); } /***** Helper Methods *****/ function populate(node, callback, force) { var runner = node.findRunner() || findFileByPath(node.path).findRunner(); updateStatus(node, "loading"); runner.populate(node, function(err) { if (err) return callback(err); // TODO updateStatus(node, "loaded"); node.fixParents(); if (node.skip) { node.findAllNodes("test").forEach(function(n) { n.skip = true; }); } callback(); }); } function filter(path) { return test.config ? test.config.excluded[path] : false; } function init(runner) { if (!test.ready) return test.on("ready", init.bind(this, runner)); if (!test.drawn) return test.once("draw", init.bind(this, runner), runner); var parent = runner.remote ? rmtNode : wsNode; runner.root.parent = parent; parent.items.push(runner.root); if (wsNode.items.length == 1 && (!tree || !tree.selectedNode)) plugin.once("draw", function() { tree.select(runner.root); }); updateStatus(runner.root, "loading"); var first = true; runner.init(filter, function(err) { if (err) return console.error(err); // TODO runner.root.isOpen = true; updateStatus(runner.root, "loaded"); runner.root.findAllNodes("file").forEach(function(node) { // Mark skipped tests if (test.config.skipped[node.path]) { node.skip = true; node.findAllNodes("test").forEach(function(n) { n.skip = true; }); } // Call Results if (first) { if (typeof node.passed == "number") emit("result", { node: node }); if (node.coverage) test.setCoverage(node); } }); runner.root.fixParents(); if (first) { // Init any tab that is already opened tabManager.getTabs().forEach(function(tab) { if (tab.path && isTest(tab.path, tab.document.value)) { decorate(findFileByPath(tab.path), tab); } }); } first = false; }); } function deinit(runner) { if (runner.root.parent) { var items = runner.root.parent.items; items.splice(items.indexOf(runner.root), 1); } tree.refresh(); } function findFileByPath(path) { var found = false; rootNode.findAllNodes("file").some(function(n) { if (n.path == path) { found = n; return true; } }); return found; } var knownTests = {}; function isTest(path, value) { if (filter(path)) return false; if (knownTests[path]) return knownTests[path]; if (!value) value = tabManager.findTab(path).document.value; test.runners.some(function(runner) { if (runner.isTest(path, value)) { knownTests[path] = runner; return true; } return false; }); return knownTests[path] || false; } // TODO export to ace editor and add loading detection function scrollToDefinition(ace, line, lineEnd) { var lineHeight = ace.renderer.$cursorLayer.config.lineHeight; var lineVisibleStart = ace.renderer.scrollTop / lineHeight; var linesVisible = ace.renderer.$size.height / lineHeight; lineEnd = Math.min(lineEnd, line + linesVisible); if (lineVisibleStart <= line && lineEnd <= lineVisibleStart + linesVisible) return; var SAFETY = 1.5; ace.scrollToLine(Math.round((line + lineEnd) / 2 - SAFETY), true); } function openTestFile(nodes, onlyWhenOpen) { (nodes || test.focussedPanel.tree.selectedNodes).forEach(function(n) { var tab; if (n.passed === 0) { var found; n.findAllNodes("test").some(function(n) { found = n; return n.passed === 0; }); n = found; } if (n.type == "file" && (!n.ownPassed || !n.output)) { if (onlyWhenOpen) { tab = tabManager.findTab(n.path); if (!tab || !tab.isActive()) return; } tabManager.openFile(n.path, true, function() {}); } else if (n.type == "file" || n.pos) { var fileNode = n.findFileNode(); if (onlyWhenOpen) { tab = tabManager.findTab(fileNode.path); if (!tab || !tab.isActive()) return; } tabManager.open({ path: fileNode.path, active: true }, function(err, tab) { if (err) return console.error(err); scrollTab(tab, n); }); } }); } function scrollTab(tab, n) { var pos = n.selpos || n.pos; var select = n.selpos ? { row: n.selpos.el, column: n.selpos.ec } : undefined; var ace = tab.editor.ace; var scroll = function() { ace.selection.clearSelection(); var sl = n.pos ? n.pos.sl : 0; var el = n.pos ? n.pos.el : 0; var a = n.annotations; if (a && a.length) { scrollToDefinition(ace, a[0].line, a[0].line); ace.moveCursorTo(a[0].line - 1, a[0].column - 1); } else { scrollToDefinition(ace, sl, el); ace.moveCursorTo(pos ? pos.sl : 0, pos ? pos.sc : 0); if (select) ace.getSession().getSelection() .selectToPosition({ row: pos.el, column: pos.ec }); } }; if (!ace.session.doc.$lines.length) ace.once("changeSession", scroll); else if (!ace.renderer.$cursorLayer.config) ace.once("afterRender", scroll); else scroll(); } /***** Methods *****/ function run(nodes, options, callback) { if (running) return stop(run.bind(this, nodes, options, callback)); running = true; if (typeof nodes == "string") { nodes = [findFileByPath(nodes)]; if (!nodes[0]) return callback(new Error("File not found")); } if (nodes && !Array.isArray(nodes)) callback = options, options = nodes, nodes = null; if (typeof options == "function") callback = options, options = null; if (!nodes) { nodes = tree.selectedNodes; if (!nodes) return callback(new Error("Nothing to do")); } var withCodeCoverage = options && options.withCodeCoverage; var transformRun = options && options.transformRun; if (transformRun) { var button = runGui.transformButton("stop"); button.setAttribute("command", "stoptest"); } var list = [], found = {}; nodes.forEach(function(n) { if (n.type == "prepare") n = n.findFileNode(); // Weak solution. It should be able to run part of a test set without knowing tests if (n.type == "all" || n.type == "root" || n.type == "runner") n.findAllNodes("file").forEach(function(n) { if (n.skip) return; list.push(n); found[n.path] = true; }); else if (withCodeCoverage) { var fileNode = n.findFileNode(); if (!found[fileNode.path]) list.push(fileNode); } else list.push(n); }); var firstRunner = list[0].findRunner(); var parallel = (!options || options.parallel === undefined ? test.config.parallel || firstRunner.defaultParallel : options.parallel) || false; var parallelConcurrency = (!options || options.parallelConcurrency === undefined ? test.config.parallelConcurrency || firstRunner.defaultParallelConcurrency : options.parallelConcurrency) || 6; options.parallel = parallel; options.parallelConcurrency = parallelConcurrency; test.lastTest = nodes; var worker = function(node, callback) { if (stopping) return callback(new Error("Terminated")); if (node.status == "pending") { // TODO do this lazily return populate(node, function(err) { if (err) return callback(err); _run(node, options, callback); }); } _run(node, options, callback); }; var complete = function(err) { emit("stop", { nodes: list }); callback(err, list); }; if (parallel) { var queue = async.queue(worker, parallelConcurrency); queue.drain = complete; list.forEach(function(item) { queue.push(item, function() {}); }); } else { async.eachSeries(list, worker, complete); } } var progress = { log: function(node, chunk) { node.fullOutput += chunk; emit("log", chunk); }, start: function(node) { updateStatus(node, "running"); }, end: function(node) { updateStatus(node, "loaded"); }, stop: [] }; function findTest(path) { return (function recur(items) { for (var j, i = 0; i < items.length; i++) { j = items[i]; if (j.type == "file") { if (j.path == path) return j; } else if (j.items) return recur(j.items); } })(rootNode.items); } function _run(node, options, callback) { if (tree && tree.filterKeyword) { if (node.type == "file") node = findFileByPath(node.path); else { node.parent.findAllNodes(node.type).some(function(n) { if (n.label == node.label) { node = n; return true; } }); } } var runner = node.findRunner(); var fileNode = node.findFileNode(); if (!runner) runner = findFileByPath(fileNode.path).findRunner(); if (runner.form) options = runner.form.toJson(null, options || {}); fileNode.fullOutput = ""; // Reset output updateStatus(node, "running"); // Clear previous run information clear([node], true); // emit("clearResult", { node: node }); var stop = runner.run(node, progress, options, function(err) { updateStatus(node, "loaded"); var tab = tabManager.findTab(fileNode.path); if (tab) decorate(fileNode, tab); delete progress.stop[stopId]; callback(err, node); // Write To Cache writeToCache(runner, fileNode.path, fileNode.serialize()); emit("result", { node: node }); }); var stopId = progress.stop.push(stop) - 1; } function clearCache(runner, callback) { fs.rmdir("~/.c9/cache/" + runner.name, { recursive: true }, function() { callback && callback.apply(this, arguments); }); } function writeToCache(runner, path, cache, callback) { fs.writeFile("~/.c9/cache/" + runner.name + "/" + path.replace(/\//g, "\\"), cache, function(err) { callback && callback(err); }); } function refreshTree(node) { while (node && !node.tree) node = node.parent; var T = node && node.tree || tree; if (T) T.refresh(); } function updateStatus(node, s) { // TODO make this more efficient by trusting the child nodes if (node.type == "file" || node.type == "testset") { var tests = node.findAllNodes("test|prepare"); var st, p = []; tests.forEach(function(test) { if (st === undefined && test.status != "loaded") st = test.status; if (!p[test.passed]) p[test.passed] = 0; p[test.passed]++; }); node.passed = p[3] ? 3 : (p[2] ? 2 : p[0] ? 0 : (p[1] ? 1 : undefined)); node.status = st || "loaded"; } else if (node.type == "root") { refreshTree(node); return; } else { node.status = s; } if (node.parent) updateStatus(node.parent, s); else refreshTree(node); } function stop(callback) { if (!running) return callback(new Error("Not Running")); var timer; stopping = Date.now(); plugin.once("stop", function(e) { clearTimeout(timer); (function _(items, first) { items.forEach(function(node) { if (node.items) _(node.items); else if (typeof node.passed != "number") node.passed = 3; // This caused unrun tests to no longer have an arrow // if (first) updateStatus(node, "loaded"); }); })(e.nodes, true); stopping = false; callback(); }); progress.stop.forEach(function(stop) { if (stop) stop(); }); timer = setTimeout(function() { emit("stop", { nodes: []}); test.transformRunButton("run"); }, 5000); } function clear(nodes, onlyNodes) { if (!nodes) nodes = rootNode.items; nodes.forEach(function(n) { n.passed = undefined; n.ownPassed = null; n.output = ""; if (n.status == "running") n.status = "loaded"; n.annotations = []; if (n.items) clear(n.items, true); }); if (onlyNodes) return; if (tree.filterKeyword) tree.filterKeyword = tree.filterKeyword; else tree.refresh(); test.runners.forEach(function(runner) { clearCache(runner); }); clearAllDecorations(); } function skip(nodes, callback) { if (typeof nodes == "function") callback = nodes, nodes = null; if (!nodes) nodes = tree.selectedNodes; var map = {}; nodes.forEach(function(fileNode) { if (fileNode.type != "file") return; if (!map[fileNode.path]) { fileNode.skip = !fileNode.skip; if (fileNode.skip) test.config.skipped[fileNode.path] = true; else delete test.config.skipped[fileNode.path]; fileNode.findAllNodes("test").forEach(function(n) { n.skip = fileNode.skip; }); map[fileNode.path] = true; } }); test.saveConfig(function(err) { tree.refresh(); callback(err); }); } function remove(nodes, callback) { if (typeof nodes == "function") callback = nodes, nodes = null; if (!nodes) nodes = tree.selectedNodes; nodes.forEach(function(fileNode) { if (fileNode.type != "file") return; if (!test.config.excluded[fileNode.path]) { fileNode.parent.children.remove(fileNode); fileNode.parent.items.remove(fileNode); test.config.excluded[fileNode.path] = true; } }); test.saveConfig(function(err) { tree.refresh(); callback(err); }); } // TODO: Think about moving this to a separate plugin function decorate(fileNode, tab) { var editor = tab.editor.ace; var session = (tab.document.getSession() || 0).session; if (!session || !tab.isActive()) { tab.once("activate", function() { setTimeout(function() { decorate(fileNode, tab); }); }); return; } if (!session.$testMarkers) { session.$testMarkers = {}; session.on("changeEditor", function(e) { if (e.oldEditor) { // TODO cleanup } if (e.editor) { decorateEditor(e.editor); } }); session.on("change", function(delta) { var inlineWidgets = session.lineAnnotations; var decorations = session.$decorations; if (!inlineWidgets) return; var startRow = delta.start.row; var len = delta.end.row - startRow; if (len === 0) { if (inlineWidgets[startRow]) inlineWidgets[startRow] = undefined; } else if (delta.action == 'remove') { inlineWidgets.splice(startRow + 1, len); decorations.splice(startRow + 1, len); } else { var args = new Array(len); args.unshift(startRow, 0); inlineWidgets.splice.apply(inlineWidgets, args); decorations.splice.apply(decorations, args); } }); } if (!session.widgetManager) { session.widgetManager = new LineWidgets(session); session.widgetManager.attach(editor); } clearDecoration(session); var showInline = settings.getBool("user/test/@inlineresults"); var nodes = fileNode.findAllNodes("test|prepare"); if (fileNode.ownPassed) nodes.push(fileNode); nodes.forEach(function(node) { if (!node.parent) fileNode.fixParents(); if (node.passed !== undefined && (node.type == "test" || node.output)) { var pos = node.pos ? node.pos.sl : 0; session.addGutterDecoration(pos, "test-" + node.passed); (session.$markers || (session.$markers = [])) .push([pos, "test-" + node.passed]); } if (showInline) { if (node.annotations) createStackWidget(editor, session, node); if (node.output && node.output.trim()) createOutputWidget(editor, session, node); } }); } function createOutputWidget(editor, session, node) { // editor.session.unfold(pos.row); // editor.selection.moveToPosition(pos); var w = { row: node.pos ? node.pos.el : 0, fullWidth: true, // coverGutter: true, el: dom.createElement("div") }; var extraClass = node.passed == 2 ? "ace_error" : (node.passed == 1 ? "ace_ok" : "ace_warning"); var el = w.el.appendChild(dom.createElement("div")); var arrow = w.el.appendChild(dom.createElement("div")); arrow.className = "error_widget_arrow " + extraClass; var pos = node.pos ? { row: node.pos.el, column: node.pos.ec } : { row: 0, column: 0 }; var left = editor.renderer.$cursorLayer.getPixelPosition(pos).left; arrow.style.left = left /*+ editor.renderer.gutterWidth*/ - 5 + "px"; var runner = node.findRunner(); if (!runner) runner = findFileByPath(node.findFileNode().path); w.el.className = "error_widget_wrapper"; el.style.whiteSpace = "pre"; el.className = "error_widget " + extraClass; el.innerHTML = runner ? runner.parseLinks(escapeHTML(node.output)) : escapeHTML(node.output); var closeBtn = document.createElement("span"); closeBtn.textContent = "\xd7"; closeBtn.className = "widget-close-button"; w.el.appendChild(closeBtn); closeBtn.onclick = function() { w.destroy(); }; closeBtn.onmousedown = function(e) { e.preventDefault(); }; w.el.addEventListener("click", function(e) { if (e.target && e.target.className == "link") { var link = e.target.getAttribute("link"); var parts = link.split(":"); fs.exists(parts[0], function(exists) { if (!exists) { commands.exec("navigate", null, { keyword: link[0] == "/" ? link.substr(1) : link }); return; } var path = parts[0].indexOf(c9.workspaceDir) === 0 ? parts[0].replace(c9.workspaceDir, "") : parts[0]; tabManager.open({ path: path, focus: true, document: { ace: { jump: { row: Number(parts[1]), column: Number(parts[1]) } } } }); }); } }, false); w.el.addEventListener("contextmenu", function(e) { if (e.which == 2 || e.which == 3) { menuInlineContext.show(e.x + 1, e.y + 1); e.stopPropagation(); e.preventDefault(); return false; } }, false); el.appendChild(dom.createElement("div")); w.destroy = function() { session.widgetManager.removeLineWidget(w); w.destroyed = true; }; session.widgetManager.addLineWidget(w); session.$lineWidgets.push(w); // w.el.onmousedown = editor.focus.bind(editor); return w; } function decorateEditor(editor) { if (editor.decorated) return; editor.renderer.on("afterRender", updateLines); var onMouseDown = function(e) { var widget = e.target; if (widget.classList.contains("widget")) { if (widget.annotation && widget.classList.contains("more")) { if (widget.output && !widget.output.destroyed) { widget.output.destroy(); widget.output = null; } else { var a = widget.annotation; widget.output = createOutputWidget(editor, a.session, { pos: { el: a.row, ec: a.column }, passed: 0, output: a.more, findRunner: a.node.findRunner.bind(a.node) }); } } e.stopPropagation(); } }; editor.decorated = true; editor.container.addEventListener("mousedown", onMouseDown, true); } function createStackWidget(editor, session, node) { decorateEditor(editor); var m, d; node.annotations.forEach(function(item) { m = item.message.trim(); if (m.length <= 50) d = m; else { if (m.match(/[\n\r]/) > -1) d = m.split("\n")[0].substr(0, 45) + " ..."; else d = m.substr(0, 20) + " ... " + m.substr(-25); } var pos = item.line - 1; session.addGutterDecoration(pos, "test-0"); (session.$markers || (session.$markers = [])) .push([pos, "test-0"]); session.lineAnnotations[pos] = { display: d, row: item.line - 1, column: item.column, more: m.length > 50 ? m : null, session: session, node: node }; }); } function updateLines(e, renderer) { var textLayer = renderer.$textLayer; var config = textLayer.config; var session = textLayer.session; if (!session.lineAnnotations) return; var first = config.firstRow; var last = config.lastRow; var lineElements = textLayer.element.childNodes; var lineElementsIdx = 0; var row = first; var foldLine = session.getNextFoldLine(row); var foldStart = foldLine ? foldLine.start.row : Infinity; var useGroups = textLayer.$useLineGroups(); while (true) { if (row > foldStart) { row = foldLine.end.row + 1; foldLine = textLayer.session.getNextFoldLine(row, foldLine); foldStart = foldLine ? foldLine.start.row : Infinity; } if (row > last) break; var lineElement = lineElements[lineElementsIdx++]; if (lineElement && session.lineAnnotations[row]) { if (useGroups) lineElement = lineElement.lastChild; var widget, a = session.lineAnnotations[row]; if (!a.element) { widget = document.createElement("span"); widget.textContent = a.display; widget.className = "widget stack-message" + (a.more ? " more" : ""); widget.annotation = a; session.lineAnnotations[row].element = widget; } else widget = a.element; lineElement.appendChild(widget); } row++; } } function clearAllDecorations() { tabManager.getTabs().forEach(function(tab) { if (tab.editorType != "ace") return; var session = (tab.document.getSession() || 0).session; if (session) clearDecoration(session); }); } function clearDecoration(session) { if (session.$markers) { session.$decorations.forEach(function(m, i) { if (m) session.$decorations[i] = m.replace(/ test-[01234]/g, ""); }); } if (session.lineAnnotations) { session.lineAnnotations.forEach(function(item) { if (item && item.element && item.element.parentNode) item.element.remove(); }); } if (session.$lineWidgets) { session.$lineWidgets.forEach(function(widget) { session.widgetManager.removeLineWidget(widget); }); } session.$markers = []; session.lineAnnotations = []; session.$lineWidgets = []; } function refresh() { tree && tree.refresh(); } /***** Lifecycle *****/ plugin.on("load", function() { load(); }); plugin.on("draw", function(e) { draw(e); }); plugin.on("show", function(e) { // txtFilter.focus(); // txtFilter.select(); }); plugin.on("hide", function(e) { // Cancel Preview // tabs.preview({ cancel: true }); }); plugin.on("unload", function() { drawn = false; tree = null; stopping = null; menuContext = null; running = null; boxFilter = null; menuInlineContext = null; }); /***** Register and define API *****/ /** * @class Template * @extends Plugin * @singleton */ plugin.freezePublicAPI({ /** * @property {Object} The tree implementation * @private */ get tree() { return tree; }, /** * */ get contextMenu() { return menuContext; }, /** * */ get root() { return rootNode; }, /** * */ refresh: refresh, /** * */ run: run, /** * */ stop: stop, /** * */ skip: skip, /** * */ remove: remove, /** * */ openTestFile: openTestFile, /** * */ findTest: findTest, /** * */ findFileByPath: findFileByPath, /** * */ writeToCache: writeToCache, /** * */ clearCache: clearCache }); register(null, { "test.all": plugin }); } });