packages feed

yi-0.3: Yi/Keymap/Vim.hs

{-# LANGUAGE FlexibleContexts, DeriveDataTypeable #-}

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


-- | Vim keymap for Yi. Emulates vim :set nocompatible
module Yi.Keymap.Vim ( keymap, VimMode, viWrite ) where

import Yi.Yi
import Yi.Style as Style
import Prelude       hiding ( any, error )

import Data.Char
import Data.Maybe           ( fromMaybe )
import Data.Dynamic
import qualified Data.Map as M

import Control.Exception    ( ioErrors, try, evaluate )
import Control.Monad.State

import Yi.Editor
import Yi.Accessor
import Yi.History
import Yi.Buffer
import Yi.Debug

import Yi.Indent

import Yi.Keymap.Emacs.Utils (completeFileName,completeBufferName)
import Yi.MiniBuffer
import Yi.TextCompletion
import Yi.Syntax.Table (highlighters)

--
-- What's missing?
--   fancier :s//
--   '.'
--   movement parameterised \> \<
--

{-
  For now we just make the selected style the same as the
  modeline_focused style... Just because i'm not good with
  styles yet - Jim
-}
defaultVimUiStyle :: Style.UIStyle
defaultVimUiStyle = Style.uiStyle { selected = Style.modeline_focused Style.uiStyle}

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

type VimMode = VimProc ()

type VimProc a = (Interact Char) a


-- | Top level
keymap :: Keymap
keymap = do write $ do setWindowFillE '~'
                       withBuffer0 unsetMarkB
                       setWindowStyleE defaultVimUiStyle
            runVim cmd_mode

runVim :: VimMode -> Keymap
runVim = comap eventToChar

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

-- The Vim VimProc is divided into several parts, roughly corresponding
-- to the different modes of vi. Each mode is in turn broken up into
-- separate VimProcs for each phase of key input in that mode.

-- | Command mode consists of simple commands that take a count arg,
-- the replace cmd, which consumes one char of input, and commands
-- that switch modes.
cmd_mode :: VimMode
cmd_mode = choice [cmd_eval,eval cmd_move,cmd2other,cmd_op]

-- | Take a VimMode that returns and action; "run" it and write the returned action.
eval :: YiAction a () => VimProc (b, a) -> VimMode
eval p = do (_, a) <- p; write a

-- | Leave a mode. This always has priority over catch-all actions inside the mode.
leave :: VimMode
leave = event '\ESC' >> adjustPriority (-1) (write msgClr)

-- | Insert mode is either insertion actions, or the meta (\ESC) action
ins_mode :: VimMode
ins_mode = write (msgEditor "-- INSERT --") >> many (ins_char <|> kwd_mode) >> leave

-- | Replace mode is like insert, except it performs writes, not inserts
rep_mode :: VimMode
rep_mode = write (msgEditor "-- REPLACE --") >> many rep_char >> leave

-- | Visual mode, similar to command mode
vis_mode :: RegionStyle -> VimMode
vis_mode regionStyle = do
  write (withBuffer (pointB >>= setSelectionMarkPointB))
  core_vis_mode regionStyle
  write (msgClr >> withBuffer unsetMarkB >> withBuffer (setDynamicB $ SelectionStyle Character))

core_vis_mode :: RegionStyle -> VimMode
core_vis_mode regionStyle = do
  write $ withEditor $ do setA regionStyleA regionStyle
                          withBuffer0 $ setDynamicB $ SelectionStyle $
                            case regionStyle of { LineWise -> fullLine; CharWise -> Character }
                          printMsg $ msg regionStyle
  many (eval cmd_move)
  (vis_single regionStyle <|| vis_multi)
  where msg CharWise = "-- VISUAL --"
        msg LineWise = "-- VISUAL LINE --"

-- | Change visual mode
change_vis_mode :: RegionStyle -> RegionStyle -> VimMode
change_vis_mode src dst | src == dst = return ()
change_vis_mode _   dst              = core_vis_mode dst

-- | A VimProc to accumulate digits.
-- typically what is needed for integer repetition arguments to commands
count :: VimProc (Maybe Int)
count = option Nothing $ do
    c <- satisfy isDigitNon0
    cs <- many (satisfy isDigit)
    return $ Just $ read (c:cs)
  where isDigitNon0 '0' = False
        isDigitNon0 x   = isDigit x

data RegionStyle = LineWise
                 | CharWise
  deriving (Eq, Typeable, Show)

instance Initializable RegionStyle where
  initial = CharWise

regionStyleToUnit :: RegionStyle -> TextUnit
regionStyleToUnit LineWise = fullLine
regionStyleToUnit CharWise = Character

fullLine :: TextUnit
fullLine = GenUnit {genEnclosingUnit=Document, genUnitBoundary=bound}
  where bound d = withOffset d $ atBoundaryB Line d
        withOffset Backward f = f
        withOffset Forward  f = savingPointB (leftB >> f)

regionStyleA :: Accessor Editor RegionStyle
regionStyleA = dynamicValueA .> dynamicA

-- ---------------------------------------------------------------------
-- | VimProc for movement commands
--
-- The may be invoked directly, or sometimes as arguments to other
-- /operator/ commands (like d).
--

cmd_move :: VimProc (RegionStyle, BufferM ())
cmd_move = do
  cnt <- count
  let x = maybe 1 id cnt
  choice ([event c >> return (CharWise, a x) | (c,a) <- moveCmdFM, (c /= '0' || Nothing == cnt) ] ++
          [event c >> return (LineWise, a x) | (c,a) <- moveUpDownCmdFM] ++
          [do event c; c' <- anyEvent; return (CharWise, a x c') | (c,a) <- move2CmdFM] ++
          [do event 'G'; return (LineWise, case cnt of
                                       Nothing -> botB >> moveToSol
                                       Just n  -> gotoLn n >> return ())
          ,do events "gg"; return (LineWise, gotoLn 0 >> return ())
          ,do events "ge"; return (CharWise, replicateM_ x $ genMoveB ViWord (Forward, InsideBound) Backward)])

