yi-core-0.13: src/Yi/Buffer/Undo.hs
{-# LANGUAGE CPP #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE TemplateHaskell #-}
-- | 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 GHC.Generics (Generic)
import Data.Binary (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, Generic)
instance Binary Change
-- | A URList consists of an undo and a redo list.
data URList = URList ![Change] ![Change]
deriving (Show, Generic)
instance Binary 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