Merge pull request #30 from github/improve-undo-manager

Improve undo manager
This commit is contained in:
Corey Johnson
2012-07-06 12:23:55 -07:00
17 changed files with 283 additions and 133 deletions

View File

@@ -0,0 +1,53 @@
Range = require 'range'
module.exports =
class BufferChangeOperation
buffer: null
oldRange: null
oldText: null
newRange: null
newText: null
constructor: ({@buffer, @oldRange, @newText}) ->
do: ->
@oldText = @buffer.getTextInRange(@oldRange)
@newRange = @calculateNewRange(@oldRange, @newText)
@changeBuffer
oldRange: @oldRange
newRange: @newRange
oldText: @oldText
newText: @newText
undo: ->
@changeBuffer
oldRange: @newRange
newRange: @oldRange
oldText: @newText
newText: @oldText
changeBuffer: ({ oldRange, newRange, newText, oldText }) ->
{ prefix, suffix } = @buffer.prefixAndSuffixForRange(oldRange)
newTextLines = newText.split('\n')
if newTextLines.length == 1
newTextLines = [prefix + newText + suffix]
else
lastLineIndex = newTextLines.length - 1
newTextLines[0] = prefix + newTextLines[0]
newTextLines[lastLineIndex] += suffix
@buffer.replaceLines(oldRange.start.row, oldRange.end.row, newTextLines)
@buffer.trigger 'change', { oldRange, newRange, oldText, newText }
newRange
calculateNewRange: (oldRange, newText) ->
newRange = new Range(oldRange.start.copy(), oldRange.start.copy())
newTextLines = newText.split('\n')
if newTextLines.length == 1
newRange.end.column += newText.length
else
lastLineIndex = newTextLines.length - 1
newRange.end.row += lastLineIndex
newRange.end.column = newTextLines[lastLineIndex].length
newRange

View File

@@ -5,10 +5,12 @@ Point = require 'point'
Range = require 'range'
EventEmitter = require 'event-emitter'
UndoManager = require 'undo-manager'
BufferChangeOperation = require 'buffer-change-operation'
module.exports =
class Buffer
@idCounter = 1
undoManager: null
modified: null
lines: null
file: null
@@ -129,40 +131,31 @@ class Buffer
change: (oldRange, newText) ->
oldRange = Range.fromObject(oldRange)
newRange = new Range(oldRange.start.copy(), oldRange.start.copy())
prefix = @lines[oldRange.start.row][0...oldRange.start.column]
suffix = @lines[oldRange.end.row][oldRange.end.column..]
oldText = @getTextInRange(oldRange)
operation = new BufferChangeOperation({buffer: this, oldRange, newText})
@pushOperation(operation)
newTextLines = newText.split('\n')
prefixAndSuffixForRange: (range) ->
prefix: @lines[range.start.row][0...range.start.column]
suffix: @lines[range.end.row][range.end.column..]
if newTextLines.length == 1
newRange.end.column += newText.length
newTextLines = [prefix + newText + suffix]
else
lastLineIndex = newTextLines.length - 1
newTextLines[0] = prefix + newTextLines[0]
newRange.end.row += lastLineIndex
newRange.end.column = newTextLines[lastLineIndex].length
newTextLines[lastLineIndex] += suffix
@lines[oldRange.start.row..oldRange.end.row] = newTextLines
replaceLines: (startRow, endRow, newLines) ->
@lines[startRow..endRow] = newLines
@modified = true
@trigger 'change', { oldRange, newRange, oldText, newText }
newRange
pushOperation: (operation, editSession) ->
if @undoManager
@undoManager.pushOperation(operation, editSession)
else
operation.do()
startUndoBatch: (selectedBufferRanges) ->
@undoManager.startUndoBatch(selectedBufferRanges)
transact: (fn) ->
@undoManager.transact(fn)
endUndoBatch: (selectedBufferRanges) ->
@undoManager.endUndoBatch(selectedBufferRanges)
undo: (editSession) ->
@undoManager.undo(editSession)
undo: ->
@undoManager.undo()
redo: ->
@undoManager.redo()
redo: (editSession) ->
@undoManager.redo(editSession)
save: ->
@saveAs(@getPath())

View File

@@ -95,6 +95,7 @@ class EditSession
new Point(row, column)
getFileExtension: -> @buffer.getExtension()
getPath: -> @buffer.getPath()
getEofBufferPosition: -> @buffer.getEofPosition()
bufferRangeForBufferRow: (row) -> @buffer.rangeForRow(row)
lineForBufferRow: (row) -> @buffer.lineForRow(row)
@@ -177,12 +178,10 @@ class EditSession
@insertText($native.readFromPasteboard())
undo: ->
if ranges = @buffer.undo()
@setSelectedBufferRanges(ranges)
@buffer.undo(this)
redo: ->
if ranges = @buffer.redo()
@setSelectedBufferRanges(ranges)
@buffer.redo(this)
foldSelection: ->
selection.fold() for selection in @getSelections()
@@ -234,10 +233,23 @@ class EditSession
@tokenizedBuffer.toggleLineCommentsInRange(range)
mutateSelectedText: (fn) ->
selections = @getSelections()
@buffer.startUndoBatch(@getSelectedBufferRanges())
fn(selection) for selection in selections
@buffer.endUndoBatch(@getSelectedBufferRanges())
@transact => fn(selection) for selection in @getSelections()
transact: (fn) ->
@buffer.transact =>
oldSelectedRanges = @getSelectedBufferRanges()
@pushOperation
undo: (editSession) ->
editSession?.setSelectedBufferRanges(oldSelectedRanges)
fn()
newSelectedRanges = @getSelectedBufferRanges()
@pushOperation
redo: (editSession) ->
editSession?.setSelectedBufferRanges(newSelectedRanges)
pushOperation: (operation) ->
@buffer.pushOperation(operation, this)
getAnchors: ->
new Array(@anchors...)

