mirror of
https://github.com/pulsar-edit/pulsar.git
synced 2024-09-20 23:48:05 +03:00
Start switch to Telepath for undo/redo
Also, TextBuffer spec passes!
This commit is contained in:
parent
2d46a98ea2
commit
3c166edd26
@ -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)
|
||||
|
@ -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"
|
@ -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.
|
||||
|
@ -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)
|
||||
|
@ -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
|
Loading…
Reference in New Issue
Block a user