| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358 |
- define [
- "ace/range"
- "ace/edit_session"
- "ace/document"
- ], () ->
- Range = require("ace/range").Range
- EditSession = require("ace/edit_session").EditSession
- Doc = require("ace/document").Document
- class UndoManager
- constructor: (@manager) ->
- @reset()
- @nextUpdateIsRemote = false
- reset: () ->
- @undoStack = []
- @redoStack = []
- execute: (options) ->
- aceDeltaSets = options.args[0]
- @session = options.args[1]
- return if !aceDeltaSets?
- lines = @session.getDocument().getAllLines()
- linesBeforeChange = @_revertAceDeltaSetsOnDocLines(aceDeltaSets, lines)
- simpleDeltaSets = @_aceDeltaSetsToSimpleDeltaSets(aceDeltaSets, linesBeforeChange)
- @undoStack.push(
- deltaSets: simpleDeltaSets
- remote: @nextUpdateIsRemote
- )
- @redoStack = []
- @nextUpdateIsRemote = false
- undo: (dontSelect) ->
- localUpdatesMade = @_shiftLocalChangeToTopOfUndoStack()
- return if !localUpdatesMade
- update = @undoStack.pop()
- return if !update?
- if update.remote
- @manager.showUndoConflictWarning()
- lines = @session.getDocument().getAllLines()
- linesBeforeDelta = @_revertSimpleDeltaSetsOnDocLines(update.deltaSets, lines)
- deltaSets = @_simpleDeltaSetsToAceDeltaSets(update.deltaSets, linesBeforeDelta)
- selectionRange = @session.undoChanges(deltaSets, dontSelect)
- @redoStack.push(update)
- return selectionRange
-
- redo: (dontSelect) ->
- update = @redoStack.pop()
- return if !update?
- lines = @session.getDocument().getAllLines()
- deltaSets = @_simpleDeltaSetsToAceDeltaSets(update.deltaSets, lines)
- selectionRange = @session.redoChanges(deltaSets, dontSelect)
- @undoStack.push(update)
- return selectionRange
- _shiftLocalChangeToTopOfUndoStack: () ->
- head = []
- localChangeExists = false
- while @undoStack.length > 0
- update = @undoStack.pop()
- head.unshift update
- if !update.remote
- localChangeExists = true
- break
- if !localChangeExists
- @undoStack = @undoStack.concat head
- return false
- else
- # Undo stack looks like undoStack ++ reorderedhead ++ head
- # Reordered head starts of empty and consumes entries from head
- # while keeping the localChange at the top for as long as it can
- localChange = head.shift()
- reorderedHead = [localChange]
- while head.length > 0
- remoteChange = head.shift()
- localChange = reorderedHead.pop()
- result = @_swapSimpleDeltaSetsOrder(localChange.deltaSets, remoteChange.deltaSets)
- if result?
- remoteChange.deltaSets = result[0]
- localChange.deltaSets = result[1]
- reorderedHead.push remoteChange
- reorderedHead.push localChange
- else
- reorderedHead.push localChange
- reorderedHead.push remoteChange
- break
- @undoStack = @undoStack.concat(reorderedHead).concat(head)
- return true
-
- _swapSimpleDeltaSetsOrder: (firstDeltaSets, secondDeltaSets) ->
- newFirstDeltaSets = @_copyDeltaSets(firstDeltaSets)
- newSecondDeltaSets = @_copyDeltaSets(secondDeltaSets)
- for firstDeltaSet in newFirstDeltaSets.slice(0).reverse()
- for firstDelta in firstDeltaSet.deltas.slice(0).reverse()
- for secondDeltaSet in newSecondDeltaSets
- for secondDelta in secondDeltaSet.deltas
- success = @_swapSimpleDeltaOrderInPlace(firstDelta, secondDelta)
- return null if !success
- return [newSecondDeltaSets, newFirstDeltaSets]
- _copyDeltaSets: (deltaSets) ->
- newDeltaSets = []
- for deltaSet in deltaSets
- newDeltaSet =
- deltas: []
- group: deltaSet.group
- newDeltaSets.push newDeltaSet
- for delta in deltaSet.deltas
- newDelta =
- position: delta.position
- newDelta.insert = delta.insert if delta.insert?
- newDelta.remove = delta.remove if delta.remove?
- newDeltaSet.deltas.push newDelta
- return newDeltaSets
- _swapSimpleDeltaOrderInPlace: (firstDelta, secondDelta) ->
- result = @_swapSimpleDeltaOrder(firstDelta, secondDelta)
- return false if !result?
- firstDelta.position = result[1].position
- secondDelta.position = result[0].position
- return true
- _swapSimpleDeltaOrder: (firstDelta, secondDelta) ->
- if firstDelta.insert? and secondDelta.insert?
- if secondDelta.position >= firstDelta.position + firstDelta.insert.length
- secondDelta.position -= firstDelta.insert.length
- return [secondDelta, firstDelta]
- else if secondDelta.position > firstDelta.position
- return null
- else
- firstDelta.position += secondDelta.insert.length
- return [secondDelta, firstDelta]
- else if firstDelta.remove? and secondDelta.remove?
- if secondDelta.position >= firstDelta.position
- secondDelta.position += firstDelta.remove.length
- return [secondDelta, firstDelta]
- else if secondDelta.position + secondDelta.remove.length > firstDelta.position
- return null
- else
- firstDelta.position -= secondDelta.remove.length
- return [secondDelta, firstDelta]
- else if firstDelta.insert? and secondDelta.remove?
- if secondDelta.position >= firstDelta.position + firstDelta.insert.length
- secondDelta.position -= firstDelta.insert.length
- return [secondDelta, firstDelta]
- else if secondDelta.position + secondDelta.remove.length > firstDelta.position
- return null
- else
- firstDelta.position -= secondDelta.remove.length
- return [secondDelta, firstDelta]
- else if firstDelta.remove? and secondDelta.insert?
- if secondDelta.position >= firstDelta.position
- secondDelta.position += firstDelta.remove.length
- return [secondDelta, firstDelta]
- else
- firstDelta.position += secondDelta.insert.length
- return [secondDelta, firstDelta]
- else
- throw "Unknown delta types"
- _applyAceDeltasToDocLines: (deltas, docLines) ->
- doc = new Doc(docLines.join("\n"))
- doc.applyDeltas(deltas)
- return doc.getAllLines()
- _revertAceDeltaSetsOnDocLines: (deltaSets, docLines) ->
- session = new EditSession(docLines.join("\n"))
- session.undoChanges(deltaSets)
- return session.getDocument().getAllLines()
- _revertSimpleDeltaSetsOnDocLines: (deltaSets, docLines) ->
- doc = docLines.join("\n")
- for deltaSet in deltaSets.slice(0).reverse()
- for delta in deltaSet.deltas.slice(0).reverse()
- if delta.remove?
- doc = doc.slice(0, delta.position) + delta.remove + doc.slice(delta.position)
- else if delta.insert?
- doc = doc.slice(0, delta.position) + doc.slice(delta.position + delta.insert.length)
- else
- throw "Unknown delta type"
- return doc.split("\n")
- _aceDeltaSetsToSimpleDeltaSets: (aceDeltaSets, docLines) ->
- for deltaSet in aceDeltaSets
- simpleDeltas = []
- for delta in deltaSet.deltas
- simpleDeltas.push @_aceDeltaToSimpleDelta(delta, docLines)
- docLines = @_applyAceDeltasToDocLines([delta], docLines)
- {
- deltas: simpleDeltas
- group: deltaSet.group
- }
-
- _simpleDeltaSetsToAceDeltaSets: (simpleDeltaSets, docLines) ->
- for deltaSet in simpleDeltaSets
- aceDeltas = []
- for delta in deltaSet.deltas
- newAceDeltas = @_simpleDeltaToAceDeltas(delta, docLines)
- docLines = @_applyAceDeltasToDocLines(newAceDeltas, docLines)
- aceDeltas = aceDeltas.concat newAceDeltas
- {
- deltas: aceDeltas
- group: deltaSet.group
- }
- _aceDeltaToSimpleDelta: (aceDelta, docLines) ->
- start = aceDelta.range.start
- linesBefore = docLines.slice(0, start.row)
- position =
- linesBefore.join("").length + # full lines
- linesBefore.length + # new line characters
- start.column # partial line
- switch aceDelta.action
- when "insertText"
- return {
- position: position
- insert: aceDelta.text
- }
- when "insertLines"
- return {
- position: position
- insert: aceDelta.lines.join("\n") + "\n"
- }
- when "removeText"
- return {
- position: position
- remove: aceDelta.text
- }
- when "removeLines"
- return {
- position: position
- remove: aceDelta.lines.join("\n") + "\n"
- }
- else
- throw "Unknown Ace action: #{aceDelta.action}"
- _simplePositionToAcePosition: (position, docLines) ->
- column = 0
- row = 0
- for line in docLines
- if position > line.length
- row++
- position -= (line + "\n").length
- else
- column = position
- break
- return {row: row, column: column}
- _textToAceActions: (simpleText, row, column, type) ->
- aceDeltas = []
- lines = simpleText.split("\n")
- range = (options) -> new Range(options.start.row, options.start.column, options.end.row, options.end.column)
- do stripFirstLine = () ->
- firstLine = lines.shift()
- if firstLine.length > 0
- aceDeltas.push {
- text: firstLine
- range: range(
- start: column: column, row: row
- end: column: column + firstLine.length, row: row
- )
- action: "#{type}Text"
- }
- column += firstLine.length
- do stripFirstNewLine = () ->
- if lines.length > 0
- aceDeltas.push {
- text: "\n"
- range: range(
- start: column: column, row: row
- end: column: 0, row: row + 1
- )
- action: "#{type}Text"
- }
- row += 1
- do stripMiddleFullLines = () ->
- middleLines = lines.slice(0, -1)
- if middleLines.length > 0
- aceDeltas.push {
- lines: middleLines
- range: range(
- start: column: 0, row: row
- end: column: 0, row: row + middleLines.length
- )
- action: "#{type}Lines"
- }
- row += middleLines.length
- do stripLastLine = () ->
- if lines.length > 0
- lastLine = lines.pop()
- aceDeltas.push {
- text: lastLine
- range: range(
- start: column: 0, row: row
- end: column: lastLine.length , row: row
- )
- action: "#{type}Text"
- }
- return aceDeltas
- _simpleDeltaToAceDeltas: (simpleDelta, docLines) ->
- {row, column} = @_simplePositionToAcePosition(simpleDelta.position, docLines)
- if simpleDelta.insert?
- return @_textToAceActions(simpleDelta.insert, row, column, "insert")
- if simpleDelta.remove?
- return @_textToAceActions(simpleDelta.remove, row, column, "remove").reverse()
- else
- throw "Unknown simple delta: #{simpleDelta}"
- _concatSimpleDeltas: (deltas) ->
- return [] if deltas.length == 0
- concattedDeltas = []
- previousDelta = deltas.shift()
- for delta in deltas
- if delta.insert? and previousDelta.insert?
- if previousDelta.position + previousDelta.insert.length == delta.position
- previousDelta =
- insert: previousDelta.insert + delta.insert
- position: previousDelta.position
- else
- concattedDeltas.push previousDelta
- previousDelta = delta
- else if delta.remove? and previousDelta.remove?
- if previousDelta.position == delta.position
- previousDelta =
- remove: previousDelta.remove + delta.remove
- position: delta.position
- else
- concattedDeltas.push previousDelta
- previousDelta = delta
- else
- concattedDeltas.push previousDelta
- previousDelta = delta
- concattedDeltas.push previousDelta
-
- return concattedDeltas
-
- hasUndo: () -> @undoStack.length > 0
- hasRedo: () -> @redoStack.length > 0
-
|