kopia lustrzana https://github.com/c9/core
626 wiersze
22 KiB
JavaScript
626 wiersze
22 KiB
JavaScript
define(function(require, exports, module) {
|
|
main.consumes = [
|
|
"Plugin", "fs", "settings", "preferences", "watcher", "tabManager",
|
|
"save", "dialog.question", "dialog.filechange", "threewaymerge"
|
|
];
|
|
main.provides = ["watcher.gui"];
|
|
return main;
|
|
|
|
function main(options, imports, register) {
|
|
var Plugin = imports.Plugin;
|
|
var watcher = imports.watcher;
|
|
var prefs = imports.preferences;
|
|
var fs = imports.fs;
|
|
var save = imports.save;
|
|
var settings = imports.settings;
|
|
var tabManager = imports.tabManager;
|
|
var question = imports["dialog.question"];
|
|
var filechange = imports["dialog.filechange"];
|
|
var threeWayMerge = imports.threewaymerge.merge;
|
|
|
|
var collabEnabled = options.collab;
|
|
|
|
/***** Initialization *****/
|
|
|
|
var plugin = new Plugin("Ajax.org", main.consumes);
|
|
var emit = plugin.getEmitter();
|
|
|
|
var removedPaths, changedPaths;
|
|
var deleteDialog, changeDialog, initialFocus;
|
|
|
|
var loaded = false;
|
|
function load(){
|
|
if (loaded) return false;
|
|
loaded = true;
|
|
|
|
removedPaths = {};
|
|
changedPaths = {};
|
|
|
|
// Settings and preferences
|
|
|
|
settings.on("read", function(e) {
|
|
settings.setDefaults("user/general", [
|
|
["automerge", "false"],
|
|
["confirmmerge", "true"]
|
|
]);
|
|
});
|
|
|
|
prefs.add({
|
|
"File" : {
|
|
position: 150,
|
|
"Watchers" : {
|
|
position: 300,
|
|
"Auto-Merge Files When a Conflict Occurs" : {
|
|
type: "checkbox",
|
|
path: "user/general/@automerge",
|
|
min: "1",
|
|
max: "64",
|
|
tooltip: "Whenever the file watcher detects a "
|
|
+ "file change on disk, 'auto merge' will fetch "
|
|
+ "the contents from disc and merges it with "
|
|
+ "the version in the editor.",
|
|
position: 2200
|
|
}
|
|
}
|
|
}
|
|
}, plugin);
|
|
|
|
// Watch for new documents and hook their value initialization
|
|
|
|
function initializeDocument(doc) {
|
|
if (!doc.meta.$savedValue)
|
|
doc.meta.$savedValue = doc.recentValue;
|
|
doc.on("setValue", function(e) {
|
|
doc.meta.$savedValue = e.value;
|
|
}, plugin);
|
|
}
|
|
|
|
tabManager.getTabs().forEach(function(tab) {
|
|
if (!tab.path)
|
|
return;
|
|
|
|
if (tab.document.undoManager.isAtBookmark()) {
|
|
initializeDocument(tab.document);
|
|
}
|
|
else {
|
|
console.error("Unsupported state");
|
|
}
|
|
|
|
if (tab.classList.contains("conflict")) {
|
|
addChangedTab(tab, true);
|
|
}
|
|
});
|
|
|
|
tabManager.on("open", function(e) {
|
|
initializeDocument(e.tab.document);
|
|
if (e.tab.classList.contains("conflict")) {
|
|
addChangedTab(e.tab, true);
|
|
}
|
|
}, plugin);
|
|
|
|
// Hook the save of the document value
|
|
|
|
save.on("beforeSave", function(e) {
|
|
e.document.meta.$savingValue = e.save;
|
|
if (e.tab.classList.contains("conflict")) {
|
|
showChangeDialog(e.tab);
|
|
}
|
|
}, plugin);
|
|
|
|
save.on("afterSave", function(e) {
|
|
var meta = e.document.meta;
|
|
if (!e.err) {
|
|
meta.$savedValue = meta.$savingValue;
|
|
watcher.watch(e.path);
|
|
}
|
|
delete meta.$savingValue;
|
|
delete meta.$mergeRoot;
|
|
}, plugin);
|
|
|
|
// Hook watcher events
|
|
|
|
// Update a file
|
|
watcher.on("change", function(e) {
|
|
var tab = tabManager.findTab(e.path);
|
|
if (tab) {
|
|
// If collab picks this up and handles the change it will return false
|
|
if (emit("docChange", {tab: tab}) === false)
|
|
return;
|
|
|
|
addChangedTab(tab, e.type === "change");
|
|
}
|
|
});
|
|
|
|
// Directory watcher is not needed if the normal watcher works
|
|
// watcher.on("directory", function(e) {
|
|
// var base = e.path;
|
|
// var files = e.files;
|
|
//
|
|
// // Rename all tabs
|
|
// tabManager.getTabs().forEach(function(tab) {
|
|
// if (tab.path && tab.path.indexOf(base) == 0) {
|
|
// // If the file is gone, lets notify the user
|
|
// if (files.indexOf(tab.path) == -1) {
|
|
// resolveFileDelete(tab);
|
|
// }
|
|
// }
|
|
// });
|
|
// });
|
|
|
|
watcher.on("delete", function(e) {
|
|
var tab = tabManager.findTab(e.path);
|
|
if (tab)
|
|
addDeletedTab(tab);
|
|
});
|
|
}
|
|
|
|
/***** Methods *****/
|
|
|
|
function addChangedTab(tab, isSameFile) {
|
|
// If we already have a dialog open, just update it, but mark the value dirty
|
|
if (changedPaths[tab.path]) {
|
|
if (changedPaths[tab.path].data)
|
|
changedPaths[tab.path].dirty = true;
|
|
if (changeDialog && changeDialog.visible)
|
|
return;
|
|
}
|
|
|
|
// Ignore changes that come in while tab is being saved
|
|
if (tab.document.meta.$saving) {
|
|
console.log("[watchers] Watcher fired, but tab is still saving", path);
|
|
return;
|
|
}
|
|
|
|
changedPaths[tab.path] = { tab: tab, resolve: resolve };
|
|
|
|
// If the terminal is currently focussed, lets wait until
|
|
// another tab is focussed
|
|
if (tabManager.focussedTab
|
|
&& tabManager.focussedTab.editorType == "terminal") {
|
|
tabManager.once("focus", function(){
|
|
addChangedTab(tab, false);
|
|
});
|
|
return;
|
|
}
|
|
|
|
function resolve() {
|
|
console.log("[watchers] resolved change event without dialog", path);
|
|
doc.tab.classList.remove("conflict");
|
|
delete doc.meta.$merge;
|
|
delete changedPaths[path];
|
|
}
|
|
|
|
var doc = tab.document;
|
|
var path = tab.path;
|
|
|
|
if (isSameFile)
|
|
checkByStatOrContents();
|
|
else
|
|
checkByContents();
|
|
|
|
function dialog(data) {
|
|
if (!changedPaths[path])
|
|
return;
|
|
changedPaths[path].data = data || changedPaths[path].data;
|
|
|
|
if (changeDialog) {
|
|
// The dialog is visible
|
|
if (changeDialog.visible === 1) {
|
|
question.all = true;
|
|
return;
|
|
}
|
|
// The dialog still is to become visible
|
|
else if (changeDialog.visible === undefined) {
|
|
changeDialog.on("show", function(){
|
|
question.all = true;
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (tabManager.focussedTab && !changedPaths[tabManager.focussedTab.path]) {
|
|
doc.tab.classList.add("conflict");
|
|
// Let's try again later, maybe then one of our paths gets focus
|
|
return tabManager.once("focus", function() {
|
|
dialog();
|
|
});
|
|
}
|
|
|
|
if (!tabManager.findTab(path)) // drat! tab is gone
|
|
return;
|
|
|
|
// Show dialogs for changed tabs
|
|
for (var changedPath in changedPaths) {
|
|
tab = changedPaths[changedPath].tab;
|
|
data = changedPaths[changedPath].data;
|
|
showChangeDialog(tab, data);
|
|
}
|
|
}
|
|
|
|
function checkByStatOrContents() {
|
|
fs.stat(path, function(err, stat) {
|
|
if (!err && doc.meta.timestamp >= stat.mtime)
|
|
return resolve();
|
|
checkByContents(stat);
|
|
});
|
|
}
|
|
|
|
function checkByContents(stat) {
|
|
fs.readFile(path, function(err, data) {
|
|
if (err) {
|
|
console.warn("[watchers] Could not read", path, "will assume it got changed");
|
|
return dialog();
|
|
}
|
|
|
|
// false alarm. File content didn't change
|
|
if (data === doc.meta.$savedValue)
|
|
return resolve();
|
|
|
|
// Store base value for merges
|
|
if (doc.meta.$mergeRoot == undefined)
|
|
doc.meta.$mergeRoot = doc.meta.$savedValue || doc.recentValue;
|
|
// Update saved value
|
|
doc.meta.$savedValue = data;
|
|
if (stat)
|
|
doc.meta.timestamp = stat.mtime;
|
|
|
|
// short cut: remote value is the same as the current value
|
|
if (data === doc.value) { // Expensive check
|
|
|
|
// Remove the changed state from the document
|
|
doc.undoManager.bookmark();
|
|
|
|
// Mark as resolved
|
|
resolve();
|
|
|
|
return;
|
|
} else {
|
|
// if this is the first time change notification comes
|
|
// remember undomanger state for deciding to merge or not
|
|
if (doc.meta.$merge == undefined)
|
|
doc.meta.$merge = !doc.undoManager.isAtBookmark();
|
|
doc.undoManager.bookmark(-2);
|
|
}
|
|
|
|
if (automerge(tab, data))
|
|
resolve();
|
|
else
|
|
dialog(data);
|
|
});
|
|
}
|
|
}
|
|
|
|
function automerge(tab, data) {
|
|
if (!settings.getBool("user/general/@automerge"))
|
|
return false;
|
|
|
|
return merge(tab, data);
|
|
}
|
|
|
|
function merge(tab, data) {
|
|
if (tab.editor.type != "ace")
|
|
return false;
|
|
|
|
var doc = tab.document;
|
|
var root = doc.meta.$mergeRoot || doc.meta.$savedValue;
|
|
|
|
if (typeof root !== "string")
|
|
return false;
|
|
|
|
var aceDoc = doc.getSession().session.doc;
|
|
var mergedValue = threeWayMerge(root, data, aceDoc);
|
|
|
|
doc.meta.$mergeRoot = data;
|
|
|
|
// If the value on disk is the same as in the document, set the bookmark
|
|
if (mergedValue == data)
|
|
doc.undoManager.bookmark();
|
|
|
|
return true;
|
|
}
|
|
|
|
function getLatestValue(path, callback) {
|
|
if (!changedPaths[path] || changedPaths[path].dirty || !changedPaths[path].data) {
|
|
fs.readFile(path, function(err, data) {
|
|
callback(err, path, data);
|
|
});
|
|
}
|
|
else {
|
|
callback(null, path, changedPaths[path].data);
|
|
}
|
|
}
|
|
|
|
function updateChangedPath(err, path, data) {
|
|
var tab = changedPaths[path].tab || tabManager.findTab(path);
|
|
if (!tab)
|
|
return changedPaths[path] && changedPaths[path].resolve();
|
|
var doc = tab.document;
|
|
doc.setBookmarkedValue(data, true);
|
|
doc.meta.timestamp = Date.now() - settings.timeOffset;
|
|
changedPaths[path].resolve();
|
|
}
|
|
|
|
function mergeChangedPath(err, path, data) {
|
|
merge(changedPaths[path].tab, data);
|
|
changedPaths[path].resolve();
|
|
}
|
|
|
|
function showChangeDialog(tab, data) {
|
|
var path, merge;
|
|
|
|
if (!tab) {
|
|
for (path in changedPaths) {
|
|
tab = changedPaths[path].tab;
|
|
data = changedPaths[path].data;
|
|
break;
|
|
}
|
|
if (!tab) return;
|
|
}
|
|
|
|
path = tab.path;
|
|
merge = tab.document.meta.$merge
|
|
&& typeof tab.document.meta.$savedValue === "string";
|
|
|
|
function no(all) { // Local | No
|
|
if (all) {
|
|
for (var id in changedPaths) {
|
|
changedPaths[id].tab.document.undoManager.bookmark(-2);
|
|
changedPaths[id].resolve();
|
|
}
|
|
}
|
|
else {
|
|
changedPaths[path].tab.document.undoManager.bookmark(-2);
|
|
changedPaths[path].resolve();
|
|
}
|
|
checkEmptyQueue();
|
|
}
|
|
|
|
function yes(all) { // Remote | Yes
|
|
if (all) {
|
|
for (var id in changedPaths) {
|
|
getLatestValue(id, updateChangedPath);
|
|
}
|
|
}
|
|
else {
|
|
getLatestValue(path, function(err, path, data) {
|
|
updateChangedPath(err, path, data);
|
|
});
|
|
}
|
|
|
|
checkEmptyQueue();
|
|
}
|
|
|
|
if (merge) {
|
|
changeDialog = filechange.show(
|
|
"File Changed",
|
|
path + " has been changed on disk.",
|
|
null,
|
|
no,
|
|
yes,
|
|
function(all) { // Merge
|
|
if (all) {
|
|
askAutoMerge();
|
|
|
|
for (var id in changedPaths) {
|
|
getLatestValue(id, mergeChangedPath);
|
|
}
|
|
}
|
|
else {
|
|
askAutoMerge();
|
|
|
|
getLatestValue(path, function(err, path, data) {
|
|
mergeChangedPath(err, path, data);
|
|
});
|
|
}
|
|
|
|
checkEmptyQueue();
|
|
},
|
|
{
|
|
merge: true,
|
|
all: Object.keys(changedPaths).length > 1
|
|
}
|
|
);
|
|
}
|
|
else {
|
|
changeDialog = question.show(
|
|
"File Changed",
|
|
path + " has been changed on disk.",
|
|
"Would you like to reload this file?",
|
|
yes, no, {
|
|
all: Object.keys(changedPaths).length > 1
|
|
}
|
|
);
|
|
}
|
|
|
|
if (!initialFocus)
|
|
initialFocus = tabManager.focussedTab;
|
|
|
|
// Focus the tab that is changed
|
|
tabManager.focusTab(tab);
|
|
}
|
|
|
|
function addDeletedTab(tab, force) {
|
|
if (!force && removedPaths[tab.path])
|
|
return;
|
|
|
|
// If the terminal is currently focussed, lets wait until
|
|
// another tab is focussed
|
|
if (tabManager.focussedTab
|
|
&& tabManager.focussedTab.editorType == "terminal") {
|
|
tabManager.once("focus", function(){
|
|
addDeletedTab(tab, true);
|
|
});
|
|
return;
|
|
}
|
|
|
|
fs.stat(tab.path, function(err, data) {
|
|
if (err && err.code === "ENOENT") {
|
|
removedPaths[tab.path] = tab;
|
|
|
|
if (deleteDialog) {
|
|
// The dialog is visible
|
|
if (deleteDialog.visible === 1) {
|
|
question.all = true;
|
|
return;
|
|
}
|
|
// The dialog still is to become visible
|
|
else if (deleteDialog.visible === undefined) {
|
|
deleteDialog.on("show", function(){
|
|
question.all = true;
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (!tabManager.findTab(tab.path)) // drat! tab is gone
|
|
return;
|
|
|
|
// Show dialog
|
|
showDeleteDialog(tab);
|
|
}
|
|
else {
|
|
watcher.watch(tab.path); // Restore file watcher
|
|
}
|
|
});
|
|
}
|
|
|
|
function showDeleteDialog(tab) {
|
|
var path;
|
|
if (!tab) {
|
|
for (path in removedPaths) {
|
|
tab = removedPaths[path];
|
|
break;
|
|
}
|
|
if (!tab) return;
|
|
}
|
|
|
|
path = tab.path;
|
|
|
|
deleteDialog = question.show(
|
|
"File removed, keep tab open?",
|
|
path + " has been deleted, or is no longer available.",
|
|
"Do you wish to keep the file open in the editor?",
|
|
function(all) { // Yes
|
|
var doc;
|
|
|
|
if (all) {
|
|
for (var id in removedPaths) {
|
|
doc = removedPaths[id].document;
|
|
doc.undoManager.bookmark(-2);
|
|
doc.meta.newfile = true;
|
|
}
|
|
removedPaths = {};
|
|
}
|
|
else {
|
|
doc = removedPaths[path].document;
|
|
doc.undoManager.bookmark(-2);
|
|
doc.meta.newfile = true;
|
|
delete removedPaths[path];
|
|
showDeleteDialog();
|
|
}
|
|
|
|
checkEmptyQueue();
|
|
},
|
|
function(all, cancel) { // No
|
|
if (all) {
|
|
for (var id in removedPaths) {
|
|
closeTab(removedPaths[id], true);
|
|
}
|
|
removedPaths = {};
|
|
}
|
|
else {
|
|
closeTab(removedPaths[path]);
|
|
delete removedPaths[path];
|
|
showDeleteDialog();
|
|
}
|
|
|
|
checkEmptyQueue();
|
|
},
|
|
{ all: Object.keys(removedPaths).length > 1 }
|
|
);
|
|
|
|
deleteDialog.on("show", function(){
|
|
if (!tabManager.findTab(path))
|
|
return false;
|
|
});
|
|
|
|
if (!initialFocus)
|
|
initialFocus = tabManager.focussedTab;
|
|
|
|
// Focus the tab that is to be deleted
|
|
tabManager.focusTab(tab);
|
|
}
|
|
|
|
function checkEmptyQueue(){
|
|
for (var prop in changedPaths) return;
|
|
for (var prop in removedPaths) return;
|
|
|
|
if (initialFocus) {
|
|
tabManager.focusTab(initialFocus);
|
|
initialFocus = null;
|
|
}
|
|
}
|
|
|
|
function closeTab(tab, noAnim) {
|
|
// Close file without a check
|
|
tab.document.meta.$ignoreSave = true;
|
|
tab.close(noAnim);
|
|
|
|
// Remove the flag for the case that the doc is restored
|
|
delete tab.document.meta.$ignoreSave;
|
|
}
|
|
|
|
function askAutoMerge() {
|
|
if (!settings.getBool("user/general/@confirmmerge"))
|
|
return;
|
|
|
|
question.show(
|
|
"Always merge?",
|
|
"Always merge on file changes?",
|
|
"Enabling 'auto merge' makes it very easy to collaborate on "
|
|
+ "files with other people, especially when combined with "
|
|
+ "'auto save'. This setting can be controlled from the "
|
|
+ "settings panel as well.",
|
|
function() { // on yes
|
|
if (question.dontAsk)
|
|
settings.set("user/general/@confirmmerge", "false");
|
|
settings.set("user/general/@automerge", "true");
|
|
},
|
|
function() { // on no
|
|
if (question.dontAsk)
|
|
settings.set("user/general/@confirmmerge", "false");
|
|
settings.set("user/general/@automerge", "false");
|
|
},
|
|
{ showDontAsk: true }
|
|
);
|
|
}
|
|
|
|
/***** Lifecycle *****/
|
|
|
|
plugin.on("load", function(){
|
|
load();
|
|
});
|
|
plugin.on("enable", function(){
|
|
|
|
});
|
|
plugin.on("disable", function(){
|
|
|
|
});
|
|
plugin.on("unload", function(){
|
|
loaded = false;
|
|
});
|
|
|
|
/***** Register and define API *****/
|
|
|
|
/**
|
|
*
|
|
*/
|
|
plugin.freezePublicAPI({
|
|
|
|
});
|
|
|
|
register(null, {
|
|
"watcher.gui": plugin
|
|
});
|
|
}
|
|
}); |