packages feed

dao-0.1.0.1: src/Dao/StepList.hs

-- "src/Dao/StepList.hs"  provides a fundamental data type used in the
-- Dao System, the "StepList", which is a cursor that can step forward
-- or backward through a list.
-- 
-- Copyright (C) 2008-2014  Ramin Honary.
-- This file is part of the Dao System.
--
-- The Dao System is free software: you can redistribute it and/or
-- modify it under the terms of the GNU General Public License as
-- published by the Free Software Foundation, either version 3 of the
-- License, or (at your option) any later version.
-- 
-- The Dao System is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-- GNU General Public License for more details.
-- 
-- You should have received a copy of the GNU General Public License
-- along with this program (see the file called "LICENSE"). If not, see
-- <http://www.gnu.org/licenses/agpl.html>.


{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}

-- | This is a line-editor object, but it works with arbitrary lists of objects, but this will work
-- for editing arbitrary lists. You could use it to create an ordinary line editor by representing a
-- file as be a list of strings representing a file. each string could further be converted to a
-- StepList containing characters to edit the line. 
--
-- This module provides a basic list cursor interface, that is, a list that you can step through
-- forward or backward, item by item. This is useful for building line editors. This module export
-- operators, so it is best not to import this module qualified. Therefore functions similar to
-- 'Data.Set.empty' or 'Data.Set.singleton' are named 'slEmpty' and 'slSingleton' to prevent name
-- conflicts without using qualified importing.
--
module Dao.StepList where

import           Control.Applicative
import           Control.Monad

import           Data.Array.IArray
import qualified Data.IntMap as I
import           Data.List
import           Data.Monoid
import           Data.Typeable

----------------------------------------------------------------------------------------------------

data StepList a
  = StepList
    { slCursor :: Int
    , slLength :: Int
    , slLeftOfCursor :: [a]
      -- ^ items to the left of the cursor are stored in reverse order. If your 'StepList' is:
      -- > list_A = [0, 1, 2, 3, 4] <> [5, 6, 7, 8]
      -- evaluating @'leftOfCursor' list_A@ produces:
      -- > [4, 3, 2, 1, 0]
    , slRightOfCursor :: [a]
      -- ^ items to the left of the cursor are stored in forward order. If your 'StepList' is:
      -- > list_A = [0, 1, 2, 3, 4] <> [5, 6, 7, 8]
      -- evaluating @'leftOfCursor' list_A@ produces:
      -- > [5, 6, 7, 8]
    }
  deriving (Eq, Ord, Show, Read, Typeable)

instance Monoid (StepList a) where { mempty = slEmpty; mappend = (+:+); }
instance Functor     StepList where { fmap  = slMap }
instance MonadPlus   StepList where { mzero = mempty; mplus = (+:+); }
instance Applicative StepList where { pure  = return; (<*>) = ap;      }
instance Alternative StepList where { empty = mempty; (<|>) = (+:+); }
instance Monad       StepList where
  -- | Return is defined by 'slSingletonL'. This is because when lists are used as a monad, the
  -- 'Control.Monad.return' operations that occur earlier in the computation place items in the list
  -- earlier than 'Control.Monad.return' operations that occur later in the computation. Therefore a
  -- monadic computation like @a >> b@ will have @a@ placed to the left of @b@.
  return = slSingletonL
  -- | Just like how the '(Prelude.>>=)' operator instantiated for Haskell lists is the
  -- 'Prelude.concatMap' function, the '(Prelude.>>=)' operator for 'StepList's is the 'slConcatMap'
  -- function.
  (>>=) = flip slConcatMap

-- | Test if the 'StepList' contains no elements.
slNull :: StepList a -> Bool
slNull sl = null (slLeftOfCursor sl) && null (slRightOfCursor sl)

-- | Create an empty 'StepList'.
slEmpty :: StepList a
slEmpty = StepList 0 0 [] []

-- | Create a 'StepList' with a single item to the right of the cursor.
slSingletonR :: a -> StepList a
slSingletonR a = StepList 1 1 [a] []

-- | Create a 'StepList' with a single item to the left of the cursor.
slSingletonL :: a -> StepList a
slSingletonL a = StepList 1 1 [a] []

