c9-core/plugins/c9.core/settings.js

696 wiersze
23 KiB
JavaScript

define(function(require, exports, module) {
main.consumes = [
"c9", "ui", "Plugin", "fs", "proc", "api", "info", "ext", "util"
];
main.provides = ["settings"];
return main;
function main(options, imports, register) {
var c9 = imports.c9;
var ext = imports.ext;
var ui = imports.ui;
var Plugin = imports.Plugin;
var fs = imports.fs;
var proc = imports.proc;
var api = imports.api;
var info = imports.info;
var util = imports.util;
var _ = require("lodash");
var join = require("path").join;
/***** Initialization *****/
var plugin = new Plugin("Ajax.org", main.consumes);
var emit = plugin.getEmitter();
// Give the info, ext plugin a reference to settings
info.settings = plugin;
ext.settings = plugin;
// We'll have a lot of listeners, so upping the limit
emit.setMaxListeners(10000);
var resetSettings = options.reset || c9.location.match(/reset=([\w\|]*)/) && RegExp.$1;
var develMode = c9.location.indexOf("devel=1") > -1;
var debugMode = c9.location.indexOf("debug=2") > -1;
var testing = options.testing;
var debug = options.debug;
// do not leave reset= in url
if (resetSettings && window.history)
window.history.pushState(null, null, location.href.replace(/reset=([\w\|]*)/,""));
var TEMPLATE = options.template || { user: {}, project: {}, state: {} };
var INTERVAL = 1000;
var PATH = {
"project" : c9.toInternalPath(options.projectConfigPath || "/.c9") + "/project.settings",
"user" : c9.toInternalPath(options.userConfigPath || "~/.c9") + "/user.settings",
"state" : c9.toInternalPath(options.stateConfigFilePath || (options.stateConfigPath || "/.c9") + "/state.settings")
};
var KEYS = Object.keys(PATH);
var saveToCloud = {};
var model = {};
var cache = {};
var diff = 0; // TODO should we allow this to be undefined and get NaN in timestamps?
var userData;
var inited = false;
function loadSettings(json) {
if (!json) {
// Load from TEMPLATE
if (options.settings == "defaults" || testing)
json = TEMPLATE;
// Load from parsed settings in the index file
else if (options.settings) {
json = options.settings;
if (debugMode)
json.state = localStorage["debugState" + c9.projectName];
for (var type in json) {
if (typeof json[type] == "string") {
if (json[type].charAt(0) == "<") {
json[type] = TEMPLATE[type];
}
else {
try {
json[type] = JSON.parse(json[type]);
} catch (e) {
json[type] = TEMPLATE[type];
}
}
}
}
}
if (!json) {
var info = {};
var count = KEYS.length;
KEYS.forEach(function(type) {
fs.readFile(PATH[type], function(err, data) {
try {
info[type] = err ? {} : JSON.parse(data);
} catch (e) {
console.error("Invalid Settings Read for ",
type, ": ", data);
info[type] = {};
}
if (--count === 0)
loadSettings(info);
});
});
return;
}
}
read(json);
events();
if (resetSettings)
saveToFile();
KEYS.forEach(function(type) {
var node = model[type];
if (node)
cache[type] = JSON.stringify(node);
});
model.loaded = true;
}
/***** Methods *****/
var dirty, timer;
function checkSave(){
if (dirty)
saveToFile();
}
function startTimer(){
if (c9.readonly) return;
clearInterval(timer);
timer = setInterval(checkSave, INTERVAL);
}
function save(force, sync) {
dirty = true;
if (force) {
saveToFile();
startTimer();
}
}
function saveToFile(sync) {
if (c9.readonly || !plugin.loaded)
return;
if (c9.debug)
console.log("Saving Settings...");
emit("write", { model : model });
model.time = new Date().getTime();
if (develMode) {
dirty = false;
return;
}
saveModel(sync);
}
function saveModel(forceSync) {
if (c9.readonly || !c9.has(c9.NETWORK)) return;
if (model.loaded && !testing) {
KEYS.forEach(function(type) {
var node = model[type];
if (!node) return;
// Get XML string
var json = util.stableStringify(node, 0, " ");
if (cache[type] == json) return; // Ignore if same as cache
// Set Cache
cache[type] = json;
// Debug mode
if (debugMode && type == "state") {
localStorage["debugState" + c9.projectName] = json;
return;
}
// Detect whether we're in standalone mode
var standalone = !options.hosted;
if (standalone || type == "project") {
fs.writeFile(PATH[type], json, forceSync, function(err){});
if (standalone && !saveToCloud[type])
return; // We're done
}
var addPid = type !== "user"
? "/" + info.getWorkspace().id
: "";
// Save settings in persistent API
api.settings.put(type + addPid, {
body: { settings: json },
sync: forceSync
}, function (err) {});
});
}
dirty = false;
}
function read(json, isReset) {
try {
if (testing) throw "testing";
KEYS.forEach(function(type) {
if (json[type])
model[type] = json[type];
});
if (resetSettings) {
var query = (resetSettings == 1
? "user|state" : resetSettings).split("|");
query.forEach(function(type) {
model[type] = TEMPLATE[type];
});
}
} catch (e) {
KEYS.forEach(function(type) {
model[type] = TEMPLATE[type];
});
}
if (!c9.debug) {
try {
emit("read", {
model: model,
ext: plugin,
reset: isReset
});
} catch (e) {
fs.writeFile(PATH.project
+ ".broken", JSON.stringify(json), function(){});
KEYS.forEach(function(type) {
model[type] = TEMPLATE[type];
});
emit("read", {
model: model,
ext: plugin,
reset: isReset
});
}
}
else {
emit("read", {
model: model,
ext: plugin,
reset: isReset
});
}
if (inited)
return;
inited = true;
plugin.on("newListener", function(type, cb) {
if (type != "read") return;
if (c9.debug || debug) {
cb({model : model, ext : plugin});
}
else {
try {
cb({ model : model, ext : plugin });
}
catch (e) {
console.error(e.message, e.stack);
}
}
});
}
var hasSetEvents;
function events() {
if (hasSetEvents) return;
hasSetEvents = true;
startTimer();
c9.on("beforequit", function(){
emit("write", { model: model, unload: true });
saveModel(true); //Forcing sync xhr works in chrome
}, plugin);
c9.on("stateChange", function(e) {
if (e.state | c9.NETWORK && e.last | c9.NETWORK)
saveToFile(); //Save to file
}, plugin);
}
function migrate(pathFrom, pathTo) {
var idx = pathFrom.lastIndexOf("/");
var nodeParent = getNode(pathFrom.substr(0, idx));
var name = pathFrom.substr(idx + 1);
var nodeFrom = nodeParent && nodeParent[name];
if (!nodeFrom) return;
// Remove node
delete nodeParent[name];
// Create new node
var nodeTo = getNode(pathTo) || setNode(pathTo, {}) && getNode(pathTo);
// Move attributes
for (var prop in nodeFrom) {
nodeTo[prop] = nodeFrom[prop];
}
}
function setDefaults(path, attr) {
var node = getNode(path) || set(path, {}, true, true) && getNode(path);
var changed;
attr.forEach(function(a) {
var name = "@" + a[0];
if (!node.hasOwnProperty(name)) {
node[name] = a[1];
emit(path + "/" + name, a[1]);
changed = true;
}
});
if (changed)
emit(path);
}
function update(type, json, ud){
// Do nothing if they are the same
if (_.isEqual(model[type], json))
return;
userData = ud;
// Compare key/values (assume source has same keys as target)
(function recur(source, target, base){
for (var prop in source) {
if (prop == "json()") {
setJson(base, source[prop]);
}
else if (typeof source[prop] == "object") {
if (!target[prop]) target[prop] = {};
recur(source[prop], target[prop], join(base, prop));
}
else if (source[prop] != target[prop]) {
set(join(base, prop), source[prop]);
}
}
})(json, model[type], type);
userData = null;
}
function setNode(query, value) {
return set(query, value, true);
}
function set(query, value, isNode, isDefault, checkDefined) {
if (!inited && !isDefault) return false;
var parts = query.split("/");
var key = parts.pop();
if (!isNode && key.charAt(0) !== "@") {
parts.push(key);
key = "json()";
}
var hash = model;
if (!parts.every(function(part) {
if (!hash[part] && checkDefined) return false;
hash = hash[part] || (hash[part] = {});
return hash;
})) {
console.warn("Setting non defined query: ", query);
return false;
}
if (hash[key] === value)
return;
var oldValue = hash[key];
hash[key] = value;
// Tell everyone this property changed
emit(parts.join("/"), value, oldValue);
// Tell everyone it's parent changed
emit(query, value, oldValue);
// Tell everyone the root type changed (user, project, state)
scheduleAnnounce(parts[0], userData);
dirty = true; //Prevent recursion
return true;
}
var timers = {};
function scheduleAnnounce(type, userData){
clearTimeout(timers[type]);
timers[type] = setTimeout(function(){
emit("change:" + type, { data: model[type], userData: userData });
});
}
function setJson(query, value) {
return set(query, value);
}
function getJson(query) {
var json = get(query, true);
if (query.indexOf("@") == -1 && query.indexOf("json()") == -1)
json = json["json()"];
if (typeof json === "object")
return JSON.parse(JSON.stringify(json));
if (typeof json === "string") {
try {
return JSON.parse(json);
} catch (e) {}
}
// do not return null or undefined so that getJson(query).foo never throws
return false;
}
function getBool(query) {
var bool = get(query);
return ui.isTrue(bool) || (ui.isFalse(bool) ? false : undefined);
}
function getNumber(query) {
var double = get(query);
return parseFloat(double, 10);
}
function getNode(query) {
return get(query, true);
}
function get(query, isNode) {
var parts = query.split("/");
if (!isNode && parts[parts.length - 1].charAt(0) !== "@")
parts.push("json()");
var hash = model;
parts.every(function(part) {
hash = hash[part];
return hash;
});
return hash === undefined ? "" : hash;
}
function exist(query) {
var parts = query.split("/");
var hash = model;
return parts.every(function(part) {
hash = hash[part];
return hash;
});
}
function reset(query) {
if (!query) query = "user|state";
var info = {};
query.split("|").forEach(function(type) {
info[type] = TEMPLATE[type];
});
read(model, true);
saveToFile();
}
/***** Lifecycle *****/
plugin.on("load", function(){
// Give ui a reference to settings
ui.settings = plugin;
// Get the Time
proc.execFile("node", {
args: ["-e", "console.log(Date.now())"]
}, function(err, stdout, stderr) {
if (err || stderr)
return;
var time = parseInt(stdout, 10);
diff = Date.now() - time;
});
});
plugin.on("enable", function(){
});
plugin.on("disable", function(){
});
plugin.on("unload", function(){
dirty = false;
diff = 0;
userData = null;
inited = false;
clearInterval(timer);
});
/***** Register and define API *****/
/**
* Settings for Cloud9. Settings are stored based on a path pointing
* to leaves. Each leaf can be accessed using the "@" char.
*
* Example:
*
* settings.set("user/tree/@width", "200");
*
* Example:
*
* settings.getNumber("user/tree/@width");
* @singleton
*/
plugin.freezePublicAPI({
/**
* Exposes the model object that stores the XML used to store the
* settings. This property is here for backwards compatibility only
* and will be removed in the next version.
* @property model
* @deprecated
* @private
*/
model: model, //Backwards compatibility, should be removed in a later version
/**
* @property {Boolean} inited whether the settings have been loaded
*/
get inited(){ return inited; },
/**
* The offset between the server time and the client time in
* milliseconds. A positive number means the client is ahead of the
* server.
* @property timeOffset
* @readonly
*/
get timeOffset(){ return diff; },
/**
*
*/
get paths(){ return PATH; },
/**
*
*/
get saveToCloud(){ return saveToCloud; },
_events: [
/**
* @event read Fires when settings are read
*/
"read",
/**
* @event write Fires when settings are written
* @param {Object} e
* @param {Boolean} e.unload specifies whether the application
* is being unloaded. During an unload there is not much time
* and only the highly urgent information should be saved in a
* way that the browser still allows (socket is gone, etc).
**/
"write"
],
/**
* Saves the most current settings after a timeout
* @param {Boolean} force forces the settings to be saved immediately
*/
save: save,
/**
* Loads the xml settings into the application
* @param {XMLElement} xml The settings xml
*/
read: read,
/**
* Sets a value in the settings tree
* @param {String} path the path specifying the key for the value
* @param {String} value the value to store in the specified location
*/
"set" : set,
/**
* Sets a value in the settings tree and serializes it as JSON
* @param {String} path the path specifying the key for the value
* @param {String} value the value to store in the specified location
*/
"setJson" : setJson,
/**
* Gets a value from the settings tree
* @param {String} path the path specifying the key for the value
*/
"get" : get,
/**
* Gets a value from the settings tree and interprets it as JSON
* @param {String} path the path specifying the key for the value
*/
"getJson" : getJson,
/**
* Gets a value from the settings tree and interprets it as Boolean
* @param {String} path the path specifying the key for the value
*/
"getBool" : getBool,
/**
* Gets a value from the settings tree and interprets it as Boolean
* @param {String} path the path specifying the key for the value
*/
"getNumber" : getNumber,
/**
* Gets an object from the settings tree and returns it
* @param {String} path the path specifying the key for the value
*/
"getNode" : getNode,
/**
* Checks to see if a node exists
* @param {String} path the path specifying the key for the value
*/
"exist" : exist,
/**
* Sets the default attributes of a settings tree node.
*
* Example:
*
* settings.setDefaults("user/myplugin", [
* ["width", 200],
* ["show", true]
* ])
*
* @param {String} path the path specifying the key for the value
* @param {Array} attr two dimensional array with name
* values of the attributes for which the defaults are set
*/
setDefaults: setDefaults,
/**
* Moves and renames attributes from one path to another path
* @param {String} fromPath the path specifying where key for the value
* @param {String} toPath the path specifying where key for the value
* @param {Array} attr two dimensional array with name
* values of the attributes for which the defaults are set
*/
migrate: migrate,
/**
* Resets the settings to their defaults
*/
reset: reset,
/**
* Update user, project or state settings incrementally
* @param {String} type
* @param {Object} settings
*/
update: update
});
if (c9.connected || options.settings)
loadSettings();
else
c9.once("connect", loadSettings);
register(null, {
settings: plugin
});
}
});