-- | movement commands
moveCmdFM :: [(Char, Int -> BufferM ())]
moveCmdFM =
-- left/right
    [('h',          left)
    ,('\^H',        left)
    ,(keyBackspace, left)
    ,('\BS',        left)
    ,('\127',       left)
    ,(keyLeft,      left)
    ,(keyRight,     right)
    ,('l',          right)
    ,(' ',          right)
    ,(keyHome,      sol)
    ,('0',          sol)
    ,('^',          const firstNonSpaceB)
    ,('$',          eol)
    ,(keyEnd,       eol)
    ,('|',          \i -> moveToSol >> moveXorEol (i-1))

-- words
    ,('w',          \i -> replicateM_ i $ genMoveB ViWord (Backward,InsideBound) Forward)
    ,('b',          \i -> replicateM_ i $ genMoveB ViWord (Backward,InsideBound) Backward)
    ,('e',          \i -> replicateM_ i $ genMoveB ViWord (Forward, InsideBound) Forward)

-- text
    ,('{',          prevNParagraphs)
    ,('}',          nextNParagraphs)

-- misc
    ,('H',          \i -> downFromTosB (i - 1))
    ,('M',          const middleB)
    ,('L',          \i -> upFromBosB (i - 1))
    ]
    where
        left  = moveXorSol
        right = moveXorEol
        sol   _ = moveToSol
        eol   _ = moveToEol

-- | up/down movement commands. these one are separated from moveCmdFM
-- because they behave differently when yanking/cuting (line mode).
moveUpDownCmdFM :: [(Char, Int -> BufferM ())]
moveUpDownCmdFM =
    [('k',          up)
    ,(keyUp,        up)
    ,('\^P',        up)
    ,('j',          down)
    ,(keyDown,      down)
    ,('\^J',        down)
    ,('\^N',        down)
    ,('\r',         down)
    ]
    where
        up   i = lineMoveRel (-i) >> return ()
        down i = lineMoveRel i    >> return ()

