{-# LANGUAGE TemplateHaskell #-} -- Copyright (c) 2004 Don Stewart - http://www.cse.unsw.edu.au/~dons -- Copyright (c) 2008 JP Bernardy -- -- | 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.Buffer.Undo ( emptyU , addChangeU , setSavedFilePointU , isAtSavedFilePointU , undoU , redoU , URList {- abstractly -} , Change(AtomicChange, InteractivePoint) ) where import Control.Monad (ap) import Data.Binary import Data.DeriveTH import Data.Derive.Binary import Yi.Buffer.Implementation data Change = SavedFilePoint | InteractivePoint | AtomicChange !Update -- !!! It's very important that the updates are forced, otherwise -- !!! we'll keep a full copy of the buffer state for each update -- !!! (thunk) put in the URList. deriving (Show) $(derive makeBinary ''Change) -- | A URList consists of an undo and a redo list. data URList = URList ![Change] ![Change] deriving (Show) $(derive makeBinary ''URList) -- | 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 emptyU :: URList emptyU = 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. addChangeU :: Change -> URList -> URList addChangeU InteractivePoint (URList us rs) = URList (addIP us) rs addChangeU 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. setSavedFilePointU :: URList -> URList setSavedFilePointU (URList undos redos) = URList (SavedFilePoint : cleanUndos) cleanRedos where cleanUndos = filter isNotSavedFilePoint undos cleanRedos = filter isNotSavedFilePoint redos isNotSavedFilePoint :: Change -> Bool isNotSavedFilePoint SavedFilePoint = False isNotSavedFilePoint _ = True -- | This undoes one interaction step. undoU :: Mark -> URList -> BufferImpl syntax -> (BufferImpl syntax, (URList, [Update])) undoU m = undoUntilInteractive m [] . undoInteractive -- | This redoes one iteraction step. redoU :: Mark -> URList -> BufferImpl syntax -> (BufferImpl syntax, (URList, [Update])) redoU = asRedo . undoU -- | Prepare undo by moving one interaction point from undoes to redoes. undoInteractive :: URList -> URList undoInteractive (URList us rs) = URList (remIP us) (addIP rs) remIP, addIP :: [Change] -> [Change] -- | Remove an initial interactive point, if there is one remIP (InteractivePoint:xs) = xs remIP xs = xs -- | Insert an initial interactive point, if there is none addIP xs@(InteractivePoint:_) = xs addIP xs = InteractivePoint:xs -- | Repeatedly undo actions, storing away the inverse operations in the -- redo list. undoUntilInteractive :: Mark -> [Update] -> URList -> BufferImpl syntax -> (BufferImpl syntax, (URList, [Update])) undoUntilInteractive pointMark xs ur@(URList cs rs) b = case cs of [] -> (b, (ur, xs)) [SavedFilePoint] -> (b, (ur, xs)) -- Why this special case? (InteractivePoint:_) -> (b, (ur, xs)) (SavedFilePoint:cs') -> undoUntilInteractive pointMark xs (URList cs' (SavedFilePoint:rs)) b (AtomicChange u:cs') -> let ur' = (URList cs' (AtomicChange (reverseUpdateI u):rs)) b' = (applyUpdateWithMoveI u b) (b'', (ur'', xs'')) = undoUntilInteractive pointMark xs ur' b' in (b'', (ur'', u:xs'')) where -- | Apply a /valid/ update and also move point in buffer to update position applyUpdateWithMoveI :: Update -> BufferImpl syntax -> BufferImpl syntax applyUpdateWithMoveI u = case updateDirection u of Forward -> apply . move Backward -> move . apply where move = modifyMarkBI pointMark (\v -> v {markPoint = updatePoint u}) apply = applyUpdateI u -- | Run the undo-function @f@ on a swapped URList making it -- operate in a redo fashion instead of undo. asRedo :: (URList -> t -> (t, (URList, [Update]))) -> URList -> t -> (t, (URList, [Update])) asRedo f ur x = let (y,(ur',rs)) = f (swapUndoRedo ur) x in (y,(swapUndoRedo ur',rs)) where swapUndoRedo :: URList -> URList swapUndoRedo (URList us rs) = URList rs us -- | undoIsAtSavedFilePoint. @True@ if the undo list is at a SavedFilePoint indicating -- 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. isAtSavedFilePointU :: URList -> Bool isAtSavedFilePointU (URList us _) = isUnchanged us where isUnchanged cs = case cs of [] -> False (SavedFilePoint : _) -> True (InteractivePoint : cs') -> isUnchanged cs' _ -> False