{-# LANGUAGE GeneralizedNewtypeDeriving, DeriveDataTypeable, ScopedTypeVariables #-} -- | This module defines a list type and operations on it; it further -- provides functions which write in and out the list. -- The goal is to make it easy for the user to store a large number of text buffers -- and cycle among them, making edits as she goes. The idea is inspired by -- \"incremental reading\", see . module Yi.IReader where import Control.Lens import Control.Exception import Control.Monad import Data.Binary import Data.Sequence as S import Data.Typeable import Data.Default import qualified Data.ByteString.Char8 as B (pack, unpack, readFile, ByteString) import qualified Data.ByteString.Lazy.Char8 as BL (fromChunks) import Yi.Buffer.HighLevel (replaceBufferContent, topB) import Yi.Buffer.Misc (bufferDynamicValueA, elemsB) import Yi.Dynamic import Yi.Keymap (withBuffer, YiM) import Yi.Paths (getArticleDbFilename) import Yi.Utils type Article = B.ByteString newtype ArticleDB = ADB { unADB :: Seq Article } deriving (Typeable, Binary) instance Default ArticleDB where def = ADB S.empty instance YiVariable ArticleDB -- | Take an 'ArticleDB', and return the first 'Article' and an ArticleDB - *without* that article. split :: ArticleDB -> (Article, ArticleDB) split (ADB adb) = case viewl adb of EmptyL -> (B.pack "", def) (a :< b) -> (a, ADB b) -- | Get the first article in the list. We use the list to express relative priority; -- the first is the most, the last least. We then just cycle through - every article gets equal time. getLatestArticle :: ArticleDB -> Article getLatestArticle = fst . split -- we only want the article -- | We remove the old first article, and we stick it on the end of the -- list using the presumably modified version. removeSetLast :: ArticleDB -> Article -> ArticleDB removeSetLast adb old = ADB (unADB (snd (split adb)) S.|> old) -- we move the last entry to the entry 'length `div` n'from the beginning; so 'shift 1' would do nothing -- (eg. the last index is 50, 50 `div` 1 == 50, so the item would be moved to where it is) -- 'shift 2' will move it to the middle of the list, though; last index = 50, then 50 `div` 2 will shift -- the item to index 25, and so on down to 50 `div` 50 - the head of the list/Seq. shift :: Int ->ArticleDB -> ArticleDB shift n adb = if n < 2 || lst < 2 then adb else ADB $ (r S.|> lastentry) >< s' where lst = S.length (unADB adb) - 1 (r,s) = S.splitAt (lst `div` n) (unADB adb) (s' :> lastentry) = S.viewr s -- | Insert a new article with top priority (that is, at the front of the list). insertArticle :: ArticleDB -> Article -> ArticleDB insertArticle (ADB adb) new = ADB (new S.<| adb) -- | Serialize given 'ArticleDB' out. writeDB :: ArticleDB -> YiM () writeDB adb = void $ io . join . fmap (`encodeFile` adb) $ getArticleDbFilename -- | Read in database from 'getArticleDbFilename' and then parse it into an 'ArticleDB'. readDB :: YiM ArticleDB readDB = io $ (getArticleDbFilename >>= r) `catch` returnDefault where r = fmap (decode . BL.fromChunks . return) . B.readFile -- We read in with strict bytestrings to guarantee the file is closed, -- and then we convert it to the lazy bytestring data.binary expects. -- This is inefficient, but alas... returnDefault (_ :: SomeException) = return def -- | Returns the database as it exists on the disk, and the current Yi buffer contents. -- Note that the Default typeclass gives us an empty Seq. So first we try the buffer -- state in the hope we can avoid a very expensive read from disk, and if we find nothing -- (that is, if we get an empty Seq), only then do we call 'readDB'. oldDbNewArticle :: YiM (ArticleDB, Article) oldDbNewArticle = do saveddb <- withBuffer $ use bufferDynamicValueA newarticle <-fmap B.pack $ withBuffer elemsB if not $ S.null (unADB saveddb) then return (saveddb, newarticle) else do olddb <- readDB return (olddb, newarticle) -- | Given an 'ArticleDB', dump the scheduled article into the buffer (replacing previous contents). setDisplayedArticle :: ArticleDB -> YiM () setDisplayedArticle newdb = do let next = getLatestArticle newdb withBuffer $ do replaceBufferContent $ B.unpack next topB -- replaceBufferContents moves us -- to bottom? assign bufferDynamicValueA newdb -- | Go to next one. This ignores the buffer, but it doesn't remove anything from the database. -- However, the ordering does change. nextArticle :: YiM () nextArticle = do (oldb,_) <- oldDbNewArticle -- Ignore buffer, just set the first article last let newdb = removeSetLast oldb (getLatestArticle oldb) writeDB newdb setDisplayedArticle newdb -- | Delete current article (the article as in the database), and go to next one. deleteAndNextArticle :: YiM () deleteAndNextArticle = do (oldb,_) <- oldDbNewArticle -- throw away changes, let ndb = ADB $ case viewl (unADB oldb) of -- drop 1st article EmptyL -> empty (_ :< b) -> b writeDB ndb setDisplayedArticle ndb -- | The main action. We fetch the old database, we fetch the modified article from the buffer, -- then we call the function 'updateSetLast' which removes the first article and pushes our modified article -- to the end of the list. saveAndNextArticle :: Int -> YiM () saveAndNextArticle n = do (oldb,newa) <- oldDbNewArticle let newdb = shift n $ removeSetLast oldb newa writeDB newdb setDisplayedArticle newdb -- | Assume the buffer is an entirely new article just imported this second, and save it. -- We don't want to use 'updateSetLast' since that will erase an article. saveAsNewArticle :: YiM () saveAsNewArticle = do oldb <- readDB -- make sure we read from disk - we aren't in iread-mode! (_,newa) <- oldDbNewArticle -- we ignore the fst - the Default is 'empty' let newdb = insertArticle oldb newa writeDB newdb