{-# LANGUAGE RankNTypes, TupleSections, FlexibleContexts, MultiParamTypeClasses, FlexibleInstances #-} module Data.Text.Region ( pt, start, lineStart, regionLength, till, linesSize, regionLines, emptyRegion, line, regionSize, expandLines, atRegion, ApplyMap(..), updateMap, cutMap, insertMap, cutRegion, insertRegion, EditAction(..), cut, paste, overwrite, inverse, applyEdit, apply, edit, edit_, push, run_, run, runGroup, undo, redo, module Data.Text.Region.Types ) where import Prelude hiding (id, (.)) import Prelude.Unicode import Control.Arrow import Control.Category import Control.Lens import Control.Monad.State import Data.Text.Region.Types -- | Make 'Point' from line and column pt ∷ Int → Int → Point pt = Point -- | 'Point' at the beginning start ∷ Point start = pt 0 0 -- | 'Point' at the beginning of line lineStart ∷ Int → Point lineStart l = pt l 0 -- | Regions length regionLength ∷ Lens' Region Size regionLength = lens fromr tor where fromr (Region f t) = t .-. f tor (Region f _) sz = Region f (f .+. sz) -- | Region from one 'Point' to another till ∷ Point → Point → Region l `till` r = Region (min l r) (max l r) -- | Distance of @n@ lines linesSize ∷ Int → Size linesSize = pt 0 -- 'Region' height in lines, any 'Region' at least of line height 1 regionLines ∷ Lens' Region Int regionLines = lens fromr tor where fromr (Region f t) = succ $ (t ^. pointLine) - (f ^. pointLine) tor (Region f t) l = Region f (set pointLine (f ^. pointLine + l) t) -- | Is 'Region' empty emptyRegion ∷ Region → Bool emptyRegion r = r ^. regionFrom ≡ r ^. regionTo -- | n'th line region, starts at the beginning of line and ends on the next line line ∷ Int → Region line l = lineStart l `till` lineStart (succ l) -- | Make 'Region' by start position and 'Size' regionSize ∷ Point → Size → Region regionSize pt' sz = pt' `till` (pt' .+. sz) -- | Expand 'Region' to contain full lines expandLines ∷ Region → Region expandLines (Region f t) = lineStart (f ^. pointLine) `till` lineStart (succ $ t ^. pointLine) -- | Get contents at 'Region' atRegion ∷ Editable s ⇒ Region → Lens' (Contents s) (Contents s) atRegion r = lens fromc toc where fromc cts = cts ^. splitted (r ^. regionTo) . _1 . splitted (r ^. regionFrom) . _2 toc cts cts' = (cts ^. splitted (r ^. regionFrom) . _1) `concatCts` cts' `concatCts` (cts ^. splitted (r ^. regionTo) . _2) class ApplyMap a where applyMap ∷ Map → a → a instance ApplyMap () where applyMap _ = id instance ApplyMap a ⇒ ApplyMap [a] where applyMap m = map (applyMap m) instance ApplyMap Map where applyMap = mappend instance ApplyMap Region where applyMap = view ∘ mapIso instance ApplyMap Point where applyMap m p = view regionFrom $ applyMap m (p `till` p) instance ApplyMap (Replace s) where applyMap m (Replace r w) = Replace (applyMap m r) w instance ApplyMap (e s) ⇒ ApplyMap (Chain e s) where applyMap m (Chain rs) = Chain (map (applyMap m) rs) -- | Update 'Region' after some action updateMap ∷ (EditAction e s, ApplyMap a) ⇒ e s → a → a updateMap = applyMap ∘ actionMap -- | Cut 'Region' mapping cutMap ∷ Region → Map cutMap rgn = Map $ iso (cutRegion rgn) (insertRegion rgn) -- | Opposite to 'cutMap' insertMap ∷ Region → Map insertMap = invert ∘ cutMap -- | Update second 'Region' position as if it was data cutted at first 'Region' cutRegion ∷ Region → Region → Region cutRegion (Region is ie) (Region s e) = Region (if is < s then (s .-. ie) .+. is else s) (if is < e then (e .-. ie) .+. is else e) -- | Update second region position as if it was data inserted at first region insertRegion ∷ Region → Region → Region insertRegion (Region is ie) (Region s e) = Region (if is < s then (s .-. is) .+. ie else s) (if is < e then (e .-. is) .+. ie else e) class (Editable s, ApplyMap (e s)) ⇒ EditAction e s where -- | Make replace action over 'Region' and 'Contents' replace ∷ Region → Contents s → e s -- | Make 'Map' from action actionMap ∷ e s → Map -- | Perform action, modifying 'Contents' and returning inverse (undo) action perform ∷ e s → State (Contents s) (e s) -- | Cuts region cut ∷ EditAction e s ⇒ Region → e s cut r = replace r emptyContents -- | Pastes 'Contents' at some 'Point' paste ∷ EditAction e s ⇒ Point → Contents s → e s paste p = replace (p `till` p) -- | Overwrites 'Contents' at some 'Point' overwrite ∷ EditAction e s ⇒ Point → Contents s → e s overwrite p c = replace (p `regionSize` measure c) c -- | Get undo-action inverse ∷ EditAction e s ⇒ Contents s → e s → e s inverse cts act = evalState (perform act) cts -- | Apply action to 'Contents' applyEdit ∷ EditAction e s ⇒ e s → Contents s → Contents s applyEdit act = snd ∘ runState (perform act) -- | 'applyEdit' for 'Edit' apply ∷ EditAction Replace s ⇒ Edit s → Contents s → Contents s apply = applyEdit instance Editable s ⇒ EditAction Replace s where replace = Replace actionMap (Replace r w) = insertMap ((r ^. regionFrom) `regionSize` measure w) `mappend` cutMap r perform (Replace r w) = state $ \cts → (Replace ((r ^. regionFrom) `regionSize` measure w) (cts ^. atRegion r), atRegion r .~ w $ cts) instance EditAction e s ⇒ EditAction (Chain e) s where replace rgn txt = Chain [replace rgn txt] actionMap (Chain []) = mempty actionMap (Chain (r : rs)) = actionMap (applyMap (actionMap r) (Chain rs)) `mappend` actionMap r perform (Chain rs) = (Chain ∘ reverse) <$> go mempty rs where go _ [] = return [] go m (c : cs) = (:) <$> perform (applyMap m c) <*> go (actionMap (applyMap m c) `mappend` m) cs -- | Run edit monad and return result with updated contents edit ∷ EditAction Replace s ⇒ s → r → EditM s r a → (a, s) edit txt rs act = second (view $ edited . from contents) $ runState (runEditM act) (editState txt rs) -- | Run edit monad and return updated contents edit_ ∷ EditAction Replace s ⇒ s → r → EditM s r a → s edit_ txt rs = snd ∘ edit txt rs -- | Push action into history, also drops redo stack push ∷ ActionIso (Edit s) → EditM s r () push e = modify (over (history . undoStack) (e :)) >> modify (set (history . redoStack) []) -- | Run edit action and returns corresponding redo-undo action run_ ∷ (EditAction Replace s, ApplyMap r) ⇒ Edit s → EditM s r (ActionIso (Edit s)) run_ e = do cts ← gets (view edited) let (undo', cts') = runState (perform e) cts modify (set edited cts') modify (over regions (applyMap $ actionMap e)) return $ ActionIso e undo' -- | Run edit action with updating undo/redo stack run ∷ (EditAction Replace s, ApplyMap r) ⇒ Edit s → EditM s r () run e = run_ e >>= push -- | Run edit actions, updating undo/redo stack for each of them, but act like they was applied simultaneously -- For example, cutting 1-st and then 3-rd letter: -- @run (cut first) >> run (cut third) -- 1234 -> 234 -> 23@ -- @runGroup [cut first, cut third] -- 1234 -> 234 -> 24@ runGroup ∷ (EditAction Replace s, ApplyMap r) ⇒ [Edit s] → EditM s r () runGroup = go mempty where go _ [] = return () go m (e:es) = run e' >> go (applyMap m $ actionMap e') es where e' = applyMap m e -- | Undo last action undo ∷ (EditAction Replace s, ApplyMap r) ⇒ EditM s r () undo = do us@(~(u:_)) ← gets (view $ history . undoStack) unless (null us) $ do _ ← run_ (u ^. actionBack) modify (over (history . undoStack) tail) modify (over (history . redoStack) (u :)) redo ∷ (EditAction Replace s, ApplyMap r) ⇒ EditM s r () redo = do rs@(~(r:_)) ← gets (view $ history . redoStack) unless (null rs) $ do _ ← run_ (r ^. action) modify (over (history . redoStack) tail) modify (over (history . undoStack) (r :))