--  | more movement commands. these ones are paramaterised by a character
-- to find in the buffer.
move2CmdFM :: [(Char, Int -> Char -> BufferM ())]
move2CmdFM =
    [('f',  \i c -> replicateM_ i $ nextCInc c)
    ,('F',  \i c -> replicateM_ i $ prevCInc c)
    ,('t',  \i c -> replicateM_ i $ nextCExc c)
    ,('T',  \i c -> replicateM_ i $ prevCExc c)
    ]

-- | Other command mode functions
cmd_eval :: VimMode
cmd_eval = do
   cnt <- count
   let i = maybe 1 id cnt
   choice
    ([event c >> write (a i) | (c,a) <- singleCmdFM ] ++
    [events evs >> write (action i) | (evs, action) <- multiCmdFM ]) <|>
    (do event 'r'; c <- anyButEscOrDel; write (writeB c)) <|>
    (events ">>" >> write (shiftIndentOfLine i)) <|>
    (events "<<" >> write (shiftIndentOfLine (-i))) <|>
    (events "ZZ" >> write (viWrite >> quitEditor))

-- TODO: escape the current word
--       at word bounds: search for \<word\>
searchCurrentWord :: YiM ()
searchCurrentWord = do
  w <- withBuffer $ readRegionB =<< regionOfB ViWord
  doSearch (Just w) [] Forward

