Start switch to Telepath for undo/redo

Also, TextBuffer spec passes!
This commit is contained in:
Corey Johnson & Nathan Sobo 2013-08-13 17:34:31 -07:00
parent 2d46a98ea2
commit 3c166edd26
5 changed files with 22 additions and 284 deletions

View File

@ -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)

View File

@ -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"

View File

@ -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.

View File

@ -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)

View File

@ -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