Merge pull request +15894 from c9/autosave

Autosave on blur
pull/483/merge
Harutyun Amirjanyan 2018-03-13 20:28:48 +04:00 zatwierdzone przez GitHub
commit 59eb6c51d6
7 zmienionych plików z 276 dodań i 180 usunięć

Wyświetl plik

@ -630,7 +630,7 @@ define(function(require, module, exports) {
if (!loaded || tab.document.meta.preview)
return;
var lastTab = focussedTab;
var lastTab = e.lastTab || focussedTab;
if (!focussedTab || focussedTab.pane == tab.pane && focussedTab != tab)
focusTab(tab, true, true);

Wyświetl plik

@ -1,154 +1,201 @@
define(function(require, exports, module) {
main.consumes = [
"Plugin", "c9", "ui", "layout", "tooltip",
"anims", "menus", "tabManager", "save",
"preferences.experimental"
"Plugin", "c9", "settings", "tabManager", "preferences.experimental", "save", "apf"
];
main.provides = ["autosave"];
return main;
function main(options, imports, register) {
var c9 = imports.c9;
var Plugin = imports.Plugin;
var apf = imports.apf;
var save = imports.save;
var tooltip = imports.tooltip;
var tabs = imports.tabManager;
var experimental = imports["preferences.experimental"];
var prefs = imports["preferences.experimental"];
var Plugin = imports.Plugin;
var settings = imports.settings;
var lang = require("ace/lib/lang");
/***** Initialization *****/
var plugin = new Plugin("Ajax.org", main.consumes);
var INTERVAL = 60000;
var CHANGE_TIMEOUT = 500;
var CHANGE_TIMEOUT = options.changeTimeout || 1000;
var SLOW_CHANGE_TIMEOUT = options.slowChangeTimeout || 30000;
var SLOW_SAVE_THRESHOLD = 100 * 1024; // 100KB
var docChangeTimeout = null;
var btnSave, autosave = true, saveInterval;
var enabled = options.testing
|| experimental.addExperiment("autosave", false, "Files/Auto-Save");
var docChangeTimeout;
var lastSaveTime = 0;
var sessionId;
var autosave;
var saveWhenIdle;
var loaded = false;
function load() {
if (loaded || !enabled) return false;
loaded = true;
// when we're back online we'll trigger an autosave if enabled
c9.on("stateChange", function(e) {
if (e.state & c9.STORAGE && !(e.last & c9.STORAGE))
check();
}, plugin);
save.getElement("btnSave", function(btn) {
btnSave = btn;
transformButton();
});
tabs.on("tabCreate", function(e) {
var tab = e.tab;
tab.document.undoManager.on("change", function(e) {
if (!autosave || !tab.path)
return;
clearTimeout(docChangeTimeout);
docChangeTimeout = setTimeout(function() {
saveTab(tab);
}, tab.document.meta.$slowSave
? SLOW_CHANGE_TIMEOUT
: CHANGE_TIMEOUT);
}, plugin);
}, plugin);
tabs.on("tabDestroy", function(e) {
if (!e.tab.path)
return;
if (tabs.getTabs().length == 1)
btnSave.hide();
saveTab(e.tab);
}, plugin);
save.on("beforeWarn", function(e) {
if (autosave && !e.tab.document.meta.newfile) {
saveTab(e.tab);
return false;
prefs.add({
"File": {
position: 150,
"Save": {
position: 100,
"Auto-Save Files": {
type: "dropdown",
position: 100,
path: "user/general/@autosave",
width: 130,
items: [
{ caption: "Off", value: false },
{ caption: "On Focus Change", value: "onFocusChange" },
{ caption: "After Delay", value: "afterDelay" },
],
}
}
}
}, plugin);
}
function transformButton() {
if (!btnSave) return;
if (btnSave.autosave === autosave) return;
if (autosave) {
// Transform btnSave
btnSave.setAttribute("caption", "");
btnSave.setAttribute("margin", "0 20");
btnSave.removeAttribute("tooltip");
btnSave.removeAttribute("command");
apf.setStyleClass(btnSave.$ext, "btnSave");
tooltip.add(btnSave, {
message: "Changes to your file are automatically saved.<br />\
View all your changes through <a href='javascript:void(0)' \
onclick='require(\"ext/revisions/revisions\").toggle();' \
class='revisionsInfoLink'>the Revision History pane</a>. \
Rollback to a previous state, or make comparisons.",
width: "250px",
hideonclick: true
}, plugin);
}
else {
}
btnSave.autosave = autosave;
settings.setDefaults("user/general", [["autosave", false]]);
settings.on("read", onSettingChange, plugin);
settings.on("user/general", onSettingChange, plugin);
save.on("beforeWarn", function(e) {
if (autosave && saveTab(e.tab))
return false;
}, plugin);
}
/***** Helpers *****/
function check() {
if (!autosave) return;
function onSettingChange() {
autosave = settings.get("user/general/@autosave");
if (autosave == "off" || autosave == "false")
autosave = false;
var pages = tabs.getTabs();
for (var tab, i = 0, l = pages.length; i < l; i++) {
if ((tab = pages[i]).document.changed && tab.path)
disable();
if (autosave == "afterDelay")
enableDelay();
if (autosave)
enable();
}
function enableDelay() {
saveWhenIdle = lang.delayedCall(function() {
var tab = tabs.focussedTab;
var ace = tab && tab.editor && tab.editor.ace;
if (ace && ace.session && sessionId == ace.session.id) {
saveTab(tab);
}
});
}
function enable() {
apf.on("movefocus", scheduleCheck);
tabs.on("tabAfterActivate", scheduleCheck, plugin);
if (saveWhenIdle)
tabs.on("focusSync", attachToTab, plugin);
window.addEventListener("blur", scheduleCheck);
}
function disable() {
sessionId = null;
if (saveWhenIdle) {
saveWhenIdle.cancel();
saveWhenIdle = null;
}
if (docChangeTimeout) {
clearTimeout(docChangeTimeout);
docChangeTimeout = null;
}
apf.off("movefocus", scheduleCheck);
tabs.off("tabAfterActivate", scheduleCheck);
tabs.off("focusSync", attachToTab);
window.removeEventListener("blur", scheduleCheck);
}
function attachToTab(e) {
var ace = e.tab && e.tab.editor && e.tab.editor.ace;
if (ace)
ace.on("beforeEndOperation", beforeEndOperation);
}
function beforeEndOperation(e, ace) {
if (!saveWhenIdle)
return ace.off("beforeEndOperation", beforeEndOperation);
if (!ace.isFocused() && !options.ignoreFocusForTesting)
return;
sessionId = ace.session.id;
if (sessionId && ace.curOp.docChanged && ace.curOp.command.name) {
var timeout = Math.min(Math.max(CHANGE_TIMEOUT, lastSaveTime || 0), SLOW_CHANGE_TIMEOUT);
saveWhenIdle.delay(timeout);
}
}
function scheduleCheck(e) {
if (docChangeTimeout)
return;
var tab;
var fromElement = e.fromElement;
var toElement = e.toElement;
if (e.type == "blur") {
tab = tabs.focussedTab;
}
else if (fromElement) {
var fakePage = fromElement.$fake;
if (toElement && (toElement == fakePage || fromElement == toElement.$fake)) {
fakePage = fromElement.$prevFake || toElement.$prevFake;
if (fakePage)
return;
}
tab = fromElement.cloud9tab || fakePage && fakePage.cloud9tab;
if (!tab || !tab.path)
return;
while (toElement) {
if (/window|menu|item/.test(toElement.localName))
return;
toElement = toElement.parentNode;
}
}
else if (e.lastTab) {
tab = e.lastTab;
}
if (!tab || !tab.path)
return;
docChangeTimeout = setTimeout(function() {
docChangeTimeout = null;
var activeElement = apf.document.activeElement;
var nodeName = activeElement && activeElement.localName;
// do nothing if the tab is still focused, or is a clone of the focussed tab
if (nodeName === "page" && tabs.focussedTab && tabs.focussedTab.path === tab.path)
return;
saveTab(tab);
});
}
function saveTab(tab, force) {
if (!autosave) return;
if (!c9.has(c9.STORAGE)) {
save.setSavingState(tab, "offline");
return;
}
var doc;
if (!force && (!tab.path
|| !(doc = tab.document).changed
|| doc.meta.newfile
|| doc.meta.nofs
|| doc.meta.error
|| doc.meta.$saving))
|| doc.meta.$saving
|| doc.meta.preview
|| !doc.hasValue()))
return;
var value = doc.value;
var slow = value.length > SLOW_SAVE_THRESHOLD;
if (slow && !doc.meta.$slowSave) {
doc.meta.$slowSave = true;
if (!c9.has(c9.STORAGE)) {
save.setSavingState(tab, "offline");
return;
}
doc.meta.$slowSave = slow;
var t = Date.now();
save.save(tab, {
silentsave: true,
timeout: 1,
value: value
}, function() {});
noUi: true,
}, function() {
lastSaveTime = t - Date.now();
});
return true;
}
/***** Lifecycle *****/
@ -156,27 +203,16 @@ define(function(require, exports, module) {
plugin.on("load", function() {
load();
});
plugin.on("enable", function() {
autosave = true;
transformButton();
});
plugin.on("disable", function() {
autosave = false;
transformButton();
});
plugin.on("unload", function() {
if (saveInterval)
clearInterval(saveInterval);
loaded = false;
disable();
autosave = false;
});
/***** Register and define API *****/
/**
* Implements auto save for Cloud9. When the user enables autosave
* the contents of files are automatically saved about 500ms after the
* change is made.
* the contents of files are automatically saved when the editor is blurred
* @singleton
**/
plugin.freezePublicAPI({ });

Wyświetl plik

@ -2,8 +2,7 @@
"use client";
require(["lib/architect/architect", "lib/chai/chai", "/vfs-root"],
function (architect, chai, baseProc) {
require(["lib/chai/chai"], function (chai) {
var expect = chai.expect;
document.body.appendChild(document.createElement("div"))
@ -16,7 +15,6 @@ require(["lib/architect/architect", "lib/chai/chai", "/vfs-root"],
debug: true,
hosted: true,
local: false,
davPrefix: "/"
},
"plugins/c9.core/ext",
@ -37,7 +35,7 @@ require(["lib/architect/architect", "lib/chai/chai", "/vfs-root"],
"plugins/c9.ide.editors/undomanager",
{
packagePath: "plugins/c9.ide.editors/editors",
defaultEditor: "texteditor"
defaultEditor: "ace"
},
"plugins/c9.ide.editors/editor",
"plugins/c9.ide.editors/tabmanager",
@ -48,17 +46,17 @@ require(["lib/architect/architect", "lib/chai/chai", "/vfs-root"],
"plugins/c9.ide.save/save",
{
packagePath: "plugins/c9.ide.save/autosave",
testing: true
ignoreFocusForTesting: true,
changeTimeout: 5,
},
{
packagePath: "plugins/c9.vfs.client/vfs_client"
packagePath: "plugins/c9.vfs.client/vfs_client_mock",
storage: false
},
"plugins/c9.vfs.client/endpoint",
"plugins/c9.ide.auth/auth",
"plugins/c9.core/api",
{
packagePath: "plugins/c9.fs/fs",
baseProc: baseProc
cli: true
},
"plugins/c9.fs/fs.cache.xml",
@ -67,27 +65,15 @@ require(["lib/architect/architect", "lib/chai/chai", "/vfs-root"],
provides: [],
setup: main
}
], architect);
]);
function main(options, imports, register) {
var settings = imports.settings
var tabs = imports.tabManager;
var fs = imports.fs;
var save = imports.save;
var autosave = imports.autosave;
function countEvents(count, expected, done) {
if (count == expected)
done();
else
throw new Error("Wrong Event Count: "
+ count + " of " + expected);
}
expect.html.setConstructor(function(tab) {
if (typeof tab == "object")
return tab.pane.aml.getPage("editor::" + tab.editorType).$ext;
});
function changeTab(path, done) {
var tab = tabs.findTab(path);
tabs.focusTab(tab);
@ -98,41 +84,114 @@ require(["lib/architect/architect", "lib/chai/chai", "/vfs-root"],
return tab;
}
function createAndChangeTab(path, options, done) {
if (!done) {
done = options;
options = {};
}
fs.writeFile(path, path, function(err) {
if (err) throw err;
options.path = path;
tabs.open(options, function() {
changeTab(path, done);
});
});
}
describe('autosave', function() {
this.timeout(5000);
before(function(done) {
beforeEach(function(done) {
tabs.once("ready", function() {
tabs.getPanes()[0].focus();
var path = "/autosave1.txt";
fs.writeFile(path, path, function(err) {
if (err) throw err;
tabs.openFile(path, function() {
setTimeout(done, 50);
});
tabs.setState(null, function() {
tabs.getPanes()[0].focus();
done();
});
});
bar.$ext.style.background = "rgba(220, 220, 220, 0.93)";
bar.$ext.style.position = "fixed";
bar.$ext.style.left = "20px";
bar.$ext.style.right = "20px";
bar.$ext.style.bottom = "20px";
bar.$ext.style.height = "150px";
document.body.style.marginBottom = "180px";
});
it('should automatically save a tab that is changed', function(done) {
var path = "/autosave1.txt";
changeTab(path, function(tab) {
it("should not autosave when restoring state", function(done) {
settings.set("user/general/@autosave", false);
var pane = tabs.getPanes()[0].hsplit(true);
prepareTabs(testRestoreState);
function prepareTabs(callback) {
createAndChangeTab("/autosave1.txt", function(tab) {
expect(tab.document.changed).to.ok;
createAndChangeTab("/__proto__", function() {
createAndChangeTab("/<h1>", function() {
createAndChangeTab("/__lookupSetter__", { pane: pane }, function() {
callback();
});
});
});
});
}
function testRestoreState() {
settings.set("user/general/@autosave", true);
expect(tabs.getTabs().length).to.equal(4);
var state = tabs.getState(null, true);
tabs.setState(null, function() {
expect(tabs.getTabs().length).to.equal(0);
setTimeout(function() {
tabs.setState(state, function() {
expect(tabs.getTabs().length).to.equal(4);
expect(tabs.getTabs()[0].document.changed).to.ok;
setTimeout(function() {
tabs.setState(null, function() {
save.off("afterSave", preventSave);
done();
});
});
});
});
});
}
function preventSave() {
done(new Error("Save is called"));
}
save.once("afterSave", preventSave);
});
it("should automatically save a tab that is changed when editor is blurred", function(done) {
settings.set("user/general/@autosave", true);
var path = "/autosave2.txt";
createAndChangeTab(path, function(tab) {
expect(tab.document.changed).to.ok;
createAndChangeTab("/__proto__", function() {
});
save.once("afterSave", function() {
fs.readFile(path, function(err, data) {
if (err) throw err;
expect(data).to.equal("test" + path);
expect(tab.document.changed).to.not.ok;
fs.unlink(path, function() {
done();
});
});
});
});
});
it("should automatically save after delay", function(done) {
settings.set("user/general/@autosave", "afterDelay");
var path = "/autosave2.txt";
createAndChangeTab(path, function(tab) {
expect(tab.document.changed).to.ok;
setTimeout(function() {
expect(tab.document.changed).to.ok;
tab.editor.ace.execCommand("insertstring", "x");
}, 10);
save.once("afterSave", function() {
fs.readFile(path, function(err, data) {
if (err) throw err;
expect(data).to.equal("testx" + path);
expect(tab.document.changed).to.not.ok;
fs.unlink(path, function() {
done();

Wyświetl plik

@ -412,7 +412,7 @@ define(function(require, exports, module) {
doc.meta.$saveBuffer = true;
}
setSavingState(tab, "saving");
setSavingState(tab, "saving", null, options.noUi);
var bookmark = doc.undoManager.position;
var loadStartT = Date.now();
@ -450,7 +450,7 @@ define(function(require, exports, module) {
if (options.path)
tab.path = options.path;
setSavingState(tab, "saved", options.timeout);
setSavingState(tab, "saved", options.timeout, options.noUi);
settings.save();
logger.log("Successfully saved " + path);
}
@ -541,7 +541,7 @@ define(function(require, exports, module) {
}
var stateTimer = null, pageTimers = {};
function setSavingState(tab, state, timeout) {
function setSavingState(tab, state, timeout, silent) {
clearTimeout(stateTimer);
clearTimeout(pageTimers[tab.name]);
@ -554,6 +554,12 @@ define(function(require, exports, module) {
else
delete doc.meta.$saving;
if (!silent)
updateSavingUi();
emit("tabSavingState", { tab: tab });
}
function updateSavingUi(tab, state, timeout) {
if (state == "saving") {
btnSave.show();
@ -621,7 +627,6 @@ define(function(require, exports, module) {
btnSave.setCaption("Not saved");
tab.classList.add("error");
}
emit("tabSavingState", { tab: tab });
}
/***** Lifecycle *****/

Wyświetl plik

@ -393,6 +393,7 @@ apf.page = function(struct, tagName) {
if (this.relPage) {
this.relPage.$ext.style.display = "";
this.parentNode.$setStyleClass(this.relPage.$ext, "curpage");
this.relPage.$prevFake = this.relPage.$fake;
this.relPage.$fake = this;
@ -604,6 +605,7 @@ apf.page = function(struct, tagName) {
if (page && page.type == _self.id) {
page.relPage = _self;
if (page.$active) {
_self.$prevFake = _self.$fake;
_self.$fake = page;
page.$activate();
}

Wyświetl plik

@ -10964,16 +10964,9 @@ apf.window = function(){
(apf.window.activeElement = amlNode).focus(true, e);
this.$settingFocus = null;
apf.dispatchEvent("movefocus", {
toElement: amlNode
});
apf.dispatchEvent("movefocus", e);
};
this.$blur = function(amlNode) {

Wyświetl plik

@ -349,7 +349,8 @@ define(function(require, exports, module) {
"preferences.experimental": {
addExperiment: function() {
return false;
}
},
add: function() {},
},
"ace.gotoline": {},
"ace.stripws": {