anyButEscOrDel :: VimProc Char
anyButEscOrDel = satisfy (not . (`elem` ('\ESC':delete')))

continueSearching :: Direction -> YiM ()
continueSearching direction = do
  withEditor $ getRegexE >>= printMsg . ("/" ++) . fst . fromMaybe ([],undefined)
  doSearch Nothing [] direction

-- | cmd mode commands
singleCmdFM :: [(Char, Int -> YiM ())]
singleCmdFM =
    [('\^B',    withBuffer . upScreensB)             -- vim does (firstNonSpaceB;moveXorSol)
    ,('\^F',    withBuffer . downScreensB)
    ,('\^G',    const viFileInfo)        -- hmm. not working. duh. we clear
    ,('\^L',    const refreshEditor)
    ,('\^R',    withBuffer . flip replicateM_ redoB)
    ,('\^Z',    const suspendEditor)
    ,('D',      const (withEditor $ withBuffer0 readRestOfLnB >>= setRegE >> withBuffer0 deleteToEol))
    ,('J',      const (withBuffer (moveToEol >> deleteN 1)))    -- the "\n"
    ,('U',      withBuffer . flip replicateM_ undoB)    -- NB not correct
    ,('n',      const $ continueSearching Forward)
    ,('u',      withBuffer . flip replicateM_ undoB)

    ,('X',      \i -> withBuffer $ do p <- pointB
                                      moveXorSol i
                                      q <- pointB
                                      when (p-q > 0) $ deleteN (p-q) )

    ,('x',      \i -> withBuffer $ do p <- pointB -- not handling eol properly
                                      moveXorEol i
                                      q <- pointB
                                      moveTo p
                                      when (q-p > 0) $ deleteN (q-p))

    ,('p',      withEditor . flip replicateM_ pasteAfter)

    ,('P',      withEditor . flip replicateM_ pasteBefore)

    ,(keyPPage, withBuffer . upScreensB)
    ,(keyNPage, withBuffer . downScreensB)
    ,('*',      const $ searchCurrentWord)
    ,('~',      \i -> withBuffer $ do
                         p <- pointB
                         moveXorEol i
                         q <- pointB
                         moveTo p
                         mapRegionB (mkRegion p q) $ \c ->
                             if isUpper c then toLower c else toUpper c
                         moveTo q)
    ]

multiCmdFM :: [(String, Int -> YiM ())]
multiCmdFM =
    [("\^Wc", const $ withEditor tryCloseE)
    ,("\^Wo", const $ withEditor closeOtherE)
    ,("\^Ws", const $ withEditor splitE)
    ,("\^Ww", const $ withEditor nextWinE)
    ,("\^WW", const $ withEditor prevWinE)
    ,("\^Wp", const $ withEditor prevWinE)

    -- since we don't have vertical splitting,
    -- these moving can be done using next/prev.
    ,(['\^W',keyDown], const $ withEditor nextWinE)
    ,(['\^W',keyUp], const $ withEditor prevWinE)
    ,(['\^W',keyRight], const $ withEditor nextWinE)
    ,(['\^W',keyLeft], const $ withEditor prevWinE)
    ]

-- | So-called 'operators', which take movement actions as arguments.
--
-- How do we achive this? We parse a known operator char then parse
-- one of the known movement commands.  We then apply the returned
-- action and then the operator. For example, we 'd' command stores
-- the current point, does a movement, then deletes from the old to
-- the new point.
cmd_op :: VimMode
cmd_op = do
  cnt <- count
  let i = maybe 1 id cnt
  choice $ [events "dd" >> write (cut  pointB (lineMoveRel (i-1) >> return ()) LineWise),
            events "yy" >> write (yank pointB (lineMoveRel (i-1) >> return ()) LineWise)] ++
           [do event c
               (regionStyle, m) <- cmd_move
               write $ a (replicateM_ i m) regionStyle
           | (c,a) <- opCmdFM]
    where
        -- | operator (i.e. movement-parameterised) actions
        opCmdFM :: [(Char, BufferM () -> RegionStyle -> EditorM ())]
        opCmdFM =
            [('d', cut  pointB),
             ('y', yank pointB)]

regionFromTo :: BufferM Point -> BufferM () -> RegionStyle -> BufferM Region
regionFromTo mstart move regionStyle = do
  start <- mstart
  move
  stop <- pointB
  let region = mkRegion start stop
  unitWiseRegion (regionStyleToUnit regionStyle) region

yank :: BufferM Point -> BufferM () -> RegionStyle -> EditorM ()
yank mstart move regionStyle = do
  txt <- withBuffer0 $ do
    p <- pointB
    region <- regionFromTo mstart move regionStyle
    moveTo p
    readRegionB region
  setRegE $ if (regionStyle /= CharWise) then '\n':txt else txt
  let rowsYanked = length (filter (== '\n') txt)
  when (rowsYanked > 2) $ printMsg $ (show rowsYanked) ++ " lines yanked"

cut :: BufferM Point -> BufferM () -> RegionStyle -> EditorM ()
cut mstart move regionStyle = do
  txt <- withBuffer0 $ do
    region <- regionFromTo mstart move regionStyle
    txt <- readRegionB region
    deleteRegionB region
    return txt
  setRegE $ if (regionStyle /= CharWise) then '\n':txt else txt
  let rowsCut = length (filter (== '\n') txt)
  when (rowsCut > 2) $ printMsg ( (show rowsCut) ++ " fewer lines")

cutSelection :: EditorM ()
cutSelection = cut getSelectionMarkPointB (return ()) =<< getA regionStyleA

yankSelection :: EditorM ()
yankSelection = yank getSelectionMarkPointB (return ()) =<< getA regionStyleA

pasteOverSelection :: EditorM ()
pasteOverSelection = do
  txt <- getRegE
  regionStyle <- getA regionStyleA
  withBuffer0 $ do
    region <- regionFromTo getSelectionMarkPointB (return ()) regionStyle
    moveTo $ regionStart region
    deleteRegionB region
    insertN txt

pasteAfter :: EditorM ()
pasteAfter = do
  txt' <- getRegE
  withBuffer0 $
    case txt' of
      '\n':txt -> moveToEol >> rightB >> insertN txt >> leftN (length txt)
      _      -> moveXorEol 1 >> insertN txt' >> leftB

pasteBefore :: EditorM ()
pasteBefore = do
  txt' <- getRegE
  withBuffer0 $ do
    case txt' of
      '\n':txt -> moveToSol >> insertN txt >> leftN (length txt)
      _      -> insertN txt' >> leftB

-- | Switching to another mode from visual mode.
--
-- All visual commands are meta actions, as they transfer control to another
-- VimProc. In this way vis_single is analogous to cmd2other
--
vis_single :: RegionStyle -> VimMode
vis_single regionStyle =
        let beginIns a = do write (a >> withBuffer0 unsetMarkB) >> ins_mode
        in choice [
            event '\ESC' >> return (),
            event 'V'    >> change_vis_mode regionStyle LineWise,
            event 'v'    >> change_vis_mode regionStyle CharWise,
            event ':'    >> ex_mode ":'<,'>",
            event 'y'    >> write yankSelection,
            event 'x'    >> write cutSelection,
            event 'd'    >> write cutSelection,
            event 'p'    >> write pasteOverSelection,
            event 'c'    >> beginIns cutSelection]


-- | These also switch mode, as all visual commands do, but these are
-- analogous to the commands in cmd_eval.  They are different in that
-- they are multiple characters
vis_multi :: VimMode
vis_multi = do
   cnt <- count
   let i = maybe 1 id cnt
   choice ([events "ZZ" >> write (viWrite >> quitEditor),
            events ">>" >> write (shiftIndentOfSelection i),
            events "<<" >> write (shiftIndentOfSelection (-i)),
            do event 'r'; x <- anyEvent; write $ do
                                   mrk <- getSelectionMarkPointB
                                   pt <- pointB
                                   text <- readRegionB (mkVimRegion mrk pt)
                                   moveTo mrk
                                   deleteRegionB (mkVimRegion mrk pt)
                                   let convert '\n' = '\n'
                                       convert  _   = x
                                   insertN $ map convert $ text] ++
           [event c >> write (a i) | (c,a) <- singleCmdFM ])


-- | Switch to another vim mode from command mode.
--
-- These commands are meta actions, as they transfer control to another
-- VimProc. Some of these commands also perform an action before switching.
--
cmd2other :: VimMode
cmd2other = let beginIns a = write a >> ins_mode
                beginIns :: EditorM () -> VimMode
        in choice [
            do event ':'     ; ex_mode ":",
            do event 'v'     ; vis_mode CharWise,
            do event 'V'     ; vis_mode LineWise,
            do event 'R'     ; rep_mode,
            do event 'i'     ; ins_mode,
            do event 'I'     ; beginIns (withBuffer0 moveToSol),
            do event 'a'     ; beginIns $ withBuffer0 $ moveXorEol 1,
            do event 'A'     ; beginIns (withBuffer0 moveToEol),
            do event 'o'     ; beginIns $ withBuffer0 $ moveToEol >> insertB '\n',
            do event 'O'     ; beginIns $ withBuffer0 $ moveToSol >> insertB '\n' >> lineUp,
            do event 'c'     ; (regionStyle, m) <- cmd_move ; beginIns $ cut pointB m regionStyle,
            do events "cc"   ; beginIns $ cut (moveToSol >> pointB) moveToEol CharWise,
            do event 'C'     ; beginIns $ cut pointB moveToEol CharWise, -- alias of "c$"
            do event 'S'     ; beginIns $ cut (moveToSol >> pointB) moveToEol CharWise, -- alias of "cc"
            do event 's'     ; beginIns $ cut pointB (moveXorEol 1) CharWise, -- alias of "cl"
            do event '/'     ; ex_mode "/",
            do event '?'     ; write $ not_implemented '?',
            leave,
            do event keyIC   ; ins_mode]


ins_mov_char :: VimMode
ins_mov_char = choice [event keyPPage >> write upScreenB,
                       event keyNPage >> write downScreenB,
                       event keyUp    >> write lineUp,
                       event keyDown  >> write lineDown,
                       event keyLeft  >> write (moveXorSol 1),
                       event keyRight >> write (moveXorEol 1),
                       event keyEnd   >> write moveToEol,
                       event keyHome  >> write moveToSol]


-- ---------------------------------------------------------------------
-- | vim insert mode
--
-- Some ideas for a better insert mode are contained in:
--
--      Poller and Garter , "A comparative study of moded and modeless
--      text editing by experienced editor users", 1983
--
-- which suggest that movement commands be added to insert mode, along
-- with delete.
--
ins_char :: VimMode
ins_char = choice [satisfy isDel  >> write (deleteB Character Backward),
                   event keyDC    >> write (deleteB Character Forward),
                   event '\t'     >> write insertTabB]
           <|> ins_mov_char
           <|| do c <- anyEvent; write (insertB c)

-- --------------------
-- | Keyword
kwd_mode :: VimMode
kwd_mode = some (event '\^N' >> adjustPriority (-1) (write wordCompleteB)) >> (write resetCompleteB)
           -- 'adjustPriority' is there to lift the ambiguity between "continuing" completion
           -- and resetting it (restarting at the 1st completion).

-- ---------------------------------------------------------------------
-- | vim replace mode
--
-- To quote vim:
--  In Replace mode, one character in the line is deleted for every character
--  you type.  If there is no character to delete (at the end of the line), the
--  typed character is appended (as in Insert mode).  Thus the number of
--  characters in a line stays the same until you get to the end of the line.
--  If a <NL> is typed, a line break is inserted and no character is deleted.
rep_char :: VimMode
rep_char = choice [satisfy isDel  >> write leftB, -- should undo unless pointer has been moved
                   event '\t'     >> write (insertN "    "),
                   event '\r'     >> write (insertB '\n')]
           <|> ins_mov_char
           <|| do c <- anyEvent; write (do e <- atEol; if e then insertB c else writeB c)

-- ---------------------------------------------------------------------
-- Ex mode. We also process regex searching mode here.

spawn_ex_buffer :: String -> YiM ()
spawn_ex_buffer prompt = do
  -- The above ensures that the action is performed on the buffer that originated the minibuffer.
  let ex_buffer_finish = do
        withEditor $ historyFinish
        lineString <- withBuffer elemsB
        withEditor closeBufferAndWindowE
        ex_eval (head prompt : lineString)
      ex_process :: VimMode
      ex_process =
          choice [enter         >> write ex_buffer_finish,
                  event '\t'    >> write completeMinibuffer,
                  event '\ESC'  >> write closeBufferAndWindowE,
                  delete        >> write bdeleteB,
                  event keyUp   >> write historyUp,
                  event keyDown >> write historyDown,
                  event keyLeft  >> write (moveXorSol 1),
                  event keyRight >> write (moveXorEol 1)]
             <|| (do c <- anyEvent; write $ insertB c)
      completeMinibuffer = withBuffer elemsB >>= ex_complete >>= withBuffer . insertN
      b_complete f = completeBufferName f >>= return . drop (length f)
      ex_complete ('e':' ':f) = completeFileName Nothing f >>= return . drop (length f)
      ex_complete ('b':' ':f)                             = b_complete f
      ex_complete ('b':'u':'f':'f':'e':'r':' ':f)         = b_complete f
      ex_complete ('b':'d':' ':f)                         = b_complete f
      ex_complete ('b':'d':'!':' ':f)                     = b_complete f
      ex_complete ('b':'d':'e':'l':'e':'t':'e':' ':f)     = b_complete f
      ex_complete ('b':'d':'e':'l':'e':'t':'e':'!':' ':f) = b_complete f
      ex_complete _ = return ""

  withEditor $ historyStart
  spawnMinibufferE prompt (const $ runVim $ ex_process) (return ())


ex_mode :: String -> VimMode
ex_mode = write . spawn_ex_buffer

-- | eval an ex command to an YiM (), also appends to the ex history
ex_eval :: String -> YiM ()
ex_eval cmd = do
  case cmd of
        -- regex searching
          ('/':pat) -> doSearch (Just pat) [] Forward

        -- TODO: We give up on re-mapping till there exists a generic Yi mechanism to do so.

        -- add mapping to command mode
          (_:'m':'a':'p':' ':_cs) -> error "Not yet implemented."

        -- add mapping to insert mode
          (_:'m':'a':'p':'!':' ':_cs) -> error "Not yet implemented."

        -- unmap a binding from command mode
          (_:'u':'n':'m':'a':'p':' ':_cs) -> error "Not yet implemented."

        -- unmap a binding from insert mode
          (_:'u':'n':'m':'a':'p':'!':' ':_cs) -> error "Not yet implemented."


        -- just a normal ex command
          (_:src) -> fn src

        -- can't happen, but deal with it
          [] -> return ()

    where
      whenUnchanged mu f = do u <- mu
                              if u then f
                                   else errorEditor "No write since last change (add ! to override)"
      quitB = whenUnchanged (withBuffer isUnchangedB) closeWindow

      quitNoW = do bufs <- withEditor $ do closeBufferE ""
                                           bufs <- gets bufferStack
                                           mapM (\x -> withGivenBuffer0 x isUnchangedB) bufs
                   whenUnchanged (return $ all id bufs) quitEditor

      quitall  = withAllBuffers quitB
      wquitall = withAllBuffers viWrite >> quitEditor
      bdelete  = whenUnchanged (withBuffer isUnchangedB) . withEditor . closeBufferE
      bdeleteNoW = withEditor . closeBufferE

      fn ""           = msgClr

      fn s@(c:_) | isDigit c = do
        e <- lift $ try $ evaluate $ read s
        case e of Left _ -> errorEditor $ "The " ++show s++ " command is unknown."
                  Right lineNum -> withBuffer (gotoLn lineNum) >> return ()

      fn "w"          = viWrite
      fn ('w':' ':f)  = viWriteTo f
      fn "qa"         = quitall
      fn "qal"        = quitall
      fn "qall"       = quitall
      fn "quita"      = quitall
      fn "quital"     = quitall
      fn "quitall"    = quitall
      fn "q"          = quitB
      fn "qu"         = quitB
      fn "qui"        = quitB
      fn "quit"       = quitB
      fn "q!"         = quitNoW
      fn "qu!"        = quitNoW
      fn "qui!"       = quitNoW
      fn "quit!"      = quitNoW
      fn "qa!"        = quitEditor
      fn "quita!"     = quitEditor
      fn "quital!"    = quitEditor
      fn "quitall!"   = quitEditor
      fn "wq"         = viWrite >> closeWindow
      fn "wqa"        = wquitall
      fn "wqal"       = wquitall
      fn "wqall"      = wquitall
      fn "x"          = do unchanged <- withBuffer isUnchangedB
                           unless unchanged viWrite
                           closeWindow
      fn "n"          = withEditor nextBufW
      fn "next"       = withEditor nextBufW
      fn "$"          = withBuffer botB
      fn "p"          = withEditor prevBufW
      fn "prev"       = withEditor prevBufW
      fn ('s':'p':_)  = withEditor splitE
      fn "e"          = revertE
      fn ('e':' ':f)  = fnewE f
      fn ('s':'e':'t':' ':'f':'t':'=':ft)  = withBuffer $ setSyntaxB $ highlighters M.! ft
      fn ('n':'e':'w':' ':f) = withEditor splitE >> fnewE f
      fn ('s':'/':cs) = viSub cs

      fn ('b':' ':"m") = withEditor $ switchToBufferWithNameE "*messages*"
      fn ('b':' ':f)   = withEditor $ switchToBufferWithNameE f
      fn "bd"                                    = bdelete ""
      fn "bdelete"                               = bdelete ""
      fn ('b':'d':' ':f)                         = bdelete f
      fn ('b':'d':'e':'l':'e':'t':'e':' ':f)     = bdelete f
      fn "bd!"                                   = bdeleteNoW ""
      fn "bdelete!"                              = bdeleteNoW ""
      fn ('b':'d':'!':' ':f)                     = bdeleteNoW f
      fn ('b':'d':'e':'l':'e':'t':'e':'!':' ':f) = bdeleteNoW f
      -- TODO: bd[!] [N]

      -- send just this line through external command /fn/
      fn ('.':'!':f) = do
            ln  <- withBuffer readLnB
            ln' <- runProcessWithInput f ln
            withBuffer $ do moveToSol
                            deleteToEol
                            insertN ln'
                            moveToSol

--    Needs to occur in another buffer
--    fn ('!':f) = runProcessWithInput f []

      fn "reload"     = reloadEditor >> return ()    -- not in vim
      fn "eval"       = withBuffer (regionOfB Document >>= readRegionB) >>= evalE -- not in vim

      fn "redr"       = refreshEditor
      fn "redraw"     = refreshEditor

      fn "u"          = withBuffer undoB
      fn "undo"       = withBuffer undoB
      fn "r"          = withBuffer redoB
      fn "redo"       = withBuffer redoB

      fn "sus"        = suspendEditor
      fn "suspend"    = suspendEditor
      fn "st"         = suspendEditor
      fn "stop"       = suspendEditor

      fn s            = errorEditor $ "The "++show s++ " command is unknown."


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

not_implemented :: Char -> YiM ()
not_implemented c = errorEditor $ "Not implemented: " ++ show c

-- ---------------------------------------------------------------------
-- Misc functions

withAllBuffers :: YiM () -> YiM ()
withAllBuffers m = mapM_ (\b -> withEditor (setBuffer b) >> m) =<< readEditor bufferStack

viFileInfo :: YiM ()
viFileInfo =
    do bufInfo <- withBuffer bufInfoB
       msgEditor $ showBufInfo bufInfo
    where
    showBufInfo :: BufferFileInfo -> String
    showBufInfo bufInfo = concat [ show $ bufInfoFileName bufInfo
         , " Line "
         , show $ bufInfoLineNo bufInfo
         , " ["
         , bufInfoPercent bufInfo
         , "]"
         ]


-- | Try to write a file in the manner of vi\/vim
-- Need to catch any exception to avoid losing bindings
viWrite :: YiM ()
viWrite = do
    mf <- withBuffer getfileB
    case mf of
        Nothing -> errorEditor "no file name associate with buffer"
        Just f  -> do
            bufInfo <- withBuffer bufInfoB
            let s   = bufInfoFileName bufInfo
            let msg = msgEditor $ show f ++" "++show s ++ "C written"
            catchJustE ioErrors (fwriteToE f >> msg) (msgEditor . show)


-- | Try to write to a named file in the manner of vi\/vim
viWriteTo :: String -> YiM ()
viWriteTo f = do
    let f' = (takeWhile (/= ' ') . dropWhile (== ' ')) f
    bufInfo <- withBuffer bufInfoB
    let s   = bufInfoFileName bufInfo
    let msg = msgEditor $ show f'++" "++show s ++ "C written"
    catchJustE ioErrors (fwriteToE f' >> msg) (msgEditor . show)

-- | Try to do a substitution
viSub :: [Char] -> YiM ()
viSub cs = do
    let (pat,rep') = break (== '/')  cs
        (rep,opts) = case rep' of
                        []     -> ([],[])
                        (_:ds) -> case break (== '/') ds of
                                    (rep'', [])    -> (rep'', [])
                                    (rep'', (_:fs)) -> (rep'',fs)
    case opts of
        []    -> do_single pat rep
        ['g'] -> do_single pat rep
        _     -> do_single pat rep-- TODO

    where do_single p r = do
                s <- searchAndRepLocal p r
                if not s then errorEditor ("Pattern not found: "++p) else msgClr

-- | Is a delete sequence
isDel :: Char -> Bool
isDel '\BS'        = True
isDel '\127'       = True
isDel c | c == keyBackspace = True
isDel _            = False

-- ---------------------------------------------------------------------
-- | Character ranges
--
delete, enter :: VimProc Char
enter   = oneOf ['\n', '\r']
delete  = oneOf delete'

delete' :: [Char]
delete' =  ['\BS', '\127', keyBackspace ]