yi-0.3: Yi/Undo.hs
--
-- Copyright (c) 2004 Don Stewart - http://www.cse.unsw.edu.au/~dons
--
--
--
-- | An implementation of restricted, linear undo, as described in:
--
-- > T. Berlage, "A selective undo mechanism for graphical user interfaces
-- > based on command objects", ACM Transactions on Computer-Human
-- > Interaction 1(3), pp. 269-294, 1994.
--
-- Implementation based on a proposal by sjw.
--
-- From Berlage:
--
-- > All buffer-mutating commands are stored (in abstract form) in an
-- > Undo list. The most recent item in this list is the action that
-- > will be undone next. When it is undone, it is removed from the Undo
-- > list, and its inverse is added to the Redo list. The last command
-- > put into the Redo list can be redone, and again prepended to the
-- > Undo list. New commands are added to the Undo list without
-- > affecting the Redo list.
--
-- Now, the above assumes that commands can be _redone_ in a state other
-- than that in which it was orginally done. This is not the case in our
-- text editor: a user may delete, for example, between an undo and a
-- redo. Berlage addresses this in S2.3. A Yi example:
--
-- > Delete some characters
-- > Undo partialy
-- > Move prior in the file, and delete another _chunk_
-- > Redo some things == corruption.
--
-- Berlage describes the /stable execution property/:
--
-- > A command is always redone in the same state that it was originally
-- > executed in, and is always undone in the state that was reached
-- > after the original execution.
--
-- > The only case where the linear undo model violates the stable
-- > execution property is when _a new command is submitted while the
-- > redo list is not empty_. The _restricted linear undo model_ ...
-- > clears the redo list in this case.
--
-- Also some discussion of this in: /The Text Editor Sam/, Rob Pike, pg 19.
--
module Yi.Undo (
emptyUR
, addUR
, setSavedPointUR
, manyUR
, undoInteractivePoint
, undoUR
, redoUR
, isUnchangedUList
, reverseUpdate
, getActionB
, URList {- abstractly -}
, Change(AtomicChange, InteractivePoint)
, changeUpdates
) where
import Yi.Buffer.Implementation
data Change = SavedFilePoint
| InteractivePoint
| AtomicChange Update
deriving Show
changeUpdates :: Change -> [Update]
changeUpdates (AtomicChange u) = [u]
changeUpdates SavedFilePoint = []
changeUpdates InteractivePoint = []
-- | A URList consists of an undo and a redo list.
data URList = URList [Change] [Change]
deriving Show
-- | A new empty 'URList'.
-- Notice we must have a saved file point as this is when we assume we are
-- opening the file so it is currently the same as the one on disk
emptyUR :: URList
emptyUR = URList [SavedFilePoint] []
-- | Add an action to the undo list.
-- According to the restricted, linear undo model, if we add a command
-- whilst the redo list is not empty, we will lose our redoable changes.
addUR :: Change -> URList -> URList
addUR InteractivePoint ur@(URList (InteractivePoint:_) _) = ur
addUR u (URList us _rs) = URList (u:us) []
-- | Add a saved file point so that we can tell that the buffer has not
-- been modified since the previous saved file point.
-- Notice that we must be sure to remove the previous saved file points
-- since they are now worthless.
setSavedPointUR :: URList -> URList
setSavedPointUR (URList undos redos) =
URList (SavedFilePoint : cleanUndos) cleanRedos
where
cleanUndos = filter isNotSavedFilePoint undos
cleanRedos = filter isNotSavedFilePoint redos
isNotSavedFilePoint :: Change -> Bool
isNotSavedFilePoint SavedFilePoint = False
isNotSavedFilePoint _ = True
undoInteractivePoint :: URList -> BufferImpl -> (BufferImpl, (URList, [Change]))
undoInteractivePoint (URList (InteractivePoint:cs) rs) b = (b, (URList cs (InteractivePoint:rs), []))
undoInteractivePoint ur b = (b, (ur, []))
-- | repeat ur action until we find an InteractivePoint.
manyUR :: (URList -> t -> (t, (URList, [a])))-> URList -> t -> (t, (URList, [a]))
manyUR _ ur@(URList [] _) b = (b, (ur, []))
manyUR _ ur@(URList [SavedFilePoint] _) b = (b, (ur, []))
manyUR _ ur@(URList (InteractivePoint:_) _) b = (b, (ur, []))
manyUR f ur@(URList _ _) b =
let (b', (ur', cs')) = f ur b
(b'', (ur'', cs'')) = manyUR f ur' b'
in (b'', (ur'', cs'' ++ cs'))
-- | Undo the last action that mutated the buffer contents. The action's
-- inverse is added to the redo list.
undoUR :: URList -> BufferImpl -> (BufferImpl, (URList, [Change]))
undoUR u@(URList [] _) b = (b, (u, []))
undoUR u@(URList [SavedFilePoint] _) b = (b, (u, []))
undoUR (URList (SavedFilePoint : rest) rs) b =
undoUR (URList rest (SavedFilePoint : rs)) b
undoUR (URList (u:us) rs) b =
let (b', r) = getActionB u b
in (b', (URList us (r:rs), [u]))
-- | Redo the last action that mutated the buffer contents. The action's
-- inverse is added to the undo list.
redoUR :: URList -> BufferImpl -> (BufferImpl, (URList, [Change]))
redoUR u@(URList _ []) b = (b, (u, []))
redoUR u@(URList _ [SavedFilePoint]) b = (b, (u, []))
redoUR (URList us (SavedFilePoint : rest)) b =
redoUR (URList (SavedFilePoint : us) rest) b
redoUR (URList us (r:rs)) b =
let (b', u) = getActionB r b
in (b', (URList (u:us) rs, [u]))
-- | isUnchangedUndoList. @True@ if the undo list is either empty or we are at a
-- SavedFilePoint indicated that the buffer has not been modified since we
-- last saved the file.
-- Note: that an empty undo list does NOT mean that the buffer is not modified since
-- the last save. Because we may have saved the file and then undone actions done before
-- the save.
isUnchangedUList :: URList -> Bool
isUnchangedUList (URList (InteractivePoint:us) rs) = isUnchangedUList (URList us rs)
isUnchangedUList (URList [] _) = False
isUnchangedUList (URList (SavedFilePoint : _) _) = True
isUnchangedUList (URList _ _) = False
-- | Given a Update, apply it to the buffer, and return the
-- Update that reverses it.
getActionB :: Change -> BufferImpl -> (BufferImpl, Change)
getActionB (AtomicChange u) b = (applyUpdateI u (moveToI (updatePoint u) b),
AtomicChange (reverseUpdate u b))
getActionB c b = (b,c)
-- | Reverse the given update
reverseUpdate :: Update -> BufferImpl -> Update
reverseUpdate (Delete p n) b = Insert p (nelemsBI n p b)
reverseUpdate (Insert p cs) _ = Delete p (length cs)