From 48b4008cab7da4ec3f572705f5e1bc355cd4f392 Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Wed, 4 Apr 2012 14:21:03 -0600 Subject: [PATCH] Changes made with multiple cursors are undone/redone in parallel --- spec/app/buffer-spec.coffee | 2 +- spec/app/editor-spec.coffee | 21 +++++++++ spec/app/root-view-spec.coffee | 2 +- spec/app/undo-manager-spec.coffee | 69 ++++++++++++++++++++++++++++++ src/app/buffer.coffee | 6 +++ src/app/composite-selection.coffee | 19 +++++--- src/app/undo-manager.coffee | 30 ++++++++++--- 7 files changed, 135 insertions(+), 14 deletions(-) diff --git a/spec/app/buffer-spec.coffee b/spec/app/buffer-spec.coffee index b3cfda047..04d974e14 100644 --- a/spec/app/buffer-spec.coffee +++ b/spec/app/buffer-spec.coffee @@ -404,7 +404,7 @@ describe 'Buffer', -> expect(buffer.positionForCharacterIndex(61)).toEqual [2, 0] expect(buffer.positionForCharacterIndex(408)).toEqual [12, 2] - + describe "undo methods", -> describe "path-change event", -> it "emits path-change event when path is changed", -> eventHandler = jasmine.createSpy('eventHandler') diff --git a/spec/app/editor-spec.coffee b/spec/app/editor-spec.coffee index c3496b671..6b417f8bf 100644 --- a/spec/app/editor-spec.coffee +++ b/spec/app/editor-spec.coffee @@ -1612,6 +1612,27 @@ describe "Editor", -> editor.trigger 'redo' expect(buffer.lineForRow(0)).toContain "foo" + it "batches the undo / redo of changes caused by multiple cursors", -> + editor.setCursorScreenPosition([0, 0]) + editor.addCursorAtScreenPosition([1, 0]) + + editor.insertText("foo") + editor.backspace() + + expect(buffer.lineForRow(0)).toContain "fovar" + expect(buffer.lineForRow(1)).toContain "fo " + + editor.trigger 'undo' + + expect(buffer.lineForRow(0)).toContain "foo" + expect(buffer.lineForRow(1)).toContain "foo" + + editor.trigger 'undo' + + expect(buffer.lineForRow(0)).not.toContain "foo" + expect(buffer.lineForRow(1)).not.toContain "foo" + + describe "when multiple lines are removed from the buffer (regression)", -> it "removes all of them from the dom", -> buffer.change(new Range([6, 24], [12, 0]), '') diff --git a/spec/app/root-view-spec.coffee b/spec/app/root-view-spec.coffee index 5b900eac1..1937e5173 100644 --- a/spec/app/root-view-spec.coffee +++ b/spec/app/root-view-spec.coffee @@ -370,4 +370,4 @@ describe "RootView", -> it "sets title to 'untitled' when buffer's path is null", -> editor = rootView.activeEditor() editor.setBuffer(new Buffer()) - expect(document.title).toBe "untitled" \ No newline at end of file + expect(document.title).toBe "untitled" diff --git a/spec/app/undo-manager-spec.coffee b/spec/app/undo-manager-spec.coffee index a1fb0f772..43064f53f 100644 --- a/spec/app/undo-manager-spec.coffee +++ b/spec/app/undo-manager-spec.coffee @@ -59,3 +59,72 @@ describe "UndoManager", -> undoManager.redo() expect(buffer.getText()).toContain 'qsport' + + describe "startUndoBatch()", -> + it "causes all changes before a call to .endUndoBatch to be undone at the same time", -> + buffer.insert([0, 0], "foo") + undoManager.startUndoBatch() + buffer.insert([1, 2], "111") + buffer.insert([1, 9], "222") + undoManager.endUndoBatch() + + expect(buffer.lineForRow(1)).toBe ' 111var 222sort = function(items) {' + + undoManager.undo() + expect(buffer.lineForRow(1)).toBe ' var sort = function(items) {' + expect(buffer.lineForRow(0)).toContain 'foo' + + undoManager.undo() + + expect(buffer.lineForRow(0)).not.toContain 'foo' + + undoManager.redo() + expect(buffer.lineForRow(0)).toContain 'foo' + + undoManager.redo() + expect(buffer.lineForRow(1)).toBe ' 111var 222sort = function(items) {' + + undoManager.undo() + expect(buffer.lineForRow(1)).toBe ' var sort = function(items) {' + + it "old: causes all changes before a call to .endUndoBatch to be undone at the same time", -> + buffer.insert([0, 0], "foo") + undoManager.startUndoBatch() + buffer.insert([1, 0], "bar") + buffer.insert([2, 0], "bar") + buffer.delete([[3, 4], [3, 8]]) + undoManager.endUndoBatch() + buffer.change([[4, 4], [4, 9]], "slongaz") + + expect(buffer.lineForRow(4)).not.toContain("while") + undoManager.undo() + expect(buffer.lineForRow(4)).toContain("while") + + expect(buffer.lineForRow(1)).toContain("bar") + expect(buffer.lineForRow(2)).toContain("bar") + expect(buffer.lineForRow(3)).not.toContain("var") + + undoManager.undo() + + expect(buffer.lineForRow(1)).not.toContain("bar") + expect(buffer.lineForRow(2)).not.toContain("bar") + expect(buffer.lineForRow(3)).toContain("var") + + undoManager.undo() + + expect(buffer.lineForRow(0)).not.toContain("foo") + + undoManager.redo() + + expect(buffer.lineForRow(0)).toContain("foo") + + undoManager.redo() + + expect(buffer.lineForRow(1)).toContain("bar") + expect(buffer.lineForRow(2)).toContain("bar") + expect(buffer.lineForRow(3)).not.toContain("var") + + undoManager.redo() + + expect(buffer.lineForRow(4)).not.toContain("while") + expect(buffer.lineForRow(4)).toContain("slongaz") diff --git a/src/app/buffer.coffee b/src/app/buffer.coffee index 6fa8e8595..b8e3c0ea2 100644 --- a/src/app/buffer.coffee +++ b/src/app/buffer.coffee @@ -127,6 +127,12 @@ class Buffer @lines[oldRange.start.row..oldRange.end.row] = newTextLines @trigger 'change', { oldRange, newRange, oldText, newText } + startUndoBatch: -> + @undoManager.startUndoBatch() + + endUndoBatch: -> + @undoManager.endUndoBatch() + undo: -> @undoManager.undo() diff --git a/src/app/composite-selection.coffee b/src/app/composite-selection.coffee index 8db1c5684..34b58fe5e 100644 --- a/src/app/composite-selection.coffee +++ b/src/app/composite-selection.coffee @@ -77,20 +77,29 @@ class CompositeSeleciton fn(selection) for selection in @getSelections() @mergeIntersectingSelections(reverse: true) + mutateSelectedText: (fn) -> + selections = @getSelections() + if selections.length > 1 + @editor.buffer.startUndoBatch() + fn(selection) for selection in selections + @editor.buffer.endUndoBatch() + else + fn(selections[0]) + insertText: (text) -> - selection.insertText(text) for selection in @getSelections() + @mutateSelectedText (selection) -> selection.insertText(text) backspace: -> - selection.backspace() for selection in @getSelections() + @mutateSelectedText (selection) -> selection.backspace() backspaceToBeginningOfWord: -> - selection.backspaceToBeginningOfWord() for selection in @getSelections() + @mutateSelectedText (selection) -> selection.backspaceToBeginningOfWord() delete: -> - selection.delete() for selection in @getSelections() + @mutateSelectedText (selection) -> selection.delete() deleteToEndOfWord: -> - selection.deleteToEndOfWord() for selection in @getSelections() + @mutateSelectedText (selection) -> selection.deleteToEndOfWord() selectToScreenPosition: (position) -> @getLastSelection().selectToScreenPosition(position) diff --git a/src/app/undo-manager.coffee b/src/app/undo-manager.coffee index 5222dfc00..0abf54d0d 100644 --- a/src/app/undo-manager.coffee +++ b/src/app/undo-manager.coffee @@ -2,6 +2,7 @@ module.exports = class UndoManager undoHistory: null redoHistory: null + currentBatch: null preserveHistory: false constructor: (@buffer) -> @@ -9,22 +10,37 @@ class UndoManager @redoHistory = [] @buffer.on 'change', (op) => unless @preserveHistory - @undoHistory.push(op) + if @currentBatch + @currentBatch.push(op) + else + @undoHistory.push([op]) @redoHistory = [] undo: -> - if op = @undoHistory.pop() + if ops = @undoHistory.pop() @preservingHistory => - @buffer.change op.newRange, op.oldText - @redoHistory.push op + opsInReverse = new Array(ops...) + opsInReverse.reverse() + for op in opsInReverse + @buffer.change op.newRange, op.oldText + @redoHistory.push ops redo: -> - if op = @redoHistory.pop() + if ops = @redoHistory.pop() @preservingHistory => - @buffer.change op.oldRange, op.newText - @undoHistory.push op + for op in ops + @buffer.change op.oldRange, op.newText + @undoHistory.push ops + + startUndoBatch: -> + @currentBatch = [] + + endUndoBatch: -> + @undoHistory.push(@currentBatch) + @currentBatch = null preservingHistory: (fn) -> @preserveHistory = true fn() @preserveHistory = false +