brick 0.43 → 0.44
raw patch · 5 files changed
+737/−143 lines, 5 filesdep ~containersdep ~microlensPVP ok
version bump matches the API change (PVP)
Dependency ranges changed: containers, microlens
API changes (from Hackage documentation)
- Brick.Widgets.List: data List n e
- Brick.Widgets.List: instance (GHC.Show.Show e, GHC.Show.Show n) => GHC.Show.Show (Brick.Widgets.List.List n e)
- Brick.Widgets.List: instance Brick.Widgets.Core.Named (Brick.Widgets.List.List n e) n
- Brick.Widgets.List: instance Data.Foldable.Foldable (Brick.Widgets.List.List n)
- Brick.Widgets.List: instance Data.Traversable.Traversable (Brick.Widgets.List.List n)
- Brick.Widgets.List: instance GHC.Base.Functor (Brick.Widgets.List.List n)
- Brick.Widgets.List: instance GHC.Generics.Generic (Brick.Widgets.List.List n e)
+ Brick.Widgets.List: class Reversible t
+ Brick.Widgets.List: class Splittable t
+ Brick.Widgets.List: data GenericList n t e
+ Brick.Widgets.List: instance (GHC.Show.Show n, GHC.Show.Show (t e)) => GHC.Show.Show (Brick.Widgets.List.GenericList n t e)
+ Brick.Widgets.List: instance Brick.Widgets.Core.Named (Brick.Widgets.List.GenericList n t e) n
+ Brick.Widgets.List: instance Brick.Widgets.List.Reversible Data.Sequence.Internal.Seq
+ Brick.Widgets.List: instance Brick.Widgets.List.Reversible Data.Vector.Vector
+ Brick.Widgets.List: instance Brick.Widgets.List.Splittable Data.Sequence.Internal.Seq
+ Brick.Widgets.List: instance Brick.Widgets.List.Splittable Data.Vector.Vector
+ Brick.Widgets.List: instance Data.Foldable.Foldable t => Data.Foldable.Foldable (Brick.Widgets.List.GenericList n t)
+ Brick.Widgets.List: instance Data.Traversable.Traversable t => Data.Traversable.Traversable (Brick.Widgets.List.GenericList n t)
+ Brick.Widgets.List: instance GHC.Base.Functor t => GHC.Base.Functor (Brick.Widgets.List.GenericList n t)
+ Brick.Widgets.List: instance GHC.Generics.Generic (Brick.Widgets.List.GenericList n t e)
+ Brick.Widgets.List: reverse :: Reversible t => t a -> t a
+ Brick.Widgets.List: slice :: Splittable t => Int -> Int -> t a -> t a
+ Brick.Widgets.List: splitAt :: Splittable t => Int -> t a -> (t a, t a)
+ Brick.Widgets.List: type List n e = GenericList n Vector e
- Brick.Types: bordersL :: forall n_aGb3. Lens' (Result n_aGb3) (BorderMap DynBorder)
+ Brick.Types: bordersL :: forall n_aGcQ. Lens' (Result n_aGcQ) (BorderMap DynBorder)
- Brick.Types: cursorLocationL :: forall n_aEPE. Lens' (CursorLocation n_aEPE) Location
+ Brick.Types: cursorLocationL :: forall n_aERr. Lens' (CursorLocation n_aERr) Location
- Brick.Types: cursorLocationNameL :: forall n_aEPE n_aGOf. Lens (CursorLocation n_aEPE) (CursorLocation n_aGOf) (Maybe n_aEPE) (Maybe n_aGOf)
+ Brick.Types: cursorLocationNameL :: forall n_aERr n_aGQ2. Lens (CursorLocation n_aERr) (CursorLocation n_aGQ2) (Maybe n_aERr) (Maybe n_aGQ2)
- Brick.Types: cursorsL :: forall n_aGb3. Lens' (Result n_aGb3) [CursorLocation n_aGb3]
+ Brick.Types: cursorsL :: forall n_aGcQ. Lens' (Result n_aGcQ) [CursorLocation n_aGcQ]
- Brick.Types: extentsL :: forall n_aGb3. Lens' (Result n_aGb3) [Extent n_aGb3]
+ Brick.Types: extentsL :: forall n_aGcQ. Lens' (Result n_aGcQ) [Extent n_aGcQ]
- Brick.Types: imageL :: forall n_aGb3. Lens' (Result n_aGb3) Image
+ Brick.Types: imageL :: forall n_aGcQ. Lens' (Result n_aGcQ) Image
- Brick.Types: visibilityRequestsL :: forall n_aGb3. Lens' (Result n_aGb3) [VisibilityRequest]
+ Brick.Types: visibilityRequestsL :: forall n_aGcQ. Lens' (Result n_aGcQ) [VisibilityRequest]
- Brick.Widgets.Dialog: dialogButtonsL :: forall a_a1g94 a_a1g9L. Lens (Dialog a_a1g94) (Dialog a_a1g9L) [(String, a_a1g94)] [(String, a_a1g9L)]
+ Brick.Widgets.Dialog: dialogButtonsL :: forall a_a1gaR a_a1gby. Lens (Dialog a_a1gaR) (Dialog a_a1gby) [(String, a_a1gaR)] [(String, a_a1gby)]
- Brick.Widgets.Dialog: dialogSelectedIndexL :: forall a_a1g94. Lens' (Dialog a_a1g94) (Maybe Int)
+ Brick.Widgets.Dialog: dialogSelectedIndexL :: forall a_a1gaR. Lens' (Dialog a_a1gaR) (Maybe Int)
- Brick.Widgets.Dialog: dialogTitleL :: forall a_a1g94. Lens' (Dialog a_a1g94) (Maybe String)
+ Brick.Widgets.Dialog: dialogTitleL :: forall a_a1gaR. Lens' (Dialog a_a1gaR) (Maybe String)
- Brick.Widgets.Dialog: dialogWidthL :: forall a_a1g94. Lens' (Dialog a_a1g94) Int
+ Brick.Widgets.Dialog: dialogWidthL :: forall a_a1gaR. Lens' (Dialog a_a1gaR) Int
- Brick.Widgets.Edit: editContentsL :: forall t_a1bcd n_a1bce t_a1bcX. Lens (Editor t_a1bcd n_a1bce) (Editor t_a1bcX n_a1bce) (TextZipper t_a1bcd) (TextZipper t_a1bcX)
+ Brick.Widgets.Edit: editContentsL :: forall t_a1be0 n_a1be1 t_a1beK. Lens (Editor t_a1be0 n_a1be1) (Editor t_a1beK n_a1be1) (TextZipper t_a1be0) (TextZipper t_a1beK)
- Brick.Widgets.FileBrowser: fileBrowserEntryFilterL :: forall n_a1pzz. Lens' (FileBrowser n_a1pzz) (Maybe (FileInfo -> Bool))
+ Brick.Widgets.FileBrowser: fileBrowserEntryFilterL :: forall n_a1qtL. Lens' (FileBrowser n_a1qtL) (Maybe (FileInfo -> Bool))
- Brick.Widgets.FileBrowser: fileBrowserSelectableL :: forall n_a1pzz. Lens' (FileBrowser n_a1pzz) (FileInfo -> Bool)
+ Brick.Widgets.FileBrowser: fileBrowserSelectableL :: forall n_a1qtL. Lens' (FileBrowser n_a1qtL) (FileInfo -> Bool)
- Brick.Widgets.List: handleListEvent :: Ord n => Event -> List n e -> EventM n (List n e)
+ Brick.Widgets.List: handleListEvent :: (Foldable t, Splittable t, Ord n) => Event -> GenericList n t e -> EventM n (GenericList n t e)
- Brick.Widgets.List: handleListEventVi :: Ord n => (Event -> List n e -> EventM n (List n e)) -> Event -> List n e -> EventM n (List n e)
+ Brick.Widgets.List: handleListEventVi :: (Foldable t, Splittable t, Ord n) => (Event -> GenericList n t e -> EventM n (GenericList n t e)) -> Event -> GenericList n t e -> EventM n (GenericList n t e)
- Brick.Widgets.List: list :: n -> Vector e -> Int -> List n e
+ Brick.Widgets.List: list :: Foldable t => n -> t e -> Int -> GenericList n t e
- Brick.Widgets.List: listClear :: List n e -> List n e
+ Brick.Widgets.List: listClear :: Monoid (t e) => GenericList n t e -> GenericList n t e
- Brick.Widgets.List: listElements :: List n e -> Vector e
+ Brick.Widgets.List: listElements :: GenericList n t e -> t e
- Brick.Widgets.List: listElementsL :: forall n_a1l8v e_a1l8w e_a1lln. Lens (List n_a1l8v e_a1l8w) (List n_a1l8v e_a1lln) (Vector e_a1l8w) (Vector e_a1lln)
+ Brick.Widgets.List: listElementsL :: forall n_a1lc8 t_a1lc9 e_a1lca t_a1lr6 e_a1lr7. Lens (GenericList n_a1lc8 t_a1lc9 e_a1lca) (GenericList n_a1lc8 t_a1lr6 e_a1lr7) (t_a1lc9 e_a1lca) (t_a1lr6 e_a1lr7)
- Brick.Widgets.List: listInsert :: Int -> e -> List n e -> List n e
+ Brick.Widgets.List: listInsert :: (Splittable t, Applicative t, Semigroup (t e)) => Int -> e -> GenericList n t e -> GenericList n t e
- Brick.Widgets.List: listItemHeight :: List n e -> Int
+ Brick.Widgets.List: listItemHeight :: GenericList n t e -> Int
- Brick.Widgets.List: listItemHeightL :: forall n_a1l8v e_a1l8w. Lens' (List n_a1l8v e_a1l8w) Int
+ Brick.Widgets.List: listItemHeightL :: forall n_a1lc8 t_a1lc9 e_a1lca. Lens' (GenericList n_a1lc8 t_a1lc9 e_a1lca) Int
- Brick.Widgets.List: listModify :: (e -> e) -> List n e -> List n e
+ Brick.Widgets.List: listModify :: Traversable t => (e -> e) -> GenericList n t e -> GenericList n t e
- Brick.Widgets.List: listMoveBy :: Int -> List n e -> List n e
+ Brick.Widgets.List: listMoveBy :: (Foldable t, Splittable t) => Int -> GenericList n t e -> GenericList n t e
- Brick.Widgets.List: listMoveByPages :: (Ord n, RealFrac m) => m -> List n e -> EventM n (List n e)
+ Brick.Widgets.List: listMoveByPages :: (Foldable t, Splittable t, Ord n, RealFrac m) => m -> GenericList n t e -> EventM n (GenericList n t e)
- Brick.Widgets.List: listMoveDown :: List n e -> List n e
+ Brick.Widgets.List: listMoveDown :: (Foldable t, Splittable t) => GenericList n t e -> GenericList n t e
- Brick.Widgets.List: listMovePageDown :: Ord n => List n e -> EventM n (List n e)
+ Brick.Widgets.List: listMovePageDown :: (Foldable t, Splittable t, Ord n) => GenericList n t e -> EventM n (GenericList n t e)
- Brick.Widgets.List: listMovePageUp :: Ord n => List n e -> EventM n (List n e)
+ Brick.Widgets.List: listMovePageUp :: (Foldable t, Splittable t, Ord n) => GenericList n t e -> EventM n (GenericList n t e)
- Brick.Widgets.List: listMoveTo :: Int -> List n e -> List n e
+ Brick.Widgets.List: listMoveTo :: (Foldable t, Splittable t) => Int -> GenericList n t e -> GenericList n t e
- Brick.Widgets.List: listMoveToElement :: Eq e => e -> List n e -> List n e
+ Brick.Widgets.List: listMoveToElement :: (Eq e, Traversable t) => e -> GenericList n t e -> GenericList n t e
- Brick.Widgets.List: listMoveUp :: List n e -> List n e
+ Brick.Widgets.List: listMoveUp :: (Foldable t, Splittable t) => GenericList n t e -> GenericList n t e
- Brick.Widgets.List: listName :: List n e -> n
+ Brick.Widgets.List: listName :: GenericList n t e -> n
- Brick.Widgets.List: listNameL :: forall n_a1l8v e_a1l8w n_a1llo. Lens (List n_a1l8v e_a1l8w) (List n_a1llo e_a1l8w) n_a1l8v n_a1llo
+ Brick.Widgets.List: listNameL :: forall n_a1lc8 t_a1lc9 e_a1lca n_a1lr8. Lens (GenericList n_a1lc8 t_a1lc9 e_a1lca) (GenericList n_a1lr8 t_a1lc9 e_a1lca) n_a1lc8 n_a1lr8
- Brick.Widgets.List: listRemove :: Int -> List n e -> List n e
+ Brick.Widgets.List: listRemove :: (Splittable t, Foldable t, Semigroup (t e)) => Int -> GenericList n t e -> GenericList n t e
- Brick.Widgets.List: listReplace :: Vector e -> Maybe Int -> List n e -> List n e
+ Brick.Widgets.List: listReplace :: (Foldable t, Splittable t) => t e -> Maybe Int -> GenericList n t e -> GenericList n t e
- Brick.Widgets.List: listReverse :: List n e -> List n e
+ Brick.Widgets.List: listReverse :: (Reversible t, Foldable t) => GenericList n t e -> GenericList n t e
- Brick.Widgets.List: listSelected :: List n e -> Maybe Int
+ Brick.Widgets.List: listSelected :: GenericList n t e -> Maybe Int
- Brick.Widgets.List: listSelectedElement :: List n e -> Maybe (Int, e)
+ Brick.Widgets.List: listSelectedElement :: (Splittable t, Foldable t) => GenericList n t e -> Maybe (Int, e)
- Brick.Widgets.List: listSelectedL :: forall n_a1l8v e_a1l8w. Lens' (List n_a1l8v e_a1l8w) (Maybe Int)
+ Brick.Widgets.List: listSelectedL :: forall n_a1lc8 t_a1lc9 e_a1lca. Lens' (GenericList n_a1lc8 t_a1lc9 e_a1lca) (Maybe Int)
- Brick.Widgets.List: renderList :: (Ord n, Show n) => (Bool -> e -> Widget n) -> Bool -> List n e -> Widget n
+ Brick.Widgets.List: renderList :: (Traversable t, Splittable t, Ord n, Show n) => (Bool -> e -> Widget n) -> Bool -> GenericList n t e -> Widget n
- Brick.Widgets.List: renderListWithIndex :: (Ord n, Show n) => (Int -> Bool -> e -> Widget n) -> Bool -> List n e -> Widget n
+ Brick.Widgets.List: renderListWithIndex :: (Traversable t, Splittable t, Ord n, Show n) => (Int -> Bool -> e -> Widget n) -> Bool -> GenericList n t e -> Widget n
Files
- CHANGELOG.md +14/−0
- brick.cabal +7/−2
- src/Brick/Widgets/List.hs +364/−140
- tests/List.hs +346/−0
- tests/Main.hs +6/−1
CHANGELOG.md view
@@ -2,6 +2,20 @@ Brick changelog --------------- +0.44+---++API changes:+ * The `List` type got its container type generalized thanks to a lot of+ work by Fraser Tweedale. Thanks to this work, the `List` now supports+ both `Data.Vector` and `Data.Sequence` as its container types out of+ the box and can be extended to support other sequence types with some+ simple type class instances. In addition, property tests are provided+ for `List` and its asymptotics are noted in the documentation. Along+ the way, various bugs in some of the list movement functions got+ fixed to bring them in line with the advertised behavior in the+ documentation. Thanks, Fraser!+ 0.43 ----
brick.cabal view
@@ -1,5 +1,5 @@ name: brick-version: 0.43+version: 0.44 synopsis: A declarative terminal user interface library description: Write terminal applications painlessly with 'brick'! You write an@@ -94,7 +94,7 @@ directory >= 1.2.5.0, dlist, filepath,- containers,+ containers >= 0.5.7, microlens >= 0.3.0.0, microlens-th, microlens-mtl,@@ -448,7 +448,12 @@ ghc-options: -Wno-orphans default-language: Haskell2010 main-is: Main.hs+ other-modules: List build-depends: base <=5, brick, containers,+ microlens,+ vector, QuickCheck+ if impl(ghc < 8.0)+ build-depends: semigroups
src/Brick/Widgets/List.hs view
@@ -10,7 +10,8 @@ -- | This module provides a scrollable list type and functions for -- manipulating and rendering it. module Brick.Widgets.List- ( List+ ( GenericList+ , List -- * Constructing a list , list@@ -56,19 +57,31 @@ , listAttr , listSelectedAttr , listSelectedFocusedAttr++ -- * Classes+ , Splittable(..)+ , Reversible(..) ) where +import Prelude hiding (reverse, splitAt)+ #if !MIN_VERSION_base(4,8,0)-import Control.Applicative ((<$>),(<*>),pure)-import Data.Foldable (Foldable)+import Control.Applicative ((<$>), (<*>), pure, (<|>))+import Data.Foldable (Foldable, find, toList) import Data.Traversable (Traversable)-#endif+#else import Control.Applicative ((<|>))+import Data.Foldable (find, toList)+#endif+import Control.Monad.Trans.State (evalState, get, put) -import Lens.Micro ((^.), (&), (.~), (%~), _2)+import Lens.Micro ((^.), (^?), (&), (.~), (%~), _2, _head)+import Data.Functor (($>))+import Data.List.NonEmpty (NonEmpty((:|))) import Data.Maybe (fromMaybe)-import Data.Monoid ((<>))+import Data.Semigroup (Semigroup, (<>), sconcat)+import qualified Data.Sequence as Seq import Graphics.Vty (Event(..), Key(..), Modifier(..)) import qualified Data.Vector as V import GHC.Generics (Generic)@@ -79,67 +92,137 @@ import Brick.Util (clamp) import Brick.AttrMap --- | List state. Lists have an element type 'e' that is the data stored--- by the list. Lists handle the following events by default:+-- | List state. Lists have a container @t@ of element type @e@ that is+-- the data stored by the list. Internally, Lists handle the following+-- events by default: -- -- * Up/down arrow keys: move cursor of selected item -- * Page up / page down keys: move cursor of selected item by one page -- at a time (based on the number of items shown) -- * Home/end keys: move cursor of selected item to beginning or end of -- list-data List n e =- List { listElements :: !(V.Vector e)- -- ^ The list's vector of elements.+--+-- The 'List' type synonym fixes @t@ to 'V.Vector' for compatibility+-- with previous versions of this library.+--+-- For a container type to be usable with 'GenericList', it must have+-- instances of 'Traversable' and 'Splittable'. The following functions+-- impose further constraints:+--+-- * 'listInsert': 'Applicative' and 'Semigroup'+-- * 'listRemove': 'Semigroup'+-- * 'listClear': 'Monoid'+-- * 'listReverse': 'Reversible'+--+data GenericList n t e =+ List { listElements :: !(t e)+ -- ^ The list's sequence of elements. , listSelected :: !(Maybe Int) -- ^ The list's selected element index, if any. , listName :: n -- ^ The list's name. , listItemHeight :: Int- -- ^ The height of the list items.+ -- ^ The height of an individual item in the list. } deriving (Functor, Foldable, Traversable, Show, Generic) -suffixLenses ''List+suffixLenses ''GenericList -instance Named (List n e) n where+-- | An alias for 'GenericList' specialized to use a 'Vector' as its+-- container type.+type List n e = GenericList n V.Vector e++instance Named (GenericList n t e) n where getName = listName -handleListEvent :: (Ord n) => Event -> List n e -> EventM n (List n e)+-- | Ordered container types that can be split at a given index. An+-- instance of this class is required for a container type to be usable+-- with 'GenericList'.+class Splittable t where+ {-# MINIMAL splitAt #-}++ -- | Split at the given index. Equivalent to @(take n xs, drop n xs)@+ -- and therefore total.+ splitAt :: Int -> t a -> (t a, t a)++ -- | Slice the structure. Equivalent to @(take n . drop i) xs@ and+ -- therefore total.+ --+ -- The default implementation applies 'splitAt' two times: first to+ -- drop elements leading up to the slice, and again to drop elements+ -- after the slice.+ slice :: Int {- ^ start index -} -> Int {- ^ length -} -> t a -> t a+ slice i n = fst . splitAt n . snd . splitAt i++-- | /O(1)/ 'splitAt'.+instance Splittable V.Vector where+ splitAt = V.splitAt++-- | /O(log(min(i,n-i)))/ 'splitAt'.+instance Splittable Seq.Seq where+ splitAt = Seq.splitAt++-- | Ordered container types where the order of elements can be+-- reversed. Only required if you want to use 'listReverse'.+class Reversible t where+ {-# MINIMAL reverse #-}+ reverse :: t a -> t a++-- | /O(n)/ 'reverse'+instance Reversible V.Vector where+ reverse = V.reverse++-- | /O(n)/ 'reverse'+instance Reversible Seq.Seq where+ reverse = Seq.reverse++-- | Handle events for list cursor movement. Events handled are:+--+-- * Up (up arrow key)+-- * Down (down arrow key)+-- * Page Up (PgUp)+-- * Page Down (PgDown)+-- * Go to first element (Home)+-- * Go to last element (End)+handleListEvent :: (Foldable t, Splittable t, Ord n)+ => Event+ -> GenericList n t e+ -> EventM n (GenericList n t e) handleListEvent e theList = case e of EvKey KUp [] -> return $ listMoveUp theList EvKey KDown [] -> return $ listMoveDown theList EvKey KHome [] -> return $ listMoveTo 0 theList- EvKey KEnd [] -> return $ listMoveTo (V.length $ listElements theList) theList+ EvKey KEnd [] -> return $ listMoveTo (length $ listElements theList) theList EvKey KPageDown [] -> listMovePageDown theList EvKey KPageUp [] -> listMovePageUp theList _ -> return theList --- | Enable list movement with the vi keys with a fallback if none--- match. Use (handleListEventVi handleListEvent) in place of--- handleListEvent to add the vi keys bindings to the standard ones.+-- | Enable list movement with the vi keys with a fallback handler if+-- none match. Use 'handleListEventVi' 'handleListEvent' in place of+-- 'handleListEvent' to add the vi keys bindings to the standard ones. -- Movements handled include: ----- * Up (k)--- * Down (j)--- * Page Up (Ctrl-b)--- * Page Down (Ctrl-f)--- * Half Page Up (Ctrl-u)+-- * Up (k)+-- * Down (j)+-- * Page Up (Ctrl-b)+-- * Page Down (Ctrl-f)+-- * Half Page Up (Ctrl-u) -- * Half Page Down (Ctrl-d)--- * Top (g)--- * Bottom (G)-handleListEventVi :: (Ord n)- => (Event -> List n e -> EventM n (List n e))+-- * Go to first element (g)+-- * Go to last element (G)+handleListEventVi :: (Foldable t, Splittable t, Ord n)+ => (Event -> GenericList n t e -> EventM n (GenericList n t e)) -- ^ Fallback event handler to use if none of the vi keys -- match. -> Event- -> List n e- -> EventM n (List n e)+ -> GenericList n t e+ -> EventM n (GenericList n t e) handleListEventVi fallback e theList = case e of EvKey (KChar 'k') [] -> return $ listMoveUp theList EvKey (KChar 'j') [] -> return $ listMoveDown theList EvKey (KChar 'g') [] -> return $ listMoveTo 0 theList- EvKey (KChar 'G') [] -> return $ listMoveTo (V.length $ listElements theList) theList+ EvKey (KChar 'G') [] -> return $ listMoveTo (length $ listElements theList) theList EvKey (KChar 'f') [MCtrl] -> listMovePageDown theList EvKey (KChar 'b') [MCtrl] -> listMovePageUp theList EvKey (KChar 'd') [MCtrl] -> listMoveByPages (0.5::Double) theList@@ -160,65 +243,87 @@ listSelectedFocusedAttr :: AttrName listSelectedFocusedAttr = listSelectedAttr <> "focused" --- | Construct a list in terms of an element type 'e'.-list :: n+-- | Construct a list in terms of container 't' with element type 'e'.+list :: (Foldable t)+ => n -- ^ The list name (must be unique)- -> V.Vector e+ -> t e -- ^ The initial list contents -> Int -- ^ The list item height in rows (all list item widgets must be- -- this high)- -> List n e+ -- this high).+ -> GenericList n t e list name es h =- let selIndex = if V.null es then Nothing else Just 0+ let selIndex = if null es then Nothing else Just 0 safeHeight = max 1 h in List es selIndex name safeHeight --- | Turn a list state value into a widget given an item drawing--- function.-renderList :: (Ord n, Show n)+-- | Render a list using the specified item drawing function.+--+-- Evaluates the underlying container up to, and a bit beyond, the+-- selected element. The exact amount depends on available height+-- for drawing and 'listItemHeight'. At most, it will evaluate up to+-- element @(i + h + 1)@ where @i@ is the selected index and @h@ is the+-- available height.+renderList :: (Traversable t, Splittable t, Ord n, Show n) => (Bool -> e -> Widget n) -- ^ Rendering function, True for the selected element -> Bool -- ^ Whether the list has focus- -> List n e+ -> GenericList n t e -- ^ The List to be rendered -> Widget n -- ^ rendered widget renderList drawElem = renderListWithIndex $ const drawElem --- | Like 'renderList', except the render function is also provided--- with the index of each element.-renderListWithIndex :: (Ord n, Show n)- => (Int -> Bool -> e -> Widget n)- -- ^ Rendering function, taking index, and True for the- -- selected element- -> Bool- -- ^ Whether the list has focus- -> List n e- -- ^ The List to be rendered- -> Widget n- -- ^ rendered widget+-- | Like 'renderList', except the render function is also provided with+-- the index of each element.+--+-- Has the same evaluation characteristics as 'renderList'.+renderListWithIndex :: (Traversable t, Splittable t, Ord n, Show n)+ => (Int -> Bool -> e -> Widget n)+ -- ^ Rendering function, taking index, and True for+ -- the selected element+ -> Bool+ -- ^ Whether the list has focus+ -> GenericList n t e+ -- ^ The List to be rendered+ -> Widget n+ -- ^ rendered widget renderListWithIndex drawElem foc l = withDefAttr listAttr $ drawListElements foc l drawElem -drawListElements :: (Ord n, Show n) => Bool -> List n e -> (Int -> Bool -> e -> Widget n) -> Widget n+imap :: (Traversable t) => (Int -> a -> b) -> t a -> t b+imap f xs =+ let act = traverse (\a -> get >>= \i -> put (i + 1) $> f i a) xs+ in evalState act 0++-- | Draws the list elements.+--+-- Evaluates the underlying container up to, and a bit beyond, the+-- selected element. The exact amount depends on available height+-- for drawing and 'listItemHeight'. At most, it will evaluate up to+-- element @(i + h + 1)@ where @i@ is the selected index and @h@ is the+-- available height.+drawListElements :: (Traversable t, Splittable t, Ord n, Show n)+ => Bool+ -> GenericList n t e+ -> (Int -> Bool -> e -> Widget n)+ -> Widget n drawListElements foc l drawElem = Widget Greedy Greedy $ do c <- getContext - let es = if num <= 0- then V.empty- else V.slice start num (l^.listElementsL)+ -- Take (numPerHeight * 2) elements, or whatever is left+ let es = slice start (numPerHeight * 2) (l^.listElementsL) idx = fromMaybe 0 (l^.listSelectedL) start = max 0 $ idx - numPerHeight + 1- num = min (numPerHeight * 2) (V.length (l^.listElementsL) - start) - -- The number of items to show is the available height divided by- -- the item height...+ -- The number of items to show is the available height+ -- divided by the item height... initialNumPerHeight = (c^.availHeightL) `div` (l^.listItemHeightL) -- ... but if the available height leaves a remainder of -- an item height then we need to ensure that we render an@@ -235,7 +340,7 @@ off = start * (l^.listItemHeightL) - drawnElements = flip V.imap es $ \i e ->+ drawnElements = flip imap es $ \i e -> let j = i + start isSelected = Just j == l^.listSelectedL elemWidget = drawElem j isSelected e@@ -249,141 +354,260 @@ render $ viewport (l^.listNameL) Vertical $ translateBy (Location (0, off)) $- vBox $ V.toList drawnElements+ vBox $ toList drawnElements -- | Insert an item into a list at the specified position.-listInsert :: Int+--+-- Complexity: the worse of 'splitAt' and `<>` for the container type.+--+-- @+-- listInsert for 'List': O(n)+-- listInsert for 'Seq.Seq': O(log(min(i, length n - i)))+-- @+listInsert :: (Splittable t, Applicative t, Semigroup (t e))+ => Int -- ^ The position at which to insert (0 <= i <= size) -> e -- ^ The element to insert- -> List n e- -> List n e+ -> GenericList n t e+ -> GenericList n t e listInsert pos e l =- let safePos = clamp 0 (V.length es) pos- es = l^.listElementsL+ let es = l^.listElementsL newSel = case l^.listSelectedL of- Nothing -> 0- Just s -> if safePos <= s- then s + 1- else s- (front, back) = V.splitAt safePos es+ Nothing -> 0+ Just s -> if pos <= s+ then s + 1+ else s+ (front, back) = splitAt pos es in l & listSelectedL .~ Just newSel- & listElementsL .~ (front V.++ (e `V.cons` back))+ & listElementsL .~ sconcat (front :| [pure e, back]) -- | Remove an element from a list at the specified position.-listRemove :: Int- -- ^ The position at which to remove an element (0 <= i < size)- -> List n e- -> List n e-listRemove pos l | V.null (l^.listElementsL) = l- | pos /= clamp 0 (V.length (l^.listElementsL) - 1) pos = l+--+-- Applies 'splitAt' two times: first to split the structure at the+-- given position, and again to remove the first element from the tail.+-- Consider the asymptotics of `splitAt` for the container type when+-- using this function.+--+-- Complexity: the worse of 'splitAt' and `<>` for the container type.+--+-- @+-- listRemove for 'List': O(n)+-- listRemove for 'Seq.Seq': O(log(min(i, n - i)))+-- @+listRemove :: (Splittable t, Foldable t, Semigroup (t e))+ => Int+ -- ^ The position at which to remove an element (0 <= i <+ -- size)+ -> GenericList n t e+ -> GenericList n t e+listRemove pos l | null (l^.listElementsL) = l+ | pos /= splitClamp l pos = l | otherwise = let newSel = case l^.listSelectedL of- Nothing -> 0- Just s | pos == 0 -> 0- | pos == s -> pos - 1- | pos < s -> s - 1- | otherwise -> s- (front, back) = V.splitAt pos es- es' = front V.++ V.tail back+ Nothing -> 0+ Just s | pos == 0 -> 0+ | pos == s -> pos - 1+ | pos < s -> s - 1+ | otherwise -> s+ (front, rest) = splitAt pos es+ (_, back) = splitAt 1 rest+ es' = front <> back es = l^.listElementsL- in l & listSelectedL .~ (if V.null es' then Nothing else Just newSel)+ in l & listSelectedL .~ (if null es' then Nothing else Just newSel) & listElementsL .~ es' -- | Replace the contents of a list with a new set of elements and--- update the new selected index. If the list is empty, empty selection is used--- instead. Otherwise, if the specified selected index (via 'Just') is not in--- the list bounds, zero is used instead.-listReplace :: V.Vector e -> Maybe Int -> List n e -> List n e+-- update the new selected index. If the list is empty, empty selection+-- is used instead. Otherwise, if the specified selected index (via+-- 'Just') is not in the list bounds, zero is used instead.+--+-- Complexity: same as 'splitAt' for the container type.+listReplace :: (Foldable t, Splittable t)+ => t e+ -> Maybe Int+ -> GenericList n t e+ -> GenericList n t e listReplace es idx l =- let newSel = if V.null es then Nothing else clamp 0 (V.length es - 1) <$> idx- in l & listSelectedL .~ newSel- & listElementsL .~ es+ let l' = l & listElementsL .~ es+ newSel = if null es then Nothing else inBoundsOrZero <$> idx+ inBoundsOrZero i+ | i == splitClamp l' i = i+ | otherwise = 0+ in l' & listSelectedL .~ newSel -- | Move the list selected index up by one. (Moves the cursor up, -- subtracts one from the index.)-listMoveUp :: List n e -> List n e+listMoveUp :: (Foldable t, Splittable t)+ => GenericList n t e+ -> GenericList n t e listMoveUp = listMoveBy (-1) -- | Move the list selected index up by one page.-listMovePageUp :: (Ord n) => List n e -> EventM n (List n e)-listMovePageUp theList = listMoveByPages (-1::Double) theList+listMovePageUp+ :: (Foldable t, Splittable t, Ord n)+ => GenericList n t e -> EventM n (GenericList n t e)+listMovePageUp = listMoveByPages (-1::Double) -- | Move the list selected index down by one. (Moves the cursor down, -- adds one to the index.)-listMoveDown :: List n e -> List n e+listMoveDown :: (Foldable t, Splittable t)+ => GenericList n t e+ -> GenericList n t e listMoveDown = listMoveBy 1 -- | Move the list selected index down by one page.-listMovePageDown :: (Ord n) => List n e -> EventM n (List n e)-listMovePageDown theList = listMoveByPages (1::Double) theList+listMovePageDown :: (Foldable t, Splittable t, Ord n)+ => GenericList n t e+ -> EventM n (GenericList n t e)+listMovePageDown = listMoveByPages (1::Double) -- | Move the list selected index by some (fractional) number of pages.-listMoveByPages :: (Ord n, RealFrac m) => m -> List n e -> EventM n (List n e)+listMoveByPages :: (Foldable t, Splittable t, Ord n, RealFrac m)+ => m+ -> GenericList n t e+ -> EventM n (GenericList n t e) listMoveByPages pages theList = do v <- lookupViewport (theList^.listNameL) case v of Nothing -> return theList- Just vp -> let- nElems = round $ pages * (fromIntegral $ vp^.vpSize._2) / (fromIntegral $ theList^.listItemHeightL)- in+ Just vp -> do+ let nElems = round $ pages * fromIntegral (vp^.vpSize._2) /+ fromIntegral (theList^.listItemHeightL) return $ listMoveBy nElems theList --- | Move the list selected index. If the index is `Just x`, adjust by the--- specified amount; if it is `Nothing` (i.e. there is no selection) and the--- direction is positive, set to `Just 0` (first element), otherwise set to--- `Just (length - 1)` (last element). Subject to validation.-listMoveBy :: Int -> List n e -> List n e+-- | Move the list selected index.+--+-- If the current selection is @Just x@, the selection is adjusted by+-- the specified amount. The value is clamped to the extents of the list+-- (i.e. the selection does not "wrap").+--+-- If the current selection is @Nothing@ (i.e. there is no selection)+-- and the direction is positive, set to @Just 0@ (first element),+-- otherwise set to @Just (length - 1)@ (last element).+--+-- Complexity: same as 'splitAt' for the container type.+--+-- @+-- listMoveBy for 'List': O(1)+-- listMoveBy for 'Seq.Seq': O(log(min(i,n-i)))+-- @+listMoveBy :: (Foldable t, Splittable t)+ => Int+ -> GenericList n t e+ -> GenericList n t e listMoveBy amt l =- let current = case l^.listSelectedL of- Nothing- | amt > 0 -> Just 0- | otherwise -> Just (V.length (l^.listElementsL) - 1)- cur -> cur- clamp' a b c- | a <= b = Just (clamp a b c)- | otherwise = Nothing- newSel = clamp' 0 (V.length (l^.listElementsL) - 1) =<< (amt +) <$> current- in l & listSelectedL .~ newSel+ let target = case l ^. listSelectedL of+ Nothing+ | amt > 0 -> 0+ | otherwise -> length (l ^. listElementsL) - 1+ Just i -> max 0 (amt + i) -- don't be negative+ in listMoveTo target l -- | Set the selected index for a list to the specified index, subject -- to validation.-listMoveTo :: Int -> List n e -> List n e+--+-- If @pos >= 0@, indexes from the start of the list (which gets+-- evaluated up to the target index)+--+-- If @pos < 0@, indexes from the end of the list (which evalutes+-- 'length' of the list).+--+-- Complexity: same as 'splitAt' for the container type.+--+-- @+-- listMoveTo for 'List': O(1)+-- listMoveTo for 'Seq.Seq': O(log(min(i,n-i)))+-- @+listMoveTo :: (Foldable t, Splittable t)+ => Int+ -> GenericList n t e+ -> GenericList n t e listMoveTo pos l =- let len = V.length (l^.listElementsL)- newSel = clamp 0 (len - 1) $ if pos < 0 then len - pos else pos- in l & listSelectedL .~ if len > 0+ let len = length (l ^. listElementsL)+ i = if pos < 0 then len - pos else pos+ newSel = splitClamp l i+ in l & listSelectedL .~ if not (null (l ^. listElementsL)) then Just newSel else Nothing +-- | Split-based clamp that avoids evaluating 'length' of the structure+-- (unless the structure is already fully evaluated).+splitClamp :: (Foldable t, Splittable t) => GenericList n t e -> Int -> Int+splitClamp l i =+ let (_, t) = splitAt i (l ^. listElementsL) -- split at i+ in+ -- If the tail is empty, then the requested index is not in the+ -- list. And because we have already seen the end of the list,+ -- using 'length' will not force unwanted computation.+ --+ -- Otherwise if tail is not empty, then we already know that i+ -- is in the list, so we don't need to know the length+ clamp 0 (if null t then length (l ^. listElementsL) - 1 else i) i+ -- | Set the selected index for a list to the index of the specified -- element if it is in the list, or leave the list unmodified otherwise.-listMoveToElement :: (Eq e) => e -> List n e -> List n e+--+-- Complexity: same as 'traverse' for the container type (typically+-- /O(n)/).+listMoveToElement :: (Eq e, Traversable t)+ => e+ -> GenericList n t e+ -> GenericList n t e listMoveToElement e l =- let i = V.elemIndex e (l^.listElementsL)+ let i = fmap fst $ find ((== e) . snd) $ imap (,) (l^.listElementsL) in l & listSelectedL %~ (i <|>) -- | Return a list's selected element, if any.-listSelectedElement :: List n e -> Maybe (Int, e)+--+-- Only evaluates as much of the container as needed.+--+-- Complexity: same as 'splitAt' for the container type.+--+-- @+-- listSelectedElement for 'List': O(1)+-- listSelectedElement for 'Seq.Seq': O(log(min(i, n - i)))+-- @+listSelectedElement :: (Splittable t, Foldable t)+ => GenericList n t e+ -> Maybe (Int, e) listSelectedElement l = do- sel <- l^.listSelectedL- return (sel, (l^.listElementsL) V.! sel)+ sel <- l^.listSelectedL+ let (_, xs) = splitAt sel (l ^. listElementsL)+ (sel,) <$> toList xs ^? _head -- | Remove all elements from the list and clear the selection.-listClear :: List n e -> List n e-listClear l = l & listElementsL .~ V.empty & listSelectedL .~ Nothing+--+-- /O(1)/+listClear :: (Monoid (t e)) => GenericList n t e -> GenericList n t e+listClear l = l & listElementsL .~ mempty & listSelectedL .~ Nothing --- | Reverse the list. The element selected before the reversal will+-- | Reverse the list. The element selected before the reversal will -- again be the selected one.-listReverse :: List n e -> List n e-listReverse theList = theList & listElementsL %~ V.reverse & listSelectedL .~ newSel- where n = V.length (listElements theList)- newSel = (-) <$> pure (n-1) <*> listSelected theList+--+-- Complexity: same as 'reverse' for the container type.+--+-- @+-- listReverse for 'List': O(n)+-- listReverse for 'Seq.Seq': O(n)+-- @+listReverse :: (Reversible t, Foldable t)+ => GenericList n t e+ -> GenericList n t e+listReverse l =+ l & listElementsL %~ reverse+ & listSelectedL %~ fmap (length (l ^. listElementsL) - 1 -) -- | Apply a function to the selected element. If no element is selected -- the list is not modified.-listModify :: (e -> e) -> List n e -> List n e-listModify f l = case listSelectedElement l of- Nothing -> l- Just (n,e) -> let es = V.update (l^.listElementsL) (return (n, f e))- in listReplace es (Just n) l+--+-- Complexity: same as 'traverse' for the container type (typically+-- /O(n)/).+listModify :: (Traversable t)+ => (e -> e)+ -> GenericList n t e+ -> GenericList n t e+listModify f l =+ case l ^. listSelectedL of+ Nothing -> l+ Just j -> l & listElementsL %~ imap (\i e -> if i == j then f e else e)
+ tests/List.hs view
@@ -0,0 +1,346 @@+{-# LANGUAGE DeriveTraversable #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeSynonymInstances #-}++module List+ (+ main+ ) where++import Prelude hiding (reverse, splitAt)++import Data.Function (on)+import qualified Data.List+import Data.Maybe (isNothing)+import Data.Monoid (Endo(..))+import Data.Proxy+import Data.Semigroup (Semigroup((<>)))++import qualified Data.Sequence as Seq+import qualified Data.Vector as V+import Lens.Micro+import Test.QuickCheck++import Brick.Util (clamp)+import Brick.Widgets.List++instance (Arbitrary n, Arbitrary a) => Arbitrary (List n a) where+ arbitrary = list <$> arbitrary <*> (V.fromList <$> arbitrary) <*> pure 1+++-- List move operations that never modify the underlying list+data ListMoveOp a+ = MoveUp+ | MoveDown+ | MoveBy Int+ | MoveTo Int+ | MoveToElement a+ deriving (Show)++instance Arbitrary a => Arbitrary (ListMoveOp a) where+ arbitrary = oneof+ [ pure MoveUp+ , pure MoveDown+ , MoveBy <$> arbitrary+ , MoveTo <$> arbitrary+ , MoveToElement <$> arbitrary+ ]++-- List operations. We don't have "page"-based movement operations+-- because these depend on render context (i.e. effect in EventM)+data ListOp a+ = Insert Int a+ | Remove Int+ | Replace Int [a]+ | Clear+ | Reverse+ | ListMoveOp (ListMoveOp a)+ deriving (Show)++instance Arbitrary a => Arbitrary (ListOp a) where+ arbitrary = frequency+ [ (1, Insert <$> arbitrary <*> arbitrary)+ , (1, Remove <$> arbitrary)+ , (1, Replace <$> arbitrary <*> arbitrary)+ , (1, pure Clear)+ , (1, pure Reverse)+ , (5, arbitrary)+ ]++-- Turn a ListOp into a List endomorphism+op :: Eq a => ListOp a -> List n a -> List n a+op (Insert i a) = listInsert i a+op (Remove i) = listRemove i+op (Replace i xs) =+ -- avoid setting index to Nothing+ listReplace (V.fromList xs) (Just i)+op Clear = listClear+op Reverse = listReverse+op (ListMoveOp mo) = moveOp mo++-- Turn a ListMoveOp into a List endomorphism+moveOp :: (Eq a) => ListMoveOp a -> List n a -> List n a+moveOp MoveUp = listMoveUp+moveOp MoveDown = listMoveDown+moveOp (MoveBy n) = listMoveBy n+moveOp (MoveTo n) = listMoveTo n+moveOp (MoveToElement a) = listMoveToElement a++applyListOps+ :: (Foldable t)+ => (op a -> List n a -> List n a) -> t (op a) -> List n a -> List n a+applyListOps f = appEndo . foldMap (Endo . f)++-- list operations keep the selected index in bounds+prop_listOpsMaintainSelectedValid+ :: (Eq a) => [ListOp a] -> List n a -> Bool+prop_listOpsMaintainSelectedValid ops l =+ let l' = applyListOps op ops l+ in+ case l' ^. listSelectedL of+ -- either there is no selection and list is empty+ Nothing -> null (l' ^. listElementsL)+ -- or the selected index is valid+ Just i -> i >= 0 && i < length (l' ^. listElementsL)++-- reversing a list keeps the selected element the same+prop_reverseMaintainsSelectedElement+ :: (Eq a) => [ListOp a] -> List n a -> Bool+prop_reverseMaintainsSelectedElement ops l =+ let+ -- apply some random list ops to (probably) set a selected element+ l' = applyListOps op ops l+ l'' = listReverse l'+ in+ fmap snd (listSelectedElement l') == fmap snd (listSelectedElement l'')++-- reversing maintains size of list+prop_reverseMaintainsSizeOfList :: List n a -> Bool+prop_reverseMaintainsSizeOfList l =+ length (l ^. listElementsL) == length (listReverse l ^. listElementsL)++-- an inserted element may always be found at the given index+-- (when target index is clamped to 0 <= n <= len)+prop_insert :: (Eq a) => Int -> a -> List n a -> Bool+prop_insert i a l =+ let+ l' = listInsert i a l+ i' = clamp 0 (length (l ^. listElementsL)) i+ in+ listSelectedElement (listMoveTo i' l') == Just (i', a)++-- inserting anywhere always increases size of list by 1+prop_insertSize :: (Eq a) => Int -> a -> List n a -> Bool+prop_insertSize i a l =+ let+ l' = listInsert i a l+ in+ length (l' ^. listElementsL) == length (l ^. listElementsL) + 1++-- inserting an element and moving to it always succeeds and+-- the selected element is the one we inserted.+--+-- The index is not necessarily the index we inserted at, because+-- the element could be present in the original list. So we don't+-- check that.+--+prop_insertMoveTo :: (Eq a) => [ListOp a] -> List n a -> Int -> a -> Bool+prop_insertMoveTo ops l i a =+ let+ l' = listInsert i a (applyListOps op ops l)+ sel = listSelectedElement (listMoveToElement a l')+ in+ fmap snd sel == Just a++-- inserting then deleting always yields a list with the original elems+prop_insertRemove :: (Eq a) => Int -> a -> List n a -> Bool+prop_insertRemove i a l =+ let+ i' = clamp 0 (length (l ^. listElementsL)) i+ l' = listInsert i' a l -- pre-clamped+ l'' = listRemove i' l'+ in+ l'' ^. listElementsL == l ^. listElementsL++-- deleting in-bounds always reduces size of list by 1+-- deleting out-of-bounds never changes list size+prop_remove :: Int -> List n a -> Bool+prop_remove i l =+ let+ len = length (l ^. listElementsL)+ i' = clamp 0 (len - 1) i+ test+ | len > 0 && i == i' = (== len - 1) -- i is in bounds+ | otherwise = (== len) -- i is out of bounds+ in+ test (length (listRemove i l ^. listElementsL))++-- deleting an element and re-inserting it at same position+-- gives the original list elements+prop_removeInsert :: (Eq a) => Int -> List n a -> Bool+prop_removeInsert i l =+ let+ sel = listSelectedElement (listMoveTo i l)+ l' = maybe id (\(i', a) -> listInsert i' a . listRemove i') sel l+ in+ l' ^. listElementsL == l ^. listElementsL++converge :: (a -> a -> Bool) -> (a -> a) -> a -> a+converge test f a+ | test (f a) a = a+ | otherwise = converge test f (f a)++-- listMoveUp always reaches 0 (or list is empty)+prop_moveUp :: (Eq a) => [ListOp a] -> List n a -> Bool+prop_moveUp ops l =+ let+ l' = applyListOps op ops l+ l'' = converge ((==) `on` (^. listSelectedL)) listMoveUp l'+ len = length (l'' ^. listElementsL)+ in+ maybe (len == 0) (== 0) (l'' ^. listSelectedL)++-- listMoveDown always reaches end of list (or list is empty)+prop_moveDown :: (Eq a) => [ListOp a] -> List n a -> Bool+prop_moveDown ops l =+ let+ l' = applyListOps op ops l+ l'' = converge ((==) `on` (^. listSelectedL)) listMoveDown l'+ len = length (l'' ^. listElementsL)+ in+ maybe (len == 0) (== len - 1) (l'' ^. listSelectedL)++-- move ops never change the list+prop_moveOpsNeverChangeList :: (Eq a) => [ListMoveOp a] -> List n a -> Bool+prop_moveOpsNeverChangeList ops l =+ let+ l' = applyListOps moveOp ops l+ in+ l' ^. listElementsL == l ^. listElementsL++-- If the list is empty, empty selection is used.+-- Otherwise, if the specified selected index is not in list bounds,+-- zero is used instead.+prop_replaceSetIndex+ :: (Eq a)+ => [ListOp a] -> List n a -> [a] -> Int -> Bool+prop_replaceSetIndex ops l xs i =+ let+ v = V.fromList xs+ l' = applyListOps op ops l+ l'' = listReplace v (Just i) l'+ i' = clamp 0 (length v - 1) i+ inBounds = i == i'+ in+ l'' ^. listSelectedL == case (null v, inBounds) of+ (True, _) -> Nothing+ (False, True) -> Just i+ (False, False) -> Just 0++-- Replacing with no index always clears the index+prop_replaceNoIndex :: (Eq a) => [ListOp a] -> List n a -> [a] -> Bool+prop_replaceNoIndex ops l xs =+ let+ v = V.fromList xs+ l' = applyListOps op ops l+ in+ isNothing (listReplace v Nothing l' ^. listSelectedL)++-- | Move the list selected index. If the index is `Just x`, adjust by the+-- specified amount; if it is `Nothing` (i.e. there is no selection) and the+-- direction is positive, set to `Just 0` (first element), otherwise set to+-- `Just (length - 1)` (last element). Subject to validation.+prop_moveByWhenNoSelection :: List n a -> Int -> Property+prop_moveByWhenNoSelection l amt =+ let+ l' = l & listSelectedL .~ Nothing+ len = length (l ^. listElementsL)+ expected = if amt > 0 then 0 else len - 1+ in+ len > 0 ==> listMoveBy amt l' ^. listSelectedL == Just expected+++splitAtLength :: (Foldable t, Splittable t) => t a -> Int -> Bool+splitAtLength l i =+ let+ len = length l+ (h, t) = splitAt i l+ in+ length h + length t == len+ && length h == clamp 0 len i++splitAtAppend+ :: (Splittable t, Semigroup (t a), Eq (t a))+ => t a -> Int -> Bool+splitAtAppend l i = uncurry (<>) (splitAt i l) == l++prop_splitAtLength_Vector :: [a] -> Int -> Bool+prop_splitAtLength_Vector = splitAtLength . V.fromList++prop_splitAtAppend_Vector :: (Eq a) => [a] -> Int -> Bool+prop_splitAtAppend_Vector = splitAtAppend . V.fromList++prop_splitAtLength_Seq :: [a] -> Int -> Bool+prop_splitAtLength_Seq = splitAtLength . Seq.fromList++prop_splitAtAppend_Seq :: (Eq a) => [a] -> Int -> Bool+prop_splitAtAppend_Seq = splitAtAppend . Seq.fromList+++reverseSingleton+ :: forall t a. (Reversible t, Applicative t, Eq (t a))+ => Proxy t -> a -> Bool+reverseSingleton _ a =+ let l = pure a :: t a+ in reverse l == l++reverseAppend+ :: (Reversible t, Semigroup (t a), Eq (t a))+ => t a -> t a -> Bool+reverseAppend l1 l2 =+ reverse (l1 <> l2) == reverse l2 <> reverse l1++prop_reverseSingleton_Vector :: (Eq a) => a -> Bool+prop_reverseSingleton_Vector = reverseSingleton (Proxy :: Proxy V.Vector)++prop_reverseAppend_Vector :: (Eq a) => [a] -> [a] -> Bool+prop_reverseAppend_Vector l1 l2 =+ reverseAppend (V.fromList l1) (V.fromList l2)++prop_reverseSingleton_Seq :: (Eq a) => a -> Bool+prop_reverseSingleton_Seq = reverseSingleton (Proxy :: Proxy Seq.Seq)++prop_reverseAppend_Seq :: (Eq a) => [a] -> [a] -> Bool+prop_reverseAppend_Seq l1 l2 =+ reverseAppend (Seq.fromList l1) (Seq.fromList l2)++++-- Laziness tests. Here we create a custom container type+-- that we use to ensure certain operations do not cause the+-- whole container to be evaulated.+--+newtype L a = L [a]+ deriving (Functor, Foldable, Traversable)++instance Splittable L where+ splitAt i (L xs) = over both L (Data.List.splitAt i xs)++-- moveBy positive amount does not evaluate 'length'+prop_moveByPosLazy :: Bool+prop_moveByPosLazy =+ let+ v = L (1:2:3:4:undefined) :: L Int+ l = list () v 1+ l' = listMoveBy 1 l+ in+ l' ^. listSelectedL == Just 1+++return []++main :: IO Bool+main = $quickCheckAll
tests/Main.hs view
@@ -3,6 +3,7 @@ import Control.Applicative import Data.Bool (bool)+import Data.Traversable (sequenceA) import System.Exit (exitFailure, exitSuccess) import Data.IMap (IMap, Run(Run))@@ -11,6 +12,8 @@ import qualified Data.IMap as IMap import qualified Data.IntMap as IntMap +import qualified List+ instance Arbitrary v => Arbitrary (Run v) where arbitrary = liftA2 (\(Positive n) -> Run n) arbitrary arbitrary @@ -107,4 +110,6 @@ return [] main :: IO ()-main = $quickCheckAll >>= bool exitFailure exitSuccess+main =+ (all id <$> sequenceA [$quickCheckAll, List.main])+ >>= bool exitFailure exitSuccess