mirror of
https://github.com/atom/atom.git
synced 2026-02-11 07:05:11 -05:00
Merge pull request #30 from github/improve-undo-manager
Improve undo manager
This commit is contained in:
53
src/app/buffer-change-operation.coffee
Normal file
53
src/app/buffer-change-operation.coffee
Normal 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
|
||||
@@ -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())
|
||||
|
||||
@@ -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...)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -22,7 +22,7 @@ class Range
|
||||
@start = pointB
|
||||
@end = pointA
|
||||
|
||||
copy: (range) ->
|
||||
copy: ->
|
||||
new Range(@start.copy(), @end.copy())
|
||||
|
||||
isEqual: (other) ->
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user