packages feed

yi-0.3: Yi/Editor.hs

{-# LANGUAGE GeneralizedNewtypeDeriving #-}

-- Copyright (c) 2004-5, 8, Don Stewart - http://www.cse.unsw.edu.au/~dons

-- | The top level editor state, and operations on it.

module Yi.Editor

{-
        -- * Buffer only stuff
        newBufferE,     -- :: String -> String -> YiM ()
        listBuffersE,   -- :: YiM ()
        closeBufferE,   -- :: String -> YiM ()

        -- * Buffer/Window
        closeBufferAndWindowE,
        switchToBufferE,
        switchToBufferOtherWindowE,
        switchToBufferWithNameE,
        nextBufW,       -- :: YiM ()
        prevBufW,       -- :: YiM ()

        -- * Basic registers
        setRegE,        -- :: String -> YiM ()
        getRegE,        -- :: EditorM String

        -- * Dynamically extensible state
        getDynamic,
        setDynamic,

        setWindowStyleE,-- :: UIStyle -> EditorM ()
-}

where

import Yi.Buffer                ( BufferRef, FBuffer (..), BufferM, newB, runBuffer, insertN )
import Yi.Buffer.HighLevel (botB)
import Text.Regex.Posix.Wrap    ( Regex )
import Yi.Style                 ( uiStyle, UIStyle )

import Yi.Debug
import Yi.Monad
import Yi.Accessor
import Yi.Dynamic
import Yi.KillRing
import Yi.Window
import Yi.WindowSet (WindowSet)
import qualified Yi.WindowSet as WS

import Prelude hiding (error)

import Data.List                ( nub )
import qualified Data.Map as M

import Control.Monad.State
import Control.Monad.Writer


-- | The Editor state
data Editor = Editor {
        bufferStack   :: ![BufferRef]               -- ^ Stack of all the buffers. Never empty;
                                                    -- first buffer is the current one.
       ,buffers       :: M.Map BufferRef FBuffer
       ,bufferRefSupply :: BufferRef

       ,windows       :: WindowSet Window

       ,uistyle       :: !UIStyle                   -- ^ ui colours
       ,dynamic       :: DynamicValues              -- ^ dynamic components

       ,windowfill    :: !Char                      -- ^ char to fill empty window space with
       ,tabwidth      :: !Int                       -- ^ width of tabs


       -- consider make the below fields part of dynamic component
       ,statusLine    :: !String
       ,yreg          :: !String                    -- ^ yank register
       ,killring      :: !Killring
       ,regex         :: !(Maybe (String,Regex))    -- ^ most recent regex
    }


bufferRefSupplyA :: Accessor Editor BufferRef
bufferRefSupplyA = Accessor bufferRefSupply (\f e -> e {bufferRefSupply = f (bufferRefSupply e)})

buffersA :: Accessor Editor (M.Map BufferRef FBuffer)
buffersA = Accessor buffers (\f e -> e {buffers = f (buffers e)})

dynamicA :: Accessor Editor DynamicValues
dynamicA = Accessor dynamic (\f e -> e {dynamic = f (dynamic e)})

windowsA :: Accessor Editor (WindowSet Window)
windowsA = Accessor windows (\f e -> e {windows = f (windows e)})

killringA :: Accessor Editor Killring
killringA = Accessor killring (\f e -> e {killring = f (killring e)})


dynA :: Initializable a => Accessor Editor a
dynA = dynamicValueA .> dynamicA

-- | The initial state
emptyEditor :: Editor
emptyEditor = Editor {
        buffers      = M.singleton (bkey buf) buf
       ,windows      = WS.new (dummyWindow $ bkey buf)
       ,bufferStack  = [bkey buf]
       ,bufferRefSupply = 1
       ,windowfill   = ' '
       ,tabwidth     = 8
       ,yreg         = []
       ,regex        = Nothing
       ,uistyle      = Yi.Style.uiStyle
       ,dynamic      = M.empty
       ,statusLine   = ""
       ,killring     = krEmpty
       }
        where buf = newB 0 "*console*" ""

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

runEditor :: EditorM a -> Editor -> (a, Editor)
runEditor = runState . fromEditorM

-- ---------------------------------------------------------------------
-- Buffer operations

newBufferRef :: EditorM BufferRef
newBufferRef = do
  modifyA bufferRefSupplyA (+ 1)
  getA bufferRefSupplyA

-- | Create and fill a new buffer, using contents of string.
stringToNewBuffer :: String -- ^ The buffer name (*not* the associated file)
                  -> String -- ^ The contents with which to populate the buffer
                  -> EditorM BufferRef
stringToNewBuffer nm cs = do
    u <- newBufferRef
    insertBuffer (newB u nm cs)

insertBuffer :: FBuffer -> EditorM BufferRef
insertBuffer b = getsAndModify $
                 \e -> (e { bufferStack = nub $ (bkey b : bufferStack e),
                            buffers = M.insert (bkey b) b (buffers e)
                          }, bkey b)

deleteBuffer :: BufferRef -> EditorM ()
deleteBuffer k = do
  bs <- gets bufferStack
  when (length bs > 1) $ do -- never delete the last buffer.
    modify $ \e -> e { bufferStack = filter (k /=) $ bufferStack e,
                       buffers = M.delete k (buffers e)
                     }

-- | Return the buffers we have
getBuffers :: EditorM [FBuffer]
getBuffers = gets (M.elems . buffers)

-- | Find buffer with this key
findBufferWith :: BufferRef -> Editor -> FBuffer
findBufferWith k e =
    case M.lookup k (buffers e) of
        Just b  -> b
        Nothing -> error "Editor.findBufferWith: no buffer has this key"

-- | Find buffer with this name
findBufferWithName :: String -> Editor -> [BufferRef]
findBufferWithName n e = map bkey $ filter (\b -> name b == n) (M.elems $ buffers e)

-- | Find buffer with given name. Fail if not found.
getBufferWithName :: String -> EditorM BufferRef
getBufferWithName bufName = do
  bs <- gets $ findBufferWithName bufName
  case bs of
    [] -> fail ("Buffer not found: " ++ bufName)
    (b:_) -> return b


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

-- | Return the next buffer
nextBuffer :: EditorM BufferRef
nextBuffer = shiftBuffer 1

-- | Return the prev buffer
prevBuffer :: EditorM BufferRef
prevBuffer = shiftBuffer (negate 1)

-- | Return the buffer using a function applied to the current window's
-- buffer's index.
shiftBuffer :: Int -> EditorM BufferRef
shiftBuffer shift = gets $ \e ->
    let bs  = bufferStack e
        n   = shift `mod` length bs
    in (bs !! n)

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

-- | Perform action with any given buffer
withGivenBuffer0 :: BufferRef -> BufferM a -> EditorM a
withGivenBuffer0 k f = withGivenBufferAndWindow0 (dummyWindow k) k f

-- | Perform action with any given buffer
withGivenBufferAndWindow0 :: Window -> BufferRef -> BufferM a -> EditorM a
withGivenBufferAndWindow0 w k f = getsAndModify $ \e ->
                        let b = findBufferWith k e
                            (v, b') = runBuffer w b f
                        in (e {buffers = M.adjust (const b') k (buffers e)},v)


-- | Perform action with current window's buffer
withBuffer0 :: BufferM a -> EditorM a
withBuffer0 f = do
  w <- getA (WS.currentA .> windowsA)
  withGivenBufferAndWindow0 w (bufkey w) f

-- | Return the current buffer
getBuffer :: EditorM BufferRef
getBuffer = gets (head . bufferStack)

-- | Set the current buffer
setBuffer :: BufferRef -> EditorM BufferRef
setBuffer k = do
  b <- gets $ findBufferWith k
  insertBuffer b -- a bit of a hack.

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

newtype EditorM a = EditorM {fromEditorM :: State Editor a}
    deriving (Monad, MonadState Editor)

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

-- | Find buffer with given name. Raise exception if not found.
getBufferWithName0 :: String -> EditorM BufferRef
getBufferWithName0 bufName = do
  bs <- gets $ findBufferWithName bufName
  case bs of
    [] -> fail ("Buffer not found: " ++ bufName)
    (b:_) -> return b


-- | Set the cmd buffer, and draw message at bottom of screen
printMsg :: String -> EditorM ()
printMsg s = do
  modify $ \e -> e { statusLine = takeWhile (/= '\n') s }
  -- also show in the messages buffer, so we don't loose any message
  [b] <- gets $ findBufferWithName "*messages*"
  withGivenBuffer0 b $ do botB; insertN (s ++ "\n")


-- ---------------------------------------------------------------------
-- registers (TODO these may be redundant now that it is easy to thread
-- state in key bindings, or maybe not.
--

-- | Put string into yank register
setRegE :: String -> EditorM ()
setRegE s = modify $ \e -> e { yreg = s }

-- | Return the contents of the yank register
getRegE :: EditorM String
getRegE = gets $ yreg

-- ---------------------------------------------------------------------
-- | Dynamically-extensible state components.
--
-- These hooks are used by keymaps to store values that result from
-- Actions (i.e. that restult from IO), as opposed to the pure values
-- they generate themselves, and can be stored internally.
--
-- The `dynamic' field is a type-indexed map.
--

-- | Retrieve a value from the extensible state
getDynamic :: Initializable a => EditorM a
getDynamic = getA (dynamicValueA .> dynamicA)

-- | Insert a value into the extensible state, keyed by its type
setDynamic :: Initializable a => a -> EditorM ()
setDynamic x = setA (dynamicValueA .> dynamicA) x

-- | A character to fill blank lines in windows with. Usually '~' for
-- vi-like editors, ' ' for everything else
setWindowFillE :: Char -> EditorM ()
setWindowFillE c = modify $ \e -> e { windowfill = c }

-- | Sets the window style.
setWindowStyleE :: UIStyle -> EditorM ()
setWindowStyleE sty = modify $ \e -> e { uistyle = sty }


-- | Attach the next buffer in the buffer list
-- to the current window.
nextBufW :: EditorM ()
nextBufW = nextBuffer >>= switchToBufferE

-- | edit the previous buffer in the buffer list
prevBufW :: EditorM ()
prevBufW = prevBuffer >>= switchToBufferE

-- | Like fnewE, create a new buffer filled with the String @s@,
-- Open up a new window onto this buffer. Doesn't associate any file
-- with the buffer (unlike fnewE) and so is good for popup internal
-- buffers (like scratch)
newBufferE :: String -> String -> EditorM BufferRef
newBufferE f s = do
    b <- stringToNewBuffer f s
    switchToBufferE b
    return b

-- | Attach the specified buffer to the current window
switchToBufferE :: BufferRef -> EditorM ()
switchToBufferE b = modifyWindows (modifier WS.currentA (\w -> w {bufkey = b}))

-- | Attach the specified buffer to some other window than the current one
switchToBufferOtherWindowE :: BufferRef -> EditorM ()
switchToBufferOtherWindowE b = shiftOtherWindow >> switchToBufferE b

-- | Switch to the buffer specified as parameter. If the buffer name is empty, switch to the next buffer.
switchToBufferWithNameE :: String -> EditorM ()
switchToBufferWithNameE "" = nextBufW
switchToBufferWithNameE bufName = switchToBufferE =<< getBufferWithName bufName

-- | Return a list of all buffers, and their indicies
listBuffersE :: EditorM [(String,Int)]
listBuffersE = do
        bs  <- getBuffers
        return $ zip (map name bs) [0..]

-- | Release resources associated with buffer
-- Note: close the current buffer if the empty string is given
closeBufferE :: String -> EditorM ()
closeBufferE bufName = do
  nextB <- nextBuffer
  b <- getBuffer
  b' <- if null bufName then return b else getBufferWithName bufName
  switchToBufferE nextB
  deleteBuffer b'

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

-- | Close current buffer and window, unless it's the last one.
closeBufferAndWindowE :: EditorM ()
closeBufferAndWindowE = do
  deleteBuffer =<< getBuffer
  tryCloseE

-- | Rotate focus to the next window
nextWinE :: EditorM ()
nextWinE = modifyWindows WS.forward

-- | Rotate focus to the previous window
prevWinE :: EditorM ()
prevWinE = modifyWindows WS.backward

-- | Apply a function to the windowset.
modifyWindows :: (WindowSet Window -> WindowSet Window) -> EditorM ()
modifyWindows f = do
  b <- getsAndModifyA windowsA $ \ws -> let ws' = f ws in (ws', bufkey $ WS.current ws')
  -- TODO: push this fiddling with current buffer into windowsA
  setBuffer b
  return ()

withWindows :: (WindowSet Window -> a) -> EditorM a
withWindows = getsA windowsA

withWindow :: (Window -> a) -> EditorM a
withWindow f = getsA (WS.currentA .> windowsA) f

-- | Split the current window, opening a second window onto current buffer.
splitE :: EditorM ()
splitE = do
  b <- getBuffer
  modifyWindows (WS.add $ dummyWindow b)


-- | Enlarge the current window
enlargeWinE :: EditorM ()
enlargeWinE = error "enlargeWinE: not implemented"

-- | Shrink the current window
shrinkWinE :: EditorM ()
shrinkWinE = error "shrinkWinE: not implemented"


-- | Close the current window, unless it is the last window open.
tryCloseE :: EditorM ()
tryCloseE = modifyWindows WS.delete

-- | Make the current window the only window on the screen
closeOtherE :: EditorM ()
closeOtherE = modifyWindows WS.deleteOthers

-- | Switch focus to some other window. If none is available, create one.
shiftOtherWindow :: EditorM ()
shiftOtherWindow = do
  len <- withWindows WS.size
  when (len == 1) splitE
  nextWinE