From 3c166edd26040bd66de79d55af993b6e748422a1 Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Tue, 13 Aug 2013 17:34:31 -0700 Subject: [PATCH] Start switch to Telepath for undo/redo Also, TextBuffer spec passes! --- spec/app/text-buffer-spec.coffee | 11 +- spec/app/undo-manager-spec.coffee | 162 ------------------------------ src/app/edit-session.coffee | 3 - src/app/text-buffer.coffee | 54 ++++------ src/app/undo-manager.coffee | 76 -------------- 5 files changed, 22 insertions(+), 284 deletions(-) delete mode 100644 spec/app/undo-manager-spec.coffee delete mode 100644 src/app/undo-manager.coffee diff --git a/spec/app/text-buffer-spec.coffee b/spec/app/text-buffer-spec.coffee index d2b77b8e1..1ac631c17 100644 --- a/spec/app/text-buffer-spec.coffee +++ b/spec/app/text-buffer-spec.coffee @@ -28,11 +28,6 @@ describe 'TextBuffer', -> buffer = project.bufferForPath(filePath) expect(buffer.getText()).toBe fsUtils.read(filePath) - it "is not modified and has no undo history", -> - buffer = project.bufferForPath(filePath) - expect(buffer.isModified()).toBeFalsy() - expect(buffer.undoManager.undoHistory.length).toBe 0 - describe "when no file exists for the path", -> it "is modified and is initially empty", -> filePath = "does-not-exist.txt" @@ -420,7 +415,7 @@ describe 'TextBuffer', -> expect(event.newText).toBe "foo\nbar" it "allows a 'changed' event handler to safely undo the change", -> - buffer.on 'changed', -> buffer.undo() + buffer.one 'changed', -> buffer.undo() buffer.change([0, 0], "hello") expect(buffer.lineForRow(0)).toBe "var quicksort = function () {" @@ -896,7 +891,7 @@ describe 'TextBuffer', -> buffer2 = null afterEach -> - buffer2.release() + buffer2?.release() describe "when the serialized buffer had no unsaved changes", -> it "loads the current contents of the file at the serialized path", -> @@ -946,6 +941,8 @@ describe 'TextBuffer', -> buffer3.destroy() it "does not include them in the serialized state", -> + buffer.append("// appending text so buffer.isModified() is true") + doc1 = buffer.getState() doc2 = doc1.clone(new Site(2)) doc1.connect(doc2) diff --git a/spec/app/undo-manager-spec.coffee b/spec/app/undo-manager-spec.coffee deleted file mode 100644 index bd52e0c19..000000000 --- a/spec/app/undo-manager-spec.coffee +++ /dev/null @@ -1,162 +0,0 @@ -UndoManager = require 'undo-manager' -{Range} = require 'telepath' - -describe "UndoManager", -> - [buffer, undoManager] = [] - - beforeEach -> - buffer = project.buildBuffer('sample.js') - undoManager = buffer.undoManager - - afterEach -> - buffer.destroy() - - describe ".undo()", -> - it "undoes the last change", -> - buffer.change(new Range([0, 5], [0, 9]), '') - buffer.insert([0, 6], 'h') - buffer.insert([0, 10], 'y') - expect(buffer.lineForRow(0)).toContain 'qshorty' - - undoManager.undo() - expect(buffer.lineForRow(0)).toContain 'qshort' - expect(buffer.lineForRow(0)).not.toContain 'qshorty' - - undoManager.undo() - expect(buffer.lineForRow(0)).toContain 'qsort' - - undoManager.undo() - expect(buffer.lineForRow(0)).toContain 'quicksort' - - it "does not throw an exception when there is nothing to undo", -> - undoManager.undo() - - describe ".redo()", -> - beforeEach -> - buffer.change(new Range([0, 5], [0, 9]), '') - buffer.insert([0, 6], 'h') - buffer.insert([0, 10], 'y') - undoManager.undo() - undoManager.undo() - expect(buffer.lineForRow(0)).toContain 'qsort' - - it "redoes the last undone change", -> - undoManager.redo() - expect(buffer.lineForRow(0)).toContain 'qshort' - - undoManager.redo() - expect(buffer.lineForRow(0)).toContain 'qshorty' - - undoManager.undo() - expect(buffer.lineForRow(0)).toContain 'qshort' - - it "does not throw an exception when there is nothing to redo", -> - undoManager.redo() - undoManager.redo() - undoManager.redo() - - it "discards the redo history when there is a new change following an undo", -> - buffer.insert([0, 6], 'p') - expect(buffer.getText()).toContain 'qsport' - - undoManager.redo() - expect(buffer.getText()).toContain 'qsport' - - describe "transaction methods", -> - describe "transact()", -> - beforeEach -> - buffer.setText('') - - it "starts a transaction that can be committed later", -> - buffer.append('1') - undoManager.transact() - buffer.append('2') - buffer.append('3') - undoManager.commit() - buffer.append('4') - - expect(buffer.getText()).toBe '1234' - undoManager.undo() - expect(buffer.getText()).toBe '123' - undoManager.undo() - expect(buffer.getText()).toBe '1' - undoManager.redo() - expect(buffer.getText()).toBe '123' - - it "starts a transaction that can be aborted later", -> - buffer.append('1') - buffer.append('2') - - undoManager.transact() - - buffer.append('3') - buffer.append('4') - expect(buffer.getText()).toBe '1234' - - undoManager.abort() - expect(buffer.getText()).toBe '12' - - undoManager.undo() - expect(buffer.getText()).toBe '1' - - undoManager.redo() - expect(buffer.getText()).toBe '12' - - undoManager.redo() - expect(buffer.getText()).toBe '12' - - describe "commit", -> - it "throws an exception if there is no current transaction", -> - expect(-> buffer.commit()).toThrow() - - it "does not record empty transactions", -> - buffer.insert([0,0], "foo") - undoManager.transact() - undoManager.commit() - undoManager.undo() - expect(buffer.lineForRow(0)).not.toContain("foo") - - describe "abort", -> - it "does not affect the undo stack when the current transaction is empty", -> - buffer.setText('') - buffer.append('1') - buffer.transact() - buffer.abort() - expect(buffer.getText()).toBe '1' - buffer.undo() - expect(buffer.getText()).toBe '' - - it "throws an exception if there is no current transaction", -> - expect(-> buffer.abort()).toThrow() - - describe "exception handling", -> - describe "when a `do` operation throws an exception", -> - it "clears the stack", -> - spyOn(console, 'error') - buffer.setText("word") - buffer.insert([0,0], "1") - expect(-> - undoManager.pushOperation(do: -> throw new Error("I'm a bad do operation")) - ).toThrow("I'm a bad do operation") - - undoManager.undo() - expect(buffer.lineForRow(0)).toBe "1word" - - describe "when an `undo` operation throws an exception", -> - it "clears the stack", -> - spyOn(console, 'error') - buffer.setText("word") - buffer.insert([0,0], "1") - undoManager.pushOperation(undo: -> throw new Error("I'm a bad undo operation")) - expect(-> undoManager.undo()).toThrow("I'm a bad undo operation") - expect(buffer.lineForRow(0)).toBe "1word" - - describe "when an `redo` operation throws an exception", -> - it "clears the stack", -> - spyOn(console, 'error') - buffer.setText("word") - buffer.insert([0,0], "1") - undoManager.pushOperation(redo: -> throw new Error("I'm a bad redo operation")) - undoManager.undo() - expect(-> undoManager.redo()).toThrow("I'm a bad redo operation") - expect(buffer.lineForRow(0)).toBe "1word" diff --git a/src/app/edit-session.coffee b/src/app/edit-session.coffee index cb03047cf..89bcd924f 100644 --- a/src/app/edit-session.coffee +++ b/src/app/edit-session.coffee @@ -782,9 +782,6 @@ class EditSession selection.insertText(fn(text)) selection.setBufferRange(range) - pushOperation: (operation) -> - @buffer.pushOperation(operation, this) - ### Public ### # Returns a valid {DisplayBufferMarker} object for the given id if one exists. diff --git a/src/app/text-buffer.coffee b/src/app/text-buffer.coffee index e01850463..acfe07d0f 100644 --- a/src/app/text-buffer.coffee +++ b/src/app/text-buffer.coffee @@ -4,8 +4,6 @@ telepath = require 'telepath' fsUtils = require 'fs-utils' File = require 'file' EventEmitter = require 'event-emitter' -UndoManager = require 'undo-manager' -BufferChangeOperation = require 'buffer-change-operation' guid = require 'guid' # Public: Represents the contents of a file. @@ -23,7 +21,6 @@ class TextBuffer stoppedChangingDelay: 300 stoppedChangingTimeout: null - undoManager: null cachedDiskContents: null cachedMemoryContents: null conflict: false @@ -37,12 +34,11 @@ class TextBuffer constructor: (optionsOrState={}, params={}) -> if optionsOrState instanceof telepath.Document @state = optionsOrState - {@project} = params @text = @state.get('text') filePath = @state.get('relativePath') @id = @state.get('id') else - {@project, filePath, initialText} = optionsOrState + {filePath, initialText} = optionsOrState @text = site.createDocument(initialText, shareStrings: true) if initialText @id = guid.create().toString() @state = site.createDocument @@ -51,12 +47,12 @@ class TextBuffer version: @constructor.version if filePath - @setPath(@project.resolve(filePath)) + @setPath(project.resolve(filePath)) if @text @updateCachedDiskContents() else @text = site.createDocument('', shareStrings: true) - @reload() if fsUtils.exists(filePath) + @reload() if fsUtils.exists(@getPath()) else @text ?= site.createDocument('', shareStrings: true) @@ -64,7 +60,6 @@ class TextBuffer @text.on 'changed', @handleTextChange @text.on 'marker-created', (marker) => @trigger 'marker-created', marker @text.on 'markers-updated', => @trigger 'markers-updated' - @undoManager = new UndoManager(this) ### Internal ### @@ -92,8 +87,11 @@ class TextBuffer serialize: -> state = @state.clone() - for marker in state.get('text').getMarkers() when marker.isRemote() - marker.destroy() + if @isModified() + for marker in state.get('text').getMarkers() when marker.isRemote() + marker.destroy() + else + state.remove('text') state getState: -> @state @@ -158,7 +156,7 @@ class TextBuffer @state.get('relativePath') setRelativePath: (relativePath) -> - @setPath(@project.resolve(relativePath)) + @setPath(project.resolve(relativePath)) # Sets the path for the file. # @@ -170,7 +168,7 @@ class TextBuffer @file = new File(path) @file.read() if @file.exists() @subscribeToFile() - @state.set('relativePath', @project.relativize(path)) + @state.set('relativePath', project.relativize(path)) @trigger "path-changed", this # Retrieves the current buffer's file extension. @@ -360,15 +358,11 @@ class TextBuffer range = Range.fromObject(range) new Range(@clipPosition(range.start), @clipPosition(range.end)) - # Undos the last operation. - # - # editSession - The {EditSession} associated with the buffer. - undo: (editSession) -> @undoManager.undo(editSession) + undo: -> + @text.undo() - # Redos the last operation. - # - # editSession - The {EditSession} associated with the buffer. - redo: (editSession) -> @undoManager.redo(editSession) + redo: -> + @text.redo() # Saves the buffer. save: -> @@ -609,26 +603,14 @@ class TextBuffer ### Internal ### - pushOperation: (operation, editSession) -> - if @undoManager - @undoManager.pushOperation(operation, editSession) - else - operation.do() - transact: (fn) -> - if isNewTransaction = @undoManager.transact() - @pushOperation(new BufferChangeOperation(buffer: this)) # restores markers on undo - if fn - try - fn() - finally - @commit() if isNewTransaction + @text.transact fn commit: -> - @pushOperation(new BufferChangeOperation(buffer: this)) # restores markers on redo - @undoManager.commit() + @text.commit() - abort: -> @undoManager.abort() + abort: -> + @text.abort() change: (oldRange, newText, options={}) -> oldRange = @clipRange(oldRange) diff --git a/src/app/undo-manager.coffee b/src/app/undo-manager.coffee deleted file mode 100644 index c8673b224..000000000 --- a/src/app/undo-manager.coffee +++ /dev/null @@ -1,76 +0,0 @@ -_ = require 'underscore' - -# Internal: The object in charge of managing redo and undo operations. -module.exports = -class UndoManager - undoHistory: null - redoHistory: null - currentTransaction: null - - constructor: -> - @clear() - - clear: -> - @currentTransaction = [] if @currentTransaction? - @undoHistory = [] - @redoHistory = [] - - pushOperation: (operation, editSession) -> - if @currentTransaction - @currentTransaction.push(operation) - else - @undoHistory.push([operation]) - @redoHistory = [] - - try - operation.do?(editSession) - catch e - @clear() - throw e - - transact: -> - isNewTransaction = not @currentTransaction? - @currentTransaction ?= [] - isNewTransaction - - commit: -> - unless @currentTransaction? - throw new Error("Trying to commit when there is no current transaction") - - empty = @currentTransaction.length is 0 - @undoHistory.push(@currentTransaction) unless empty - @currentTransaction = null - not empty - - abort: -> - unless @currentTransaction? - throw new Error("Trying to abort when there is no current transaction") - - if @commit() - @undo() - @redoHistory.pop() - - undo: (editSession) -> - try - if batch = @undoHistory.pop() - opsInReverse = new Array(batch...) - opsInReverse.reverse() - op.undo?(editSession) for op in opsInReverse - @redoHistory.push batch - batch.oldSelectionRanges - catch e - @clear() - throw e - - redo: (editSession) -> - try - if batch = @redoHistory.pop() - for op in batch - op.do?(editSession) - op.redo?(editSession) - - @undoHistory.push(batch) - batch.newSelectionRanges - catch e - @clear() - throw e