-- | Create a 'StepList' from a list of elements and an initial cursor position. The cursor position
-- can be out of range, it will be placed at the end by default.
slFromList :: Int -> [a] -> StepList a
slFromList i lst =
  let len = length lst
      cur = max 0 i
      (left, right) = splitAt cur lst
  in  StepList
      { slCursor        = min cur len
      , slLength        = len
      , slLeftOfCursor  = left
      , slRightOfCursor = right
      }

-- | Create a 'StepList' with two lists, the items to the left of the cursor, and the items to the
-- right of the cursor. The elements of the list to the left of the cursor are not reversed when
-- constructing the 'StepList' with this constructor. When the cursor is moved back to the
-- beginning of the list, the items will be reversed, i.e.
-- > slToList (slFromList [0,1,2,3] [4,5,6,7])
-- will evaluate to the list:
-- > [3,2,1,0,4,5,6,7]
slFromLeftRight :: [a] -> [a] -> StepList a
slFromLeftRight left right =
  StepList{ slCursor=cur, slLength=cur+rightlen, slLeftOfCursor=left, slRightOfCursor=right } where
    cur      = length left
    rightlen = length right

-- | Assign values to integer indicies in the 'Data.IntMap.IntMap' data type, and this function will
-- convert the 'Data.IntMap.IntMap' to a 'StepList'. Negative indicies will be placed to the left of
-- the cursor, with the lowest value being furthest left, positive indicies will be placed to the
-- right of the cursor with the highest value being furthest right.
slFromIntMap :: I.IntMap a -> StepList a
slFromIntMap im = slFromLeftRight (fmap snd $ sortfst left) (fmap snd $ sortfst right) where
  sortfst = sortBy (\a b -> compare (fst a) (fst b))
  (left, right) = partition ((<0) . fst) $ I.assocs im

slToArray :: StepList a -> Maybe (Array Int a)
slToArray (StepList cur len left right) =
  if len==0
  then Nothing
  else Just $ array (negate cur, len-cur-1)
          (zip (iterate (subtract 1) (negate 1)) left ++ zip (iterate (+1) 0) right)

-- | Creates an array storing the list where the indicies are 'Data.Array.IArray.bounds' such that
-- value at index zero is the value returned by 'slHeadR', items before the cursor have negative
-- indicies, and items after the cursor are zero or positive. If the cursor is at the right-most
-- position of the list, the resulting array, every item is assigned to a negative index.
slFromArray :: Maybe (Array Int a) -> StepList a
slFromArray arr = case arr of
  Nothing  -> mempty
  Just arr ->
    let bnds = bounds arr
        (lo, hi) = maybe bnds id $ do
          (lo, hi) <- return bnds
          (lo, hi) <- return (min lo hi, max lo hi)
          return $ if lo>0 then (0, hi-lo) else (lo, hi)
        len   = hi - lo + 1
        cur   = negate lo
        left  = map (arr!) (takeWhile (>=lo) $ iterate (subtract 1) (negate 1))
        right = map (arr!) (takeWhile (<=hi) $ iterate (+1) 0)
    in  StepList cur len left right

-- | Convert the 'StepList' to an ordinary Haskell list.
slToList :: StepList a -> [a]
slToList (StepList _ _ left right) = reverse left ++ right

-- | When two 'StepList's are concatenated with @a +:+ b@ think of @b@ being inserted into the cursor
-- position at @a@, with all the items to the left of the cursor in @b@ being placed to the right of
-- the cursor of @a@ and all the items to the right of the cursor of @b@ being placed to the left
-- the cursor of @a@. Here is an ASCII illustration:
-- > list_A         = [a0, a1, a2, a3, a4]         <>                             [a5, a6, a7, a8]
-- >         list_B =                     [b0, b1] <> [b2, b3, b4, b5, b6, b7, b8]
-- > ----- concatenate -----
-- > list_A<>list_B = [a0, a1, a2, a3, a4, b0, b1] <> [b2, b3, b4, b5, b6, b7, b8, a5, a6, a7, a8]
(+:+) :: StepList a -> StepList a -> StepList a
(StepList c1 n1 b1 a1) +:+ (StepList c2 n2 b2 a2) = StepList (c1+c2) (n1+n2) (b1++b2) (a2++a1)
infixr 5 +:+

