Merge pull request #1489 from ajaxorg/undo_merge

Undo merging
This commit is contained in:
Lennart Kats 2013-07-12 00:04:40 -07:00
commit aded11265f
7 changed files with 288 additions and 66 deletions

View file

@ -118,13 +118,15 @@ exports.commands = [{
bindKey: bindKey("Ctrl-Shift-Home", "Command-Shift-Up"), bindKey: bindKey("Ctrl-Shift-Home", "Command-Shift-Up"),
exec: function(editor) { editor.getSelection().selectFileStart(); }, exec: function(editor) { editor.getSelection().selectFileStart(); },
multiSelectAction: "forEach", multiSelectAction: "forEach",
readOnly: true readOnly: true,
group: "fileJump"
}, { }, {
name: "gotostart", name: "gotostart",
bindKey: bindKey("Ctrl-Home", "Command-Home|Command-Up"), bindKey: bindKey("Ctrl-Home", "Command-Home|Command-Up"),
exec: function(editor) { editor.navigateFileStart(); }, exec: function(editor) { editor.navigateFileStart(); },
multiSelectAction: "forEach", multiSelectAction: "forEach",
readOnly: true readOnly: true,
group: "fileJump"
}, { }, {
name: "selectup", name: "selectup",
bindKey: bindKey("Shift-Up", "Shift-Up"), bindKey: bindKey("Shift-Up", "Shift-Up"),
@ -142,13 +144,15 @@ exports.commands = [{
bindKey: bindKey("Ctrl-Shift-End", "Command-Shift-Down"), bindKey: bindKey("Ctrl-Shift-End", "Command-Shift-Down"),
exec: function(editor) { editor.getSelection().selectFileEnd(); }, exec: function(editor) { editor.getSelection().selectFileEnd(); },
multiSelectAction: "forEach", multiSelectAction: "forEach",
readOnly: true readOnly: true,
group: "fileJump"
}, { }, {
name: "gotoend", name: "gotoend",
bindKey: bindKey("Ctrl-End", "Command-End|Command-Down"), bindKey: bindKey("Ctrl-End", "Command-End|Command-Down"),
exec: function(editor) { editor.navigateFileEnd(); }, exec: function(editor) { editor.navigateFileEnd(); },
multiSelectAction: "forEach", multiSelectAction: "forEach",
readOnly: true readOnly: true,
group: "fileJump"
}, { }, {
name: "selectdown", name: "selectdown",
bindKey: bindKey("Shift-Down", "Shift-Down"), bindKey: bindKey("Shift-Down", "Shift-Down"),

View file

@ -406,10 +406,11 @@ var EditSession = function(text, mode) {
if (self.$deltas.length > 0) { if (self.$deltas.length > 0) {
undoManager.execute({ undoManager.execute({
action: "aceupdate", action: "aceupdate",
args: [self.$deltas, self] args: [self.$deltas, self],
merge: self.mergeUndoDeltas
}); });
} }
self.mergeUndoDeltas = false;
self.$deltas = []; self.$deltas = [];
} }
this.$informUndoManager = lang.delayedCall(this.$syncInformUndoManager); this.$informUndoManager = lang.delayedCall(this.$syncInformUndoManager);
@ -1220,7 +1221,7 @@ var EditSession = function(text, mode) {
var range, point; var range, point;
var lastDeltaIsInsert = false; var lastDeltaIsInsert = false;
if (isInsert(delta)) { if (isInsert(delta)) {
range = delta.range.clone(); range = Range.fromPoints(delta.range.start, delta.range.end);
lastDeltaIsInsert = true; lastDeltaIsInsert = true;
} else { } else {
range = Range.fromPoints(delta.range.start, delta.range.start); range = Range.fromPoints(delta.range.start, delta.range.start);
@ -1252,6 +1253,11 @@ var EditSession = function(text, mode) {
// Check if this range and the last undo range has something in common. // Check if this range and the last undo range has something in common.
// If true, merge the ranges. // If true, merge the ranges.
if (lastUndoRange != null) { if (lastUndoRange != null) {
if (Range.comparePoints(lastUndoRange.start, range.start) == 0) {
lastUndoRange.start.column += range.end.column - range.start.column;
lastUndoRange.end.column += range.end.column - range.start.column;
}
var cmp = lastUndoRange.compareRange(range); var cmp = lastUndoRange.compareRange(range);
if (cmp == 1) { if (cmp == 1) {
range.setStart(lastUndoRange.start); range.setStart(lastUndoRange.start);

View file

@ -50,8 +50,6 @@ var defaultCommands = require("./commands/default_commands").commands;
var config = require("./config"); var config = require("./config");
/** /**
*
*
* The main entry point into the Ace functionality. * The main entry point into the Ace functionality.
* *
* The `Editor` manages the [[EditSession]] (which manages [[Document]]s), as well as the [[VirtualRenderer]], which draws everything to the screen. * The `Editor` manages the [[EditSession]] (which manages [[Document]]s), as well as the [[VirtualRenderer]], which draws everything to the screen.
@ -88,6 +86,11 @@ var Editor = function(renderer, session) {
wrap: true wrap: true
}); });
this.$historyTracker = this.$historyTracker.bind(this);
this.commands.on("exec", this.$historyTracker);
this.$initOperationListeners();
this.setSession(session || new EditSession("")); this.setSession(session || new EditSession(""));
config.resetOptions(this); config.resetOptions(this);
config._emit("editor", this); config._emit("editor", this);
@ -97,6 +100,115 @@ var Editor = function(renderer, session) {
oop.implement(this, EventEmitter); oop.implement(this, EventEmitter);
this.$initOperationListeners = function() {
function last(a) {return a[a.length - 1]};
this.selections = [];
this.commands.on("exec", function(e) {
this.startOperation(e);
var command = e.command;
if (command.group == "fileJump") {
var prev = this.prevOp;
if (!prev || prev.command.group != "fileJump") {
this.lastFileJumpPos = last(this.selections)
}
} else {
this.lastFileJumpPos = null;
}
}.bind(this), true);
this.commands.on("afterExec", function(e) {
var command = e.command;
if (command.group == "fileJump") {
if (this.lastFileJumpPos && !this.curOp.selectionChanged) {
this.selection.fromJSON(this.lastFileJumpPos);
return
}
}
this.endOperation(e);
}.bind(this), true);
this.$opResetTimer = lang.delayedCall(this.endOperation.bind(this));
this.on("change", function() {
this.curOp || this.startOperation();
this.curOp.docChanged = true;
}.bind(this), true);
this.on("changeSelection", function() {
this.curOp || this.startOperation();
this.curOp.selectionChanged = true;
}.bind(this), true);
}
this.curOp = null;
this.prevOp = {};
this.startOperation = function(commadEvent) {
if (this.curOp) {
if (!commadEvent || this.curOp.command)
return;
this.prevOp = this.curOp;
}
if (!commadEvent) {
this.previousCommand = null;
commadEvent = {};
}
this.$opResetTimer.schedule();
this.curOp = {
command: commadEvent.command || {},
args: commadEvent.args
};
this.selections.push(this.selection.toJSON());
};
this.endOperation = function() {
if (this.curOp) {
this.prevOp = this.curOp;
this.curOp = null;
}
};
this.$historyTracker = function(e) {
if (!this.$mergeUndoDeltas)
return;
var prev = this.prevOp;
var mergeableCommands = ["backspace", "del", "insertstring"];
// previous command was the same
var shouldMerge = prev.command && (e.command.name == prev.command.name);
if (e.command.name == "insertstring") {
var text = e.args;
if (this.mergeNextCommand === undefined)
this.mergeNextCommand = true;
shouldMerge = shouldMerge
&& this.mergeNextCommand // previous command allows to coalesce with
&& (!/\s/.test(text) || /\s/.test(prev.args)) // previous insertion was of same type
this.mergeNextCommand = true;
} else {
shouldMerge = shouldMerge
&& mergeableCommands.indexOf(e.command.name) !== -1// the command is mergeable
}
if (
this.$mergeUndoDeltas != "always"
&& Date.now() - this.sequenceStartTime > 2000
) {
shouldMerge = false; // the sequence is too long
}
if (shouldMerge)
this.session.mergeUndoDeltas = true;
else if (mergeableCommands.indexOf(e.command.name) !== -1)
this.sequenceStartTime = Date.now();
};
/** /**
* Sets a new key handler, such as "vim" or "windows". * Sets a new key handler, such as "vim" or "windows".
* @param {String} keyboardHandler The new key handler * @param {String} keyboardHandler The new key handler
@ -701,8 +813,14 @@ var Editor = function(renderer, session) {
if (this.getBehavioursEnabled()) { if (this.getBehavioursEnabled()) {
// Get a transform if the current mode wants one. // Get a transform if the current mode wants one.
var transform = mode.transformAction(session.getState(cursor.row), 'insertion', this, session, text); var transform = mode.transformAction(session.getState(cursor.row), 'insertion', this, session, text);
if (transform) if (transform) {
if (text !== transform.text) {
this.session.mergeUndoDeltas = false;
this.$mergeNextCommand = false;
}
text = transform.text; text = transform.text;
}
} }
if (text == "\t") if (text == "\t")
@ -2242,6 +2360,10 @@ config.defineOptions(Editor.prototype, "editor", {
values: ["ace", "slim", "smooth", "wide"], values: ["ace", "slim", "smooth", "wide"],
initialValue: "ace" initialValue: "ace"
}, },
mergeUndoDeltas: {
values: [false, true, "always"],
initialValue: true
},
behavioursEnabled: {initialValue: true}, behavioursEnabled: {initialValue: true},
wrapBehavioursEnabled: {initialValue: true}, wrapBehavioursEnabled: {initialValue: true},

View file

@ -171,6 +171,30 @@ module.exports = {
selection.addRange(range2); selection.addRange(range2);
editor.execCommand('insertfoo'); editor.execCommand('insertfoo');
assert.equal('l1foo\nl2foo', editor.getValue()); assert.equal('l1foo\nl2foo', editor.getValue());
},
"test multiselect fromJSON/toJSON": function() {
var doc = new EditSession(["l1", "l2"]);
editor = new Editor(new MockRenderer(), doc);
MultiSelect(editor);
var selection = editor.selection;
var before = selection.toJSON();
var range1 = new Range(0,2,0,2);
var range2 = new Range(1,2,1,2);
selection.fromOrientedRange(range1)
selection.addRange(range2);
var after = selection.toJSON();
selection.fromJSON(before);
assert.ok(!selection.isEqual(after));
assert.ok(selection.isEqual(before));
selection.fromJSON(after);
assert.ok(!selection.isEqual(before));
assert.ok(selection.isEqual(after));
} }
}; };

View file

@ -258,6 +258,8 @@ var Selection = function(session) {
this.setSelectionAnchor(range.start.row, range.start.column); this.setSelectionAnchor(range.start.row, range.start.column);
this.selectTo(range.end.row, range.end.column); this.selectTo(range.end.row, range.end.column);
} }
if (this.getRange().isEmpty())
this.$isEmpty = true;
this.$desiredColumn = null; this.$desiredColumn = null;
}; };
@ -861,6 +863,52 @@ var Selection = function(session) {
return range; return range;
} }
this.toJSON = function() {
if (this.rangeCount) {
var data = this.ranges.map(function(r) {
var r1 = r.clone();
r1.isBackwards = r.cursor == r.start;
return r1;
});
} else {
var data = this.getRange();
data.isBackwards = this.isBackwards();
}
return data;
};
this.fromJSON = function(data) {
if (data.start == undefined) {
if (this.rangeList) {
this.toSingleRange(data[0]);
for (var i = data.length; i--; ) {
var r = Range.fromPoints(data[i].start, data[i].end);
if (data.isBackwards)
r.cursor = r.start;
this.addRange(r, true);
}
return;
} else
data = data[0];
}
if (this.rangeList)
this.toSingleRange(data);
this.setSelectionRange(data, data.isBackwards);
};
this.isEqual = function(data) {
if ((data.length || this.rangeCount) && data.length != this.rangeCount)
return false;
if (!data.length || !this.ranges)
return this.getRange().isEqual(data);
for (var i = this.ranges.length; i--; ) {
if (!this.ranges[i].isEqual(data[i]))
return false
}
return true;
}
}).call(Selection.prototype); }).call(Selection.prototype);
exports.Selection = Selection; exports.Selection = Selection;

View file

@ -434,9 +434,7 @@ module.exports = {
}, },
"test (keyboard navigation) when curLine is not EOL and targetLine is all whitespace new column should be current column": function() { "test (keyboard navigation) when curLine is not EOL and targetLine is all whitespace new column should be current column": function() {
var session = new EditSession("function (a) {\n\ var session = new EditSession("function (a) {\n \n}");
\n\
}");
var selection = session.getSelection(); var selection = session.getSelection();
selection.moveCursorTo(2, 0); selection.moveCursorTo(2, 0);
@ -445,16 +443,33 @@ module.exports = {
assert.position(selection.getCursor(), 1, 0); assert.position(selection.getCursor(), 1, 0);
}, },
"test (keyboard navigation) when curLine is EOL and targetLine is shorter dan current column, new column should be targetLine's EOL": function() { "test (keyboard navigation) when curLine is EOL and targetLine is shorter than current column, new column should be targetLine's EOL": function() {
var session = new EditSession("function (a) {\n\ var session = new EditSession("function (a) {\n \n}");
\n\
}");
var selection = session.getSelection(); var selection = session.getSelection();
selection.moveCursorTo(0, 14); selection.moveCursorTo(0, 14);
selection.moveCursorDown(); selection.moveCursorDown();
assert.position(selection.getCursor(), 1, 4); assert.position(selection.getCursor(), 1, 4);
},
"test fromJSON/toJSON": function() {
var session = new EditSession("function (a) {\n \n}");
var selection = session.getSelection();
selection.moveCursorTo(0, 14);
selection.moveCursorDown();
assert.position(selection.getCursor(), 1, 4);
var data = selection.toJSON();
data = JSON.parse(JSON.stringify(data))
selection.moveCursorDown();
assert.position(selection.getCursor(), 2, 1);
assert.ok(!selection.isEqual(data));
selection.fromJSON(data);
assert.position(selection.getCursor(), 1, 4);
assert.ok(selection.isEqual(data));
} }
}; };

View file

@ -63,6 +63,9 @@ var UndoManager = function() {
this.execute = function(options) { this.execute = function(options) {
var deltas = options.args[0]; var deltas = options.args[0];
this.$doc = options.args[1]; this.$doc = options.args[1];
if (options.merge && this.hasUndo()){
deltas = this.$undoStack.pop().concat(deltas);
}
this.$undoStack.push(deltas); this.$undoStack.push(deltas);
this.$redoStack = []; this.$redoStack = [];