kopia lustrzana https://github.com/c9/core
696 wiersze
23 KiB
JavaScript
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
|
|
});
|
|
}
|
|
});
|