slMap :: (a -> b) -> StepList a -> StepList b
slMap f (StepList cur len left right) = StepList cur len (map f left) (map f right)

slConcat :: [StepList a] -> StepList a
slConcat = foldl (+:+) slEmpty

-- | The @(Control.Monad.>>=)@ operator applied to lists is the same as the 'Data.List.concatMap'
-- function. In the case of 'StepList', the @(Control.Monad.>>=)@ operator is defined with
-- 'Data.Monoid.mconcat' and 'Prelude.map'.
slConcatMap :: (a -> StepList b) -> StepList a -> StepList b
slConcatMap f = slConcat . map f . slToList

-- | Place an item to the left of the cursor. This operator binds to the RIGHT with a precedence of
-- 5, it does not bind left. The reason is that this operator is more similar to the Haskell list
-- operator @(:)@.
-- > x <: ([0, 1, 2] <> [3, 4])
-- >      ([0, 1, 2, x] <> [3, 4])
(<|) :: a -> (StepList a -> StepList a)
(<|) a (StepList cur len left right) = StepList (cur+1) (len+1) (a:left) right
infixr 5 <|

-- | Place an item to the right of the cursor. This operator binds to the right with a precedence of
-- 5, just like the Haskell list operator @(:)@.
-- > x :> ([0, 1, 2] <> [3, 4])
-- >      ([0, 1, 2] <> [x, 3, 4])
(|>) :: a -> StepList a -> StepList a
(|>) a (StepList cur len left right) = StepList cur (len+1) left (a:right)
infixr 5 |>

-- | Place an item to the left of the cursor. This operator binds to the RIGHT with a precedence of
-- 5, it does not bind left. The reason is that this operator is more similar to the Haskell list
-- operator @(++)@.
-- > [a0, a1, a2] <++ ([b0, b1, b2] <> [b3, b4])
-- >                  ([b0, ab, b2, a0, a1, a2] <> [b3, b4])
(<++) :: [a] -> StepList a -> StepList a
(<++) ox sl = let len = length ox in
  sl{ slLeftOfCursor = slLeftOfCursor sl ++ reverse ox
    , slCursor = slCursor sl + len
    , slLength = slLength sl + len
    }
infixr 5 <++

-- | Place an item to the right of the cursor. This operator binds to the right with a precedence of
-- 5, just like the Haskell list operator @(++)@.
-- > [a0, a1, a2] ++> ([b0, b1, b2] <> [b3, b4])
-- >                  ([b0, b1, b2] <> [a0, a1, a2, b3, b4])
(++>) :: [a] -> StepList a -> StepList a
(++>) ox sl = let len = length ox in
  sl{ slRightOfCursor = ox ++ slRightOfCursor sl
    , slLength = slLength sl + len
    }
infixr 5 ++>

-- | Returns 'Prelude.True' if it is possible to move the cursor left or right by @n@ steps.
slShiftCheck :: Int -> StepList a -> Bool
slShiftCheck delta (StepList cur len _ _) = inRange (0, len) (cur+delta)

-- | Returns 'Prelude.True' if it the index is within the bounds of the list.
slIndexCheck :: Int -> StepList a -> Bool
slIndexCheck i (StepList _ len _ _) = inRange (0, len) i

