c9-core/plugins/c9.ide.editors/metadata.js

594 wiersze
22 KiB
JavaScript

define(function(require, exports, module) {
main.consumes = [
"c9", "Plugin", "fs", "settings", "tabManager", "dialog.error",
"dialog.question", "preferences", "save"
];
main.provides = ["metadata"];
return main;
function main(options, imports, register) {
var c9 = imports.c9;
var Plugin = imports.Plugin;
var fs = imports.fs;
var settings = imports.settings;
var save = imports.save;
var tabs = imports.tabManager;
var confirm = imports["dialog.question"].show;
var showError = imports["dialog.error"].show;
var prefs = imports.preferences;
/***** Initialization *****/
var plugin = new Plugin("Ajax.org", main.consumes);
var emit = plugin.getEmitter();
var PATH = options.path || "/.c9/metadata";
var WORKSPACE = "/workspace";
var jobs = {};
var changed = {};
var cached = {};
var worker, timer;
var CHANGE_CHECK_INTERVAL = options.changeCheckInterval || 30000;
var loaded = false;
function load(){
if (loaded) return false;
loaded = true;
// Schedule for inspection when tab becomes active
tabs.on("tabAfterActivate", function(e) {
// If disabled don't do anything
if (!e.tab.loaded || !settings.getBool("user/metadata/@enabled"))
return;
if (e.lastTab)
changed[e.lastTab.name] = e.lastTab;
changed[e.tab.name] = e.tab;
}, plugin);
// Closing a tab
tabs.on("tabAfterClose", function (e) {
// If disabled don't do anything
if (!settings.getBool("user/metadata/@enabled"))
return;
var doc = e.tab.document;
if (!e.tab.path) {
fs.unlink(PATH + "/" + e.tab.name, function(){ return false });
}
else if (doc.meta.newfile || doc.meta.$ignoreSave) {
fs.unlink(PATH + WORKSPACE + e.tab.path, function(){ return false });
}
else if (check(e.tab) !== false) {
delete changed[e.tab.name];
delete cached[e.tab.name];
}
}, plugin);
// Opening a file
tabs.on("beforeOpen", function(e) {
// If disabled don't do anything
if (!settings.getBool("user/metadata/@enabled"))
return;
// Don't load metadata if document state is defined or value is set
if (e.tab.path && e.options.document.filter === false
|| !e.tab.path && !e.options.document.filter
|| e.options.value)
return;
// todo should tabmanager check this?
e.options.loadFromDisk = e.loadFromDisk
&& !e.tab.document.meta.newfile
&& !e.tab.document.meta.nofs;
// Fetch the metadata and real data
var callback = function(err) {
if (e.loadFromDisk)
e.callback(err);
};
loadMetadata(e.tab, e.options, callback, e.options.init);
return e.loadFromDisk ? false : true;
}, plugin);
// Check every half a minute for changed tabs
timer = setInterval(function(){
checkChangedTabs();
}, CHANGE_CHECK_INTERVAL);
// Delete metadata when file is renamed
save.on("saveAs", function(e){
if (e.path != e.oldPath)
fs.unlink(PATH + WORKSPACE + e.oldPath, function(){ return false });
});
// Settings
settings.on("read", function(e) {
settings.setDefaults("user/metadata", [
["enabled", "true"],
["undolimit", "100"],
]);
}, plugin);
settings.on("write", function(e) {
if (e.unload) return;
checkChangedTabs();
}, plugin);
function checkChangedTabs(unload) {
// If disabled don't do anything
if (!settings.getBool("user/metadata/@enabled"))
return;
tabs.getPanes().forEach(function(pane) {
var tab = pane.getTab();
if (tab) {
changed[tab.name] = tab;
}
});
for (var name in changed) {
if (check(changed[name], unload) === false)
return;
}
changed = {};
}
// Preferences
prefs.add({
"File" : {
position: 150,
"Meta Data" : {
position: 200,
"Store Meta Data of Opened Files" : {
type: "checkbox",
path: "user/metadata/@enabled",
position: 100
},
"Maximum of Undo Stack Items in Meta Data" : {
type: "spinner",
path: "user/metadata/@undolimit",
position: 200,
min: 10,
max: 10000
}
}
}
}, plugin);
// Exiting Cloud9
c9.on("beforequit", function(){
checkChangedTabs(true);
}, plugin);
// Initial Load
tabs.getTabs().forEach(function(tab) {
var options = tab.getState();
options.loadFromDisk = tab.path
&& !tab.document.meta.newfile
&& !tab.document.meta.nofs
// autoload to false prevents loading data, used by image editor
&& (!tab.editor || tab.editor.autoload !== false);
loadMetadata(tab, options, function(err) {
if (err) {
tab.unload();
showError("File not found '" + tab.path + "'");
return;
}
tab.classList.remove("loading");
}, true);
});
}
/***** Methods *****/
function check(tab, forceSync) {
var docChanged = tab.document.changed || tab.document.meta.nofs;
// Don't save state if we're offline
if (!c9.has(c9.STORAGE))
return false;
if (tab.meta.$loadingMetadata)
return;
// Ignore metadata files and preview pages and debug files and
// tabs that are not loaded
if (!tab.loaded
|| tab.path && tab.path.indexOf(PATH) === 0
|| tab.document.meta.preview)
return;
var state = tab.document.getState();
// Make sure timestamp is preserved
if (state.meta)
state.timestamp = state.meta.timestamp;
// meta is recorded by the tab state
delete state.meta;
// If we discarded the file before closing, clear that data
if (tab.document.meta.$ignoreSave) {
delete state.value;
delete state.changed;
delete state.undoManager;
docChanged = true;
}
if (docChanged || typeof state.value == "undefined" || forceSync) {
if (forceSync && !docChanged && state.undoManager.stack.length)
delete state.value;
write(forceSync);
}
else {
hash(state.value, function(err, hash) {
if (err) return;
delete state.value;
delete state.changed;
state.hash = hash;
write(forceSync);
});
}
function write(forceSync) {
if (c9.readonly) return;
var limit = settings.getNumber("user/metadata/@undolimit");
var undo = state.undoManager;
if (undo) {
var start = Math.max(0, undo.position - limit);
undo.stack.splice(0, start);
undo.position -= start;
undo.mark -= start;
}
try {
// This throws when a structure is circular
var sstate = JSON.stringify(state);
} catch (e) {
// debugger;
return;
}
// Lets not save large metadata
if (sstate.length > 1024 * 1024) {
sstate = "";
state = {};
}
if (cached[tab.name] != sstate) {
cached[tab.name] = sstate;
if (tab.path) {
var path = (tab.path.charAt(0) == "~" ? "/" : "") + tab.path;
fs.metadata(path, state, forceSync, function(err) {
if (err)
return;
});
}
else {
fs.metadata("/_/_/" + tab.name, state, forceSync, function(err) {
if (err)
return;
});
}
}
}
return true;
}
function merge(from, to) {
for (var prop in from) {
if (to[prop] && typeof from[prop] == "object")
merge(from[prop], to[prop]);
else
to[prop] = from[prop];
}
}
function loadMetadata(tab, options, callback, init) {
// // When something goes wrong somewhere in cloud9, this can happen
// if (tab.path && "/~".indexOf(tab.path.charAt(0)) == -1)
// debugger;
var path = tab.path
? PATH + WORKSPACE + (tab.path.charAt(0) == "~" ? "/" : "") + tab.path
: PATH + "/" + tab.name;
var storedValue, storedMetadata;
var xhr;
// Prevent Saving of Metadata
tab.meta.$loadingMetadata = true;
// Progress Handling
tab.classList.add("loading");
var loadStartT = Date.now();
// Handler to show loading indicator
function progress(loaded, total, complete) {
var data = {
total: total,
loaded: loaded,
complete: complete,
dt: Date.now() - loadStartT
};
tab.document.progress(data);
}
if (tab.path) {
if (options.loadFromDisk === false) {
// This is for new files and other files that will store
// their value in the metadata
receive(tab.document.value);
// Don't load metadata of newly created newfile tabs (runtime)
if (!init) {
receive(null, -1);
return;
}
}
else {
tab.classList.add("loading");
// progress(0, 1, 0);
var cb = function(err, data, metadata, res) {
if (err) return callback(err);
receive(data, metadata || -1);
};
xhr = emit("beforeReadFile", {
tab: tab,
path: tab.path,
callback: cb,
progress: progress
});
if (!xhr)
xhr = fs.readFileWithMetadata(tab.path, "utf8", cb, progress);
}
}
if (!xhr) {
xhr = fs.readFile(path, "utf8", function(err, data) {
receive(null, err ? -1 : data);
}, progress);
}
// Cancel file opening when tab is closed
var abort = function(){ xhr && xhr.abort(); };
tab.on("close", abort);
tabs.on("open", function wait(e) {
if (e.tab == tab) {
tab.off("close", abort);
tab.off("open", wait);
}
}, tab);
function receive(value, metadata) {
var state;
if (value !== null)
storedValue = value;
if (metadata) {
storedMetadata = metadata;
if (metadata != -1) {
cached[tab.name]
= metadata.replace(/"timestamp":\d+\,?$/, "");
}
}
// Final state processing and then we're done
var compareModified = false;
function done(state, cleansed) {
// Import state from options
var doc = options.document;
if (doc instanceof Plugin)
doc = doc.getState();
delete doc.fullState;
delete doc.value;
delete doc.undoManager;
if (!doc.changed)
delete doc.changed;
if (cleansed) {
delete state.undoManager;
if (tab.editor && state[tab.editor.type])
state[tab.editor.type].cleansed = true;
}
// Preserve timestamp from metadata
if (!doc.meta) doc.meta = {};
doc.meta.timestamp = state.timestamp;
delete state.timestamp;
// Merge the two sources
merge(doc, state);
if (tab.document.hasValue())
delete state.value;
// Keep original value from disk
state.meta.$savedValue = storedValue;
var sameValue = state.value === storedValue;
if (options.loadFromDisk && (sameValue || compareModified)) {
fs.stat(tab.path, function(err, stat) {
if (err) return;
if (compareModified) {
// @todo this won't work well on windows, because
// there is a 20s period in which the mtime is
// the same. The solution would be to have a
// way to compare the saved document to the
// loaded document that created the state
if (!state.meta || state.meta.timestamp < stat.mtime) {
var doc = tab.document;
function checkChange(){
confirm("File Changed",
tab.path + " has been changed on disk.",
"Would you like to reload this file?",
function(){ // Yes
// Set new value and clear undo state
doc.setBookmarkedValue(storedValue, true);
doc.meta.timestamp = stat.mtime;
},
function(){ // No
// Set to changed
doc.undoManager.bookmark(-2);
doc.meta.timestamp = stat.mtime;
},
{ merge: false, all: false }
);
}
if (state.meta.preview) {
doc.editor.on("focus", function wait(e) {
if (doc.editor.activeDocument == doc) {
doc.editor.off("focus", wait);
checkChange();
}
}, doc);
return;
}
else
checkChange();
}
}
else {
tab.document.meta.timestamp = stat.mtime;
}
});
}
// Set new state
tab.document.setState(state);
// Declare done
delete tab.meta.$loadingMetadata;
callback();
}
if ((!tab.path || storedValue !== undefined) && storedMetadata) {
try{
state = storedMetadata == -1
? {} : JSON.parse(storedMetadata);
}
catch (e){ state = {} }
// There's a hash. Lets compare it to the hash of the
// current value. If they are the same we can keep the
// undo stack, otherwise we'll clear the undo stack
if (state.hash && typeof storedValue == "string") {
state.value = storedValue;
hash(storedValue, function(err, hash) {
done(state, state.hash != hash);
});
return; // Wait until hash is retrieved
}
else if (state.value && tab.path) {
// If the stored value is not the same as the value
// on disk we need to find out which is newer
if (state.value != storedValue)
compareModified = true;
}
else {
state.value = storedValue;
}
done(state);
}
}
}
hash.counter = 0;
function hash(data, callback) {
if (!worker) {
worker = new Worker('/static/lib/rusha/rusha.min.js');
worker.addEventListener("message", function(e) {
// @todo security?
if (jobs[e.data.id]) {
jobs[e.data.id](null, e.data.hash);
delete jobs[e.data.id];
}
});
}
worker.postMessage({ id: ++hash.counter, data: data });
jobs[hash.counter] = callback;
if (hash.counter === 30000)
hash.counter = 0;
}
/***** Lifecycle *****/
plugin.on("load", function(){
load();
});
plugin.on("enable", function(){
});
plugin.on("disable", function(){
});
plugin.on("unload", function(){
loaded = false;
clearInterval(timer);
});
/***** Register and define API *****/
/**
* Manages metadata for tabs in Cloud9. Each tab in Cloud9 has
* additional information that needs to be stored.
*
* When you open a file in Cloud9, it generally is opened in a tab and
* displayed using the {@link ace.Ace Ace} editor. The ace editor maintains a
* lot of state while displaying the file, such as the scroll position,
* the selection, the folds, the syntax highligher, etc. The document
* also serializes the value and the complete undo stack. Editors
* that don't open files can still hold metadata. The {@link terminal.Terminal Terminal}
* for instance has selection, scroll state and scroll history. All this
* information can be saved to disk by the metadata plugin.
*
* The metadata is saved in ~/.c9/metadata. The metadata plugin plugs
* into the tabManager and takes over the loading of the file content
* so that the loading of the content and the metadata is synchronized.
* This plugin is also responsible for saving the metadata back to the
* workspace.
*
* @singleton
**/
plugin.freezePublicAPI({
});
register(null, {
metadata: plugin
});
}
});