/*global requirejs*/ define(function(require, exports, module) { main.consumes = [ "PreferencePanel", "settings", "ui", "util", "Form", "ext", "c9", "apf", "dialog.alert", "dialog.confirm", "layout", "proc", "menus", "commands", "dialog.error", "dialog.info", "tree.favorites", "fs", "tree", "vfs", "plugin.debug", "preferences.experimental" ]; main.provides = ["plugin.manager", "pluginManager"]; return main; /* - Show Packages to be updated - Open Plugin Store - Open Cloud9 in Debug Mode - List all installed packages - Filter - Core packages - Name - Version - Description - Load Time - Plugin profile - Pre-installed - * - Custom packages - * - Actions: - Uninstall - Report Issue - Open README - Open in Cloud9 DataProvider.variableHeightRowMixin.call(this) in datagrid constructor and set node.height harutyun [1:11 PM] or add a custom getItemHeight function like https://github.com/c9/newclient/blob/master/node_modules/ace_tree/demo/demo.js#L63 (edited) */ function main(options, imports, register) { var PreferencePanel = imports.PreferencePanel; var settings = imports.settings; var layout = imports.layout; var ui = imports.ui; var c9 = imports.c9; var menus = imports.menus; var fs = imports.fs; var commands = imports.commands; var ext = imports.ext; var tree = imports.tree; var proc = imports.proc; var util = imports.util; var qs = require("querystring"); var apf = imports.apf; var vfs = imports.vfs; var alert = imports["dialog.alert"].show; var confirm = imports["dialog.confirm"].show; var showError = imports["dialog.error"].show; var showInfo = imports["dialog.info"].show; var favs = imports["tree.favorites"]; var pluginDebug = imports["plugin.debug"]; var experimental = imports["preferences.experimental"]; var search = require("../c9.ide.navigate/search"); var Tree = require("ace_tree/tree"); var TreeData = require("./managerdp"); var join = require("path").join; var basename = require("path").basename; var dirname = require("path").dirname; var async = require("async"); var staticPrefix = options.staticPrefix; var architect; var CORE = { "c9.core": 1, "c9.fs": 1, "c9.ide.preferences": 1, "c9.ide.panels": 1, "c9.ide.plugins": 1, "c9.ide.login": 1, "c9.vfs.client": 1, "c9.ide.console": 1, "c9.ide.editors": 1, "c9.ide.dialog.common": 1, "c9.ide.dialog.file": 1, "c9.ide.dialog.login": 1, "c9.ide.errorhandler": 1, "c9.ide.help": 1, "c9.ide.keys": 1, "c9.ide.restore": 1, "c9.ide.watcher": 1, "c9.ide.tree": 1, "c9.ide.info": 1, "c9.ide.layout.classic": 1, "c9.ide.terminal": 1, "c9.ide.ace": 1, "c9.ide.clipboard": 1, "c9.nodeapi": 1 }; var GROUPS = { "custom": "Installed Plugins", "pre": "Pre-installed plugins", "core": "Core Plugins", "runtime": "Plugins created runtime" }; var TEMPLATES = { "plugin.simple": "Empty Plugin", "plugin.default": "Full Plugin", "plugin.installer": "Installer Plugin", "plugin.bundle": "Cloud9 Bundle" }; // @TODO add sorting /***** Initialization *****/ var ENABLED = c9.location.indexOf("debug=2") > -1 || experimental.addExperiment( "plugin-manager", options.devel, "SDK/Plugin Manager" ); var plugin = new PreferencePanel("Ajax.org", main.consumes, { caption: "Plugin Manager", className: "plugins", form: false, noscroll: true, index: 200, visible: ENABLED, }); // var emit = plugin.getEmitter(); var model, datagrid, filterbox; var btnUninstall, btnReport, btnReadme, btnCloud9, btnReload; var loaded = false; function load() { if (loaded) return false; loaded = true; if (!ENABLED) return; // @TODO enable/disable plugins -> move to ext // settings.on("read", function(e) { // updateCommandsFromSettings(); // }, plugin); // commands.on("update", function(){ // changed = true; // updateCommandsFromSettings(); // }, plugin); if (options.devel) { commands.addCommand({ name: "reloadLastPlugin", bindKey: { mac: "F4", win: "F4" }, hint: "reload plugin last reloaded in plugin manager", exec: function() { var name = getLastReloaded(); if (!name) return commands.exec("reloadPlugin", null, { panel: plugin }); reload(name); } }, plugin); commands.addCommand({ name: "reloadPlugin", group: "Plugins", exec: function() { commands.exec("openpreferences", null, { panel: plugin }); } }, plugin); menus.addItemByPath("Tools/~", new ui.divider(), 100000, plugin); menus.addItemByPath("Tools/Developer", null, 100100, plugin); menus.addItemByPath("Tools/Developer/Reload Built-in Plugin...", new ui.item({ command: "reloadPlugin" }), 1100, plugin); menus.addItemByPath("Tools/Developer/Reload Last Plugin", new ui.item({ command: "reloadLastPlugin", isAvailable: getLastReloaded }), 1200, plugin); } menus.addItemByPath("File/New Plugin", null, 210, plugin); Object.keys(TEMPLATES).forEach(function(name) { menus.addItemByPath("File/New Plugin/" + TEMPLATES[name], new ui.item({ onclick: function() { createNewPlugin(name); } }), 210, plugin); }); ext.on("register", function() { setTimeout(reloadModel); }); } var drawn; function draw(e) { if (drawn) return; drawn = true; model = new TreeData(); model.emptyMessage = "No plugins found"; model.columns = [{ caption: "Name", value: "name", // getText: function(p){ // return p.name + " (" + p.items.length + ")"; // }, width: "250", type: "tree" }, { caption: "Version", // value: "version", getText: function(p) { return p.version || (p.isPackage ? p.items.length && p.items[0].version || "" : ""); }, width: "100" }, { caption: "Startup Time", // value: "time", width: "100", getText: function(p) { if (p.time !== undefined) return (p.time || 0) + "ms"; var total = 0; if (p.isPackage || p.name == "runtime") { p.items.forEach(function(item) { total += item.time || 0; }); } else { p.items.forEach(function(p) { p.items && p.items.forEach(function(item) { total += item.time || 0; }); }); } return (p.time = total) + "ms"; } }, { caption: "Enabled", value: "enabled", width: "100" // }, { // caption: "Package", // value: "package", // @todo make a link // width: "100%" }, { caption: "Developer", // value: "developer", getText: function(p) { return p.developer || (p.isPackage ? p.items.length && p.items[0].developer || "" : ""); }, width: "150" }]; layout.on("eachTheme", function(e) { var height = parseInt(ui.getStyleRule(".bar-preferences .blackdg .tree-row", "height"), 10) || 24; model.rowHeightInner = height; model.rowHeight = height; if (e.changed) datagrid.resize(true); }); reloadModel(); // type: "custom", // title: "Introduction", // position: 1, // node: intro = new ui.bar({ // height: 149, // "class" : "intro", // style: "padding:12px;position:relative;" // }) // intro.$int.innerHTML = // '

Keybindings

Change these settings to configure ' // + 'how Cloud9 responds to your keyboard commands.

' // + '

You can also manually edit your keymap file.

' // + '

Hint: Double click on the keystroke cell in the table below to change the keybinding.

'; // intro.$int.querySelector("a").onclick = function(){ editUserKeys(); }; var hbox = new ui.hbox({ htmlNode: e.html, padding: 5, edge: "10 10 0 10", childNodes: [ filterbox = new apf.codebox({ realtime: true, skin: "codebox", "class": "dark", clearbutton: true, focusselect: true, height: 27, width: 250, singleline: true, "initial-message": "Search installed plugins" }), new ui.filler({}), btnUninstall = new ui.button({ skin: "btn-default-css3", caption: "Uninstall", class: "btn-red", onclick: function() { var item = datagrid.selection.getCursor(); if (item.isPackage) uninstall(item.name, function() {}); else if (item.enabled == "true") disable(item.name, function() {}); else enable(item.name, function() {}); } }), btnReport = new ui.button({ skin: "btn-default-css3", caption: "Report Issue" }), btnReadme = new ui.button({ skin: "btn-default-css3", caption: "Open README" }), btnCloud9 = new ui.button({ skin: "btn-default-css3", caption: "Open in Cloud9" }), btnReload = new ui.button({ skin: "btn-default-css3", caption: "Reload", onclick: function() { var item = datagrid.selection.getCursor(); if (item.enabled && item.name) reload(item.name); } }) ] }); var div = e.html.appendChild(document.createElement("div")); div.style.position = "absolute"; div.style.left = "10px"; div.style.right = "10px"; div.style.bottom = "10px"; div.style.top = "50px"; datagrid = new Tree(div); datagrid.setTheme({ cssClass: "blackdg" }); datagrid.setDataProvider(model); layout.on("resize", function() { datagrid.resize(); }, plugin); function setTheme(e) { filterbox.setAttribute("class", e.theme.indexOf("dark") > -1 ? "dark" : ""); } layout.on("themeChange", setTheme); setTheme({ theme: settings.get("user/general/@skin") }); filterbox.ace.commands.addCommands([ { bindKey: "Enter", exec: function() { } }, { bindKey: "Esc", exec: function(ace) { ace.setValue(""); } } ]); filterbox.ace.on("input", function(e) { applyFilter(); }); // when tab is restored datagrids size might be wrong // todo: remove this when apf bug is fixed datagrid.once("mousemove", function() { datagrid.resize(true); }); datagrid.on("changeSelection", function(e) { var item = datagrid.selection.getCursor(); if (item.isGroup) { btnUninstall.disable(); btnReport.disable(); btnReadme.disable(); btnCloud9.disable(); btnReload.disable(); } else { if (item.isPackage) { btnUninstall.setCaption("Uninstall"); btnUninstall.setAttribute("class", "btn-red"); } else { btnUninstall.setCaption(item.enabled == "true" ? "Disable" : "Enable"); btnUninstall.setAttribute("class", item.enabled == "true" ? "btn-red" : "btn-green"); } if (CORE[item.name] || item.parent.parent && item.parent.parent.isType == "core") { btnUninstall.disable(); } else { btnUninstall.enable(); } if (item.isPackage || CORE[item.name] || item.parent.parent && item.parent.parent.isType == "core") { btnReload.disable(); } else { btnReload.enable(); } btnReport.enable(); btnReadme.enable(); btnCloud9.enable(); } }); } /***** Methods *****/ function reloadModel() { if (!model) return; var groups = {}; var packages = {}; var root = []; ["custom", "pre", "core", "runtime"].forEach(function(name) { root.push(groups[name] = { items: [], isOpen: name != "runtime", className: "group", isGroup: true, isType: name, noSelect: true, name: GROUPS[name] }); }); var lut = ext.named; ext.plugins.forEach(function(plugin) { var info = architect.pluginToPackage[plugin.name]; var packageName = info && info.package || "runtime"; var groupName; if (CORE[packageName]) groupName = "core"; else if (info && info.isAdditionalMode) groupName = "custom"; else groupName = "pre"; var package; if (packageName == "runtime") { package = groups.runtime; } else { package = packages[packageName]; if (!package) groups[groupName].items.push(package = packages[packageName] = { items: [], isPackage: true, className: "package", parent: groups[groupName], name: packageName }); } package.items.push({ name: plugin.name, enabled: lut[plugin.name].loaded ? "true" : "false", time: plugin.time, version: info && info.version || "N/A", parent: package, package: packageName, developer: plugin.developer == "Ajax.org" ? "Cloud9" : plugin.developer }); }); model.cachedRoot = { items: root }; applyFilter(); } function applyFilter() { model.keyword = filterbox && filterbox.getValue(); if (!model.keyword) { model.reKeyword = null; model.setRoot(model.cachedRoot); // model.isOpen = function(node){ return node.isOpen; } } else { model.reKeyword = new RegExp("(" + util.escapeRegExp(model.keyword) + ")", 'i'); var root = search.treeSearch(model.cachedRoot.items, model.keyword, true); model.setRoot(root); // model.isOpen = function(node){ return true; }; } } function uninstall(name) { btnUninstall.setAttribute("caption", "..."); btnUninstall.disable(); // @TODO first disable the plugin proc.spawn("c9", { args: ["uninstall", name]}, function(err, p) { p.stdout.on("data", function(c) { }); p.stderr.on("data", function(c) { }); p.on("exit", function(code) { if (code) { return alert("Could not uninstall plugin", "Could not uninstall plugin", "Could not uninstall plugin"); } btnUninstall.setAttribute("caption", "Uninstall"); btnUninstall.enable(); }); }); } function enable(name) { try { ext.enablePlugin(name); } catch (e) { alert("Could not disable plugin", "Got an error when disabling plugin: " + name, e.message); return false; } reloadModel(); } function disable(name, callback) { var deps = ext.getDependencies(name); var plugins = ext.named; for (var i = 0; i < deps.length; i++) { ext.getDependencies(deps[i]).forEach(function(name) { if (deps.indexOf(name) == -1) deps.push(name); }); } if (deps.length) { confirm("Found " + deps.length + " plugins that depend on this plugin.", "Would you like to disable all the plugins that depend on '" + name + "'?", "These plugins would also be disabled: " + deps.join(", "), // Yes function() { if (deps.reverse().every(function(name) { console.log("Disabling", name); return !recurDisable(name); })) { disable(name); reloadModel(); } }, // No function() { callback(new Error("User Cancelled")); }); } else { var e = disable(name); if (!e) reloadModel(); callback(e); } function recurDisable(name) { var deps = ext.getDependencies(name); if (deps.length) { if (!deps.every(function(name) { return !recurDisable(name); })) return false; } return disable(name); } function disable(name) { if (!plugins[name].loaded) return; try { ext.disablePlugin(name); } catch (e) { alert("Could not disable plugin", "Got an error when disabling plugin: " + name, e.message); return e; } } } function createNewPlugin(template) { if (!template) template = "c9.ide.default"; var url = staticPrefix + "/" + join("templates", template + ".tar.gz"); if (!url.match(/^http/)) url = location.origin + url; function getPath(callback, i) { i = i || 0; var path = join("~", ".c9/plugins/", template + (i ? "." + i : "")); fs.exists(path, function(exists) { if (exists) return getPath(callback, i + 1); callback(null, path); }); } function handleError(err) { showError("Could not create plugin."); console.error(err); } getPath(function(err, path) { if (err) return handleError(err); var pluginsDir = join("~", ".c9/plugins/_/"); var pluginsDirAbsolute = pluginsDir.replace(/^~/, c9.home); var tarPath = join(pluginsDir, template + ".tar.gz"); var tarPathAbsolute = tarPath.replace(/^~/, c9.home); // Download tar file with template for plugin proc.execFile("bash", { args: ["-c", [ // using mkdirp since "--create-dirs" is broken on windows "mkdir", "-p", util.escapeShell(dirname(tarPathAbsolute)), ";", "curl", "-L", util.escapeShell(url), "-o", util.escapeShell(tarPathAbsolute)].join(" ") ] }, function(err, stderr, stdout) { if (err) return handleError(err); // Untar tar file proc.execFile("bash", { args: ["-c", ["tar", "-zxvf", util.escapeShell(tarPath), "-C", util.escapeShell(pluginsDirAbsolute)].join(" ")] }, function(err, stderr, stdout) { if (err) return handleError(err); // Move template to the right folder var dirPath = join(dirname(tarPath), template); fs.rename(dirPath, path, function(err) { if (err) return handleError(err); // Remove .tar.gz fs.unlink(tarPath, function() { // Add plugin to favorites favs.addFavorite(dirname(pluginsDir), "plugins"); // Select and expand the folder of the plugin tree.expandAndSelect(path); }); }); }); }); }); } function reload(name) { showReloadTip(name); var href = document.location.href.replace(/[?&]reload=[^&]+/, ""); href += (href.match(/\?/) ? "&" : "?") + "reload=" + name; window.history.replaceState(window.history.state, null, href); for (var plugin in architect.lut) { if (architect.lut[plugin].provides.indexOf(name) < 0) continue; pluginDebug.reloadPackage(plugin); return; } } function showReloadTip(name) { if (options.devel) { var key = commands.getHotkey("reloadLastPlugin"); if (commands.platform == "mac") key = apf.hotkeys.toMacNotation(key); if (!getLastReloaded()) { showInfo("Reloaded " + name + ". Press " + key + " to reload again.", 3000); return; } } showInfo("Reloaded " + name + ".", 1000); } function getLastReloaded() { return qs.parse(document.location.search.substr(1)).reload; } var packages = {}; function loadPackage(options, callback) { if (Array.isArray(options)) return async.map(options, loadPackage, callback || function() {}); if (typeof options == "string") { if (/^https?:/.test(options)) { options = { url: options }; } else if (/^[~\/]/.test(options)) { options = { path: options }; } else if (/^[~\/]/.test(options)) { options = { url: require.toUrl(options) }; } } if (!options.url && options.path) options.url = vfs.vfsUrl(options.path); var parts = options.url.split("/"); var root = parts.pop(); options.url = parts.join("/"); if (!options.name) { // try to find the name from file name options.name = /^package\.(.*)\.js$|$/.exec(root)[1]; // try folder name if (!options.name || options.name == "json") options.name = parts[parts.length - 1]; // try parent folder name if (/^(.?build|master)/.test(options.name)) options.name = parts[parts.length - 2]; // remove version from the name options.name = options.name.replace(/@.*$/, ""); } if (!options.packageName) options.packageName = root.replace(/\.js$/, ""); if (!options.rootDir) options.rootDir = "plugins"; var name = options.name; var id = options.rootDir + "/" + name; var pathMappings = {}; pathMappings[id] = options.url; requirejs.config({ paths: pathMappings }); requirejs.undef(id + "/", true); if (/\.js$/.test(root)) { require([options.url + "/" + root], function(json) { json = json || require(id + "/" + options.packageName); getPluginsFromPackage(json, callback); }, function(err) { addError("Error loading plugin", err); }); } else if (options.path && /\.json$/.test(root)) { fs.readFile(options.path, function(err, value) { if (err) return addError("Error reading " + options.path, err); try { var json = JSON.parse(value); } catch (e) { return addError("Error parsing package.json", e); } json.fromVfs = true; getPluginsFromPackage(json, callback); }); } else if (options.url && /\.json$/.test(root)) { require(["text!" + options.id + "/" + root], function(value) { try { var json = JSON.parse(value); } catch (e) { return addError("Error parsing package.json", e); } getPluginsFromPackage(json, callback); }, function(err) { addError("Error loading plugin", err); }); } else { callback && callback(new Error("Missing path and url")); } function addError(message, err) { if (!packages[name]) packages[name] = {}; packages[name].filePath = options.path; packages[name].url = options.url; packages[name].__error = new Error(message + "\n" + err.message); reloadModel(); callback && callback(err); } function getPluginsFromPackage(json, callback) { var plugins = []; if (json.name != name) name = json.name; var unhandledPlugins = json.c9 && json.c9.plugins || json.plugins; if (unhandledPlugins) { Object.keys(unhandledPlugins).forEach(function(name) { var plugin = unhandledPlugins[name]; if (typeof plugin == "string") plugin = { packagePath: plugin }; if (!plugin.packagePath) plugin.packagePath = id + "/" + name; plugin.staticPrefix = options.url; plugins.push(plugin); }); } packages[json.name] = json; json.filePath = options.path; json.url = options.url; if (!json.c9) json.c9 = {}; json.c9.plugins = plugins; json.enabled = true; json.path = id; loadPlugins(plugins, callback); } } function loadPlugins(plugins, callback) { architect.loadAdditionalPlugins(plugins, function(err) { callback && callback && callback(err); }); } /***** Lifecycle *****/ plugin.on("load", function() { load(); }); plugin.on("draw", function(e) { draw(e); }); plugin.on("activate", function(e) { datagrid && datagrid.resize(); }); plugin.on("resize", function(e) { datagrid && datagrid.resize(); }); plugin.on("enable", function() { }); plugin.on("disable", function() { }); plugin.on("unload", function() { loaded = false; drawn = false; architect = null; model = null; datagrid = null; filterbox = null; btnUninstall = null; btnReport = null; btnReadme = null; btnCloud9 = null; btnReload = null; }); /***** Register and define API *****/ /** * **/ plugin.freezePublicAPI({ /** * */ get architect() { throw new Error(); }, set architect(v) { architect = v; architect.on("ready-additional", function() { reloadModel(); }); }, /** * */ createNewPlugin: createNewPlugin, /** * */ loadPackage: loadPackage, /** * */ uninstall: uninstall, /** * */ enable: enable, /** * */ disable: disable, /** * */ reload: reload }); register(null, { "pluginManager": plugin, "plugin.manager": plugin }); } });