-- | Shift the cursor @delta@ elements to the left if @delta@ is negative, or @delta@ elements to
-- the right if @delta@ is positive.
slCursorShift :: Int -> StepList a -> StepList a
slCursorShift delta0 a@(StepList cur len left right)
  | delta0==0 = a
  | delta0< 0 =
      let delta = max delta0 (negate cur)
          (middle, left') = splitAt (abs delta) left
      in StepList (cur+delta) len left' (reverse middle ++ right)
  | delta0> 0 =
      let delta = min delta0 (len-cur)
          (middle, right') = splitAt delta right
      in StepList (cur+delta) len (reverse middle ++ left) right'
  | otherwise = error "case statement of Dao.StepList.slCursorShift"

-- | Place the cursor at an index position.
slCursorTo :: Int -> StepList a -> StepList a
slCursorTo i a@(StepList cur _ _ _) = slCursorShift (i-cur) a

-- | Lookup a value at an absolute index (not relative to the cursor).
slIndex :: Int -> StepList a -> Maybe a
slIndex i sl =
  let (StepList _ _ _ right) = slCursorShift i sl
  in case right of { [] -> Nothing; o:_ -> return o; }

-- | A bounds value expressed as a pair of indicies relative to the current cursor position can be
-- converted to an bounds value expressed as a pair of indicies relative to the 0th element in the
-- 'StepList'. This is the inverse operation of 'slAbsToRel'.
slRelToAbs :: StepList a -> (Int, Int) -> (Int, Int)
slRelToAbs (StepList cur _ _ _) (lo, hi) = (cur+lo, cur+hi)

-- | A bounds value expressed as a pair of indicies relative to the 0th position in the 'StepList'
-- can be converted to a bounds value expressed as a pair of indicies relative to the current
-- cursor position. This is the inverse operation of 'slRelToAbs'.
slAbsToRel :: StepList a -> (Int, Int) -> (Int, Int)
slAbsToRel (StepList cur _ _ _) (lo, hi) = (lo-cur, hi-cur)

-- | Selects items around the cursor, with the option of deleting the items selected from the
-- 'StepList'. The first boolean parameter indicates whether or not the list should be altered by
-- deleting the items that were selected. Items to the left of the cursor are selected if the
-- indicies are negative and items to the right of the cursor are selected if the indicies are
-- positive. Specify the range to select by passing a lower and upper bound relative to the cursor.
-- For example, to select a range of ten items, five before the cursor and five after the cursor,
-- use @slGetRelRange False (negate 5) 5@. To delete that same range from the 'StepList' and also
-- return it use @slGetRelRange True (negate 5) 5@.
--
-- This function evaluates to a pair of 'StepList's. The 'Prelude.fst' is the selected items, the
-- 'Prelude.snd' is the modified 'StepList' which may be identical to the parameter 'StepList' if
-- the 'doDelete::Bool' parameter is 'Prelude.False'.
slCutRelRange :: Bool -> (Int, Int) -> StepList a -> (StepList a, StepList a)
slCutRelRange doDelete (lo, hi) o@(StepList cur len left right) = maybe (mempty, o) id $ do
  let cutBiased minLen list = do
        let lim = Just . min minLen . abs
        lo <- lim $ min lo hi
        hi <- lim $ max lo hi
        let cutLen = hi-lo
        guard (cutLen/=0)
        (keep, list) <- Just $ splitAt lo     list
        (cut , list) <- Just $ splitAt cutLen list
        return (cutLen, keep, cut, list)
  case lo of
    lo|lo<0  -> case hi of
      hi|hi<=0 -> do
        (cutLen, keep, cut, left) <- cutBiased cur left
        cut <- Just $ StepList cutLen cutLen cut []
        Just $
          if doDelete
          then (cut, StepList (max 0 (cur-cutLen)) (len-cutLen) (left++keep) right)
          else (cut, o)
      hi|hi>0  -> do
        lo <- Just $ min cur (abs lo)
        hi <- Just $ min (len-cur) hi
        let cutLen = hi+lo
        (midLeft,  left ) <- Just $ splitAt lo left
        (midRight, right) <- Just $ splitAt hi right
        middle            <- Just $ StepList lo cutLen midLeft midRight
        Just $
          if doDelete
          then (middle, StepList (max 0 (cur-cutLen)) (len-cutLen) left right)
          else (middle, o)
      _ -> undefined
    lo|lo>=0 -> case hi of
      hi|hi<=0 -> if lo==hi then Nothing else Just $ slCutRelRange doDelete (hi, lo) o
      hi|hi>0  -> do
        (cutLen, keep, cut, right) <- cutBiased (len-cur) right
        cut <- Just $ StepList 0 cutLen [] cut
        Just $
          if doDelete
          then (cut, StepList cur (max cur (len-cutLen)) left (keep++right))
          else (cut, o)
      _ -> undefined
    _ -> undefined

-- | Like 'slCutRelRange' but operates on an upper and lower bound indicated by absolute indicies,
-- rather than indicies relative to the cursor.
slCutAbsRange :: Bool -> (Int, Int) -> StepList a -> (StepList a, StepList a)
slCutAbsRange doDelete bnds list = slCutRelRange doDelete (slAbsToRel list bnds) list

slDeleteRelRange :: (Int, Int) -> StepList a -> StepList a
slDeleteRelRange bnds = snd . slCutAbsRange True bnds

slDeleteAbsRange :: (Int, Int) -> StepList a -> StepList a
slDeleteAbsRange bnds list = slDeleteRelRange (slAbsToRel list bnds) list

slCopyRelRange :: (Int, Int) -> StepList a -> StepList a
slCopyRelRange bnds = fst . slCutRelRange False bnds

slCopyAbsRange :: (Int, Int) -> StepList a -> StepList a
slCopyAbsRange bnds list = slCopyRelRange (slAbsToRel list bnds) list

-- | Many functions may need to modify a 'StepList' but only on the elements to the left or right of
-- the cursor. These functions take a boolean type called 'Bias'
data Bias = ToLeft | ToRight deriving (Eq, Ord, Bounded, Enum, Ix, Show, Read)

-- | Modify the items to the left or right of the cursor. The function will return a polymorphic
-- value paired with an updated list of items to the left of the cursor.
biasedApply :: Bias -> ([elems] -> (result, [elems])) -> StepList elems -> (result, StepList elems)
biasedApply bias f (StepList cur len left right) = case bias of
  ToRight -> let (result, right') = f right in (result, StepList cur (length right') left right')
  ToLeft  -> 
    let (result, left') = f left
        newlen = length left'
    in (result, StepList newlen (len-cur+newlen) left' right)

-- | Evaluates to true if the cursor is beyond the left or right-most position in the list.
slAtEnd :: Bias -> StepList a -> Bool
slAtEnd bias (StepList cur len _ _) = case bias of
  ToRight -> cur==len
  ToLeft  -> cur==0

-- | Move the cursor to the right-most or left-most end of the 'StepList', depending on the 'Bias'
-- value given. The word Return has nothing to do with monad, we use this term because this function
-- is similar to the /carriage return/ of a typewriter.
slReturn :: Bias -> StepList a -> StepList a
slReturn bias (StepList cur len left right) = case bias of
  ToLeft  -> StepList 0 (cur+len) [] (reverse left ++ right)
  ToRight -> StepList len len (left ++ reverse right) []

slHeadR :: StepList a -> a
slHeadR (StepList _ _ _ a) = head a

slHeadL :: StepList a -> a
slHeadL (StepList _ _ a _) = head a

slTail :: Bias -> StepList elems -> StepList elems
slTail bias = snd . biasedApply bias ((,) () . tail)

slTake :: Bias -> Int -> StepList a -> StepList a
slTake bias n = snd . biasedApply bias ((,) () . take n)

slTakeWhile :: Bias -> (a -> Bool) -> StepList a -> StepList a
slTakeWhile bias p = snd . biasedApply bias ((,) () . takeWhile p)

slDrop :: Bias -> Int -> StepList a -> StepList a
slDrop bias n = snd . biasedApply bias ((,) () . drop n)

slDropWhile :: Bias -> (a -> Bool) -> StepList a -> StepList a
slDropWhile bias p = snd . biasedApply bias ((,) () . dropWhile p)

slSplitAt :: Bias -> Int -> StepList a -> ([a], StepList a)
slSplitAt bias n = biasedApply bias (splitAt n)

slSpan :: Bias -> (a -> Bool) -> StepList a -> ([a], StepList a)
slSpan bias p = biasedApply bias (span p)

slBreak :: Bias -> (a -> Bool) -> StepList a -> ([a], StepList a)
slBreak bias p = biasedApply bias (break p)

-- | Apply a modifier to the whole list, try to keep the cursor in the same place. If the 'StepList'
-- shrinks below where the cursor was, the cursor is placed at the end of the 'StepList'.
slMapAll :: ([a] -> [b]) -> StepList a -> StepList b
slMapAll fn (StepList cur _ left right) =
  let (left', right') = splitAt cur (fn $ reverse left ++ right)
      newlen = length left' + length right'
  in StepList (min cur newlen) newlen (reverse left') right'