View File

@@ -75,26 +75,30 @@ class Project
getSoftWrap: -> @softWrap
setSoftWrap: (@softWrap) ->
open: (filePath) ->
open: (filePath, editSessionOptions={}) ->
if filePath?
filePath = @resolve(filePath)
buffer = @bufferWithPath(filePath) ? @buildBuffer(filePath)
else
buffer = @buildBuffer()
editSession = new EditSession
project: this
buffer: buffer
tabText: @getTabText()
autoIndent: @getAutoIndent()
softTabs: @getSoftTabs()
softWrap: @getSoftWrap()
@buildEditSession(buffer, editSessionOptions)
buildEditSession: (buffer, editSessionOptions) ->
options = _.extend(@defaultEditSessionOptions(), editSessionOptions)
options.project = this
options.buffer = buffer
editSession = new EditSession(options)
@editSessions.push editSession
@trigger 'new-edit-session', editSession
editSession
defaultEditSessionOptions: ->
tabText: @getTabText()
autoIndent: @getAutoIndent()
softTabs: @getSoftTabs()
softWrap: @getSoftWrap()
destroy: ->
for editSession in _.clone(@editSessions)
@removeEditSession(editSession)

View File

@@ -22,7 +22,7 @@ class Range
@start = pointB
@end = pointA
copy: (range) ->
copy: ->
new Range(@start.copy(), @end.copy())
isEqual: (other) ->

View File

@@ -1,56 +1,46 @@
_ = require 'underscore'
module.exports =
class UndoManager
undoHistory: null
redoHistory: null
currentBatch: null
preserveHistory: false
startBatchCallCount: null
currentTransaction: null
constructor: (@buffer) ->
constructor: ->
@startBatchCallCount = 0
@undoHistory = []
@redoHistory = []
@buffer.on 'change', (op) =>
unless @preserveHistory
if @currentBatch
@currentBatch.push(op)
else
@undoHistory.push([op])
@redoHistory = []
undo: ->
pushOperation: (operation, editSession) ->
if @currentTransaction
@currentTransaction.push(operation)
else
@undoHistory.push([operation])
@redoHistory = []
operation.do?(editSession)
transact: (fn) ->
if @currentTransaction
fn()
else
@currentTransaction = []
fn()
@undoHistory.push(@currentTransaction) if @currentTransaction.length
@currentTransaction = null
undo: (editSession) ->
if batch = @undoHistory.pop()
@preservingHistory =>
opsInReverse = new Array(batch...)
opsInReverse.reverse()
for op in opsInReverse
@buffer.change op.newRange, op.oldText
@redoHistory.push batch
opsInReverse = new Array(batch...)
opsInReverse.reverse()
op.undo?(editSession) for op in opsInReverse
@redoHistory.push batch
batch.oldSelectionRanges
redo: ->
redo: (editSession) ->
if batch = @redoHistory.pop()
@preservingHistory =>
for op in batch
@buffer.change op.oldRange, op.newText
@undoHistory.push batch
for op in batch
op.do?(editSession)
op.redo?(editSession)
@undoHistory.push(batch)
batch.newSelectionRanges
startUndoBatch: (ranges) ->
@startBatchCallCount++
return if @startBatchCallCount > 1
@currentBatch = []
@currentBatch.oldSelectionRanges = ranges
endUndoBatch: (ranges) ->
@startBatchCallCount--
return if @startBatchCallCount > 0
@currentBatch.newSelectionRanges = ranges
@undoHistory.push(@currentBatch) if @currentBatch.length > 0
@currentBatch = null
preservingHistory: (fn) ->
@preserveHistory = true
fn()
@preserveHistory = false

View File

@@ -5,11 +5,12 @@ class SnippetExpansion
constructor: (snippet, @editSession) ->
@editSession.selectToBeginningOfWord()
startPosition = @editSession.getCursorBufferPosition()
@editSession.insertText(snippet.body)
if snippet.tabStops.length
@placeTabStopAnchorRanges(startPosition, snippet.tabStops)
if snippet.lineCount > 1
@indentSubsequentLines(startPosition.row, snippet)
@editSession.transact =>
@editSession.insertText(snippet.body)
if snippet.tabStops.length
@placeTabStopAnchorRanges(startPosition, snippet.tabStops)
if snippet.lineCount > 1
@indentSubsequentLines(startPosition.row, snippet)
placeTabStopAnchorRanges: (startPosition, tabStopRanges) ->
@tabStopAnchorRanges = tabStopRanges.map ({start, end}) =>
@@ -53,3 +54,8 @@ class SnippetExpansion
destroy: ->
anchorRange.destroy() for anchorRange in @tabStopAnchorRanges
@editSession.snippetExpansion = null
restore: (@editSession) ->
@editSession.snippetExpansion = this
@tabStopAnchorRanges = @tabStopAnchorRanges.map (anchorRange) =>
@editSession.addAnchorRange(anchorRange.getBufferRange())

View File

@@ -28,7 +28,12 @@ module.exports =
editSession = editor.activeEditSession
prefix = editSession.getLastCursor().getCurrentWordPrefix()
if snippet = @snippetsByExtension[editSession.getFileExtension()][prefix]
editSession.snippetExpansion = new SnippetExpansion(snippet, editSession)
editSession.transact ->
snippetExpansion = new SnippetExpansion(snippet, editSession)
editSession.snippetExpansion = snippetExpansion
editSession.pushOperation
undo: -> snippetExpansion.destroy()
redo: (editSession) -> snippetExpansion.restore(editSession)
else
e.abortKeyBinding()