diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,4 @@
+#!/usr/bin/env runhaskell
+import Distribution.Simple
+main :: IO ()
+main = defaultMain
diff --git a/src/Control/Exc.hs b/src/Control/Exc.hs
new file mode 100644
--- /dev/null
+++ b/src/Control/Exc.hs
@@ -0,0 +1,23 @@
+{-# LANGUAGE ScopedTypeVariables #-}
+
+-- | Utilities for working with new Control.Exception
+module Control.Exc (ignoringException, printingException, orException)
+where
+
+import Prelude
+import Control.Exception (catch, SomeException)
+
+-- | Execute IO (Maybe a) action replacing all exceptions with return value of Nothing.
+ignoringException :: IO (Maybe a) -> IO (Maybe a)
+ignoringException f = f `catch` ignore
+  where ignore (_ :: SomeException) = return Nothing
+
+-- | Execute IO () action, replacing all exceptions with messages
+printingException :: String -> IO a -> IO a
+printingException desc f = f `catch` handler
+  where handler (err :: SomeException) = fail $ concat [desc, " failed: ", show err]
+
+-- | Execute IO () action, replacing all exceptions with messages
+orException :: IO a -> IO a -> IO a
+orException f g = f `catch` handler
+  where handler (_ :: SomeException) = g
diff --git a/src/Data/DelayList.hs b/src/Data/DelayList.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/DelayList.hs
@@ -0,0 +1,25 @@
+-- maybe use package event-list instead.
+module Data.DelayList (insert, decrease, DelayList) where
+
+type DelayList a = [(Int, a)]
+
+-- | Subtraction, but treat maxBound as infinity. i.e. maxBound -? x == maxBound
+(-?) :: Int -> Int -> Int
+x -? y | x == maxBound = x
+       | otherwise = x - y
+
+insert :: (Int, a) -> DelayList a -> DelayList a
+insert (d, a) [] = [(d, a)]
+insert (d, a) l@(h@(d', _):t)
+       | d == d' = (d, a):t
+       | d <  d' = (d, a) : decrease d l
+       -- d >  d'
+       | otherwise = h : insert (d -? d', a) t
+
+decrease :: Int -> DelayList a -> DelayList a
+decrease _ [] = []
+decrease d l@((d',a):t)
+    | d <= 0 = l
+    | d < d' = (d' -? d, a):t
+    -- d >= d'
+    | otherwise = decrease (d - d') t
diff --git a/src/Parser/Incremental.hs b/src/Parser/Incremental.hs
new file mode 100644
--- /dev/null
+++ b/src/Parser/Incremental.hs
@@ -0,0 +1,360 @@
+{-# LANGUAGE GADTs               #-}
+{-# LANGUAGE RankNTypes          #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TypeOperators       #-}
+
+-- TODO:
+-- better interface
+-- have error messages in the right order
+-- have a message for plain failures as well / remove failure in recoveries
+
+-- Optimize profile info (no more Ints)
+
+module Parser.Incremental (Process,
+                           recoverWith, symbol, eof, lookNext, testNext, run,
+                           mkProcess, profile, pushSyms, pushEof, evalL, evalR, feedZ,
+                           Parser(Look, Enter, Yuck), countWidth, fullLog, LogEntry(..),
+                           evalL'
+                          ) where
+
+import Control.Arrow       (first, second, (***))
+import Control.Applicative (Alternative ((<|>), empty))
+import Data.Tree           (Tree (Node))
+
+data a :< b = (:<) {top :: a, _rest :: b}
+infixr :<
+
+-- | Parser specification
+data Parser s a where
+    Pure  :: a                               -> Parser s a
+    Appl  :: Parser s (b -> a) -> Parser s b -> Parser s a
+
+    Bind  :: Parser s a -> (a -> Parser s b) -> Parser s b
+
+    Look  :: Parser s a -> (s -> Parser s a) -> Parser s a
+    Shif  :: Parser s a                      -> Parser s a
+    Empt  ::                                    Parser s a
+    Disj  :: Parser s a -> Parser s a        -> Parser s a
+    Yuck  :: Parser s a                      -> Parser s a
+    Enter :: String -> Parser s a            -> Parser s a
+
+-- | Parser process
+data Steps s a where
+    Val     :: a -> Steps s r                                -> Steps s (a :< r)
+    App     :: Steps s ((b -> a) :< (b :< r))                -> Steps s (a :< r)
+    Done    ::                                                  Steps s ()
+    Shift   :: Steps s a                                     -> Steps s a
+    Sh'     :: Steps s a                                     -> Steps s a
+    Sus     :: Steps s a -> (s -> Steps s a)                 -> Steps s a
+    Best    :: Ordering -> Profile -> Steps s a -> Steps s a -> Steps s a
+    Dislike :: Steps s a                                     -> Steps s a
+    Log     :: String -> Steps s a                           -> Steps s a
+    Fail    ::                                                  Steps s a
+
+-- profile !! s = number of Dislikes found to do s Shifts
+data Profile = PSusp | PFail | PRes Int | !Int :> Profile
+    deriving Show
+
+mapSucc :: Profile -> Profile
+mapSucc PSusp = PSusp
+mapSucc PFail = PFail
+mapSucc (PRes x) = PRes (succ x)
+mapSucc (x :> xs) = succ x :> mapSucc xs
+
+-- Map lookahead to maximum dislike difference we accept. When looking much further,
+-- we are more prone to discard smaller differences. It's essential that this drops below 0 when
+-- its argument increases, so that we can discard things with dislikes using only
+-- finite lookahead.
+dislikeThreshold :: Int -> Int
+dislikeThreshold n
+    | n < 5 = 0
+    | otherwise = -1 -- we looked 5 tokens ahead, and still have no clue who is the best. Pick at random.
+
+-- | Compute the combination of two profiles, as well as which one is the best.
+better :: Int -> Profile -> Profile -> (Ordering, Profile)
+better _ PFail p = (GT, p) -- avoid failure
+better _ p PFail = (LT, p)
+better _ PSusp _ = (EQ, PSusp) -- could not decide before suspension => leave undecided.
+better _ _ PSusp = (EQ, PSusp)
+better _ (PRes x) (PRes y) = if x <= y then (LT, PRes x) else (GT, PRes y)  -- two results, just pick the best.
+better lk xs@(PRes x) (y:>ys) = if x == 0 || y-x > dislikeThreshold lk then (LT, xs) else min x y +> better (lk+1) xs ys
+better lk (y:>ys) xs@(PRes x) = if x == 0 || y-x > dislikeThreshold lk then (GT, xs) else min x y +> better (lk+1) ys xs
+better lk (x:>xs) (y:>ys)
+    | x == 0 && y == 0 = recur -- never drop things with no error: this ensures to find a correct parse if it exists.
+    | x - y > threshold = (GT, y:>ys)
+    | y - x > threshold = (LT, x:>xs) -- if at any point something is too disliked, drop it.
+    | otherwise = recur
+    where threshold = dislikeThreshold lk
+          recur = min x y +> better (lk + 1) xs ys
+
+(+>) :: Int -> (t, Profile) -> (t, Profile)
+x +> ~(ordering, xs) = (ordering, x :> xs)
+
+data LogEntry = LLog String | LEmpty | LDislike | LShift
+                | LDone | LFail | LSusp | LS String
+    deriving Show
+
+rightLog :: Steps s r -> Tree LogEntry
+rightLog (Val _ p) = rightLog p
+rightLog (App p) = rightLog p
+rightLog (Shift p) = Node LShift [rightLog p]
+rightLog (Done) = Node LDone []
+rightLog (Fail) = Node LFail []
+rightLog (Dislike p) = Node LDislike [rightLog p]
+rightLog (Log msg p) = Node (LLog msg) [rightLog p]
+rightLog (Sus _ _) = Node LSusp []
+rightLog (Best _ _ l r) = Node LEmpty (rightLog l:[rightLog r])
+rightLog (Sh' _) = error "Sh' should be hidden by Sus"
+
+profile :: Steps s r -> Profile
+profile (Val _ p) = profile p
+profile (App p) = profile p
+profile (Shift p) = 0 :> profile p
+profile (Done) = PRes 0 -- success with zero dislikes
+profile (Fail) = PFail
+profile (Dislike p) = mapSucc (profile p)
+profile (Log _ p) = profile p
+profile (Sus _ _) = PSusp
+profile (Best _ pr _ _) = pr
+profile (Sh' _) = error "Sh' should be hidden by Sus"
+
+instance Show (Steps s r) where
+    show (Val _ p) = 'v' : show p
+    show (App p) = '*' : show p
+    show (Done) = "1"
+    show (Shift p) = '>' : show p
+    show (Sh' p) = '\'' : show p
+    show (Dislike p) = '?' : show p
+    show (Log msg p) = "[" ++ msg ++ "]"  ++ show p
+    show (Fail) = "0"
+    show (Sus _ _) = "..."
+    show (Best _ _ p q) = "(" ++ show p ++ ")" ++ show q
+
+countWidth :: Zip s r -> Int
+countWidth (Zip _ _ r) = countWidth' r
+  where countWidth' :: Steps s r -> Int
+        countWidth' r' = case r' of
+            (Best _ _ p q) ->  countWidth' p + countWidth' q
+            (Val _ p) -> countWidth' p
+            (App p) -> countWidth' p
+            (Done) -> 1
+            (Shift p) -> countWidth' p
+            (Sh' p) -> countWidth' p
+            (Dislike p) -> countWidth' p
+            (Log _ p) -> countWidth' p
+            (Fail) -> 1
+            (Sus _ _) -> 1
+
+instance Show (RPolish i o) where
+    show (RPush _ p) = show p ++ "^"
+    show (RApp p) = show p ++ "@"
+    show (RStop) = "!"
+
+
+apply :: forall t t1 a. ((t -> a) :< (t :< t1)) -> a :< t1
+apply ~(f:< ~(a:<r)) = f a :< r
+
+-- | Right-eval a fully defined process (ie. one that has no Sus)
+evalR' :: Steps s r -> (r, [String])
+evalR' Done = ((), [])
+evalR' (Val a r) = first (a :<) (evalR' r)
+evalR' (App s) = first apply (evalR' s)
+evalR' (Shift v) = evalR' v
+evalR' (Dislike v) = evalR' v
+evalR' (Log err v) = second (err:) (evalR' v)
+evalR' (Fail) = error "evalR: No parse!"
+evalR' (Sus _ _) = error "evalR: Not fully evaluated!"
+evalR' (Sh' _) = error "evalR: Sh' should be hidden by Sus"
+evalR' (Best choice _ p q) = case choice of
+    LT -> evalR' p
+    GT -> evalR' q
+    EQ -> error $ "evalR: Ambiguous parse: " ++ show p ++ " ~~~ " ++ show q
+
+
+instance Functor (Parser s) where
+    fmap f = (pure f <*>)
+
+instance Applicative (Parser s) where
+    (<*>) = Appl
+    pure = Pure
+
+instance Alternative (Parser s) where
+    (<|>) = Disj
+    empty = Empt
+
+instance Monad (Parser s) where
+    (>>=) = Bind
+    return = pure
+    fail _message = Empt
+
+toQ :: Parser s a -> forall h r. ((h,a) -> Steps s r)  -> h -> Steps s r
+toQ (Look a f) = \k h -> Sus (toQ a k h) (\s -> toQ (f s) k h)
+toQ (p `Appl` q) = \k -> toQ p $ toQ q $ \((h, b2a), b) -> k (h, b2a b)
+toQ (Pure a)     = \k h -> k (h, a)
+toQ (Disj p q)   = \k h -> iBest (toQ p k h) (toQ q k h)
+toQ (Bind p a2q) = \k -> toQ p (\(h,a) -> toQ (a2q a) k h)
+toQ Empt = \_k _h -> Fail
+toQ (Yuck p) = \k h -> Dislike $ toQ p k h
+toQ (Enter err p) = \k h -> Log err $ toQ p k h
+toQ (Shif p) = \k h -> Sh' $ toQ p k h
+
+toP :: Parser s a -> forall r. Steps s r -> Steps s (a :< r)
+toP (Look a f) = \fut -> Sus (toP a fut) (\s -> toP (f s) fut)
+toP (Appl f x) = App . toP f . toP x
+toP (Pure x)   = Val x
+toP Empt = const Fail
+toP (Disj a b)  = \fut -> iBest (toP a fut) (toP b fut)
+toP (Bind p a2q) = \fut -> toQ p (\(_,a) -> toP (a2q a) fut) ()
+toP (Yuck p) = Dislike . toP p
+toP (Enter err p) = Log err . toP p
+toP (Shif p) = Sh' . toP p
+
+-- | Intelligent, caching best.
+iBest :: Steps s a -> Steps s a -> Steps s a
+iBest p q = let ~(choice, pr) = better 0 (profile p) (profile q) in Best choice pr p q
+
+symbol :: forall s. (s -> Bool) -> Parser s s
+symbol f = Look empty $ \s -> if f s then Shif $ pure s else empty
+
+eof :: forall s. Parser s ()
+eof = Look (pure ()) (const empty)
+
+-- | Push a chunk of symbols or eof in the process. This forces some suspensions.
+feed :: Maybe [s] -> Steps s r -> Steps s r
+feed (Just []) p = p  -- nothing more left to feed
+feed ss p = case p of
+                  (Sus nil cons) -> case ss of
+                      Just [] -> p -- no more info, stop feeding
+                      Nothing -> feed Nothing nil -- finish
+                      Just (s:_) -> feed ss (cons s)
+                  (Shift p') -> Shift (feed ss p')
+                  (Sh' p')   -> Shift (feed (fmap (drop 1) ss) p')
+                  (Dislike p') -> Dislike (feed ss p')
+                  (Log err p') -> Log err (feed ss p')
+                  (Val x p') -> Val x (feed ss p')
+                  (App p') -> App (feed ss p')
+                  Done -> Done
+                  Fail -> Fail
+                  Best _ _ p' q' -> iBest (feed ss p') (feed ss q')
+                  -- TODO: it would be nice to be able to reuse the profile here.
+
+
+feedZ :: Maybe [s] -> Zip s r -> Zip s r
+feedZ x = onRight (feed x)
+
+
+-- Move the zipper to right, and simplify if something is pushed in
+-- the left part.
+
+evalL :: forall s output. Zip s output -> Zip s output
+evalL (Zip errs0 l0 r0) = help errs0 l0 r0
+  where
+      help :: [String] -> RPolish mid output -> Steps s mid -> Zip s output
+      help errs l rhs = case rhs of
+          (Val a r) -> help errs (simplify (RPush a l)) r
+          (App r)  -> help errs (RApp l) r
+          (Shift p) -> help errs l p
+          (Log err p) -> help (err:errs) l p
+          (Dislike p) -> help errs l p
+          (Best choice _ p q) -> case choice of
+              LT -> help errs l p
+              GT -> help errs l q
+              EQ -> reZip errs l rhs -- don't know where to go: don't speculate on evaluating either branch.
+          _ -> reZip errs l rhs
+      reZip :: [String] -> RPolish mid output -> Steps s mid -> Zip s output
+      reZip errs l r = l `seq` Zip errs l r
+
+evalL' :: Zip s output -> Zip s output
+evalL' (Zip errs0 l0 r0) = Zip errs0 l0 (simplRhs r0)
+    where simplRhs :: Steps s a ->Steps s a
+          simplRhs rhs = case rhs of
+            (Val a r) -> Val a (simplRhs r)
+            (App r)  -> App (simplRhs r)
+            (Shift p) -> Shift (simplRhs p)
+            (Log err p) -> Log err $ simplRhs p
+            (Dislike p) -> Dislike $ simplRhs p
+            (Best choice _ p q) -> case choice of
+                LT -> simplRhs p
+                GT -> simplRhs q
+                EQ -> iBest (simplRhs p) (simplRhs q)
+            x -> x
+
+-- | Push some symbols.
+pushSyms :: forall s r. [s] -> Zip s r -> Zip s r
+pushSyms x = feedZ (Just x)
+
+-- | Push eof
+pushEof :: forall s r. Zip s r -> Zip s r
+pushEof = feedZ Nothing
+
+-- | Make a parser into a process.
+mkProcess :: forall s a. Parser s a -> Process s a
+mkProcess p = Zip [] RStop (toP p Done)
+
+-- | Run a process (in case you do not need the incremental interface)
+run :: Process s a -> [s] -> (a, [String])
+run p input = evalR $ pushEof $ pushSyms input p
+
+testNext :: (Maybe s -> Bool) -> Parser s ()
+testNext f = Look (if f Nothing then ok else empty) (\s ->
+   if f $ Just s then ok else empty)
+    where ok = pure ()
+
+lookNext :: Parser s (Maybe s)
+lookNext = Look (pure Nothing) (pure . Just)
+
+
+
+-- | Parse the same thing as the argument, but will be used only as
+-- backup. ie, it will be used only if disjuncted with a failing
+-- parser.
+
+recoverWith :: Parser s a -> Parser s a
+recoverWith = Enter "recoverWith" . Yuck
+
+----------------------------------------------------
+
+--------------------------------
+-- The zipper for efficient evaluation:
+
+-- Arbitrary expressions in Reverse Polish notation.
+-- This can also be seen as an automaton that transforms a stack.
+-- RPolish is indexed by the types in the stack consumed by the automaton (input),
+-- and the stack produced (output)
+data RPolish input output where
+  RPush :: a -> RPolish (a :< rest) output -> RPolish rest output
+  RApp :: RPolish (b :< rest) output -> RPolish ((a -> b) :< a :< rest) output
+  RStop :: RPolish rest rest
+
+-- Evaluate the output of an RP automaton, given an input stack
+evalRP :: RPolish input output -> input -> output
+evalRP RStop  acc = acc
+evalRP (RPush v r) acc = evalRP r (v :< acc)
+evalRP (RApp r) ~(f :< ~(a :< rest)) = evalRP r (f a :< rest)
+
+-- execute the automaton as far as possible
+simplify :: RPolish s output -> RPolish s output
+simplify (RPush x (RPush f (RApp r))) = simplify (RPush (f x) r)
+simplify x = x
+
+evalR :: Zip token (a :< rest) -> (a, [String])
+evalR (Zip errs l r) = ((top . evalRP l) *** (errs ++)) (evalR' r)
+
+-- Gluing a Polish expression and an RP automaton.
+-- This can also be seen as a zipper of Polish expressions.
+data Zip s output where
+   Zip :: [String] -> RPolish mid output -> Steps s mid -> Zip s output
+   -- note that the Stack produced by the Polish expression matches
+   -- the stack consumed by the RP automaton.
+
+fullLog :: Zip s output -> ([String],Tree LogEntry)
+fullLog (Zip msg _ rhs) = (reverse msg, rightLog rhs)
+
+instance Show (Zip s output) where
+    show (Zip errs l r) = show l ++ "<>" ++ show r ++ ", errs = " ++ show errs
+
+onRight :: (forall r. Steps s r -> Steps s r) -> Zip s a -> Zip s a
+onRight f (Zip errs x y) = Zip errs x (f y)
+
+
+type Process token result = Zip token (result :< ())
diff --git a/src/System/CanonicalizePath.hs b/src/System/CanonicalizePath.hs
new file mode 100644
--- /dev/null
+++ b/src/System/CanonicalizePath.hs
@@ -0,0 +1,104 @@
+{-# LANGUAGE CPP               #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# OPTIONS_HADDOCK show-extensions #-}
+
+-- |
+-- Module      :  System.CanonicalizePath
+-- License     :  GPL-2
+-- Maintainer  :  yi-devel@googlegroups.com
+-- Stability   :  experimental
+-- Portability :  portable
+--
+-- System.Directory.canonicalizePath replacement
+module System.CanonicalizePath
+  ( canonicalizePath
+  , normalisePath
+  , replaceShorthands
+  ) where
+
+
+#ifdef mingw32_HOST_OS
+import           System.FilePath          (normalise)
+import qualified System.Win32             as Win32
+#endif
+
+import           Control.Exc              (ignoringException)
+import           Control.Monad            (foldM)
+import           Data.List.Split          (splitOneOf)
+import           Data.Monoid              ((<>))
+import qualified Data.Text                as T (Text, empty, splitOn)
+import           System.Directory         (getCurrentDirectory)
+import           System.FilePath          (isAbsolute, isDrive, pathSeparator,
+                                           pathSeparators, takeDirectory, (</>))
+import           System.PosixCompat.Files (readSymbolicLink)
+
+-- | Removes `/./` `//` and `/../` sequences from path,
+-- doesn't follow symlinks
+normalisePath :: FilePath -> IO FilePath
+normalisePath path = do
+  absPath <- makeAbsolute path
+  return $ foldl combinePath "/" $ splitPath absPath
+
+-- | Returns absolute name of the file, which doesn't contain
+-- any `/./`, `/../`, `//` sequences or symlinks
+canonicalizePath :: FilePath -> IO FilePath
+canonicalizePath path = do
+#if !defined(mingw32_HOST_OS)
+  absPath <- makeAbsolute path
+  foldM (\x y -> expandSym $ combinePath x y) "/" $ splitPath absPath
+#else
+  Win32.getFullPathName . normalise $ path
+#endif
+
+-- | Dereferences symbolic links until regular
+-- file/directory/something_else appears
+expandSym :: FilePath -> IO FilePath
+expandSym fpath = do
+  -- System.Posix.Files.getFileStatus dereferences symlink before
+  -- checking its status, so it's useless here
+  deref <- ignoringException (Just <$> readSymbolicLink fpath)
+  case deref of
+    Just slink -> expandSym (if isAbsolute slink
+                    then slink
+                    else foldl combinePath (takeDirectory fpath) $ splitPath slink)
+    Nothing -> return fpath
+
+-- | Make a path absolute.
+makeAbsolute :: FilePath -> IO FilePath
+makeAbsolute f
+    | not (null f) && head f `elem` ['~', pathSeparator] = return f
+    | otherwise = fmap (</> f) getCurrentDirectory
+
+-- | Combines two paths, moves up one level on ..
+combinePath :: FilePath -> String -> FilePath
+combinePath x "."  = x
+combinePath x ".." = takeDirectory x
+combinePath "/" y  = "/" </> y
+combinePath x y
+    | isDrive x = (x ++ [pathSeparator]) </> y -- "C:" </> "bin" = "C:bin"
+    | otherwise = x </> y
+
+-- Replace utility shorthands, similar to Emacs
+--
+-- @
+-- somepath//someotherpath  ≅ /someotherpath
+-- somepath/~/someotherpath ≅ ~/someotherpath
+-- @
+replaceShorthands :: T.Text -> T.Text
+replaceShorthands = r "/~" "~/" . r "//" "/"
+  where
+    r :: T.Text -> T.Text -> T.Text -> T.Text
+    r s r' a = case T.splitOn s a of
+      []     -> T.empty
+      [a']   -> a'
+      _ : as -> r' <> last as
+
+-- | Splits path into parts by path separator
+--
+-- Text version would look like
+--
+-- @'T.filter' (not . T.null) . T.split (`elem` pathSeparators)@
+--
+-- But we should move to @system-filepath@ package anyway.
+splitPath :: FilePath -> [String]
+splitPath = filter (not . null) . splitOneOf pathSeparators
diff --git a/src/System/FriendlyPath.hs b/src/System/FriendlyPath.hs
new file mode 100644
--- /dev/null
+++ b/src/System/FriendlyPath.hs
@@ -0,0 +1,34 @@
+module System.FriendlyPath
+  ( userToCanonPath
+  , expandTilda
+  , isAbsolute'
+  ) where
+
+import System.CanonicalizePath (canonicalizePath)
+import System.Directory        (getHomeDirectory)
+import System.FilePath         (isAbsolute, normalise, pathSeparator)
+import System.PosixCompat.User (getUserEntryForName, homeDirectory)
+
+
+-- canonicalizePath follows symlinks, and does not work if the directory does not exist.
+
+-- | Canonicalize a user-friendly path
+userToCanonPath :: FilePath -> IO String
+userToCanonPath f = canonicalizePath =<< expandTilda f
+
+-- | Turn a user-friendly path into a computer-friendly path by expanding the leading tilda.
+expandTilda :: String -> IO FilePath
+expandTilda ('~':path)
+  | null path || (head path == pathSeparator) = (++ path) <$> getHomeDirectory
+  -- Home directory of another user, e.g. ~root/
+  | otherwise = let username = takeWhile (/= pathSeparator) path
+                    dirname = drop (length username) path
+                in  (normalise . (++ dirname) . homeDirectory) <$> getUserEntryForName username
+expandTilda path = return path
+
+-- | Is a user-friendly path absolute?
+isAbsolute' :: String -> Bool
+isAbsolute' ('~':_) = True
+isAbsolute' p = isAbsolute p
+
+
diff --git a/src/Yi.hs b/src/Yi.hs
new file mode 100644
--- /dev/null
+++ b/src/Yi.hs
@@ -0,0 +1,55 @@
+{-# LANGUAGE CPP #-}
+{-# OPTIONS_HADDOCK show-extensions #-}
+
+-- |
+-- Module      :  Yi
+-- License     :  GPL-2
+-- Maintainer  :  yi-devel@googlegroups.com
+-- Stability   :  experimental
+-- Portability :  portable
+--
+-- Facade of the Yi library, for use by confguration file. Just
+-- re-exports a bunch of modules.
+--
+-- You should therefore:
+--
+-- @     import Yi@
+--
+-- in your @~/.config/yi/yi.hs@.
+
+module Yi
+  (
+    module Data.Prototype, -- prototypes are mainly there for config; makes sense to export them.
+    module Yi.Boot,
+    module Yi.Buffer,
+    module Yi.Config,
+    module Yi.Config.Default,
+    module Yi.Core,
+    module Yi.Dired,
+    module Yi.Editor,
+    module Yi.Eval,
+    module Yi.File,
+    module Yi.Keymap,
+    module Yi.Keymap.Keys,
+    module Yi.Misc,
+    module Yi.Search,
+    module Yi.Style,
+    module Yi.Style.Library,
+  ) where
+
+import Data.Prototype
+import Yi.Boot
+import Yi.Buffer
+import Yi.Config
+import Yi.Config.Default
+import Yi.Core
+import Yi.Dired
+import Yi.Editor
+import Yi.Eval
+import Yi.File
+import Yi.Keymap
+import Yi.Keymap.Keys
+import Yi.Misc
+import Yi.Search
+import Yi.Style
+import Yi.Style.Library
diff --git a/src/Yi/Boot.hs b/src/Yi/Boot.hs
new file mode 100644
--- /dev/null
+++ b/src/Yi/Boot.hs
@@ -0,0 +1,97 @@
+{-# LANGUAGE CPP #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# OPTIONS_HADDOCK show-extensions #-}
+
+-- |
+-- Module      :  Yi.Boot
+-- License     :  GPL-2
+-- Maintainer  :  yi-devel@googlegroups.com
+-- Stability   :  experimental
+-- Portability :  portable
+--
+-- Boot process of Yi.
+--
+-- Uses Dyre to implement the XMonad-style dynamic reconfiguration.
+
+module Yi.Boot (yi, yiDriver, yiDriver', reload) where
+
+import qualified Config.Dyre as Dyre
+import qualified Config.Dyre.Options as Dyre
+import qualified Config.Dyre.Params as Dyre
+import           Config.Dyre.Relaunch
+import           Lens.Micro.Platform
+import           Data.Text ()
+import qualified Data.Text.IO as T (putStrLn)
+import           System.Environment
+import           System.Exit
+import           Yi.Boot.Internal
+import           Yi.Buffer.Misc (BufferId(..))
+import           Yi.Config
+import           Yi.Editor
+import           Yi.Keymap
+import           Yi.Main
+import           Yi.Option (OptionError(..))
+import           Yi.Paths (getCustomConfigPath)
+import           Yi.Rope (fromString)
+
+-- | Once the custom yi is compiled this restores the editor state (if
+-- requested) then proceeds to run the editor.
+realMain :: (Config, ConsoleConfig) -> IO ()
+realMain configs = restoreBinaryState Nothing >>= main configs
+
+-- | If the custom yi compile produces errors or warnings then the
+-- messages are presented as a separate activity in the editor.
+--
+-- The use of a separate activity prevents any other initial actions
+-- from immediately masking the output.
+showErrorsInConf :: (Config, ConsoleConfig) -> String -> (Config, ConsoleConfig)
+showErrorsInConf c errs = c & _1 . initialActionsA %~ (makeAction openErrBuf :)
+  where
+    openErrBuf = splitE >> newBufferE (MemBuffer "*errors*") (fromString errs)
+
+-- | Handy alias for 'yiDriver'.
+yi :: Config -> IO ()
+yi = yiDriver
+
+-- | Called from the yi built with the user's configuration. Does not
+-- ignore unknown arguments.
+yiDriver :: Config -> IO ()
+yiDriver = yiDriver' False
+
+-- | Used by both the yi executable and the custom yi that is built
+-- from the user's configuration. The yi executable uses a default
+-- config.
+yiDriver' :: Bool -> Config -> IO ()
+yiDriver' ignoreUnknownArgs cfg = do
+  args <- Dyre.withDyreOptions Dyre.defaultParams getArgs
+  -- we do the arg processing before dyre, so we can extract
+  -- '--ghc-option=' and '--help' and so on.
+  case do_args ignoreUnknownArgs cfg args of
+    Left (OptionError err code) -> T.putStrLn err >> exitWith code
+    Right (finalCfg, cfgcon) -> do
+      modules <- getCustomConfigPath (userConfigDir cfgcon) "modules"
+      let yiParams = Dyre.defaultParams
+                      { Dyre.projectName  = "yi"
+                      , Dyre.realMain     = realMain
+                      , Dyre.showError    = showErrorsInConf
+                      , Dyre.configDir    = Just $ userConfigDir cfgcon
+                      , Dyre.ghcOpts      = ["-threaded", "-O2", "-rtsopts"]
+                                            ++ ["-i" ++ modules]
+                                            ++ profilingParams
+                                            ++ ghcOptions cfgcon
+                      , Dyre.includeCurrentDirectory = False
+                      , Dyre.rtsOptsHandling = Dyre.RTSAppend ["-I5"]
+                      }
+      Dyre.wrapMain yiParams (finalCfg, cfgcon)
+
+-- | CPP-guarded profiling params.
+profilingParams :: [String]
+profilingParams =
+#ifdef EVENTLOG
+  ["-eventlog", "-rtsopts"] ++
+#endif
+#ifdef PROFILING
+  ["-prof", "-auto-all", "-rtsopts"
+  , "-osuf=p_o", "-hisuf=p_hi"] ++
+#endif
+  []
diff --git a/src/Yi/Boot/Internal.hs b/src/Yi/Boot/Internal.hs
new file mode 100644
--- /dev/null
+++ b/src/Yi/Boot/Internal.hs
@@ -0,0 +1,30 @@
+{-# OPTIONS_HADDOCK show-extensions #-}
+
+-- |
+-- Module      :  Yi.Boot.Internal
+-- License     :  GPL-2
+-- Maintainer  :  yi-devel@googlegroups.com
+-- Stability   :  experimental
+-- Portability :  portable
+--
+-- Internal use for Yi.Boot
+
+module Yi.Boot.Internal where
+
+import Config.Dyre.Relaunch (relaunchWithBinaryState)
+import Control.Monad.Base   (liftBase)
+import Control.Monad.State  (get)
+import Yi.Keymap            (YiM, withUI)
+import Yi.Types             (withEditor)
+import Yi.UI.Common         (end)
+
+-- | "reloads" the configuration
+--
+-- Serializes the editor state and relaunches Yi using the serialized
+-- state. The launch of Yi will result in recompilation of the user's
+-- custom Yi. This, in effect, "reloads" the configuration.
+reload :: YiM ()
+reload = do
+  editor <- withEditor get
+  withUI (`end` False)
+  liftBase $ relaunchWithBinaryState (Just editor) Nothing
diff --git a/src/Yi/Buffer.hs b/src/Yi/Buffer.hs
new file mode 100644
--- /dev/null
+++ b/src/Yi/Buffer.hs
@@ -0,0 +1,42 @@
+{-# OPTIONS_HADDOCK show-extensions #-}
+
+-- |
+-- Module      :  Yi.Buffer
+-- License     :  GPL-2
+-- Maintainer  :  yi-devel@googlegroups.com
+-- Stability   :  experimental
+-- Portability :  portable
+--
+-- The 'Buffer' module defines monadic editing operations over
+-- one-dimensional buffers, maintaining a current /point/.
+--
+-- This module acts as a facade for the Buffer.* modules.
+
+module Yi.Buffer
+  ( module Yi.Buffer.Basic
+  , module Yi.Buffer.HighLevel
+  , module Yi.Buffer.Indent
+  , module Yi.Buffer.Misc
+  , module Yi.Buffer.Normal
+  , module Yi.Buffer.Region
+  , module Yi.Buffer.TextUnit
+  , module Yi.Buffer.Undo
+  -- Implementation re-exports (move out of implementation?)
+  , UIUpdate (..)
+  , Update (..)
+  , updateIsDelete
+  , markGravityAA
+  , markPointAA
+  )
+where
+
+import Yi.Buffer.Basic
+import Yi.Buffer.HighLevel
+import Yi.Buffer.Indent
+import Yi.Buffer.Misc
+import Yi.Buffer.Normal
+import Yi.Buffer.Region
+import Yi.Buffer.TextUnit
+import Yi.Buffer.Undo
+
+import Yi.Buffer.Implementation
diff --git a/src/Yi/Buffer/Adjusted.hs b/src/Yi/Buffer/Adjusted.hs
new file mode 100644
--- /dev/null
+++ b/src/Yi/Buffer/Adjusted.hs
@@ -0,0 +1,83 @@
+{-# OPTIONS_HADDOCK show-extensions #-}
+
+-- |
+-- Module      :  Yi.Buffer.Adjusted
+-- License     :  GPL-2
+-- Maintainer  :  yi-devel@googlegroups.com
+-- Stability   :  experimental
+-- Portability :  portable
+--
+-- This module re-exports Yi.Buffer overriding insert* and delete* functions
+-- with their more indent-aware variants. It is intended to be imported
+-- instead of Yi.Buffer or qualified to avoid name clashes.
+
+module Yi.Buffer.Adjusted
+    ( bdeleteB
+    , insertB
+    , insertN
+    , insertNAt
+    , deleteB
+    , deleteN
+    , deleteRegionB
+    , deleteRegionWithStyleB
+    , module Yi.Buffer
+    ) where
+
+import           Control.Monad       (forM_, when)
+import           Yi.Buffer           hiding (bdeleteB, insertB, insertN, insertNAt
+                                            , deleteB, deleteN, deleteNAt
+                                            , deleteRegionB, deleteRegionWithStyleB)
+import qualified Yi.Buffer           as B (deleteN, insertB, insertNAt)
+import           Yi.Misc             (adjBlock)
+import qualified Yi.Rope             as R (YiString, countNewLines, length, take)
+import           Yi.Utils            (SemiNum ((~-)))
+
+insertNAt :: R.YiString -> Point -> BufferM ()
+insertNAt rope point | R.countNewLines rope > 0 = B.insertNAt rope point
+insertNAt rope point = B.insertNAt rope point >> adjBlock (R.length rope)
+
+-- | Insert the list at current point, extending size of buffer
+insertN :: R.YiString -> BufferM ()
+insertN rope = insertNAt rope =<< pointB
+
+-- | Insert the char at current point, extending size of buffer
+insertB :: Char -> BufferM ()
+insertB c = B.insertB c >> adjBlock 1
+
+-- | @deleteNAt n p@ deletes @n@ characters forwards from position @p@
+deleteNAt :: Direction -> Int -> Point -> BufferM ()
+deleteNAt dir n pos = do
+  els <- R.take n <$> streamB Forward pos
+  applyUpdate (Delete pos dir els)
+  when (R.countNewLines els == 0) $
+      adjBlock (-(R.length els))
+
+deleteN :: Int -> BufferM ()
+deleteN n = pointB >>= deleteNAt Forward n
+
+deleteB :: TextUnit -> Direction -> BufferM ()
+deleteB unit dir = deleteRegionB =<< regionOfPartNonEmptyB unit dir
+
+bdeleteB :: BufferM ()
+bdeleteB = deleteB Character Backward
+
+deleteRegionB :: Region -> BufferM ()
+deleteRegionB r =
+    deleteNAt (regionDirection r)
+              (fromIntegral (regionEnd r ~- regionStart r))
+              (regionStart r)
+
+deleteRegionWithStyleB :: Region -> RegionStyle -> BufferM Point
+deleteRegionWithStyleB reg Block = savingPointB $ do
+    (start, lengths) <- shapeOfBlockRegionB reg
+    moveTo start
+    forM_ (zip [1..] lengths) $ \(i, l) -> do
+        B.deleteN l
+        moveTo start
+        lineMoveRel i
+    return start
+
+deleteRegionWithStyleB reg style = savingPointB $ do
+    effectiveRegion <- convertRegionToStyleB reg style
+    deleteRegionB effectiveRegion
+    return $! regionStart effectiveRegion
diff --git a/src/Yi/Buffer/HighLevel.hs b/src/Yi/Buffer/HighLevel.hs
new file mode 100644
--- /dev/null
+++ b/src/Yi/Buffer/HighLevel.hs
@@ -0,0 +1,1184 @@
+{-# LANGUAGE LambdaCase        #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# OPTIONS_HADDOCK show-extensions #-}
+{-# LANGUAGE MultiWayIf        #-}
+
+-- |
+-- Module      :  Yi.Buffer.HighLevel
+-- License     :  GPL-2
+-- Maintainer  :  yi-devel@googlegroups.com
+-- Stability   :  experimental
+-- Portability :  portable
+--
+-- High level operations on buffers.
+
+module Yi.Buffer.HighLevel
+    ( atEof
+    , atEol
+    , atLastLine
+    , atSol
+    , atSof
+    , bdeleteB
+    , bdeleteLineB
+    , bkillWordB
+    , botB
+    , bufInfoB
+    , BufferFileInfo (..)
+    , capitaliseWordB
+    , deleteBlankLinesB
+    , deleteHorizontalSpaceB
+    , deleteRegionWithStyleB
+    , deleteToEol
+    , deleteTrailingSpaceB
+    , downFromTosB
+    , downScreenB
+    , downScreensB
+    , exchangePointAndMarkB
+    , fillParagraph
+    , findMatchingPairB
+    , firstNonSpaceB
+    , flipRectangleB
+    , getBookmarkB
+    , getLineAndCol
+    , getLineAndColOfPoint
+    , getNextLineB
+    , getNextNonBlankLineB
+    , getRawestSelectRegionB
+    , getSelectionMarkPointB
+    , getSelectRegionB
+    , gotoCharacterB
+    , hasWhiteSpaceBefore
+    , incrementNextNumberByB
+    , insertRopeWithStyleB
+    , isCurrentLineAllWhiteSpaceB
+    , isCurrentLineEmptyB
+    , isNumberB
+    , killWordB
+    , lastNonSpaceB
+    , leftEdgesOfRegionB
+    , leftOnEol
+    , lineMoveVisRel
+    , linePrefixSelectionB
+    , lineStreamB
+    , lowercaseWordB
+    , middleB
+    , modifyExtendedSelectionB
+    , moveNonspaceOrSol
+    , movePercentageFileB
+    , moveToMTB
+    , moveToEol
+    , moveToSol
+    , moveXorEol
+    , moveXorSol
+    , nextCExc
+    , nextCInc
+    , nextCInLineExc
+    , nextCInLineInc
+    , nextNParagraphs
+    , nextWordB
+    , prevCExc
+    , prevCInc
+    , prevCInLineExc
+    , prevCInLineInc
+    , prevNParagraphs
+    , prevWordB
+    , readCurrentWordB
+    , readLnB
+    , readPrevWordB
+    , readRegionRopeWithStyleB
+    , replaceBufferContent
+    , revertB
+    , rightEdgesOfRegionB
+    , scrollB
+    , scrollCursorToBottomB
+    , scrollCursorToTopB
+    , scrollScreensB
+    , scrollToCursorB
+    , scrollToLineAboveWindowB
+    , scrollToLineBelowWindowB
+    , setSelectionMarkPointB
+    , setSelectRegionB
+    , shapeOfBlockRegionB
+    , sortLines
+    , sortLinesWithRegion
+    , snapInsB
+    , snapScreenB
+    , splitBlockRegionToContiguousSubRegionsB
+    , swapB
+    , switchCaseChar
+    , test3CharB
+    , testHexB
+    , toggleCommentB
+    , topB
+    , unLineCommentSelectionB
+    , upFromBosB
+    , uppercaseWordB
+    , upScreenB
+    , upScreensB
+    , vimScrollB
+    , vimScrollByB
+    , markWord
+    ) where
+
+import           Lens.Micro.Platform      (over, use, (%=), (.=), _last)
+import           Control.Monad            (forM, forM_, replicateM_, unless, void, when)
+import           Control.Monad.RWS.Strict (ask)
+import           Control.Monad.State      (gets)
+import           Data.Char                (isDigit, isHexDigit, isOctDigit, isSpace, isUpper, toLower, toUpper)
+import           Data.List                (intersperse, sort)
+import           Data.Maybe               (catMaybes, fromMaybe, listToMaybe)
+import           Data.Monoid              ((<>))
+import qualified Data.Text                as T (Text, toLower, toUpper, unpack)
+import           Data.Time                (UTCTime)
+import           Data.Tuple               (swap)
+import           Numeric                  (readHex, readOct, showHex, showOct)
+import           Yi.Buffer.Basic          (Direction (..), Mark, Point (..), Size (Size))
+import           Yi.Buffer.Misc
+import           Yi.Buffer.Normal
+import           Yi.Buffer.Region
+import           Yi.Config.Misc           (ScrollStyle (SingleLine))
+import           Yi.Rope                  (YiString)
+import qualified Yi.Rope                  as R
+import           Yi.String                (capitalizeFirst, fillText, isBlank, mapLines, onLines, overInit)
+import           Yi.Utils                 (SemiNum ((+~), (-~)))
+import           Yi.Window                (Window (actualLines, width, wkey))
+
+-- ---------------------------------------------------------------------
+-- Movement operations
+
+
+-- | Move point between the middle, top and bottom of the screen
+-- If the point stays at the middle, it'll be gone to the top
+-- else if the point stays at the top, it'll be gone to the bottom
+-- else it'll be gone to the middle
+moveToMTB :: BufferM ()
+moveToMTB = (==) <$> curLn <*> screenMidLn >>= \case
+    True -> downFromTosB 0
+    _    -> (==) <$> curLn <*> screenTopLn >>= \case
+                True -> upFromBosB 0
+                _    -> downFromTosB =<< (-) <$> screenMidLn <*> screenTopLn
+
+
+-- | Move point to start of line
+moveToSol :: BufferM ()
+moveToSol = maybeMoveB Line Backward
+
+-- | Move point to end of line
+moveToEol :: BufferM ()
+moveToEol = maybeMoveB Line Forward
+
+-- | Move cursor to origin
+topB :: BufferM ()
+topB = moveTo 0
+
+-- | Move cursor to end of buffer
+botB :: BufferM ()
+botB = moveTo =<< sizeB
+
+-- | Move left if on eol, but not on blank line
+leftOnEol :: BufferM ()
+-- @savingPrefCol@ is needed, because deep down @leftB@ contains @forgetPrefCol@
+-- which messes up vertical cursor motion in Vim normal mode
+leftOnEol = savingPrefCol $ do
+        eol <- atEol
+        sol <- atSol
+        when (eol && not sol) leftB
+
+-- | Move @x@ chars back, or to the sol, whichever is less
+moveXorSol :: Int -> BufferM ()
+moveXorSol x = replicateM_ x $ do c <- atSol; unless c leftB
+
+-- | Move @x@ chars forward, or to the eol, whichever is less
+moveXorEol :: Int -> BufferM ()
+moveXorEol x = replicateM_ x $ do c <- atEol; unless c rightB
+
+-- | Move to first char of next word forwards
+nextWordB :: BufferM ()
+nextWordB = moveB unitWord Forward
+
+-- | Move to first char of next word backwards
+prevWordB :: BufferM ()
+prevWordB = moveB unitWord Backward
+
+-- * Char-based movement actions.
+
+gotoCharacterB :: Char -> Direction -> RegionStyle -> Bool -> BufferM ()
+gotoCharacterB c dir style stopAtLineBreaks = do
+    start <- pointB
+    let predicate = if stopAtLineBreaks then (`elem` [c, '\n']) else (== c)
+        (move, moveBack) = if dir == Forward then (rightB, leftB) else (leftB, rightB)
+    doUntilB_ (predicate <$> readB) move
+    b <- readB
+    if stopAtLineBreaks && b == '\n'
+    then moveTo start
+    else when (style == Exclusive && b == c) moveBack
+
+-- | Move to the next occurence of @c@
+nextCInc :: Char -> BufferM ()
+nextCInc c = gotoCharacterB c Forward Inclusive False
+
+nextCInLineInc :: Char -> BufferM ()
+nextCInLineInc c = gotoCharacterB c Forward Inclusive True
+
+-- | Move to the character before the next occurence of @c@
+nextCExc :: Char -> BufferM ()
+nextCExc c = gotoCharacterB c Forward Exclusive False
+
+nextCInLineExc :: Char -> BufferM ()
+nextCInLineExc c = gotoCharacterB c Forward Exclusive True
+
+-- | Move to the previous occurence of @c@
+prevCInc :: Char -> BufferM ()
+prevCInc c = gotoCharacterB c Backward Inclusive False
+
+prevCInLineInc :: Char -> BufferM ()
+prevCInLineInc c = gotoCharacterB c Backward Inclusive True
+
+-- | Move to the character after the previous occurence of @c@
+prevCExc :: Char -> BufferM ()
+prevCExc c = gotoCharacterB c Backward Exclusive False
+
+prevCInLineExc :: Char -> BufferM ()
+prevCInLineExc c = gotoCharacterB c Backward Exclusive True
+
+-- | Move to first non-space character in this line
+firstNonSpaceB :: BufferM ()
+firstNonSpaceB = do
+  moveToSol
+  untilB_ ((||) <$> atEol <*> ((not . isSpace) <$> readB)) rightB
+
+-- | Move to the last non-space character in this line
+lastNonSpaceB :: BufferM ()
+lastNonSpaceB = do
+  moveToEol
+  untilB_ ((||) <$> atSol <*> ((not . isSpace) <$> readB)) leftB
+
+-- | Go to the first non space character in the line;
+-- if already there, then go to the beginning of the line.
+moveNonspaceOrSol :: BufferM ()
+moveNonspaceOrSol = do
+  prev <- readPreviousOfLnB
+  if R.all isSpace prev then moveToSol else firstNonSpaceB
+
+-- | True if current line consists of just a newline (no whitespace)
+isCurrentLineEmptyB :: BufferM Bool
+isCurrentLineEmptyB = savingPointB $ moveToSol >> atEol
+
+-- | Note: Returns False if line doesn't have any characters besides a newline
+isCurrentLineAllWhiteSpaceB :: BufferM Bool
+isCurrentLineAllWhiteSpaceB = savingPointB $ do
+    isEmpty <- isCurrentLineEmptyB
+    if isEmpty
+    then return False
+    else do
+        let go = do
+                  eol <- atEol
+                  if eol
+                  then return True
+                  else do
+                      c <- readB
+                      if isSpace c
+                      then rightB >> go
+                      else return False
+        moveToSol
+        go
+
+------------
+
+-- | Move down next @n@ paragraphs
+nextNParagraphs :: Int -> BufferM ()
+nextNParagraphs n = replicateM_ n $ moveB unitEmacsParagraph Forward
+
+-- | Move up prev @n@ paragraphs
+prevNParagraphs :: Int -> BufferM ()
+prevNParagraphs n = replicateM_ n $ moveB unitEmacsParagraph Backward
+
+-- ! Examples:
+-- @goUnmatchedB Backward '(' ')'@
+-- Move to the previous unmatched '('
+-- @goUnmatchedB Forward '{' '}'@
+-- Move to the next unmatched '}'
+goUnmatchedB :: Direction -> Char -> Char -> BufferM ()
+goUnmatchedB dir cStart' cStop' = getLineAndCol >>= \position ->
+    stepB >> readB >>= go position (0::Int)
+    where
+        go pos opened c
+           | c == cStop && opened == 0 = return ()
+           | c == cStop       = goIfNotEofSof pos (opened-1)
+           | c == cStart      = goIfNotEofSof pos (opened+1)
+           | otherwise        = goIfNotEofSof pos  opened
+        goIfNotEofSof pos opened = atEof >>= \eof -> atSof >>= \sof ->
+            if not eof && not sof
+                then stepB >> readB >>= go pos opened
+                else gotoLn (fst pos) >> moveToColB (snd pos)
+        (stepB, cStart, cStop) | dir == Forward = (rightB, cStart', cStop')
+                               | otherwise      = (leftB, cStop', cStart')
+
+-----------------------------------------------------------------------
+-- Queries
+
+-- | Return true if the current point is the start of a line
+atSol :: BufferM Bool
+atSol = atBoundaryB Line Backward
+
+-- | Return true if the current point is the end of a line
+atEol :: BufferM Bool
+atEol = atBoundaryB Line Forward
+
+-- | True if point at start of file
+atSof :: BufferM Bool
+atSof = atBoundaryB Document Backward
+
+-- | True if point at end of file
+atEof :: BufferM Bool
+atEof = atBoundaryB Document Forward
+
+-- | True if point at the last line
+atLastLine :: BufferM Bool
+atLastLine = savingPointB $ do
+    moveToEol
+    (==) <$> sizeB <*> pointB
+
+-- | Get the current line and column number
+getLineAndCol :: BufferM (Int, Int)
+getLineAndCol = (,) <$> curLn <*> curCol
+
+getLineAndColOfPoint :: Point -> BufferM (Int, Int)
+getLineAndColOfPoint p = savingPointB $ moveTo p >> getLineAndCol
+
+-- | Read the line the point is on
+readLnB :: BufferM YiString
+readLnB = readUnitB Line
+
+-- | Read from point to beginning of line
+readPreviousOfLnB :: BufferM YiString
+readPreviousOfLnB = readRegionB =<< regionOfPartB Line Backward
+
+hasWhiteSpaceBefore :: BufferM Bool
+hasWhiteSpaceBefore = fmap isSpace (prevPointB >>= readAtB)
+
+-- | Get the previous point, unless at the beginning of the file
+prevPointB :: BufferM Point
+prevPointB = do
+  sof <- atSof
+  if sof then pointB
+         else do p <- pointB
+                 return $ Point (fromPoint p - 1)
+
+-- | Reads in word at point.
+readCurrentWordB :: BufferM YiString
+readCurrentWordB = readUnitB unitWord
+
+-- | Reads in word before point.
+readPrevWordB :: BufferM YiString
+readPrevWordB = readPrevUnitB unitViWordOnLine
+
+-------------------------
+-- Deletes
+
+-- | Delete one character backward
+bdeleteB :: BufferM ()
+bdeleteB = deleteB Character Backward
+
+-- | Delete forward whitespace or non-whitespace depending on
+-- the character under point.
+killWordB :: BufferM ()
+killWordB = deleteB unitWord Forward
+
+-- | Delete backward whitespace or non-whitespace depending on
+-- the character before point.
+bkillWordB :: BufferM ()
+bkillWordB = deleteB unitWord Backward
+
+-- | Delete backward to the sof or the new line character
+bdeleteLineB :: BufferM ()
+bdeleteLineB = atSol >>= \sol -> if sol then bdeleteB else deleteB Line Backward
+
+
+-- UnivArgument is in Yi.Keymap.Emacs.Utils but we can't import it due
+-- to cyclic imports.
+-- | emacs' @delete-horizontal-space@ with the optional argument.
+deleteHorizontalSpaceB :: Maybe Int -> BufferM ()
+deleteHorizontalSpaceB u = do
+  c <- curCol
+  reg <- regionOfB Line
+  text <- readRegionB reg
+  let (r, jb) = deleteSpaces c text
+  modifyRegionB (const r) reg
+  -- Jump backwards to where the now-deleted spaces have started so
+  -- it's consistent and feels natural instead of leaving us somewhere
+  -- in the text.
+  moveToColB $ c - jb
+  where
+    deleteSpaces :: Int -> R.YiString -> (R.YiString, Int)
+    deleteSpaces c l =
+      let (f, b) = R.splitAt c l
+          f' = R.dropWhileEnd isSpace f
+          cleaned = f' <> case u of
+            Nothing -> R.dropWhile isSpace b
+            Just _ -> b
+      -- We only want to jump back the number of spaces before the
+      -- point, not the total number of characters we're removing.
+      in (cleaned, R.length f - R.length f')
+
+----------------------------------------
+-- Transform operations
+
+-- | capitalise the word under the cursor
+uppercaseWordB :: BufferM ()
+uppercaseWordB = transformB (R.withText T.toUpper) unitWord Forward
+
+-- | lowerise word under the cursor
+lowercaseWordB :: BufferM ()
+lowercaseWordB = transformB (R.withText T.toLower) unitWord Forward
+
+-- | capitalise the first letter of this word
+capitaliseWordB :: BufferM ()
+capitaliseWordB = transformB capitalizeFirst unitWord Forward
+
+switchCaseChar :: Char -> Char
+switchCaseChar c = if isUpper c then toLower c else toUpper c
+
+-- | Delete to the end of line, excluding it.
+deleteToEol :: BufferM ()
+deleteToEol = deleteRegionB =<< regionOfPartB Line Forward
+
+-- | Transpose two characters, (the Emacs C-t action)
+swapB :: BufferM ()
+swapB = do eol <- atEol
+           when eol leftB
+           transposeB Character Forward
+
+-- | Delete trailing whitespace from all lines. Uses 'savingPositionB'
+-- to get back to where it was.
+deleteTrailingSpaceB :: BufferM ()
+deleteTrailingSpaceB =
+  regionOfB Document >>=
+  savingPositionB . modifyRegionB (tru . mapLines stripEnd)
+  where
+    -- Strips the space from the end of each line, preserving
+    -- newlines.
+    stripEnd :: R.YiString -> R.YiString
+    stripEnd x = case R.last x of
+      Nothing -> x
+      Just '\n' -> (`R.snoc` '\n') $ R.dropWhileEnd isSpace x
+      _ -> R.dropWhileEnd isSpace x
+
+    -- | Cut off trailing newlines, making sure to preserve one.
+    tru :: R.YiString -> R.YiString
+    tru x = if R.length x == 0
+            then x
+            else (`R.snoc` '\n') $ R.dropWhileEnd (== '\n') x
+
+-- ----------------------------------------------------
+-- | Marks
+
+-- | Set the current buffer selection mark
+setSelectionMarkPointB :: Point -> BufferM ()
+setSelectionMarkPointB p = (.= p) . markPointA =<< selMark <$> askMarks
+
+-- | Get the current buffer selection mark
+getSelectionMarkPointB :: BufferM Point
+getSelectionMarkPointB = use . markPointA =<< selMark <$> askMarks
+
+-- | Exchange point & mark.
+exchangePointAndMarkB :: BufferM ()
+exchangePointAndMarkB = do m <- getSelectionMarkPointB
+                           p <- pointB
+                           setSelectionMarkPointB p
+                           moveTo m
+
+getBookmarkB :: String -> BufferM Mark
+getBookmarkB = getMarkB . Just
+
+
+-- ---------------------------------------------------------------------
+-- Buffer operations
+
+data BufferFileInfo =
+    BufferFileInfo { bufInfoFileName :: FilePath
+                   , bufInfoSize     :: Int
+                   , bufInfoLineNo   :: Int
+                   , bufInfoColNo    :: Int
+                   , bufInfoCharNo   :: Point
+                   , bufInfoPercent  :: T.Text
+                   , bufInfoModified :: Bool
+                   }
+
+-- | File info, size in chars, line no, col num, char num, percent
+bufInfoB :: BufferM BufferFileInfo
+bufInfoB = do
+    s <- sizeB
+    p <- pointB
+    m <- gets isUnchangedBuffer
+    l <- curLn
+    c <- curCol
+    nm <- gets identString
+    let bufInfo = BufferFileInfo { bufInfoFileName = T.unpack nm
+                                 , bufInfoSize     = fromIntegral s
+                                 , bufInfoLineNo   = l
+                                 , bufInfoColNo    = c
+                                 , bufInfoCharNo   = p
+                                 , bufInfoPercent  = getPercent p s
+                                 , bufInfoModified = not m
+                                 }
+    return bufInfo
+
+-----------------------------
+-- Window-related operations
+
+upScreensB :: Int -> BufferM ()
+upScreensB = scrollScreensB . negate
+
+downScreensB :: Int -> BufferM ()
+downScreensB = scrollScreensB
+
+-- | Scroll up 1 screen
+upScreenB :: BufferM ()
+upScreenB = scrollScreensB (-1)
+
+-- | Scroll down 1 screen
+downScreenB :: BufferM ()
+downScreenB = scrollScreensB 1
+
+-- | Scroll by n screens (negative for up)
+scrollScreensB :: Int -> BufferM ()
+scrollScreensB n = do
+    h <- askWindow actualLines
+    scrollB $ n * max 0 (h - 1) -- subtract some amount to get some overlap (emacs-like).
+
+-- | Same as scrollB, but also moves the cursor
+vimScrollB :: Int -> BufferM ()
+vimScrollB n = do scrollB n
+                  void $ lineMoveRel n
+
+-- | Same as scrollByB, but also moves the cursor
+vimScrollByB :: (Int -> Int) -> Int -> BufferM ()
+vimScrollByB f n = do h <- askWindow actualLines
+                      vimScrollB $ n * f h
+
+-- | Move to middle line in screen
+scrollToCursorB :: BufferM ()
+scrollToCursorB = do
+    MarkSet f i _ <- markLines
+    h <- askWindow actualLines
+    let m = f + (h `div` 2)
+    scrollB $ i - m
+
+-- | Move cursor to the top of the screen
+scrollCursorToTopB :: BufferM ()
+scrollCursorToTopB = do
+    MarkSet f i _ <- markLines
+    scrollB $ i - f
+
+-- | Move cursor to the bottom of the screen
+scrollCursorToBottomB :: BufferM ()
+scrollCursorToBottomB = do
+    MarkSet _ i _ <- markLines
+    r <- winRegionB
+    t <- lineOf (regionEnd r - 1)
+    scrollB $ i - t
+
+-- | Scroll by n lines.
+scrollB :: Int -> BufferM ()
+scrollB n = do
+  MarkSet fr _ _ <- askMarks
+  savingPointB $ do
+    moveTo =<< use (markPointA fr)
+    void $ gotoLnFrom n
+    (markPointA fr .=) =<< pointB
+  w <- askWindow wkey
+  (%=) pointFollowsWindowA (\old w' -> ((w == w') || old w'))
+
+
+-- Scroll line above window to the bottom.
+scrollToLineAboveWindowB :: BufferM ()
+scrollToLineAboveWindowB = do downFromTosB 0
+                              replicateM_ 1 lineUp
+                              scrollCursorToBottomB
+
+-- Scroll line below window to the top.
+scrollToLineBelowWindowB :: BufferM ()
+scrollToLineBelowWindowB = do upFromBosB 0
+                              replicateM_ 1 lineDown
+                              scrollCursorToTopB
+
+-- | Move the point to inside the viewable region
+snapInsB :: BufferM ()
+snapInsB = do
+    movePoint <- use pointFollowsWindowA
+    w <- askWindow wkey
+    when (movePoint w) $ do
+        r <- winRegionB
+        p <- pointB
+        moveTo $ max (regionStart r) $ min (regionEnd r) p
+
+-- | return index of Sol on line @n@ above current line
+indexOfSolAbove :: Int -> BufferM Point
+indexOfSolAbove n = pointAt $ gotoLnFrom (negate n)
+
+data  RelPosition = Above | Below | Within
+  deriving (Show)
+
+-- | return relative position of the point @p@
+-- relative to the region defined by the points @rs@ and @re@
+pointScreenRelPosition :: Point -> Point -> Point -> RelPosition
+pointScreenRelPosition p rs re
+  | rs > p && p > re = Within
+  | p < rs = Above
+  | p > re = Below
+pointScreenRelPosition _ _ _ = Within -- just to disable the non-exhaustive pattern match warning
+
+-- | Move the visible region to include the point
+snapScreenB :: Maybe ScrollStyle ->BufferM Bool
+snapScreenB style = do
+    movePoint <- use pointFollowsWindowA
+    w <- askWindow wkey
+    if movePoint w then return False else do
+        inWin <- pointInWindowB =<< pointB
+        if inWin then return False else do
+            h <- askWindow actualLines
+            r <- winRegionB
+            p <- pointB
+            let gap = case style of
+                        Just SingleLine -> case pointScreenRelPosition p (regionStart r) (regionEnd r) of
+                                             Above  -> 0
+                                             Below  -> h - 1
+                                             Within -> 0 -- Impossible but handle it anyway
+                        _               -> h `div` 2
+            i <- indexOfSolAbove gap
+            f <- fromMark <$> askMarks
+            markPointA f .= i
+            return True
+
+
+-- | Move to @n@ lines down from top of screen
+downFromTosB :: Int -> BufferM ()
+downFromTosB n = do
+  moveTo =<< use . markPointA =<< fromMark <$> askMarks
+  replicateM_ n lineDown
+
+-- | Move to @n@ lines up from the bottom of the screen
+upFromBosB :: Int -> BufferM ()
+upFromBosB n = do
+  r <- winRegionB
+  moveTo (regionEnd r - 1)
+  moveToSol
+  replicateM_ n lineUp
+
+-- | Move to middle line in screen
+middleB :: BufferM ()
+middleB = do
+  w <- ask
+  f <- fromMark <$> askMarks
+  moveTo =<< use (markPointA f)
+  replicateM_ (actualLines w `div` 2) lineDown
+
+pointInWindowB :: Point -> BufferM Bool
+pointInWindowB p = nearRegion p <$> winRegionB
+
+-----------------------------
+-- Region-related operations
+
+-- | Return the region between point and mark
+getRawestSelectRegionB :: BufferM Region
+getRawestSelectRegionB = do
+  m <- getSelectionMarkPointB
+  p <- pointB
+  return $ mkRegion p m
+
+-- | Return the empty region if the selection is not visible.
+getRawSelectRegionB :: BufferM Region
+getRawSelectRegionB = do
+  s <- use highlightSelectionA
+  if s then getRawestSelectRegionB else do
+     p <- pointB
+     return $ mkRegion p p
+
+-- | Get the current region boundaries. Extended to the current selection unit.
+getSelectRegionB :: BufferM Region
+getSelectRegionB = do
+  regionStyle <- getRegionStyle
+  r <- getRawSelectRegionB
+  convertRegionToStyleB r regionStyle
+
+-- | Select the given region: set the selection mark at the 'regionStart'
+-- and the current point at the 'regionEnd'.
+setSelectRegionB :: Region -> BufferM ()
+setSelectRegionB region = do
+  highlightSelectionA .= True
+  setSelectionMarkPointB $ regionStart region
+  moveTo $ regionEnd region
+
+------------------------------------------
+-- Some line related movements/operations
+
+deleteBlankLinesB :: BufferM ()
+deleteBlankLinesB = do
+  isThisBlank <- isBlank <$> readLnB
+  when isThisBlank $ do
+    p <- pointB
+    -- go up to the 1st blank line in the group
+    void $ whileB (R.null <$> getNextLineB Backward) lineUp
+    q <- pointB
+    -- delete the whole blank region.
+    deleteRegionB $ mkRegion p q
+
+-- | Get a (lazy) stream of lines in the buffer, starting at the /next/ line
+-- in the given direction.
+lineStreamB :: Direction -> BufferM [YiString]
+lineStreamB dir = fmap rev . R.lines <$> (streamB dir =<< pointB)
+  where
+    rev = case dir of
+      Forward -> id
+      Backward -> R.reverse
+
+-- | Get the next line of text in the given direction. This returns
+-- simply 'Nothing' if there no such line.
+getMaybeNextLineB :: Direction -> BufferM (Maybe YiString)
+getMaybeNextLineB dir = listToMaybe <$> lineStreamB dir
+
+-- | The same as 'getMaybeNextLineB' but avoids the use of the 'Maybe'
+-- type in the return by returning the empty string if there is no
+-- next line.
+getNextLineB :: Direction -> BufferM YiString
+getNextLineB dir = fromMaybe R.empty <$> getMaybeNextLineB dir
+
+-- | Get closest line to the current line (not including the current
+-- line) in the given direction which satisfies the given condition.
+-- Returns 'Nothing' if there is no line which satisfies the
+-- condition.
+getNextLineWhichB :: Direction -> (YiString -> Bool) -> BufferM (Maybe YiString)
+getNextLineWhichB dir cond = listToMaybe . filter cond <$> lineStreamB dir
+
+-- | Returns the closest line to the current line which is non-blank,
+-- in the given direction. Returns the empty string if there is no
+-- such line (for example if we are on the top line already).
+getNextNonBlankLineB :: Direction -> BufferM YiString
+getNextNonBlankLineB dir =
+  fromMaybe R.empty <$> getNextLineWhichB dir (not . R.null)
+
+------------------------------------------------
+-- Some more utility functions involving
+-- regions (generally that which is selected)
+
+modifyExtendedSelectionB :: TextUnit -> (R.YiString -> R.YiString) -> BufferM ()
+modifyExtendedSelectionB unit transform
+    = modifyRegionB transform =<< unitWiseRegion unit =<< getSelectRegionB
+
+-- | Prefix each line in the selection using the given string.
+linePrefixSelectionB :: R.YiString -- ^ The string that starts a line comment
+                     ->  BufferM ()
+linePrefixSelectionB s =
+  modifyExtendedSelectionB Line . overInit $ mapLines (s <>)
+
+-- | Uncomments the selection using the given line comment
+-- starting string. This only works for the comments which
+-- begin at the start of the line.
+unLineCommentSelectionB :: R.YiString -- ^ The string which begins a
+                                      -- line comment
+                        -> R.YiString -- ^ A potentially shorter
+                                      -- string that begins a comment
+                        -> BufferM ()
+unLineCommentSelectionB s1 s2 =
+  modifyExtendedSelectionB Line $ mapLines unCommentLine
+  where
+  (l1, l2) = (R.length s1, R.length s2)
+
+  unCommentLine :: R.YiString -> R.YiString
+  unCommentLine line = case (R.splitAt l1 line, R.splitAt l2 line) of
+    ((f, s) , (f', s')) | s1 == f   -> s
+                        | s2 == f'  -> s'
+                        | otherwise -> line
+
+-- | Just like 'toggleCommentSelectionB' but automatically inserts a
+-- whitespace suffix to the inserted comment string. In fact:
+toggleCommentB :: R.YiString -> BufferM ()
+toggleCommentB c = toggleCommentSelectionB (c `R.snoc` ' ') c
+
+-- | Toggle line comments in the selection by adding or removing a
+-- prefix to each line.
+toggleCommentSelectionB :: R.YiString -> R.YiString -> BufferM ()
+toggleCommentSelectionB insPrefix delPrefix = do
+  l <- readUnitB Line
+  if delPrefix == R.take (R.length delPrefix) l
+    then unLineCommentSelectionB insPrefix delPrefix
+    else linePrefixSelectionB insPrefix
+
+-- | Replace the contents of the buffer with some string
+replaceBufferContent :: YiString -> BufferM ()
+replaceBufferContent newvalue = do
+  r <- regionOfB Document
+  replaceRegionB r newvalue
+
+-- | Fill the text in the region so it fits nicely 80 columns.
+fillRegion :: Region -> BufferM ()
+fillRegion = modifyRegionB (R.unlines . fillText 80)
+
+fillParagraph :: BufferM ()
+fillParagraph = fillRegion =<< regionOfB unitParagraph
+
+-- | Sort the lines of the region.
+sortLines :: BufferM ()
+sortLines = modifyExtendedSelectionB Line (onLines sort)
+
+-- | Forces an extra newline into the region (if one exists)
+modifyExtendedLRegion :: Region -> (R.YiString -> R.YiString) -> BufferM ()
+modifyExtendedLRegion region transform = do
+    reg <- unitWiseRegion Line region
+    modifyRegionB transform (fixR reg)
+  where fixR reg = mkRegion (regionStart reg) $ regionEnd reg + 1
+
+sortLinesWithRegion :: Region -> BufferM ()
+sortLinesWithRegion region = modifyExtendedLRegion region (onLines sort')
+    where sort' [] = []
+          sort' lns =
+              if hasnl (last lns)
+                  then sort lns
+                  else over _last
+                      -- should be completely safe since every element contains newline
+                      (fromMaybe (error "sortLinesWithRegion fromMaybe") . R.init) . sort $
+                          over _last (`R.snoc` '\n') lns
+          hasnl t | R.last t == Just '\n' = True
+                  | otherwise = False
+
+-- | Helper function: revert the buffer contents to its on-disk version
+revertB :: YiString -> Maybe R.ConverterName -> UTCTime -> BufferM ()
+revertB s cn now = do
+  r <- regionOfB Document
+  replaceRegionB r s
+  encodingConverterNameA .= cn
+  markSavedB now
+
+-- get lengths of parts covered by block region
+--
+-- Consider block region starting at 'o' and ending at 'z':
+--
+--    start
+--      |
+--     \|/
+-- def foo(bar):
+--     baz
+--
+--     ab
+--     xyz0
+--      /|\
+--       |
+--     finish
+--
+-- shapeOfBlockRegionB returns (regionStart, [2, 2, 0, 1, 2])
+-- TODO: accept stickToEol flag
+shapeOfBlockRegionB :: Region -> BufferM (Point, [Int])
+shapeOfBlockRegionB reg = savingPointB $ do
+    (l0, c0) <- getLineAndColOfPoint $ regionStart reg
+    (l1, c1) <- getLineAndColOfPoint $ regionEnd reg
+    let (left, top, bottom, right) = (min c0 c1, min l0 l1, max l0 l1, max c0 c1)
+    lengths <- forM [top .. bottom] $ \l -> do
+        void $ gotoLn l
+        moveToColB left
+        currentLeft <- curCol
+        if currentLeft /= left
+        then return 0
+        else do
+            moveToColB right
+            rightAtEol <- atEol
+            leftOnEol
+            currentRight <- curCol
+            return $ if currentRight == 0 && rightAtEol
+                     then 0
+                     else currentRight - currentLeft + 1
+    startingPoint <- pointOfLineColB top left
+    return (startingPoint, lengths)
+
+leftEdgesOfRegionB :: RegionStyle -> Region -> BufferM [Point]
+leftEdgesOfRegionB Block reg = savingPointB $ do
+    (l0, _) <- getLineAndColOfPoint $ regionStart reg
+    (l1, _) <- getLineAndColOfPoint $ regionEnd reg
+    moveTo $ regionStart reg
+    fmap catMaybes $ forM [0 .. abs (l0 - l1)] $ \i -> savingPointB $ do
+        void $ lineMoveRel i
+        p <- pointB
+        eol <- atEol
+        return (if not eol then Just p else Nothing)
+leftEdgesOfRegionB LineWise reg = savingPointB $ do
+    lastSol <- do
+        moveTo $ regionEnd reg
+        moveToSol
+        pointB
+    let  go acc p = do moveTo p
+                       moveToSol
+                       edge <- pointB
+                       if edge >= lastSol
+                       then return $ reverse (edge:acc)
+                       else do
+                           void $ lineMoveRel 1
+                           go (edge:acc) =<< pointB
+    go [] (regionStart reg)
+leftEdgesOfRegionB _ r = return [regionStart r]
+
+rightEdgesOfRegionB :: RegionStyle -> Region -> BufferM [Point]
+rightEdgesOfRegionB Block reg = savingPointB $ do
+    (l0, _) <- getLineAndColOfPoint $ regionStart reg
+    (l1, _) <- getLineAndColOfPoint $ regionEnd reg
+    moveTo $ 1 + regionEnd reg
+    fmap reverse $ forM [0 .. abs (l0 - l1)] $ \i -> savingPointB $ do
+        void $ lineMoveRel $ -i
+        pointB
+rightEdgesOfRegionB LineWise reg = savingPointB $ do
+    lastEol <- do
+        moveTo $ regionEnd reg
+        moveToEol
+        pointB
+    let  go acc p = do moveTo p
+                       moveToEol
+                       edge <- pointB
+                       if edge >= lastEol
+                       then return $ reverse (edge:acc)
+                       else do
+                           void $ lineMoveRel 1
+                           go (edge:acc) =<< pointB
+    go [] (regionStart reg)
+rightEdgesOfRegionB _ reg = savingPointB $ do
+    moveTo $ regionEnd reg
+    leftOnEol
+    fmap return pointB
+
+splitBlockRegionToContiguousSubRegionsB :: Region -> BufferM [Region]
+splitBlockRegionToContiguousSubRegionsB reg = savingPointB $ do
+    (start, lengths) <- shapeOfBlockRegionB reg
+    moveTo start
+    forM lengths $ \l -> do
+        p0 <- pointB
+        moveXorEol l
+        p1 <- pointB
+        let subRegion = mkRegion p0 p1
+        moveTo p0
+        void $ lineMoveRel 1
+        return subRegion
+
+deleteRegionWithStyleB :: Region -> RegionStyle -> BufferM Point
+deleteRegionWithStyleB reg Block = savingPointB $ do
+    (start, lengths) <- shapeOfBlockRegionB reg
+    moveTo start
+    forM_ (zip [1..] lengths) $ \(i, l) -> do
+        deleteN l
+        moveTo start
+        lineMoveRel i
+    return start
+
+deleteRegionWithStyleB reg style = savingPointB $ do
+    effectiveRegion <- convertRegionToStyleB reg style
+    deleteRegionB effectiveRegion
+    return $! regionStart effectiveRegion
+
+readRegionRopeWithStyleB :: Region -> RegionStyle -> BufferM YiString
+readRegionRopeWithStyleB reg Block = savingPointB $ do
+    (start, lengths) <- shapeOfBlockRegionB reg
+    moveTo start
+    chunks <- forM lengths $ \l ->
+        if l == 0
+        then lineMoveRel 1 >> return mempty
+        else do
+            p <- pointB
+            r <- readRegionB $ mkRegion p (p +~ Size l)
+            void $ lineMoveRel 1
+            return r
+    return $ R.intersperse '\n' chunks
+readRegionRopeWithStyleB reg style = readRegionB =<< convertRegionToStyleB reg style
+
+insertRopeWithStyleB :: YiString -> RegionStyle -> BufferM ()
+insertRopeWithStyleB rope Block = savingPointB $ do
+  let ls = R.lines rope
+      advanceLine = atLastLine >>= \case
+        False -> void $ lineMoveRel 1
+        True -> do
+          col <- curCol
+          moveToEol
+          newlineB
+          insertN $ R.replicateChar col ' '
+
+  sequence_ $ intersperse advanceLine $ fmap (savingPointB . insertN) ls
+insertRopeWithStyleB rope LineWise = do
+    moveToSol
+    savingPointB $ insertN rope
+insertRopeWithStyleB rope _ = insertN rope
+
+-- consider the following buffer content
+--
+-- 123456789
+-- qwertyuio
+-- asdfgh
+--
+-- The following examples use characters from that buffer as points.
+-- h' denotes the newline after h
+--
+-- 1 r -> 4 q
+-- 9 q -> 1 o
+-- q h -> y a
+-- a o -> h' q
+-- o a -> q h'
+-- 1 a -> 1 a
+--
+-- property: fmap swap (flipRectangleB a b) = flipRectangleB b a
+flipRectangleB :: Point -> Point -> BufferM (Point, Point)
+flipRectangleB p0 p1 = savingPointB $ do
+    (_, c0) <- getLineAndColOfPoint p0
+    (_, c1) <- getLineAndColOfPoint p1
+    case compare c0 c1 of
+        EQ -> return (p0, p1)
+        GT -> swap <$> flipRectangleB p1 p0
+        LT -> do
+            -- now we know that c0 < c1
+            moveTo p0
+            moveXorEol $ c1 - c0
+            flippedP0 <- pointB
+            return (flippedP0, p1 -~ Size (c1 - c0))
+
+movePercentageFileB :: Int -> BufferM ()
+movePercentageFileB i = do
+    let f :: Double
+        f = case fromIntegral i / 100.0 of
+               x | x > 1.0 -> 1.0
+                 | x < 0.0 -> 0.0 -- Impossible?
+                 | otherwise -> x
+    lineCount <- lineCountB
+    void $ gotoLn $ floor (fromIntegral lineCount * f)
+    firstNonSpaceB
+
+findMatchingPairB :: BufferM ()
+findMatchingPairB = do
+    let go dir a b = goUnmatchedB dir a b >> return True
+        goToMatch = do
+          c <- readB
+          case c of '(' -> go Forward  '(' ')'
+                    ')' -> go Backward '(' ')'
+                    '{' -> go Forward  '{' '}'
+                    '}' -> go Backward '{' '}'
+                    '[' -> go Forward  '[' ']'
+                    ']' -> go Backward '[' ']'
+                    _   -> otherChar
+        otherChar = do eof <- atEof
+                       eol <- atEol
+                       if eof || eol
+                           then return False
+                           else rightB >> goToMatch
+    p <- pointB
+    foundMatch <- goToMatch
+    unless foundMatch $ moveTo p
+
+-- Vim numbers
+
+-- | Increase (or decrease if negative) next number on line by n.
+incrementNextNumberByB :: Int -> BufferM ()
+incrementNextNumberByB n = do
+    start <- pointB
+    untilB_ (not <$> isNumberB) $ moveXorSol 1
+    untilB_          isNumberB  $ moveXorEol 1
+    begin <- pointB
+    beginIsEol <- atEol
+    untilB_ (not <$> isNumberB) $ moveXorEol 1
+    end <- pointB
+    if beginIsEol then moveTo start
+    else do modifyRegionB (increment n) (mkRegion begin end)
+            moveXorSol 1
+
+-- | Increment number in string by n.
+increment :: Int -> R.YiString -> R.YiString
+increment n l = R.fromString $ go (R.toString l)
+  where
+    go ('0':'x':xs) = (\ys -> '0':'x':ys) . (`showHex` "") . (+ n) . fst . head . readHex $ xs
+    go ('0':'o':xs) = (\ys -> '0':'o':ys) . (`showOct` "") . (+ n) . fst . head . readOct $ xs
+    go s            = show . (+ n) . (\x -> read x :: Int) $ s
+
+-- | Is character under cursor a number.
+isNumberB :: BufferM Bool
+isNumberB = do
+    eol <- atEol
+    sol <- atSol
+    if sol then isDigit <$> readB
+    else if eol then return False
+         else test3CharB
+
+-- | Used by isNumber to test if current character under cursor is a number.
+test3CharB :: BufferM Bool
+test3CharB = do
+    moveXorSol 1
+    previous <- readB
+    moveXorEol 2
+    next <- readB
+    moveXorSol 1
+    current <- readB
+    if | previous == '0' && current == 'o' && isOctDigit next -> return True  -- octal format
+       | previous == '0' && current == 'x' && isHexDigit next -> return True  -- hex format
+       |                    current == '-' && isDigit next    -> return True  -- negative numbers
+       |                    isDigit current                   -> return True  -- all decimal digits
+       |                    isHexDigit current                -> testHexB     -- ['a'..'f'] for hex
+       | otherwise                                            -> return False
+
+-- | Characters ['a'..'f'] are part of a hex number only if preceded by 0x.
+-- Test if the current occurence of ['a'..'f'] is part of a hex number.
+testHexB :: BufferM Bool
+testHexB = savingPointB $ do
+    untilB_ (not . isHexDigit <$> readB) (moveXorSol 1)
+    leftChar <- readB
+    moveXorSol 1
+    leftToLeftChar <- readB
+    if leftChar == 'x' && leftToLeftChar == '0'
+    then return True
+    else return False
+
+-- | Move point down by @n@ lines
+-- If line extends past width of window, count moving
+-- a single line as moving width points to the right.
+lineMoveVisRel :: Int -> BufferM ()
+lineMoveVisRel = movingToPrefVisCol . lineMoveVisRelUp
+
+lineMoveVisRelUp :: Int -> BufferM ()
+lineMoveVisRelUp 0 = return ()
+lineMoveVisRelUp n | n < 0 = lineMoveVisRelDown $ negate n
+                   | otherwise = do
+    wid <- width <$> use lastActiveWindowA
+    col <- curCol
+    len <- pointB >>= eolPointB >>= colOf
+    let jumps = (len `div` wid) - (col `div` wid)
+        next = n - jumps
+    if next <= 0
+        then moveXorEol (n * wid)
+        else do moveXorEol (jumps * wid)
+                void $ gotoLnFrom 1
+                lineMoveVisRelUp $ next - 1
+
+lineMoveVisRelDown :: Int -> BufferM ()
+lineMoveVisRelDown 0 = return ()
+lineMoveVisRelDown n | n < 0 = lineMoveVisRelUp $ negate n
+                     | otherwise = do
+    wid <- width <$> use lastActiveWindowA
+    col <- curCol
+    let jumps = col `div` wid
+        next = n - jumps
+    if next <= 0
+        then leftN (n * wid)
+        else do leftN (jumps * wid)
+                void $ gotoLnFrom $ -1
+                moveToEol
+                lineMoveVisRelDown $ next - 1
+
+-- | Implements the same logic that emacs' `mark-word` does.
+-- Checks the mark point and moves it forth (or backward) for one word.
+markWord :: BufferM ()
+markWord = do
+    curPos <- pointB
+    curMark <- getSelectionMarkPointB
+    isVisible <- getVisibleSelection
+
+    savingPointB $ do
+        if not isVisible
+        then nextWordB
+        else do
+            moveTo curMark
+            if curMark < curPos
+            then prevWordB
+            else nextWordB
+
+        setVisibleSelection True
+        pointB >>= setSelectionMarkPointB
diff --git a/src/Yi/Buffer/Implementation.hs b/src/Yi/Buffer/Implementation.hs
new file mode 100644
--- /dev/null
+++ b/src/Yi/Buffer/Implementation.hs
@@ -0,0 +1,452 @@
+{-# LANGUAGE CPP                       #-}
+{-# LANGUAGE DeriveDataTypeable        #-}
+{-# LANGUAGE DeriveGeneric             #-}
+{-# LANGUAGE ExistentialQuantification #-}
+{-# LANGUAGE FlexibleContexts          #-}
+{-# LANGUAGE FlexibleInstances         #-}
+{-# LANGUAGE PatternGuards             #-}
+{-# LANGUAGE Rank2Types                #-}
+{-# LANGUAGE TemplateHaskell           #-}
+{-# OPTIONS_HADDOCK show-extensions #-}
+
+-- |
+-- Module      :  Yi.Buffer.Implementation
+-- License     :  GPL-2
+-- Maintainer  :  yi-devel@googlegroups.com
+-- Stability   :  experimental
+-- Portability :  portable
+--
+-- 'Buffer' implementation, wrapping Rope
+
+module Yi.Buffer.Implementation
+  ( UIUpdate (..)
+  , Update (..)
+  , updateIsDelete
+  , Point
+  , Mark, MarkValue(..)
+  , Size
+  , Direction (..)
+  , BufferImpl
+  , Overlay (..)
+  , mkOverlay
+  , overlayUpdate
+  , applyUpdateI
+  , isValidUpdate
+  , reverseUpdateI
+  , nelemsBI
+  , sizeBI
+  , newBI
+  , solPoint
+  , solPoint'
+  , eolPoint'
+  , charsFromSolBI
+  , regexRegionBI
+  , getMarkDefaultPosBI
+  , modifyMarkBI
+  , getMarkValueBI
+  , getMarkBI
+  , newMarkBI
+  , deleteMarkValueBI
+  , setSyntaxBI
+  , addOverlayBI
+  , delOverlayBI
+  , delOverlaysOfOwnerBI
+  , getOverlaysOfOwnerBI
+  , updateSyntax
+  , getAst, focusAst
+  , strokesRangesBI
+  , getStream
+  , getIndexedStream
+  , lineAt
+  , SearchExp
+  , markPointAA
+  , markGravityAA
+  , mem
+  ) where
+
+import           GHC.Generics        (Generic)
+
+import           Data.Array          ((!))
+import           Data.Binary         (Binary (..))
+import           Data.Function       (on)
+import           Data.List           (groupBy)
+import qualified Data.Map            as M (Map, delete, empty, findMax, insert, lookup, map, maxViewWithKey)
+import           Data.Maybe          (fromMaybe)
+import qualified Data.Set            as Set (Set, delete, empty, filter, insert, map, toList)
+import           Data.Typeable       (Typeable)
+import           Yi.Buffer.Basic     (Direction (..), Mark (..), WindowRef, reverseDir)
+import           Yi.Regex            (RegexLike (matchAll), SearchExp, searchRegex)
+import           Yi.Region           (Region (..), fmapRegion, mkRegion, nearRegion, regionSize)
+import           Yi.Rope             (YiString)
+import qualified Yi.Rope             as R
+import           Yi.Style            (StyleName, UIStyle (hintStyle, strongHintStyle))
+import           Yi.Syntax
+import           Yi.Utils            (SemiNum ((+~), (~-)), makeLensesWithSuffix, mapAdjust')
+
+
+data MarkValue = MarkValue { markPoint   :: !Point
+                           , markGravity :: !Direction}
+               deriving (Ord, Eq, Show, Typeable, Generic)
+
+makeLensesWithSuffix "AA" ''MarkValue
+
+instance Binary MarkValue
+
+type Marks = M.Map Mark MarkValue
+
+data HLState syntax = forall cache. HLState !(Highlighter cache syntax) !cache
+
+data Overlay = Overlay
+    { overlayOwner      :: R.YiString
+    , overlayBegin     :: MarkValue
+    , overlayEnd       :: MarkValue
+    , overlayStyle     :: StyleName
+    , overlayAnnotation :: R.YiString
+    }
+
+instance Eq Overlay where
+    Overlay a b c _ msg == Overlay a' b' c' _ msg' =
+        a == a' && b == b' && c == c' && msg == msg'
+
+instance Ord Overlay where
+    compare (Overlay a b c _ msg) (Overlay a' b' c' _ msg')
+        = mconcat
+            [ compare a a'
+            , compare b b'
+            , compare c c'
+            , compare msg msg'
+            ]
+
+data BufferImpl syntax = FBufferData
+    { mem         :: !YiString -- ^ buffer text
+    , marks       :: !Marks -- ^ Marks for this buffer
+    , markNames   :: !(M.Map String Mark)
+    , hlCache     :: !(HLState syntax) -- ^ syntax highlighting state
+    , overlays    :: !(Set.Set Overlay)
+    -- ^ set of (non overlapping) visual overlay regions
+    , dirtyOffset :: !Point
+    -- ^ Lowest modified offset since last recomputation of syntax
+    } deriving Typeable
+
+dummyHlState :: HLState syntax
+dummyHlState = HLState noHighlighter (hlStartState noHighlighter)
+
+-- Atm we can't store overlays because stylenames are functions (can't be serialized)
+-- TODO: ideally I'd like to get rid of overlays entirely; although we could imagine them storing mere styles.
+instance Binary (BufferImpl ()) where
+    put b = put (mem b) >> put (marks b) >> put (markNames b)
+    get = FBufferData <$> get <*> get <*> get <*> pure dummyHlState <*> pure Set.empty <*> pure 0
+
+-- | Mutation actions (also used the undo or redo list)
+--
+-- For the undo/redo, we use the /partial checkpoint/ (Berlage, pg16) strategy to store
+-- just the components of the state that change.
+--
+-- Note that the update direction is only a hint for moving the cursor
+-- (mainly for undo purposes); the insertions and deletions are always
+-- applied Forward.
+--
+-- Note that keeping the text does not cost much: we keep the updates in the undo list;
+-- if it's a "Delete" it means we have just inserted the text in the buffer, so the update shares
+-- the data with the buffer. If it's an "Insert" we have to keep the data any way.
+data Update
+    = Insert
+    { updatePoint :: !Point
+    , updateDirection :: !Direction
+    , _insertUpdateString :: !YiString
+    }
+    | Delete
+    { updatePoint :: !Point
+    , updateDirection :: !Direction
+    , _deleteUpdateString :: !YiString
+    } deriving (Show, Typeable, Generic)
+
+instance Binary Update
+
+updateIsDelete :: Update -> Bool
+updateIsDelete Delete {} = True
+updateIsDelete Insert {} = False
+
+updateString :: Update -> YiString
+updateString (Insert _ _ s) = s
+updateString (Delete _ _ s) = s
+
+updateSize :: Update -> Size
+updateSize = Size . fromIntegral . R.length . updateString
+
+data UIUpdate = TextUpdate !Update
+              | StyleUpdate !Point !Size
+    deriving (Generic)
+instance Binary UIUpdate
+
+--------------------------------------------------
+-- Low-level primitives.
+
+-- | New FBuffer filled from string.
+newBI :: YiString -> BufferImpl ()
+newBI s = FBufferData s M.empty M.empty dummyHlState Set.empty 0
+
+-- | Write string into buffer.
+insertChars :: YiString -> YiString -> Point -> YiString
+insertChars p cs (Point i) = left `R.append` cs `R.append` right
+    where (left, right) = R.splitAt i p
+{-# INLINE insertChars #-}
+
+-- | Write string into buffer.
+deleteChars :: YiString -> Point -> Size -> YiString
+deleteChars p (Point i) (Size n) = left `R.append` right
+    where (left, rest) = R.splitAt i p
+          right = R.drop n rest
+{-# INLINE deleteChars #-}
+
+------------------------------------------------------------------------
+-- Mid-level insert/delete
+
+-- | Shift a mark position, supposing an update at a given point, by a given amount.
+-- Negative amount represent deletions.
+shiftMarkValue :: Point -> Size -> MarkValue -> MarkValue
+shiftMarkValue from by (MarkValue p gravity) = MarkValue shifted gravity
+    where shifted | p < from  = p
+                  | p == from = case gravity of
+                                  Backward -> p
+                                  Forward -> p'
+                  | otherwise {- p > from -} = p'
+              where p' = max from (p +~ by)
+
+mapOvlMarks :: (MarkValue -> MarkValue) -> Overlay -> Overlay
+mapOvlMarks f (Overlay _owner s e v msg) = Overlay _owner (f s) (f e) v msg
+
+-------------------------------------
+-- * "high-level" (exported) operations
+
+-- | Point of EOF
+sizeBI :: BufferImpl syntax -> Point
+sizeBI = Point . R.length . mem
+
+-- | Return @n@ Chars starting at @i@ of the buffer.
+nelemsBI :: Int -> Point -> BufferImpl syntax -> YiString
+nelemsBI n (Point i) = R.take n . R.drop i . mem
+
+getStream :: Direction -> Point -> BufferImpl syntax -> YiString
+getStream Forward  (Point i) = R.drop i . mem
+getStream Backward (Point i) = R.reverse . R.take i . mem
+
+-- | TODO: This guy is a pretty big bottleneck and only one function
+-- uses it which in turn is only seldom used and most of the output is
+-- thrown away anyway. We could probably get away with never
+-- converting this to String here. The old implementation did so
+-- because it worked over ByteString but we don't have to.
+getIndexedStream :: Direction -> Point -> BufferImpl syntax -> [(Point,Char)]
+getIndexedStream Forward  (Point p) = zip [Point p..] . R.toString . R.drop p . mem
+getIndexedStream Backward (Point p) = zip (dF (pred (Point p))) . R.toReverseString . R.take p . mem
+    where
+      dF n = n : dF (pred n)
+
+-- | Create an "overlay" for the style @sty@ between points @s@ and @e@
+mkOverlay :: R.YiString -> Region -> StyleName -> R.YiString -> Overlay
+mkOverlay owner r =
+    Overlay owner
+        (MarkValue (regionStart r) Backward)
+        (MarkValue (regionEnd r) Forward)
+
+-- | Obtain a style-update for a specific overlay
+overlayUpdate :: Overlay -> UIUpdate
+overlayUpdate (Overlay _owner (MarkValue s _) (MarkValue e _) _ _ann) =
+    StyleUpdate s (e ~- s)
+
+-- | Add a style "overlay" between the given points.
+addOverlayBI :: Overlay -> BufferImpl syntax -> BufferImpl syntax
+addOverlayBI ov fb = fb{overlays = Set.insert ov (overlays fb)}
+
+-- | Remove a previously added "overlay"
+delOverlayBI :: Overlay -> BufferImpl syntax -> BufferImpl syntax
+delOverlayBI ov fb = fb{overlays = Set.delete ov (overlays fb)}
+
+delOverlaysOfOwnerBI :: R.YiString -> BufferImpl syntax -> BufferImpl syntax
+delOverlaysOfOwnerBI owner fb =
+    fb{overlays = Set.filter ((/= owner) . overlayOwner) (overlays fb)}
+
+getOverlaysOfOwnerBI :: R.YiString -> BufferImpl syntax -> Set.Set Overlay
+getOverlaysOfOwnerBI owner fb =
+    Set.filter ((== owner) . overlayOwner) (overlays fb)
+
+-- FIXME: this can be really inefficient.
+
+-- | Return style information for the range @(i,j)@ Style information
+--   is derived from syntax highlighting, active overlays and current regexp.  The
+--   returned list contains tuples @(l,s,r)@ where every tuple is to
+--   be interpreted as apply the style @s@ from position @l@ to @r@ in
+--   the buffer.  In each list, the strokes are guaranteed to be
+--   ordered and non-overlapping.  The lists of strokes are ordered by
+--   decreasing priority: the 1st layer should be "painted" on top.
+strokesRangesBI :: (Point -> Point -> Point -> [Stroke]) ->
+  Maybe SearchExp -> Region -> Point -> BufferImpl syntax -> [[Stroke]]
+strokesRangesBI getStrokes regex rgn  point fb = result
+  where
+    i = regionStart rgn
+    j = regionEnd rgn
+    dropBefore = dropWhile (\s ->spanEnd s <= i)
+    takeIn  = takeWhile (\s -> spanBegin s <= j)
+
+    groundLayer = [Span i mempty j]
+
+    -- zero-length spans seem to break stroking in general, so filter them out!
+    syntaxHlLayer = filter (\(Span b _m a) -> b /= a)  $ getStrokes point i j
+
+    layers2 = map (map overlayStroke) $ groupBy ((==) `on` overlayOwner) $  Set.toList $ overlays fb
+    layer3 = case regex of
+               Just re -> takeIn $ map hintStroke $ regexRegionBI re (mkRegion i j) fb
+               Nothing -> []
+    result = map (map clampStroke . takeIn . dropBefore) (layer3 : layers2 ++ [syntaxHlLayer, groundLayer])
+    overlayStroke (Overlay _owner sm  em a _msg) =
+        Span (markPoint sm) a (markPoint em)
+    clampStroke (Span l x r) = Span (max i l) x (min j r)
+    hintStroke r = Span (regionStart r) (if point `nearRegion` r then strongHintStyle else hintStyle) (regionEnd r)
+
+------------------------------------------------------------------------
+-- Point based editing
+
+-- | Checks if an Update is valid
+isValidUpdate :: Update -> BufferImpl syntax -> Bool
+isValidUpdate u b = case u of
+                    (Delete p _ _)   -> check p && check (p +~ updateSize u)
+                    (Insert p _ _)   -> check p
+    where check (Point x) = x >= 0 && x <= R.length (mem b)
+
+-- | Apply a /valid/ update
+applyUpdateI :: Update -> BufferImpl syntax -> BufferImpl syntax
+applyUpdateI u fb = touchSyntax (updatePoint u) $
+                    fb {mem = p', marks = M.map shift (marks fb),
+                                   overlays = Set.map (mapOvlMarks shift) (overlays fb)}
+                                   -- FIXME: this is inefficient; find a way to use mapMonotonic
+                                   -- (problem is that marks can have different gravities)
+    where (p', amount) = case u of
+                           Insert pnt _ cs -> (insertChars p cs pnt, sz)
+                           Delete pnt _ _  -> (deleteChars p pnt sz, negate sz)
+          sz = updateSize u
+          shift = shiftMarkValue (updatePoint u) amount
+          p = mem fb
+          -- FIXME: remove collapsed overlays
+
+-- | Reverse the given update
+reverseUpdateI :: Update -> Update
+reverseUpdateI (Delete p dir cs) = Insert p (reverseDir dir) cs
+reverseUpdateI (Insert p dir cs) = Delete p (reverseDir dir) cs
+
+
+------------------------------------------------------------------------
+-- Line based editing
+
+-- | Line at the given point. (Lines are indexed from 1)
+lineAt :: Point -- ^ Line for which to grab EOL for
+       -> BufferImpl syntax -> Int
+lineAt (Point p) fb = 1 + R.countNewLines (R.take p $ mem fb)
+
+-- | Point that starts the given line (Lines are indexed from 1)
+solPoint :: Int -> BufferImpl syntax -> Point
+solPoint line fb = Point $ R.length $ fst $ R.splitAtLine (line - 1) (mem fb)
+
+-- | Point that's at EOL. Notably, this puts you right before the
+-- newline character if one exists, and right at the end of the text
+-- if one does not.
+eolPoint' :: Point
+             -- ^ Point from which we take the line to find the EOL of
+          -> BufferImpl syntax
+          -> Point
+eolPoint' p@(Point ofs) fb = Point . checkEol . fst . R.splitAtLine ln $ mem fb
+  where
+    ln = lineAt p fb
+    -- In case we're somewhere without trailing newline, we need to
+    -- stay where we are
+    checkEol t =
+      let l' = R.length t
+      in case R.last t of
+          -- We're looking at EOL and we weren't asking for EOL past
+          -- this point, so back up one for good visual effect
+          Just '\n' | l' > ofs -> l' - 1
+          -- We asked for EOL past the last newline so just go to the
+          -- very end of content
+          _ -> l'
+
+-- | Get begining of the line relatively to @point@.
+solPoint' :: Point -> BufferImpl syntax -> Point
+solPoint' point fb = solPoint (lineAt point fb) fb
+
+charsFromSolBI :: Point -> BufferImpl syntax -> YiString
+charsFromSolBI pnt fb = nelemsBI (fromIntegral $ pnt - sol) sol fb
+    where sol = solPoint' pnt fb
+
+-- | Return indices of all strings in buffer matching regex, inside the given region.
+regexRegionBI :: SearchExp -> Region -> forall syntax. BufferImpl syntax -> [Region]
+regexRegionBI se r fb = case dir of
+     Forward  -> fmap (fmapRegion addPoint . matchedRegion) $ matchAll' $ R.toString        bufReg
+     Backward -> fmap (fmapRegion subPoint . matchedRegion) $ matchAll' $ R.toReverseString bufReg
+    where matchedRegion arr = let (off,len) = arr!0 in mkRegion (Point off) (Point (off+len))
+          addPoint (Point x) = Point (p + x)
+          subPoint (Point x) = Point (q - x)
+          matchAll' = matchAll (searchRegex dir se)
+          dir = regionDirection r
+          Point p = regionStart r
+          Point q = regionEnd r
+          Size s = regionSize r
+          bufReg = R.take s . R.drop p $ mem fb
+
+newMarkBI :: MarkValue -> BufferImpl syntax -> (BufferImpl syntax, Mark)
+newMarkBI initialValue fb =
+    let maxId = fromMaybe 0 $ markId . fst . fst <$> M.maxViewWithKey (marks fb)
+        newMark = Mark $ maxId + 1
+        fb' = fb { marks = M.insert newMark initialValue (marks fb)}
+    in (fb', newMark)
+
+getMarkValueBI :: Mark -> BufferImpl syntax -> Maybe MarkValue
+getMarkValueBI m (FBufferData { marks = marksMap } ) = M.lookup m marksMap
+
+deleteMarkValueBI :: Mark -> BufferImpl syntax -> BufferImpl syntax
+deleteMarkValueBI m fb = fb { marks = M.delete m (marks fb) }
+
+getMarkBI :: String -> BufferImpl syntax -> Maybe Mark
+getMarkBI name FBufferData {markNames = nms} = M.lookup name nms
+
+-- | Modify a mark value.
+modifyMarkBI :: Mark -> (MarkValue -> MarkValue) -> (forall syntax. BufferImpl syntax -> BufferImpl syntax)
+modifyMarkBI m f fb = fb {marks = mapAdjust' f m (marks fb)}
+-- NOTE: we must insert the value strictly otherwise we can hold to whatever structure the value of the mark depends on.
+-- (often a whole buffer)
+
+setSyntaxBI :: ExtHL syntax -> BufferImpl oldSyntax -> BufferImpl syntax
+setSyntaxBI (ExtHL e) fb = touchSyntax 0 $ fb {hlCache = HLState e (hlStartState e)}
+
+touchSyntax ::  Point -> BufferImpl syntax -> BufferImpl syntax
+touchSyntax touchedIndex fb = fb { dirtyOffset = min touchedIndex (dirtyOffset fb)}
+
+updateSyntax :: BufferImpl syntax -> BufferImpl syntax
+updateSyntax fb@FBufferData {dirtyOffset = touchedIndex, hlCache = HLState hl cache}
+    | touchedIndex == maxBound = fb
+    | otherwise
+    = fb {dirtyOffset = maxBound,
+          hlCache = HLState hl (hlRun hl getText touchedIndex cache)
+         }
+    where getText = Scanner 0 id (error "getText: no character beyond eof")
+                     (\idx -> getIndexedStream Forward idx fb)
+
+------------------------------------------------------------------------
+
+-- | Returns the requested mark, creating a new mark with that name (at the supplied position) if needed
+getMarkDefaultPosBI :: Maybe String -> Point -> BufferImpl syntax -> (BufferImpl syntax, Mark)
+getMarkDefaultPosBI name defaultPos fb@FBufferData {marks = mks, markNames = nms} =
+  case flip M.lookup nms =<< name of
+    Just m' -> (fb, m')
+    Nothing ->
+           let newMark = Mark (1 + max 1 (markId $ fst (M.findMax mks)))
+               nms' = case name of
+                        Nothing -> nms
+                        Just nm -> M.insert nm newMark nms
+               mks' = M.insert newMark (MarkValue defaultPos Forward) mks
+           in (fb {marks = mks', markNames = nms'}, newMark)
+
+
+getAst :: WindowRef -> BufferImpl syntax -> syntax
+getAst w FBufferData {hlCache = HLState (SynHL {hlGetTree = gt}) cache} = gt cache w
+
+focusAst ::  M.Map WindowRef Region -> BufferImpl syntax -> BufferImpl syntax
+focusAst r b@FBufferData {hlCache = HLState s@(SynHL {hlFocus = foc}) cache} = b {hlCache = HLState s (foc r cache)}
diff --git a/src/Yi/Buffer/Indent.hs b/src/Yi/Buffer/Indent.hs
new file mode 100644
--- /dev/null
+++ b/src/Yi/Buffer/Indent.hs
@@ -0,0 +1,376 @@
+{-# LANGUAGE LambdaCase        #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# OPTIONS_HADDOCK show-extensions #-}
+
+-- |
+-- Module      :  Yi.Buffer.Region
+-- License     :  GPL-2
+-- Maintainer  :  yi-devel@googlegroups.com
+-- Stability   :  experimental
+-- Portability :  portable
+--
+-- Handles indentation in the keymaps. Includes:
+--
+--  * (TODO) Auto-indentation to the previous lines indentation
+--  * Tab-expansion
+--  * Shifting of the indentation for a region of text
+
+module Yi.Buffer.Indent
+    ( autoIndentB
+    , cycleIndentsB
+    , indentAsNextB
+    , indentAsPreviousB
+    , indentAsTheMostIndentedNeighborLineB
+    , indentOfB
+    , indentOfCurrentPosB
+    , indentSettingsB
+    , indentToB
+    , modifyIndentB
+    , newlineAndIndentB
+    , shiftIndentOfRegionB
+    , tabB
+    ) where
+
+import           Control.Monad       ()
+import           Data.Char           (isSpace)
+import           Data.List           (nub, sort)
+import           Data.Monoid         ((<>))
+import           Yi.Buffer.Basic     (Direction (..))
+import           Yi.Buffer.HighLevel (firstNonSpaceB, getNextLineB, getNextNonBlankLineB, moveToSol, readLnB)
+import           Yi.Buffer.Misc
+import           Yi.Buffer.Normal    ()
+import           Yi.Buffer.Region    (Region (regionStart), mkRegion, modifyRegionB, readRegionB)
+import           Yi.Buffer.TextUnit  (regionWithTwoMovesB)
+import           Yi.Rope             (YiString)
+import qualified Yi.Rope             as R
+import           Yi.String           (mapLines)
+
+{- |
+  Return either a \t or the number of spaces specified by tabSize in the
+  IndentSettings. Note that if you actually want to insert a tab character
+  (for example when editing makefiles) then you should use: @insertB '\t'@.
+-}
+tabB :: BufferM String
+tabB = do
+  indentSettings <- indentSettingsB
+  return $ if expandTabs indentSettings
+    then replicate (tabSize indentSettings) ' '
+    else "\t"
+
+{-|
+  A specialisation of 'autoIndentHelperB'.
+  This is the most basic and the user is encouraged to
+  specialise 'autoIndentHelperB' on their own.
+-}
+autoIndentB :: IndentBehaviour -> BufferM ()
+autoIndentB = autoIndentHelperB fetchPreviousIndentsB indentsOfString
+  where
+  -- Returns the indentation hints considering the given
+  -- string as the line above the current one.
+  -- The hints added are:
+  --     The indent of the given string
+  --     The indent of the given string plus two
+  --     The offset of the last open bracket if any in the line.
+  indentsOfString :: YiString -> BufferM [Int]
+  indentsOfString input = do
+    indent       <- indentOfB input
+    bracketHints <- lastOpenBracketHint input
+    indentSettings <- indentSettingsB
+    return $ indent : (indent + shiftWidth indentSettings) : bracketHints
+
+{-|
+  This takes two arguments the first is a function to
+  obtain indentation hints from lines above the current one.
+  The second is a function to obtain a set of indentation hints
+  from the previous line. Both of these are in the 'BufferM'
+  monad although the second seems like it is unnecessary.
+  However we must take into account the length of tabs which come
+  from the the tab settings and hence we must be in the 'BufferM'
+  monad.
+
+  To get the straightforward behaviour of the indents of all previous
+  lines until one of them has zero indent call this with:
+  @autoIndentHelperB fetchPreviousIndentsB (fmap (: []) indentOfB)@
+  However commonly we wish to have something more interesting for
+  the second argument, in particular we commonly wish to have the
+  last opening bracket of the previous line as well as its indent.
+-}
+autoIndentHelperB :: BufferM [ Int ]
+                  -- ^ Action to fetch hints from previous lines
+                 -> (YiString -> BufferM [ Int ])
+                 -- ^ Action to calculate hints from previous line
+                 -> IndentBehaviour
+                 -- ^ Sets the indent behaviour,
+                 -- see 'Yi.Buffer.IndentBehaviour' for a description
+                 -> BufferM ()
+autoIndentHelperB getUpwards getPrevious indentBehave =
+  do upwardHints   <- savingExcursionB getUpwards
+     previousLine  <- getNextLineB Backward
+     previousHints <- getPrevious previousLine
+     let allHints = upwardHints ++ previousHints
+     cycleIndentsB indentBehave allHints
+
+-- | Cycles through the indentation hints. It does this without
+-- requiring to set/get any state. We just look at the current
+-- indentation of the current line and moving to the largest
+-- indent that is
+cycleIndentsB :: IndentBehaviour -> [Int] -> BufferM ()
+cycleIndentsB _ [] = return ()
+cycleIndentsB indentBehave indents =
+    do currentLine   <- readLnB
+       currentIndent <- indentOfB currentLine
+       indentToB $ chooseIndent currentIndent (sort $ nub indents)
+  where
+  -- Is the function to choose the indent from the given current
+  -- indent to the given list of indentation hints.
+  chooseIndent :: Int -> [ Int ] -> Int
+  chooseIndent =
+    case indentBehave of
+      IncreaseCycle -> chooseIncreaseCycle
+      DecreaseCycle -> chooseDecreaseCycle
+      IncreaseOnly  -> chooseIncreaseOnly
+      DecreaseOnly  -> chooseDecreaseOnly
+
+
+
+  -- Choose the indentation hint which is one more than the current
+  -- indentation hint unless the current is the largest or larger than
+  -- all the indentation hints in which case choose the smallest
+  -- (which will often be zero)
+  chooseIncreaseCycle :: Int -> [ Int ] -> Int
+  chooseIncreaseCycle currentIndent hints =
+    -- Similarly to 'chooseDecreasing' if 'above' is null then
+    -- we will go to the first of below which will be the smallest
+    -- indentation hint, if above is not null then we are moving to
+    -- the indentation hint which is one above the current.
+    head (above ++ below)
+    where
+    (below, above) = span (<= currentIndent) hints
+
+  -- Note that these functions which follow generally assume that
+  -- the list of hints which have been given is already sorted
+  -- and that the list is non-empty
+
+  -- Choose the indentation hint one less than the current indentation
+  -- unless the current indentation is the smallest (usually zero)
+  -- in which case choose the largest indentation hint.
+  chooseDecreaseCycle :: Int -> [ Int ] -> Int
+  chooseDecreaseCycle currentIndent hints =
+    -- So in particular if 'below' is null then we will
+    -- go to the largest indentation, if below is not null
+    -- we go to the largest indentation which is *not* higher
+    -- than the current one.
+    last (above ++ below)
+    where
+    (below, above) = span (< currentIndent) hints
+
+  chooseIncreaseOnly :: Int -> [ Int ] -> Int
+  chooseIncreaseOnly currentIndent hints =
+    head $ filter (> currentIndent) hints ++ [ currentIndent ]
+
+  chooseDecreaseOnly :: Int -> [ Int ] -> Int
+  chooseDecreaseOnly currentIndent hints =
+    last $ currentIndent : filter (< currentIndent) hints
+
+{-|
+  A function generally useful as the first argument to
+  'autoIndentHelperB'. This searches the lines above
+  the current line for the indentations of each line
+  until we get to a line which has no indentation
+  *and* is not empty. Indicating that we have reached
+  the outer scope.
+-}
+fetchPreviousIndentsB :: BufferM [Int]
+fetchPreviousIndentsB = do
+  -- Move up one line,
+  moveOffset <- lineMoveRel (-1)
+  line       <- readLnB
+  indent     <- indentOfB line
+  -- So if we didn't manage to move upwards
+  -- or the current offset was zero *and* the line
+  -- was non-blank then we return just the current
+  -- indent (it might be the first line but indented some.)
+  if moveOffset == 0 || (indent == 0 && R.any (not . isSpace) line)
+    then return [ indent ]
+    else (indent :) <$> fetchPreviousIndentsB
+
+-- | Returns the position of the last opening bracket on the
+-- line which is not closed on the same line.
+-- Note that if we have unmatched parentheses such as "( ]"
+-- then we may not get the correct answer, but in that case
+-- then arguably we don't really care if we get the correct
+-- answer (at least if we get it wrong the user may notice
+-- their error).
+-- We return a list here as it's a convenient way of returning
+-- no hint in the case of there being no non-closed bracket
+-- and normally such a hint will be part of a list of hints
+-- anyway.
+-- NOTE: this could be easily modified to return the indentations
+-- of *all* the non-closed opening brackets. But I think this is
+-- not what you generally want.
+-- TODO: we also do not care whether or not the bracket is within
+-- a string or escaped. If someone feels up to caring about that
+-- by all means please fix this.
+lastOpenBracketHint :: YiString -> BufferM [ Int ]
+lastOpenBracketHint input =
+  case getOpen 0 $ R.reverse input of
+    Nothing -> return []
+    Just s  -> return <$> spacingOfB s
+  where
+  -- We get the last open bracket by counting through
+  -- the reversed line, when we see a closed bracket we
+  -- add one to the count. When we see an opening bracket
+  -- decrease the count. If we see an opening bracket when the
+  -- count is 0 we return the remaining (reversed) string
+  -- as the part of the line which preceds the last opening bracket.
+  -- This can then be turned into an indentation by calling 'spacingOfB'
+  -- on it so that tabs are counted as tab length.
+  -- NOTE: that this will work even if tab occur in the middle of the line
+  getOpen :: Int -> YiString -> Maybe YiString
+  getOpen i s = let rest = R.drop 1 s in case R.head s of
+    Nothing -> Nothing
+    Just c
+        -- If it is opening and we have no closing to match
+        -- then we return the rest of the line
+      | isOpening c && i == 0 -> Just rest
+        -- If i is not zero then we have matched one of the
+        -- closing parentheses and we can decrease the nesting count.
+      | isOpening c           -> getOpen (i - 1) rest
+        -- If the character is a closing bracket then we must increase
+        -- the nesting count
+      | isClosing c           -> getOpen (i + 1) rest
+        -- If it is just a normal character forget about it and move on.
+      | otherwise             -> getOpen i rest
+
+  isOpening :: Char -> Bool
+  isOpening '(' = True
+  isOpening '[' = True
+  isOpening '{' = True
+  isOpening _   = False
+
+  isClosing :: Char -> Bool
+  isClosing ')' = True
+  isClosing ']' = True
+  isClosing '}' = True
+  isClosing _   = False
+
+-- | Returns the indentation of a given string. Note that this depends
+-- on the current indentation settings.
+indentOfB :: YiString -> BufferM Int
+indentOfB = spacingOfB . R.takeWhile isSpace
+
+makeIndentString :: Int -> BufferM YiString
+makeIndentString level = do
+  IndentSettings et _ sw <- indentSettingsB
+  let (q, r) = level `quotRem` sw
+  if et
+  then return (R.replicate level " ")
+  else return (R.replicate q "\t" <> R.replicate r " ")
+
+-- | Returns the length of a given string taking into account the
+-- white space and the indentation settings.
+spacingOfB :: YiString -> BufferM Int
+spacingOfB text = do
+  indentSettings <- indentSettingsB
+  return $ countIndent indentSettings text
+
+{-| Indents the current line to the given indentation level.
+    In addition moves the point according to where it was on the
+    line originally. If we were somewhere within the indentation
+    (ie at the start of the line or on an empty line) then we want
+    to just go to the end of the (new) indentation.
+    However if we are currently pointing somewhere within the text
+    of the line then we wish to remain pointing to the same character.
+-}
+indentToB :: Int -> BufferM ()
+indentToB = modifyIndentB . const
+
+-- | Modifies current line indent measured in visible spaces.
+-- Respects indent settings. Calling this with value (+ 4)
+-- will turn "\t" into "\t\t" if shiftwidth is 4 and into
+-- "\t    " if shiftwidth is 8
+-- If current line is empty nothing happens.
+modifyIndentB :: (Int -> Int) -> BufferM ()
+modifyIndentB f = do
+  leadingSpaces <- regionWithTwoMovesB moveToSol firstNonSpaceB
+  newLeadinSpaces <-
+    readRegionB leadingSpaces >>= indentOfB >>= makeIndentString . f
+  modifyRegionB (const newLeadinSpaces) leadingSpaces
+
+-- | Indent as much as the previous line
+indentAsPreviousB :: BufferM ()
+indentAsPreviousB = indentAsNeighborLineB Backward
+
+-- | Indent as much as the next line
+indentAsNextB :: BufferM ()
+indentAsNextB = indentAsNeighborLineB Forward
+
+indentAsTheMostIndentedNeighborLineB :: BufferM ()
+indentAsTheMostIndentedNeighborLineB = do
+  prevLine <- getNextNonBlankLineB Backward
+  nextLine <- getNextNonBlankLineB Forward
+  prevIndent <- indentOfB prevLine
+  nextIndent <- indentOfB nextLine
+  indentToB (max prevIndent nextIndent)
+
+indentAsNeighborLineB :: Direction -> BufferM ()
+indentAsNeighborLineB dir = do
+  otherLine   <- getNextNonBlankLineB dir
+  otherIndent <- indentOfB otherLine
+  indentToB otherIndent
+
+-- | Insert a newline at point and indent the new line as the previous one.
+newlineAndIndentB :: BufferM ()
+newlineAndIndentB = newlineB >> indentAsPreviousB
+
+-- | Set the padding of the string to newCount, filling in tabs if
+-- expandTabs is set in the buffers IndentSettings
+rePadString :: IndentSettings -> Int -> R.YiString -> R.YiString
+rePadString indentSettings newCount input
+    | newCount <= 0 = rest
+    | expandTabs indentSettings = R.replicateChar newCount ' ' <> rest
+    | otherwise = tabs <> spaces <> rest
+    where (_indents,rest) = R.span isSpace input
+          tabs   = R.replicateChar (newCount `div` tabSize indentSettings) '\t'
+          spaces = R.replicateChar (newCount `mod` tabSize indentSettings) ' '
+
+-- | Counts the size of the indent in the given text.
+--
+-- Assumes nothing but tabs and spaces: uses 'isSpace'.
+countIndent :: IndentSettings -> R.YiString -> Int
+countIndent i t = R.foldl' (\i' c -> i' + spacing c) 0 indents
+  where
+    (indents, _) = R.span isSpace t
+
+    spacing '\t' = tabSize i
+    spacing _    = 1
+
+-- | shifts right (or left if num is negative) num times, filling in tabs if
+-- expandTabs is set in the buffers IndentSettings
+indentString :: IndentSettings -> Int -> R.YiString -> R.YiString
+indentString is numOfShifts i = rePadString is newCount i
+    where
+      newCount = countIndent is i + (shiftWidth is * numOfShifts)
+
+-- | Increases the indentation on the region by the given amount of shiftWidth
+shiftIndentOfRegionB :: Int -> Region -> BufferM ()
+shiftIndentOfRegionB shiftCount region = do
+    is <- indentSettingsB
+    let indentFn :: R.YiString -> R.YiString
+        indentFn line = if not (R.null line) && line /= "\n"
+            then indentString is shiftCount line
+            else line
+    modifyRegionB (mapLines indentFn) region
+    moveTo $ regionStart region
+    firstNonSpaceB
+
+-- | Return the number of spaces at the beginning of the line, up to
+-- the point.
+indentOfCurrentPosB :: BufferM Int
+indentOfCurrentPosB = do
+  p <- pointB
+  moveToSol
+  sol <- pointB
+  moveTo p
+  let region = mkRegion p sol
+  readRegionB region >>= spacingOfB
diff --git a/src/Yi/Buffer/Misc.hs b/src/Yi/Buffer/Misc.hs
new file mode 100644
--- /dev/null
+++ b/src/Yi/Buffer/Misc.hs
@@ -0,0 +1,1181 @@
+{-# LANGUAGE CPP                        #-}
+{-# LANGUAGE DeriveDataTypeable         #-}
+{-# LANGUAGE DeriveFoldable             #-}
+{-# LANGUAGE DeriveFunctor              #-}
+{-# LANGUAGE DeriveTraversable          #-}
+{-# LANGUAGE ExistentialQuantification  #-}
+{-# LANGUAGE FlexibleContexts           #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE ImpredicativeTypes         #-}
+{-# LANGUAGE LambdaCase                 #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE ScopedTypeVariables        #-}
+{-# LANGUAGE StandaloneDeriving         #-}
+{-# LANGUAGE TemplateHaskell            #-}
+{-# LANGUAGE TupleSections              #-}
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+{-# OPTIONS_HADDOCK show-extensions #-}
+
+-- |
+-- Module      :  Yi.Buffer.Misc
+-- License     :  GPL-2
+-- Maintainer  :  yi-devel@googlegroups.com
+-- Stability   :  experimental
+-- Portability :  portable
+--
+-- The 'Buffer' module defines monadic editing operations over one-dimensional
+-- buffers, maintaining a current /point/.
+
+module Yi.Buffer.Misc
+  ( FBuffer (FBuffer, bmode)
+  , BufferM (..)
+  , WinMarks, MarkSet (..)
+  , bkey
+  , getMarks
+  , runBuffer
+  , runBufferFull
+  , runBufferDummyWindow
+  , screenTopLn
+  , screenMidLn
+  , screenBotLn
+  , curLn
+  , curCol
+  , colOf
+  , lineOf
+  , lineCountB
+  , sizeB
+  , pointB
+  , pointOfLineColB
+  , solPointB
+  , eolPointB
+  , markLines
+  , moveTo
+  , moveToColB
+  , moveToLineColB
+  , lineMoveRel
+  , lineUp
+  , lineDown
+  , newB
+  , MarkValue (..)
+  , Overlay
+      (overlayAnnotation, overlayBegin, overlayEnd, overlayOwner, overlayStyle)
+  , mkOverlay
+  , gotoLn
+  , gotoLnFrom
+  , leftB
+  , rightB
+  , moveN
+  , leftN
+  , rightN
+  , insertN
+  , insertNAt
+  , insertB
+  , deleteN
+  , nelemsB
+  , writeB
+  , writeN
+  , newlineB
+  , deleteNAt
+  , readB
+  , elemsB
+  , undosA
+  , undoB
+  , redoB
+  , getMarkB
+  , setMarkHereB
+  , setNamedMarkHereB
+  , mayGetMarkB
+  , getMarkValueB
+  , markPointA
+  , modifyMarkB
+  , newMarkB
+  , deleteMarkB
+  , getVisibleSelection
+  , setVisibleSelection
+  , isUnchangedBuffer
+  , setAnyMode
+  , setMode
+  , setMode0
+  , modifyMode
+  , regexRegionB
+  , regexB
+  , readAtB
+  , getModeLine
+  , getPercent
+  , setInserting
+  , savingPrefCol
+  , forgetPreferCol
+  , movingToPrefCol
+  , movingToPrefVisCol
+  , preferColA
+  , markSavedB
+  , retroactivelyAtSavePointB
+  , addOverlayB
+  , delOverlayB
+  , delOverlaysOfOwnerB
+  , getOverlaysOfOwnerB
+  , isPointInsideOverlay
+  , savingExcursionB
+  , savingPointB
+  , savingPositionB
+  , pendingUpdatesA
+  , highlightSelectionA
+  , rectangleSelectionA
+  , readOnlyA
+  , insertingA
+  , pointFollowsWindowA
+  , revertPendingUpdatesB
+  , askWindow
+  , clearSyntax
+  , focusSyntax
+  , Mode (..)
+  , modeNameA
+  , modeAppliesA
+  , modeHLA
+  , modePrettifyA
+  , modeKeymapA
+  , modeIndentA
+  , modeAdjustBlockA
+  , modeFollowA
+  , modeIndentSettingsA
+  , modeToggleCommentSelectionA
+  , modeGetStrokesA
+  , modeOnLoadA
+  , modeGotoDeclarationA
+  , modeModeLineA
+  , AnyMode (..)
+  , IndentBehaviour (..)
+  , IndentSettings (..)
+  , expandTabsA
+  , tabSizeA
+  , shiftWidthA
+  , modeAlwaysApplies
+  , modeNeverApplies
+  , emptyMode
+  , withModeB
+  , withMode0
+  , onMode
+  , withSyntaxB
+  , withSyntaxB'
+  , keymapProcessA
+  , strokesRangesB
+  , streamB
+  , indexedStreamB
+  , askMarks
+  , pointAt
+  , SearchExp
+  , lastActiveWindowA
+  , putBufferDyn
+  , getBufferDyn
+  , shortIdentString
+  , identString
+  , miniIdentString
+  , identA
+  , directoryContentA
+  , BufferId (..)
+  , file
+  , lastSyncTimeA
+  , replaceCharB
+  , replaceCharWithBelowB
+  , replaceCharWithAboveB
+  , insertCharWithBelowB
+  , insertCharWithAboveB
+  , pointAfterCursorB
+  , destinationOfMoveB
+  , withEveryLineB
+  , startUpdateTransactionB
+  , commitUpdateTransactionB
+  , applyUpdate
+  , betweenB
+  , decreaseFontSize
+  , increaseFontSize
+  , indentSettingsB
+  , fontsizeVariationA
+  , encodingConverterNameA
+  , stickyEolA
+  ) where
+
+import           Prelude                        hiding (foldr, mapM, notElem)
+
+import           Lens.Micro.Platform                     (Lens', lens, (%~), (^.), use, (.=), (%=), view)
+import           Control.Monad.RWS.Strict       (Endo (Endo, appEndo),
+                                                 MonadReader (ask), MonadState,
+                                                 MonadWriter (tell),
+                                                 asks, gets, join, modify,
+                                                 replicateM_, runRWS, void,
+                                                 when, (<>))
+import           Data.Binary                    (Binary (..), Get)
+import           Data.Char                      (ord)
+import           Data.Default                   (Default (def))
+import           Data.DynamicState.Serializable (getDyn, putDyn)
+import           Data.Foldable                  (Foldable (foldr), forM_, notElem)
+import qualified Data.Map                       as M (Map, empty, insert, lookup)
+import           Data.Maybe                     (fromMaybe, isNothing)
+import qualified Data.Set                       as Set (Set)
+import qualified Data.Text                      as T (Text, concat, justifyRight, pack, snoc, unpack)
+import qualified Data.Text.Encoding             as E (decodeUtf8, encodeUtf8)
+import           Data.Time                      (UTCTime (UTCTime))
+import           Data.Traversable               (Traversable (mapM), forM)
+import           Numeric                        (showHex)
+import           System.FilePath                (joinPath, splitPath)
+import           Yi.Buffer.Basic                (BufferRef, Point (..), Size (Size), WindowRef)
+import           Yi.Buffer.Implementation
+import           Yi.Buffer.Undo
+import           Yi.Interact                    as I (P (End))
+import           Yi.Monad                       (getsAndModify)
+import           Yi.Region                      (Region, mkRegion)
+import           Yi.Rope                        (YiString)
+import qualified Yi.Rope                        as R
+import           Yi.Syntax                      (ExtHL (ExtHL), Stroke, noHighlighter)
+import           Yi.Types
+import           Yi.Utils                       (SemiNum ((+~)), makeClassyWithSuffix, makeLensesWithSuffix)
+import           Yi.Window                      (Window (width, wkey, actualLines), dummyWindow)
+
+
+uses l f = f <$> use l
+
+-- In addition to Buffer's text, this manages (among others):
+--  * Log of updates mades
+--  * Undo
+
+makeClassyWithSuffix "A" ''Attributes
+
+instance HasAttributes FBuffer where
+    attributesA = lens attributes (\(FBuffer f1 f2 _) a -> FBuffer f1 f2 a)
+
+-- | Gets a short identifier of a buffer. If we're given a 'MemBuffer'
+-- then just wraps the buffer name like so: @*name*@. If we're given a
+-- 'FileBuffer', it drops the the number of characters specified.
+--
+-- >>> shortIdentString 3 (MemBuffer "hello")
+-- "*hello*"
+-- >>> shortIdentString 3 (FileBuffer "hello")
+-- "lo"
+shortIdentString :: Int -- ^ Number of characters to drop from FileBuffer names
+                 -> FBuffer -- ^ Buffer to work with
+                 -> T.Text
+shortIdentString dl b = case b ^. identA of
+  MemBuffer bName -> "*" <> bName <> "*"
+  FileBuffer fName -> T.pack . joinPath . drop dl $ splitPath fName
+
+-- | Gets the buffer's identifier string, emphasising the 'MemBuffer':
+--
+-- >>> identString (MemBuffer "hello")
+-- "*hello*"
+-- >>> identString (FileBuffer "hello")
+-- "hello"
+identString :: FBuffer -> T.Text
+identString b = case b ^. identA of
+  MemBuffer bName -> "*" <> bName <> "*"
+  FileBuffer fName -> T.pack fName
+
+
+-- TODO: proper instance + de-orphan
+instance Show FBuffer where
+    show b = Prelude.concat [ "Buffer #", show (bkey b)
+                            , " (",  T.unpack (identString b), ")" ]
+
+
+miniIdentString :: FBuffer -> T.Text
+miniIdentString b = case b ^. identA of
+  MemBuffer bufName -> bufName
+  FileBuffer _ -> "MINIFILE:"
+
+-- unfortunately the dynamic stuff can't be read.
+instance Binary FBuffer where
+    put (FBuffer binmode r attributes_) =
+      let strippedRaw :: BufferImpl ()
+          strippedRaw = setSyntaxBI (modeHL emptyMode) r
+      in do
+          put binmode
+          put strippedRaw
+          put attributes_
+    get =
+        FBuffer <$> get <*> getStripped <*> get
+      where getStripped :: Get (BufferImpl ())
+            getStripped = get
+
+-- | update the syntax information (clear the dirty "flag")
+clearSyntax :: FBuffer -> FBuffer
+clearSyntax = modifyRawbuf updateSyntax
+
+queryRawbuf :: (forall syntax. BufferImpl syntax -> x) -> FBuffer -> x
+queryRawbuf f (FBuffer _ fb _) = f fb
+
+modifyRawbuf :: (forall syntax. BufferImpl syntax -> BufferImpl syntax) -> FBuffer -> FBuffer
+modifyRawbuf f (FBuffer f1 f2 f3) = FBuffer f1 (f f2) f3
+
+queryAndModifyRawbuf :: (forall syntax. BufferImpl syntax -> (BufferImpl syntax,x)) ->
+                     FBuffer -> (FBuffer, x)
+queryAndModifyRawbuf f (FBuffer f1 f5 f3) =
+    let (f5', x) = f f5
+    in (FBuffer f1 f5' f3, x)
+
+file :: FBuffer -> Maybe FilePath
+file b = case b ^. identA of
+  FileBuffer f -> Just f
+  MemBuffer _ -> Nothing
+
+highlightSelectionA :: Lens' FBuffer Bool
+highlightSelectionA = selectionStyleA .
+  lens highlightSelection (\e x -> e { highlightSelection = x })
+
+rectangleSelectionA :: Lens' FBuffer Bool
+rectangleSelectionA = selectionStyleA .
+  lens rectangleSelection (\e x -> e { rectangleSelection = x })
+
+-- | Just stores the mode name.
+instance Binary (Mode syntax) where
+    put = put . E.encodeUtf8 . modeName
+    get = do
+      n <- E.decodeUtf8 <$> get
+      return (emptyMode {modeName = n})
+
+-- | Increases the font size in the buffer by specified number. What
+-- this number actually means depends on the front-end.
+increaseFontSize :: Int -> BufferM ()
+increaseFontSize x = fontsizeVariationA %= \fs -> max 1 (fs + x)
+
+-- | Decreases the font size in the buffer by specified number. What
+-- this number actually means depends on the front-end.
+decreaseFontSize :: Int -> BufferM ()
+decreaseFontSize x = fontsizeVariationA %= \fs -> max 1 (fs - x)
+
+-- | Given a buffer, and some information update the modeline
+--
+-- N.B. the contents of modelines should be specified by user, and
+-- not hardcoded.
+getModeLine :: [T.Text] -> BufferM T.Text
+getModeLine prefix = withModeB (`modeModeLine` prefix)
+
+defaultModeLine :: [T.Text] -> BufferM T.Text
+defaultModeLine prefix = do
+    col <- curCol
+    pos <- pointB
+    ln <- curLn
+    p <- pointB
+    s <- sizeB
+    curChar <- readB
+    ro <-use readOnlyA
+    modeNm <- gets (withMode0 modeName)
+    unchanged <- gets isUnchangedBuffer
+    enc <- use encodingConverterNameA >>= return . \case
+      Nothing -> mempty :: T.Text
+      Just cn -> T.pack $ R.unCn cn
+    let pct
+          | pos == 0 || s == 0 = " Top"
+          | pos == s = " Bot"
+          | otherwise = getPercent p s
+        changed = if unchanged then "-" else "*"
+        readOnly' = if ro then "%" else changed
+        hexxed = T.pack $ showHex (ord curChar) ""
+        hexChar = "0x" <> T.justifyRight 2 '0' hexxed
+        toT = T.pack . show
+
+    nm <- gets $ shortIdentString (length prefix)
+    return $ T.concat [ enc, " ", readOnly', changed, " ", nm
+                      , "     ", hexChar, "  "
+                      , "L", T.justifyRight 5 ' ' (toT ln)
+                      , "  "
+                      , "C", T.justifyRight 3 ' ' (toT col)
+                      , "  ", pct , "  ", modeNm , "  ", toT $ fromPoint p
+                      ]
+
+-- | Given a point, and the file size, gives us a percent string
+getPercent :: Point -> Point -> T.Text
+getPercent a b = T.justifyRight 3 ' ' (T.pack $ show p) `T.snoc` '%'
+    where p = ceiling (aa / bb * 100.0 :: Double) :: Int
+          aa = fromIntegral a :: Double
+          bb = fromIntegral b :: Double
+
+queryBuffer :: (forall syntax. BufferImpl syntax -> x) -> BufferM x
+queryBuffer = gets . queryRawbuf
+
+modifyBuffer :: (forall syntax. BufferImpl syntax -> BufferImpl syntax) -> BufferM ()
+modifyBuffer = modify . modifyRawbuf
+
+queryAndModify :: (forall syntax. BufferImpl syntax -> (BufferImpl syntax,x)) -> BufferM x
+queryAndModify = getsAndModify . queryAndModifyRawbuf
+
+-- | Adds an "overlay" to the buffer
+addOverlayB :: Overlay -> BufferM ()
+addOverlayB ov = do
+  pendingUpdatesA %= (++ [overlayUpdate ov])
+  modifyBuffer $ addOverlayBI ov
+
+getOverlaysOfOwnerB :: R.YiString -> BufferM (Set.Set Overlay)
+getOverlaysOfOwnerB owner = queryBuffer (getOverlaysOfOwnerBI owner)
+
+-- | Remove an existing "overlay"
+delOverlayB :: Overlay -> BufferM ()
+delOverlayB ov = do
+  pendingUpdatesA %= (++ [overlayUpdate ov])
+  modifyBuffer $ delOverlayBI ov
+
+delOverlaysOfOwnerB :: R.YiString -> BufferM ()
+delOverlaysOfOwnerB owner =
+  modifyBuffer $ delOverlaysOfOwnerBI owner
+
+isPointInsideOverlay :: Point -> Overlay -> Bool
+isPointInsideOverlay point overlay =
+    let Overlay _ (MarkValue start _) (MarkValue finish _) _ _ = overlay
+    in start <= point && point <= finish
+
+-- | Execute a @BufferM@ value on a given buffer and window.  The new state of
+-- the buffer is returned alongside the result of the computation.
+runBuffer :: Window -> FBuffer -> BufferM a -> (a, FBuffer)
+runBuffer w b f =
+    let (a, _, b') = runBufferFull w b f
+    in (a, b')
+
+getMarks :: Window -> BufferM (Maybe WinMarks)
+getMarks = gets . getMarksRaw
+
+getMarksRaw :: Window -> FBuffer -> Maybe WinMarks
+getMarksRaw w b = M.lookup (wkey w) (b ^. winMarksA)
+
+runBufferFull :: Window -> FBuffer -> BufferM a -> (a, [Update], FBuffer)
+runBufferFull w b f =
+    let (a, b', updates) = runRWS (fromBufferM f') w b
+        f' = do
+            ms <- getMarks w
+            when (isNothing ms) $ do
+                -- this window has no marks for this buffer yet; have to create them.
+                newMarkValues <- if wkey (b ^. lastActiveWindowA) == def
+                    then return
+                        -- no previous window, create some marks from scratch.
+                         MarkSet { insMark = MarkValue 0 Forward,
+                                   selMark = MarkValue 0 Backward, -- sel
+                                   fromMark = MarkValue 0 Backward } -- from
+                    else do
+                        Just mrks  <- uses winMarksA (M.lookup $ wkey (b ^. lastActiveWindowA))  
+                        forM mrks getMarkValueB
+                newMrks <- forM newMarkValues newMarkB
+                winMarksA %= M.insert (wkey w) newMrks
+            lastActiveWindowA .= w
+            f
+    in (a, updates, pendingUpdatesA %~ (++ fmap TextUpdate updates) $ b')
+
+getMarkValueRaw :: Mark -> FBuffer -> MarkValue
+getMarkValueRaw m = fromMaybe (MarkValue 0 Forward) . queryRawbuf (getMarkValueBI m)
+
+getMarkValueB :: Mark -> BufferM MarkValue
+getMarkValueB = gets . getMarkValueRaw
+
+newMarkB :: MarkValue -> BufferM Mark
+newMarkB v = queryAndModify $ newMarkBI v
+
+deleteMarkB :: Mark -> BufferM ()
+deleteMarkB m = modifyBuffer $ deleteMarkValueBI m
+
+-- | Execute a @BufferM@ value on a given buffer, using a dummy window.  The new state of
+-- the buffer is discarded.
+runBufferDummyWindow :: FBuffer -> BufferM a -> a
+runBufferDummyWindow b = fst . runBuffer (dummyWindow $ bkey b) b
+
+
+-- | Mark the current point in the undo list as a saved state.
+markSavedB :: UTCTime -> BufferM ()
+markSavedB t = do undosA %= setSavedFilePointU
+                  lastSyncTimeA .= t
+
+bkey :: FBuffer -> BufferRef
+bkey = view bkey__A
+
+isUnchangedBuffer :: FBuffer -> Bool
+isUnchangedBuffer = isAtSavedFilePointU . view undosA
+
+startUpdateTransactionB :: BufferM ()
+startUpdateTransactionB = do
+  transactionPresent <- use updateTransactionInFlightA
+  if transactionPresent
+  then error "Already started update transaction"
+  else do
+    undosA %= addChangeU InteractivePoint
+    updateTransactionInFlightA .= True
+
+commitUpdateTransactionB :: BufferM ()
+commitUpdateTransactionB = do
+  transactionPresent <- use updateTransactionInFlightA
+  if not transactionPresent
+  then error "Not in update transaction"
+  else do
+    updateTransactionInFlightA .= False
+    transacAccum <- use updateTransactionAccumA
+    updateTransactionAccumA .= []
+
+    undosA %= (appEndo . mconcat) (Endo . addChangeU . AtomicChange <$> transacAccum)
+    undosA %= addChangeU InteractivePoint
+
+
+undoRedo :: (forall syntax. Mark -> URList -> BufferImpl syntax
+             -> (BufferImpl syntax, (URList, [Update])))
+         -> BufferM ()
+undoRedo f = do
+  isTransacPresent <- use updateTransactionInFlightA
+  if isTransacPresent
+  then error "Can't undo while undo transaction is in progress"
+  else do
+      m <- getInsMark
+      ur <- use undosA
+      (ur', updates) <- queryAndModify (f m ur)
+      undosA .= ur'
+      tell updates
+
+undoB :: BufferM ()
+undoB = undoRedo undoU
+
+redoB :: BufferM ()
+redoB = undoRedo redoU
+
+-- | Undo all updates that happened since last save,
+-- perform a given action and redo all updates again.
+-- Given action must not modify undo history.
+retroactivelyAtSavePointB :: BufferM a -> BufferM a
+retroactivelyAtSavePointB action = do
+    (undoDepth, result) <- go 0
+    replicateM_ undoDepth redoB
+    return result
+    where
+        go step = do
+            atSavedPoint <- gets isUnchangedBuffer
+            if atSavedPoint
+            then (step,) <$> action
+            else undoB >> go (step + 1)
+
+
+-- | Analogous to const, but returns a function that takes two parameters,
+-- rather than one.
+const2 :: t -> t1 -> t2 -> t
+const2 x _ _ = x
+
+-- | Mode applies function that always returns True.
+modeAlwaysApplies :: a -> b -> Bool
+modeAlwaysApplies = const2 True
+
+-- | Mode applies function that always returns False.
+modeNeverApplies :: a -> b -> Bool
+modeNeverApplies = const2 False
+
+emptyMode :: Mode syntax
+emptyMode = Mode
+  {
+   modeName = "empty",
+   modeApplies = modeNeverApplies,
+   modeHL = ExtHL noHighlighter,
+   modePrettify = const $ return (),
+   modeKeymap = id,
+   modeIndent = \_ _ -> return (),
+   modeAdjustBlock = \_ _ -> return (),
+   modeFollow = const emptyAction,
+   modeIndentSettings = IndentSettings
+   { expandTabs = True
+   , tabSize = 8
+   , shiftWidth = 4
+   },
+   modeToggleCommentSelection = Nothing,
+   modeGetStrokes = \_ _ _ _ -> [],
+   modeOnLoad = return (),
+   modeGotoDeclaration = return (),
+   modeModeLine = defaultModeLine
+  }
+
+-- | Create buffer named @nm@ with contents @s@
+newB :: BufferRef -> BufferId -> YiString -> FBuffer
+newB unique nm s =
+ FBuffer { bmode  = emptyMode
+         , rawbuf = newBI s
+         , attributes =
+ Attributes { ident  = nm
+            , bkey__ = unique
+            , undos  = emptyU
+            , preferCol = Nothing
+            , preferVisCol = Nothing
+            , stickyEol = False
+            , bufferDynamic = mempty
+            , pendingUpdates = []
+            , selectionStyle = SelectionStyle False False
+            , keymapProcess = I.End
+            , winMarks = M.empty
+            , lastActiveWindow = dummyWindow unique
+            , lastSyncTime = epoch
+            , readOnly = False
+            , directoryContent = False
+            , inserting = True
+            , pointFollowsWindow = const False
+            , updateTransactionInFlight = False
+            , updateTransactionAccum = []
+            , fontsizeVariation = 0
+            , encodingConverterName = Nothing
+            } }
+
+epoch :: UTCTime
+epoch = UTCTime (toEnum 0) (toEnum 0)
+
+-- | Point of eof
+sizeB :: BufferM Point
+sizeB = queryBuffer sizeBI
+
+-- | Extract the current point
+pointB :: BufferM Point
+pointB = use . markPointA =<< getInsMark
+
+nelemsB :: Int -> Point -> BufferM YiString
+nelemsB n i = R.take n <$> streamB Forward i
+
+streamB :: Direction -> Point -> BufferM YiString
+streamB dir i = queryBuffer $ getStream dir i
+
+indexedStreamB :: Direction -> Point -> BufferM [(Point,Char)]
+indexedStreamB dir i = queryBuffer $ getIndexedStream dir i
+
+strokesRangesB :: Maybe SearchExp -> Region -> BufferM [[Stroke]]
+strokesRangesB regex r = do
+  p <- pointB
+  getStrokes <- withSyntaxB modeGetStrokes
+  queryBuffer $ strokesRangesBI getStrokes regex r p
+
+------------------------------------------------------------------------
+-- Point based operations
+
+-- | Move point in buffer to the given index
+moveTo :: Point -> BufferM ()
+moveTo x = do
+  forgetPreferCol
+  maxP <- sizeB
+  let p = case () of
+        _ | x < 0 -> Point 0
+          | x > maxP -> maxP
+          | otherwise -> x
+  (.= p) . markPointA =<< getInsMark
+
+------------------------------------------------------------------------
+
+setInserting :: Bool -> BufferM ()
+setInserting = (insertingA .=)
+
+checkRO :: BufferM Bool
+checkRO = do
+  ro <- use readOnlyA
+  when ro (fail "Read Only Buffer")
+  return ro
+
+applyUpdate :: Update -> BufferM ()
+applyUpdate update = do
+  ro    <- checkRO
+  valid <- queryBuffer (isValidUpdate update)
+  when (not ro && valid) $ do
+    forgetPreferCol
+    let reversed = reverseUpdateI update
+    modifyBuffer (applyUpdateI update)
+
+    isTransacPresent <- use updateTransactionInFlightA
+    if isTransacPresent
+    then updateTransactionAccumA %= (reversed:)
+    else undosA %= addChangeU (AtomicChange reversed)
+
+    tell [update]
+   -- otherwise, just ignore.
+
+-- | Revert all the pending updates; don't touch the point.
+revertPendingUpdatesB :: BufferM ()
+revertPendingUpdatesB = do
+  updates <- use pendingUpdatesA
+  modifyBuffer (flip (foldr (\u bi -> applyUpdateI (reverseUpdateI u) bi)) [u | TextUpdate u <- updates])
+
+-- | Write an element into the buffer at the current point.
+writeB :: Char -> BufferM ()
+writeB c = do
+  deleteN 1
+  insertB c
+
+-- | Write the list into the buffer at current point.
+writeN :: YiString -> BufferM ()
+writeN cs = do
+  off <- pointB
+  deleteNAt Forward (R.length cs) off
+  insertNAt cs off
+
+-- | Insert newline at current point.
+newlineB :: BufferM ()
+newlineB = insertB '\n'
+
+------------------------------------------------------------------------
+
+-- | Insert given 'YiString' at specified point, extending size of the
+-- buffer.
+insertNAt :: YiString -> Point -> BufferM ()
+insertNAt rope pnt = applyUpdate (Insert pnt Forward rope)
+
+-- | Insert the 'YiString' at current point, extending size of buffer
+insertN :: YiString -> BufferM ()
+insertN cs = pointB >>= insertNAt cs
+
+-- | Insert the char at current point, extending size of buffer
+--
+-- Implementation note: This just 'insertB's a 'R.singleton'. This
+-- seems sub-optimal because we should be able to do much better
+-- without spewing chunks of size 1 everywhere. This approach is
+-- necessary however so an 'Update' can be recorded. A possible
+-- improvement for space would be to have ‘yi-rope’ package optimise
+-- for appends with length 1.
+insertB :: Char -> BufferM ()
+insertB = insertN . R.singleton
+
+------------------------------------------------------------------------
+
+-- | @deleteNAt n p@ deletes @n@ characters forwards from position @p@
+deleteNAt :: Direction -> Int -> Point -> BufferM ()
+deleteNAt _ 0 _ = return ()
+deleteNAt dir n pos = do
+  els <- R.take n <$> streamB Forward pos
+  applyUpdate $ Delete pos dir els
+
+
+------------------------------------------------------------------------
+-- Line based editing
+
+-- | Return the current line number
+curLn :: BufferM Int
+curLn = do
+    p <- pointB
+    queryBuffer (lineAt p)
+
+
+-- | Top line of the screen
+screenTopLn :: BufferM Int
+screenTopLn = do
+    p <- use . markPointA =<< fromMark <$> askMarks
+    queryBuffer (lineAt p)
+
+
+-- | Middle line of the screen
+screenMidLn :: BufferM Int
+screenMidLn = (+) <$> screenTopLn <*> (div <$> screenLines <*> pure 2)
+
+
+-- | Bottom line of the screen
+screenBotLn :: BufferM Int
+screenBotLn = (+) <$> screenTopLn <*> screenLines
+
+
+-- | Amount of lines in the screen
+screenLines :: BufferM Int
+screenLines = pred <$> askWindow actualLines
+
+
+-- | Return line numbers of marks
+markLines :: BufferM (MarkSet Int)
+markLines = mapM getLn =<< askMarks
+        where getLn m = use (markPointA m) >>= lineOf
+
+
+-- | Go to line number @n@. @n@ is indexed from 1. Returns the
+-- actual line we went to (which may be not be the requested line,
+-- if it was out of range)
+gotoLn :: Int -> BufferM Int
+gotoLn x = do
+  moveTo 0
+  succ <$> gotoLnFrom (x - 1)
+
+---------------------------------------------------------------------
+
+setMode0 :: forall syntax. Mode syntax -> FBuffer -> FBuffer
+setMode0 m (FBuffer _ rb at) = FBuffer m (setSyntaxBI (modeHL m) rb) at
+
+modifyMode0 :: (forall syntax. Mode syntax -> Mode syntax) -> FBuffer -> FBuffer
+modifyMode0 f (FBuffer m rb f3) = FBuffer m' (setSyntaxBI (modeHL m') rb) f3
+  where m' = f m
+
+-- | Set the mode
+setAnyMode :: AnyMode -> BufferM ()
+setAnyMode (AnyMode m) = setMode m
+
+setMode :: Mode syntax -> BufferM ()
+setMode m = do
+  modify (setMode0 m)
+  -- reset the keymap process so we use the one of the new mode.
+  keymapProcessA .= I.End
+  modeOnLoad m
+
+-- | Modify the mode
+modifyMode :: (forall syntax. Mode syntax -> Mode syntax) -> BufferM ()
+modifyMode f = do
+  modify (modifyMode0 f)
+  -- reset the keymap process so we use the one of the new mode.
+  keymapProcessA .= I.End
+
+onMode :: (forall syntax. Mode syntax -> Mode syntax) -> AnyMode -> AnyMode
+onMode f (AnyMode m) = AnyMode (f m)
+
+withMode0 :: (forall syntax. Mode syntax -> a) -> FBuffer -> a
+withMode0 f FBuffer {bmode = m} = f m
+
+withModeB :: (forall syntax. Mode syntax -> BufferM a) -> BufferM a
+withModeB = join . gets . withMode0
+
+withSyntax0 :: (forall syntax. Mode syntax -> syntax -> a) -> WindowRef -> FBuffer -> a
+withSyntax0 f wk (FBuffer bm rb _attrs) = f bm (getAst wk rb)
+
+
+withSyntaxB :: (forall syntax. Mode syntax -> syntax -> a) -> BufferM a
+withSyntaxB f = withSyntax0 f <$> askWindow wkey <*> use id
+
+
+focusSyntax ::  M.Map WindowRef Region -> FBuffer -> FBuffer
+focusSyntax r = modifyRawbuf (focusAst r)
+
+withSyntaxB' :: (forall syntax. Mode syntax -> syntax -> BufferM a) -> BufferM a
+withSyntaxB' = join . withSyntaxB
+
+-- | Return indices of strings in buffer matched by regex in the
+-- given region.
+regexRegionB :: SearchExp -> Region -> BufferM [Region]
+regexRegionB regex region = queryBuffer $ regexRegionBI regex region
+
+-- | Return indices of next string in buffer matched by regex in the
+-- given direction
+regexB :: Direction -> SearchExp -> BufferM [Region]
+regexB dir rx = do
+  p <- pointB
+  s <- sizeB
+  regexRegionB rx (mkRegion p (case dir of Forward -> s; Backward -> 0))
+
+---------------------------------------------------------------------
+
+modifyMarkRaw :: Mark -> (MarkValue -> MarkValue) -> FBuffer -> FBuffer
+modifyMarkRaw m f = modifyRawbuf $ modifyMarkBI m f
+
+modifyMarkB :: Mark -> (MarkValue -> MarkValue) -> BufferM ()
+modifyMarkB = (modify .) . modifyMarkRaw
+
+setMarkHereB :: BufferM Mark
+setMarkHereB = getMarkB Nothing
+
+setNamedMarkHereB :: String -> BufferM ()
+setNamedMarkHereB name = do
+    p <- pointB
+    getMarkB (Just name) >>= (.= p) . markPointA
+
+-- | Highlight the selection
+setVisibleSelection :: Bool -> BufferM ()
+setVisibleSelection = (highlightSelectionA .=)
+
+-- | Whether the selection is highlighted
+getVisibleSelection :: BufferM Bool
+getVisibleSelection = use highlightSelectionA
+
+getInsMark :: BufferM Mark
+getInsMark = insMark <$> askMarks
+
+askMarks :: BufferM WinMarks
+askMarks = do
+    Just ms <- getMarks =<< ask
+    return ms
+
+getMarkB :: Maybe String -> BufferM Mark
+getMarkB m = do
+  p <- pointB
+  queryAndModify (getMarkDefaultPosBI m p)
+
+mayGetMarkB :: String -> BufferM (Maybe Mark)
+mayGetMarkB m = queryBuffer (getMarkBI m)
+
+-- | Move point by the given number of characters.
+-- A negative offset moves backwards a positive one forward.
+moveN :: Int -> BufferM ()
+moveN n = do
+    s <- sizeB
+    moveTo =<< min s . max 0 . (+~ Size n) <$> pointB
+
+-- | Move point -1
+leftB :: BufferM ()
+leftB = leftN 1
+
+-- | Move cursor -n
+leftN :: Int -> BufferM ()
+leftN n = moveN (-n)
+
+-- | Move cursor +1
+rightB :: BufferM ()
+rightB = rightN 1
+
+-- | Move cursor +n
+rightN :: Int -> BufferM ()
+rightN = moveN
+
+-- ---------------------------------------------------------------------
+-- Line based movement and friends
+
+-- | Move point down by @n@ lines. @n@ can be negative.
+-- Returns the actual difference in lines which we moved which
+-- may be negative if the requested line difference is negative.
+lineMoveRel :: Int -> BufferM Int
+lineMoveRel = movingToPrefCol . gotoLnFrom
+
+movingToPrefCol :: BufferM a -> BufferM a
+movingToPrefCol f = do
+  prefCol <- use preferColA
+  targetCol <- maybe curCol return prefCol
+  r <- f
+  moveToColB targetCol
+  preferColA .= Just targetCol
+  return r
+
+-- | Moves to a visual column within the current line as shown
+-- on the editor (ie, moving within the current width of a
+-- single visual line)
+movingToPrefVisCol :: BufferM a -> BufferM a
+movingToPrefVisCol f = do
+  prefCol <- use preferVisColA
+  targetCol <- maybe curVisCol return prefCol
+  r <- f
+  moveToVisColB targetCol
+  preferVisColA .= Just targetCol
+  return r
+
+moveToColB :: Int -> BufferM ()
+moveToColB targetCol = do
+  solPnt <- solPointB =<< pointB
+  chrs <- R.toString <$> nelemsB targetCol solPnt
+  is <- indentSettingsB
+  let cols = scanl (colMove is) 0 chrs    -- columns corresponding to the char
+      toSkip = takeWhile (\(char,col) -> char /= '\n' && col < targetCol) (zip chrs cols)
+  moveTo $ solPnt +~ fromIntegral (length toSkip)
+
+moveToVisColB :: Int -> BufferM ()
+moveToVisColB targetCol = do
+  col <- curCol
+  wid <- width <$> use lastActiveWindowA
+  let jumps = col `div` wid
+  moveToColB $ jumps * wid + targetCol
+
+moveToLineColB :: Int -> Int -> BufferM ()
+moveToLineColB line col = gotoLn line >> moveToColB col
+
+pointOfLineColB :: Int -> Int -> BufferM Point
+pointOfLineColB line col = savingPointB $ moveToLineColB line col >> pointB
+
+forgetPreferCol :: BufferM ()
+forgetPreferCol = preferColA .= Nothing >> preferVisColA .= Nothing
+
+savingPrefCol :: BufferM a -> BufferM a
+savingPrefCol f = do
+  pc <- use preferColA
+  pv <- use preferVisColA
+  result <- f
+  preferColA .= pc
+  preferVisColA .= pv
+  return result
+
+-- | Move point up one line
+lineUp :: BufferM ()
+lineUp = void (lineMoveRel (-1))
+
+-- | Move point down one line
+lineDown :: BufferM ()
+lineDown = void (lineMoveRel 1)
+
+-- | Return the contents of the buffer.
+elemsB :: BufferM YiString
+elemsB = queryBuffer mem
+
+-- | Returns the contents of the buffer between the two points.
+--
+-- If the @startPoint >= endPoint@, empty string is returned. If the
+-- points are out of bounds, as much of the content as possible is
+-- taken: you're not guaranteed to get @endPoint - startPoint@
+-- characters.
+betweenB :: Point -- ^ Point to start at
+         -> Point -- ^ Point to stop at
+         -> BufferM YiString
+betweenB (Point s) (Point e) =
+  if s >= e
+  then return (mempty :: YiString)
+  else snd . R.splitAt s . fst . R.splitAt e <$> elemsB
+
+-- | Read the character at the current point
+readB :: BufferM Char
+readB = pointB >>= readAtB
+
+-- | Read the character at the given index
+-- This is an unsafe operation: character NUL is returned when out of bounds
+readAtB :: Point -> BufferM Char
+readAtB i = R.head <$> nelemsB 1 i >>= return . \case
+  Nothing -> '\0'
+  Just c  -> c
+
+replaceCharB :: Char -> BufferM ()
+replaceCharB c = do
+    writeB c
+    leftB
+
+replaceCharWithBelowB :: BufferM ()
+replaceCharWithBelowB = replaceCharWithVerticalOffset 1
+
+replaceCharWithAboveB :: BufferM ()
+replaceCharWithAboveB = replaceCharWithVerticalOffset (-1)
+
+insertCharWithBelowB :: BufferM ()
+insertCharWithBelowB = maybe (return ()) insertB =<< maybeCharBelowB
+
+insertCharWithAboveB :: BufferM ()
+insertCharWithAboveB = maybe (return ()) insertB =<< maybeCharAboveB
+
+replaceCharWithVerticalOffset :: Int -> BufferM ()
+replaceCharWithVerticalOffset offset =
+    maybe (return ()) replaceCharB =<< maybeCharWithVerticalOffset offset
+
+maybeCharBelowB :: BufferM (Maybe Char)
+maybeCharBelowB = maybeCharWithVerticalOffset 1
+
+maybeCharAboveB :: BufferM (Maybe Char)
+maybeCharAboveB = maybeCharWithVerticalOffset (-1)
+
+maybeCharWithVerticalOffset :: Int -> BufferM (Maybe Char)
+maybeCharWithVerticalOffset offset = savingPointB $ do
+    l0 <- curLn
+    c0 <- curCol
+    void $ lineMoveRel offset
+    l1 <- curLn
+    c1 <- curCol
+    curChar <- readB
+    return $ if c0 == c1
+                && l0 + offset == l1
+                && curChar `notElem` ("\n\0" :: String)
+             then Just curChar
+             else Nothing
+
+-- | Delete @n@ characters forward from the current point
+deleteN :: Int -> BufferM ()
+deleteN n = pointB >>= deleteNAt Forward n
+
+------------------------------------------------------------------------
+
+-- | Gives the 'IndentSettings' for the current buffer.
+indentSettingsB :: BufferM IndentSettings
+indentSettingsB = withModeB $ return . modeIndentSettings
+
+-- | Current column.
+-- Note that this is different from offset or number of chars from sol.
+-- (This takes into account tabs, unicode chars, etc.)
+curCol :: BufferM Int
+curCol = colOf =<< pointB
+
+-- | Current column, visually.
+curVisCol :: BufferM Int
+curVisCol = rem <$> curCol <*> (width <$> use lastActiveWindowA)
+
+colOf :: Point -> BufferM Int
+colOf p = do
+  is <- indentSettingsB
+  R.foldl' (colMove is) 0 <$> queryBuffer (charsFromSolBI p)
+
+lineOf :: Point -> BufferM Int
+lineOf p = queryBuffer $ lineAt p
+
+lineCountB :: BufferM Int
+lineCountB = lineOf =<< sizeB
+
+-- | Decides which column we should be on after the given character.
+colMove :: IndentSettings -> Int -> Char -> Int
+colMove is col '\t' | tabSize is > 1 = col + tabSize is
+colMove _  col _    = col + 1
+
+-- | Returns start of line point for a given point @p@
+solPointB :: Point -> BufferM Point
+solPointB p = queryBuffer $ solPoint' p
+
+-- | Returns end of line for given point.
+eolPointB :: Point -> BufferM Point
+eolPointB p = queryBuffer $ eolPoint' p
+
+-- | Go to line indexed from current point
+-- Returns the actual moved difference which of course
+-- may be negative if the requested difference was negative.
+gotoLnFrom :: Int -> BufferM Int
+gotoLnFrom x = do
+    l <- curLn
+    p' <- queryBuffer $ solPoint (l + x)
+    moveTo p'
+    l' <- curLn
+    return (l' - l)
+
+-- | Access to a value into the extensible state, keyed by its type.
+--   This allows you to retrieve inside a 'BufferM' monad, ie:
+--
+-- > value <- getBufferDyn
+getBufferDyn :: forall m a. (Default a, YiVariable a, MonadState FBuffer m, Functor m) => m a
+getBufferDyn = fromMaybe (def :: a) <$> getDyn (use bufferDynamicA) (bufferDynamicA .=)
+
+-- | Access to a value into the extensible state, keyed by its type.
+--   This allows you to save inside a 'BufferM' monad, ie:
+--
+-- > putBufferDyn updatedvalue
+putBufferDyn :: (YiVariable a, MonadState FBuffer m, Functor m) => a -> m ()
+putBufferDyn = putDyn (use bufferDynamicA) (bufferDynamicA .=)
+
+-- | perform a @BufferM a@, and return to the current point. (by using a mark)
+savingExcursionB :: BufferM a -> BufferM a
+savingExcursionB f = do
+    m <- getMarkB Nothing
+    res <- f
+    moveTo =<< use (markPointA m)
+    return res
+
+markPointA :: Mark -> Lens' FBuffer Point
+markPointA mark = lens getter setter where
+  getter b = markPoint $ getMarkValueRaw mark b
+  setter b pos = modifyMarkRaw mark (\v -> v {markPoint = pos}) b
+
+-- | Perform an @BufferM a@, and return to the current point.
+savingPointB :: BufferM a -> BufferM a
+savingPointB f = savingPrefCol $ do
+  p <- pointB
+  res <- f
+  moveTo p
+  return res
+
+-- | Perform an @BufferM a@, and return to the current line and column
+-- number. The difference between this and 'savingPointB' is that here
+-- we attempt to return to the specific line and column number, rather
+-- than a specific number of characters from the beginning of the
+-- buffer.
+--
+-- In case the column is further away than EOL, the point is left at
+-- EOL: 'moveToLineColB' is used internally.
+savingPositionB :: BufferM a -> BufferM a
+savingPositionB f = savingPrefCol $ do
+  (c, l) <- (,) <$> curCol <*> curLn
+  res <- f
+  moveToLineColB l c
+  return res
+
+pointAt :: BufferM a -> BufferM Point
+pointAt f = savingPointB (f *> pointB)
+
+pointAfterCursorB :: Point -> BufferM Point
+pointAfterCursorB p = pointAt $ do
+  moveTo p
+  rightB
+
+-- | What would be the point after doing the given action?
+-- The argument must not modify the buffer.
+destinationOfMoveB :: BufferM a -> BufferM Point
+destinationOfMoveB f = savingPointB (f >> pointB)
+
+-------------
+-- Window
+
+askWindow :: (Window -> a) -> BufferM a
+askWindow = asks
+
+withEveryLineB :: BufferM () -> BufferM ()
+withEveryLineB action = savingPointB $ do
+  lineCount <- lineCountB
+  forM_ [1 .. lineCount] $ \l -> do
+    void $ gotoLn l
+    action
+
+makeLensesWithSuffix "A" ''IndentSettings
+makeLensesWithSuffix "A" ''Mode
diff --git a/src/Yi/Buffer/Normal.hs b/src/Yi/Buffer/Normal.hs
new file mode 100644
--- /dev/null
+++ b/src/Yi/Buffer/Normal.hs
@@ -0,0 +1,103 @@
+{-# LANGUAGE CPP #-}
+
+-- | A normalized API to many buffer operations.
+
+-- The idea is that most operations should be parametric in both
+--  * the textual units they work on
+--  * the direction towards which they operate (if applicable)
+
+module Yi.Buffer.Normal ( TextUnit(Character, Line, VLine, Document, GenUnit)
+                        , isAnySep
+                        , isWordChar
+                        , leftBoundaryUnit
+                        , outsideUnit
+                        , unitDelimited
+                        , unitEmacsParagraph
+                        , unitParagraph
+                        , unitSentence
+                        , unitSep
+                        , unitSepThisLine
+                        , unitViWORD
+                        , unitViWORDAnyBnd
+                        , unitViWORDOnLine
+                        , unitViWord
+                        , unitViWordAnyBnd
+                        , unitViWordOnLine
+                        , unitWord
+                         -- TextUnit is exported abstract intentionally:
+                         -- we'd like to move more units to the GenUnit format.
+                        , atBoundaryB
+                        , deleteB
+                        , doIfCharB
+                        , doUntilB_
+                        , genMaybeMoveB
+                        , genMoveB
+                        , maybeMoveB
+                        , moveB
+                        , numberOfB
+                        , readPrevUnitB
+                        , readUnitB
+                        , regionOfB
+                        , regionOfNonEmptyB
+                        , regionOfPartB
+                        , regionOfPartNonEmptyAtB
+                        , regionOfPartNonEmptyB
+                        , transformB
+                        , transposeB
+                        , untilB
+                        , untilB_
+                        , whileB
+                        , BoundarySide(..)
+                        , checkPeekB
+                        , genAtBoundaryB
+                        , genEnclosingUnit
+                        , genUnitBoundary
+                        , RegionStyle(..)
+                        , convertRegionToStyleB
+                        , extendRegionToBoundaries
+                        , getRegionStyle
+                        , mkRegionOfStyleB
+                        , putRegionStyle
+                        , unitWiseRegion
+                        ) where
+
+import           Data.List          (sort)
+import           Yi.Buffer.Basic    (Direction (Backward, Forward), Point)
+import           Yi.Buffer.Misc     (BufferM, getBufferDyn, moveTo, pointB, putBufferDyn, savingPointB)
+import           Yi.Buffer.Region   (Region (..), inclusiveRegionB, mkRegion, mkRegion')
+import           Yi.Buffer.TextUnit
+import           Yi.Types           (RegionStyle (..))
+
+getRegionStyle :: BufferM RegionStyle
+getRegionStyle = getBufferDyn
+putRegionStyle :: RegionStyle -> BufferM ()
+putRegionStyle = putBufferDyn
+
+convertRegionToStyleB :: Region -> RegionStyle -> BufferM Region
+convertRegionToStyleB r = mkRegionOfStyleB (regionStart r) (regionEnd r)
+
+mkRegionOfStyleB :: Point -> Point -> RegionStyle -> BufferM Region
+mkRegionOfStyleB start' stop' regionStyle =
+   let [start, stop] = sort [start', stop']
+       region = mkRegion start stop in
+   case regionStyle of
+     LineWise  -> inclusiveRegionB =<< unitWiseRegion Line region
+     Inclusive -> inclusiveRegionB region
+     Exclusive -> return region
+     Block     -> return region
+
+unitWiseRegion :: TextUnit -> Region -> BufferM Region
+unitWiseRegion unit = extendRegionToBoundaries unit InsideBound OutsideBound
+
+-- | Extend the given region to boundaries of the text unit.
+-- For instance one can extend the selection to complete lines, or
+-- paragraphs.
+extendRegionToBoundaries :: TextUnit -> BoundarySide -> BoundarySide -> Region -> BufferM Region
+extendRegionToBoundaries unit bs1 bs2 region = savingPointB $ do
+  moveTo $ regionStart region
+  genMaybeMoveB unit (Backward, bs1) Backward
+  start <- pointB
+  moveTo $ regionEnd region
+  genMaybeMoveB unit (Forward, bs2) Forward
+  stop <- pointB
+  return $ mkRegion' (regionDirection region) start stop
diff --git a/src/Yi/Buffer/Region.hs b/src/Yi/Buffer/Region.hs
new file mode 100644
--- /dev/null
+++ b/src/Yi/Buffer/Region.hs
@@ -0,0 +1,145 @@
+{-# LANGUAGE CPP #-}
+{-# OPTIONS_HADDOCK show-extensions #-}
+
+-- |
+-- Module      :  Yi.Buffer.Region
+-- License     :  GPL-2
+-- Maintainer  :  yi-devel@googlegroups.com
+-- Stability   :  experimental
+-- Portability :  portable
+--
+-- This module defines buffer operation on regions
+
+module Yi.Buffer.Region
+  ( module Yi.Region
+  , swapRegionsB
+  , deleteRegionB
+  , replaceRegionB
+  , readRegionB
+  , mapRegionB
+  , modifyRegionB
+  , winRegionB
+  , inclusiveRegionB
+  , blockifyRegion
+  , joinLinesB
+  , concatLinesB
+  , linesOfRegionB
+  ) where
+
+import           Control.Monad       (when)
+import           Data.Char           (isSpace)
+import           Data.List           (sort)
+import           Yi.Buffer.Misc
+import           Yi.Region
+import           Yi.Rope             (YiString)
+import qualified Yi.Rope             as R (YiString, cons, dropWhile, filter
+                                          , lines, lines', map, null, length)
+import           Yi.String           (overInit)
+import           Yi.Utils            (SemiNum ((~-)))
+import           Yi.Window           (winRegion)
+
+
+
+winRegionB :: BufferM Region
+winRegionB = askWindow winRegion
+
+-- | Delete an arbitrary part of the buffer
+deleteRegionB :: Region -> BufferM ()
+deleteRegionB r = deleteNAt (regionDirection r) (fromIntegral (regionEnd r ~- regionStart r)) (regionStart r)
+
+readRegionB :: Region -> BufferM YiString
+readRegionB r = nelemsB (fromIntegral (regionEnd r - i)) i
+    where i = regionStart r
+
+-- | Replace a region with a given rope.
+replaceRegionB :: Region -> YiString -> BufferM ()
+replaceRegionB r s = do
+  deleteRegionB r
+  insertNAt s $ regionStart r
+
+-- | Map the given function over the characters in the region.
+mapRegionB :: Region -> (Char -> Char) -> BufferM ()
+mapRegionB r f = do
+  text <- readRegionB r
+  replaceRegionB r (R.map f text)
+
+-- | Swap the content of two Regions
+swapRegionsB :: Region -> Region -> BufferM ()
+swapRegionsB r r'
+    | regionStart r > regionStart r' = swapRegionsB r' r
+    | otherwise = do w0 <- readRegionB r
+                     w1 <- readRegionB r'
+                     replaceRegionB r' w0
+                     replaceRegionB r  w1
+
+-- | Modifies the given region according to the given
+-- string transformation function
+modifyRegionB :: (R.YiString -> R.YiString)
+                 -- ^ The string modification function
+              -> Region
+                 -- ^ The region to modify
+              -> BufferM ()
+modifyRegionB f region = f <$> readRegionB region >>= replaceRegionB region
+
+-- | Extend the right bound of a region to include it.
+inclusiveRegionB :: Region -> BufferM Region
+inclusiveRegionB r =
+          if regionStart r <= regionEnd r
+              then mkRegion (regionStart r) <$> pointAfterCursorB (regionEnd r)
+              else mkRegion <$> pointAfterCursorB (regionStart r) <*> pure (regionEnd r)
+
+-- | See a region as a block/rectangular region,
+-- since regions are represented by two point, this returns
+-- a list of small regions form this block region.
+blockifyRegion :: Region -> BufferM [Region]
+blockifyRegion r = savingPointB $ do
+  [lowCol, highCol] <- sort <$> mapM colOf [regionStart r, regionEnd r]
+  startLine <- lineOf $ regionStart r
+  endLine   <- lineOf $ regionEnd r
+  when (startLine > endLine) $ fail "blockifyRegion: impossible"
+  mapM (\line -> mkRegion <$> pointOfLineColB line lowCol
+                          <*> pointOfLineColB line (1 + highCol))
+       [startLine..endLine]
+
+-- | Joins lines in the region with a single space, skipping any empty
+-- lines.
+joinLinesB :: Region -> BufferM ()
+joinLinesB = savingPointB . modifyRegionB g'
+  where
+    g' = overInit $ mconcat . pad . R.lines
+
+    pad :: [R.YiString] -> [R.YiString]
+    pad [] = []
+    pad (x:xs) = x : fmap (skip (R.cons ' ' . R.dropWhile isSpace)) xs
+
+    skip g x = if R.null x then x else g x
+
+-- | Concatenates lines in the region preserving the trailing newline
+-- if any.
+concatLinesB :: Region -> BufferM ()
+concatLinesB = savingPointB . modifyRegionB (overInit $ R.filter (/= '\n'))
+
+-- | Gets the lines of a region (as a region), preserving newlines. Thus the
+-- resulting list of regions is a partition of the original region.
+--
+-- The direction of the region is preserved and all smaller regions will
+-- retain that direction.
+--
+-- Note that regions should never be empty, so it would be odd for this to
+-- return an empty list...
+linesOfRegionB :: Region -> BufferM [Region]
+linesOfRegionB region = do
+    let start = regionStart region
+        end = regionEnd region
+        direction = regionDirection region
+    ls <- R.lines' <$> readRegionB region
+    return $ case ls of
+        [] -> []
+        (l:ls') -> let initialRegion = mkRegion' direction start (start + fromIntegral (R.length l))
+                   in scanl nextRegion initialRegion ls'
+
+-- | Given some text and the previous region, finds the next region
+-- (used for implementing linesOfRegionB, not generally useful)
+nextRegion :: Region -> R.YiString -> Region
+nextRegion r l = mkRegion' (regionDirection r) (regionEnd r) (regionEnd r + len)
+    where len = fromIntegral $ R.length l
diff --git a/src/Yi/Buffer/TextUnit.hs b/src/Yi/Buffer/TextUnit.hs
new file mode 100644
--- /dev/null
+++ b/src/Yi/Buffer/TextUnit.hs
@@ -0,0 +1,440 @@
+{-# LANGUAGE DeriveDataTypeable #-}
+{-# OPTIONS_HADDOCK show-extensions #-}
+
+-- |
+-- Module      :  Yi.Buffer.TextUnit
+-- License     :  GPL-2
+-- Maintainer  :  yi-devel@googlegroups.com
+-- Stability   :  experimental
+-- Portability :  portable
+--
+-- Working with blocks (units) of text.
+--
+
+module Yi.Buffer.TextUnit
+    ( TextUnit(..)
+    , outsideUnit
+    , leftBoundaryUnit
+    , unitWord
+    , unitViWord
+    , unitViWORD
+    , unitViWordAnyBnd
+    , unitViWORDAnyBnd
+    , unitViWordOnLine
+    , unitViWORDOnLine
+    , unitDelimited
+    , unitSentence, unitEmacsParagraph, unitParagraph
+    , isAnySep, unitSep, unitSepThisLine, isWordChar
+    , moveB, maybeMoveB
+    , transformB, transposeB
+    , regionOfB, regionOfNonEmptyB, regionOfPartB
+    , regionWithTwoMovesB
+    , regionOfPartNonEmptyB, regionOfPartNonEmptyAtB
+    , readPrevUnitB, readUnitB
+    , untilB, doUntilB_, untilB_, whileB, doIfCharB
+    , atBoundaryB
+    , numberOfB
+    , deleteB, genMaybeMoveB
+    , genMoveB, BoundarySide(..), genAtBoundaryB
+    , checkPeekB
+    , halfUnit
+    , deleteUnitB
+    ) where
+
+
+import           Control.Monad       (void, when, (<=<))
+import           Data.Char           (GeneralCategory (LineSeparator, ParagraphSeparator, Space),
+                                      generalCategory, isAlphaNum, isSeparator, isSpace)
+import           Data.Typeable       (Typeable)
+import           Yi.Buffer.Basic     (Direction (..), Point (Point), mayReverse, reverseDir)
+import           Yi.Buffer.Misc
+import           Yi.Buffer.Region
+import           Yi.Rope             (YiString)
+import qualified Yi.Rope             as R (head, reverse, tail, toString)
+
+
+
+-- | Designate a given "unit" of text.
+data TextUnit = Character -- ^ a single character
+              | Line  -- ^ a line of text (between newlines)
+              | VLine -- ^ a "vertical" line of text (area of text between two characters at the same column number)
+              | Document -- ^ the whole document
+              | GenUnit {genEnclosingUnit :: TextUnit,
+                         genUnitBoundary  :: Direction -> BufferM Bool}
+      -- there could be more text units, like Page, Searched, etc. it's probably a good
+      -- idea to use GenUnit though.
+                deriving Typeable
+
+-- | Turns a unit into its "negative" by inverting the boundaries. For example,
+-- @outsideUnit unitViWord@ will be the unit of spaces between words. For units
+-- without boundaries ('Character', 'Document', ...), this is the identity
+-- function.
+outsideUnit :: TextUnit -> TextUnit
+outsideUnit (GenUnit enclosing boundary) = GenUnit enclosing (boundary . reverseDir)
+outsideUnit x = x -- for a lack of better definition
+
+-- | Common boundary checking function: run the condition on @len@
+-- characters in specified direction shifted by specified offset.
+genBoundary :: Int -- ^ Offset from current position
+            -> Int -- ^ Look-ahead
+            -> (YiString -> Bool) -- ^ predicate
+            -> Direction -- ^ Direction to look in
+            -> BufferM Bool
+genBoundary ofs len condition dir = condition <$> peekB
+  where
+    peekB = do
+      Point p' <- pointB
+      let pt@(Point p) = Point (p' + mayNegate ofs)
+      case dir of
+        Forward -> betweenB pt (Point $ max 0 p + len)
+        Backward -> R.reverse <$> betweenB (Point $ p - len) pt
+
+    mayNegate = case dir of
+      Forward -> id
+      Backward -> negate
+
+-- | a word as in use in Emacs (fundamental mode)
+unitWord :: TextUnit
+unitWord =
+  GenUnit Document $
+  \direction -> checkPeekB (-1) [isWordChar, not . isWordChar] direction
+
+-- | delimited on the left and right by given characters, boolean
+-- argument tells if whether those are included.
+unitDelimited :: Char -> Char -> Bool -> TextUnit
+unitDelimited left right included = GenUnit Document $ \direction ->
+   case (included,direction) of
+       (False, Backward) -> do
+           isCursorOnLeftChar <- (== left) <$> readB
+           when isCursorOnLeftChar rightB
+           checkPeekB 0 [(== left)] Backward
+       (False, Forward)  -> do
+           isCursorOnRightChar <- (== right) <$> readB
+           isTextUnitBlank <- checkPeekB 0 [(== left)] Backward
+           if isTextUnitBlank && isCursorOnRightChar
+           then leftB >> return True
+           else return isCursorOnRightChar
+       (True,  Backward) -> checkPeekB 0 [(== left)] Forward
+       (True,  Forward)  -> rightB >> checkPeekB 0 [(== right)] Backward
+
+isWordChar :: Char -> Bool
+isWordChar x = isAlphaNum x || x == '_'
+
+isNl :: Char -> Bool
+isNl = (== '\n')
+
+-- | Tells if a char can end a sentence ('.', '!', '?').
+isEndOfSentence :: Char -> Bool
+isEndOfSentence = (`elem` ".!?")
+
+-- | Verifies that the string matches all the predicates, pairwise. If
+-- the string is "too small", then return 'False'. Note the length of
+-- predicates has to be finite.
+checks :: [Char -> Bool] -> YiString -> Bool
+checks ps' t' = go ps' (R.toString t')
+  where
+    go [] _ = True
+    go _ [] = False
+    go (p:ps) (x:xs) =  p x && go ps xs
+
+checkPeekB :: Int -> [Char -> Bool] -> Direction -> BufferM Bool
+checkPeekB offset conds = genBoundary offset (length conds) (checks conds)
+
+-- | Helper that takes first two characters of YiString. Faster than
+-- take 2 and string conversion.
+firstTwo :: YiString -> Maybe (Char, Char)
+firstTwo t = case R.head t of
+  Nothing -> Nothing
+  Just c -> case R.tail t >>= R.head of
+    Nothing -> Nothing
+    Just c' -> Just (c, c')
+
+atViWordBoundary :: (Char -> Int) -> Direction -> BufferM Bool
+atViWordBoundary charType = genBoundary (-1) 2 $ \cs -> case firstTwo cs of
+  Just (c1, c2) -> isNl c1 && isNl c2 -- stop at empty lines
+                   || not (isSpace c1) && (charType c1 /= charType c2)
+  Nothing -> True
+
+atAnyViWordBoundary :: (Char -> Int) -> Direction -> BufferM Bool
+atAnyViWordBoundary charType = genBoundary (-1) 2 $ \cs -> case firstTwo cs of
+  Just (c1, c2) -> isNl c1 || isNl c2 || charType c1 /= charType c2
+  Nothing -> True
+
+atViWordBoundaryOnLine :: (Char -> Int) -> Direction -> BufferM Bool
+atViWordBoundaryOnLine charType = genBoundary (-1) 2 $ \cs -> case firstTwo cs of
+  Just (c1, c2)-> isNl c1 || isNl c2 || not (isSpace c1) && charType c1 /= charType c2
+  Nothing -> True
+
+unitViWord :: TextUnit
+unitViWord = GenUnit Document $ atViWordBoundary viWordCharType
+
+unitViWORD :: TextUnit
+unitViWORD = GenUnit Document $ atViWordBoundary viWORDCharType
+
+unitViWordAnyBnd :: TextUnit
+unitViWordAnyBnd = GenUnit Document $ atAnyViWordBoundary viWordCharType
+
+unitViWORDAnyBnd :: TextUnit
+unitViWORDAnyBnd = GenUnit Document $ atAnyViWordBoundary viWORDCharType
+
+unitViWordOnLine :: TextUnit
+unitViWordOnLine = GenUnit Document $ atViWordBoundaryOnLine viWordCharType
+
+unitViWORDOnLine :: TextUnit
+unitViWORDOnLine = GenUnit Document $ atViWordBoundaryOnLine viWORDCharType
+
+viWordCharType :: Char -> Int
+viWordCharType c | isSpace c    = 1
+                 | isWordChar c = 2
+                 | otherwise    = 3
+
+viWORDCharType :: Char -> Int
+viWORDCharType c | isSpace c = 1
+                 | otherwise = 2
+
+-- | Separator characters (space, tab, unicode separators). Most of
+-- the units above attempt to identify "words" with various
+-- punctuation and symbols included or excluded. This set of units is
+-- a simple inverse: it is true for "whitespace" or "separators" and
+-- false for anything that is not (letters, numbers, symbols,
+-- punctuation, whatever).
+isAnySep :: Char -> Bool
+isAnySep c = isSeparator c || isSpace c || generalCategory c `elem` seps
+  where
+    seps = [ Space, LineSeparator, ParagraphSeparator ]
+
+atSepBoundary :: Direction -> BufferM Bool
+atSepBoundary = genBoundary (-1) 2 $ \cs -> case firstTwo cs of
+  Just (c1, c2) -> isNl c1 || isNl c2 || isAnySep c1 /= isAnySep c2
+  Nothing -> True
+
+-- | unitSep is true for any kind of whitespace/separator
+unitSep :: TextUnit
+unitSep = GenUnit Document atSepBoundary
+
+-- | unitSepThisLine is true for any kind of whitespace/separator on this line only
+unitSepThisLine :: TextUnit
+unitSepThisLine = GenUnit Line atSepBoundary
+
+
+-- | Is the point at a @Unit@ boundary in the specified @Direction@?
+atBoundary :: TextUnit -> Direction -> BufferM Bool
+atBoundary Document Backward = (== 0) <$> pointB
+atBoundary Document Forward  = (>=)   <$> pointB <*> sizeB
+atBoundary Character _ = return True
+atBoundary VLine _ = return True -- a fallacy; this needs a little refactoring.
+atBoundary Line direction = checkPeekB 0 [isNl] direction
+atBoundary (GenUnit _ atBound) dir = atBound dir
+
+enclosingUnit :: TextUnit -> TextUnit
+enclosingUnit (GenUnit enclosing _) = enclosing
+enclosingUnit _ = Document
+
+atBoundaryB :: TextUnit -> Direction -> BufferM Bool
+atBoundaryB Document d = atBoundary Document d
+atBoundaryB u d = (||) <$> atBoundary u d <*> atBoundaryB (enclosingUnit u) d
+
+-- | Paragraph to implement emacs-like forward-paragraph/backward-paragraph
+unitEmacsParagraph :: TextUnit
+unitEmacsParagraph = GenUnit Document $ checkPeekB (-2) [not . isNl, isNl, isNl]
+
+-- | Paragraph that begins and ends in the paragraph, not the empty lines surrounding it.
+unitParagraph :: TextUnit
+unitParagraph = GenUnit Document $ checkPeekB (-1) [not . isNl, isNl, isNl]
+
+unitSentence :: TextUnit
+unitSentence = GenUnit unitEmacsParagraph $ \dir -> checkPeekB (if dir == Forward then -1 else 0) (mayReverse dir [isEndOfSentence, isSpace]) dir
+
+-- | Unit that have its left and right boundaries at the left boundary of the argument unit.
+leftBoundaryUnit :: TextUnit -> TextUnit
+leftBoundaryUnit u = GenUnit Document (\_dir -> atBoundaryB u Backward)
+
+-- | @genAtBoundaryB u d s@ returns whether the point is at a given boundary @(d,s)@ .
+-- Boundary @(d,s)@ , taking Word as example, means:
+--      Word
+--     ^^  ^^
+--     12  34
+-- 1: (Backward,OutsideBound)
+-- 2: (Backward,InsideBound)
+-- 3: (Forward,InsideBound)
+-- 4: (Forward,OutsideBound)
+--
+-- rules:
+-- genAtBoundaryB u Backward InsideBound  = atBoundaryB u Backward
+-- genAtBoundaryB u Forward  OutsideBound = atBoundaryB u Forward
+genAtBoundaryB :: TextUnit -> Direction -> BoundarySide -> BufferM Bool
+genAtBoundaryB u d s = withOffset (off u d s) $ atBoundaryB u d
+    where withOffset 0 f = f
+          withOffset ofs f = savingPointB (((ofs +) <$> pointB) >>= moveTo >> f)
+          off _    Backward  InsideBound = 0
+          off _    Backward OutsideBound = 1
+          off _    Forward   InsideBound = 1
+          off _    Forward  OutsideBound = 0
+
+
+numberOfB :: TextUnit -> TextUnit -> BufferM Int
+numberOfB unit containingUnit = savingPointB $ do
+                   maybeMoveB containingUnit Backward
+                   start <- pointB
+                   moveB containingUnit Forward
+                   end <- pointB
+                   moveTo start
+                   length <$> untilB ((>= end) <$> pointB) (moveB unit Forward)
+
+whileB :: BufferM Bool -> BufferM a -> BufferM [a]
+whileB cond = untilB (not <$> cond)
+
+-- | Repeat an action until the condition is fulfilled or the cursor
+-- stops moving. The Action may be performed zero times.
+untilB :: BufferM Bool -> BufferM a -> BufferM [a]
+untilB cond f = do
+  stop <- cond
+  if stop then return [] else doUntilB cond f
+
+-- | Repeat an action until the condition is fulfilled or the cursor
+-- stops moving. The Action is performed at least once.
+doUntilB :: BufferM Bool -> BufferM a -> BufferM [a]
+doUntilB cond f = loop
+  where loop = do
+          p <- pointB
+          x <- f
+          p' <- pointB
+          stop <- cond
+          (x:) <$> if p /= p' && not stop
+            then loop
+            else return []
+
+doUntilB_ :: BufferM Bool -> BufferM a -> BufferM ()
+doUntilB_ cond f = void (doUntilB cond f) -- maybe do an optimized version?
+
+untilB_ :: BufferM Bool -> BufferM a -> BufferM ()
+untilB_ cond f = void (untilB cond f) -- maybe do an optimized version?
+
+-- | Do an action if the current buffer character passes the predicate
+doIfCharB :: (Char -> Bool) -> BufferM a -> BufferM ()
+doIfCharB p o = readB >>= \c -> when (p c) $ void o
+
+
+-- | Boundary side
+data BoundarySide = InsideBound | OutsideBound
+    deriving Eq
+
+-- | Generic move operation
+-- Warning: moving To the (OutsideBound, Backward) bound of Document is impossible (offset -1!)
+-- @genMoveB u b d@: move in direction d until encountering boundary b or unit u. See 'genAtBoundaryB' for boundary explanation.
+genMoveB :: TextUnit -> (Direction, BoundarySide) -> Direction -> BufferM ()
+genMoveB Document (Forward,InsideBound) Forward = moveTo =<< subtract 1 <$> sizeB
+genMoveB Document _                     Forward = moveTo =<< sizeB
+genMoveB Document _ Backward = moveTo 0 -- impossible to go outside beginning of doc.
+genMoveB Character _ Forward  = rightB
+genMoveB Character _ Backward = leftB
+genMoveB VLine     _ Forward  = do
+  ofs <- lineMoveRel 1
+  when (ofs < 1) (maybeMoveB Line Forward)
+genMoveB VLine _ Backward = lineUp
+genMoveB unit (boundDir, boundSide) moveDir =
+  doUntilB_ (genAtBoundaryB unit boundDir boundSide) (moveB Character moveDir)
+
+-- | Generic maybe move operation.
+-- As genMoveB, but don't move if we are at boundary already.
+genMaybeMoveB :: TextUnit -> (Direction, BoundarySide) -> Direction -> BufferM ()
+-- optimized case for Document
+genMaybeMoveB Document boundSpec moveDir = genMoveB Document boundSpec moveDir
+-- optimized case for start/end of Line
+genMaybeMoveB Line (Backward, InsideBound) Backward = moveTo =<< solPointB =<< pointB
+genMaybeMoveB Line (Forward, OutsideBound) Forward = moveTo =<< eolPointB =<< pointB
+genMaybeMoveB unit (boundDir, boundSide) moveDir =
+  untilB_ (genAtBoundaryB unit boundDir boundSide) (moveB Character moveDir)
+
+
+-- | Move to the next unit boundary
+moveB :: TextUnit -> Direction -> BufferM ()
+moveB u d = genMoveB u (d, case d of Forward -> OutsideBound; Backward -> InsideBound) d
+
+
+-- | As 'moveB', unless the point is at a unit boundary
+
+-- So for example here moveToEol = maybeMoveB Line Forward;
+-- in that it will move to the end of current line and nowhere if we
+-- are already at the end of the current line. Similarly for moveToSol.
+
+maybeMoveB :: TextUnit -> Direction -> BufferM ()
+maybeMoveB u d = genMaybeMoveB u (d, case d of Forward -> OutsideBound; Backward -> InsideBound) d
+
+transposeB :: TextUnit -> Direction -> BufferM ()
+transposeB unit direction = do
+  moveB unit (reverseDir direction)
+  w0 <- pointB
+  moveB unit direction
+  w0' <- pointB
+  moveB unit direction
+  w1' <- pointB
+  moveB unit (reverseDir direction)
+  w1 <- pointB
+  swapRegionsB (mkRegion w0 w0') (mkRegion w1 w1')
+  moveTo w1'
+
+-- | Transforms the region given by 'TextUnit' in the 'Direction' with
+-- user-supplied function.
+transformB :: (YiString -> YiString) -> TextUnit -> Direction -> BufferM ()
+transformB f unit direction = do
+  p <- pointB
+  moveB unit direction
+  q <- pointB
+  let r = mkRegion p q
+  replaceRegionB r =<< f <$> readRegionB r
+
+-- | Delete between point and next unit boundary, return the deleted region.
+deleteB :: TextUnit -> Direction -> BufferM ()
+deleteB unit dir = deleteRegionB =<< regionOfPartNonEmptyB unit dir
+
+regionWithTwoMovesB :: BufferM a -> BufferM b -> BufferM Region
+regionWithTwoMovesB move1 move2 =
+    savingPointB $ mkRegion <$> (move1 >> pointB) <*> (move2 >> pointB)
+
+-- | Region of the whole textunit where the current point is.
+regionOfB :: TextUnit -> BufferM Region
+regionOfB unit = regionWithTwoMovesB (maybeMoveB unit Backward) (maybeMoveB unit Forward)
+
+-- An alternate definition would be the following, but it can return two units if the current point is between them.
+-- eg.  "word1 ^ word2" would return both words.
+-- regionOfB unit = mkRegion
+--                  <$> pointAfter (maybeMoveB unit Backward)
+--                  <*> destinationOfMoveB (maybeMoveB unit Forward)
+-- | Non empty region of the whole textunit where the current point is.
+regionOfNonEmptyB :: TextUnit -> BufferM Region
+regionOfNonEmptyB unit = savingPointB $
+  mkRegion <$> (maybeMoveB unit Backward >> pointB) <*> (moveB unit Forward >> pointB)
+
+-- | Region between the point and the next boundary.
+-- The region is empty if the point is at the boundary.
+regionOfPartB :: TextUnit -> Direction -> BufferM Region
+regionOfPartB unit dir = mkRegion <$> pointB <*> destinationOfMoveB (maybeMoveB unit dir)
+
+-- | Non empty region between the point and the next boundary,
+-- In fact the region can be empty if we are at the end of file.
+regionOfPartNonEmptyB :: TextUnit -> Direction -> BufferM Region
+regionOfPartNonEmptyB unit dir = mkRegion <$> pointB <*> destinationOfMoveB (moveB unit dir)
+
+-- | Non empty region at given point and the next boundary,
+regionOfPartNonEmptyAtB :: TextUnit -> Direction -> Point -> BufferM Region
+regionOfPartNonEmptyAtB unit dir p = do
+    oldP <- pointB
+    moveTo p
+    r <- regionOfPartNonEmptyB unit dir
+    moveTo oldP
+    return r
+
+readPrevUnitB :: TextUnit -> BufferM YiString
+readPrevUnitB unit = readRegionB =<< regionOfPartNonEmptyB unit Backward
+
+readUnitB :: TextUnit -> BufferM YiString
+readUnitB = readRegionB <=< regionOfB
+
+halfUnit :: Direction -> TextUnit -> TextUnit
+halfUnit dir (GenUnit enclosing boundary) =
+  GenUnit enclosing (\d -> if d == dir then boundary d else return False)
+halfUnit _dir tu = tu
+
+deleteUnitB :: TextUnit -> Direction -> BufferM ()
+deleteUnitB unit dir = deleteRegionB =<< regionOfPartNonEmptyB unit dir
diff --git a/src/Yi/Buffer/Undo.hs b/src/Yi/Buffer/Undo.hs
new file mode 100644
--- /dev/null
+++ b/src/Yi/Buffer/Undo.hs
@@ -0,0 +1,169 @@
+{-# LANGUAGE CPP                #-}
+{-# LANGUAGE DeriveGeneric      #-}
+{-# LANGUAGE TemplateHaskell    #-}
+
+-- | An implementation of restricted, linear undo, as described in:
+--
+-- >    T. Berlage, "A selective undo mechanism for graphical user interfaces
+-- >    based on command objects", ACM Transactions on Computer-Human
+-- >    Interaction 1(3), pp. 269-294, 1994.
+--
+-- Implementation based on a proposal by sjw.
+--
+-- From Berlage:
+--
+-- >    All buffer-mutating commands are stored (in abstract form) in an
+-- >    Undo list. The most recent item in this list is the action that
+-- >    will be undone next. When it is undone, it is removed from the Undo
+-- >    list, and its inverse is added to the Redo list. The last command
+-- >    put into the Redo list can be redone, and again prepended to the
+-- >    Undo list. New commands are added to the Undo list without
+-- >    affecting the Redo list.
+--
+-- Now, the above assumes that commands can be _redone_ in a state other
+-- than that in which it was orginally done. This is not the case in our
+-- text editor: a user may delete, for example, between an undo and a
+-- redo. Berlage addresses this in S2.3. A Yi example:
+--
+-- >    Delete some characters
+-- >    Undo partialy
+-- >    Move prior in the file, and delete another _chunk_
+-- >    Redo some things  == corruption.
+--
+-- Berlage describes the /stable execution property/:
+--
+-- >    A command is always redone in the same state that it was originally
+-- >    executed in, and is always undone in the state that was reached
+-- >    after the original execution.
+--
+-- >    The only case where the linear undo model violates the stable
+-- >    execution property is when _a new command is submitted while the
+-- >    redo list is not empty_. The _restricted linear undo model_ ...
+-- >    clears the redo list in this case.
+--
+-- Also some discussion of this in: /The Text Editor Sam/, Rob Pike, pg 19.
+--
+
+module Yi.Buffer.Undo (
+    emptyU
+  , addChangeU
+  , setSavedFilePointU
+  , isAtSavedFilePointU
+  , undoU
+  , redoU
+  , URList             {- abstractly -}
+  , Change(AtomicChange, InteractivePoint)
+   ) where
+
+import           GHC.Generics             (Generic)
+
+import           Data.Binary              (Binary (..))
+import           Yi.Buffer.Implementation
+
+data Change = SavedFilePoint
+            | InteractivePoint
+            | AtomicChange !Update
+-- !!! It's very important that the updates are forced, otherwise
+-- !!! we'll keep a full copy of the buffer state for each update
+-- !!! (thunk) put in the URList.
+            deriving (Show, Generic)
+instance Binary Change
+
+-- | A URList consists of an undo and a redo list.
+data URList = URList ![Change] ![Change]
+            deriving (Show, Generic)
+instance Binary URList
+
+-- | A new empty 'URList'.
+-- Notice we must have a saved file point as this is when we assume we are
+-- opening the file so it is currently the same as the one on disk
+emptyU :: URList
+emptyU = URList [SavedFilePoint] []
+
+-- | Add an action to the undo list.
+-- According to the restricted, linear undo model, if we add a command
+-- whilst the redo list is not empty, we will lose our redoable changes.
+addChangeU :: Change -> URList -> URList
+addChangeU InteractivePoint (URList us rs) = URList (addIP us) rs
+addChangeU u (URList us _rs) = URList (u:us) []
+
+-- | Add a saved file point so that we can tell that the buffer has not
+-- been modified since the previous saved file point.
+-- Notice that we must be sure to remove the previous saved file points
+-- since they are now worthless.
+setSavedFilePointU :: URList -> URList
+setSavedFilePointU (URList undos redos) =
+  URList (SavedFilePoint : cleanUndos) cleanRedos
+  where
+  cleanUndos = filter isNotSavedFilePoint undos
+  cleanRedos = filter isNotSavedFilePoint redos
+  isNotSavedFilePoint :: Change -> Bool
+  isNotSavedFilePoint SavedFilePoint = False
+  isNotSavedFilePoint _              = True
+
+-- | This undoes one interaction step.
+undoU :: Mark -> URList -> BufferImpl syntax -> (BufferImpl syntax, (URList, [Update]))
+undoU m = undoUntilInteractive m [] . undoInteractive
+
+-- | This redoes one iteraction step.
+redoU :: Mark -> URList -> BufferImpl syntax -> (BufferImpl syntax, (URList, [Update]))
+redoU = asRedo . undoU
+
+-- | Prepare undo by moving one interaction point from undoes to redoes.
+undoInteractive :: URList -> URList
+undoInteractive (URList us rs) = URList (remIP us) (addIP rs)
+
+remIP, addIP :: [Change] -> [Change]
+
+-- | Remove an initial interactive point, if there is one
+remIP (InteractivePoint:xs) = xs
+remIP xs = xs
+
+-- | Insert an initial interactive point, if there is none
+addIP xs@(InteractivePoint:_) = xs
+addIP xs = InteractivePoint:xs
+
+-- | Repeatedly undo actions, storing away the inverse operations in the
+--   redo list.
+undoUntilInteractive :: Mark -> [Update] -> URList -> BufferImpl syntax -> (BufferImpl syntax, (URList, [Update]))
+undoUntilInteractive pointMark xs ur@(URList cs rs) b = case cs of
+      []                   -> (b, (ur, xs))
+      [SavedFilePoint]     -> (b, (ur, xs)) -- Why this special case?
+      (InteractivePoint:_) -> (b, (ur, xs))
+      (SavedFilePoint:cs') ->
+        undoUntilInteractive pointMark xs (URList cs' (SavedFilePoint:rs)) b
+      (AtomicChange u:cs') ->
+        let ur' = URList cs' (AtomicChange (reverseUpdateI u):rs)
+            b' = applyUpdateWithMoveI u b
+            (b'', (ur'', xs'')) = undoUntilInteractive pointMark xs ur' b'
+        in (b'', (ur'', u:xs''))
+    where
+      -- Apply a /valid/ update and also move point in buffer to update position
+      applyUpdateWithMoveI :: Update -> BufferImpl syntax -> BufferImpl syntax
+      applyUpdateWithMoveI u = case updateDirection u of
+                                 Forward ->  apply . move
+                                 Backward -> move . apply
+          where move = modifyMarkBI pointMark (\v -> v {markPoint = updatePoint u})
+                apply = applyUpdateI u
+
+-- | Run the undo-function @f@ on a swapped URList making it
+--   operate in a redo fashion instead of undo.
+asRedo :: (URList -> t -> (t, (URList, [Update]))) -> URList -> t -> (t, (URList, [Update]))
+asRedo f ur x = let (y,(ur',rs)) = f (swapUndoRedo ur) x in (y,(swapUndoRedo ur',rs))
+  where
+    swapUndoRedo :: URList -> URList
+    swapUndoRedo (URList us rs) = URList rs us
+
+-- | undoIsAtSavedFilePoint. @True@ if the undo list is at a SavedFilePoint indicating
+--   that the buffer has not been modified since we last saved the file.
+-- Note: that an empty undo list does NOT mean that the buffer is not modified since
+-- the last save. Because we may have saved the file and then undone actions done before
+-- the save.
+isAtSavedFilePointU :: URList -> Bool
+isAtSavedFilePointU (URList us _) = isUnchanged us
+  where
+    isUnchanged cs = case cs of
+      []                       -> False
+      (SavedFilePoint : _)     -> True
+      (InteractivePoint : cs') -> isUnchanged cs'
+      _                        -> False
diff --git a/src/Yi/Command.hs b/src/Yi/Command.hs
new file mode 100644
--- /dev/null
+++ b/src/Yi/Command.hs
@@ -0,0 +1,146 @@
+{-# LANGUAGE DeriveDataTypeable         #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE TypeOperators              #-}
+{-# OPTIONS_HADDOCK show-extensions #-}
+
+-- |
+-- Module      :  Yi.Command
+-- License     :  GPL-2
+-- Maintainer  :  yi-devel@googlegroups.com
+-- Stability   :  experimental
+-- Portability :  portable
+--
+-- Various high-level functions to further classify.
+
+module Yi.Command where
+
+import           Control.Exception   (SomeException)
+import           Lens.Micro.Platform ((.=))
+import           Control.Monad       (void)
+import           Control.Monad.Base  (liftBase)
+import           Data.Binary         (Binary)
+import           Data.Default        (Default)
+import qualified Data.Text           as T (Text, init, filter, last, length, unpack)
+import           Data.Typeable       (Typeable)
+import           System.Exit         (ExitCode (..))
+import           Yi.Buffer           (BufferId (MemBuffer), BufferRef, identA, setMode)
+import           Yi.Core             (startSubprocess)
+import           Yi.Editor
+import           Yi.Keymap           (YiM, withUI)
+import           Yi.MiniBuffer
+import qualified Yi.Mode.Compilation as Compilation (mode)
+import qualified Yi.Mode.Interactive as Interactive (spawnProcess)
+import           Yi.Monad            (maybeM)
+import           Yi.Process          (runShellCommand, shellFileName)
+import qualified Yi.Rope             as R (fromText)
+import           Yi.Types            (YiVariable)
+import           Yi.UI.Common        (reloadProject)
+import           Yi.Utils            (io)
+
+
+---------------------------
+-- | Changing the buffer name quite useful if you have
+-- several the same. This also breaks the relation with the file.
+
+changeBufferNameE :: YiM ()
+changeBufferNameE = withMinibufferFree "New buffer name:" strFun
+  where
+    strFun :: T.Text -> YiM ()
+    strFun = withCurrentBuffer . (.=) identA . MemBuffer
+
+----------------------------
+-- | shell-command with argument prompt
+shellCommandE :: YiM ()
+shellCommandE = withMinibufferFree "Shell command:" shellCommandV
+
+----------------------------
+-- | shell-command with a known argument
+shellCommandV :: T.Text -> YiM ()
+shellCommandV cmd = do
+  (exitCode,cmdOut,cmdErr) <- liftBase . runShellCommand $ T.unpack cmd
+  case exitCode of
+    ExitSuccess -> if T.length (T.filter (== '\n') cmdOut) > 17
+                   then withEditor . void $ -- see GitHub issue #477
+                          newBufferE (MemBuffer "Shell Command Output")
+                                     (R.fromText cmdOut)
+                   else printMsg $ case cmdOut of
+                     "" -> "(Shell command with no output)"
+                     -- Drop trailing newline from output
+                     xs -> if T.last xs == '\n' then T.init xs else xs
+    ExitFailure _ -> printMsg cmdErr
+
+----------------------------
+-- Cabal-related commands
+newtype CabalBuffer = CabalBuffer {cabalBuffer :: Maybe BufferRef}
+    deriving (Default, Typeable, Binary)
+
+instance YiVariable CabalBuffer
+
+----------------------------
+-- | cabal-configure
+cabalConfigureE :: CommandArguments -> YiM ()
+cabalConfigureE = cabalRun "configure" configureExit
+
+configureExit :: Either SomeException ExitCode -> YiM ()
+configureExit (Right ExitSuccess) = reloadProjectE "."
+configureExit _ = return ()
+
+
+reloadProjectE :: String -> YiM ()
+reloadProjectE s = withUI $ \ui -> reloadProject ui s
+
+-- | Run the given commands with args and pipe the ouput into the build buffer,
+-- which is shown in an other window.
+buildRun :: T.Text -> [T.Text] -> (Either SomeException ExitCode -> YiM x) -> YiM ()
+buildRun cmd args onExit = withOtherWindow $ do
+   b <- startSubprocess (T.unpack cmd) (T.unpack <$> args) onExit
+   maybeM deleteBuffer =<< cabalBuffer <$> getEditorDyn
+   putEditorDyn $ CabalBuffer $ Just b
+   withCurrentBuffer $ setMode Compilation.mode
+   return ()
+
+makeBuild :: CommandArguments -> YiM ()
+makeBuild (CommandArguments args) = buildRun "make" args (const $ return ())
+
+cabalRun :: T.Text -> (Either SomeException ExitCode -> YiM x) -> CommandArguments -> YiM ()
+cabalRun cmd onExit (CommandArguments args) = buildRun "cabal" (cmd:args) onExit
+
+makeRun :: (Either SomeException ExitCode -> YiM x) -> CommandArguments -> YiM ()
+makeRun onExit (CommandArguments args) = buildRun "make" args onExit
+
+-----------------------
+-- | cabal-build
+cabalBuildE :: CommandArguments -> YiM ()
+cabalBuildE = cabalRun "build" (const $ return ())
+
+makeBuildE :: CommandArguments -> YiM ()
+makeBuildE = makeRun (const $ return ())
+
+shell :: YiM BufferRef
+shell = do
+    sh <- io shellFileName
+    Interactive.spawnProcess sh ["-i"]
+    -- use the -i option for interactive mode (assuming bash)
+
+-- | Search the source files in the project.
+searchSources :: String ::: RegexTag -> YiM ()
+searchSources = grepFind (Doc "*.hs")
+
+-- | Perform a find+grep operation
+grepFind :: String ::: FilePatternTag -> String ::: RegexTag -> YiM ()
+grepFind (Doc filePattern) (Doc searchedRegex) = withOtherWindow $ do
+    void $ startSubprocess "find" [".",
+                                      "-name", "_darcs", "-prune", "-o",
+                                      "-name", filePattern, "-exec", "grep", "-Hnie", searchedRegex, "{}", ";"] (const $ return ())
+    withCurrentBuffer $ setMode Compilation.mode
+    return ()
+
+-----------------------
+-- | stack-build
+
+stackCommandE :: T.Text -> CommandArguments -> YiM ()
+stackCommandE cmd = stackRun cmd (const $ return ())
+
+stackRun :: T.Text -> (Either SomeException ExitCode -> YiM x) -> CommandArguments -> YiM ()
+stackRun cmd onExit (CommandArguments args) = buildRun "stack" (cmd:args) onExit
diff --git a/src/Yi/Command/Help.hs b/src/Yi/Command/Help.hs
new file mode 100644
--- /dev/null
+++ b/src/Yi/Command/Help.hs
@@ -0,0 +1,58 @@
+{-# LANGUAGE DeriveDataTypeable         #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE OverloadedStrings          #-}
+-- |
+-- Module      :  Yi.Command.Help
+-- License     :  GPL-2
+-- Maintainer  :  yi-devel@googlegroups.com
+-- Stability   :  experimental
+-- Portability :  portable
+--
+-- Help command support
+-- This module uses Yi.Eval.describeNamedAction to
+-- show whatever information about particular action is available
+-- from current evaluator (ghciEvaluator currently presents only type.)
+-- TODO: Would be nice to show excerpt from Haddock documentation in the future.
+--
+-- If given no arguments, the help index is shown (using @getAllNamesInScope@).
+--
+-- Please do not try to show descriptions for the whole index,
+-- as our interface to GHCi is too slow.
+
+module Yi.Command.Help(displayHelpFor) where
+
+import           Data.Binary         (Binary)
+import           Data.Default        (Default)
+import qualified Data.Text           as T (Text, pack, unlines, unpack)
+import           Data.Typeable       (Typeable)
+import           Yi.Buffer           (BufferId (MemBuffer), BufferRef)
+import           Yi.Editor
+import           Yi.Eval             (describeNamedAction, getAllNamesInScope)
+import           Yi.Keymap           (YiM)
+import           Yi.Monad            (maybeM)
+import qualified Yi.Rope             as R (fromText)
+import           Yi.Types            (YiVariable)
+
+
+-- | Displays help for a given name, or help index, if no name is given
+displayHelpFor :: T.Text -> YiM ()
+displayHelpFor name = helpFor name >>= displayHelpBuffer
+
+-- | Finds help text to display, given a command argument
+helpFor :: T.Text -> YiM T.Text
+helpFor ""    = (T.unlines . map T.pack) <$> getAllNamesInScope
+helpFor name  = T.pack    <$> describeNamedAction (T.unpack name)
+
+-- * To make help buffer unique:
+-- | Dynamic YiVariable to store the help buffer reference.
+newtype HelpBuffer = HelpBuffer { helpBuffer :: Maybe BufferRef }
+    deriving (Default, Typeable, Binary)
+
+instance YiVariable HelpBuffer
+
+-- | Display help buffer with a given text...
+displayHelpBuffer :: T.Text -> YiM ()
+displayHelpBuffer text = withEditor $ withOtherWindow $ do
+   maybeM deleteBuffer =<< helpBuffer <$> getEditorDyn
+   b <- newBufferE (MemBuffer "*help*") $ R.fromText text
+   putEditorDyn $ HelpBuffer $ Just b
diff --git a/src/Yi/Completion.hs b/src/Yi/Completion.hs
new file mode 100644
--- /dev/null
+++ b/src/Yi/Completion.hs
@@ -0,0 +1,141 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# OPTIONS_HADDOCK show-extensions #-}
+
+-- |
+-- Module      :  Yi.Completion
+-- License     :  GPL-2
+-- Maintainer  :  yi-devel@googlegroups.com
+-- Stability   :  experimental
+-- Portability :  portable
+--
+-- Collection of functions for completion and matching.
+
+module Yi.Completion
+  ( completeInList, completeInList'
+  , completeInListCustomShow
+  , commonPrefix
+  , prefixMatch, infixUptoEndMatch
+  , subsequenceMatch
+  , containsMatch', containsMatch, containsMatchCaseInsensitive
+  , isCasePrefixOf
+  )
+where
+
+import           Data.Function       (on)
+import           Data.List           (find, nub)
+import           Data.Maybe          (catMaybes)
+import           Data.Monoid         ((<>))
+import qualified Data.Text           as T (Text, breakOn, isPrefixOf, length, null, tails, toCaseFold)
+import           Yi.Editor           (EditorM, printMsg, printMsgs)
+import           Yi.String           (commonTPrefix', showT)
+import           Yi.Utils            (commonPrefix)
+
+
+-------------------------------------------
+-- General completion
+
+-- | Like usual 'T.isPrefixOf' but user can specify case sensitivity.
+-- See 'T.toCaseFold' for exotic unicode gotchas.
+isCasePrefixOf :: Bool -- ^ Is case-sensitive?
+             -> T.Text
+             -> T.Text
+             -> Bool
+isCasePrefixOf True = T.isPrefixOf
+isCasePrefixOf False = T.isPrefixOf `on` T.toCaseFold
+
+-- | Prefix matching function, for use with 'completeInList'
+prefixMatch :: T.Text -> T.Text -> Maybe T.Text
+prefixMatch prefix s = if prefix `T.isPrefixOf` s then Just s else Nothing
+
+-- | Text from the match up to the end, for use with 'completeInList'
+infixUptoEndMatch :: T.Text -> T.Text -> Maybe T.Text
+infixUptoEndMatch needle haystack = case T.breakOn needle haystack of
+  (_, t) -> if T.null t then Nothing else Just t
+
+-- | A simple fuzzy match algorithm. Example: "abc" matches "a1b2c"
+subsequenceMatch :: String -> String -> Bool
+subsequenceMatch needle haystack = go needle haystack
+  where go (n:ns) (h:hs) | n == h = go ns hs
+        go (n:ns) (h:hs) | n /= h = go (n:ns) hs
+        go [] _ = True
+        go _ [] = False
+        go _ _  = False
+
+-- | TODO: this is a terrible function, isn't this just
+-- case-insensitive infix? – Fūzetsu
+containsMatch' :: Bool -> T.Text -> T.Text -> Maybe T.Text
+containsMatch' caseSensitive pattern str =
+  const str <$> find (pattern `tstPrefix`) (T.tails str)
+  where
+    tstPrefix = isCasePrefixOf caseSensitive
+
+containsMatch :: T.Text -> T.Text -> Maybe T.Text
+containsMatch = containsMatch' True
+
+containsMatchCaseInsensitive :: T.Text -> T.Text -> Maybe T.Text
+containsMatchCaseInsensitive = containsMatch' False
+
+
+-- | Complete a string given a user input string, a matching function
+-- and a list of possibilites. Matching function should return the
+-- part of the string that matches the user string.
+completeInList :: T.Text -- ^ Input to match on
+               -> (T.Text -> Maybe T.Text) -- ^ matcher function
+               -> [T.Text] -- ^ items to match against
+               -> EditorM T.Text
+completeInList = completeInListCustomShow id
+
+-- | Same as 'completeInList', but maps @showFunction@ on possible
+-- matches when printing
+completeInListCustomShow :: (T.Text -> T.Text) -- ^ Show function
+                         -> T.Text -- ^ Input to match on
+                         -> (T.Text -> Maybe T.Text) -- ^ matcher function
+                         -> [T.Text] -- ^ items to match against
+                         -> EditorM T.Text
+completeInListCustomShow showFunction s match possibilities
+    | null filtered = printMsg "No match" >> return s
+    | prefix /= s = return prefix
+    | isSingleton filtered = printMsg "Sole completion" >> return s
+    | prefix `elem` filtered =
+        printMsg ("Complete, but not unique: " <> showT filtered) >> return s
+    | otherwise = printMsgs (map showFunction filtered)
+                  >> return (bestMatch filtered s)
+    where
+      prefix   = commonTPrefix' filtered
+      filtered = filterMatches match possibilities
+
+completeInList' :: T.Text
+                -> (T.Text -> Maybe T.Text)
+                -> [T.Text]
+                -> EditorM T.Text
+completeInList' s match l = case filtered of
+  [] -> printMsg "No match" >> return s
+  [x] | s == x    -> printMsg "Sole completion" >> return s
+      | otherwise -> return x
+  _ -> printMsgs filtered >> return (bestMatch filtered s)
+  where
+    filtered = filterMatches match l
+
+-- | This function attempts to provide a better tab completion result in
+-- cases where more than one file matches our prefix. Consider directory with
+-- following files: @["Main.hs", "Main.hi", "Main.o", "Test.py", "Foo.hs"]@.
+--
+-- After inserting @Mai@ into the minibuffer and attempting to complete, the
+-- possible matches will be filtered in 'completeInList'' to
+-- @["Main.hs", "Main.hi", "Main.o"]@ however because of multiple matches,
+-- the buffer will not be updated to say @Main.@ but will instead stay at @Mai@.
+--
+-- This is extremely tedious when trying to complete filenames in directories
+-- with many files so here we try to catch common prefixes of filtered files and
+-- if the result is longer than what we have, we use it instead.
+bestMatch :: [T.Text] -> T.Text -> T.Text
+bestMatch fs s = let p = commonTPrefix' fs
+                 in if T.length p > T.length s then p else s
+
+filterMatches :: Eq a => (b -> Maybe a) -> [b] -> [a]
+filterMatches match = nub . catMaybes . fmap match
+
+-- Not really necessary but a bit faster than @(length l) == 1@
+isSingleton :: [a] -> Bool
+isSingleton [_] = True
+isSingleton _   = False
diff --git a/src/Yi/CompletionTree.hs b/src/Yi/CompletionTree.hs
new file mode 100644
--- /dev/null
+++ b/src/Yi/CompletionTree.hs
@@ -0,0 +1,194 @@
+{-# LANGUAGE TupleSections #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE UndecidableInstances #-}
+
+{- |
+Module      :  Yi.CompletionTree
+License     :  GPL-2
+Maintainer  :  yi-devel@googlegroups.com
+Stability   :  experimental
+Portability :  portable
+
+Little helper for completion interfaces.
+
+Intended to be imported qualified:
+
+>import qualified Yi.CompletionTree as CT
+-}
+module Yi.CompletionTree (
+  -- * CompletionTree type
+  CompletionTree (CompletionTree),
+  -- * Lists
+  fromList, toList,
+  -- * Modification
+  complete, update,
+  -- * Debugging
+  pretty,
+  -- ** Lens
+  unCompletionTree
+  ) where
+
+import           Control.Arrow       (first)
+import           Data.Function       (on)
+import           Data.List           (partition, maximumBy, intercalate)
+import qualified Data.Map.Strict     as M
+import           Data.Map.Strict     (Map)
+import           Data.Maybe          (isJust, fromJust, listToMaybe, catMaybes)
+import qualified Data.ListLike       as LL
+import           Data.ListLike       (ListLike)
+import           Lens.Micro.Platform (over, Lens', _2, (.~), (&))
+import           Data.Binary         (Binary)
+
+-- | A CompletionTree is a map of partial completions.
+--
+-- Example:
+--
+-- fromList ["put","putStr","putStrLn","print","abc"]
+--
+-- Gives the following tree:
+--
+--            / \
+--          "p" "abc"
+--         /  \
+--      "ut"  "rint"
+--      /  \
+--   "Str"  ""
+--   /  \
+-- "Ln"  ""
+--
+-- (The empty strings are needed to denote the end of a word)
+-- (A CompletionTree is not limited to a binary tree)
+newtype CompletionTree a = CompletionTree {_unCompletionTree :: (Map a (CompletionTree a))}
+  deriving (Monoid, Eq, Binary)
+
+unCompletionTree :: Lens' (CompletionTree a) (Map a (CompletionTree a))
+unCompletionTree f ct = (\unCompletionTree' -> ct {_unCompletionTree = unCompletionTree'}) <$>
+                        f (_unCompletionTree ct)
+
+instance (Ord a, Show a, ListLike a i) => Show (CompletionTree a) where
+  show ct = "fromList " ++ show (toList ct)
+
+-- | This function converts a list of completable elements to a CompletionTree
+-- It finds elements that share a common prefix and groups them.
+--
+-- prop> fromList . toList = id
+fromList :: (Ord a, ListLike a i, Eq i) => [a] -> CompletionTree a
+fromList [] = mempty
+fromList (x:xs)
+  | x == mempty = over unCompletionTree (M.insert mempty mempty) (fromList xs)
+  | otherwise = case maximumBy' (compare `on` childrenIn xs) (tail $ LL.inits x) of
+      Nothing -> over unCompletionTree (M.insert x mempty) (fromList xs)
+      Just parent -> case first (x:) $ partition (parent `LL.isPrefixOf`) xs of
+        ([_],rest) -> over unCompletionTree (M.insert parent mempty) $ fromList rest
+        (hasParent, rest) -> over unCompletionTree (M.insert parent (fromList $
+           map (fromJust . LL.stripPrefix parent) hasParent)) $ fromList rest
+      -- A parent is the prefix and the children are the items with the parent as prefix
+      where childrenIn list parent = length $ filter (parent `LL.isPrefixOf`) list
+
+-- | The largest element of a non-empty structure with respect to the
+-- given comparison function, Nothing if there are multiple 'largest' elements.
+maximumBy' :: Eq a => (a -> a -> Ordering) -> [a] -> Maybe a
+maximumBy' cmp l | atleast 2 (== max') l = Nothing
+                 | otherwise = Just max'
+  where  max' = maximumBy cmp l
+         -- This short-circuits if the condition is met n times before the end of the list.
+         atleast :: Int -> (a -> Bool) -> [a] -> Bool
+         atleast 0 _ _ = True
+         atleast _ _ [] = False
+         atleast n cmp' (x:xs) | cmp' x = atleast (n - 1) cmp' xs
+                               | otherwise = atleast n cmp' xs
+
+-- | Complete as much as possible without guessing.
+--
+-- Examples:
+--
+-- >>> complete $ fromList ["put","putStrLn","putStr"]
+-- ("put", fromList ["","Str","StrLn"])
+--
+-- >>> complete $ fromList ["put","putStr","putStrLn","abc"]
+-- ("", fromList ["put","putStr","putStrLn","abc"])
+complete :: (Eq i, Ord a, ListLike a i) => CompletionTree a -> (a, CompletionTree a)
+complete (CompletionTree ct)
+  | M.size ct == 1 = if snd (M.elemAt 0 ct) == mempty
+                       then M.elemAt 0 ct & _2 .~ fromList [mempty]
+                       else M.elemAt 0 ct
+  | otherwise = (mempty,CompletionTree ct)
+
+-- | Update the CompletionTree with new information.
+-- An empty list means that there is no completion left.
+-- A [mempty] means that the end of a word is reached.
+--
+-- Examples:
+--
+-- >>> update (fromList ["put","putStr"]) "p"
+-- fromList ["ut","utStr"]
+--
+-- >>> update (fromList ["put","putStr"]) "put"
+-- fromList ["","Str"]
+--
+-- >>> update (fromList ["put","putStr"]) "putS"
+-- fromList ["tr"]
+--
+-- >>> update (fromList ["put"]) "find"
+-- fromList []
+--
+-- >>> update (fromList ["put"]) "put"
+-- fromList [""]
+update :: (Ord a, ListLike a i, Eq i) => CompletionTree a -> a -> CompletionTree a
+update (CompletionTree ct) p
+  -- p is empty, this case just doesn't make sense:
+  | mempty == p = error "Can't update a CompletionTree with a mempty"
+  -- p is a key in the map ct that doesn't have children:
+  -- (This means the end of a word is reached)
+  | isJust one && mempty == fromJust one = CompletionTree $ M.singleton mempty mempty
+  -- p is a key in the map ct with children:
+  | isJust one = fromJust one
+  -- a substring of p is a key in ct:
+  | isJust remaining = uncurry update $ fromJust remaining
+  -- p is a substring of a key in ct:
+  | otherwise = CompletionTree $ M.mapKeys fromJust
+                               $ M.filterWithKey (const . isJust)
+                               $ M.mapKeys (LL.stripPrefix p) ct
+  where
+    one = M.lookup p ct
+    remaining = listToMaybe . catMaybes $
+      map (\p' -> (,fromJust $ LL.stripPrefix p' p) <$> M.lookup p' ct) (tail $ LL.inits p)
+
+-- | Converts a CompletionTree to a list of completions.
+--
+-- prop> toList . fromList = sort . nub
+--
+-- Examples:
+--
+-- >>> toList mempty
+-- []
+--
+-- >>> toList (fromList ["a"])
+-- ["a"]
+--
+-- >>> toList (fromList ["a","a","a"])
+-- ["a"]
+--
+-- >>> toList (fromList ["z","x","y"])
+-- ["x","y","z"]
+toList :: (Ord a, ListLike a i) => CompletionTree a -> [a]
+toList ct
+  | mempty == ct = []
+  | otherwise = toList' ct
+  where
+    toList' (CompletionTree ct)
+      | M.null ct = [mempty]
+      | otherwise = concat $ M.elems $ M.mapWithKey (\k v -> map (k `LL.append`) $ toList' v) ct
+
+-- TODO: make this function display a tree and rename to showTree
+-- | For debugging purposes.
+--
+-- Example:
+--
+-- >>> putStrLn $ pretty $ fromList ["put", "putStr", "putStrLn"]
+-- ["put"[""|"Str"[""|"Ln"]]]
+pretty :: Show a => CompletionTree a -> String
+pretty (CompletionTree ct)
+  | M.null ct = ""
+  | otherwise = "[" ++ intercalate "|" (M.elems (M.mapWithKey (\k v -> shows k (pretty v)) ct)) ++ "]"
diff --git a/src/Yi/Config.hs b/src/Yi/Config.hs
new file mode 100644
--- /dev/null
+++ b/src/Yi/Config.hs
@@ -0,0 +1,31 @@
+{-# OPTIONS_HADDOCK show-extensions #-}
+
+-- |
+-- Module      :  Yi.Config
+-- License     :  GPL-2
+-- Maintainer  :  yi-devel@googlegroups.com
+-- Stability   :  experimental
+-- Portability :  portable
+--
+-- Module exposing common user settings. Users most likely want to be starting
+-- with "Yi.Config.Default".
+
+module Yi.Config ( Config(..), UIConfig(..), UIBoot, CursorStyle(..)
+                 , module Yi.Config.Lens
+                 , configStyle, configFundamentalMode, configTopLevelKeymap
+                 ) where
+
+import Data.Prototype (extractValue)
+import Yi.Config.Lens
+import Yi.Style       (UIStyle)
+import Yi.Types       (AnyMode, Config (..), CursorStyle (..), Keymap,
+                       UIBoot, UIConfig (..), extractTopKeymap)
+
+configStyle :: UIConfig -> UIStyle
+configStyle = extractValue . configTheme
+
+configFundamentalMode :: Config -> AnyMode
+configFundamentalMode = last . modeTable
+
+configTopLevelKeymap :: Config -> Keymap
+configTopLevelKeymap = extractTopKeymap . defaultKm
diff --git a/src/Yi/Config/Default.hs b/src/Yi/Config/Default.hs
new file mode 100644
--- /dev/null
+++ b/src/Yi/Config/Default.hs
@@ -0,0 +1,137 @@
+{-# LANGUAGE CPP               #-}
+{-# LANGUAGE OverloadedStrings #-}
+
+module Yi.Config.Default (defaultConfig) where
+
+
+import           Lens.Micro.Platform          ((.~), (^.), use)
+import           Control.Monad
+import qualified Data.HashMap.Strict as HM
+import qualified Data.Map            as M
+import           Data.Monoid
+import           Paths_yi_core
+import           System.FilePath
+
+import           Yi.Buffer
+import           Yi.Command          (cabalBuildE, cabalConfigureE, grepFind,
+                                      makeBuild, reloadProjectE, searchSources,
+                                      shell)
+import           Yi.Config
+import           Yi.Config.Misc
+import           Yi.Core             (errorEditor, quitEditor)
+import           Yi.Editor
+import           Yi.Eval             (publishedActions)
+import           Yi.File
+import qualified Yi.Interact         as I
+import           Yi.Keymap
+import           Yi.Keymap.Keys
+import           Yi.Layout
+import           Yi.Mode.Common      (fundamentalMode)
+import qualified Yi.Rope             as R
+import           Yi.Search
+import           Yi.Style.Library
+import           Yi.Utils
+
+import           Yi.Types            ()
+
+-- | List of published Actions
+
+-- THIS MUST BE OF THE FORM:
+-- ("symbol", box symbol")
+-- ... so we can hope getting rid of this someday.
+-- Failing to conform to this rule exposes the code to instant deletion.
+--
+-- TODO: String → Text/YiString
+defaultPublishedActions :: HM.HashMap String Action
+defaultPublishedActions = HM.fromList
+    [
+      ("atBoundaryB"            , box atBoundaryB)
+    , ("cabalBuildE"            , box cabalBuildE)
+    , ("cabalConfigureE"        , box cabalConfigureE)
+    , ("closeBufferE"           , box closeBufferE)
+    , ("deleteB"                , box deleteB)
+    , ("deleteBlankLinesB"      , box deleteBlankLinesB)
+    , ("getSelectRegionB"       , box getSelectRegionB)
+    , ("grepFind"               , box grepFind)
+    , ("insertB"                , box insertB)
+    , ("leftB"                  , box leftB)
+    , ("linePrefixSelectionB"   , box linePrefixSelectionB)
+    , ("lineStreamB"            , box lineStreamB)
+--    , ("mkRegion"               , box mkRegion) -- can't make 'instance Promptable Region'
+    , ("makeBuild"              , box makeBuild)
+    , ("moveB"                  , box moveB)
+    , ("numberOfB"              , box numberOfB)
+    , ("pointB"                 , box pointB)
+    , ("regionOfB"              , box regionOfB)
+    , ("regionOfPartB"          , box regionOfPartB)
+    , ("regionOfPartNonEmptyB"  , box regionOfPartNonEmptyB)
+    , ("reloadProjectE"         , box reloadProjectE)
+    , ("replaceString"          , box replaceString)
+    , ("revertE"                , box revertE)
+    , ("shell"                  , box shell)
+    , ("searchSources"          , box searchSources)
+    , ("setAnyMode"             , box setAnyMode)
+    , ("sortLines"              , box sortLines)
+    , ("unLineCommentSelectionB", box unLineCommentSelectionB)
+    , ("writeB"                 , box writeB)
+    ]
+
+  where
+    box :: (Show x, YiAction a x) => a -> Action
+    box = makeAction
+
+
+defaultConfig :: Config
+defaultConfig =
+  publishedActions .~ defaultPublishedActions $
+  Config { startFrontEnd    = error "panic: no frontend compiled in! (configure with -fvty or another frontend.)"
+         , configUI         =  UIConfig
+           { configFontSize = Just 10
+           , configFontName = Nothing
+           , configScrollWheelAmount = 4
+           , configScrollStyle = Nothing
+           , configCursorStyle = FatWhenFocusedAndInserting
+           , configLineWrap = True
+           , configLeftSideScrollBar = True
+           , configAutoHideScrollBar = False
+           , configAutoHideTabBar = True
+           , configWindowFill = ' '
+           , configTheme = defaultTheme
+           }
+         , defaultKm        = modelessKeymapSet nilKeymap
+         , startActions     = []
+         , initialActions   = []
+         , modeTable = [AnyMode fundamentalMode]
+         , debugMode = False
+         , configKillringAccumulate = False
+         , configCheckExternalChangesObsessively = True
+         , configRegionStyle = Exclusive
+         , configInputPreprocess = I.idAutomaton
+         , bufferUpdateHandler = []
+         , layoutManagers = [hPairNStack 1, vPairNStack 1, tall, wide]
+         , configVars = mempty
+         }
+
+nilKeymap :: Keymap
+nilKeymap = choice [
+             char 'q' ?>>! quitEditor,
+             char 'h' ?>>! configHelp
+            ]
+            <|| (anyEvent >>! errorEditor "Keymap not defined, 'q' to quit, 'h' for help.")
+    where
+      configHelp :: YiM ()
+      configHelp = do
+        dataDir <- io getDataDir
+        let x <//> y = R.fromString (x </> y)
+            welcomeText = R.unlines
+              [ "This instance of Yi is not configured."
+              , ""
+              , "To get a standard reasonable keymap, you can run yi with"
+              , "either --as=cua, --as=vim or --as=emacs."
+              , ""
+              , "You should however create your own ~/.config/yi/yi.hs file."
+              , "As a starting point it's recommended to use one of the configs"
+              , "from " <> (dataDir <//> "example-configs/")
+              , ""
+              ]
+        withEditor_ $ newBufferE (MemBuffer "configuration help") welcomeText
diff --git a/src/Yi/Config/Lens.hs b/src/Yi/Config/Lens.hs
new file mode 100644
--- /dev/null
+++ b/src/Yi/Config/Lens.hs
@@ -0,0 +1,26 @@
+{-# LANGUAGE TemplateHaskell #-}
+{-# OPTIONS_HADDOCK show-extensions #-}
+
+-- |
+-- Module      :  Yi.Config.Lens
+-- License     :  GPL-2
+-- Maintainer  :  yi-devel@googlegroups.com
+-- Stability   :  experimental
+-- Portability :  portable
+--
+-- Lenses for types exported in Yi.Config. This module serves as a
+-- convenience module, for easy re-exporting.
+
+module Yi.Config.Lens where
+
+import Lens.Micro.Platform        (Lens)
+import Data.Default      (Default (def))
+import Data.DynamicState (_dyn)
+import Yi.Types          (Config (..), UIConfig (..), YiConfigVariable)
+import Yi.Utils          (makeLensesWithSuffix)
+
+makeLensesWithSuffix "A" ''Config
+makeLensesWithSuffix "A" ''UIConfig
+
+configVariable :: YiConfigVariable a => Lens Config Config a a
+configVariable = configVarsA . _dyn def
diff --git a/src/Yi/Config/Misc.hs b/src/Yi/Config/Misc.hs
new file mode 100644
--- /dev/null
+++ b/src/Yi/Config/Misc.hs
@@ -0,0 +1,3 @@
+module Yi.Config.Misc where
+
+data ScrollStyle = SnapToCenter | SingleLine
diff --git a/src/Yi/Config/Simple.hs b/src/Yi/Config/Simple.hs
new file mode 100644
--- /dev/null
+++ b/src/Yi/Config/Simple.hs
@@ -0,0 +1,377 @@
+{-# LANGUAGE CPP #-}
+{-# LANGUAGE Rank2Types #-}
+{-# OPTIONS_GHC -fno-warn-duplicate-exports #-}
+{-# OPTIONS_HADDOCK show-extensions #-}
+
+-- |
+-- Module      :  Yi.Config.Simple
+-- License     :  GPL-2
+-- Maintainer  :  yi-devel@googlegroups.com
+-- Stability   :  experimental
+-- Portability :  portable
+--
+-- A simplified configuration interface for Yi.
+--
+-- This module provides a simple configuration API, allowing users to
+-- start with an initial configuration and imperatively (monadically)
+-- modify it. Some common actions (keybindings, selecting modes,
+-- choosing the frontend) have been given special commands
+-- ('globalBindKeys', 'setFrontendPreferences', 'addMode', and so on).
+--
+-- A simple configuration might look like the following:
+--
+-- @
+-- import Yi.Config.Simple
+-- import qualified Yi.Mode.Haskell as Haskell
+-- -- note: don't import "Yi", or else there will be name clashes
+--
+-- main = 'configMain' 'defaultEmacsConfig' $ do
+--   'fontSize' '%=' 'Just' 10
+--   'modeBindKeys' Haskell.cleverMode ('metaCh' \'q\' '?>>!' 'reload')
+--   'globalBindKeys' ('metaCh' \'r\' '?>>!' 'reload')
+-- @
+--
+-- A lot of the fields here are specified with the 'Field' type. To write
+-- a field, use ('%='). To read, use 'get'. For modification, use
+-- ('modify'). For example, the functions @foo@ and @bar@ are equivalent:
+--
+-- @
+-- foo = 'modify' 'layoutManagers' 'reverse'
+-- bar = do
+--  lms <- 'get' 'layoutManagers'
+--  'layoutManagers' '%=' 'reverse' lms
+-- @
+
+module Yi.Config.Simple (
+  -- * The main interface
+  ConfigM,
+  configMain,
+  Field,
+  -- * Modes, commands, and keybindings
+  globalBindKeys,
+  modeBindKeys,
+  modeBindKeysByName,
+  addMode,
+  modifyMode,
+  modifyModeByName,
+  -- * Evaluation of commands
+  evaluator,
+#ifdef HINT
+  ghciEvaluator,
+#endif
+  publishedActionsEvaluator,
+  publishAction,
+  publishedActions,
+  -- * Appearance
+  fontName,
+  fontSize,
+  scrollWheelAmount,
+  scrollStyle,
+  ScrollStyle(..),
+  cursorStyle,
+  CursorStyle(..),
+  Side(..),
+  scrollBarSide,
+  autoHideScrollBar,
+  autoHideTabBar,
+  lineWrap,
+  windowFill,
+  theme,
+  -- ** Layout
+  layoutManagers,
+  -- * Debugging
+  debug,
+  -- * Startup hooks
+  runOnStartup,
+  runAfterStartup,
+  -- * Advanced
+  -- $advanced
+  startActions,
+  initialActions,
+  defaultKm,
+  inputPreprocess,
+  modes,
+  regionStyle,
+  killringAccumulate,
+  bufferUpdateHandler,
+  -- * Module exports
+
+  -- we can't just export 'module Yi', because then we would get
+  -- clashes with Yi.Config
+  module Yi.Boot,
+  module Yi.Buffer,
+  module Yi.Core,
+  module Yi.Dired,
+  module Yi.Editor,
+  module Yi.File,
+  module Yi.Config,
+  module Yi.Config.Default,
+  module Yi.Keymap,
+  module Yi.Keymap.Keys,
+  module Yi.Layout,
+  module Yi.Search,
+  module Yi.Style,
+  module Yi.Style.Library,
+  module Yi.Misc,
+ ) where
+
+import           Lens.Micro.Platform (Lens', (.=), (%=), (%~), use, lens)
+import           Control.Monad.State hiding (modify, get)
+import           Data.Maybe(mapMaybe)
+import qualified Data.Text as T
+import           Text.Printf(printf)
+import           Yi.Boot
+import           Yi.Buffer hiding (modifyMode)
+import           Yi.Config.Default
+import           Yi.Config.Misc
+import           Yi.Config.Simple.Types
+import           Yi.Core
+import           Yi.Dired
+import           Yi.Editor
+import           Yi.Eval
+import           Yi.File
+import           Yi.Keymap
+import           Yi.Keymap.Keys
+import           Yi.Layout
+import           Yi.Misc
+import           Yi.Search
+import           Yi.Style
+import           Yi.Style.Library
+import           Yi.Utils
+
+-- we do explicit imports because we reuse a lot of the names
+import           Yi.Config(Config, UIConfig, startFrontEndA, configUIA,
+                           startActionsA, initialActionsA, defaultKmA,
+                           configInputPreprocessA, modeTableA, debugModeA,
+                           configRegionStyleA, configKillringAccumulateA,
+                           bufferUpdateHandlerA, configFontNameA,
+                           configFontSizeA, configScrollWheelAmountA,
+                           configScrollStyleA, configCursorStyleA,
+                           CursorStyle(..), configLeftSideScrollBarA,
+                           configAutoHideScrollBarA, configAutoHideTabBarA,
+                           configLineWrapA, configWindowFillA, configThemeA,
+                           layoutManagersA, configVarsA,
+                           )
+
+
+--------------- Main interface
+-- newtype ConfigM a   (imported)
+
+-- | Starts with the given initial config, makes the described
+-- modifications, then starts yi.
+configMain :: Config -> ConfigM () -> IO ()
+configMain c m = yi =<< execStateT (runConfigM m) c
+
+------------------------- Modes, commands, and keybindings
+-- | Adds the given key bindings to the `global keymap'. The bindings
+-- will override existing bindings in the case of a clash.
+globalBindKeys :: Keymap -> ConfigM ()
+globalBindKeys a = (defaultKmA . topKeymapA) %= (||> a)
+
+-- | @modeBindKeys mode keys@ adds the keybindings in @keys@ to all
+-- modes with the same name as @mode@.
+--
+-- As with 'modifyMode', a mode by the given name must already be
+-- registered, or the function will have no effect, and issue a
+-- command-line warning.
+modeBindKeys :: Mode syntax -> Keymap -> ConfigM ()
+modeBindKeys mode keys =
+  ensureModeRegistered "modeBindKeys" (modeName mode) boundKeys
+  where
+    boundKeys = modeBindKeysByName (modeName mode) keys
+
+-- | @modeBindKeysByName name keys@ adds the keybindings in @keys@ to
+-- all modes with name @name@ (if it is registered). Consider using
+-- 'modeBindKeys' instead.
+modeBindKeysByName :: T.Text -> Keymap -> ConfigM ()
+modeBindKeysByName name k =
+  ensureModeRegistered "modeBindKeysByName" name modMode
+  where
+    f :: (KeymapSet -> KeymapSet) -> KeymapSet -> KeymapSet
+    f mkm km = topKeymapA %~ (||> k) $ mkm km
+
+    modMode = modifyModeByName name (modeKeymapA %~ f)
+
+-- | Register the given mode. It will be preferred over any modes
+-- already defined.
+addMode :: Mode syntax -> ConfigM ()
+addMode m = modeTableA %= (AnyMode m :)
+
+-- | @modifyMode mode f@ modifies all modes with the same name as
+-- @mode@, using the function @f@.
+--
+-- Note that the @mode@ argument is only used by its 'modeName'. In
+-- particular, a mode by the given name must already be registered, or
+-- this function will have no effect, and issue a command-line
+-- warning.
+--
+-- @'modifyMode' mode f = 'modifyModeByName' ('modeName' mode) f@
+modifyMode :: Mode syntax
+           -> (forall syntax'. Mode syntax' -> Mode syntax')
+           -> ConfigM ()
+modifyMode mode f = ensureModeRegistered "modifyMode" (modeName mode) modMode
+  where
+    modMode = modifyModeByName (modeName mode) f
+
+-- | @modifyModeByName name f@ modifies the mode with name @name@
+-- using the function @f@. Consider using 'modifyMode' instead.
+modifyModeByName :: T.Text
+                 -> (forall syntax. Mode syntax -> Mode syntax)
+                 -> ConfigM ()
+modifyModeByName name f =
+    ensureModeRegistered "modifyModeByName" name $ modeTableA %= fmap (onMode g)
+        where
+            g :: forall syntax. Mode syntax -> Mode syntax
+            g m | modeName m == name = f m
+                | otherwise          = m
+
+-- helper functions
+warn :: String -> String -> ConfigM ()
+warn caller msg = io $ putStrLn $ printf "Warning: %s: %s" caller msg
+-- the putStrLn shouldn't be necessary, but it doesn't print anything
+-- if it's not there...
+
+isModeRegistered :: T.Text -> ConfigM Bool
+isModeRegistered name =
+  any (\(AnyMode mode) -> modeName mode == name) <$> use modeTableA
+
+-- ensure the given mode is registered, and if it is, then run the given action.
+ensureModeRegistered :: String -> T.Text -> ConfigM () -> ConfigM ()
+ensureModeRegistered caller name m = do
+  isRegistered <- isModeRegistered name
+  if isRegistered
+   then m
+   else warn caller (printf "mode \"%s\" is not registered." (T.unpack name))
+
+--------------------- Appearance
+-- | 'Just' the font name, or 'Nothing' for default.
+fontName :: Field (Maybe String)
+fontName = configUIA . configFontNameA
+
+-- | 'Just' the font size, or 'Nothing' for default.
+fontSize :: Field (Maybe Int)
+fontSize = configUIA . configFontSizeA
+
+-- | Amount to move the buffer when using the scroll wheel.
+scrollWheelAmount :: Field Int
+scrollWheelAmount = configUIA . configScrollWheelAmountA
+
+-- | 'Just' the scroll style, or 'Nothing' for default.
+scrollStyle :: Field (Maybe ScrollStyle)
+scrollStyle = configUIA . configScrollStyleA
+
+-- | See 'CursorStyle' for documentation.
+cursorStyle :: Field CursorStyle
+cursorStyle = configUIA . configCursorStyleA
+
+data Side = LeftSide | RightSide
+
+-- | Which side to display the scroll bar on.
+scrollBarSide :: Field Side
+scrollBarSide = configUIA . configLeftSideScrollBarA . fromBool
+  where
+    fromBool :: Lens' Bool Side
+    fromBool = lens (\b -> if b then LeftSide else RightSide)
+                    (\_ s -> case s of { LeftSide -> True; RightSide -> False })
+
+-- | Should the scroll bar autohide?
+autoHideScrollBar :: Field Bool
+autoHideScrollBar = configUIA . configAutoHideScrollBarA
+
+-- | Should the tab bar autohide?
+autoHideTabBar :: Field Bool
+autoHideTabBar = configUIA . configAutoHideTabBarA
+
+-- | Should lines be wrapped?
+lineWrap :: Field Bool
+lineWrap = configUIA . configLineWrapA
+
+-- | The character with which to fill empty window space. Usually
+-- \'~\' for vi-like editors, \' \' for everything else.
+windowFill :: Field Char
+windowFill = configUIA . configWindowFillA
+
+-- | UI colour theme.
+theme :: Field Theme
+theme = configUIA . configThemeA
+
+---------- Layout
+-- | List of registered layout managers. When cycling through layouts,
+-- this list will be consulted.
+layoutManagers :: Field [AnyLayoutManager]
+layoutManagers = layoutManagersA
+
+------------------------ Debugging
+-- | Produce a .yi.dbg file with debugging information?
+debug :: Field Bool
+debug = debugModeA
+
+----------- Startup hooks
+-- | Run when the editor is started (this is run after all actions
+-- which have already been registered)
+runOnStartup :: Action -> ConfigM ()
+runOnStartup action = runManyOnStartup [action]
+
+-- | List version of 'runOnStartup'.
+runManyOnStartup :: [Action] -> ConfigM ()
+runManyOnStartup actions = startActions %= (++ actions)
+
+-- | Run after the startup actions have completed, or on reload (this
+-- is run after all actions which have already been registered)
+runAfterStartup :: Action -> ConfigM ()
+runAfterStartup action = runManyAfterStartup [action]
+
+-- | List version of 'runAfterStartup'.
+runManyAfterStartup :: [Action] -> ConfigM ()
+runManyAfterStartup actions = initialActions %= (++ actions)
+
+------------------------ Advanced
+{- $advanced
+
+These fields are here for completeness -- that is, to expose all the
+functionality of the "Yi.Config" module. However, most users probably
+need not use these fields, typically because they provide advanced
+functinality, or because a simpler interface for the common case is
+available above.
+
+-}
+
+-- | Actions to run when the editor is started. Consider using
+-- 'runOnStartup' or 'runManyOnStartup' instead.
+startActions :: Field [Action]
+startActions = startActionsA
+
+-- | Actions to run after startup or reload. Consider using
+-- 'runAfterStartup' or 'runManyAfterStartup' instead.
+initialActions :: Field [Action]
+initialActions = initialActionsA
+
+-- | Default keymap to use.
+defaultKm :: Field KeymapSet
+defaultKm = defaultKmA
+
+-- | ?
+inputPreprocess :: Field (P Event Event)
+inputPreprocess = configInputPreprocessA
+
+-- | List of modes by order of preference. Consider using 'addMode',
+-- 'modeBindKeys', or 'modifyMode' instead.
+modes :: Field [AnyMode]
+modes = modeTableA
+
+-- | Set to 'Exclusive' for an emacs-like behaviour. Consider starting
+-- with 'defaultEmacsConfig', 'defaultVimConfig', or
+-- 'defaultCuaConfig' to instead.
+regionStyle :: Field RegionStyle
+regionStyle = configRegionStyleA
+
+-- | Set to 'True' for an emacs-like behaviour, where all deleted text
+-- is accumulated in a killring. Consider starting with
+-- 'defaultEmacsConfig', 'defaultVimConfig', or 'defaultCuaConfig'
+-- instead.
+killringAccumulate :: Field Bool
+killringAccumulate = configKillringAccumulateA
+
+-- | ?
+bufferUpdateHandler :: Field [[Update] -> BufferM ()]
+bufferUpdateHandler = bufferUpdateHandlerA
diff --git a/src/Yi/Config/Simple/Types.hs b/src/Yi/Config/Simple/Types.hs
new file mode 100644
--- /dev/null
+++ b/src/Yi/Config/Simple/Types.hs
@@ -0,0 +1,38 @@
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE Rank2Types                 #-}
+
+-- | exports from "Yi.Config.Simple" which are useful to \"core yi\" rather than just config files.
+module Yi.Config.Simple.Types
+ where
+
+import Lens.Micro.Platform (Lens')
+import Control.Monad.Base  (MonadBase)
+import Control.Monad.State (MonadState, StateT)
+import Yi.Config           (Config, configVariable)
+import Yi.Types            (YiConfigVariable)
+
+-- | The configuration monad. Run it with 'configMain'.
+newtype ConfigM a = ConfigM {
+    runConfigM :: StateT Config IO a
+  } deriving (Monad, Functor, Applicative, MonadState Config, MonadBase IO)
+
+-- | Fields that can be modified with all lens machinery.
+type Field a = Lens' Config a
+
+{- | Accessor for any 'YiConfigVariable', to be used by modules defining
+'YiConfigVariable's. Such modules should provide a custom-named field.
+For instance, take the following hypothetical 'YiConfigVariable':
+
+@newtype UserName = UserName { unUserName :: String }
+  deriving(Typeable, Binary, Default)
+instance YiConfigVariable UserName
+
+$(nameDeriveAccessors ''UserName (\n -> Just (n ++ \"A\")))
+
+userName :: 'Field' 'String'
+userName = unUserNameA '.' 'customVariable'@
+
+Here, the hypothetical library would provide the field @userName@ to be used in preference to @customVariable@.
+-}
+customVariable :: YiConfigVariable a => Field a
+customVariable = configVariable
diff --git a/src/Yi/Core.hs b/src/Yi/Core.hs
new file mode 100644
--- /dev/null
+++ b/src/Yi/Core.hs
@@ -0,0 +1,559 @@
+{-# LANGUAGE LambdaCase          #-}
+{-# LANGUAGE OverloadedStrings   #-}
+{-# LANGUAGE Rank2Types          #-}
+{-# LANGUAGE RecursiveDo         #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# OPTIONS_HADDOCK show-extensions #-}
+
+-- |
+-- Module      :  Yi.Core
+-- License     :  GPL-2
+-- Maintainer  :  yi-devel@googlegroups.com
+-- Stability   :  experimental
+-- Portability :  portable
+--
+-- The core actions of Yi. This module is the link between the editor
+-- and the UI. Key bindings, and libraries should manipulate Yi
+-- through the interface defined here.
+
+module Yi.Core
+  (
+  -- * Construction and destruction
+    startEditor
+  , quitEditor          -- :: YiM ()
+
+  -- * User interaction
+  , refreshEditor       -- :: YiM ()
+  , suspendEditor       -- :: YiM ()
+  , userForceRefresh
+
+  -- * Global editor actions
+  , errorEditor         -- :: String -> YiM ()
+  , closeWindow         -- :: YiM ()
+  , closeWindowEmacs
+
+  -- * Interacting with external commands
+  , runProcessWithInput          -- :: String -> String -> YiM String
+  , startSubprocess                 -- :: FilePath -> [String] -> YiM ()
+  , sendToProcess
+
+  -- * Misc
+  , runAction
+  , withSyntax
+  , focusAllSyntax
+  , forkAction
+  ) where
+
+import           Prelude                        hiding (elem, mapM_, or)
+
+import           Control.Concurrent             (ThreadId, forkIO, forkOS,
+                                                 modifyMVar, modifyMVar_,
+                                                 newMVar, readMVar, threadDelay)
+import           Control.Exc                    (ignoringException)
+import           Control.Exception              (SomeException, handle)
+import           Lens.Micro.Platform            (mapped, use, view, (%=), (%~), 
+                                                 (&), (.=), (.~), (^.))
+import           Control.Monad                  (forever, void, when)
+import           Control.Monad.Base             (MonadBase (liftBase))
+import           Control.Monad.Except           ()
+import           Control.Monad.Reader           (MonadReader (ask), ReaderT (runReaderT), asks)
+import qualified Data.DelayList                 as DelayList (decrease, insert)
+import           Data.Foldable                  (elem, find, forM_, mapM_, or, toList)
+import           Data.List                      (partition)
+import           Data.List.NonEmpty             (NonEmpty (..))
+import qualified Data.List.PointedList.Circular as PL (PointedList (_focus), length)
+import           Data.List.Split                (splitOn)
+import qualified Data.Map                       as M (assocs, delete, empty, fromList, insert, member)
+import           Data.Maybe                     (fromMaybe, isNothing)
+import           Data.Monoid                    (First (First, getFirst), (<>))
+import qualified Data.Text                      as T (Text, pack, unwords)
+import           Data.Time                      (getCurrentTime)
+import           Data.Time.Clock.POSIX          (posixSecondsToUTCTime)
+import           Data.Traversable               (forM)
+import           GHC.Conc                       (labelThread)
+import           System.Directory               (doesFileExist)
+import           System.Exit                    (ExitCode)
+import           System.IO                      (Handle, hPutStr, hWaitForInput)
+import           System.PosixCompat.Files       (getFileStatus, modificationTime)
+import           System.Process                 (ProcessHandle,
+                                                 getProcessExitCode,
+                                                 readProcessWithExitCode,
+                                                 terminateProcess)
+import           Yi.Buffer
+import           Yi.Config
+import           Yi.Debug                       (logPutStrLn)
+import           Yi.Editor
+import           Yi.Keymap
+import           Yi.Keymap.Keys
+import           Yi.KillRing                    (krEndCmd)
+import           Yi.Monad                       (gets)
+import           Yi.PersistentState             (loadPersistentState, savePersistentState)
+import           Yi.Process
+import qualified Yi.Rope                        as R (YiString, fromString, readFile)
+import           Yi.String                      (chomp, showT)
+import           Yi.Style                       (errorStyle, strongHintStyle)
+import qualified Yi.UI.Common                   as UI (UI (end, layout, main, refresh, suspend, userForceRefresh))
+import           Yi.Utils                       (io)
+import           Yi.Window                      (bufkey, dummyWindow, isMini, winRegion, wkey)
+
+
+uses l f = f <$> use l
+-- | Make an action suitable for an interactive run.
+-- UI will be refreshed.
+interactive :: IsRefreshNeeded -> [Action] -> YiM ()
+interactive isRefreshNeeded action = do
+  evs <- withEditor $ use pendingEventsA
+  logPutStrLn $ ">>> interactively" <> showEvs evs
+  withEditor $ buffersA %= (fmap $ undosA %~ addChangeU InteractivePoint)
+  mapM_ runAction action
+  withEditor $ killringA %= krEndCmd
+  when (isRefreshNeeded == MustRefresh) refreshEditor
+  logPutStrLn "<<<"
+  return ()
+
+-- ---------------------------------------------------------------------
+-- | Start up the editor, setting any state with the user preferences
+-- and file names passed in, and turning on the UI
+--
+startEditor :: Config -> Maybe Editor -> IO ()
+startEditor cfg st = do
+    let uiStart = startFrontEnd cfg
+
+    logPutStrLn "Starting Core"
+
+    -- Use an empty state unless resuming from an earlier session and
+    -- one is already available
+    let editor = fromMaybe emptyEditor st
+    -- here to add load history etc?
+
+    -- Setting up the 1st window is a bit tricky because most
+    -- functions assume there exists a "current window"
+    newSt <- newMVar $ YiVar editor 1 M.empty
+    (ui, runYi) <- mdo
+        let handler (exception :: SomeException) =
+              runYi $ errorEditor (showT exception) >> refreshEditor
+
+            inF []     = return ()
+            inF (e:es) = handle handler $ runYi $ dispatch (e :| es)
+
+            outF refreshNeeded acts =
+                handle handler $ runYi $ interactive refreshNeeded acts
+            runYi f   = runReaderT (runYiM f) yi
+            yi        = Yi ui inF outF cfg newSt
+        ui <- uiStart cfg inF (outF MustRefresh) editor
+        return (ui, runYi)
+
+    runYi loadPersistentState
+
+    runYi $ do
+      if isNothing st
+        -- process options if booting for the first time
+        then postActions NoNeedToRefresh $ startActions cfg
+        -- otherwise: recover the mode of buffers
+        else withEditor $ buffersA.mapped %= recoverMode (modeTable cfg)
+      postActions NoNeedToRefresh $ initialActions cfg ++ [makeAction showErrors]
+
+    runYi refreshEditor
+
+    UI.main ui -- transfer control to UI
+
+-- | Runs a 'YiM' action in a separate thread.
+--
+-- Notes:
+--
+-- * It seems to work but I don't know why
+--
+-- * Maybe deadlocks?
+--
+-- * If you're outputting into the Yi window, you should really limit
+-- the rate at which you do so: for example, the Pango front-end will
+-- quite happily segfault/double-free if you output too fast.
+--
+-- I am exporting this for those adventurous to play with but I have
+-- only discovered how to do this a night before the release so it's
+-- rather experimental. A simple function that prints a message once a
+-- second, 5 times, could be written like this:
+--
+-- @
+-- printer :: YiM ThreadId
+-- printer = do
+--   mv <- io $ newMVar (0 :: Int)
+--   forkAction (suicide mv) MustRefresh $ do
+--     c <- io $ do
+--       modifyMVar_ mv (return . succ)
+--       tryReadMVar mv
+--     case c of
+--       Nothing -> printMsg "messaging unknown time"
+--       Just x -> printMsg $ "message #" <> showT x
+--   where
+--     suicide mv = tryReadMVar mv >>= \case
+--       Just i | i >= 5 -> return True
+--       _ -> threadDelay 1000000 >> return False
+-- @
+forkAction :: (YiAction a x, Show x)
+           => IO Bool
+              -- ^ runs after we insert the action: this may be a
+              -- thread delay or a thread suicide or whatever else;
+              -- when delay returns False, that's our signal to
+              -- terminate the thread.
+           -> IsRefreshNeeded
+              -- ^ should we refresh after each action
+           -> a
+              -- ^ The action to actually run
+           -> YiM ThreadId
+forkAction delay ref ym = onYiVar $ \yi yv -> do
+  let loop = do
+        yiOutput yi ref [makeAction ym]
+        delay >>= \b -> when b loop
+  t <- forkIO loop
+  return (yv, t)
+
+recoverMode :: [AnyMode] -> FBuffer -> FBuffer
+recoverMode tbl buffer  = case fromMaybe (AnyMode emptyMode) (find (\(AnyMode m) -> modeName m == oldName) tbl) of
+    AnyMode m -> setMode0 m buffer
+  where oldName = case buffer of FBuffer {bmode = m} -> modeName m
+
+postActions :: IsRefreshNeeded -> [Action] -> YiM ()
+postActions refreshNeeded actions = do yi <- ask; liftBase $ yiOutput yi refreshNeeded actions
+
+-- | Display the errors buffer if it is not already visible.
+showErrors :: YiM ()
+showErrors = withEditor $ do
+               bs <- gets $ findBufferWithName "*errors*"
+               case bs of
+                 [] -> return ()
+                 _  -> do splitE
+                          switchToBufferWithNameE "*errors*"
+
+-- | Process events by advancing the current keymap automaton and
+-- executing the generated actions.
+dispatch :: NonEmpty Event -> YiM ()
+dispatch (ev :| evs) = do
+  yi <- ask
+  (userActions, _p') <- withCurrentBuffer $ do
+    keymap <- gets (withMode0 modeKeymap)
+    p0 <- use keymapProcessA
+    let km = extractTopKeymap $ keymap $ defaultKm $ yiConfig yi
+    let freshP = Chain (configInputPreprocess $ yiConfig yi) (mkAutomaton km)
+        p = case computeState p0 of
+              Dead  -> freshP
+              _     -> p0
+        (actions, p') = processOneEvent p ev
+        state = computeState p'
+        ambiguous = case state of
+            Ambiguous _ -> True
+            _ -> False
+    keymapProcessA .= (if ambiguous then freshP else p')
+    let actions0 = case state of
+          Dead -> [EditorA $ do
+                      evs' <- use pendingEventsA
+                      printMsg ("Unrecognized input: " <> showEvs (evs' ++ [ev]))]
+          _ -> actions
+
+        actions1 = [ EditorA (printMsg "Keymap was in an ambiguous state! Resetting it.")
+                   | ambiguous]
+
+    return (actions0 ++ actions1, p')
+
+  let decay, pendingFeedback :: EditorM ()
+      decay = statusLinesA %= DelayList.decrease 1
+      pendingFeedback = do pendingEventsA %= (++ [ev])
+                           if null userActions
+                               then printMsg . showEvs =<< use pendingEventsA
+                               else pendingEventsA .= []
+      allActions = [makeAction decay] ++ userActions ++ [makeAction pendingFeedback]
+
+  case evs of
+    [] -> postActions MustRefresh allActions
+    (e:es) -> postActions NoNeedToRefresh allActions >> dispatch (e :| es)
+
+
+showEvs :: [Event] -> T.Text
+showEvs = T.unwords . fmap (T.pack . prettyEvent)
+
+-- ---------------------------------------------------------------------
+-- Meta operations
+
+-- | Quit.
+quitEditor :: YiM ()
+quitEditor = do
+    savePersistentState
+    onYiVar $ terminateSubprocesses (const True)
+    withUI (`UI.end` True)
+
+-- | Update (visible) buffers if they have changed on disk.
+-- FIXME: since we do IO here we must catch exceptions!
+checkFileChanges :: Editor -> IO Editor
+checkFileChanges e0 = do
+  now <- getCurrentTime
+  -- Find out if any file was modified "behind our back" by
+  -- other processes.
+  newBuffers <- forM (buffers e0) $ \b ->
+    let nothing = return (b, Nothing)
+    in if bkey b `elem` visibleBuffers
+    then
+      case b ^. identA of
+         FileBuffer fname -> do
+            fe <- doesFileExist fname
+            if not fe then nothing else do
+                modTime <- fileModTime fname
+                if b ^. lastSyncTimeA < modTime
+                   then if isUnchangedBuffer b
+                     then R.readFile fname >>= return . \case
+                            Left m ->
+                              (runDummy b (readOnlyA .= True), Just $ msg3 m)
+                            Right (newContents, c) ->
+                              (runDummy b (revertB newContents (Just c) now), Just msg1)
+                     else return (b, Just msg2)
+                   else nothing
+         _ -> nothing
+    else nothing
+  -- show appropriate update message if applicable
+  return $ case getFirst (foldMap (First . snd) newBuffers) of
+         Just msg -> (statusLinesA %~ DelayList.insert msg) e0 {buffers = fmap fst newBuffers}
+         Nothing -> e0
+  where msg1 = (1, (["File was changed by a concurrent process, reloaded!"], strongHintStyle))
+        msg2 = (1, (["Disk version changed by a concurrent process"], strongHintStyle))
+        msg3 x = (1, (["File changed on disk to unknown encoding, not updating buffer: " <> x], strongHintStyle))
+        visibleBuffers = bufkey <$> windows e0
+        fileModTime f = posixSecondsToUTCTime . realToFrac . modificationTime <$> getFileStatus f
+        runDummy b act = snd $ runBuffer (dummyWindow $ bkey b) b act
+
+-- | Hide selection, clear "syntax dirty" flag (as appropriate).
+clearAllSyntaxAndHideSelection :: Editor -> Editor
+clearAllSyntaxAndHideSelection = buffersA %~ fmap (clearSyntax . clearHighlight)
+  where
+    clearHighlight fb =
+      -- if there were updates, then hide the selection.
+      let h = view highlightSelectionA fb
+          us = view pendingUpdatesA fb
+      in highlightSelectionA .~ (h && null us) $ fb
+
+
+-- Focus syntax tree on the current window, for all visible buffers.
+focusAllSyntax :: Editor -> Editor
+focusAllSyntax e6 = buffersA %~ fmap (\b -> focusSyntax (regions b) b) $ e6
+    where regions b = M.fromList [(wkey w, winRegion w) | w <- toList $ windows e6, bufkey w == bkey b]
+          -- Why bother filtering the region list? After all the trees
+          -- are lazily computed. Answer: focusing is an incremental
+          -- algorithm. Each "focused" path depends on the previous
+          -- one. If we left unforced focused paths, we'd create a
+          -- long list of thunks: a memory leak.
+
+-- | Redraw
+refreshEditor :: YiM ()
+refreshEditor = onYiVar $ \yi var -> do
+        let cfg = yiConfig yi
+            runOnWins a = runEditor cfg
+                                    (do ws <- use windowsA
+                                        forM ws $ flip withWindowE a)
+            style = configScrollStyle $ configUI cfg
+        let scroll e3 = let (e4, relayout) = runOnWins (snapScreenB style) e3 in
+                -- Scroll windows to show current points as appropriate
+                -- Do another layout pass if there was any scrolling;
+                (if or relayout then UI.layout (yiUi yi) else return) e4
+
+        e7 <- (if configCheckExternalChangesObsessively cfg
+               then checkFileChanges
+               else return) (yiEditor var) >>=
+             return . clearAllSyntaxAndHideSelection >>=
+             -- Adjust window sizes according to UI info
+             UI.layout (yiUi yi) >>=
+             scroll >>=
+             -- Adjust point according to the current layout;
+             return . fst . runOnWins snapInsB >>=
+             return . focusAllSyntax >>=
+             -- Clear "pending updates" and "followUp" from buffers.
+             return . (buffersA %~ fmap (clearUpdates . clearFollow))
+        -- Display the new state of the editor
+        UI.refresh (yiUi yi) e7
+        -- Terminate stale processes.
+        terminateSubprocesses (staleProcess $ buffers e7) yi var {yiEditor = e7}
+  where
+    clearUpdates = pendingUpdatesA .~ []
+    clearFollow = pointFollowsWindowA .~ const False
+    -- Is this process stale? (associated with a deleted buffer)
+    staleProcess bs p = not (bufRef p `M.member` bs)
+
+
+-- | Suspend the program
+suspendEditor :: YiM ()
+suspendEditor = withUI UI.suspend
+
+------------------------------------------------------------------------
+
+------------------------------------------------------------------------
+-- | Pipe a string through an external command, returning the stdout
+-- chomp any trailing newline (is this desirable?)
+--
+-- Todo: varients with marks?
+--
+runProcessWithInput :: String -> String -> YiM String
+runProcessWithInput cmd inp = do
+    let (f:args) = splitOn " " cmd
+    (_,out,_err) <- liftBase $ readProcessWithExitCode f args inp
+    return (chomp "\n" out)
+
+------------------------------------------------------------------------
+
+-- | Same as 'Yi.Editor.printMsg', but do nothing instead of printing @()@
+msgEditor :: T.Text -> YiM ()
+msgEditor "()" = return ()
+msgEditor s = printMsg s
+
+runAction :: Action -> YiM ()
+runAction (YiA act) = act >>= msgEditor . showT
+runAction (EditorA act) = withEditor act >>= msgEditor . showT
+runAction (BufferA act) = withCurrentBuffer act >>= msgEditor . showT
+
+-- | Show an error on the status line and log it.
+errorEditor :: T.Text -> YiM ()
+errorEditor s = do
+  printStatus (["error: " <> s], errorStyle)
+  logPutStrLn $ "errorEditor: " <> s
+
+-- | Close the current window.
+-- If this is the last window open, quit the program.
+--
+-- CONSIDER: call quitEditor when there are no other window in the
+-- 'interactive' function. (Not possible since the windowset type
+-- disallows it -- should it be relaxed?)
+closeWindow :: YiM ()
+closeWindow = do
+    winCount <- withEditor $ uses windowsA PL.length
+    tabCount <- withEditor $ uses tabsA PL.length
+    when (winCount == 1 && tabCount == 1) quitEditor
+    withEditor tryCloseE
+
+-- | This is a like 'closeWindow' but with emacs behaviour of C-x 0:
+-- if we're trying to close the minibuffer or last buffer in the
+-- editor, then just print a message warning the user about it rather
+-- closing mini or quitting editor.
+closeWindowEmacs :: YiM ()
+closeWindowEmacs = do
+  wins <- withEditor $ use windowsA
+  let winCount = PL.length wins
+  tabCount <- withEditor $ uses tabsA PL.length
+
+  case () of
+   _ | winCount == 1 && tabCount == 1 ->
+         printMsg "Attempt to delete sole ordinary window"
+     | isMini (PL._focus wins) ->
+         printMsg "Attempt to delete the minibuffer"
+     | otherwise -> withEditor tryCloseE
+
+onYiVar :: (Yi -> YiVar -> IO (YiVar, a)) -> YiM a
+onYiVar f = do
+  yi <- ask
+  io $ modifyMVar (yiVar yi) (f yi)
+
+-- | Kill a given subprocess
+terminateSubprocesses :: (SubprocessInfo -> Bool) -> Yi -> YiVar -> IO (YiVar, ())
+terminateSubprocesses shouldTerminate _yi var = do
+  let (toKill, toKeep) =
+        partition (shouldTerminate . snd) $ M.assocs $ yiSubprocesses var
+  void $ forM toKill $ terminateProcess . procHandle . snd
+  return (var & yiSubprocessesA .~ M.fromList toKeep, ())
+
+-- | Start a subprocess with the given command and arguments.
+startSubprocess :: FilePath
+                -> [String]
+                -> (Either SomeException ExitCode -> YiM x)
+                -> YiM BufferRef
+startSubprocess cmd args onExit = onYiVar $ \yi var -> do
+        let (e', bufref) = runEditor
+                              (yiConfig yi)
+                              (printMsg ("Launched process: " <> T.pack cmd)
+                               >> newEmptyBufferE (MemBuffer bufferName))
+                              (yiEditor var)
+            procid = yiSubprocessIdSupply var + 1
+        procinfo <- createSubprocess cmd args bufref
+        startSubprocessWatchers procid procinfo yi onExit
+        return (var & yiEditorA .~ e'
+                    & yiSubprocessIdSupplyA .~ procid
+                    & yiSubprocessesA %~ M.insert procid procinfo
+               , bufref)
+  where
+    bufferName = T.unwords [ "output from", T.pack cmd, showT args ]
+
+startSubprocessWatchers :: SubprocessId
+                        -> SubprocessInfo
+                        -> Yi
+                        -> (Either SomeException ExitCode -> YiM x)
+                        -> IO ()
+startSubprocessWatchers procid procinfo yi onExit =
+    mapM_ (\(labelSuffix, run) -> do
+              threadId <- forkOS run
+              labelThread threadId (procCmd procinfo ++ labelSuffix))
+          ([("Err", pipeToBuffer (hErr procinfo) (send . append True)) | separateStdErr procinfo] ++
+           [("Out", pipeToBuffer (hOut procinfo) (send . append False)),
+            ("Exit", waitForExit (procHandle procinfo) >>= reportExit)])
+  where
+    send :: YiM () -> IO ()
+    send a = yiOutput yi MustRefresh [makeAction a]
+
+    -- TODO: This 'String' here is due to 'pipeToBuffer' but I don't
+    -- know how viable it would be to read from a process as Text.
+    -- Probably not worse than String but needs benchmarking.
+    append :: Bool -> String -> YiM ()
+    append atMark =
+      withEditor . appendToBuffer atMark (bufRef procinfo) . R.fromString
+
+    reportExit :: Either SomeException ExitCode -> IO ()
+    reportExit ec = send $ do
+      append True $ "Process exited with " <> show ec
+      removeSubprocess procid
+      void $ onExit ec
+
+removeSubprocess :: SubprocessId -> YiM ()
+removeSubprocess procid = asks yiVar >>= liftBase . flip modifyMVar_ (pure . (yiSubprocessesA %~ M.delete procid))
+
+-- | Appends a 'R.YiString' to the given buffer.
+--
+-- TODO: Figure out and document the Bool here. Probably to do with
+-- 'startSubprocessWatchers'.
+appendToBuffer :: Bool      -- Something to do with stdout/stderr?
+               -> BufferRef -- ^ Buffer to append to
+               -> R.YiString  -- ^ Text to append
+               -> EditorM ()
+appendToBuffer atErr bufref s = withGivenBuffer bufref $ do
+    -- We make sure stdout is always after stderr. This ensures that
+    -- the output of the two pipe do not get interleaved. More
+    -- importantly, GHCi prompt should always come after the error
+    -- messages.
+    me <- getMarkB (Just "StdERR")
+    mo <- getMarkB (Just "StdOUT")
+    let mms = if atErr then [mo, me] else [mo]
+    forM_ mms (`modifyMarkB` (markGravityAA .~ Forward))
+    insertNAt s =<< use (markPointA (if atErr then me else mo))
+    forM_ mms (`modifyMarkB` (markGravityAA .~ Backward))
+
+sendToProcess :: BufferRef -> String -> YiM ()
+sendToProcess bufref s = do
+    yi <- ask
+    find ((== bufref) . bufRef) . yiSubprocesses <$> liftBase (readMVar (yiVar yi)) >>= \case
+      Just subProcessInfo -> io $ hPutStr (hIn subProcessInfo) s
+      Nothing -> printMsg "Could not get subProcessInfo in sendToProcess"
+
+pipeToBuffer :: Handle -> (String -> IO ()) -> IO ()
+pipeToBuffer h append = void . ignoringException . forever $ do
+  _ <- hWaitForInput h (-1)
+  r <- readAvailable h
+  append r
+
+waitForExit :: ProcessHandle -> IO (Either SomeException ExitCode)
+waitForExit ph =
+    handle (\e -> return (Left (e :: SomeException))) $ do
+      mec <- getProcessExitCode ph
+      case mec of
+          Nothing -> threadDelay (500*1000) >> waitForExit ph
+          Just ec -> return (Right ec)
+
+withSyntax :: (Show x, YiAction a x) => (forall syntax. Mode syntax -> syntax -> a) -> YiM ()
+withSyntax f = do
+            b <- gets currentBuffer
+            act <- withGivenBuffer b $ withSyntaxB f
+            runAction $ makeAction act
+
+userForceRefresh :: YiM ()
+userForceRefresh = withUI UI.userForceRefresh
diff --git a/src/Yi/Debug.hs b/src/Yi/Debug.hs
new file mode 100644
--- /dev/null
+++ b/src/Yi/Debug.hs
@@ -0,0 +1,100 @@
+{-# LANGUAGE CPP               #-}
+{-# LANGUAGE FlexibleContexts  #-}
+{-# LANGUAGE LambdaCase        #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# OPTIONS_HADDOCK show-extensions #-}
+
+-- |
+-- Module      :  Yi.Debug
+-- License     :  GPL-2
+-- Maintainer  :  yi-devel@googlegroups.com
+-- Stability   :  experimental
+-- Portability :  portable
+--
+-- Debug utilities used throughout Yi.
+
+module Yi.Debug ( initDebug, trace, traceM, traceM_, logPutStrLn
+                , logError, logStream, Yi.Debug.error ) where
+
+import Control.Concurrent
+    ( dupChan, getChanContents, forkIO, myThreadId, Chan )
+import Control.Monad.Base ( liftBase, MonadBase )
+import Data.IORef ( readIORef, writeIORef, IORef, newIORef )
+import Data.Monoid ( (<>) )
+import qualified Data.Text as T ( pack, snoc, unpack, Text )
+import GHC.Conc ( labelThread )
+import System.IO
+    ( hFlush, hPutStrLn, IOMode(WriteMode), openFile, Handle )
+import System.IO.Unsafe ( unsafePerformIO )
+
+#if __GLASGOW_HASKELL__ < 710
+import Data.Time (formatTime, getCurrentTime)
+import System.Locale (defaultTimeLocale)
+#else
+import Data.Time (formatTime, getCurrentTime, defaultTimeLocale)
+#endif
+
+dbgHandle :: IORef (Maybe Handle)
+dbgHandle = unsafePerformIO $ newIORef Nothing
+{-# NOINLINE dbgHandle #-}
+
+-- | Set the file to which debugging output should be written. Though this
+-- is called /init/Debug.
+-- Debugging output is not created by default (i.e., if this function
+-- is never called.)
+-- The target file can not be changed, nor debugging disabled.
+initDebug :: FilePath -> IO ()
+initDebug f = do
+  hndl <- readIORef dbgHandle
+  case hndl of
+    Nothing -> do openFile f WriteMode >>= writeIORef dbgHandle . Just
+                  logPutStrLn "Logging initialized."
+    Just _ -> logPutStrLn "Attempt to re-initialize the logging system."
+
+
+-- | Outputs the given string before returning the second argument.
+trace :: T.Text -> a -> a
+trace s e = unsafePerformIO $ logPutStrLn s >> return e
+{-# NOINLINE trace #-}
+
+error :: T.Text -> a
+error s = unsafePerformIO $ logPutStrLn s >> Prelude.error (T.unpack s)
+
+logPutStrLn :: MonadBase IO m => T.Text -> m ()
+logPutStrLn s = liftBase $
+  readIORef dbgHandle >>= \case
+    Nothing -> return ()
+    Just h -> do
+      time <-  getCurrentTime
+      tId <- myThreadId
+      let m = show tId ++ " " ++ T.unpack s
+      hPutStrLn h $ formatTime defaultTimeLocale rfc822DateFormat' time ++ m
+      hFlush h
+  where
+    -- A bug in rfc822DateFormat makes us use our own format string
+    rfc822DateFormat' = "%a, %d %b %Y %H:%M:%S %Z"
+
+logError :: MonadBase IO m => T.Text -> m ()
+logError s = logPutStrLn $ "error: " <> s
+
+logStream :: Show a => T.Text -> Chan a -> IO ()
+logStream msg ch = do
+  logPutStrLn $ "Logging stream " <> msg
+  logThreadId <- forkIO $ logStreamThread msg ch
+  labelThread logThreadId "LogStream"
+
+logStreamThread :: Show a => T.Text -> Chan a -> IO ()
+logStreamThread msg ch = do
+  stream <- getChanContents =<< dupChan ch
+  mapM_ logPutStrLn [ msg `T.snoc` '(' <> T.pack (show i) `T.snoc` ')'
+                     <> T.pack (show event)
+                    | (event, i) <- zip stream [(0::Int)..]
+                    ]
+
+-- | Traces @x@ and returns @y@.
+traceM :: Monad m => T.Text -> a -> m a
+traceM x y = trace x $ return y
+
+-- | Like traceM, but returns ().
+traceM_ :: Monad m => T.Text -> m ()
+traceM_ x = traceM x ()
diff --git a/src/Yi/Dired.hs b/src/Yi/Dired.hs
new file mode 100644
--- /dev/null
+++ b/src/Yi/Dired.hs
@@ -0,0 +1,1118 @@
+{-# LANGUAGE CPP                #-}
+{-# LANGUAGE DeriveDataTypeable #-}
+{-# LANGUAGE DeriveGeneric      #-}
+{-# LANGUAGE LambdaCase         #-}
+{-# LANGUAGE OverloadedStrings  #-}
+{-# LANGUAGE TemplateHaskell    #-}
+{-# OPTIONS_HADDOCK show-extensions #-}
+
+{-|
+Module      :  Yi.Dired
+License     :  GPL-2
+Maintainer  :  yi-devel@googlegroups.com
+Stability   :  experimental
+Portability :  portable
+
+A simple
+<https://www.gnu.org/software/emacs/manual/html_node/emacs/Dired.html dired>
+implementation for Yi.
+
+= TODO
+
+* add more comments
+* Support symlinks
+* Mark operations
+
+    * search
+
+* Improve the colouring to show
+
+    * loaded buffers
+    * .hs files
+    * marked files
+
+* Fix old mod dates (> 6months) to show year
+* Fix the 'number of links' field to show actual values not just 1...
+* Automatic support for browsing .zip, .gz files etc...
+-}
+
+module Yi.Dired
+  ( dired
+  , diredDir
+  , diredDirBuffer
+  , editFile
+  ) where
+
+import           GHC.Generics             (Generic)
+
+import           Control.Applicative      ((<|>))
+import           Control.Category         ((>>>))
+import           Control.Exc              (orException, printingException)
+import           Lens.Micro.Platform      (makeLenses, use, (%~), (&), (.=), (.~), (^.))
+import           Control.Monad.Reader     (asks, foldM, unless, void, when)
+import qualified Data.Attoparsec.Text     as P
+import           Data.Binary              (Binary)
+import           Data.Char                (toLower)
+import           Data.Default             (Default, def)
+import           Data.Foldable            (find, foldl')
+import           Data.List                (any, elem, sum, transpose)
+import qualified Data.Map                 as M (Map, assocs, delete, empty,
+                                                findWithDefault, fromList,
+                                                insert, keys, lookup, map,
+                                                mapKeys, union, (!))
+import           Data.Maybe               (fromMaybe)
+import           Data.Monoid              ((<>))
+import qualified Data.Text                as T (Text, pack, unpack)
+import           Data.Time.Clock.POSIX    (posixSecondsToUTCTime)
+import           Data.Typeable            (Typeable)
+import           System.CanonicalizePath  (canonicalizePath)
+import           System.Directory         (copyFile, createDirectoryIfMissing,
+                                           doesDirectoryExist, doesFileExist,
+                                           getDirectoryContents, getPermissions,
+                                           removeDirectoryRecursive, writable)
+import           System.FilePath          (dropTrailingPathSeparator,
+                                           equalFilePath, isAbsolute,
+                                           takeDirectory, takeFileName, (</>))
+import           System.FriendlyPath      (userToCanonPath)
+import           System.PosixCompat.Files (FileStatus, fileExist, fileGroup,
+                                           fileMode, fileOwner, fileSize,
+                                           getSymbolicLinkStatus,
+                                           groupExecuteMode, groupReadMode,
+                                           groupWriteMode, isBlockDevice,
+                                           isCharacterDevice, isDirectory,
+                                           isNamedPipe, isRegularFile, isSocket,
+                                           isSymbolicLink, linkCount,
+                                           modificationTime, otherExecuteMode,
+                                           otherReadMode, otherWriteMode,
+                                           ownerExecuteMode, ownerReadMode,
+                                           ownerWriteMode, readSymbolicLink,
+                                           readSymbolicLink, removeLink, rename,
+                                           unionFileModes)
+import           System.PosixCompat.Types (FileMode, GroupID, UserID)
+import           System.PosixCompat.User  (GroupEntry, GroupEntry (..),
+                                           UserEntry (..), getAllGroupEntries,
+                                           getAllUserEntries,
+                                           getGroupEntryForID,
+                                           getUserEntryForID, groupID, userID)
+import           Text.Printf              (printf)
+import           Yi.Buffer
+import           Yi.Config                (modeTable)
+import           Yi.Core                  (errorEditor)
+import           Yi.Editor
+import           Yi.Keymap                (Keymap, YiM, topKeymapA)
+import           Yi.Keymap.Keys
+import           Yi.MiniBuffer            (noHint, spawnMinibufferE, withMinibuffer, withMinibufferFree)
+import           Yi.Misc                  (getFolder, promptFile)
+import           Yi.Monad                 (gets)
+import qualified Yi.Rope                  as R
+import           Yi.String                (showT)
+import           Yi.Style
+import           Yi.Types                 (YiVariable, yiConfig)
+import           Yi.Utils                 (io, makeLensesWithSuffix)
+
+
+#if __GLASGOW_HASKELL__ < 710
+import System.Locale (defaultTimeLocale)
+import Data.Time     (UTCTime, formatTime, getCurrentTime)
+#else
+import Data.Time     (UTCTime, formatTime, getCurrentTime, defaultTimeLocale)
+#endif
+
+-- Have no idea how to keep track of this state better, so here it is ...
+data DiredOpState = DiredOpState
+    { _diredOpSucCnt :: !Int -- ^ keep track of the num of successful operations
+    , _diredOpForAll :: Bool -- ^ if True, DOChoice will be bypassed
+    } deriving (Show, Eq, Typeable, Generic)
+
+instance Default DiredOpState where
+    def = DiredOpState { _diredOpSucCnt = 0, _diredOpForAll = False }
+
+instance Binary DiredOpState
+
+instance YiVariable DiredOpState
+
+makeLenses ''DiredOpState
+
+data DiredFileInfo = DiredFileInfo
+    { permString             :: R.YiString
+    , numLinks               :: Integer
+    , owner                  :: R.YiString
+    , grp                    :: R.YiString
+    , sizeInBytes            :: Integer
+    , modificationTimeString :: R.YiString
+    } deriving (Show, Eq, Typeable, Generic)
+
+data DiredEntry
+    = DiredFile DiredFileInfo
+    | DiredDir DiredFileInfo
+    | DiredSymLink DiredFileInfo R.YiString
+    | DiredSocket DiredFileInfo
+    | DiredBlockDevice DiredFileInfo
+    | DiredCharacterDevice DiredFileInfo
+    | DiredNamedPipe DiredFileInfo
+    | DiredNoInfo
+    deriving (Show, Eq, Typeable, Generic)
+
+-- | Alias serving as documentation of some arguments. We keep most
+-- paths as 'R.YiString' for the sole reason that we'll have to render
+-- them.
+type DiredFilePath = R.YiString
+
+-- | Handy alias for 'DiredEntry' map.
+type DiredEntries = M.Map DiredFilePath DiredEntry
+
+data DiredState = DiredState
+    { diredPath        :: FilePath -- ^ The full path to the directory being viewed
+     -- FIXME Choose better data structure for Marks...
+    , diredMarks      :: M.Map FilePath Char
+      -- ^ Map values are just leafnames, not full paths
+    , diredEntries    :: DiredEntries
+      -- ^ keys are just leafnames, not full paths
+    , diredFilePoints :: [(Point,Point,FilePath)]
+      -- ^ position in the buffer where filename is
+    , diredNameCol    :: Int
+      -- ^ position on line where filename is (all pointA are this col)
+    , diredCurrFile   :: FilePath
+      -- ^ keep the position of pointer (for refreshing dired buffer)
+    } deriving (Show, Eq, Typeable, Generic)
+
+makeLensesWithSuffix "A" ''DiredState
+
+instance Binary DiredState
+
+instance Default DiredState where
+    def = DiredState { diredPath       = mempty
+                     , diredMarks      = mempty
+                     , diredEntries    = mempty
+                     , diredFilePoints = mempty
+                     , diredNameCol    = 0
+                     , diredCurrFile   = mempty
+                     }
+
+instance YiVariable DiredState
+
+instance Binary DiredEntry
+instance Binary DiredFileInfo
+
+-- | If file exists, read contents of file into a new buffer, otherwise
+-- creating a new empty buffer. Replace the current window with a new
+-- window onto the new buffer.
+--
+-- If the file is already open, just switch to the corresponding buffer.
+--
+-- Need to clean up semantics for when buffers exist, and how to attach
+-- windows to buffers.
+--
+-- @Yi.File@ module re-exports this, you probably want to import that
+-- instead.
+--
+-- In case of a decoding failure, failure message is returned instead
+-- of the 'BufferRef'.
+editFile :: FilePath -> YiM (Either T.Text BufferRef)
+editFile filename = do
+    f <- io $ userToCanonPath filename
+
+    dupBufs <- filter (maybe False (equalFilePath f) . file) <$> gets bufferSet
+
+    dirExists  <- io $ doesDirectoryExist f
+    fileExists <- io $ doesFileExist f
+
+    b <- case dupBufs of
+      [] -> if dirExists
+            then Right <$> diredDirBuffer f
+            else do
+              nb <- if fileExists
+                    then fileToNewBuffer f
+                    else Right <$> newEmptyBuffer f
+              case nb of
+                Left m -> return $ Left m
+                Right buf -> Right <$> setupMode f buf
+
+      (h:_) -> return . Right $ bkey h
+
+    case b of
+     Left m -> return $ Left m
+     Right bf -> withEditor (switchToBufferE bf >> addJumpHereE) >> return b
+  where
+    fileToNewBuffer :: FilePath -> YiM (Either T.Text BufferRef)
+    fileToNewBuffer f = io getCurrentTime >>= \n -> io (R.readFile f) >>= \case
+      Left m -> return $ Left m
+      Right (contents, conv) -> do
+        permissions <- io $ getPermissions f
+
+        b <- stringToNewBuffer (FileBuffer f) contents
+        withGivenBuffer b $ do
+          encodingConverterNameA .= Just conv
+          markSavedB n
+          unless (writable permissions) (readOnlyA .= True)
+
+        return $ Right b
+
+    newEmptyBuffer :: FilePath -> YiM BufferRef
+    newEmptyBuffer f =
+      stringToNewBuffer (FileBuffer f) mempty
+
+    setupMode :: FilePath -> BufferRef -> YiM BufferRef
+    setupMode f b = do
+      tbl <- asks (modeTable . yiConfig)
+      content <- withGivenBuffer b elemsB
+
+      let header = R.take 1024 content
+          pc = "-*-" *> P.skipWhile (== ' ') *> P.takeWhile (/= ' ') <* P.skipWhile (== ' ') <* "-*-"
+                    <|> P.skip (const True) *> P.skipWhile (/= '-') *> pc
+          hmode = case P.parseOnly pc (R.toText header) of
+                    Left _ -> ""
+                    Right str -> str
+          Just mode = find (\(AnyMode m) -> modeName m == hmode) tbl <|>
+                      find (\(AnyMode m) -> modeApplies m f header) tbl <|>
+                      Just (AnyMode emptyMode)
+      case mode of
+          AnyMode newMode -> withGivenBuffer b $ setMode newMode
+
+      return b
+
+
+bypassReadOnly :: BufferM a -> BufferM a
+bypassReadOnly f = do ro <- use readOnlyA
+                      readOnlyA .= False
+                      res <- f
+                      readOnlyA .= ro
+                      return res
+
+filenameColOf :: BufferM () -> BufferM ()
+filenameColOf f = getBufferDyn >>= (.=) preferColA . Just . diredNameCol >> f
+
+resetDiredOpState :: YiM ()
+resetDiredOpState = withCurrentBuffer $ putBufferDyn (def :: DiredOpState)
+
+incDiredOpSucCnt :: YiM ()
+incDiredOpSucCnt =
+  withCurrentBuffer $ getBufferDyn >>= putBufferDyn . (diredOpSucCnt %~ succ)
+
+getDiredOpState :: YiM DiredOpState
+getDiredOpState = withCurrentBuffer getBufferDyn
+
+modDiredOpState :: (DiredOpState -> DiredOpState) -> YiM ()
+modDiredOpState f = withCurrentBuffer $ getBufferDyn >>= putBufferDyn . f
+
+-- | Execute the operations
+--
+-- Pass the list of remaining operations down, insert new ops at the
+-- head if needed
+procDiredOp :: Bool -> [DiredOp] -> YiM ()
+procDiredOp counting (DORemoveFile f:ops) = do
+  io $ printingException ("Remove file " <> f) (removeLink f)
+  when counting postproc
+  procDiredOp counting ops
+    where postproc = do incDiredOpSucCnt
+                        withCurrentBuffer $ diredUnmarkPath (takeFileName f)
+procDiredOp counting (DORemoveDir f:ops) = do
+  io $ printingException ("Remove directory " <> f) (removeDirectoryRecursive f)
+  -- document suggests removeDirectoryRecursive will follow
+  -- symlinks in f, but it seems not the case, at least on OS X.
+  when counting postproc
+  procDiredOp counting ops
+    where postproc = do
+            incDiredOpSucCnt
+            withCurrentBuffer $ diredUnmarkPath (takeFileName f)
+procDiredOp _counting (DORemoveBuffer _:_) = undefined -- TODO
+procDiredOp counting  (DOCopyFile o n:ops) = do
+  io $ printingException ("Copy file " <> o) (copyFile o n)
+  when counting postproc
+  procDiredOp counting ops
+    where postproc = do
+            incDiredOpSucCnt
+            withCurrentBuffer $ diredUnmarkPath (takeFileName o)
+            -- TODO: mark copied files with "C" if the target dir's
+            -- dired buffer exists
+procDiredOp counting (DOCopyDir o n:ops) = do
+  contents <- io $ printingException
+              (concat ["Copy directory ", o, " to ", n]) doCopy
+  subops <- io $ mapM builder $ filter (`notElem` [".", ".."]) contents
+  procDiredOp False subops
+  when counting postproc
+  procDiredOp counting ops
+    where postproc = do
+            incDiredOpSucCnt
+            withCurrentBuffer $ diredUnmarkPath (takeFileName o)
+          -- perform dir copy: create new dir and create other copy ops
+          doCopy :: IO [FilePath]
+          doCopy = do
+            exists <- doesDirectoryExist n
+            when exists $ removeDirectoryRecursive n
+            createDirectoryIfMissing True n
+            getDirectoryContents o
+          -- build actual copy operations
+          builder :: FilePath -> IO DiredOp
+          builder name = do
+            let npath = n </> name
+            let opath = o </> name
+            isDir <- doesDirectoryExist opath
+            return $ DOCkOverwrite npath $ getOp isDir opath npath
+                where getOp isDir = if isDir then DOCopyDir else DOCopyFile
+
+
+procDiredOp counting (DORename o n:ops) = do
+  io $ printingException (concat ["Rename ", o, " to ", n]) (rename o n)
+  when counting postproc
+  procDiredOp counting ops
+    where postproc = do
+            incDiredOpSucCnt
+            withCurrentBuffer $ diredUnmarkPath (takeFileName o)
+procDiredOp counting r@(DOConfirm prompt eops enops:ops) =
+  withMinibuffer (R.toText $ prompt <> " (yes/no)") noHint (act . T.unpack)
+  where act s = case map toLower s of
+                  "yes" -> procDiredOp counting (eops <> ops)
+                  "no"  -> procDiredOp counting (enops <> ops)
+                  _     -> procDiredOp counting r
+                             -- TODO: show an error msg
+procDiredOp counting (DOCheck check eops enops:ops) = do
+  res <- io check
+  procDiredOp counting (if res then eops <> ops else enops <> ops)
+procDiredOp counting (DOCkOverwrite fp op:ops) = do
+  exists <- io $ fileExist fp
+  procDiredOp counting (if exists then newOp:ops else op:ops)
+      where newOp = DOChoice ("Overwrite " <> R.fromString fp <> " ?") op
+procDiredOp counting (DOInput prompt opGen:ops) =
+  promptFile (R.toText prompt) (act . T.unpack)
+    where act s = procDiredOp counting $ opGen s <> ops
+procDiredOp counting (DONoOp:ops) = procDiredOp counting ops
+procDiredOp counting (DOFeedback f:ops) =
+  getDiredOpState >>= f >> procDiredOp counting ops
+procDiredOp counting r@(DOChoice prompt op:ops) = do
+  st <- getDiredOpState
+  if st ^. diredOpForAll
+    then proceedYes
+    else withEditor_ $ spawnMinibufferE msg (const askKeymap)
+    where msg = R.toText $ prompt <> " (y/n/!/q/h)"
+          askKeymap = choice [ char 'n' ?>>! noAction
+                             , char 'y' ?>>! yesAction
+                             , char '!' ?>>! allAction
+                             , char 'q' ?>>! quit
+                             , char 'h' ?>>! help
+                             ]
+          noAction = cleanUp >> proceedNo
+          yesAction = cleanUp >> proceedYes
+          allAction = do cleanUp
+                         modDiredOpState (diredOpForAll .~ True)
+                         proceedYes
+          quit = cleanUp >> printMsg "Quit"
+          help = do
+            printMsg
+              "y: yes, n: no, !: yes on all remaining items, q: quit, h: help"
+            cleanUp
+            procDiredOp counting r -- repeat
+          -- use cleanUp to get back the original buffer
+          cleanUp = withEditor closeBufferAndWindowE
+          proceedYes = procDiredOp counting (op:ops)
+          proceedNo = procDiredOp counting ops
+procDiredOp _ _ = return ()
+
+
+-- | Delete a list of file in the given directory
+--
+-- 1. Ask for confirmation, if yes, perform deletions, otherwise
+-- showNothing
+--
+-- 2. Confirmation is required for recursive deletion of non-empty
+-- directry, but only the top level one
+--
+-- 3. Show the number of successful deletions at the end of the excution
+--
+-- 4. TODO: ask confirmation for wether to remove the associated
+-- buffers when a file is removed
+askDelFiles :: FilePath -> [(FilePath, DiredEntry)] -> YiM ()
+askDelFiles dir fs =
+  case fs of
+    (_x:_) -> do
+            resetDiredOpState
+            -- TODO: show the file name list in new tmp window
+            opList <- io $ sequence ops
+            -- a deletion command is mapped to a list of deletions
+            -- wrapped up by DOConfirm
+            -- TODO: is `counting' necessary here?
+            let ops' = opList <> [DOFeedback showResult]
+            procDiredOp True [DOConfirm prompt ops' [DOFeedback showNothing]]
+    -- no files listed
+    []     -> procDiredOp True [DOFeedback showNothing]
+    where
+      prompt = R.concat [ "Delete "
+                        , R.fromString . show $ length fs
+                        , " file(s)?"
+                        ]
+      ops = map opGenerator fs
+      showResult st = do
+        diredRefresh
+        printMsg $ showT (st ^. diredOpSucCnt) <> " of "
+                    <> showT total <> " deletions done"
+      showNothing _ = printMsg "(No deletions requested)"
+      total = length fs
+      opGenerator :: (FilePath, DiredEntry) -> IO DiredOp
+      opGenerator (fn, de) = do
+                   exists <- fileExist path
+                   if exists then case de of
+                     (DiredDir _dfi) -> do
+                       isNull <- fmap nullDir $ getDirectoryContents path
+                       return $ if isNull then DOConfirm recDelPrompt
+                                               [DORemoveDir path] [DONoOp]
+                                else DORemoveDir path
+                     _               -> return (DORemoveFile path)
+                     else return DONoOp
+          where path = dir </> fn
+                recDelPrompt = "Recursive delete of " <> R.fromString fn <> "?"
+                -- Test the emptyness of a folder
+                nullDir :: [FilePath] -> Bool
+                nullDir = Data.List.any (not . flip Data.List.elem [".", ".."])
+
+diredDoDel :: YiM ()
+diredDoDel = do
+  dir <- currentDir
+  maybefile <- withCurrentBuffer fileFromPoint
+  case maybefile of
+    Just (fn, de) -> askDelFiles dir [(fn, de)]
+    Nothing       -> noFileAtThisLine
+
+diredDoMarkedDel :: YiM ()
+diredDoMarkedDel = do
+  dir <- currentDir
+  fs <- markedFiles (== 'D')
+  askDelFiles dir fs
+
+diredKeymap :: Keymap -> Keymap
+diredKeymap = important $ withArg mainMap
+  where
+    -- produces a copy of the map allowing for C-u
+    withArg :: (Maybe Int -> Keymap) -> Keymap
+    withArg k = choice [ ctrlCh 'u' ?>> k (Just 1) , k Nothing ]
+
+    mainMap :: Maybe Int -> Keymap
+    mainMap univArg = choice
+      [ char 'p'                   ?>>! filenameColOf lineUp
+      , oneOf [char 'n', char ' ']  >>! filenameColOf lineDown
+      , char 'd'                   ?>>! diredMarkDel
+      , char 'g'                   ?>>! diredRefresh
+      , char 'm'                   ?>>! diredMark
+      , char '^'                   ?>>! diredUpDir
+      , char '+'                   ?>>! diredCreateDir
+      , char 'q'                   ?>>!
+          ((deleteBuffer =<< gets currentBuffer) :: EditorM ())
+      , char 'x'                   ?>>! diredDoMarkedDel
+      , oneOf [ctrl $ char 'm', spec KEnter, char 'f', char 'e'] >>! diredLoad
+        -- Currently ‘o’ misbehaves, seems this naive method loses
+        -- track of buffers.
+      , char 'o'                   ?>>! withOtherWindow diredLoad
+      , char 'u'                   ?>>! diredUnmark Forward
+      , spec KBS                   ?>>! diredUnmark Backward
+      , char 'D'                   ?>>! diredDoDel
+      , char 'U'                   ?>>! diredUnmarkAll
+      , char 'R'                   ?>>! diredRename
+      , char 'C'                   ?>>! diredCopy
+      , char '*'                   ?>>  multiMarks univArg
+      ]
+
+    multiMarks :: Maybe Int -> Keymap
+    multiMarks univArg = choice
+      [ char '!' ?>>! diredUnmarkAll
+      , char '@' ?>>! diredMarkSymlinks univArg
+      , char '/' ?>>! diredMarkDirectories univArg
+      , char 't' ?>>! diredToggleAllMarks
+      ]
+
+dired :: YiM ()
+dired = do
+    printMsg "Dired..."
+    maybepath <- withCurrentBuffer $ gets file
+    dir <- io $ getFolder maybepath
+    void $ editFile dir
+
+diredDir :: FilePath -> YiM ()
+diredDir dir = void (diredDirBuffer dir)
+
+diredDirBuffer :: FilePath -> YiM BufferRef
+diredDirBuffer d = do
+    -- Emacs doesn't follow symlinks, probably Yi shouldn't do too
+    dir <- io $ canonicalizePath d
+    b <- stringToNewBuffer (FileBuffer dir) mempty
+    withEditor $ switchToBufferE b
+    withCurrentBuffer $ do
+      state <- getBufferDyn
+      putBufferDyn (state & diredPathA .~ dir)
+      directoryContentA .= True
+    diredRefresh
+    return b
+
+-- | Write the contents of the supplied directory into the current
+-- buffer in dired format
+diredRefresh :: YiM ()
+diredRefresh = do
+    dState <- withCurrentBuffer getBufferDyn
+    let dir = diredPath dState
+    -- Scan directory
+    di <- io $ diredScanDir dir
+    currFile <- if null (diredFilePoints dState)
+                then return ""
+                else do maybefile <- withCurrentBuffer fileFromPoint
+                        case maybefile of
+                          Just (fp, _) -> return fp
+                          Nothing      -> return ""
+    let ds = diredEntriesA .~ di $ diredCurrFileA .~ currFile $ dState
+    -- Compute results
+    let dlines = linesToDisplay ds
+        (strss, stys, strs) = unzip3 dlines
+        strss' = transpose $ map doPadding $ transpose strss
+        namecol = if null strss' then 0 else
+                  let l1details = init $ head strss'
+                  in Data.List.sum (map R.length l1details) + length l1details
+
+    -- Set buffer contents
+    withCurrentBuffer $ do -- Clear buffer
+      readOnlyA .= False
+      ---- modifications begin here
+      deleteRegionB =<< regionOfB Document
+      -- Write Header
+      insertN $ R.fromString dir <> ":\n"
+      p <- pointB
+      -- paint header
+      addOverlayB $ mkOverlay "dired" (mkRegion 0 (p-2)) headStyle ""
+      ptsList <- mapM insertDiredLine $ zip3 strss' stys strs
+      putBufferDyn $ diredFilePointsA .~ ptsList $ diredNameColA .~ namecol $ ds
+
+      -- Colours for Dired come from overlays not syntax highlighting
+      modifyMode $ modeKeymapA .~  topKeymapA %~ diredKeymap
+                   >>> modeNameA .~ "dired"
+      diredRefreshMark
+      ---- no modifications after this line
+      readOnlyA .= True
+      when (null currFile) $ moveTo (p-2)
+      case getRow currFile ptsList of
+        Just rpos -> filenameColOf $ moveTo rpos
+        Nothing   -> filenameColOf lineDown
+    where
+    getRow fp recList = lookup fp (map (\(a,_b,c)->(c,a)) recList)
+    headStyle = const (withFg grey)
+    doPadding :: [DRStrings] -> [R.YiString]
+    doPadding drs = map (pad ((maximum . map drlength) drs)) drs
+
+    pad _n (DRPerms s)  = s
+    pad n  (DRLinks s)  = R.replicate (max 0 (n - R.length s)) " " <> s
+    pad n  (DROwners s) = s <> R.replicate (max 0 (n - R.length s)) " " <> " "
+    pad n  (DRGroups s) = s <> R.replicate (max 0 (n - R.length s)) " "
+    pad n  (DRSizes s)  = R.replicate (max 0 (n - R.length s)) " " <> s
+    pad n  (DRDates s)  = R.replicate (max 0 (n - R.length s)) " " <> s
+    pad _n (DRFiles s)  = s       -- Don't right-justify the filename
+
+    drlength = R.length . undrs
+
+-- | Returns a tuple containing the textual region (the end of) which
+-- is used for 'click' detection and the FilePath of the file
+-- represented by that textual region
+insertDiredLine :: ([R.YiString], StyleName, R.YiString)
+                -> BufferM (Point, Point, FilePath)
+insertDiredLine (fields, sty, filenm) = bypassReadOnly $ do
+  insertN . R.unwords $ init fields
+  p1 <- pointB
+  insertN  $ ' ' `R.cons` last fields
+  p2 <- pointB
+  newlineB
+  addOverlayB (mkOverlay "dired" (mkRegion p1 p2) sty "")
+  return (p1, p2, R.toString filenm)
+
+data DRStrings = DRPerms {undrs :: R.YiString}
+               | DRLinks {undrs :: R.YiString}
+               | DROwners {undrs :: R.YiString}
+               | DRGroups {undrs :: R.YiString}
+               | DRSizes {undrs :: R.YiString}
+               | DRDates {undrs :: R.YiString}
+               | DRFiles {undrs :: R.YiString}
+
+-- | Return a List of (prefix,
+-- fullDisplayNameIncludingSourceAndDestOfLink, style, filename)
+linesToDisplay :: DiredState -> [([DRStrings], StyleName, R.YiString)]
+linesToDisplay dState = map (uncurry lineToDisplay) (M.assocs entries)
+  where
+    entries = diredEntries dState
+
+    lineToDisplay k (DiredFile v)      =
+      (l " -" v <> [DRFiles k], defaultStyle, k)
+    lineToDisplay k (DiredDir v)       =
+      (l " d" v <> [DRFiles k], const (withFg blue), k)
+    lineToDisplay k (DiredSymLink v s) =
+      (l " l" v <> [DRFiles $ k <> " -> " <> s], const (withFg cyan), k)
+    lineToDisplay k (DiredSocket v) =
+      (l " s" v <> [DRFiles k], const (withFg magenta), k)
+    lineToDisplay k (DiredCharacterDevice v) =
+      (l " c" v <> [DRFiles k], const (withFg yellow), k)
+    lineToDisplay k (DiredBlockDevice v) =
+      (l " b" v <> [DRFiles k], const (withFg yellow), k)
+    lineToDisplay k (DiredNamedPipe v) =
+      (l " p" v <> [DRFiles k], const (withFg brown), k)
+    lineToDisplay k DiredNoInfo        =
+      ([DRFiles $ k <> " : Not a file/dir/symlink"], defaultStyle, k)
+
+    l pre v = [DRPerms $ pre <> permString v,
+               DRLinks . R.fromString $ printf "%4d" (numLinks v),
+               DROwners $ owner v,
+               DRGroups $ grp v,
+               DRSizes . R.fromString $ printf "%8d" (sizeInBytes v),
+               DRDates $ modificationTimeString v]
+
+-- | Return dired entries for the contents of the supplied directory
+diredScanDir :: FilePath -> IO DiredEntries
+diredScanDir dir = do
+    files <- getDirectoryContents dir
+    foldM (lineForFile dir) M.empty files
+    where
+    lineForFile :: FilePath
+                -> DiredEntries
+                -> FilePath
+                -> IO DiredEntries
+    lineForFile d m f = do
+      let fp = d </> f
+      fileStatus <- getSymbolicLinkStatus fp
+      dfi <- lineForFilePath fp fileStatus
+      let islink = isSymbolicLink fileStatus
+      linkTarget <- if islink then readSymbolicLink fp else return mempty
+      let de
+            | isDirectory fileStatus = DiredDir dfi
+            | isRegularFile fileStatus = DiredFile dfi
+            | islink = DiredSymLink dfi (R.fromString linkTarget)
+            | isSocket fileStatus = DiredSocket dfi
+            | isCharacterDevice fileStatus = DiredCharacterDevice dfi
+            | isBlockDevice fileStatus = DiredBlockDevice dfi
+            | isNamedPipe fileStatus = DiredNamedPipe dfi
+            | otherwise = DiredNoInfo
+      return $ M.insert (R.fromString f) de m
+
+    lineForFilePath :: FilePath -> FileStatus -> IO DiredFileInfo
+    lineForFilePath fp fileStatus = do
+      let modTimeStr = R.fromString . shortCalendarTimeToString
+                       . posixSecondsToUTCTime . realToFrac
+                       $ modificationTime fileStatus
+      let uid = fileOwner fileStatus
+          gid = fileGroup fileStatus
+          fn = takeFileName fp
+      _filenm <- if isSymbolicLink fileStatus
+                 then return . ((fn <> " -> ") <>) =<< readSymbolicLink fp
+                 else return fn
+      ownerEntry <- orException (getUserEntryForID uid)
+                    (fmap (scanForUid uid) getAllUserEntries)
+      groupEntry <- orException (getGroupEntryForID gid)
+                    (fmap (scanForGid gid) getAllGroupEntries)
+      let fmodeStr = (modeString . fileMode) fileStatus
+          sz = toInteger $ fileSize fileStatus
+          ownerStr   = R.fromString $ userName ownerEntry
+          groupStr   = R.fromString $ groupName groupEntry
+          numOfLinks = toInteger $ linkCount fileStatus
+      return DiredFileInfo { permString = fmodeStr
+                           , numLinks = numOfLinks
+                           , owner = ownerStr
+                           , grp = groupStr
+                           , sizeInBytes = sz
+                           , modificationTimeString = modTimeStr}
+
+
+-- | Needed on Mac OS X 10.4
+scanForUid :: UserID -> [UserEntry] -> UserEntry
+scanForUid uid entries = fromMaybe missingEntry $
+                                   find ((uid ==) . userID) entries
+  where
+    missingEntry = UserEntry "?" mempty uid 0 mempty mempty mempty
+
+-- | Needed on Mac OS X 10.4
+scanForGid :: GroupID -> [GroupEntry] -> GroupEntry
+scanForGid gid entries = fromMaybe missingEntry $
+                                   find ((gid ==) . groupID) entries
+  where
+    missingEntry = GroupEntry "?" mempty gid mempty
+
+
+modeString :: FileMode -> R.YiString
+modeString fm = ""
+                <> strIfSet "r" ownerReadMode
+                <> strIfSet "w" ownerWriteMode
+                <> strIfSet "x" ownerExecuteMode
+                <> strIfSet "r" groupReadMode
+                <> strIfSet "w" groupWriteMode
+                <> strIfSet "x" groupExecuteMode
+                <> strIfSet "r" otherReadMode
+                <> strIfSet "w" otherWriteMode
+                <> strIfSet "x" otherExecuteMode
+    where
+    strIfSet s mode = if fm == (fm `unionFileModes` mode) then s else "-"
+
+
+shortCalendarTimeToString :: UTCTime -> String
+shortCalendarTimeToString = formatTime defaultTimeLocale "%b %d %H:%M"
+
+-- Default Filter: omit files ending in '~' or '#' and also '.' and '..'.
+-- TODO: customizable filters?
+--diredOmitFile :: String -> Bool
+--diredOmitFile = undefined
+
+diredMark :: BufferM ()
+diredMark = diredMarkWithChar '*' lineDown
+
+diredMarkDel :: BufferM ()
+diredMarkDel = diredMarkWithChar 'D' lineDown
+
+-- | Generic mark toggler.
+diredMarkKind :: Maybe Int
+                 -- ^ universal argument, usually indicating whether
+                 -- to mark or unmark. Here ‘Just …’ is taken as
+                 -- unmark.
+              -> (DiredFilePath -> DiredEntry -> Bool)
+                 -- ^ Picks which entries to consider
+              -> Char
+                 -- ^ Character used for marking. Pass garbage if
+                 -- unmarking.
+              -> BufferM ()
+diredMarkKind m p c = bypassReadOnly $ do
+  dState <- getBufferDyn
+  let es = M.assocs $ diredEntries dState
+      ms = M.fromList [ (R.toString fp, c) | (fp, e) <- es, p fp e ]
+  putBufferDyn (dState & diredMarksA %~ run ms)
+  diredRefreshMark
+  where
+    run :: M.Map FilePath Char -> M.Map FilePath Char -> M.Map FilePath Char
+    run ms cms = case m of
+      Nothing -> M.union ms cms
+      Just _ -> deleteKeys cms (M.keys ms)
+
+diredMarkSymlinks :: Maybe Int -> BufferM ()
+diredMarkSymlinks m = diredMarkKind m p '*'
+  where
+    p _ DiredSymLink {} = True
+    p _ _ = False
+
+diredMarkDirectories :: Maybe Int -> BufferM ()
+diredMarkDirectories m = diredMarkKind m p '*'
+  where
+    p "." DiredDir {} = False
+    p ".." DiredDir {} = False
+    p _ DiredDir {} = True
+    p _ _ = False
+
+diredToggleAllMarks :: BufferM ()
+diredToggleAllMarks = bypassReadOnly $ do
+  dState <- getBufferDyn
+  let es = diredEntries dState
+  putBufferDyn (dState & diredMarksA %~ tm es)
+  diredRefreshMark
+  where
+    -- Get all entries, filter out the ones that are marked already,
+    -- then mark everything that remains, in effect toggling the
+    -- marks.
+    tm :: DiredEntries -> M.Map FilePath Char -> M.Map FilePath Char
+    tm de ms = let unmarked = deleteKeys (M.mapKeys R.toString de) (M.keys ms)
+               in M.map (const '*') unmarked
+
+-- | Delete all the keys from the map.
+deleteKeys :: Ord k => M.Map k v -> [k] -> M.Map k v
+deleteKeys = foldl' (flip M.delete)
+
+diredMarkWithChar :: Char -> BufferM () -> BufferM ()
+diredMarkWithChar c mv = bypassReadOnly $
+  fileFromPoint >>= \case
+    Just (fn, _de) -> do
+      state <- getBufferDyn
+      putBufferDyn (state & diredMarksA %~ M.insert fn c)
+      filenameColOf mv
+      diredRefreshMark
+    Nothing -> filenameColOf mv
+
+diredRefreshMark :: BufferM ()
+diredRefreshMark = do
+  b <- pointB
+  dState <- getBufferDyn
+  let posDict = diredFilePoints dState
+      markMap = diredMarks dState
+      draw (pos, _, fn) = case M.lookup fn markMap of
+        Just mark -> do
+          moveTo pos >> moveToSol >> insertB mark >> deleteN 1
+          e <- pointB
+          addOverlayB $
+            mkOverlay "dired" (mkRegion (e - 1) e) (styleOfMark mark) ""
+        Nothing ->
+          -- for deleted marks
+          moveTo pos >> moveToSol >> insertN " " >> deleteN 1
+  mapM_ draw posDict
+  moveTo b
+    where
+      styleOfMark '*' = const (withFg green)
+      styleOfMark 'D' = const (withFg red)
+      styleOfMark  _  = defaultStyle
+
+-- | Removes mark from current file (if any) and moves in the
+-- specified direction.
+diredUnmark :: Direction -- ^ Direction to move in after unmarking
+            -> BufferM ()
+diredUnmark d = bypassReadOnly $ do
+  let lineDir = case d of { Forward -> lineDown; Backward -> lineUp; }
+  fileFromPoint >>= \case
+    Just (fn, _de) -> do
+      diredUnmarkPath fn
+      filenameColOf lineDir
+      diredRefreshMark
+    Nothing        -> filenameColOf lineDir
+
+
+diredUnmarkPath :: FilePath -> BufferM()
+diredUnmarkPath fn = getBufferDyn >>= putBufferDyn.(diredMarksA %~ M.delete fn)
+
+diredUnmarkAll :: BufferM ()
+diredUnmarkAll = bypassReadOnly $ do
+                   getBufferDyn >>= putBufferDyn.(diredMarksA .~ M.empty)
+                   filenameColOf $ return ()
+                   diredRefreshMark
+
+currentDir :: YiM FilePath
+currentDir = diredPath <$> withCurrentBuffer getBufferDyn
+
+-- | move selected files in a given directory to the target location given
+-- by user input
+--
+-- if multiple source
+-- then if target is not a existing dir
+--      then error
+--      else move source files into target dir
+-- else if target is dir
+--      then if target exist
+--           then move source file into target dir
+--           else if source is dir and parent of target exists
+--                then move source to target
+--                else error
+--      else if parent of target exist
+--           then move source to target
+--           else error
+askRenameFiles :: FilePath -> [(FilePath, DiredEntry)] -> YiM ()
+askRenameFiles dir fs = case fs of
+  [_x] -> do resetDiredOpState
+             procDiredOp True [DOInput prompt sOpIsDir]
+  _x:_ -> do resetDiredOpState
+             procDiredOp True [DOInput prompt mOpIsDirAndExists]
+  []   ->    procDiredOp True [DOFeedback showNothing]
+  where
+    mkErr t = return . DOFeedback . const $ errorEditor t
+    prompt = "Move " <> R.fromString (show total) <> " item(s) to:"
+    mOpIsDirAndExists t = [DOCheck (doesDirectoryExist t) posOps negOps]
+      where
+        posOps = map builder fs <> [DOFeedback showResult]
+        negOps = mkErr $ T.pack t <> " is not directory!"
+        builder (fn, _de) = let old = dir </> fn
+                                new = t </> fn
+                            in DOCkOverwrite new (DORename old new)
+    sOpIsDir t = [DOCheck (doesDirectoryExist t) posOps sOpDirRename]
+        where (fn, _) = head fs -- the only item
+              posOps = [DOCkOverwrite new (DORename old new),
+                        DOFeedback showResult]
+                  where new = t </> fn
+                        old = dir </> fn
+              sOpDirRename = [DOCheck ckParentDir posOps' negOps,
+                              DOFeedback showResult]
+                  where posOps' = [DOCkOverwrite new (DORename old new)]
+                        p = "Cannot move " <> T.pack old
+                            <> " to " <> T.pack new
+                        negOps = mkErr p
+                        new = t
+                        old = dir </> fn
+                        ps = dropTrailingPathSeparator t
+                        ckParentDir = doesDirectoryExist $ takeDirectory ps
+    showResult st = do
+      diredRefresh
+      printMsg $ showT (st ^. diredOpSucCnt) <> " of "
+                  <> showT total <> " item(s) moved."
+    showNothing _ = printMsg "Quit"
+    total = length fs
+
+-- | copy selected files in a given directory to the target location given
+-- by user input
+--
+-- askCopyFiles follow the same logic as askRenameFiles,
+-- except dir and file are done by different DiredOP
+askCopyFiles :: FilePath -> [(FilePath, DiredEntry)] -> YiM ()
+askCopyFiles dir fs =
+  case fs of
+    [_x] -> do resetDiredOpState
+               procDiredOp True [DOInput prompt sOpIsDir]
+    _x:_ -> do resetDiredOpState
+               procDiredOp True [DOInput prompt mOpIsDirAndExists]
+    []   ->    procDiredOp True [DOFeedback showNothing]
+  where
+    prompt = "Copy " <> R.fromString (show total) <> " item(s) to:"
+    mOpIsDirAndExists t = [DOCheck (doesDirectoryExist t) posOps negOps]
+      where
+        posOps = map builder fs <> [DOFeedback showResult]
+        negOps = [DOFeedback . const $
+                  errorEditor (T.pack t <> " is not directory!")]
+        builder (fn, de) = let old = dir </> fn
+                               new = t </> fn
+                           in DOCkOverwrite new (op4Type de old new)
+    sOpIsDir t = [DOCheck (doesDirectoryExist t) posOps sOpDirCopy]
+      where (fn, de) = head fs -- the only item
+            posOps = [DOCkOverwrite new (op4Type de old new),
+                      DOFeedback showResult]
+                where new = t </> fn
+                      old = dir </> fn
+            sOpDirCopy = [DOCheck ckParentDir posOps' negOps,
+                          DOFeedback showResult]
+                where posOps' = [DOCkOverwrite new (op4Type de old new)]
+                      p = "Cannot copy " <> T.pack old <> " to " <> T.pack new
+                      negOps =
+                          [DOFeedback . const $ errorEditor p]
+                      new = t
+                      old = dir </> fn
+                      ckParentDir = doesDirectoryExist $
+                                    takeDirectory (dropTrailingPathSeparator t)
+    showResult st = do
+      diredRefresh
+      printMsg $ showT (st ^. diredOpSucCnt) <> " of "
+                  <> showT total <> " item(s) copied."
+    showNothing _ = printMsg "Quit"
+    total = length fs
+    op4Type :: DiredEntry -> FilePath -> FilePath -> DiredOp
+    op4Type (DiredDir _) = DOCopyDir
+    op4Type _            = DOCopyFile
+
+diredRename :: YiM ()
+diredRename = do
+  dir <- currentDir
+  fs <- markedFiles (== '*')
+  if null fs then do maybefile <- withCurrentBuffer fileFromPoint
+                     case maybefile of
+                       Just (fn, de) -> askRenameFiles dir [(fn, de)]
+                       Nothing       -> noFileAtThisLine
+             else askRenameFiles dir fs
+
+diredCopy :: YiM ()
+diredCopy = do
+  dir <- currentDir
+  fs <- markedFiles (== '*')
+  if null fs then do maybefile <- withCurrentBuffer fileFromPoint
+                     case maybefile of
+                       Just (fn, de) -> askCopyFiles dir [(fn, de)]
+                       Nothing       -> noFileAtThisLine
+             else askCopyFiles dir fs
+
+diredLoad :: YiM ()
+diredLoad = do
+  dir <- currentDir
+  withCurrentBuffer fileFromPoint >>= \case
+    Just (fn, de) -> do
+      let sel = dir </> fn
+          sel' = T.pack sel
+      case de of
+        (DiredFile _dfi) -> do
+          exists <- io $ doesFileExist sel
+          if exists
+            then void $ editFile sel
+            else printMsg $ sel' <> " no longer exists"
+        (DiredDir _dfi)  -> do
+          exists <- io $ doesDirectoryExist sel
+          if exists
+            then diredDir sel
+            else printMsg $ sel' <> " no longer exists"
+        (DiredSymLink _dfi dest') -> do
+          let dest = R.toString dest'
+              target = if isAbsolute dest then dest else dir </> dest
+          existsFile <- io $ doesFileExist target
+          existsDir <- io $ doesDirectoryExist target
+          printMsg $ "Following link:" <> T.pack target
+          if existsFile then void $ editFile target else
+            if existsDir then diredDir target else
+              printMsg $ T.pack target <> " does not exist"
+        (DiredSocket _dfi) -> do
+          exists <- io $ doesFileExist sel
+          printMsg (if exists
+                    then "Can't open Socket " <> sel'
+                    else sel' <> " no longer exists")
+        (DiredBlockDevice _dfi) -> do
+          exists <- io $ doesFileExist sel
+          printMsg (if exists
+                    then "Can't open Block Device " <> sel'
+                    else sel' <> " no longer exists")
+        (DiredCharacterDevice _dfi) -> do
+          exists <- io $ doesFileExist sel
+          printMsg (if exists
+                    then "Can't open Character Device " <> sel'
+                    else sel' <> " no longer exists")
+        (DiredNamedPipe _dfi) -> do
+          exists <- io $ doesFileExist sel
+          printMsg (if exists
+                    then "Can't open Pipe " <> sel'
+                    else sel' <> " no longer exists")
+        DiredNoInfo -> printMsg $ "No File Info for:" <> sel'
+    Nothing        -> noFileAtThisLine
+
+
+noFileAtThisLine :: YiM ()
+noFileAtThisLine = printMsg "(No file at this line)"
+
+-- | Extract the filename at point. NB this may fail if the buffer has
+-- been edited. Maybe use Markers instead.
+fileFromPoint :: BufferM (Maybe (FilePath, DiredEntry))
+fileFromPoint = do
+    p <- pointB
+    dState <- getBufferDyn
+    let candidates = filter (\(_,p2,_)->p <= p2) (diredFilePoints dState)
+        finddef f = M.findWithDefault DiredNoInfo (R.fromString f)
+    return $ case candidates of
+      ((_, _, f):_) -> Just (f, finddef f $ diredEntries dState)
+      _             -> Nothing
+
+markedFiles :: (Char -> Bool) -> YiM [(FilePath, DiredEntry)]
+markedFiles cond = do
+  dState <- withCurrentBuffer getBufferDyn
+  let fs = fst . unzip $ filter (cond . snd) (M.assocs $ diredMarks dState)
+  return $ map (\f -> (f, diredEntries dState M.! R.fromString f)) fs
+
+diredUpDir :: YiM ()
+diredUpDir = do
+    dir <- currentDir
+    diredDir $ takeDirectory dir
+
+diredCreateDir :: YiM ()
+diredCreateDir =
+  withMinibufferFree "Create Dir:" $ \nm -> do
+    dir <- currentDir
+    let newdir = dir </> T.unpack nm
+    printMsg $ "Creating " <> T.pack newdir <> "..."
+    io $ createDirectoryIfMissing True newdir
+    diredRefresh
+
+
+-- | Elementary operations for dired file operations
+-- Map a dired mark operation (e.g. delete, rename, copy) command
+-- into a list of DiredOps, and use procDiredOp to excute them.
+-- Logic and implementation of each operation are packaged in procDiredOp
+-- See askDelFiles for example.
+-- If new elem op is added, just add corresponding procDiredOp to handle it.
+data DiredOp = DORemoveFile FilePath
+             | DORemoveDir FilePath
+             | DOCopyFile FilePath FilePath
+             | DOCopyDir FilePath FilePath
+             | DORename FilePath FilePath
+             | DORemoveBuffer FilePath
+             -- ^ remove the buffers that associate with the file
+             | DOConfirm R.YiString [DiredOp] [DiredOp]
+             -- ^ prompt a "yes/no" question. If yes, execute the
+             -- first list of embedded DiredOps otherwise execute the
+             -- second list of embedded DiredOps
+             | DOCheck (IO Bool) [DiredOp] [DiredOp]
+             -- ^ similar to DOConfirm, but no user interaction. Could
+             -- be used to check file existence
+             | DOCkOverwrite FilePath DiredOp
+             -- ^ this is a shortcut, it invokes DCChoice if file exists
+             | DOInput R.YiString (String -> [DiredOp])
+             -- ^ prompt a string and collect user input.
+             -- the embedded list of DiredOps is generated based on input,
+             -- Remember that the input should be checked with DOCheck
+             | DOChoice R.YiString DiredOp
+             -- ^ prompt a string, provide keybindings for 'y', 'n',
+             -- '!', 'q' and optional 'h' (help) this is useful when
+             -- overwriting of existing files is required to complete
+             -- the op choice '!' will bypass following DOChoice
+             -- prompts.
+             | DOFeedback (DiredOpState -> YiM ())
+             -- ^ to feedback, given the state. such as show the result.
+             | DONoOp
+             -- ^ no operation
diff --git a/src/Yi/Editor.hs b/src/Yi/Editor.hs
new file mode 100644
--- /dev/null
+++ b/src/Yi/Editor.hs
@@ -0,0 +1,818 @@
+{-# LANGUAGE LambdaCase        #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+{-# OPTIONS_HADDOCK show-extensions #-}
+
+-- |
+-- Module      :  Yi.Editor
+-- License     :  GPL-2
+-- Maintainer  :  yi-devel@googlegroups.com
+-- Stability   :  experimental
+-- Portability :  portable
+--
+-- The top level editor state, and operations on it. This is inside an
+-- internal module for easy re-export with Yi.Types bits.
+
+module Yi.Editor ( Editor(..), EditorM, MonadEditor(..)
+                 , runEditor
+                 , acceptedInputsOtherWindow
+                 , addJumpAtE
+                 , addJumpHereE
+                 , alternateBufferE
+                 , askConfigVariableA
+                 , bufferSet
+                 , buffersA
+                 , closeBufferAndWindowE
+                 , closeBufferE
+                 , closeOtherE
+                 , clrStatus
+                 , commonNamePrefix
+                 , currentBuffer
+                 , currentRegexA
+                 , currentWindowA
+                 , deleteBuffer
+                 , deleteTabE
+                 , emptyEditor
+                 , findBuffer
+                 , findBufferWith
+                 , findBufferWithName
+                 , findWindowWith
+                 , focusWindowE
+                 , getBufferStack
+                 , getBufferWithName
+                 , getBufferWithNameOrCurrent
+                 , getEditorDyn
+                 , getRegE
+                 , jumpBackE
+                 , jumpForwardE
+                 , killringA
+                 , layoutManagerNextVariantE
+                 , layoutManagerPreviousVariantE
+                 , layoutManagersNextE
+                 , layoutManagersPreviousE
+                 , layoutManagersPrintMsgE
+                 , maxStatusHeightA
+                 , moveTabE
+                 , moveWinNextE
+                 , moveWinPrevE
+                 , newBufferE
+                 , newEmptyBufferE
+                 , newTabE
+                 , newTempBufferE
+                 , newWindowE
+                 , nextTabE
+                 , nextWinE
+                 , onCloseActionsA
+                 , pendingEventsA
+                 , prevWinE
+                 , previousTabE
+                 , printMsg
+                 , printMsgs
+                 , printStatus
+                 , pushWinToFirstE
+                 , putEditorDyn
+                 , searchDirectionA
+                 , setDividerPosE
+                 , setRegE
+                 , setStatus
+                 , shiftOtherWindow
+                 , splitE
+                 , statusLine
+                 , statusLineInfo
+                 , statusLinesA
+                 , stringToNewBuffer
+                 , swapWinWithFirstE
+                 , switchToBufferE
+                 , switchToBufferWithNameE
+                 , tabsA
+                 , tryCloseE
+                 , windows
+                 , windowsA
+                 , windowsOnBufferE
+                 , withCurrentBuffer
+                 , withEveryBuffer
+                 , withGivenBuffer
+                 , withGivenBufferAndWindow
+                 , withOtherWindow
+                 , withWindowE
+                 ) where
+
+import           Prelude                        hiding (all, concatMap, foldl, foldr)
+
+import           Lens.Micro.Platform            (Lens', lens, mapped,
+                                                 use, view, (%=), (%~),
+                                                 (&), (.~), (^.), (.=),
+                                                 ASetter)
+import           Control.Monad                  (forM_, liftM)
+import           Control.Monad.Reader           (MonadReader (ask), asks,
+                                                 unless, when)
+import           Control.Monad.State            (gets, modify, MonadState)
+import           Data.Binary                    (Binary, get, put)
+import           Data.Default                   (Default, def)
+import qualified Data.DelayList                 as DelayList (insert)
+import           Data.DynamicState.Serializable (getDyn, putDyn)
+import           Data.Foldable                  (Foldable (foldl, foldr), all, concatMap, toList)
+import           Data.List                      (delete, (\\))
+import           Data.List.NonEmpty             (NonEmpty (..), fromList, nub)
+import qualified Data.List.NonEmpty             as NE (filter, head, length, toList, (<|))
+import qualified Data.List.PointedList          as PL (atEnd, moveTo)
+import qualified Data.List.PointedList.Circular as PL (PointedList (..), delete,
+                                                       deleteLeft, deleteOthers,
+                                                       deleteRight, focus,
+                                                       insertLeft, insertRight,
+                                                       length, next, previous,
+                                                       singleton, _focus)
+import qualified Data.Map                       as M (delete, elems, empty,
+                                                      insert, lookup, singleton, (!))
+import           Data.Maybe                     (fromJust, fromMaybe, isNothing)
+import qualified Data.Monoid                    as Mon ((<>))
+import           Data.Semigroup                 ((<>))
+import qualified Data.Text                      as T (Text, null, pack, unlines, unpack, unwords)
+import           System.FilePath                (splitPath)
+import           Yi.Buffer
+import           Yi.Config
+import           Yi.Interact                    as I (accepted, mkAutomaton)
+import           Yi.JumpList                    (Jump (..), JumpList, addJump, jumpBack, jumpForward)
+import           Yi.KillRing                    (krEmpty, krGet, krPut, krSet)
+import           Yi.Layout
+import           Yi.Monad                       (getsAndModify)
+import           Yi.Rope                        (YiString, empty, fromText)
+import qualified Yi.Rope                        as R (YiString, fromText, snoc)
+import           Yi.String                      (listify)
+import           Yi.Style                       (defaultStyle)
+import           Yi.Tab
+import           Yi.Types
+import           Yi.Utils
+import           Yi.Window
+
+assign :: MonadState s m => ASetter s s a b -> b -> m ()
+assign = (.=)
+
+uses l f = f <$> use l 
+
+instance Binary Editor where
+  put (Editor bss bs supply ts dv _sl msh kr regex _dir _ev _cwa ) =
+    let putNE (x :| xs) = put x >> put xs
+    in putNE bss >> put bs >> put supply >> put ts
+       >> put dv >> put msh >> put kr >> put regex
+  get = do
+    bss <- (:|) <$> get <*> get
+    bs <- get
+    supply <- get
+    ts <- get
+    dv <- get
+    msh <- get
+    kr <- get
+    regex <- get
+    return $ emptyEditor { bufferStack = bss
+                         , buffers = bs
+                         , refSupply = supply
+                         , tabs_ = ts
+                         , dynamic = dv
+                         , maxStatusHeight = msh
+                         , killring = kr
+                         , currentRegex = regex
+                         }
+
+-- | The initial state
+emptyEditor :: Editor
+emptyEditor = Editor
+  { buffers      = M.singleton (bkey buf) buf
+  , tabs_        = PL.singleton tab
+  , bufferStack  = bkey buf :| []
+  , refSupply    = 3
+  , currentRegex = Nothing
+  , searchDirection = Forward
+  , dynamic      = mempty
+  , statusLines  = DelayList.insert (maxBound, ([""], defaultStyle)) []
+  , killring     = krEmpty
+  , pendingEvents = []
+  , maxStatusHeight = 1
+  , onCloseActions = M.empty
+  }
+  where buf = newB 0 (MemBuffer "console") mempty
+        win = (dummyWindow (bkey buf)) { wkey = WindowRef 1 , isMini = False }
+        tab = makeTab1 2 win
+
+
+-- ---------------------------------------------------------------------
+
+makeLensesWithSuffix "A" ''Editor
+
+windows :: Editor -> PL.PointedList Window
+windows e = e ^. windowsA
+
+windowsA :: Lens' Editor (PL.PointedList Window)
+windowsA = currentTabA . tabWindowsA
+
+tabsA :: Lens' Editor (PL.PointedList Tab)
+tabsA = fixCurrentBufferA_ . tabs_A
+
+currentTabA :: Lens' Editor Tab
+currentTabA = tabsA . PL.focus
+
+askConfigVariableA :: (YiConfigVariable b, MonadEditor m) => m b
+askConfigVariableA = do cfg <- askCfg
+                        return $ cfg ^. configVariable
+
+-- ---------------------------------------------------------------------
+-- Buffer operations
+
+newRef :: MonadEditor m => m Int
+newRef = withEditor (refSupplyA %= (+ 1) >> use refSupplyA)
+
+newBufRef :: MonadEditor m => m BufferRef
+newBufRef = liftM BufferRef newRef
+
+-- | Create and fill a new buffer, using contents of string.
+-- | Does not focus the window, or make it the current window.
+-- | Call newWindowE or switchToBufferE to take care of that.
+stringToNewBuffer :: MonadEditor m
+                  => BufferId -- ^ The buffer indentifier
+                  -> YiString -- ^ The contents with which to populate
+                              -- the buffer
+                  -> m BufferRef
+stringToNewBuffer nm cs = withEditor $ do
+    u <- newBufRef
+    defRegStyle <- configRegionStyle <$> askCfg
+    insertBuffer $ newB u nm cs
+    m <- asks configFundamentalMode
+    withGivenBuffer u $ do
+        putRegionStyle defRegStyle
+        setAnyMode m
+    return u
+
+insertBuffer :: MonadEditor m => FBuffer -> m ()
+insertBuffer b = withEditor . modify $ \e ->
+    -- insert buffers at the end, so that
+    -- "background" buffers do not interfere.
+    e { bufferStack = nub (bufferStack e <> (bkey b :| []))
+      , buffers = M.insert (bkey b) b (buffers e)}
+
+
+-- Prevent possible space leaks in the editor structure
+forceFold1 :: Foldable t => t a -> t a
+forceFold1 x = foldr seq x x
+
+forceFoldTabs :: Foldable t => t Tab -> t Tab
+forceFoldTabs x = foldr (seq . forceTab) x x
+
+-- | Delete a buffer (and release resources associated with it).
+deleteBuffer :: MonadEditor m => BufferRef -> m ()
+deleteBuffer k = withEditor $ do
+  -- If the buffer has an associated close action execute that now.
+  -- Unless the buffer is the last buffer in the editor. In which case
+  -- it cannot be closed and, I think, the close action should not be
+  -- applied.
+  --
+  -- The close actions seem dangerous, but I know of no other simple
+  -- way to resolve issues related to what buffer receives actions
+  -- after the minibuffer closes.
+  gets bufferStack >>= \case
+    _ :| [] -> return ()
+    _ -> M.lookup k <$> gets onCloseActions
+         >>= \m_action -> fromMaybe (return ()) m_action
+  -- Now try deleting the buffer. Checking, once again, that it is not
+  -- the last buffer.
+  bs <- gets bufferStack
+  ws <- use windowsA
+  case bs of
+    b0 :| nextB : _ -> do
+      let pickOther w = if bufkey w == k then w {bufkey = other} else w
+          visibleBuffers = bufkey <$> toList ws
+
+          -- This ‘head’ always works because we witness that length of
+          -- bs ≥ 2 (through case) and ‘delete’ only deletes up to 1
+          -- element so we at worst we end up with something like
+          -- ‘head $ [] ++ [foo]’ when bs ≡ visibleBuffers
+          bs' = NE.toList bs
+          other = head $ (bs' \\ visibleBuffers) ++ delete k bs'
+      when (b0 == k) $
+        -- we delete the currently selected buffer: the next buffer
+        -- will become active in the main window, therefore it must be
+        -- assigned a new window.
+        switchToBufferE nextB
+
+      -- NOTE: This *only* works if not all bufferStack buffers are
+      -- equivalent to ‘k’. Assuring that there are no duplicates in
+      -- the bufferStack is equivalent in this case because of its
+      -- length.
+      modify $ \e ->
+        e & bufferStackA %~ fromList . forceFold1 . NE.filter (k /=)
+          & buffersA %~ M.delete k
+          & tabs_A %~ forceFoldTabs . fmap (mapWindows pickOther)
+          -- all windows open on that buffer must switch to another
+          -- buffer.
+
+      windowsA . mapped . bufAccessListA %= forceFold1 . filter (k /=)
+    _ -> return () -- Don't delete the last buffer.
+
+-- | Return the buffers we have, /in no particular order/
+bufferSet :: Editor -> [FBuffer]
+bufferSet = M.elems . buffers
+
+-- | Return a prefix that can be removed from all buffer paths while
+-- keeping them unique.
+commonNamePrefix :: Editor -> [FilePath]
+commonNamePrefix = commonPrefix . fmap (dropLast . splitPath)
+                   . fbufs . fmap (^. identA) . bufferSet
+  where dropLast [] = []
+        dropLast x = init x
+        fbufs xs = [ x | FileBuffer x <- xs ]
+          -- drop the last component, so that it is never hidden.
+
+getBufferStack :: MonadEditor m => m (NonEmpty FBuffer)
+getBufferStack = withEditor $ do
+  bufMap <- gets buffers
+  gets $ fmap (bufMap M.!) . bufferStack
+
+findBuffer :: MonadEditor m => BufferRef -> m (Maybe FBuffer)
+findBuffer k = withEditor (gets (M.lookup k . buffers))
+
+-- | Find buffer with this key
+findBufferWith :: BufferRef -> Editor -> FBuffer
+findBufferWith k e = case M.lookup k (buffers e) of
+  Just x -> x
+  Nothing -> error "Editor.findBufferWith: no buffer has this key"
+
+-- | Find buffers with this name
+findBufferWithName :: T.Text -> Editor -> [BufferRef]
+findBufferWithName n e =
+  let bufs = M.elems $ buffers e
+      sameIdent b = shortIdentString (length $ commonNamePrefix e) b == n
+  in map bkey $ filter sameIdent bufs
+
+-- | Find buffer with given name. Fail if not found.
+getBufferWithName :: MonadEditor m => T.Text -> m BufferRef
+getBufferWithName bufName = withEditor $ do
+  bs <- gets $ findBufferWithName bufName
+  case bs of
+    [] -> fail ("Buffer not found: " ++ T.unpack bufName)
+    b:_ -> return b
+
+------------------------------------------------------------------------
+-- | Perform action with any given buffer, using the last window that
+-- was used for that buffer.
+withGivenBuffer :: MonadEditor m => BufferRef -> BufferM a -> m a
+withGivenBuffer k f = do
+    b <- gets (findBufferWith k)
+
+    withGivenBufferAndWindow (b ^. lastActiveWindowA) k f
+
+-- | Perform action with any given buffer
+withGivenBufferAndWindow :: MonadEditor m
+    => Window -> BufferRef -> BufferM a -> m a
+withGivenBufferAndWindow w k f = withEditor $ do
+  accum <- asks configKillringAccumulate
+  let edit e = let b = findBufferWith k e
+                   (v, us, b') = runBufferFull w b f
+               in (e & buffersA .~ mapAdjust' (const b') k (buffers e)
+                     & killringA %~
+                        if accum && all updateIsDelete us
+                        then foldl (.) id $ reverse [ krPut dir s
+                                                    | Delete _ dir s <- us ]
+                        else id
+
+                  , (us, v))
+  (us, v) <- getsAndModify edit
+
+  updHandler <- return . bufferUpdateHandler =<< ask
+  unless (null us || null updHandler) $
+    forM_ updHandler (\h -> withGivenBufferAndWindow w k (h us))
+  return v
+
+-- | Perform action with current window's buffer
+withCurrentBuffer :: MonadEditor m => BufferM a -> m a
+withCurrentBuffer f = withEditor $ do
+  w <- use currentWindowA
+  withGivenBufferAndWindow w (bufkey w) f
+
+withEveryBuffer :: MonadEditor m => BufferM a -> m [a]
+withEveryBuffer action =
+    withEditor (gets bufferStack) >>= mapM (`withGivenBuffer` action) . NE.toList
+
+currentWindowA :: Lens' Editor Window
+currentWindowA = windowsA . PL.focus
+
+-- | Return the current buffer
+currentBuffer :: Editor -> BufferRef
+currentBuffer = NE.head . bufferStack
+
+-----------------------
+-- Handling of status
+
+-- | Prints a message with 'defaultStyle'.
+printMsg :: MonadEditor m => T.Text -> m ()
+printMsg s = printStatus ([s], defaultStyle)
+
+-- | Prints a all given messages with 'defaultStyle'.
+printMsgs :: MonadEditor m => [T.Text] -> m ()
+printMsgs s = printStatus (s, defaultStyle)
+
+printStatus :: MonadEditor m => Status -> m ()
+printStatus = setTmpStatus 1
+
+-- | Set the "background" status line
+setStatus :: MonadEditor m => Status -> m ()
+setStatus = setTmpStatus maxBound
+
+-- | Clear the status line
+clrStatus :: EditorM ()
+clrStatus = setStatus ([""], defaultStyle)
+
+statusLine :: Editor -> [T.Text]
+statusLine = fst . statusLineInfo
+
+statusLineInfo :: Editor -> Status
+statusLineInfo = snd . head . statusLines
+
+setTmpStatus :: MonadEditor m => Int -> Status -> m ()
+setTmpStatus delay s = withEditor $ do
+  statusLinesA %= DelayList.insert (delay, s)
+  -- also show in the messages buffer, so we don't loose any message
+  bs <- gets (filter ((== MemBuffer "messages") . view identA) . M.elems . buffers)
+
+  b <- case bs of
+         (b':_) -> return $ bkey b'
+         [] -> stringToNewBuffer (MemBuffer "messages") mempty
+  let m = listify $ R.fromText <$> fst s
+  withGivenBuffer b $ botB >> insertN (m `R.snoc` '\n')
+
+-- ---------------------------------------------------------------------
+-- kill-register (vim-style) interface to killring.
+--
+-- Note that our vim keymap currently has its own registers
+-- and doesn't use killring.
+
+-- | Put string into yank register
+setRegE :: R.YiString -> EditorM ()
+setRegE s = killringA %= krSet s
+
+-- | Return the contents of the yank register
+getRegE :: EditorM R.YiString
+getRegE = uses killringA krGet
+
+-- ---------------------------------------------------------------------
+-- | 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
+getEditorDyn :: (MonadEditor m, YiVariable a, Default a, Functor m) => m a
+getEditorDyn = fromMaybe def <$> getDyn (use dynamicA) (assign dynamicA)
+
+-- | Insert a value into the extensible state, keyed by its type
+putEditorDyn :: (MonadEditor m, YiVariable a, Functor m) => a -> m ()
+putEditorDyn = putDyn (use dynamicA) (assign dynamicA)
+
+-- | Like fnewE, create a new buffer filled with the String @s@,
+-- Switch the current window to this buffer. Doesn't associate any
+-- file with the buffer (unlike fnewE) and so is good for popup
+-- internal buffers (like scratch)
+newBufferE :: BufferId   -- ^ buffer name
+           -> YiString -- ^ buffer contents
+           -> EditorM BufferRef
+newBufferE f s = do
+    b <- stringToNewBuffer f s
+    switchToBufferE b
+    return b
+
+-- | Like 'newBufferE' but defaults to empty contents.
+newEmptyBufferE :: BufferId -> EditorM BufferRef
+newEmptyBufferE f = newBufferE f Yi.Rope.empty
+
+alternateBufferE :: Int -> EditorM ()
+alternateBufferE n = do
+    Window { bufAccessList = lst } <- use currentWindowA
+    if null lst || (length lst - 1) < n
+      then fail "no alternate buffer"
+      else switchToBufferE $ lst!!n
+
+-- | Create a new zero size window on a given buffer
+newZeroSizeWindow :: Bool -> BufferRef -> WindowRef -> Window
+newZeroSizeWindow mini bk ref = Window mini bk [] 0 0 emptyRegion ref 0 Nothing
+
+-- | Create a new window onto the given buffer.
+newWindowE :: Bool -> BufferRef -> EditorM Window
+newWindowE mini bk = newZeroSizeWindow mini bk . WindowRef <$> newRef
+
+-- | Attach the specified buffer to the current window
+switchToBufferE :: BufferRef -> EditorM ()
+switchToBufferE bk = windowsA . PL.focus %= \w ->
+  w & bufkeyA .~ bk
+    & bufAccessListA %~ forceFold1 . (bufkey w:) . filter (bk /=)
+
+-- | Switch to the buffer specified as parameter. If the buffer name
+-- is empty, switch to the next buffer.
+switchToBufferWithNameE :: T.Text -> EditorM ()
+switchToBufferWithNameE "" = alternateBufferE 0
+switchToBufferWithNameE bufName = switchToBufferE =<< getBufferWithName bufName
+
+-- | Close a buffer.
+-- Note: close the current buffer if the empty string is given
+closeBufferE :: T.Text -> EditorM ()
+closeBufferE nm = deleteBuffer =<< getBufferWithNameOrCurrent nm
+
+getBufferWithNameOrCurrent :: MonadEditor m => T.Text -> m BufferRef
+getBufferWithNameOrCurrent t = withEditor $
+    case T.null t of
+        True -> gets currentBuffer
+        False -> getBufferWithName t
+
+
+------------------------------------------------------------------------
+
+-- | Close current buffer and window, unless it's the last one.
+closeBufferAndWindowE :: EditorM ()
+closeBufferAndWindowE = do
+  -- Fetch the current buffer *before* closing the window.
+  -- The tryCloseE, since it uses tabsA, will have the
+  -- current buffer "fixed" to the buffer of the window that is
+  -- brought into focus. If the current buffer is accessed after the
+  -- tryCloseE then the current buffer may not be the same as the
+  -- buffer before tryCloseE. This would be bad.
+  b <- gets currentBuffer
+  tryCloseE
+  deleteBuffer b
+
+-- | Rotate focus to the next window
+nextWinE :: EditorM ()
+nextWinE = windowsA %= PL.next
+
+-- | Rotate focus to the previous window
+prevWinE :: EditorM ()
+prevWinE = windowsA %= PL.previous
+
+-- | Swaps the focused window with the first window. Useful for
+-- layouts such as 'HPairOneStack', for which the first window is the
+-- largest.
+swapWinWithFirstE :: EditorM ()
+swapWinWithFirstE = windowsA %= swapFocus (fromJust . PL.moveTo 0)
+
+-- | Moves the focused window to the first window, and moves all other
+-- windows down the stack.
+pushWinToFirstE :: EditorM ()
+pushWinToFirstE = windowsA %= pushToFirst
+  where
+      pushToFirst ws = case PL.delete ws of
+          Nothing -> ws
+          Just ws' -> PL.insertLeft (ws ^. PL.focus) (fromJust $ PL.moveTo 0 ws')
+
+-- | Swap focused window with the next one
+moveWinNextE :: EditorM ()
+moveWinNextE = windowsA %= swapFocus PL.next
+
+-- | Swap focused window with the previous one
+moveWinPrevE :: EditorM ()
+moveWinPrevE = windowsA %= swapFocus PL.previous
+
+-- | A "fake" accessor that fixes the current buffer after a change of
+-- the current window.
+--
+-- Enforces invariant that top of buffer stack is the buffer of the
+-- current window.
+fixCurrentBufferA_ :: Lens' Editor Editor
+fixCurrentBufferA_ = lens id (\_old new -> let
+  ws = windows new
+  b = findBufferWith (bufkey $ PL._focus ws) new
+  newBufferStack = nub (bkey b NE.<| bufferStack new)
+  -- make sure we do not hold to old versions by seqing the length.
+  in NE.length newBufferStack `seq` new & bufferStackA .~ newBufferStack)
+
+withWindowE :: Window -> BufferM a -> EditorM a
+withWindowE w = withGivenBufferAndWindow w (bufkey w)
+
+findWindowWith :: WindowRef -> Editor -> Window
+findWindowWith k e =
+    head $ concatMap (\win -> [win | wkey win == k]) $ windows e
+
+-- | Return the windows that are currently open on the buffer whose
+-- key is given
+windowsOnBufferE :: BufferRef -> EditorM [Window]
+windowsOnBufferE k = do
+  ts <- use tabsA
+  let tabBufEq = concatMap (\win -> [win | bufkey win == k]) . (^. tabWindowsA)
+  return $ concatMap tabBufEq ts
+
+-- | bring the editor focus the window with the given key.
+--
+-- Fails if no window with the given key is found.
+focusWindowE :: WindowRef -> EditorM ()
+focusWindowE k = do
+    -- Find the tab index and window index
+    ts <- use tabsA
+    let check (False, i) win = if wkey win == k
+                                    then (True, i)
+                                    else (False, i + 1)
+        check r@(True, _) _win = r
+
+        searchWindowSet (False, tabIndex, _) ws =
+            case foldl check (False, 0) (ws ^. tabWindowsA) of
+                (True, winIndex) -> (True, tabIndex, winIndex)
+                (False, _)       -> (False, tabIndex + 1, 0)
+        searchWindowSet r@(True, _, _) _ws = r
+
+    case foldl searchWindowSet  (False, 0, 0) ts of
+        (False, _, _) -> fail $ "No window with key " ++ show k ++ "found. (focusWindowE)"
+        (True, tabIndex, winIndex) -> do
+            assign tabsA (fromJust $ PL.moveTo tabIndex ts)
+            windowsA %= fromJust . PL.moveTo winIndex
+
+-- | Split the current window, opening a second window onto current buffer.
+-- TODO: unfold newWindowE here?
+splitE :: EditorM ()
+splitE = do
+  w <- gets currentBuffer >>= newWindowE False
+  windowsA %= PL.insertRight w
+
+-- | Prints the description of the current layout manager in the status bar
+layoutManagersPrintMsgE :: EditorM ()
+layoutManagersPrintMsgE = do
+  lm <- use $ currentTabA . tabLayoutManagerA
+  printMsg . T.pack $ describeLayout lm
+
+-- | Cycle to the next layout manager, or the first one if the current
+-- one is nonstandard.
+layoutManagersNextE :: EditorM ()
+layoutManagersNextE = withLMStackE PL.next >> layoutManagersPrintMsgE
+
+-- | Cycle to the previous layout manager, or the first one if the
+-- current one is nonstandard.
+layoutManagersPreviousE :: EditorM ()
+layoutManagersPreviousE = withLMStackE PL.previous >> layoutManagersPrintMsgE
+
+-- | Helper function for 'layoutManagersNext' and 'layoutManagersPrevious'
+withLMStackE :: (PL.PointedList AnyLayoutManager
+                -> PL.PointedList AnyLayoutManager)
+            -> EditorM ()
+withLMStackE f = askCfg >>= \cfg ->
+  currentTabA . tabLayoutManagerA %= go (layoutManagers cfg)
+  where
+   go [] lm = lm
+   go lms lm =
+     case findPL (layoutManagerSameType lm) lms of
+       Nothing -> head lms
+       Just lmsPL -> f lmsPL ^. PL.focus
+
+-- | Next variant of the current layout manager, as given by 'nextVariant'
+layoutManagerNextVariantE :: EditorM ()
+layoutManagerNextVariantE = do
+  currentTabA . tabLayoutManagerA %= nextVariant
+  layoutManagersPrintMsgE
+
+-- | Previous variant of the current layout manager, as given by
+-- 'previousVariant'
+layoutManagerPreviousVariantE :: EditorM ()
+layoutManagerPreviousVariantE = do
+  currentTabA . tabLayoutManagerA %= previousVariant
+  layoutManagersPrintMsgE
+
+-- | Sets the given divider position on the current tab
+setDividerPosE :: DividerRef -> DividerPosition -> EditorM ()
+setDividerPosE ref = assign (currentTabA . tabDividerPositionA ref)
+
+-- | Creates a new tab containing a window that views the current buffer.
+newTabE :: EditorM ()
+newTabE = do
+    bk <- gets currentBuffer
+    win <- newWindowE False bk
+    ref <- newRef
+    tabsA %= PL.insertRight (makeTab1 ref win)
+
+-- | Moves to the next tab in the round robin set of tabs
+nextTabE :: EditorM ()
+nextTabE = tabsA %= PL.next
+
+-- | Moves to the previous tab in the round robin set of tabs
+previousTabE :: EditorM ()
+previousTabE = tabsA %= PL.previous
+
+-- | Moves the focused tab to the given index, or to the end if the
+-- index is not specified.
+moveTabE :: Maybe Int -> EditorM ()
+moveTabE Nothing  = do
+    count <- uses tabsA PL.length
+    tabsA %= fromJust . PL.moveTo (pred count)
+moveTabE (Just n) = do
+    newTabs <- uses tabsA (PL.moveTo n)
+    when (isNothing newTabs) failure
+    assign tabsA $ fromJust newTabs
+  where failure = fail $ "moveTab " ++ show n ++ ": no such tab"
+
+-- | Deletes the current tab. If there is only one tab open then error out.
+--   When the last tab is focused, move focus to the left, otherwise
+--   move focus to the right.
+deleteTabE :: EditorM ()
+deleteTabE = tabsA %= fromMaybe failure . deleteTab
+  where failure = error "deleteTab: cannot delete sole tab"
+        deleteTab tabs = if PL.atEnd tabs
+                         then PL.deleteLeft tabs
+                         else PL.deleteRight tabs
+
+-- | Close the current window. If there is only one tab open and the tab
+-- contains only one window then do nothing.
+tryCloseE :: EditorM ()
+tryCloseE = do
+    ntabs <- uses tabsA PL.length
+    nwins <- uses windowsA PL.length
+    unless (ntabs == 1 && nwins == 1) $ if nwins == 1
+      -- Could the Maybe response from deleteLeft be used instead of the
+      -- def 'if'?
+      then tabsA %= fromJust . PL.deleteLeft
+      else windowsA %= fromJust . PL.deleteLeft
+
+-- | Make the current window the only window on the screen
+closeOtherE :: EditorM ()
+closeOtherE = windowsA %= PL.deleteOthers
+
+-- | Switch focus to some other window. If none is available, create one.
+shiftOtherWindow :: MonadEditor m => m ()
+shiftOtherWindow = withEditor $ do
+  len <- uses windowsA PL.length
+  if len == 1
+    then splitE
+    else nextWinE
+
+-- | Execute the argument in the context of an other window. Create
+-- one if necessary. The current window is re-focused after the
+-- argument has completed.
+withOtherWindow :: MonadEditor m => m a -> m a
+withOtherWindow f = do
+  shiftOtherWindow
+  x <- f
+  withEditor prevWinE
+  return x
+
+acceptedInputs :: EditorM [T.Text]
+acceptedInputs = do
+  km <- defaultKm <$> askCfg
+  keymap <- withCurrentBuffer $ gets (withMode0 modeKeymap)
+  let l = I.accepted 3 . I.mkAutomaton . extractTopKeymap . keymap $ km
+  return $ fmap T.unwords l
+
+-- | Shows the current key bindings in a new window
+acceptedInputsOtherWindow :: EditorM ()
+acceptedInputsOtherWindow = do
+  ai <- acceptedInputs
+  b <- stringToNewBuffer (MemBuffer "keybindings") (fromText $ T.unlines ai)
+  w <- newWindowE False b
+  windowsA %= PL.insertRight w
+
+addJumpHereE :: EditorM ()
+addJumpHereE = addJumpAtE =<< withCurrentBuffer pointB
+
+addJumpAtE :: Point -> EditorM ()
+addJumpAtE point = do
+  w <- use currentWindowA
+  shouldAddJump <- case jumpList w of
+    Just (PL.PointedList _ (Jump mark bf) _) -> do
+      bfStillAlive <- gets (M.lookup bf . buffers)
+      case bfStillAlive of
+        Nothing -> return False
+        _ -> do
+          p <- withGivenBuffer bf . use $ markPointA mark
+          return $! (p, bf) /= (point, bufkey w)
+    _ -> return True
+  when shouldAddJump $ do
+    m <- withCurrentBuffer setMarkHereB
+    let bf = bufkey w
+        j = Jump m bf
+    assign currentWindowA $ w & jumpListA %~ addJump j
+    return ()
+
+jumpBackE :: EditorM ()
+jumpBackE = addJumpHereE >> modifyJumpListE jumpBack
+
+jumpForwardE :: EditorM ()
+jumpForwardE = modifyJumpListE jumpForward
+
+modifyJumpListE :: (JumpList -> JumpList) -> EditorM ()
+modifyJumpListE f = do
+  w <- use currentWindowA
+  case f $ w ^. jumpListA of
+    Nothing -> return ()
+    Just (PL.PointedList _ (Jump mark bf) _) -> do
+      switchToBufferE bf
+      withCurrentBuffer $ use (markPointA mark) >>= moveTo
+      currentWindowA . jumpListA %= f
+
+-- | Creates an in-memory buffer with a unique name.
+newTempBufferE :: EditorM BufferRef
+newTempBufferE = do
+  e <- gets id
+  -- increment the index of the hint until no buffer is found with that name
+  let find_next currentName (nextName:otherNames) =
+          case findBufferWithName currentName e of
+            (_b : _) -> find_next nextName otherNames
+            []      -> currentName
+      find_next _ [] = error "Looks like nearly infinite list has just ended."
+      next_tmp_name = find_next name names
+      (name : names) = (fmap (("tmp-" Mon.<>) . T.pack . show) [0 :: Int ..])
+
+  newEmptyBufferE (MemBuffer next_tmp_name)
diff --git a/src/Yi/Eval.hs b/src/Yi/Eval.hs
new file mode 100644
--- /dev/null
+++ b/src/Yi/Eval.hs
@@ -0,0 +1,401 @@
+{-# LANGUAGE CPP                        #-}
+{-# LANGUAGE DeriveDataTypeable         #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE LambdaCase                 #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE TemplateHaskell            #-}
+#ifdef HINT
+{-# LANGUAGE FlexibleContexts #-}
+#endif
+{-# OPTIONS_HADDOCK show-extensions #-}
+
+-- |
+-- Module      :  Yi.Eval
+-- License     :  GPL-2
+-- Maintainer  :  yi-devel@googlegroups.com
+-- Stability   :  experimental
+-- Portability :  portable
+--
+-- Evaluator for actions ('Action', 'YiAction'). Uses a @GHCi@ session
+-- under the hood.
+
+module Yi.Eval (
+        -- * Main (generic) evaluation interface
+        execEditorAction,
+        getAllNamesInScope,
+        describeNamedAction,
+        Evaluator(..),
+        evaluator,
+        -- ** Standard evaluators
+#ifdef HINT
+        ghciEvaluator,
+#endif
+        publishedActionsEvaluator,
+        publishedActions,
+        publishAction,
+        -- * Eval/Interpretation
+        jumpToErrorE,
+        jumpToE,
+        consoleKeymap
+) where
+
+import Prelude hiding (mapM_)
+
+import Lens.Micro.Platform ( (^.), (.=), (%=) )
+import Control.Monad (when, void, forever)
+import Data.Array ( elems )
+import Data.Binary ( Binary )
+import Data.Default ( Default, def )
+import Data.Foldable ( mapM_ )
+import qualified Data.HashMap.Strict as M
+    ( HashMap, insert, lookup, empty, keys )
+import Data.Monoid ( (<>) )
+import Data.Typeable ( Typeable )
+#ifdef HINT
+import Control.Concurrent
+    ( takeMVar, putMVar, newEmptyMVar, MVar, forkIO )
+import Control.Monad.Base ( MonadBase )
+import Control.Monad.Catch ( try )
+import Control.Monad.Trans ( lift )
+import Data.Binary ( get, put )
+import Data.List ( sort )
+import qualified Language.Haskell.Interpreter as LHI
+    ( typeOf,
+      setImportsQ,
+      searchPath,
+      set,
+      runInterpreter,
+      ModuleElem(Data, Class, Fun),
+      getModuleExports,
+      as,
+      loadModules,
+      languageExtensions,
+      OptionVal((:=)),
+      InterpreterError,
+      Extension(OverloadedStrings),
+      setTopLevelModules,
+      interpret )
+import System.Directory ( doesFileExist )
+import Yi.Boot.Internal ( reload )
+import Yi.Core ( errorEditor )
+import Yi.Editor
+    ( getEditorDyn,
+      putEditorDyn,
+      MonadEditor)
+import qualified Yi.Paths ( getEvaluatorContextFilename )
+import Yi.String ( showT )
+import Yi.Utils ( io )
+#endif
+import Text.Read ( readMaybe )
+import Yi.Buffer
+    ( gotoLn,
+      moveXorEol,
+      BufferM,
+      readLnB,
+      pointB,
+      botB,
+      insertN,
+      getBookmarkB,
+      markPointA )
+import Yi.Config.Simple.Types ( customVariable, Field, ConfigM )
+import Yi.Core ( runAction )
+import Yi.Types ( YiVariable, YiConfigVariable )
+import Yi.Editor
+    ( printMsg,
+      askCfg,
+      withCurrentBuffer,
+      withCurrentBuffer )
+import Yi.File ( openingNewFile )
+import Yi.Hooks ( runHook )
+import Yi.Keymap
+    ( YiM, Action, YiAction, makeAction, Keymap, write )
+import Yi.Keymap.Keys ( event, Event(..), Key(KEnter) )
+import Yi.Regex ( Regex, makeRegex, matchOnceText )
+import qualified Yi.Rope as R
+    ( toString, YiString, splitAt, length )
+import Yi.Utils ( makeLensesWithSuffix )
+
+infixl 1 <&>
+(<&>) :: Functor f => f a -> (a -> b) -> f b
+a <&> f = f <$> a
+-- TODO: should we be sticking Text here?
+
+-- | Runs the action, as written by the user.
+--
+-- The behaviour of this function can be customised by modifying the
+-- 'Evaluator' variable.
+execEditorAction :: String -> YiM ()
+execEditorAction = runHook execEditorActionImpl
+
+-- | Lists the action names in scope, for use by 'execEditorAction',
+-- and 'help' index.
+--
+-- The behaviour of this function can be customised by modifying the
+-- 'Evaluator' variable.
+getAllNamesInScope :: YiM [String]
+getAllNamesInScope = runHook getAllNamesInScopeImpl
+
+-- | Describes the named action in scope, for use by 'help'.
+--
+-- The behaviour of this function can be customised by modifying the
+-- 'Evaluator' variable.
+describeNamedAction :: String -> YiM String
+describeNamedAction = runHook describeNamedActionImpl
+
+-- | Config variable for customising the behaviour of
+-- 'execEditorAction' and 'getAllNamesInScope'.
+--
+-- Set this variable using 'evaluator'. See 'ghciEvaluator' and
+-- 'finiteListEvaluator' for two implementation.
+data Evaluator = Evaluator
+  { execEditorActionImpl :: String -> YiM ()
+    -- ^ implementation of 'execEditorAction'
+  , getAllNamesInScopeImpl :: YiM [String]
+    -- ^ implementation of 'getAllNamesInScope'
+  , describeNamedActionImpl :: String -> YiM String
+    -- ^ describe named action (or at least its type.), simplest implementation is at least @return@.
+  } deriving (Typeable)
+
+-- | The evaluator to use for 'execEditorAction' and
+-- 'getAllNamesInScope'.
+evaluator :: Field Evaluator
+evaluator = customVariable
+
+-- * Evaluator based on GHCi
+-- | Cached variable for getAllNamesInScopeImpl
+newtype NamesCache = NamesCache [String] deriving (Typeable, Binary)
+
+instance Default NamesCache where
+    def = NamesCache []
+instance YiVariable NamesCache
+
+-- | Cached dictioary for describeNameImpl
+newtype HelpCache = HelpCache (M.HashMap String String) deriving (Typeable, Binary)
+
+instance Default HelpCache where
+    def = HelpCache M.empty
+instance YiVariable HelpCache
+
+#ifdef HINT
+type HintRequest = (String, MVar (Either LHI.InterpreterError Action))
+newtype HintThreadVar = HintThreadVar (Maybe (MVar HintRequest))
+  deriving (Typeable, Default)
+
+instance Binary HintThreadVar where
+  put _ = return ()
+  get = return def
+instance YiVariable HintThreadVar
+
+getHintThread :: (MonadEditor m, MonadBase IO m) => m (MVar HintRequest)
+getHintThread = do
+  HintThreadVar x <- getEditorDyn
+  case x of
+    Just t -> return t
+    Nothing -> do
+      req <- io newEmptyMVar
+      contextFile <- Yi.Paths.getEvaluatorContextFilename
+      void . io . forkIO $ hintEvaluatorThread req contextFile
+      putEditorDyn . HintThreadVar $ Just req
+      return req
+
+hintEvaluatorThread :: MVar HintRequest -> FilePath -> IO ()
+hintEvaluatorThread request contextFile = do
+  haveUserContext <- doesFileExist contextFile
+  void $ LHI.runInterpreter $ do
+    LHI.set [LHI.searchPath LHI.:= []]
+
+    LHI.set [LHI.languageExtensions LHI.:= [ LHI.OverloadedStrings ]]
+    when haveUserContext $ do
+      LHI.loadModules [contextFile]
+      LHI.setTopLevelModules ["Env"]
+
+    -- Yi.Keymap: Action lives there
+    LHI.setImportsQ [("Yi", Nothing), ("Yi.Keymap",Just "Yi.Keymap")]
+    forever $ do
+      (s,response) <- lift $ takeMVar request
+      res <- try $ LHI.interpret ("Yi.makeAction (" ++ s ++ ")") (LHI.as :: Action)
+      lift $ putMVar response res
+
+-- Evaluator implemented by calling GHCi. This evaluator can run
+-- arbitrary expressions in the class 'YiAction'.
+--
+-- The following two imports are always present:
+--
+-- > import Yi
+-- > import qualified Yi.Keymap as Yi.Keymap
+--
+-- Also, if the file
+--
+-- > $HOME/.config/yi/local/Env.hs
+--
+-- exists, it is imported unqualified.
+ghciEvaluator :: Evaluator
+ghciEvaluator = Evaluator { execEditorActionImpl = execAction
+                          , getAllNamesInScopeImpl = getNames
+                          , describeNamedActionImpl = describeName -- TODO: use haddock to add docs
+                          }
+  where
+    execAction :: String -> YiM ()
+    execAction "reload" = reload
+    execAction s = do
+      request <- getHintThread
+      res <- io $ do
+        response <- newEmptyMVar
+        putMVar request (s,response)
+        takeMVar response
+      case res of
+        Left err -> errorEditor (showT err)
+        Right action -> runAction action
+
+    getNames :: YiM [String]
+    getNames = do
+      NamesCache cache <- getEditorDyn
+      result <- if null cache
+                then do
+                  res <- io $ LHI.runInterpreter $ do
+                    LHI.set [LHI.searchPath LHI.:= []]
+                    LHI.getModuleExports "Yi"
+                  return $ case res of
+                    Left err ->[show err]
+                    Right exports -> flattenExports exports
+                else return $ sort cache
+      putEditorDyn $ NamesCache result
+      return result
+
+    flattenExports :: [LHI.ModuleElem] -> [String]
+    flattenExports = concatMap flattenExport
+
+    flattenExport :: LHI.ModuleElem -> [String]
+    flattenExport (LHI.Fun x) = [x]
+    flattenExport (LHI.Class _ xs) = xs
+    flattenExport (LHI.Data _ xs) = xs
+      
+    describeName :: String -> YiM String
+    describeName name = do
+      HelpCache cache <- getEditorDyn
+      description <- case name `M.lookup` cache of
+                       Nothing -> do
+                         result <- io $ LHI.runInterpreter $ do
+                           LHI.set [LHI.searchPath LHI.:= []]
+                           -- when haveUserContext $ do
+                           --   LHI.loadModules [contextFile]
+                           --   LHI.setTopLevelModules ["Env"]
+                           LHI.setImportsQ [("Yi", Nothing), ("Yi.Keymap",Just "Yi.Keymap")]
+                           LHI.typeOf name
+                         let newDescription = either show id result
+                         putEditorDyn $ HelpCache $ M.insert name newDescription cache
+                         return newDescription
+                       Just description -> return description
+      return $ name ++ " :: " ++ description
+
+#endif
+
+-- * 'PublishedActions' evaluator
+
+newtype PublishedActions = PublishedActions {
+    _publishedActions :: M.HashMap String Action
+  } deriving(Typeable, Monoid)
+
+instance Default PublishedActions where def = mempty
+
+makeLensesWithSuffix "A" ''PublishedActions
+instance YiConfigVariable PublishedActions
+
+-- | Accessor for the published actions. Consider using
+-- 'publishAction'.
+publishedActions :: Field (M.HashMap String Action)
+publishedActions = customVariable . _publishedActionsA
+
+-- | Publish the given action, by the given name. This will overwrite
+-- any existing actions by the same name.
+publishAction :: (YiAction a x, Show x) => String -> a -> ConfigM ()
+publishAction s a = publishedActions %= M.insert s (makeAction a)
+
+-- | Evaluator based on a fixed list of published actions. Has a few
+-- differences from 'ghciEvaluator':
+--
+-- * expressions can't be evaluated
+--
+-- * all suggested actions are actually valued
+--
+-- * (related to the above) doesn't contain junk actions from Prelude
+--
+-- * doesn't require GHCi backend, so uses less memory
+publishedActionsEvaluator :: Evaluator
+publishedActionsEvaluator = Evaluator
+  { getAllNamesInScopeImpl = askCfg <&> M.keys . (^. publishedActions)
+  , execEditorActionImpl = \s ->
+      askCfg <&> M.lookup s . (^. publishedActions) >>= mapM_ runAction
+  , describeNamedActionImpl = return -- TODO: try to show types using TemplateHaskell!
+  }
+
+-- * Miscellaneous interpreter
+
+-- | Jumps to specified position in a given file.
+jumpToE :: FilePath -- ^ Filename to make the jump in.
+        -> Int -- ^ Line to jump to.
+        -> Int -- ^ Column to jump to.
+        -> YiM ()
+jumpToE filename line column =
+  openingNewFile filename $ gotoLn line >> moveXorEol column
+
+-- | Regex parsing the error message format.
+errorRegex :: Regex
+errorRegex = makeRegex ("^(.+):([0-9]+):([0-9]+):.*$" :: String)
+
+-- | Parses an error message. Fails if it can't parse out the needed
+-- information, namely filename, line number and column number.
+parseErrorMessage :: R.YiString -> Maybe (String, Int, Int)
+parseErrorMessage ln = do
+  (_ ,result, _) <- matchOnceText errorRegex (R.toString ln)
+  case take 3 $ map fst $ elems result of
+    [_, fname, l, c] -> (,,) <$> return fname <*> readMaybe l <*> readMaybe c
+    _                        -> Nothing
+
+-- | Tries to parse an error message at current line using
+-- 'parseErrorMessage'.
+parseErrorMessageB :: BufferM (Maybe (String, Int, Int))
+parseErrorMessageB = parseErrorMessage <$> readLnB
+
+-- | Tries to jump to error at the current line. See
+-- 'parseErrorMessageB'.
+jumpToErrorE :: YiM ()
+jumpToErrorE = withCurrentBuffer parseErrorMessageB >>= \case
+  Nothing -> printMsg "Couldn't parse out an error message."
+  Just (f, l, c) -> jumpToE f l c
+
+prompt :: R.YiString
+prompt = "Yi> "
+
+-- | Tries to strip the 'prompt' from the front of the given 'String'.
+-- If the prompt is not found, returns the input command as-is.
+takeCommand :: R.YiString -> R.YiString
+takeCommand t = case R.splitAt (R.length prompt) t of
+  (f, s) -> if f == prompt then s else t
+
+consoleKeymap :: Keymap
+consoleKeymap = do
+  _ <- event (Event KEnter [])
+  write $ withCurrentBuffer readLnB >>= \x -> case parseErrorMessage x of
+    Just (f,l,c) -> jumpToE f l c
+    Nothing -> do
+      withCurrentBuffer $ do
+        p <- pointB
+        botB
+        p' <- pointB
+        when (p /= p') $ insertN ("\n" <> prompt <> takeCommand x)
+        insertN "\n"
+        pt <- pointB
+        insertN prompt
+        bm <- getBookmarkB "errorInsert"
+        markPointA bm .= pt
+      execEditorAction . R.toString $ takeCommand x
+
+instance Default Evaluator where
+#ifdef HINT
+    def = ghciEvaluator
+#else
+    def = publishedActionsEvaluator
+#endif
+
+instance YiConfigVariable Evaluator
diff --git a/src/Yi/Event.hs b/src/Yi/Event.hs
new file mode 100644
--- /dev/null
+++ b/src/Yi/Event.hs
@@ -0,0 +1,69 @@
+module Yi.Event
+    (
+     Event(..), prettyEvent,
+     Key(..), Modifier(..),
+
+     -- * Key codes
+     eventToChar
+    ) where
+
+import Data.Bits   (setBit)
+import Data.Char   (chr, ord)
+
+data Modifier = MShift | MCtrl | MMeta | MSuper | MHyper
+                deriving (Show,Eq,Ord)
+
+data Key = KEsc | KFun Int | KPrtScr | KPause | KASCII Char | KBS | KIns
+         | KHome | KPageUp | KDel | KEnd | KPageDown | KNP5 | KUp | KMenu
+         | KLeft | KDown | KRight | KEnter | KTab deriving (Eq,Show,Ord)
+
+data Event = Event Key [Modifier] deriving (Eq)
+
+instance Ord Event where
+    compare (Event k1 m1) (Event k2 m2) = compare m1 m2 `mappend` compare k1 k2
+    -- so, all Ctrl+char, meta+char, etc. all form a continuous range
+
+instance Show Event where
+    show = prettyEvent
+
+prettyEvent :: Event -> String
+prettyEvent (Event k mods) =
+           concatMap ((++ "-") . prettyModifier) mods ++ prettyKey k
+  where prettyKey (KFun i) = 'F' : show i
+        prettyKey (KASCII c) = [c]
+        prettyKey key = tail $ show key
+        prettyModifier m = [ show m !! 1]
+
+
+
+-- | Map an Event to a Char. This is used in the emacs keymap for Ctrl-Q and vim keymap 'insertSpecialChar'
+eventToChar :: Event -> Char
+eventToChar (Event KEnter _) = '\CR'
+eventToChar (Event KEsc _)   = '\ESC'
+eventToChar (Event KBS _)    = '\127'
+eventToChar (Event KTab _)   = '\t'
+
+eventToChar (Event (KASCII c) mods) = (if MMeta `elem` mods then setMeta else id) $
+                                      (if MCtrl `elem` mods then ctrlLowcase else id) c
+
+eventToChar _ev = '?'
+
+
+
+remapChar :: Char -> Char -> Char -> Char -> Char -> Char
+remapChar a1 b1 a2 _ c
+    | a1 <= c && c <= b1 = chr $ ord c - ord a1 + ord a2
+    | otherwise          = c
+
+ctrlLowcase :: Char -> Char
+ctrlLowcase   = remapChar 'a'   'z'   '\^A' '\^Z'
+
+-- set the meta bit, as if Mod1/Alt had been pressed
+setMeta :: Char -> Char
+setMeta c = chr (setBit (ord c) metaBit)
+
+metaBit :: Int
+metaBit = 7
+
+
+
diff --git a/src/Yi/File.hs b/src/Yi/File.hs
new file mode 100644
--- /dev/null
+++ b/src/Yi/File.hs
@@ -0,0 +1,199 @@
+{-# LANGUAGE DeriveDataTypeable #-}
+{-# LANGUAGE LambdaCase         #-}
+{-# LANGUAGE OverloadedStrings  #-}
+{-# LANGUAGE TemplateHaskell    #-}
+{-# OPTIONS_HADDOCK show-extensions #-}
+
+-- |
+-- Module      :  Yi.File
+-- License     :  GPL-2
+-- Maintainer  :  yi-devel@googlegroups.com
+-- Stability   :  experimental
+-- Portability :  portable
+
+module Yi.File (
+  -- * File-based actions
+  editFile,
+  openingNewFile,
+  openNewFile,
+
+  viWrite, viWriteTo, viSafeWriteTo,
+  fwriteE,
+  fwriteBufferE,
+  fwriteAllY,
+  fwriteToE,
+  backupE,
+  revertE,
+
+  -- * Helper functions
+  setFileName,
+  deservesSave,
+
+  -- * Configuration
+  preSaveHooks
+ ) where
+
+import           Lens.Micro.Platform    ((.=), makeLenses, use, view, (^.))
+import           Control.Monad          (filterM, void, when)
+import           Control.Monad.Base     (liftBase)
+import           Data.Default           (Default, def)
+import           Data.Monoid            ((<>))
+import qualified Data.Text              as T (Text, append, cons, pack, unpack)
+import           Data.Time              (getCurrentTime)
+import           Data.Typeable          (Typeable)
+import           System.Directory       (doesDirectoryExist, doesFileExist)
+import           System.FriendlyPath    (userToCanonPath)
+import           Yi.Buffer
+import           Yi.Config.Simple.Types (Field, customVariable)
+import           Yi.Core                (errorEditor, runAction)
+import           Yi.Dired               (editFile)
+import           Yi.Editor
+import           Yi.Keymap              ()
+import           Yi.Monad               (gets)
+import qualified Yi.Rope                as R (readFile, writeFile, writeFileUsingText)
+import           Yi.String              (showT)
+import           Yi.Types
+import           Yi.Utils               (io)
+
+newtype PreSaveHooks = PreSaveHooks { _unPreSaveHooks :: [Action] }
+    deriving Typeable
+
+instance Default PreSaveHooks where
+    def = PreSaveHooks []
+
+instance YiConfigVariable PreSaveHooks
+
+makeLenses ''PreSaveHooks
+
+preSaveHooks :: Field [Action]
+preSaveHooks = customVariable . unPreSaveHooks
+
+-- | Tries to open a new buffer with 'editFile' and runs the given
+-- action on the buffer handle if it succeeds.
+--
+-- If the 'editFile' fails, just the failure message is printed.
+openingNewFile :: FilePath -> BufferM a -> YiM ()
+openingNewFile fp act = editFile fp >>= \case
+  Left m -> printMsg m
+  Right ref -> void $ withGivenBuffer ref act
+
+-- | Same as @openingNewFile@ with no action to run after.
+openNewFile :: FilePath -> YiM ()
+openNewFile = flip openingNewFile $ return ()
+
+-- | Revert to the contents of the file on disk
+revertE :: YiM ()
+revertE =
+  withCurrentBuffer (gets file) >>= \case
+    Just fp -> do
+      now <- io getCurrentTime
+      rf <- liftBase $ R.readFile fp >>= \case
+        Left m -> print ("Can't revert: " <> m) >> return Nothing
+        Right (c, cv) -> return $ Just (c, Just cv)
+      case rf of
+       Nothing -> return ()
+       Just (s, conv) -> do
+         withCurrentBuffer $ revertB s conv now
+         printMsg ("Reverted from " <> showT fp)
+    Nothing -> printMsg "Can't revert, no file associated with buffer."
+
+
+-- | Try to write a file in the manner of vi/vim
+-- Need to catch any exception to avoid losing bindings
+viWrite :: YiM ()
+viWrite =
+  withCurrentBuffer (gets file) >>= \case
+    Nothing -> errorEditor "no file name associated with buffer"
+    Just f  -> do
+      bufInfo <- withCurrentBuffer bufInfoB
+      let s   = bufInfoFileName bufInfo
+      succeed <- fwriteE
+      let message = (showT f <>) (if f == s
+                        then " written"
+                        else " " <> showT s <> " written")
+      when succeed $ printMsg message
+
+-- | Try to write to a named file in the manner of vi/vim
+viWriteTo :: T.Text -> YiM ()
+viWriteTo f = do
+  bufInfo <- withCurrentBuffer bufInfoB
+  let s   = T.pack $ bufInfoFileName bufInfo
+  succeed <- fwriteToE f
+  let message = f `T.append` if f == s
+                             then " written"
+                             else ' ' `T.cons` s `T.append` " written"
+  when succeed $ printMsg message
+
+-- | Try to write to a named file if it doesn't exist. Error out if it does.
+viSafeWriteTo :: T.Text -> YiM ()
+viSafeWriteTo f = do
+  existsF <- liftBase $ doesFileExist (T.unpack f)
+  if existsF
+    then errorEditor $ f <> ": File exists (add '!' to override)"
+    else viWriteTo f
+
+-- | Write current buffer to disk, if this buffer is associated with a file
+fwriteE :: YiM Bool
+fwriteE = fwriteBufferE =<< gets currentBuffer
+
+-- | Write a given buffer to disk if it is associated with a file.
+fwriteBufferE :: BufferRef -> YiM Bool
+fwriteBufferE bufferKey = do
+  nameContents <- withGivenBuffer bufferKey $ do
+    fl <- gets file
+    st <- streamB Forward 0
+    conv <- use encodingConverterNameA
+    return (fl, st, conv)
+
+  case nameContents of
+    (Just f, contents, conv) -> io (doesDirectoryExist f) >>= \case
+      True -> printMsg "Can't save over a directory, doing nothing." >> return False
+      False -> do
+        hooks <- view preSaveHooks <$> askCfg
+        mapM_ runAction hooks
+        mayErr <- liftBase $ case conv of
+          Nothing -> R.writeFileUsingText f contents >> return Nothing
+          Just cn -> R.writeFile f contents cn
+        case mayErr of
+          Just err -> printMsg err >> return False
+          Nothing -> io getCurrentTime >>= withGivenBuffer bufferKey . markSavedB
+                     >> return True
+    (Nothing, _, _) -> printMsg "Buffer not associated with a file" >> return False
+
+-- | Write current buffer to disk as @f@. The file is also set to @f@.
+fwriteToE :: T.Text -> YiM Bool
+fwriteToE f = do
+  b <- gets currentBuffer
+  setFileName b (T.unpack f)
+  fwriteBufferE b
+
+-- | Write all open buffers
+fwriteAllY :: YiM Bool
+fwriteAllY = do
+    modifiedBuffers <- filterM deservesSave =<< gets bufferSet
+    and <$> mapM fwriteBufferE (fmap bkey modifiedBuffers)
+
+-- | Make a backup copy of file
+backupE :: FilePath -> YiM ()
+backupE = error "backupE not implemented"
+
+
+-- | Associate buffer with file; canonicalize the given path name.
+setFileName :: BufferRef -> FilePath -> YiM ()
+setFileName b filename = do
+  cfn <- liftBase $ userToCanonPath filename
+  withGivenBuffer b $ (.=) identA $ FileBuffer cfn
+
+-- | Checks if the given buffer deserves a save: whether it's a file
+-- buffer and whether it's pointing at a file rather than a directory.
+deservesSave :: FBuffer -> YiM Bool
+deservesSave b
+   | isUnchangedBuffer b = return False
+   | otherwise = isFileBuffer b
+
+-- | Is there a proper file associated with the buffer?
+-- In other words, does it make sense to offer to save it?
+isFileBuffer :: FBuffer -> YiM Bool
+isFileBuffer b = case b ^. identA of
+  MemBuffer _ -> return False
+  FileBuffer fn -> not <$> liftBase (doesDirectoryExist fn)
diff --git a/src/Yi/History.hs b/src/Yi/History.hs
new file mode 100644
--- /dev/null
+++ b/src/Yi/History.hs
@@ -0,0 +1,151 @@
+{-# LANGUAGE CPP                 #-}
+{-# LANGUAGE DeriveDataTypeable  #-}
+{-# LANGUAGE FlexibleInstances   #-}
+{-# LANGUAGE OverloadedStrings   #-}
+{-# LANGUAGE Rank2Types          #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# OPTIONS_HADDOCK show-extensions #-}
+
+-- |
+-- Module      :  Yi.History
+-- License     :  GPL-2
+-- Maintainer  :  yi-devel@googlegroups.com
+-- Stability   :  experimental
+-- Portability :  portable
+--
+-- ‘Command history’ implementation.
+
+module Yi.History where
+
+import           Control.Applicative (liftA3)
+import           Lens.Micro.Platform          (Lens', lens, set, (^.))
+import           Data.Binary         (Binary, get, put)
+import           Data.Default        (Default, def)
+import           Data.List           (nub)
+import qualified Data.Map            as M (Map, findWithDefault, insert, mapKeys)
+import           Data.Monoid         ((<>))
+import qualified Data.Text           as T (Text, isPrefixOf, null, pack, unpack)
+import qualified Data.Text.Encoding  as E (decodeUtf8, encodeUtf8)
+import           Data.Typeable       (Typeable)
+import           Yi.Buffer           (elemsB, replaceBufferContent)
+import           Yi.Editor
+import qualified Yi.Rope             as R (fromText, toText)
+import           Yi.Types            (YiVariable)
+
+newtype Histories = Histories (M.Map T.Text History)
+                  deriving (Show, Eq, Typeable)
+
+instance Binary Histories where
+  put (Histories m) = put $ M.mapKeys T.unpack m
+  get = Histories . M.mapKeys T.pack <$> get
+
+instance Default Histories where
+  def = Histories def
+
+data History = History { _historyCurrent  :: Int
+                       , _historyContents :: [T.Text]
+                       , _historyPrefix   :: T.Text
+                       } deriving (Show, Eq, Typeable)
+
+instance Default History where
+    def = History (-1) [] mempty
+
+instance Binary History where
+  put (History cu co pr) =
+    put cu >> put (map E.encodeUtf8 co) >> put (E.encodeUtf8 pr)
+  get = liftA3 History get (fmap E.decodeUtf8 <$> get) (E.decodeUtf8 <$> get)
+
+instance YiVariable Histories
+
+dynKeyA :: (Default v, Ord k) => k -> Lens' (M.Map k v) v
+dynKeyA key = lens (M.findWithDefault def key) (flip (M.insert key))
+
+miniBuffer :: T.Text
+miniBuffer = "minibuffer"
+
+historyUp :: EditorM ()
+historyUp = historyMove miniBuffer 1
+
+historyDown :: EditorM ()
+historyDown = historyMove miniBuffer (-1)
+
+historyStart :: EditorM ()
+historyStart = historyStartGen miniBuffer
+
+-- | Start an input session with History
+historyStartGen :: T.Text -> EditorM ()
+historyStartGen ident = do
+  Histories histories <- getEditorDyn
+  let (History _cur cont pref) = histories ^. dynKeyA ident
+  setHistory ident (History 0 (nub ("":cont)) pref) histories
+
+historyFinish :: EditorM ()
+historyFinish = historyFinishGen miniBuffer (R.toText <$> withCurrentBuffer elemsB)
+
+-- | Finish the current input session with history.
+historyFinishGen :: T.Text -> EditorM T.Text -> EditorM ()
+historyFinishGen ident getCurValue = do
+  Histories histories <- getEditorDyn
+  let History _cur cont pref = histories ^. dynKeyA ident
+  curValue <- getCurValue
+  let cont' = dropWhile (curValue ==) . dropWhile T.null $ cont
+  curValue `seq`        -- force the new value, otherwise we'll hold
+                        -- on to the buffer from which it's computed
+    cont'         `seq` -- force checking the top of the history,
+                        -- otherwise we'll build up thunks
+    setHistory ident (History (-1) (curValue:cont') pref) histories
+
+historyFind :: [T.Text] -> Int -> Int -> Int -> T.Text -> Int
+historyFind cont len cur delta pref =
+  case (next < 0, next >= len) of
+    (True,_) -> next
+    (_,True) -> next
+    (_,_) -> if pref `T.isPrefixOf` (cont !! next)
+      then next
+      else historyFind cont len cur deltaLarger pref
+  where
+    next = cur + delta
+    deltaLarger = delta + signum delta
+
+historyMove :: T.Text -> Int -> EditorM ()
+historyMove ident delta = do
+  s <- historyMoveGen ident delta (R.toText <$> withCurrentBuffer elemsB)
+  withCurrentBuffer . replaceBufferContent . R.fromText $ s
+
+historyMoveGen :: T.Text -> Int -> EditorM T.Text -> EditorM T.Text
+historyMoveGen ident delta getCurValue = do
+  Histories histories <- getEditorDyn
+  let History cur cont pref = histories ^. dynKeyA ident
+
+  curValue <- getCurValue
+  let len = length cont
+      next = historyFind cont len cur delta pref
+      nextValue = cont !! next
+  case (next < 0, next >= len) of
+    (True, _) -> do
+      printMsg $ "end of " <> ident <> " history, no next item."
+      return curValue
+    (_, True) -> do
+      printMsg $ "beginning of " <> ident <> " history, no previous item."
+      return curValue
+    (_,_) -> do
+      let contents = take cur cont ++ [curValue] ++ drop (cur + 1) cont
+      setHistory ident (History next contents pref) histories
+      return nextValue
+
+historyPrefixSet :: T.Text -> EditorM ()
+historyPrefixSet = historyPrefixSet' miniBuffer
+
+historyPrefixSet' :: T.Text -> T.Text -> EditorM ()
+historyPrefixSet' ident pref = do
+  Histories histories <- getEditorDyn
+  let History cur cont _pref = histories ^. dynKeyA ident
+  setHistory ident (History cur cont pref) histories
+
+-- | Helper that sets the given history at ident and 'putEditorDyn's
+-- the result.
+setHistory :: (MonadEditor m, Functor m) => T.Text -- ^ identifier
+           -> History -- ^ History to set
+           -> M.Map T.Text History -- ^ Map of existing histories
+           -> m ()
+setHistory i h = putEditorDyn . Histories . set (dynKeyA i) h
diff --git a/src/Yi/Hoogle.hs b/src/Yi/Hoogle.hs
new file mode 100644
--- /dev/null
+++ b/src/Yi/Hoogle.hs
@@ -0,0 +1,98 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# OPTIONS_HADDOCK show-extensions #-}
+
+-- |
+-- Module      :  Yi.Hoogle
+-- License     :  GPL-2
+-- Maintainer  :  yi-devel@googlegroups.com
+-- Stability   :  experimental
+-- Portability :  portable
+--
+-- Provides functions for calling Hoogle on the commandline, and
+-- processing results into a form useful for completion or insertion.
+
+module Yi.Hoogle where
+
+import           Control.Arrow       ((&&&))
+import           Data.Char           (isUpper)
+import           Data.List           (nub)
+import qualified Data.Text           as T (isInfixOf, lines, unpack)
+import           System.Exit         (ExitCode (ExitFailure))
+import           Yi.Buffer           (readRegionB, regionOfB, replaceRegionB, unitWord)
+import           Yi.Editor           (printMsgs, withCurrentBuffer)
+import           Yi.Keymap           (YiM)
+import           Yi.Process          (runProgCommand)
+import qualified Yi.Rope             as R (YiString, fromText, head, null, toString, toText, words)
+import           Yi.String           (showT)
+import           Yi.Utils            (io)
+
+-- | Remove anything starting with uppercase letter. These denote
+-- either module names or types.
+caseSensitize :: [R.YiString] -> [R.YiString]
+caseSensitize = filter p
+  where
+    p :: R.YiString -> Bool
+    p t = case R.head t of
+      Nothing -> False
+      Just c  -> not $ isUpper c
+
+-- | Hoogle's output includes a sort of type keyword, telling whether
+-- a hit is a package name, syntax, a module name, etc. But we care
+-- primarily about the function names, so we filter out anything
+-- containing the keywords.
+gv :: [R.YiString] -> [R.YiString]
+gv = filter f
+  where
+    ks = ["module ", " type ", "package ", " data ", " keyword "]
+    f x = not $ any (`T.isInfixOf` R.toText x) ks
+
+-- | Query Hoogle, with given search and options. This errors out on no
+-- results or if the hoogle command is not on path.
+hoogleRaw :: R.YiString -> R.YiString -> IO [R.YiString]
+hoogleRaw srch opts = do
+  let options = filter (not . R.null) [opts, srch]
+  outp@(_status, out, _err) <- runProgCommand "hoogle" (R.toString <$> options)
+  case outp of
+    (ExitFailure 1, "", "") -> -- no output, probably failed to run binary
+      fail "Error running hoogle command.  Is hoogle on path?"
+    (ExitFailure 1, xs, _) -> fail $ "hoogle failed with: " ++ T.unpack xs
+    _ -> return ()
+  -- TODO: bench ‘R.fromText . T.lines’ vs ‘R.lines . R.fromText’
+  let results = fmap R.fromText . T.lines $ out
+  if results == ["No results found"]
+    then fail "No Hoogle results"
+    else return results
+
+-- | Filter the output of 'hoogleRaw' to leave just functions.
+hoogleFunctions :: R.YiString -> IO [R.YiString]
+hoogleFunctions a =
+  caseSensitize . gv . nub . map ((!!1) . R.words) <$> hoogleRaw a ""
+
+-- | Return module-function pairs.
+hoogleFunModule :: R.YiString -> IO [(R.YiString, R.YiString)]
+hoogleFunModule a = map ((head &&& (!! 1)) . R.words) . gv <$> hoogleRaw a ""
+
+-- | Call out to 'hoogleFunModule', and overwrite the word at point with
+-- the first returned function.
+hoogle :: YiM R.YiString
+hoogle = do
+    (wordRegion,word) <- withCurrentBuffer $ do
+      wordRegion <- regionOfB unitWord
+      word <- readRegionB wordRegion
+      return (wordRegion, word)
+    ((modl,fun):_) <- io $ hoogleFunModule word
+
+    withCurrentBuffer $ replaceRegionB wordRegion fun
+    return modl
+
+-- | Call out to 'hoogleRaw', and print inside the Minibuffer the results of
+-- searching Hoogle with the word at point.
+hoogleSearch :: YiM ()
+hoogleSearch = do
+  word <- withCurrentBuffer $ do
+    wordRegion <- regionOfB unitWord
+    readRegionB wordRegion
+  results <- io $ hoogleRaw word ""
+
+  -- The quotes help legibility between closely-packed results
+  printMsgs $ map showT results
diff --git a/src/Yi/Hooks.hs b/src/Yi/Hooks.hs
new file mode 100644
--- /dev/null
+++ b/src/Yi/Hooks.hs
@@ -0,0 +1,102 @@
+{- | This module provides assistance in implementing \"hooks\" in Yi.
+This module provides no major new functionality -- only assistance in
+using 'YiConfigVariable's more easily to implement hooks.
+
+We consider a simple example. Suppose we have a function
+
+> promptForFile :: Maybe FilePath -> YiM FilePath
+
+which prompts the user to select a file from their file system,
+starting with the provided directory (if actually provided). Since
+this is a frequent task in Yi, it is important for it to be as
+user-friendly as possible. If opinions vary on the meaning of
+\"user-friendly\", then we would really like to provide multiple
+implementations of @promptForFile@, and allow users to select which
+implementation to use in their config files.
+
+A way to achieve this is using hooks, as follows:
+
+> -- create a new type
+> newtype FilePrompter = FilePrompter
+>   { runFilePrompter :: Maybe FilePath -> YiM FilePath }
+>   deriving (Typeable)
+> $(nameDeriveAccessors ''FilePrompter (n -> Just (n ++ "A")))
+>
+> -- give some implementations
+> filePrompter1, filePrompter2, filePrompter3 :: FilePrompter
+> ...
+>
+> -- declare FilePrompter as a YiConfigVariable (so it can go in the Config)
+> instance YiConfigVariable FilePrompter
+>
+> -- specify the default FilePrompter
+> instance Default FilePrompter where
+>    def = filePrompter1
+>
+> -- replace the old promptForFile function with a shim
+> promptForFile :: Maybe FilePath -> YiM FilePath
+> promptForFile = runHook runFilePrompter
+>
+> -- provide a custom-named Field for Yi.Config.Simple (not
+> -- strictly necessary, but user-friendly)
+> filePrompter :: Field FilePrompter
+> filePrompter = customVariable
+
+The user can write
+
+> ...
+>    filePrompter %= filePrompter2
+> ...
+
+in their config file, and calls to @promptForFile@ will now use the
+different prompter. Library code which called @promptForFile@ does not
+need to be changed, but it gets the new @filePrompter2@ behaviour
+automatically.
+
+See "Yi.Eval" for a real example of hooks.
+-}
+
+module Yi.Hooks(
+  -- * Convenience function 'runHook'
+  runHook,
+  HookType,
+  -- * Re-exports from "Yi.Config.Simple"
+  customVariable,
+  Field,
+ )
+  where
+
+import Lens.Micro.Platform             ((^.))
+import Yi.Config              (configVariable)
+import Yi.Config.Simple.Types (Field, customVariable)
+import Yi.Editor              (EditorM, askCfg)
+import Yi.Keymap              (YiM)
+import Yi.Types               (YiConfigVariable)
+
+-- | Looks up the configured value for the hook, and runs it. The
+-- argument to 'runHook' will typically be a record accessor. See
+-- 'HookType' for the valid hook types.
+runHook :: (HookType ty, YiConfigVariable var) => (var -> ty) -> ty
+runHook = runHookImpl
+
+-- | The class of \"valid hooks\". This class is exported abstractly,
+-- but the instances can be phrased quite simply: the functions (of
+-- arbitrarily many arguments, including zero) which run in either the
+-- 'EditorM' or 'YiM' monads.
+--
+--A typical example would be something like
+--
+-- @Int -> String -> 'EditorM' String@.
+class HookType ty where
+    runHookImpl :: YiConfigVariable var => (var -> ty) -> ty
+
+instance HookType (EditorM a) where
+    runHookImpl lookupHook = do
+        cfg <- askCfg
+        lookupHook (cfg ^. configVariable)
+instance HookType (YiM a) where
+    runHookImpl lookupHook = do
+        cfg <- askCfg
+        lookupHook (cfg ^. configVariable)
+instance HookType b => HookType (a -> b) where
+    runHookImpl lookupHook a = runHookImpl (($a) . lookupHook)
diff --git a/src/Yi/IncrementalParse.hs b/src/Yi/IncrementalParse.hs
new file mode 100644
--- /dev/null
+++ b/src/Yi/IncrementalParse.hs
@@ -0,0 +1,33 @@
+{-# LANGUAGE ScopedTypeVariables #-}
+
+module Yi.IncrementalParse (recoverWith, symbol, eof, lookNext, testNext,
+                            State, P, Parser(..), AlexState (..), scanner) where
+
+import           Parser.Incremental (Parser (..), Process, eof, evalL, evalR,
+                                     lookNext, mkProcess, pushEof, pushSyms,
+                                     recoverWith, symbol, testNext)
+
+import           Yi.Lexer.Alex      (AlexState (..))
+import           Yi.Syntax          (Scanner (..))
+
+type P s a = Parser s a
+
+type State st token result = (st, Process token result)
+
+scanner :: forall st token result. Parser token result -> Scanner st token -> Scanner (State st token result) result
+scanner parser input = Scanner
+    {
+      scanInit = (scanInit input, mkProcess parser),
+      scanLooked = scanLooked input . fst,
+      scanRun = run,
+      scanEmpty = fst $ evalR $ pushEof $ mkProcess parser
+    }
+    where
+        run :: State st token result -> [(State st token result, result)]
+        run (st,process) = updateState0 process $ scanRun input st
+
+        updateState0 :: Process token result -> [(st,token)] -> [(State st token result, result)]
+        updateState0 _        [] = []
+        updateState0 curState toks@((st,tok):rest) = ((st, curState), result) : updateState0 nextState rest
+            where nextState =       evalL $           pushSyms [tok]           curState
+                  result    = fst $ evalR $ pushEof $ pushSyms (fmap snd toks) curState
diff --git a/src/Yi/Interact.hs b/src/Yi/Interact.hs
new file mode 100644
--- /dev/null
+++ b/src/Yi/Interact.hs
@@ -0,0 +1,329 @@
+{-# LANGUAGE FlexibleInstances      #-}
+{-# LANGUAGE FunctionalDependencies #-}
+{-# LANGUAGE GADTs                  #-}
+{-# LANGUAGE OverloadedStrings      #-}
+{-# LANGUAGE ScopedTypeVariables    #-}
+{-# LANGUAGE UndecidableInstances   #-}
+{-# OPTIONS_HADDOCK show-extensions #-}
+
+{-|
+Module      :  Yi.Interact
+License     :  GPL-2
+Maintainer  :  yi-devel@googlegroups.com
+Stability   :  experimental
+Portability :  portable
+
+This is a library of interactive processes combinators, usable to
+define extensible keymaps.
+
+(Inspired by the Parsec library, written by Koen Claessen)
+
+The processes are:
+
+* composable: in parallel using '<|>', in sequence using monadic bind.
+
+* extensible: it is always possible to override a behaviour by combination of
+  'adjustPriority' and '<|>'. (See also '<||' for a convenient combination of the two.)
+
+* monadic: sequencing is done via monadic bind. (leveraging the whole
+  battery of monadic tools that Haskell provides)
+
+The processes can parse input, and write output that depends on it.
+
+The semantics are quite obvious; only disjunction
+deserve a bit more explanation:
+
+in @p = (a '<|>' b)@, what happens if @a@ and @b@ recognize the same
+input (prefix), but produce conflicting output?
+
+* if the output is the same (as by the Eq class), then the processes (prefixes) are "merged"
+* if a Write is more prioritized than the other, the one with low priority will be discarded
+* otherwise, the output will be delayed until one of the branches can be discarded.
+* if there is no way to disambiguate, then no output will be generated anymore.
+  This situation can be detected by using 'possibleActions' however.
+-}
+
+module Yi.Interact
+    (
+     I, P (Chain,End),
+     InteractState (..),
+     MonadInteract (..),
+     deprioritize,
+     important,
+     (<||),
+     (||>),
+     option,
+     oneOf,
+     processOneEvent,
+     computeState,
+     event,
+     events,
+     choice,
+     mkAutomaton, idAutomaton,
+     runWrite,
+     anyEvent,
+     eventBetween,
+     accepted
+    ) where
+
+import           Control.Applicative (Alternative ((<|>), empty))
+import           Control.Arrow       (first)
+import           Lens.Micro.Platform          (_1, _2, view)
+import           Control.Monad.State (MonadPlus (..), MonadTrans (lift), StateT)
+import           Data.Function       (on)
+import           Data.List           (groupBy)
+import qualified Data.Text           as T (Text, append, pack)
+
+------------------------------------------------
+-- Classes
+
+-- | Abstraction of monadic interactive processes
+class (Eq w, Monad m, Alternative m, Applicative m, MonadPlus m) => MonadInteract m w e | m -> w e where
+    write :: w -> m ()
+    -- ^ Outputs a result.
+    eventBounds :: Ord e => Maybe e -> Maybe e -> m e
+    -- ^ Consumes and returns the next character.
+    --   Fails if there is no input left, or outside the given bounds.
+    adjustPriority :: Int -> m ()
+
+
+-------------------------------------------------
+-- State transformation
+
+-- Needs -fallow-undecidable-instances
+-- TODO: abstract over MonadTransformer
+instance MonadInteract m w e => MonadInteract (StateT s m) w e where
+    write = lift . write
+    eventBounds l h = lift (eventBounds l h)
+    adjustPriority p = lift (adjustPriority p)
+
+---------------------------------------------------------------------------
+-- | Interactive process description
+
+-- TODO: Replace 'Doc:' by ^ when haddock supports GADTs
+data I ev w a where
+    Returns :: a -> I ev w a
+    Binds :: I ev w a -> (a -> I ev w b) -> I ev w b
+    Gets :: Ord ev => Maybe ev -> Maybe ev -> I ev w ev
+    -- Doc: Accept any character between given bounds. Bound is ignored if 'Nothing'.
+    Fails :: I ev w a
+    Writes :: w -> I ev w ()
+    Priority :: Int -> I ev w ()
+    Plus :: I ev w a -> I ev w a -> I ev w a
+
+instance Functor (I event w) where
+  fmap f i = pure f <*> i
+
+instance Applicative (I ev w) where
+    pure = return
+    a <*> b = do f <- a; x <- b; return (f x)
+
+instance Alternative (I ev w) where
+    empty = Fails
+    (<|>) = Plus
+
+instance Monad (I event w) where
+  return  = Returns
+  fail _  = Fails
+  (>>=)   = Binds
+
+instance Eq w => MonadPlus (I event w) where
+  mzero = Fails
+  mplus = Plus
+
+instance Eq w => MonadInteract (I event w) w event where
+    write = Writes
+    eventBounds = Gets
+    adjustPriority = Priority
+
+
+infixl 3 <||
+
+deprioritize :: (MonadInteract f w e) => f ()
+deprioritize = adjustPriority 1
+
+(<||), (||>) :: (MonadInteract f w e) => f a -> f a -> f a
+a <|| b = a <|> (deprioritize >> b)
+
+(||>) = flip (<||)
+
+-- | Just like '(<||)' but in prefix form. It 'deprioritize's the
+-- second argument.
+important :: MonadInteract f w e => f a -> f a -> f a
+important a b = a <|| b
+
+-- | Convert a process description to an "executable" process.
+mkProcess :: Eq w => I ev w a -> (a -> P ev w) -> P ev w
+mkProcess (Returns x) = \fut -> fut x
+mkProcess Fails = const Fail
+mkProcess (m `Binds` f) = \fut -> mkProcess m (\a -> mkProcess (f a) fut)
+mkProcess (Gets l h) = Get l h
+mkProcess (Writes w) = \fut -> Write w (fut ())
+mkProcess (Priority p) = \fut -> Prior p (fut ())
+mkProcess (Plus a b) = \fut -> Best (mkProcess a fut) (mkProcess b fut)
+
+
+----------------------------------------------------------------------
+-- Process type
+
+-- | Operational representation of a process
+data P event w
+    = Ord event => Get (Maybe event) (Maybe event) (event -> P event w)
+    | Fail
+    | Write w (P event w)
+    | Prior Int (P event w) -- low numbers indicate high priority
+    | Best (P event w) (P event w)
+    | End
+    | forall mid. (Show mid, Eq mid) => Chain (P event mid) (P mid w)
+
+accepted :: (Show ev) => Int -> P ev w -> [[T.Text]]
+accepted 0 _ = [[]]
+accepted d (Get (Just low) (Just high) k) = do
+    t <- accepted (d - 1) (k low)
+    let h = if low == high
+            then showT low
+            else showT low `T.append` ".." `T.append` showT high
+    return (h : t)
+accepted _ (Get Nothing Nothing _) = [["<any>"]]
+accepted _ (Get Nothing (Just e) _) = [[".." `T.append` showT e]]
+accepted _ (Get (Just e) Nothing _) = [[showT e `T.append` ".."]]
+accepted _ Fail = []
+accepted _ (Write _ _) = [[]] -- this should show what action we get...
+accepted d (Prior _ p) = accepted d p
+accepted d (Best p q) = accepted d p ++ accepted d q
+accepted _ End = []
+accepted _ (Chain _ _) = error "accepted: chain not supported"
+
+-- Utility function
+showT :: Show a => a -> T.Text
+showT = T.pack . show
+
+-- ---------------------------------------------------------------------------
+-- Operations over P
+
+runWrite :: Eq w => P event w -> [event] -> [w]
+runWrite _ [] = []
+runWrite p (c:cs) = let (ws, p') = processOneEvent p c in ws ++ runWrite p' cs
+
+processOneEvent :: Eq w => P event w -> event -> ([w], P event w)
+processOneEvent p e = pullWrites $ pushEvent p e
+
+-- | Push an event in the automaton
+pushEvent :: P ev w -> ev -> P ev w
+pushEvent (Best c d) e = Best (pushEvent c e) (pushEvent d e)
+pushEvent (Write w c) e = Write w (pushEvent c e)
+pushEvent (Prior p c) e = Prior p (pushEvent c e)
+pushEvent (Get l h f) e = if test (e >=) l && test (e <=) h then f e else Fail
+    where test = maybe True
+pushEvent Fail _ = Fail
+pushEvent End _ = End
+pushEvent (Chain p q) e = Chain (pushEvent p e) q
+
+-- | Abstraction of the automaton state.
+data InteractState event w =  Ambiguous [(Int,w,P event w)] | Waiting | Dead | Running w (P event w)
+
+instance Monoid (InteractState event w) where
+    -- not used at the moment:
+    mappend (Running w c) _ = Running w c
+    mappend _ (Running w c) = Running w c
+    -- don't die if that can be avoided
+    mappend Dead p = p
+    mappend p Dead = p
+    -- If a branch is not determined, wait for it.
+    mappend Waiting _ = Waiting
+    mappend _ Waiting = Waiting
+    -- ambiguity remains
+    mappend (Ambiguous a) (Ambiguous b) = Ambiguous (a ++ b)
+    mempty = Ambiguous []
+
+
+-- | find all the writes that are accessible.
+findWrites :: Int -> P event w -> InteractState event w
+findWrites p (Best c d) = findWrites p c `mappend` findWrites p d
+findWrites p (Write w c) = Ambiguous [(p,w,c)]
+findWrites p (Prior dp c) = findWrites (p+dp) c
+findWrites _ Fail = Dead
+findWrites _ End = Dead
+findWrites _ (Get{})     = Waiting
+findWrites p (Chain a b) = case computeState a of
+    Dead -> Dead
+    Ambiguous _ -> Dead -- If ambiguity, don't try to do anything clever for now; die.
+    Running w c -> findWrites p (Chain c (pushEvent b w)) -- pull as much as possible from the left automaton
+    Waiting -> case findWrites p b of
+        Ambiguous choices -> Ambiguous [(p',w',Chain a c') | (p',w',c') <- choices]
+        Running w' c' -> Running w' (Chain a c') -- when it has nothing more, pull from the right.
+        Dead -> Dead
+        Waiting -> Waiting
+
+computeState :: Eq w => P event w -> InteractState event  w
+computeState a = case findWrites 0 a of
+    Ambiguous actions ->
+      let prior = minimum $ map (view _1) actions
+          bests = groupBy ((==) `on` view _2) $
+                    filter ((prior ==) . view _1) actions
+      in case bests of
+        [(_,w,c):_] -> Running w c
+        _ -> Ambiguous $ map head bests
+    s -> s
+
+
+
+pullWrites :: Eq w => P event w -> ([w], P event w)
+pullWrites a = case computeState a of
+    Running w c -> first (w:) (pullWrites c)
+    _ -> ([], a)
+
+
+instance (Show w, Show ev) => Show (P ev w) where
+    show (Get Nothing Nothing _) = "?"
+    show (Get (Just l) (Just h) _p) | l == h = show l -- ++ " " ++ show (p l)
+    show (Get l h _) = maybe "" show l ++ ".." ++ maybe "" show h
+    show (Prior p c) = ":" ++ show p ++ show c
+    show (Write w c) = "!" ++ show w ++ "->" ++ show c
+    show (End) = "."
+    show (Fail) = "*"
+    show (Best p q) = "{" ++ show p ++ "|" ++ show q ++ "}"
+    show (Chain a b) = show a ++ ">>>" ++ show b
+
+-- ---------------------------------------------------------------------------
+-- Derived operations
+oneOf :: (Ord event, MonadInteract m w event) => [event] -> m event
+oneOf s = choice $ map event s
+
+anyEvent :: (Ord event, MonadInteract m w event) => m event
+anyEvent = eventBounds Nothing Nothing
+
+eventBetween :: (Ord e, MonadInteract m w e) => e -> e -> m e
+eventBetween l h = eventBounds (Just l) (Just h)
+
+event :: (Ord event, MonadInteract m w event) => event -> m event
+-- ^ Parses and returns the specified character.
+event e = eventBetween e e
+
+events :: (Ord event, MonadInteract m w event) => [event] -> m [event]
+-- ^ Parses and returns the specified list of events (lazily).
+events = mapM event
+
+choice :: (MonadInteract m w e) => [m a] -> m a
+-- ^ Combines all parsers in the specified list.
+choice []     = fail "No choice succeeds"
+choice [p]    = p
+choice (p:ps) = p `mplus` choice ps
+
+option :: (MonadInteract m w e) => a -> m a -> m a
+-- ^ @option x p@ will either parse @p@ or return @x@ without consuming
+--   any input.
+option x p = p `mplus` return x
+
+mkAutomaton :: Eq w => I ev w a -> P ev w
+mkAutomaton i = mkProcess i (const End)
+
+-- An automaton that produces its input
+idAutomaton :: (Ord a, Eq a) => P a a
+idAutomaton = Get Nothing Nothing $ \e -> Write e idAutomaton
+-- It would be much nicer to write:
+--    mkAutomaton (forever 0 (anyEvent >>= write))
+-- however this creates a memory leak. Unfortunately I don't understand why.
+-- To witness:
+--    dist/build/yi/yi +RTS -hyI -hd
+-- Then type some characters. (Binds grows linearly)
diff --git a/src/Yi/JumpList.hs b/src/Yi/JumpList.hs
new file mode 100644
--- /dev/null
+++ b/src/Yi/JumpList.hs
@@ -0,0 +1,46 @@
+{-# LANGUAGE CPP                #-}
+{-# LANGUAGE DeriveGeneric      #-}
+{-# LANGUAGE TemplateHaskell    #-}
+
+module Yi.JumpList
+    ( JumpList
+    , Jump(..)
+    , addJump
+    , jumpBack
+    , jumpForward
+    ) where
+
+import           GHC.Generics          (Generic)
+
+import           Data.Binary           (Binary)
+import           Data.List.PointedList as PL (PointedList (..), next, previous)
+import           Yi.Buffer.Basic       (BufferRef, Mark)
+
+
+type JumpList = Maybe (PL.PointedList Jump)
+
+data Jump = Jump {
+        jumpMark      :: Mark
+      , jumpBufferRef :: BufferRef
+    } deriving (Generic)
+
+instance Binary Jump
+
+instance Show Jump where
+    show (Jump mark bufref) = "<Jump " ++ show mark ++ " " ++ show bufref ++ ">"
+
+addJump :: Jump -> JumpList -> JumpList
+addJump j (Just (PL.PointedList past present _future)) = Just $ PL.PointedList (present:past) j []
+addJump j Nothing = Just $ PL.PointedList [] j []
+
+jumpBack :: JumpList -> JumpList
+jumpBack = modifyJumpList previous
+
+jumpForward :: JumpList -> JumpList
+jumpForward = modifyJumpList next
+
+modifyJumpList :: (PointedList Jump -> Maybe (PointedList Jump)) -> JumpList -> JumpList
+modifyJumpList f (Just jumps) = case f jumps of
+                                Nothing -> Just jumps
+                                Just jumps' -> Just jumps'
+modifyJumpList _ Nothing = Nothing
diff --git a/src/Yi/Keymap.hs b/src/Yi/Keymap.hs
new file mode 100644
--- /dev/null
+++ b/src/Yi/Keymap.hs
@@ -0,0 +1,143 @@
+{-# LANGUAGE DeriveDataTypeable         #-}
+{-# LANGUAGE ExistentialQuantification  #-}
+{-# LANGUAGE FlexibleContexts           #-}
+{-# LANGUAGE FlexibleInstances          #-}
+{-# LANGUAGE FunctionalDependencies     #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE MultiParamTypeClasses      #-}
+{-# LANGUAGE Rank2Types                 #-}
+{-# LANGUAGE StandaloneDeriving         #-}
+{-# LANGUAGE TemplateHaskell            #-}
+{-# LANGUAGE TypeSynonymInstances       #-}
+{-# OPTIONS_HADDOCK show-extensions #-}
+
+-- |
+-- Module      :  Yi.Keymap
+-- License     :  GPL-2
+-- Maintainer  :  yi-devel@googlegroups.com
+-- Stability   :  experimental
+-- Portability :  portable
+--
+-- 'Keymap', 'YiM' and 'Action's.
+
+
+module Yi.Keymap
+    ( Action(..)
+    , emptyAction
+    , Interact
+    , KeymapM
+    , Keymap
+    , KeymapEndo
+    , KeymapProcess
+    , KeymapSet(..)
+    , topKeymapA
+    , insertKeymapA
+    , extractTopKeymap
+    , modelessKeymapSet
+    , YiM(..)
+    , withUI
+    , unsafeWithEditor
+    , readEditor
+    , catchDynE
+    , catchJustE
+    , handleJustE
+    , YiAction (..)
+    , Yi(..)
+    , IsRefreshNeeded(..)
+    , YiVar(..)
+    , write
+    , withModeY
+
+    -- * Lenses
+    , yiSubprocessesA
+    , yiEditorA
+    , yiSubprocessIdSupplyA
+    , yiConfigA
+    , yiInputA
+    , yiOutputA
+    , yiUiA
+    , yiVarA
+    ) where
+
+import           Control.Exception    (Exception, catch, catchJust)
+import           Control.Monad.Reader (ReaderT (ReaderT, runReaderT))
+import           Control.Monad.State  (gets)
+import           Yi.Buffer            ()
+import qualified Yi.Editor            as Editor (currentBuffer, findBuffer)
+import qualified Yi.Interact          as I (MonadInteract, write)
+import           Yi.Monad             (with)
+import           Yi.Types
+import           Yi.UI.Common         (UI)
+import           Yi.Utils             (io, makeLensesWithSuffix)
+
+
+-----------------------
+-- Keymap basics
+
+-- | @write a@ returns a keymap that just outputs the action @a@.
+write :: (I.MonadInteract m Action ev, YiAction a x, Show x) => a -> m ()
+write x = I.write (makeAction x)
+
+--------------------------------
+-- Uninteresting glue code
+
+withUI :: (UI Editor -> IO a) -> YiM a
+withUI = with yiUi
+
+readEditor :: MonadEditor m => (Editor -> a) -> m a
+readEditor f = withEditor (gets f)
+
+catchDynE :: Exception exception => YiM a -> (exception -> YiM a) -> YiM a
+catchDynE (YiM inner) handler
+    = YiM $ ReaderT (\r -> catch (runReaderT inner r) (\e -> runReaderT (runYiM $ handler e) r))
+
+catchJustE :: (Exception e) => (e -> Maybe b) -- ^ Predicate to select exceptions
+           -> YiM a      -- ^ Computation to run
+           -> (b -> YiM a) -- ^   Handler
+           -> YiM a
+catchJustE p (YiM c) h = YiM $ ReaderT (\r -> catchJust p (runReaderT c r) (\b -> runReaderT (runYiM $ h b) r))
+
+handleJustE :: (Exception e) => (e -> Maybe b) -> (b -> YiM a) -> YiM a -> YiM a
+handleJustE p h c = catchJustE p c h
+
+-- -------------------------------------------
+
+class YiAction a x | a -> x where
+    makeAction :: Show x => a -> Action
+
+instance YiAction (IO x) x where
+    makeAction = YiA . io
+
+instance YiAction (YiM x) x where
+    makeAction = YiA
+
+instance YiAction (EditorM x) x where
+    makeAction = EditorA
+
+instance YiAction (BufferM x) x where
+    makeAction = BufferA
+
+instance YiAction Action () where
+    makeAction = id
+
+makeLensesWithSuffix "A" ''KeymapSet
+
+modelessKeymapSet :: Keymap -> KeymapSet
+modelessKeymapSet k = KeymapSet
+ { insertKeymap = k
+ , topKeymap = k
+ }
+
+-- | @withModeY f@ runs @f@ on the current buffer's mode. As this runs in
+-- the YiM monad, we're able to do more than with just 'withModeB' such as
+-- prompt the user for something before running the action.
+withModeY :: (forall syntax. Mode syntax -> YiM ()) -> YiM ()
+withModeY f = do
+   bufref <- gets Editor.currentBuffer
+   mfbuf <- Editor.findBuffer bufref
+   case mfbuf of
+     Nothing -> return ()
+     Just (FBuffer {bmode = m}) -> f m
+
+makeLensesWithSuffix "A" ''YiVar
+makeLensesWithSuffix "A" ''Yi
diff --git a/src/Yi/Keymap/Keys.hs b/src/Yi/Keymap/Keys.hs
new file mode 100644
--- /dev/null
+++ b/src/Yi/Keymap/Keys.hs
@@ -0,0 +1,123 @@
+{-# LANGUAGE FlexibleContexts  #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# OPTIONS_HADDOCK show-extensions #-}
+
+-- |
+-- Module      :  Yi.Keymap.Keys
+-- License     :  GPL-2
+-- Maintainer  :  yi-devel@googlegroups.com
+-- Stability   :  experimental
+-- Portability :  portable
+--
+-- Combinators for building keymaps.
+
+module Yi.Keymap.Keys
+    (
+     module Yi.Event,
+     module Yi.Interact,
+     printableChar, textChar,
+     charOf, shift, meta, ctrl, super, hyper, spec, char,
+     (>>!), (>>=!), (?>>), (?>>!), (?*>>), (?*>>!),
+     ctrlCh, metaCh, hyperCh,
+     optMod,
+     pString
+    ) where
+
+import Prelude       hiding (error)
+
+import Control.Monad (unless)
+import Data.Char     (isAlpha, isPrint, toUpper)
+import Data.List     (nub, sort)
+import Yi.Debug      (error)
+import Yi.Event      (Event (..), Key (..), Modifier (..), eventToChar, prettyEvent)
+import Yi.Interact   hiding (write)
+import Yi.Keymap     (Action, KeymapM, YiAction, write)
+
+printableChar :: (MonadInteract m w Event) => m Char
+printableChar = do
+  Event (KASCII c) [] <- anyEvent
+  unless (isPrint c) $
+       fail "unprintable character"
+  return c
+
+-- | Parse any character that can be inserted in the text.
+textChar :: KeymapM Char
+textChar = do
+    -- Why only ASCII?
+    Event (KASCII c) [] <- anyEvent
+    return c
+
+pString :: (MonadInteract m w Event) => String -> m [Event]
+pString = events . map char
+
+charOf :: (MonadInteract m w Event) => (Event -> Event) -> Char -> Char -> m Char
+charOf modifier l h =
+    do Event (KASCII c) _ <- eventBetween (modifier $ char l) (modifier $ char h)
+       return c
+
+shift,ctrl,meta,super,hyper :: Event -> Event
+shift (Event (KASCII c) ms) | isAlpha c = Event (KASCII (toUpper c)) ms
+                            | otherwise = error "shift: unhandled event"
+shift (Event k ms) = Event k $ nub $ sort (MShift:ms)
+
+ctrl (Event k ms) = Event k $ nub $ sort (MCtrl:ms)
+
+meta (Event k ms) = Event k $ nub $ sort (MMeta:ms)
+
+super (Event k ms) = Event k $ nub $ sort (MSuper:ms)
+
+hyper (Event k ms) = Event k $ nub $ sort (MHyper:ms)
+
+char :: Char -> Event
+char '\t' = Event KTab []
+char '\r' = Event KEnter []
+char '\n' = Event KEnter []
+char c = Event (KASCII c) []
+
+ctrlCh :: Char -> Event
+ctrlCh = ctrl . char
+
+metaCh :: Char -> Event
+metaCh = meta . char
+
+hyperCh :: Char -> Event
+hyperCh = hyper . char
+
+-- | @optMod f ev@ produces a 'MonadInteract' that consumes @ev@ or @f ev@
+optMod ::(MonadInteract m w Event) => (Event -> Event) -> Event -> m Event
+optMod f ev = oneOf [ev, f ev]
+
+-- | Convert a special key into an event
+spec :: Key -> Event
+spec k = Event k []
+
+-- | > p >>! act = p >> 'write' act
+(>>!) :: (MonadInteract m Action Event, YiAction a x, Show x) => m b -> a -> m ()
+p >>! act = p >> write act
+
+-- | > p >>=! act = p >>= 'write' . act
+(>>=!) :: (MonadInteract m Action Event, YiAction a x, Show x) => m b -> (b -> a) -> m ()
+p >>=! act = p >>= write . act
+
+-- | @ ev ?>> proc = 'event' ev >> proc @
+(?>>) :: (MonadInteract m action Event) => Event -> m a -> m a
+ev ?>> proc = event ev >> proc
+
+-- | @ ev ?>>! act = 'event' ev >> 'write' act @
+(?>>!) :: (MonadInteract m Action Event, YiAction a x, Show x) => Event -> a -> m ()
+ev ?>>! act = event ev >> write act
+
+-- | @ ev ?*>> proc = 'events' ev >> proc @
+(?*>>) :: (MonadInteract m action Event) => [Event] -> m a -> m a
+ev ?*>> proc = events ev >> proc
+
+-- | @ ev ?*>>! act = 'events' ev >> 'write' act @
+(?*>>!) :: (MonadInteract m Action Event, YiAction a x, Show x) => [Event] -> a -> m ()
+ev ?*>>! act = events ev >> write act
+
+infixl 1 >>!
+infixl 1 >>=!
+infixr 0 ?>>!
+infixr 0 ?>>
+infixr 0 ?*>>!
+infixr 0 ?*>>
diff --git a/src/Yi/KillRing.hs b/src/Yi/KillRing.hs
new file mode 100644
--- /dev/null
+++ b/src/Yi/KillRing.hs
@@ -0,0 +1,89 @@
+{-# LANGUAGE CPP                #-}
+{-# LANGUAGE TemplateHaskell    #-}
+{-# OPTIONS_HADDOCK show-extensions #-}
+
+-- |
+-- Module      :  Yi.KillRing
+-- License     :  GPL-2
+-- Maintainer  :  yi-devel@googlegroups.com
+-- Stability   :  experimental
+-- Portability :  portable
+--
+-- Killring operations.
+
+module Yi.KillRing ( Killring
+                   , _krKilled
+                   , _krContents
+                   , krKilled
+                   , krContents
+                   , krEndCmd
+                   , krPut
+                   , krSet, krGet
+                   , krEmpty
+                   , krLastYank
+                   )
+    where
+
+import           Prelude             hiding (head, tail, take)
+
+import           Lens.Micro.Platform ((^.), makeLenses)
+import           Data.Binary         (Binary, get, put)
+import           Data.List.NonEmpty  (NonEmpty (..), head, take)
+import           Data.Monoid         ((<>))
+import           Yi.Buffer.Basic     (Direction (..))
+import qualified Yi.Rope             as R (YiString, length)
+
+
+data Killring = Killring { _krKilled :: Bool
+                         , _krAccumulate :: Bool
+                         , _krContents :: NonEmpty R.YiString
+                         , _krLastYank :: Bool
+                         } deriving (Show, Eq)
+
+instance Binary Killring where
+  put (Killring k a c l) =
+    let putNE (x :| xs) = put x >> put xs
+    in put k >> put a >> putNE c >> put l
+  get = let getNE = (:|) <$> get <*> get
+        in Killring <$> get <*> get <*> getNE <*> get
+
+makeLenses ''Killring
+
+maxDepth :: Int
+maxDepth = 10
+
+krEmpty :: Killring
+krEmpty = Killring { _krKilled = False
+                   , _krAccumulate = False
+                   , _krContents = mempty :| mempty
+                   , _krLastYank = False
+                   }
+
+-- | Finish an atomic command, for the purpose of killring accumulation.
+krEndCmd :: Killring -> Killring
+krEndCmd kr = kr { _krKilled = False , _krAccumulate = kr ^. krKilled }
+
+-- | Put some text in the killring.
+-- It's accumulated if the last command was a kill too
+krPut :: Direction -> R.YiString -> Killring -> Killring
+krPut dir s kr@Killring { _krContents = r@(x :| xs) }
+    = kr { _krKilled = True
+         , _krContents =
+           if kr ^. krAccumulate
+           then (case dir of Forward  -> x <> s
+                             Backward -> s <> x) :| xs
+           else push s r
+         }
+
+-- | Push a string in the killring.
+push :: R.YiString -> NonEmpty R.YiString -> NonEmpty R.YiString
+push s r@(h :| t) = s :| if R.length h <= 1 then t else take maxDepth r
+-- Don't save very small cutted text portions.
+
+-- | Set the top of the killring. Never accumulate the previous content.
+krSet :: R.YiString -> Killring -> Killring
+krSet s kr@Killring {_krContents = _ :| xs} = kr {_krContents = s :| xs}
+
+-- | Get the top of the killring.
+krGet :: Killring -> R.YiString
+krGet = head . _krContents
diff --git a/src/Yi/Layout.hs b/src/Yi/Layout.hs
new file mode 100644
--- /dev/null
+++ b/src/Yi/Layout.hs
@@ -0,0 +1,397 @@
+{-# LANGUAGE DeriveDataTypeable        #-}
+{-# LANGUAGE DeriveFunctor             #-}
+{-# LANGUAGE ExistentialQuantification #-}
+{-# LANGUAGE Rank2Types                #-}
+{-# LANGUAGE TupleSections             #-}
+{-# LANGUAGE ViewPatterns              #-}
+{-# OPTIONS_GHC -funbox-strict-fields #-} -- we might as well unbox our Ints.
+
+-- | This module defines the layout manager interface (see 'LayoutManager'). To desgin a new layout manager, just make an instance of this class.
+module Yi.Layout
+  (
+    -- * Concrete layouts
+    Layout(..),
+    Orientation(..),
+    DividerPosition,
+    DividerRef,
+    RelativeSize,
+    dividerPositionA,
+    findDivider,
+
+    -- * Layout managers
+    -- ** The interface
+    LayoutManager(..),
+    AnyLayoutManager(..),
+    layoutManagerSameType,
+    -- ** Standard managers
+    wide,
+    tall,
+    slidyTall,
+    slidyWide,
+    hPairNStack,
+    vPairNStack,
+    -- * Utility functions
+    -- ** Layouts as rectangles
+    Rectangle(..),
+    HasNeighborWest,
+    layoutToRectangles,
+    -- ** Transposing things
+    Transposable(..),
+    Transposed(..),
+    -- ** 'DividerRef' combinators
+    -- $divRefCombinators
+    LayoutM,
+    pair,
+    singleWindow,
+    stack,
+    evenStack,
+    runLayoutM,
+  )
+ where
+
+import           Control.Applicative        ((<|>))
+import           Control.Arrow              (first)
+import           Lens.Micro.Platform                 (Lens', lens)
+import qualified Control.Monad.State.Strict as Monad (State, evalState, get, put)
+import           Data.Default               (Default, def)
+import           Data.List                  (foldl', mapAccumL)
+import           Data.Maybe                 (fromMaybe, isNothing)
+import           Data.Typeable              (Typeable, cast, typeOf)
+
+-------------------------------- Some design notes ----------------------
+-- [Treatment of mini windows]
+
+-- Mini windows are not subject to layout; instead, they are always
+-- placed at the bottom of the screen. There are multiple reasons for
+-- this, as discussed in
+-- https://groups.google.com/d/topic/yi-devel/vhTObC25dpY/discussion, one
+-- being that for many layouts, the bottom (or top) of the screen is the
+-- only reasonable place for mini windows (for example, think about
+-- side-by-side layouts).
+
+-- [Design of the 'Layout' datatype]
+
+-- The 'Layout' datatype is currently implemented in terms of
+-- horizontal stacks and vertical stacks. An alternative approach,
+-- which xmonad uses, is the following: a 'Layout a' could be a
+-- function @a -> Rectangle@ which specifies in coordinates where a
+-- window should be placed.
+--
+-- While this alternative is more flexible than the current approach
+-- in allowing spiral layouts and the like, the vty UI doesn't support
+-- this: only vertical and horizontal composition of images is
+-- allowed.
+
+
+
+----------------------------------- Concrete 'Layout's.
+-- | UI-agnostic layout schema. The basic constructs are
+-- (horizontal/vertical) stacks with fixed ratios between window
+-- sizes; and (horizontal/vertical) pairs with a slider in between (if
+-- available).
+data Layout a
+  = SingleWindow a
+  | Stack {
+      orientation :: !Orientation,              -- ^ Orientation
+      wins        :: [(Layout a, RelativeSize)] -- ^ The layout stack, with the given weights
+        -- TODO: fix strictness for stack (it's still lazy)
+      }
+  | Pair {
+       orientation :: !Orientation,     -- ^ Orientation
+       divPos      :: !DividerPosition, -- ^ Initial position of the divider
+       divRef      :: !DividerRef,      -- ^ Index of the divider (for updating the divider position)
+       pairFst     :: !(Layout a),      -- ^ Upper of of the pair
+       pairSnd     :: !(Layout a)       -- ^ Lower of the pair
+    }
+  deriving(Typeable, Eq, Functor)
+
+-- | Accessor for the 'DividerPosition' with given reference
+dividerPositionA :: DividerRef -> Lens' (Layout a) DividerPosition
+dividerPositionA ref = lens getter (flip setter) where
+  setter pos = set'
+    where
+      set' s@(SingleWindow _) = s
+      set' p@Pair{} | divRef p == ref = p{ divPos = pos }
+                    | otherwise       = p{ pairFst = set' (pairFst p), pairSnd = set' (pairSnd p) }
+      set' s@Stack{} = s{ wins = fmap (first set') (wins s) }
+
+  getter = fromMaybe invalidRef . get'
+
+  get' (SingleWindow _) = Nothing
+  get' p@Pair{} | divRef p == ref = Just (divPos p)
+                | otherwise       = get' (pairFst p) <|> get' (pairSnd p)
+  get' s@Stack{} = foldl' (<|>) Nothing (fmap (get' . fst) (wins s))
+
+  invalidRef = error "Yi.Layout.dividerPositionA: invalid DividerRef"
+
+-- | Find the divider nearest to a given window, or just the first one
+-- in case the argument is 'Nothing'
+findDivider :: Eq a => Maybe a -> Layout a -> Maybe DividerRef
+findDivider mbw = go [] where
+  go path (SingleWindow w) = maybe Nothing (\w' ->
+                               if w == w' && not (null path)
+                               then Just (head path) else Nothing) mbw
+  go path (Pair _ _ ref l1 l2) = if isNothing mbw then Just ref
+                                 else let p' = ref : path
+                                      in go p' l1 <|> go p' l2
+  go path (Stack _ ws) = foldr (<|>) Nothing $ map (go path . fst) ws
+
+instance Show a => Show (Layout a) where
+  show (SingleWindow a) = show a
+  show (Stack o s) = show o ++ " stack " ++ show s
+  show p@(Pair{}) = show (orientation p) ++ " " ++ show (pairFst p, pairSnd p)
+
+-- | The def layout consists of a single window
+instance Default a => Default (Layout a) where
+  def = SingleWindow def
+
+-- | Orientations for 'Stack' and 'Pair'
+data Orientation
+  = Horizontal
+  | Vertical
+  deriving(Eq, Show)
+
+-- | Divider reference
+type DividerRef = Int
+
+-- | Divider position, in the range (0,1)
+type DividerPosition = Double
+
+-- | Relative sizes, for 'Stack'
+type RelativeSize = Double
+
+----------------------------------------------------- Layout managers
+-- TODO: add Binary requirement when possible
+-- | The type of layout managers. See the layout managers 'tall', 'hPairNStack' and 'slidyTall' for some example implementations.
+class (Typeable m, Eq m) => LayoutManager m where
+  -- | Given the old layout and the new list of windows, construct a
+  -- layout for the new list of windows.
+  --
+  -- If the layout manager uses sliding dividers, then a user will expect that most
+  -- of these dividers don't move when adding a new window. It is the layout
+  -- manager's responsibility to ensure that this is the case, and this is the
+  -- purpose of the @Layout a@ argument.
+  --
+  -- The old layout may come from a different layout manager, in which case the layout manager is free to ignore it.
+  pureLayout :: m -> Layout a -> [a] -> Layout a
+  -- | Describe the layout in a form suitable for the user.
+  describeLayout :: m -> String
+  -- | Cycles to the next variant, if there is one (the default is 'id')
+  nextVariant :: m -> m
+  nextVariant = id
+  -- | Cycles to the previous variant, if there is one (the default is 'id'
+  previousVariant :: m -> m
+  previousVariant = id
+
+-- | Existential wrapper for 'Layout'
+data AnyLayoutManager = forall m. LayoutManager m => AnyLayoutManager !m
+  deriving(Typeable)
+
+instance Eq AnyLayoutManager where
+  (AnyLayoutManager l1) == (AnyLayoutManager l2) = maybe False (== l2) (cast l1)
+
+instance LayoutManager (AnyLayoutManager) where
+  pureLayout (AnyLayoutManager l) = pureLayout l
+  describeLayout (AnyLayoutManager l) = describeLayout l
+  nextVariant (AnyLayoutManager l) = AnyLayoutManager (nextVariant l)
+  previousVariant (AnyLayoutManager l) = AnyLayoutManager (previousVariant l)
+
+-- | The default layout is 'tallLayout'
+instance Default AnyLayoutManager where
+  def = hPairNStack 1
+
+-- | True if the internal layout managers have the same type (but are not necessarily equal).
+layoutManagerSameType :: AnyLayoutManager -> AnyLayoutManager -> Bool
+layoutManagerSameType (AnyLayoutManager l1) (AnyLayoutManager l2) = typeOf l1 == typeOf l2
+
+------------------------------ Standard layouts
+-- | Tall windows (i.e. places windows side-by-side, equally spaced)
+data Tall = Tall
+  deriving(Eq, Typeable)
+
+-- | Windows placed side-by-side, equally spaced.
+tall :: AnyLayoutManager
+tall = AnyLayoutManager Tall
+
+instance LayoutManager Tall where
+  pureLayout Tall _oldLayout ws = runLayoutM $ evenStack Horizontal (fmap singleWindow ws)
+  describeLayout Tall = "Windows positioned side-by-side"
+
+-- | Wide windows (windows placed on top of one another, equally spaced)
+data Wide = Wide
+  deriving(Eq, Typeable)
+
+instance LayoutManager Wide where
+  pureLayout Wide _oldLayout ws = runLayoutM $ evenStack Vertical (fmap singleWindow ws)
+  describeLayout Wide = "Windows positioned above one another"
+
+-- | Windows placed on top of one another, equally spaced
+wide :: AnyLayoutManager
+wide = AnyLayoutManager Wide
+
+-- | Tall windows, with arranged in a balanced binary tree with sliders in between them
+data SlidyTall = SlidyTall
+  deriving(Eq, Typeable)
+
+-- | Tall windows, arranged in a balanced binary tree with sliders in between them.
+slidyTall :: AnyLayoutManager
+slidyTall = AnyLayoutManager SlidyTall
+
+instance LayoutManager SlidyTall where
+  -- an error on input [] is easier to debug than an infinite loop.
+  pureLayout SlidyTall _oldLayout [] = error "Yi.Layout: empty window list unexpected"
+  pureLayout SlidyTall oldLayout xs = runLayoutM (go (Just oldLayout) xs) where
+     go _layout [x] = singleWindow x
+     go layout (splitList -> (lxs, rxs)) =
+       case layout of
+           -- if the old layout had a pair in the same point of the tree, use its divider position
+           Just (Pair Horizontal pos _ l r) -> pair Horizontal pos (go (Just l) lxs) (go (Just r) rxs)
+           -- otherwise, just use divider position 0.5
+           _ -> pair Horizontal 0.5 (go Nothing lxs) (go Nothing rxs)
+
+  describeLayout SlidyTall = "Slidy tall windows, with balanced-position sliders"
+
+splitList :: [a] -> ([a], [a])
+splitList xs = splitAt ((length xs + 1) `div` 2) xs
+
+-- | Transposed version of 'SlidyTall'
+newtype SlidyWide = SlidyWide (Transposed SlidyTall)
+  deriving(Eq, Typeable)
+
+-- | Transposed version of 'slidyTall'
+slidyWide :: AnyLayoutManager
+slidyWide = AnyLayoutManager (SlidyWide (Transposed SlidyTall))
+
+instance LayoutManager SlidyWide where
+    pureLayout (SlidyWide w) = pureLayout w
+    describeLayout _ = "Slidy wide windows, with balanced-position sliders"
+
+-- | Fixed number of \"main\" windows on the left; stack of windows on the right
+data HPairNStack = HPairNStack !Int
+  deriving(Eq, Typeable)
+
+-- | @n@ windows on the left; stack of windows on the right.
+hPairNStack :: Int -> AnyLayoutManager
+hPairNStack n | n < 1     = error "Yi.Layout.hPairNStackLayout: n must be at least 1"
+                    | otherwise = AnyLayoutManager (HPairNStack n)
+
+instance LayoutManager HPairNStack where
+    pureLayout (HPairNStack n) oldLayout (fmap singleWindow -> xs)
+          | length xs <= n = runLayoutM $ evenStack Vertical xs
+          | otherwise = runLayoutM $ case splitAt n xs of
+              (ls, rs) ->  pair Horizontal pos
+                 (evenStack Vertical ls)
+                 (evenStack Vertical rs)
+       where
+          pos = case oldLayout of
+              Pair Horizontal pos' _ _ _ -> pos'
+              _ -> 0.5
+
+    describeLayout (HPairNStack n) = show n ++ " windows on the left; remaining windows on the right"
+    nextVariant (HPairNStack n) = HPairNStack (n+1)
+    previousVariant (HPairNStack n) = HPairNStack (max (n-1) 1)
+
+newtype VPairNStack = VPairNStack (Transposed HPairNStack)
+  deriving(Eq, Typeable)
+
+-- | Transposed version of 'hPairNStack'.
+vPairNStack :: Int -> AnyLayoutManager
+vPairNStack n = AnyLayoutManager (VPairNStack (Transposed (HPairNStack n)))
+
+instance LayoutManager VPairNStack where
+    pureLayout (VPairNStack lm) = pureLayout lm
+    previousVariant (VPairNStack lm) = VPairNStack (previousVariant lm)
+    nextVariant (VPairNStack lm) = VPairNStack (nextVariant lm)
+    describeLayout (VPairNStack (Transposed (HPairNStack n))) = show n ++ " windows on top; remaining windows beneath"
+
+----------------------- Utils
+
+-- | A general bounding box
+data Rectangle = Rectangle { rectX, rectY, rectWidth, rectHeight :: !Double }
+  deriving(Eq, Show)
+
+-- | Used by the vty frontend to draw vertical separators
+type HasNeighborWest = Bool
+
+layoutToRectangles :: HasNeighborWest -> Rectangle -> Layout a -> [(a, Rectangle, HasNeighborWest)]
+layoutToRectangles nb bounds (SingleWindow a) = [(a, bounds, nb)]
+layoutToRectangles nb bounds (Stack o ts) = handleStack o bounds ts'
+    where ts' = if o == Vertical then setNbs nb ts
+                else case ts of
+                       (l, s) : xs -> (l, s, nb) : setNbs True xs
+                       []          -> []
+          setNbs val = map (\(l, s) -> (l, s, val))
+layoutToRectangles nb bounds (Pair o p _ a b) = handleStack o bounds [(a,p,nb), (b,1-p,nb')]
+    where nb' = if o == Horizontal then True else nb
+
+handleStack :: Orientation -> Rectangle
+            -> [(Layout a, RelativeSize, HasNeighborWest)]
+            -> [(a, Rectangle, HasNeighborWest)]
+handleStack o bounds tiles = concat . snd . mapAccumL doTile startPos $ tiles
+    where 
+      (totalSpace, startPos, mkBounds) = case o of
+          Vertical   -> (rectHeight bounds, rectY bounds,
+                         \pos size -> bounds { rectY = pos, rectHeight = size })
+          Horizontal -> (rectWidth bounds,  rectX bounds,
+                         \pos size -> bounds { rectX = pos, rectWidth  = size })
+      totalWeight' = sum . fmap (\(_, s, _) -> s) $ tiles
+      totalWeight = if totalWeight' > 0 then totalWeight'
+                    else error "Yi.Layout: Stacks must have positive weights"
+      spacePerWeight = totalSpace / totalWeight
+      doTile pos (t, wt, nb) = (pos + wt * spacePerWeight,
+                               layoutToRectangles nb (mkBounds pos (wt * spacePerWeight)) t)
+
+----------- Flipping things
+-- | Things with orientations which can be flipped
+class Transposable r where transpose :: r -> r
+instance Transposable Orientation where { transpose Horizontal = Vertical; transpose Vertical = Horizontal }
+instance Transposable (Layout a) where
+    transpose (SingleWindow a) = SingleWindow a
+    transpose (Stack o ws) = Stack (transpose o) (fmap (first transpose) ws)
+    transpose (Pair o p r a b) = Pair (transpose o) p r (transpose a) (transpose b)
+
+-- | Same as 'lm', but with all 'Orientation's 'transpose'd. See 'slidyWide' for an example of its use.
+newtype Transposed lm = Transposed lm
+  deriving(Eq, Typeable)
+
+instance LayoutManager lm => LayoutManager (Transposed lm) where
+    pureLayout (Transposed lm) l ws = transpose (pureLayout lm (transpose l) ws)
+    describeLayout (Transposed lm) = "Transposed version of: " ++ describeLayout lm
+    nextVariant (Transposed lm) = Transposed (nextVariant lm)
+    previousVariant (Transposed lm) = Transposed (previousVariant lm)
+
+-------------------- 'DividerRef' combinators
+-- $divRefCombinators
+-- It is tedious and error-prone for 'LayoutManager's to assign 'DividerRef's themselves. Better is to use these monadic smart constructors for 'Layout'. For example, the layout
+--
+-- @'Pair' 'Horizontal' 0.5 0 ('Pair' 'Vertical' 0.5 1 ('SingleWindow' w1) ('SingleWindow' w2)) ('SingleWindow' w3)@
+--
+-- could be with the combinators below as
+--
+-- @'runLayoutM' $ 'pair' 'Horizontal' 0.5 ('pair' 'Vertical' 0.5 ('singleWindow' w1) ('singleWindow' w2)) ('singleWindow' w3)@
+--
+-- These combinators do will also ensure strictness of the 'wins' field of 'Stack'. They also tidy up and do some error checking: length-1 stacks are removed (they are unnecessary); length-0 stacks raise errors.
+
+-- | A 'Layout a' wrapped in a state monad for tracking 'DividerRef's. This type is /not/ itself a monad, but should rather be thought of as a 'DividerRef'-free version of the 'Layout' type.
+newtype LayoutM a = LayoutM (Monad.State DividerRef (Layout a))
+
+singleWindow :: a -> LayoutM a
+singleWindow a = LayoutM (pure (SingleWindow a))
+
+pair :: Orientation -> DividerPosition -> LayoutM a -> LayoutM a -> LayoutM a
+pair o p (LayoutM l1) (LayoutM l2) = LayoutM $ do
+    ref <- Monad.get
+    Monad.put (ref+1)
+    Pair o p ref <$> l1 <*> l2
+
+stack :: Orientation -> [(LayoutM a, RelativeSize)] -> LayoutM a
+stack _ [] = error "Yi.Layout: Length-0 stack"
+stack _ [l] = fst l
+stack o ls = LayoutM (Stack o <$> mapM (\(LayoutM lm,rs) -> (,rs) <$> lm) ls)
+
+-- | Special case of 'stack' with all 'RelativeSize's equal.
+evenStack :: Orientation -> [LayoutM a] -> LayoutM a
+evenStack o ls = stack o (fmap (\l -> (l,1)) ls)
+
+runLayoutM :: LayoutM a -> Layout a
+runLayoutM (LayoutM l) = Monad.evalState l 0
diff --git a/src/Yi/Main.hs b/src/Yi/Main.hs
new file mode 100644
--- /dev/null
+++ b/src/Yi/Main.hs
@@ -0,0 +1,178 @@
+{-# LANGUAGE CPP #-}
+{-# LANGUAGE DeriveDataTypeable #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE OverloadedStrings #-}
+
+-- | This is the main module of Yi, called with configuration from the user.
+-- Here we mainly process command line arguments.
+
+module Yi.Main (
+                -- * Static main
+                main,
+                -- * Command line processing
+                do_args,
+                ConsoleConfig(..),
+               ) where
+
+import Control.Monad
+import Data.Char
+import Data.Monoid
+import qualified Data.Text as T
+import Data.List (intercalate)
+import Data.Version (showVersion)
+import Lens.Micro.Platform (view)
+import System.Console.GetOpt
+import System.Exit
+#ifndef HLINT
+#include "ghcconfig.h"
+#endif
+
+import Yi.Buffer
+import Yi.Config
+import Yi.Core (startEditor)
+import Yi.Debug
+import Yi.Editor
+import Yi.File
+import Yi.Keymap
+import Yi.Option (YiOption, OptionError(..), yiCustomOptions)
+import Yi.Paths (getConfigDir)
+import Paths_yi_core
+
+-- | Configuration information which can be set in the command-line, but not
+-- in the user's configuration file.
+data ConsoleConfig =
+  ConsoleConfig {
+     ghcOptions :: [String],
+     selfCheck :: Bool,
+     userConfigDir :: IO FilePath
+  }
+
+defaultConsoleConfig :: ConsoleConfig
+defaultConsoleConfig =
+  ConsoleConfig {
+                  ghcOptions = [],
+                  selfCheck = False,
+                  userConfigDir = Yi.Paths.getConfigDir
+                }
+
+-- ---------------------------------------------------------------------
+-- | Argument parsing. Pretty standard.
+
+data Opts = Help
+          | Version
+          | LineNo String
+          | EditorNm String
+          | File String
+          | Frontend String
+          | ConfigFile String
+          | SelfCheck
+          | GhcOption String
+          | Debug
+          | OpenInTabs
+          | CustomNoArg YiOption
+          | CustomReqArg (String -> YiOption) String
+          | CustomOptArg (Maybe String -> YiOption) (Maybe String)
+
+-- | List of editors for which we provide an emulation.
+editors :: [(String,Config -> Config)]
+editors = []
+
+builtinOptions :: [OptDescr Opts]
+builtinOptions =
+  [ Option []     ["self-check"]  (NoArg  SelfCheck)             "Run self-checks"
+  , Option ['f']  ["frontend"]    (ReqArg Frontend   "FRONTEND") frontendHelp
+  , Option ['y']  ["config-file"] (ReqArg ConfigFile "PATH")     "Specify a folder containing a configuration yi.hs file"
+  , Option ['V']  ["version"]     (NoArg  Version)               "Show version information"
+  , Option ['h']  ["help"]        (NoArg  Help)                  "Show this help"
+  , Option []     ["debug"]       (NoArg  Debug)                 "Write debug information in a log file"
+  , Option ['l']  ["line"]        (ReqArg LineNo     "NUM")      "Start on line number"
+  , Option []     ["as"]          (ReqArg EditorNm   "EDITOR")   editorHelp
+  , Option []     ["ghc-option"]  (ReqArg GhcOption  "OPTION")   "Specify option to pass to ghc when compiling configuration file"
+  , Option [openInTabsShort] [openInTabsLong] (NoArg  OpenInTabs)  "Open files in tabs"
+  ] where frontendHelp = "Select frontend"
+          editorHelp   = "Start with editor keymap, where editor is one of:\n" ++ (intercalate ", " . fmap fst) editors
+
+convertCustomOption :: OptDescr YiOption -> OptDescr Opts
+convertCustomOption (Option short long desc help) = Option short long desc' help
+    where desc' = convertCustomArgDesc desc
+
+convertCustomArgDesc :: ArgDescr YiOption -> ArgDescr Opts
+convertCustomArgDesc (NoArg o) = NoArg (CustomNoArg o)
+convertCustomArgDesc (ReqArg f s) = ReqArg (CustomReqArg f) s
+convertCustomArgDesc (OptArg f s) = OptArg (CustomOptArg f) s
+
+customOptions :: Config -> [OptDescr Opts]
+customOptions = fmap convertCustomOption . view yiCustomOptions
+
+openInTabsShort :: Char
+openInTabsShort = 'p'
+
+openInTabsLong :: String
+openInTabsLong = "open-in-tabs"
+
+-- | usage string.
+usage :: [OptDescr Opts] -> T.Text
+usage opts = T.pack $ usageInfo "Usage: yi [option...] [file]" opts
+
+versinfo :: T.Text
+versinfo = "yi " <> T.pack (showVersion version)
+
+-- | Transform the config with options
+do_args :: Bool -> Config -> [String] -> Either OptionError (Config, ConsoleConfig)
+do_args ignoreUnknown cfg args = let options = customOptions cfg ++ builtinOptions in
+    case getOpt' (ReturnInOrder File) options args of
+        (os, [], [], []) -> handle options os
+        (os, _, u:us, []) -> if ignoreUnknown
+                                    then handle options os
+                                    else fail $ "unknown arguments: " ++ intercalate ", " (u:us)
+        (os, ex, ey, errs) -> fail (concat errs)
+    where
+        shouldOpenInTabs = ("--" ++ openInTabsLong) `elem` args
+                         || ('-':[openInTabsShort]) `elem` args
+        handle options os = foldM (getConfig options shouldOpenInTabs) (cfg, defaultConsoleConfig) (reverse os)
+
+-- | Update the default configuration based on a command-line option.
+getConfig :: [OptDescr Opts] -> Bool -> (Config, ConsoleConfig) -> Opts -> Either OptionError (Config, ConsoleConfig)
+getConfig options shouldOpenInTabs (cfg, cfgcon) opt =
+    case opt of
+      Frontend _    -> fail "Panic: frontend not found"
+      Help          -> Left $ OptionError (usage options) ExitSuccess
+      Version       -> Left $ OptionError versinfo ExitSuccess
+      Debug         -> return (cfg { debugMode = True }, cfgcon)
+      LineNo l      -> case startActions cfg of
+                         x : xs -> return (cfg { startActions = x:makeAction (gotoLn (read l)):xs }, cfgcon)
+                         []     -> fail "The `-l' option must come after a file argument"
+
+      File filename -> if shouldOpenInTabs && not (null (startActions cfg)) then
+                         prependActions [YiA $ openNewFile filename, EditorA newTabE]
+                       else
+                         prependAction $ openNewFile filename
+
+      EditorNm emul -> case lookup (fmap toLower emul) editors of
+             Just modifyCfg -> return (modifyCfg cfg, cfgcon)
+             Nothing -> fail $ "Unknown emulation: " ++ show emul
+      GhcOption ghcOpt -> return (cfg, cfgcon { ghcOptions = ghcOptions cfgcon ++ [ghcOpt] })
+      ConfigFile f -> return (cfg, cfgcon { userConfigDir = return f })
+      CustomNoArg o -> do
+          cfg' <- o cfg
+          return (cfg', cfgcon)
+      CustomReqArg f s -> do
+          cfg' <- f s cfg
+          return (cfg', cfgcon)
+      CustomOptArg f s -> do
+          cfg' <- f s cfg
+          return (cfg', cfgcon)
+      _ -> return (cfg, cfgcon)
+  where
+    prependActions as = return (cfg { startActions = fmap makeAction as ++ startActions cfg }, cfgcon)
+    prependAction a = return (cfg { startActions = makeAction a : startActions cfg}, cfgcon)
+
+-- ---------------------------------------------------------------------
+-- | Static main. This is the front end to the statically linked
+-- application, and the real front end, in a sense. 'dynamic_main' calls
+-- this after setting preferences passed from the boot loader.
+--
+main :: (Config, ConsoleConfig) -> Maybe Editor -> IO ()
+main (cfg, _cfgcon) state = do
+  when (debugMode cfg) $ initDebug ".yi.dbg"
+  startEditor cfg state
diff --git a/src/Yi/MiniBuffer.hs b/src/Yi/MiniBuffer.hs
new file mode 100644
--- /dev/null
+++ b/src/Yi/MiniBuffer.hs
@@ -0,0 +1,438 @@
+{-# LANGUAGE DeriveDataTypeable         #-}
+{-# LANGUAGE EmptyDataDecls             #-}
+{-# LANGUAGE FlexibleInstances          #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE LambdaCase                 #-}
+{-# LANGUAGE MultiParamTypeClasses      #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE ScopedTypeVariables        #-}
+{-# LANGUAGE TypeOperators              #-}
+{-# LANGUAGE TypeSynonymInstances       #-}
+{-# LANGUAGE UndecidableInstances       #-}
+{-# OPTIONS_HADDOCK show-extensions #-}
+
+-- |
+-- Module      :  Yi.Minibuffer
+-- License     :  GPL-2
+-- Maintainer  :  yi-devel@googlegroups.com
+-- Stability   :  experimental
+-- Portability :  portable
+--
+-- Functions working with the minibuffer.
+
+module Yi.MiniBuffer ( spawnMinibufferE, withMinibufferFree, withMinibuffer
+                     , withMinibufferGen, withMinibufferFin, noHint
+                     , noPossibilities, mkCompleteFn, simpleComplete
+                     , infixComplete, infixComplete', anyModeByName
+                     , getAllModeNames, matchingBufferNames, anyModeByNameM
+                     , anyModeName, (:::)(..), LineNumber, RegexTag
+                     , FilePatternTag, ToKill, CommandArguments(..)
+                     , commentRegion, promptingForBuffer, debugBufferContent
+                     ) where
+
+import           Control.Concurrent             (threadDelay)
+import           Lens.Micro.Platform                   (use, (%=))
+import           Control.Monad                  (forM, void, when, (<=<), (>=>))
+import           Data.Foldable                  (find, toList)
+import           Data.IORef                     (newIORef, readIORef, writeIORef)
+import qualified Data.List.PointedList.Circular as PL (find, insertRight)
+import           Data.Maybe                     (catMaybes, fromJust, fromMaybe)
+import           Data.Proxy                     (Proxy)
+import           Data.String                    (IsString)
+import qualified Data.Text                      as T (Text, append, head,
+                                                      isInfixOf, null, pack,
+                                                      snoc, unpack, words)
+import           Data.Typeable                  (Typeable)
+import           System.CanonicalizePath        (replaceShorthands)
+import           Yi.Buffer
+import           Yi.Completion
+import           Yi.Config                      (modeTable)
+import           Yi.Core                        (forkAction, runAction)
+import           Yi.Editor
+import           Yi.History                     (historyFinishGen, historyMove, historyStartGen)
+import           Yi.Keymap
+import           Yi.Keymap.Keys
+import           Yi.Monad                       (gets)
+import qualified Yi.Rope                        as R (YiString, fromText, toText)
+import           Yi.String                      (commonTPrefix)
+import           Yi.Style                       (defaultStyle)
+import           Yi.Utils                       (io)
+import           Yi.Window                      (bufkey)
+
+-- | Prints out the rope of the current buffer as-is to stdout.
+--
+-- The only way to stop it is to close the buffer in question which
+-- should free up the 'BufferRef'.
+debugBufferContent :: YiM ()
+debugBufferContent = promptingForBuffer "buffer to trace:"
+                     debugBufferContentUsing (\_ x -> x)
+
+debugBufferContentUsing :: BufferRef -> YiM ()
+debugBufferContentUsing b = do
+  mv <- io $ newIORef mempty
+  keepGoing <- io $ newIORef True
+  let delay = threadDelay 100000 >> readIORef keepGoing
+  void . forkAction delay NoNeedToRefresh $
+    findBuffer b >>= \case
+      Nothing -> io $ writeIORef keepGoing True
+      Just _ -> do
+        ns <- withGivenBuffer b elemsB :: YiM R.YiString
+        io $ readIORef mv >>= \c ->
+          when (c /= ns) (print ns >> void (writeIORef mv ns))
+
+-- | Prompts for a buffer name, turns it into a 'BufferRef' and passes
+-- it on to the handler function. Uses all known buffers for hinting.
+promptingForBuffer :: T.Text -- ^ Prompt
+                   -> (BufferRef -> YiM ()) -- ^ Handler
+                   -> ([BufferRef] -> [BufferRef] -> [BufferRef])
+                   -- ^ Hint pre-processor. It takes the list of open
+                   -- buffers and a list of all buffers, and should
+                   -- spit out all the buffers to possibly hint, in
+                   -- the wanted order. Note the hinter uses name
+                   -- prefix for filtering regardless of what you do
+                   -- here.
+                   -> YiM ()
+promptingForBuffer prompt act hh = do
+    openBufs <- fmap bufkey . toList <$> use windowsA
+    names <- withEditor $ do
+      bs <- toList . fmap bkey <$> getBufferStack
+      let choices = hh openBufs bs
+      prefix <- gets commonNamePrefix
+      forM choices $ \k ->
+        gets (shortIdentString (length prefix) . findBufferWith k)
+    withMinibufferFin prompt names (withEditor . getBufferWithName >=> act)
+
+-- | Prompts the user for comment syntax to use for the current mode.
+commentRegion :: YiM ()
+commentRegion =
+  withCurrentBuffer (gets $ withMode0 modeToggleCommentSelection) >>= \case
+    Nothing ->
+      withMinibufferFree "No comment syntax is defined. Use: " $ \cString ->
+        withCurrentBuffer $ do
+          let toggle = toggleCommentB (R.fromText cString)
+          void toggle
+          modifyMode $ \x -> x { modeToggleCommentSelection = Just toggle }
+    Just b -> withCurrentBuffer b
+
+-- | Open a minibuffer window with the given prompt and keymap
+-- The third argument is an action to perform after the minibuffer
+-- is opened such as move to the first occurence of a searched for
+-- string. If you don't need this just supply @return ()@
+spawnMinibufferE :: T.Text -> KeymapEndo -> EditorM BufferRef
+spawnMinibufferE prompt kmMod = do
+  b <- stringToNewBuffer (MemBuffer prompt) mempty
+  -- Now create the minibuffer keymap and switch to the minibuffer window
+  withGivenBuffer b $
+    modifyMode $ \m -> m { modeKeymap = \kms -> kms { topKeymap = kmMod (insertKeymap kms)
+                                                    } }
+  -- The minibuffer window must not be moved from the position newWindowE places it!
+  -- First: This way the minibuffer is just below the window that was in focus when
+  -- the minibuffer was spawned. This clearly indicates what window is the target of
+  -- some actions. Such as searching or the :w (save) command in the Vim keymap.
+  -- Second: The users of the minibuffer expect the window and buffer that was in
+  -- focus when the minibuffer was spawned to be in focus when the minibuffer is closed
+  -- Given that window focus works as follows:
+  --    - The new window is broguht into focus.
+  --    - The previous window in focus is to the left of the new window in the window
+  --    set list.
+  --    - When a window is deleted and is in focus then the window to the left is brought
+  --    into focus.
+  --
+  -- If the minibuffer is moved then when the minibuffer is deleted the window brought
+  -- into focus may not be the window that spawned the minibuffer.
+  w <- newWindowE True b
+  windowsA %= PL.insertRight w
+  return b
+
+-- | @withMinibuffer prompt completer act@: open a minibuffer with @prompt@. Once
+-- a string @s@ is obtained, run @act s@. @completer@ can be used to complete
+-- functions: it returns a list of possible matches.
+withMinibuffer :: T.Text -> (T.Text -> YiM [T.Text]) -> (T.Text -> YiM ()) -> YiM ()
+withMinibuffer prompt getPossibilities =
+  withMinibufferGen "" giveHint prompt completer (const $ return ())
+    where giveHint s = catMaybes . fmap (prefixMatch s) <$> getPossibilities s
+          completer = simpleComplete getPossibilities
+
+-- | Makes a completion function.
+mkCompleteFn :: (T.Text -> (T.Text -> Maybe T.Text)
+                 -> [T.Text] -> EditorM T.Text)
+                -- ^ List completion, such as 'completeInList'.
+                -> (T.Text -> T.Text -> Maybe T.Text)
+                -- ^ Matcher such as 'prefixMatch'
+                -> (T.Text -> YiM [T.Text])
+                -- ^ Function to fetch possibilites for completion.
+                -> T.Text
+                -- ^ Input to try and complete against
+                -> YiM T.Text
+mkCompleteFn completeInListFn match getPossibilities s = do
+              possibles <- getPossibilities s
+              withEditor $ completeInListFn s (match s) possibles
+
+simpleComplete :: (T.Text -> YiM [T.Text]) -> T.Text -> YiM T.Text
+simpleComplete = mkCompleteFn completeInList prefixMatch
+
+infixComplete' :: Bool -> (T.Text -> YiM [T.Text]) -> T.Text -> YiM T.Text
+infixComplete' caseSensitive = mkCompleteFn completeInList' $ containsMatch' caseSensitive
+
+infixComplete :: (T.Text -> YiM [T.Text]) -> T.Text -> YiM T.Text
+infixComplete = infixComplete' True
+
+-- | Hint function that does nothing, for use with @'withMinibufferGen'@
+noHint :: a -> YiM [a]
+noHint = const $ return []
+
+noPossibilities :: String -> YiM [ String ]
+noPossibilities _s = return []
+
+-- | @withMinibufferFree prompt act@:
+-- Simple version of @'withMinibufferGen'@
+withMinibufferFree :: T.Text -> (T.Text -> YiM ()) -> YiM ()
+withMinibufferFree prompt = withMinibufferGen "" noHint prompt
+                            return (const $ return ())
+
+-- | @withMinibufferGen proposal getHint prompt completer onTyping act@:
+-- open a minibuffer with @prompt@, and initial content @proposal@. Once
+-- a string @s@ is obtained, run @act s@. @completer@ can be used to
+-- complete inputs by returning an incrementally better match, and
+-- getHint can give an immediate feedback to the user on the current
+-- input.
+--
+-- @on Typing@ is an extra action which will fire with every user
+-- key-press and receives minibuffer contents. Use something like
+-- @const $ return ()@ if you don't need this.
+withMinibufferGen :: T.Text -> (T.Text -> YiM [T.Text]) -> T.Text
+                  -> (T.Text -> YiM T.Text) -> (T.Text -> YiM ())
+                  -> (T.Text -> YiM ()) -> YiM ()
+withMinibufferGen proposal getHint prompt completer onTyping act = do
+  initialBuffer <- gets currentBuffer
+  initialWindow <- use currentWindowA
+  let innerAction :: YiM ()
+      -- ^ Read contents of current buffer (which should be the minibuffer), and
+      -- apply it to the desired action
+      closeMinibuffer = closeBufferAndWindowE >>
+                        windowsA %= fromJust . PL.find initialWindow
+      showMatchings = showMatchingsOf . R.toText =<< withCurrentBuffer elemsB
+      showMatchingsOf userInput =
+        printStatus =<< withDefaultStyle <$> getHint userInput
+      withDefaultStyle msg = (msg, defaultStyle)
+      typing = onTyping . R.toText =<< withCurrentBuffer elemsB
+
+      innerAction = do
+        lineString <- withEditor $ do
+          let bufToText = R.toText <$> withCurrentBuffer elemsB
+          historyFinishGen prompt bufToText
+          lineString <- bufToText
+          closeMinibuffer
+          switchToBufferE initialBuffer
+          -- The above ensures that the action is performed on the buffer
+          -- that originated the minibuffer.
+          return lineString
+        act lineString
+
+      up   = historyMove prompt 1
+      down = historyMove prompt (-1)
+
+      rebindings =
+        choice [oneOf [spec KEnter, ctrl $ char 'm'] >>! innerAction,
+                oneOf [spec KUp,    meta $ char 'p'] >>! up,
+                oneOf [spec KDown,  meta $ char 'n'] >>! down,
+                oneOf [spec KTab,   ctrl $ char 'i']
+                  >>! completionFunction completer >>! showMatchings,
+                ctrl (char 'g')                     ?>>! closeMinibuffer]
+
+  showMatchingsOf ""
+  withEditor $ do
+      historyStartGen prompt
+      void $ spawnMinibufferE (prompt `T.snoc` ' ')
+        (\bindings -> rebindings <|| (bindings >> write showMatchings
+                                      >> write typing))
+      withCurrentBuffer . replaceBufferContent . R.fromText
+        $ replaceShorthands proposal
+
+-- | Open a minibuffer, given a finite number of suggestions.
+withMinibufferFin :: T.Text -> [T.Text] -> (T.Text -> YiM ()) -> YiM ()
+withMinibufferFin prompt possibilities act
+    = withMinibufferGen "" hinter prompt completer
+      (const $ return ()) (act . best)
+  where
+    -- The function for returning the hints provided to the user underneath
+    -- the input, basically all those that currently match.
+    hinter s = return $ match s
+    -- All those which currently match.
+    match s = filter (s `T.isInfixOf`) possibilities
+
+    -- The best match from the list of matches
+    -- If the string matches completely then we take that
+    -- otherwise we just take the first match.
+    best s
+      | s `elem` matches = s
+      | null matches       = s
+      | otherwise          = head matches
+      where matches = match s
+
+    -- We still want "TAB" to complete even though the user could just
+    -- press return with an incomplete possibility. The reason is we
+    -- may have for example two possibilities which share a long
+    -- prefix and hence we wish to press tab to complete up to the
+    -- point at which they differ.
+    completer s = return $ fromMaybe s $ commonTPrefix $ catMaybes (infixUptoEndMatch s <$> possibilities)
+
+-- | TODO: decide whether we should be keeping 'T.Text' here or moving
+-- to 'YiString'.
+completionFunction :: (T.Text -> YiM T.Text) -> YiM ()
+completionFunction f = do
+  p <- withCurrentBuffer pointB
+  let r = mkRegion 0 p
+  text <- withCurrentBuffer $ readRegionB r
+  compl <- R.fromText <$> f (R.toText text)
+
+  -- it's important to do this before removing the text, so if the
+  -- completion function raises an exception, we don't delete the
+  -- buffer contents.
+  withCurrentBuffer $ replaceRegionB r compl
+
+class Promptable a where
+    getPromptedValue :: T.Text -> YiM a
+    getPrompt :: Proxy a -> T.Text
+    getMinibuffer :: Proxy a -> T.Text -> (T.Text -> YiM ()) -> YiM ()
+    getMinibuffer _ = withMinibufferFree
+
+doPrompt :: forall a. Promptable a => (a -> YiM ()) -> YiM ()
+doPrompt act = getMinibuffer witness (getPrompt witness `T.append` ":") (act <=< getPromptedValue)
+  where
+    witness = undefined
+    witness :: Proxy a
+
+instance Promptable String where
+    getPromptedValue = return . T.unpack
+    getPrompt _ = "String"
+
+instance Promptable Char where
+    getPromptedValue x = if T.null x
+                         then error "Please supply a character."
+                         else return $ T.head x
+    getPrompt _ = "Char"
+
+instance Promptable Int where
+    getPromptedValue = return . read . T.unpack
+    getPrompt _ = "Integer"
+
+instance Promptable T.Text where
+  getPromptedValue = return
+  getPrompt _ = "Text"
+
+instance Promptable R.YiString where
+  getPromptedValue = return . R.fromText
+  getPrompt _ = "YiString"
+
+-- helper functions:
+getPromptedValueList :: [(T.Text, a)] -> T.Text -> YiM a
+getPromptedValueList vs s = maybe (error "Invalid choice") return (lookup s vs)
+
+getMinibufferList :: [(T.Text, a)] -> Proxy a -> T.Text
+                  -> (T.Text -> YiM ()) -> YiM ()
+getMinibufferList vs _ prompt = withMinibufferFin prompt (fmap fst vs)
+
+enumAll :: (Enum a, Bounded a, Show a) => [(T.Text, a)]
+enumAll = fmap (\v -> (T.pack $ show v, v)) [minBound..]
+
+instance Promptable Direction where
+    getPromptedValue = getPromptedValueList enumAll
+    getPrompt _ = "Direction"
+    getMinibuffer = getMinibufferList enumAll
+
+textUnits :: [(T.Text, TextUnit)]
+textUnits =
+       [("Character", Character),
+        ("Document", Document),
+        ("Line", Line),
+        ("Paragraph", unitParagraph),
+        ("Word", unitWord),
+        ("ViWord", unitViWord)
+       ]
+
+instance Promptable TextUnit where
+    getPromptedValue = getPromptedValueList textUnits
+    getPrompt _ = "Unit"
+    getMinibuffer = getMinibufferList textUnits
+
+instance Promptable Point where
+    getPromptedValue s = Point <$> getPromptedValue s
+    getPrompt _ = "Point"
+
+anyModeName :: AnyMode -> T.Text
+anyModeName (AnyMode m) = modeName m
+
+-- TODO: Better name
+anyModeByNameM :: T.Text -> YiM (Maybe AnyMode)
+anyModeByNameM n = find ((n==) . anyModeName) . modeTable <$> askCfg
+
+anyModeByName :: T.Text -> YiM AnyMode
+anyModeByName n = anyModeByNameM n >>= \case
+  Nothing -> fail $ "anyModeByName: no such mode: " ++ T.unpack n
+  Just m  -> return m
+
+getAllModeNames :: YiM [T.Text]
+getAllModeNames = fmap anyModeName . modeTable <$> askCfg
+
+instance Promptable AnyMode where
+    getPrompt _ = "Mode"
+    getPromptedValue = anyModeByName
+    getMinibuffer _ prompt act = do
+      names <- getAllModeNames
+      withMinibufferFin prompt names act
+
+instance Promptable BufferRef where
+    getPrompt _ = "Buffer"
+    getPromptedValue = getBufferWithNameOrCurrent
+    getMinibuffer _ prompt act = do
+      bufs <- matchingBufferNames
+      withMinibufferFin prompt bufs act
+
+-- | Returns all the buffer names
+matchingBufferNames :: YiM [T.Text]
+matchingBufferNames = withEditor $ do
+  p <- gets commonNamePrefix
+  bs <- gets bufferSet
+  return $ fmap (shortIdentString $ length p) bs
+
+instance (YiAction a x, Promptable r) => YiAction (r -> a) x where
+    makeAction f = YiA $ doPrompt (runAction . makeAction . f)
+
+-- | Tag a type with a documentation
+newtype (:::) t doc = Doc {fromDoc :: t} deriving (Eq, Typeable, Num, IsString)
+
+instance Show x => Show (x ::: t) where
+    show (Doc d) = show d
+
+instance (DocType doc, Promptable t) => Promptable (t ::: doc) where
+    getPrompt _ = typeGetPrompt (error "typeGetPrompt should not enter its argument" :: doc)
+    getPromptedValue x = Doc <$> getPromptedValue x
+
+class DocType t where
+    -- | What to prompt the user when asked this type?
+    typeGetPrompt :: t -> T.Text
+
+data LineNumber
+instance DocType LineNumber where
+    typeGetPrompt _ = "Line"
+
+data ToKill
+instance DocType ToKill where
+    typeGetPrompt _ = "kill buffer"
+
+
+data RegexTag deriving Typeable
+instance DocType RegexTag where
+    typeGetPrompt _ = "Regex"
+
+data FilePatternTag deriving Typeable
+instance DocType FilePatternTag where
+    typeGetPrompt _ = "File pattern"
+
+newtype CommandArguments = CommandArguments [T.Text]
+    deriving (Show, Eq, Typeable)
+
+instance Promptable CommandArguments where
+    getPromptedValue = return . CommandArguments . T.words
+    getPrompt _ = "Command arguments"
diff --git a/src/Yi/Misc.hs b/src/Yi/Misc.hs
new file mode 100644
--- /dev/null
+++ b/src/Yi/Misc.hs
@@ -0,0 +1,214 @@
+{-# LANGUAGE LambdaCase        #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE TupleSections     #-}
+{-# LANGUAGE TypeOperators     #-}
+{-# OPTIONS_HADDOCK show-extensions #-}
+
+-- |
+-- Module      :  Yi.Misc
+-- License     :  GPL-2
+-- Maintainer  :  yi-devel@googlegroups.com
+-- Stability   :  experimental
+-- Portability :  portable
+--
+-- Various high-level functions to further classify.
+
+module Yi.Misc ( getAppropriateFiles, getFolder, cd, pwd, matchingFileNames
+               , rot13Char, placeMark, selectAll, adjBlock, adjIndent
+               , promptFile , promptFileChangingHints, matchFile, completeFile
+               , printFileInfoE, debugBufferContent
+               ) where
+
+import           Control.Monad           (filterM, (>=>))
+import           Control.Monad.Base      (liftBase)
+import           Data.Char               (chr, isAlpha, isLower, isUpper, ord)
+import           Data.List               ((\\))
+import           Data.Maybe              (isNothing)
+import qualified Data.Text               as T (Text, append, concat, isPrefixOf,
+                                               pack, stripPrefix, unpack)
+import           System.CanonicalizePath (canonicalizePath, replaceShorthands, replaceShorthands)
+import           System.Directory        (doesDirectoryExist,
+                                          getCurrentDirectory,
+                                          getDirectoryContents,
+                                          setCurrentDirectory)
+import           System.Environment      (lookupEnv)
+import           System.FilePath         (addTrailingPathSeparator,
+                                          hasTrailingPathSeparator,
+                                          takeDirectory, takeFileName, (</>))
+import           System.FriendlyPath     (expandTilda, isAbsolute')
+import           Yi.Buffer
+import           Yi.Completion           (completeInList')
+import           Yi.Editor               (EditorM, printMsg, withCurrentBuffer)
+import           Yi.Keymap               (YiM)
+import           Yi.MiniBuffer           (debugBufferContent, mkCompleteFn, withMinibufferGen)
+import           Yi.Monad                (gets)
+import qualified Yi.Rope                 as R (fromText)
+import           Yi.Utils                (io)
+
+-- | Given a possible starting path (which if not given defaults to
+-- the current directory) and a fragment of a path we find all files
+-- within the given (or current) directory which can complete the
+-- given path fragment. We return a pair of both directory plus the
+-- filenames on their own that is without their directories. The
+-- reason for this is that if we return all of the filenames then we
+-- get a 'hint' which is way too long to be particularly useful.
+getAppropriateFiles :: Maybe T.Text -> T.Text -> YiM (T.Text, [ T.Text ])
+getAppropriateFiles start s' = do
+  curDir <- case start of
+    Nothing -> do bufferPath <- withCurrentBuffer $ gets file
+                  liftBase $ getFolder bufferPath
+    Just path -> return $ T.unpack path
+  let s = T.unpack $ replaceShorthands s'
+      sDir = if hasTrailingPathSeparator s then s else takeDirectory s
+      searchDir
+        | null sDir = curDir
+        | isAbsolute' sDir = sDir
+        | otherwise = curDir </> sDir
+  searchDir' <- liftBase $ expandTilda searchDir
+  let fixTrailingPathSeparator f = do
+        isDir <- doesDirectoryExist (searchDir' </> f)
+        return . T.pack $ if isDir then addTrailingPathSeparator f else f
+
+  files <- liftBase $ getDirectoryContents searchDir'
+
+  -- Remove the two standard current-dir and parent-dir as we do not
+  -- need to complete or hint about these as they are known by users.
+  let files' = files \\ [ ".", ".." ]
+  fs <- liftBase $ mapM fixTrailingPathSeparator files'
+  let matching = filter (T.isPrefixOf . T.pack $ takeFileName s) fs
+  return (T.pack sDir, matching)
+
+-- | Given a path, trim the file name bit if it exists.  If no path
+--   given, return current directory.
+getFolder :: Maybe String -> IO String
+getFolder Nothing     = getCurrentDirectory
+getFolder (Just path) = do
+  isDir <- doesDirectoryExist path
+  let dir = if isDir then path else takeDirectory path
+  if null dir then getCurrentDirectory else return dir
+
+
+-- | Given a possible path and a prefix, return matching file names.
+matchingFileNames :: Maybe T.Text -> T.Text -> YiM [T.Text]
+matchingFileNames start s = do
+  (sDir, files) <- getAppropriateFiles start s
+
+  -- There is one common case when we don't need to prepend @sDir@ to @files@:
+  --
+  -- Suppose user just wants to edit a file "foobar" in current directory
+  -- and inputs ":e foo<Tab>"
+  --
+  -- @sDir@ in this case equals to "." and "foo" would not be
+  -- a prefix of ("." </> "foobar"), resulting in a failed completion
+  --
+  -- However, if user inputs ":e ./foo<Tab>", we need to prepend @sDir@ to @files@
+  let results = if isNothing start && sDir == "." && not ("./" `T.isPrefixOf` s)
+                   then files
+                   else fmap (T.pack . (T.unpack sDir </>) . T.unpack) files
+
+  return results
+
+-- | Place mark at current point. If there's an existing mark at point
+-- already, deactivate mark.
+placeMark :: BufferM ()
+placeMark = (==) <$> pointB <*> getSelectionMarkPointB >>= \case
+  True -> setVisibleSelection False
+  False -> setVisibleSelection True >> pointB >>= setSelectionMarkPointB
+
+-- | Select the contents of the whole buffer
+selectAll :: BufferM ()
+selectAll = botB >> placeMark >> topB >> setVisibleSelection True
+
+adjBlock :: Int -> BufferM ()
+adjBlock x = withSyntaxB' (\m s -> modeAdjustBlock m s x)
+
+-- | A simple wrapper to adjust the current indentation using
+-- the mode specific indentation function but according to the
+-- given indent behaviour.
+adjIndent :: IndentBehaviour -> BufferM ()
+adjIndent ib = withSyntaxB' (\m s -> modeIndent m s ib)
+
+-- | Generic emacs style prompt file action. Takes a @prompt@ and a continuation
+-- @act@ and prompts the user with file hints.
+promptFile :: T.Text -> (T.Text -> YiM ()) -> YiM ()
+promptFile prompt act = promptFileChangingHints prompt (const return) act
+
+-- | As 'promptFile' but additionally allows the caller to transform
+-- the list of hints arbitrarily, such as only showing directories.
+promptFileChangingHints :: T.Text -- ^ Prompt
+                        -> (T.Text -> [T.Text] -> YiM [T.Text])
+                        -- ^ Hint transformer: current path, generated hints
+                        -> (T.Text -> YiM ()) -- ^ Action over choice
+                        -> YiM ()
+promptFileChangingHints prompt ht act = do
+  maybePath <- withCurrentBuffer $ gets file
+  startPath <- T.pack . addTrailingPathSeparator
+               <$> liftBase (canonicalizePath =<< getFolder maybePath)
+  -- TODO: Just call withMinibuffer
+  withMinibufferGen startPath (\x -> findFileHint startPath x >>= ht x) prompt
+    (completeFile startPath) showCanon (act . replaceShorthands)
+  where
+    showCanon = withCurrentBuffer . replaceBufferContent . R.fromText . replaceShorthands
+
+matchFile :: T.Text -> T.Text -> Maybe T.Text
+matchFile path proposedCompletion =
+  let realPath = replaceShorthands path
+  in T.append path <$> T.stripPrefix realPath proposedCompletion
+
+completeFile :: T.Text -> T.Text -> YiM T.Text
+completeFile startPath =
+  mkCompleteFn completeInList' matchFile $ matchingFileNames (Just startPath)
+
+-- | For use as the hint when opening a file using the minibuffer. We
+-- essentially return all the files in the given directory which have
+-- the given prefix.
+findFileHint :: T.Text -> T.Text -> YiM [T.Text]
+findFileHint startPath s = snd <$> getAppropriateFiles (Just startPath) s
+
+onCharLetterCode :: (Int -> Int) -> Char -> Char
+onCharLetterCode f c | isAlpha c = chr (f (ord c - a) `mod` 26 + a)
+                     | otherwise = c
+                     where a | isUpper c = ord 'A'
+                             | isLower c = ord 'a'
+                             | otherwise = undefined
+
+-- | Like @M-x cd@, it changes the current working directory. Mighty
+-- useful when we don't start Yi from the project directory or want to
+-- switch projects, as many tools only use the current working
+-- directory.
+cd :: YiM ()
+cd = promptFileChangingHints "switch directory to:" dirs $ \path ->
+  io $ getFolder (Just $ T.unpack path) >>= clean . T.pack
+       >>= System.Directory.setCurrentDirectory . addTrailingPathSeparator
+  where
+     replaceHome p@('~':'/':xs) = lookupEnv "HOME" >>= return . \case
+       Nothing -> p
+       Just h -> h </> xs
+     replaceHome p = return p
+     clean = replaceHome . T.unpack . replaceShorthands >=> canonicalizePath
+
+     x <//> y = T.pack $ takeDirectory (T.unpack x) </> T.unpack y
+
+     dirs :: T.Text -> [T.Text] -> YiM [T.Text]
+     dirs x xs = do
+       xsc <- io $ mapM (\y -> (,y) <$> clean (x <//> y)) xs
+       filterM (io . doesDirectoryExist . fst) xsc >>= return . map snd
+
+-- | Shows current working directory. Also see 'cd'.
+pwd :: YiM ()
+pwd = io getCurrentDirectory >>= printMsg . T.pack
+
+rot13Char :: Char -> Char
+rot13Char = onCharLetterCode (+13)
+
+printFileInfoE :: EditorM ()
+printFileInfoE = printMsg . showBufInfo =<< withCurrentBuffer bufInfoB
+    where showBufInfo :: BufferFileInfo -> T.Text
+          showBufInfo bufInfo = T.concat
+            [ T.pack $ bufInfoFileName bufInfo
+            , " Line "
+            , T.pack . show $ bufInfoLineNo bufInfo
+            , " ["
+            , bufInfoPercent bufInfo
+            , "]"
+            ]
diff --git a/src/Yi/Mode/Common.hs b/src/Yi/Mode/Common.hs
new file mode 100644
--- /dev/null
+++ b/src/Yi/Mode/Common.hs
@@ -0,0 +1,168 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# OPTIONS_HADDOCK show-extensions #-}
+
+-- |
+-- Module      :  Yi.Mode.Common
+-- License     :  GPL-2
+-- Maintainer  :  yi-devel@googlegroups.com
+-- Stability   :  experimental
+-- Portability :  portable
+--
+-- Common functions used by modes.
+
+module Yi.Mode.Common (TokenBasedMode, fundamentalMode,
+                 anyExtension, extensionOrContentsMatch, 
+                 linearSyntaxMode, hookModes, 
+                 applyModeHooks, lookupMode, styleMode,
+                 extensionMatches, shebangParser
+                ) where
+
+import           Lens.Micro.Platform  ((%~), (&), (.~), (^.))
+import           Control.Applicative  ((<|>))
+import           Control.Monad        (void)
+import qualified Data.Attoparsec.Text as P
+import           Data.Maybe           (fromMaybe, isJust)
+import           System.FilePath      (takeExtension)
+import qualified Data.Text            as T (Text)
+
+import           Yi.Buffer
+import qualified Yi.IncrementalParse  as IncrParser (scanner)
+import           Yi.Keymap            (YiM)
+import           Yi.Lexer.Alex
+import           Yi.MiniBuffer        (anyModeByNameM)
+import qualified Yi.Rope              as R (YiString, toText)
+import           Yi.Search            (makeSimpleSearch)
+import           Yi.Style             (StyleName)
+import           Yi.Syntax            (ExtHL (ExtHL))
+import           Yi.Syntax.Driver     (mkHighlighter)
+import           Yi.Syntax.OnlineTree (Tree, manyToks)
+import           Yi.Syntax.Tree       (tokenBasedStrokes)
+
+type TokenBasedMode tok = Mode (Tree (Tok tok))
+
+-- TODO: Move this mode to it's own module
+-- | The only built in mode of yi
+fundamentalMode :: Mode syntax
+fundamentalMode = emptyMode
+  { modeName = "fundamental"
+  , modeApplies = modeAlwaysApplies
+  , modeIndent = const autoIndentB
+  , modePrettify = const fillParagraph
+  , modeGotoDeclaration = do
+       currentPoint <- pointB
+       currentWord <- readCurrentWordB
+       currentWordBeginningPoint <- regionStart <$> regionOfB unitWord
+       _ <- gotoLn 0
+       word <- return $ makeSimpleSearch currentWord
+       searchResults <- regexB Forward word
+       case searchResults of
+           (declarationRegion : _) -> do
+               searchPoint <- return $ regionStart declarationRegion
+               if currentWordBeginningPoint /= searchPoint
+               then moveTo searchPoint
+               else moveTo currentPoint
+           [] -> moveTo currentPoint
+  }
+
+-- | Creates a 'TokenBasedMode' from a 'Lexer' and a function that
+-- turns tokens into 'StyleName'.
+linearSyntaxMode' :: Show (l s)
+                  => Lexer l s (Tok t) i
+                  -> (t -> StyleName)
+                  -> TokenBasedMode t
+linearSyntaxMode' scanToken tts = fundamentalMode
+  & modeHLA .~ ExtHL (mkHighlighter $ IncrParser.scanner manyToks . lexer)
+  & modeGetStrokesA .~ tokenBasedStrokes tokenToStroke
+  where
+    tokenToStroke = fmap tts . tokToSpan
+    lexer = lexScanner scanToken
+
+-- | Specialised version of 'linearSyntaxMode'' for the common case,
+-- wrapping up into a 'Lexer' with 'commonLexer'.
+linearSyntaxMode :: Show s => s -- ^ Starting state
+                 -> TokenLexer AlexState s (Tok t) AlexInput
+                 -> (t -> StyleName)
+                 -> TokenBasedMode t
+linearSyntaxMode initSt scanToken =
+  linearSyntaxMode' (commonLexer scanToken initSt)
+
+styleMode :: Show (l s) => StyleLexer l s t i
+          -> TokenBasedMode t
+styleMode l = linearSyntaxMode' (l ^. styleLexer) (l ^. tokenToStyle)
+
+-- | Determines if the file's extension is one of the extensions in the list.
+extensionMatches :: [String]
+                 -> FilePath
+                 -> Bool
+extensionMatches extensions fileName = extension `elem` extensions'
+    where extension = takeExtension fileName
+          extensions' = ['.' : ext | ext <- extensions]
+
+-- | When applied to an extensions list, creates a 'Mode.modeApplies' function.
+anyExtension :: [String] -- ^ List of extensions
+             -> FilePath -- ^ Path to compare against
+             -> a        -- ^ File contents. Currently unused but see
+                         -- 'extensionOrContentsMatch'.
+             -> Bool
+anyExtension extensions fileName _contents
+    = extensionMatches extensions fileName
+
+-- | When applied to an extensions list and regular expression pattern, creates
+-- a 'Mode.modeApplies' function.
+extensionOrContentsMatch :: [String] -> P.Parser () -> FilePath -> R.YiString -> Bool
+extensionOrContentsMatch extensions parser fileName contents
+    = extensionMatches extensions fileName || m
+    where
+        m = case P.parseOnly parser $ R.toText contents of
+              Left _ -> False
+              Right _ -> True
+
+{- | Generate a parser for shebang patterns
+the generated parser will match only if the shebang is at the start of a line
+
+==== __Examples__
+
+> shebangParser "runhaskell"
+
+generates a parser that matches "#!\/usr\/bin\/env runhaskell\\n"
+(but also "djsjfaj\\n\\n\\n\\r\\n#!    \/usr\/bin\/env       runhaskell       \\ndkasfkda\\n\\r\\nkasfaj")
+
+__Note:__ You can get @("runhaskell" :: Parser String)@ by using the OverloadedStrings extension
+
+> shebangParser "python"
+
+generates a parser that matches "#!\/usr\/bin\/env python\\n"
+
+__Note:__ it doesn't match "#!\/usr\/bin\/env python2\\n" (that's why the newline is required)
+
+It is also possible to use more complex parsers:
+
+> shebangParser ("python" *> ("2" <|> "3" <|> ""))
+
+generates a parser that matches any of:
+
+  * "#!\/usr\/bin\/env python\\n"
+  * "#!\/usr\/bin\/env python2\\n"
+  * "#!\/usr\/bin\/env python3\\n"
+-}
+shebangParser :: P.Parser a -> P.Parser ()
+shebangParser p = void p'
+  where
+    p' = "#!" *> P.skipWhile (== ' ') *> "/usr/bin/env " *> P.skipWhile (== ' ') *> p *> P.skipWhile (== ' ') *> P.endOfLine
+     <|> P.skip (const True) *> P.skipWhile (not . P.isEndOfLine) *> P.skipWhile P.isEndOfLine *> p'
+
+-- | Adds a hook to all matching hooks in a list
+hookModes :: (AnyMode -> Bool) -> BufferM () -> [AnyMode] -> [AnyMode]
+hookModes p h = map $ \am@(AnyMode m) ->
+  if p am then AnyMode (m & modeOnLoadA %~ (>> h)) else am
+
+-- | Apply a list of mode hooks to a list of AnyModes
+applyModeHooks :: [(AnyMode -> Bool, BufferM ())] -> [AnyMode] -> [AnyMode]
+applyModeHooks hs ms = flip map ms $ \am -> case filter (($ am) . fst) hs of
+    [] -> am
+    ls -> onMode (modeOnLoadA %~ \x -> foldr ((>>) . snd) x ls) am
+
+-- | Check whether a mode of the same name is already in modeTable and
+-- returns the original mode, if it isn't the case.
+lookupMode :: AnyMode -> YiM AnyMode
+lookupMode am@(AnyMode m) = fromMaybe am <$> anyModeByNameM (modeName m)
diff --git a/src/Yi/Mode/Compilation.hs b/src/Yi/Mode/Compilation.hs
new file mode 100644
--- /dev/null
+++ b/src/Yi/Mode/Compilation.hs
@@ -0,0 +1,40 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# OPTIONS_HADDOCK show-extensions #-}
+
+-- |
+-- Module      :  Yi.Mode.Compilation
+-- License     :  GPL-2
+-- Maintainer  :  yi-devel@googlegroups.com
+-- Stability   :  experimental
+-- Portability :  portable
+--
+-- A 'Mode' for working with buffers showing the results of compilations.
+module Yi.Mode.Compilation where
+
+import           Lens.Micro.Platform           ((%~), (&), (.~))
+import           Data.Text            ()
+import           Yi.Buffer
+import           Yi.Core              (withSyntax)
+import           Yi.Editor            (shiftOtherWindow, withCurrentBuffer)
+import           Yi.File              (openingNewFile)
+import           Yi.Keymap            (Action (YiA), topKeymapA)
+import           Yi.Keymap.Keys       (Key (KEnter), spec, (<||), (?>>!))
+import           Yi.Lexer.Alex        (Posn (..), Tok (..))
+import qualified Yi.Lexer.Compilation as Compilation (Token (Report), lexer)
+import           Yi.Mode.Common       (TokenBasedMode, styleMode)
+import qualified Yi.Syntax.OnlineTree as OnlineTree (tokAtOrBefore)
+
+mode :: TokenBasedMode Compilation.Token
+mode = styleMode Compilation.lexer
+  & modeAppliesA .~ modeNeverApplies
+  & modeNameA .~ "compilation"
+  & modeKeymapA .~ topKeymapA %~ ((spec KEnter ?>>! withSyntax modeFollow) <||)
+  & modeFollowA .~ YiA . follow
+  where
+    follow errs = withCurrentBuffer pointB >>= \point ->
+      case OnlineTree.tokAtOrBefore point errs of
+        Just t@Tok {tokT = Compilation.Report filename line col _} -> do
+          withCurrentBuffer . moveTo . posnOfs $ tokPosn t
+          shiftOtherWindow
+          openingNewFile filename $ gotoLn line >> rightN col
+        _ -> return ()
diff --git a/src/Yi/Mode/Interactive.hs b/src/Yi/Mode/Interactive.hs
new file mode 100644
--- /dev/null
+++ b/src/Yi/Mode/Interactive.hs
@@ -0,0 +1,135 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# OPTIONS_HADDOCK show-extensions #-}
+
+-- |
+-- Module      :  Yi.Mode.Interactive
+-- License     :  GPL-2
+-- Maintainer  :  yi-devel@googlegroups.com
+-- Stability   :  experimental
+-- Portability :  portable
+--
+-- Collection of 'Mode's for working with Haskell.
+
+module Yi.Mode.Interactive where
+
+import           Control.Concurrent   (threadDelay)
+import           Lens.Micro.Platform           (use, (%~), (.=))
+import           Data.Monoid          ((<>))
+import qualified Data.Text            as T (Text)
+import           Yi.Buffer
+import           Yi.Core              (sendToProcess, startSubprocess, withSyntax)
+import           Yi.Editor
+import           Yi.History           (historyFinishGen, historyMoveGen, historyStartGen)
+import           Yi.Keymap            (YiM, topKeymapA)
+import           Yi.Keymap.Keys       (Key (KEnter, KHome), char, choice, meta, spec, (<||), (?>>!))
+import           Yi.Lexer.Alex        (Tok)
+import           Yi.Lexer.Compilation (Token)
+import qualified Yi.Mode.Compilation  as Compilation (mode)
+import           Yi.Mode.Common       (lookupMode)
+import           Yi.Monad             (gets)
+import qualified Yi.Rope              as R (YiString, fromText, toString, toText)
+import qualified Yi.Syntax.OnlineTree as OnlineTree (Tree)
+import           Yi.Utils             (io)
+
+
+mode :: Mode (OnlineTree.Tree (Tok Token))
+mode = Compilation.mode
+  { modeApplies = modeNeverApplies,
+    modeName = "interactive",
+    modeKeymap = topKeymapA %~ (<||)
+     (choice
+      [spec KHome ?>>! moveToSol,
+       spec KEnter ?>>! do
+          eof <- withCurrentBuffer atLastLine
+          if eof
+            then feedCommand
+            else withSyntax modeFollow,
+       meta (char 'p') ?>>! interactHistoryMove 1,
+       meta (char 'n') ?>>! interactHistoryMove (-1)
+      ])
+  }
+
+interactId :: T.Text
+interactId = "Interact"
+
+-- | TODO: we're just converting back and forth here, 'historyMoveGen'
+-- and friends need to migrate to YiString it seems.
+interactHistoryMove :: Int -> EditorM ()
+interactHistoryMove delta =
+  historyMoveGen interactId delta (R.toText <$> withCurrentBuffer getInput) >>= inp
+  where
+    inp = withCurrentBuffer . setInput . R.fromText
+
+interactHistoryFinish :: EditorM ()
+interactHistoryFinish =
+  historyFinishGen interactId (R.toText <$> withCurrentBuffer getInput)
+
+interactHistoryStart :: EditorM ()
+interactHistoryStart = historyStartGen interactId
+
+getInputRegion :: BufferM Region
+getInputRegion = do mo <- getMarkB (Just "StdOUT")
+                    p <- pointAt botB
+                    q <- use $ markPointA mo
+                    return $ mkRegion p q
+
+getInput :: BufferM R.YiString
+getInput = readRegionB =<< getInputRegion
+
+setInput :: R.YiString -> BufferM ()
+setInput val = flip replaceRegionB val =<< getInputRegion
+
+-- | Open a new buffer for interaction with a process.
+spawnProcess :: String -> [String] -> YiM BufferRef
+spawnProcess = spawnProcessMode mode
+
+-- | open a new buffer for interaction with a process, using any
+-- interactive-derived mode
+spawnProcessMode :: Mode syntax -> FilePath -> [String] -> YiM BufferRef
+spawnProcessMode interMode cmd args = do
+    b <- startSubprocess cmd args (const $ return ())
+    withEditor interactHistoryStart
+    mode' <- lookupMode $ AnyMode interMode
+    withCurrentBuffer $ do
+        m1 <- getMarkB (Just "StdERR")
+        m2 <- getMarkB (Just "StdOUT")
+        modifyMarkB m1 (\v -> v {markGravity = Backward})
+        modifyMarkB m2 (\v -> v {markGravity = Backward})
+        setAnyMode mode'
+    return b
+
+
+
+-- | Send the type command to the process
+feedCommand :: YiM ()
+feedCommand = do
+  b <- gets currentBuffer
+  withEditor interactHistoryFinish
+  cmd <- withCurrentBuffer $ do
+      botB
+      newlineB
+      me <- getMarkB (Just "StdERR")
+      mo <- getMarkB (Just "StdOUT")
+      p <- pointB
+      q <- use $ markPointA mo
+      cmd <- readRegionB $ mkRegion p q
+      markPointA me .= p
+      markPointA mo .= p
+      return $ R.toString cmd
+  withEditor interactHistoryStart
+  sendToProcess b cmd
+
+-- | Send command, recieve reply
+queryReply :: BufferRef -> String -> YiM R.YiString
+queryReply buf cmd = do
+    start <- withGivenBuffer buf (botB >> pointB)
+    sendToProcess buf (cmd <> "\n")
+    io $ threadDelay 50000  -- Hack to let ghci finish writing its output.
+    withGivenBuffer buf $ do
+      botB
+      moveToSol
+      leftB -- There is probably a much better way to do this moving around, but it works
+      end <- pointB
+      result <- readRegionB (mkRegion start end)
+      botB
+      return result
diff --git a/src/Yi/Monad.hs b/src/Yi/Monad.hs
new file mode 100644
--- /dev/null
+++ b/src/Yi/Monad.hs
@@ -0,0 +1,41 @@
+module Yi.Monad (
+                 gets,
+                 getsAndModify,
+                 maybeM,
+                 repeatUntilM,
+                 whenM,
+                 with,
+                ) where
+
+import Control.Monad.Base   (MonadBase, liftBase)
+import Control.Monad.Reader (MonadReader, ask)
+import Control.Monad.State  (MonadState, get, gets, put, when)
+
+-- | Combination of the Control.Monad.State 'modify' and 'gets'
+getsAndModify :: MonadState s m => (s -> (s,a)) -> m a
+getsAndModify f = do
+  e <- get
+  let (e',result) = f e
+  put e'
+  return result
+
+with :: (MonadReader r m, MonadBase b m) => (r -> a) -> (a -> b c) -> m c
+with f g = do
+    yi <- ask
+    liftBase $ g (f yi)
+
+whenM :: Monad m => m Bool -> m () -> m ()
+whenM mtest ma = mtest >>= flip when ma
+
+maybeM :: Monad m => (x -> m ()) -> Maybe x -> m ()
+maybeM _ Nothing = return ()
+maybeM f (Just x) = f x
+
+-- | Rerun the monad until the boolean result is false, collecting list of results.
+repeatUntilM :: Monad m => m (Bool,a) -> m [a]
+repeatUntilM m = do
+  (proceed,x) <- m
+  if proceed 
+    then (do xs <- repeatUntilM m 
+             return (x:xs))
+    else return [x]
diff --git a/src/Yi/Option.hs b/src/Yi/Option.hs
new file mode 100644
--- /dev/null
+++ b/src/Yi/Option.hs
@@ -0,0 +1,125 @@
+{-# LANGUAGE TemplateHaskell #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE Rank2Types #-}
+{-# OPTIONS_HADDOCK show-extensions #-}
+
+-- |
+-- Module      :  Yi.Option
+-- License     :  GPL-2
+-- Maintainer  :  yi-devel@googlegroups.com
+-- Stability   :  experimental
+-- Portability :  portable
+--
+-- Command-line options
+
+module Yi.Option
+    (
+    -- * Types
+      YiOption
+    , YiOptionDescr
+    , YiOptions
+    , OptionError(..)
+
+    -- * Core
+    , yiCustomOptions
+    , consYiOption
+    , consYiOptions
+
+    -- * Argument-less options
+    , yiBoolOption
+    , yiFlagOption
+    , yiActionFlagOption
+
+    -- * Argument-taking options
+    , yiStringOption
+    , yiStringOption'
+    , yiActionOption
+    , yiActionOption'
+    )
+where
+
+import           Data.Default          (Default)
+import qualified Data.Text             as T (Text)
+import           Data.Typeable         (Typeable)
+import           Data.String           (IsString, fromString)
+import           Lens.Micro.Platform   (Lens', makeLenses, over, set)
+import           System.Exit           (ExitCode)
+import           System.Console.GetOpt (OptDescr, ArgDescr(..))
+import           Yi.Config.Lens        (configVariable, startActionsA)
+import           Yi.Types              (Action, Config, YiConfigVariable)
+
+data OptionError = OptionError T.Text ExitCode
+
+-- | An option is a function that attempts to change the configuration of the
+-- editor at runtime.
+type YiOption = Config -> Either OptionError Config
+
+type YiOptionDescr = OptDescr YiOption
+
+-- | Custom options that should be accepted. Provided in user configuration.
+--
+-- The general flow is that the user adds options to his configuration. Options
+-- are essentially functions describing how to modify the configuration at runtime.
+-- When an option is called, it gets the current config and may modify it (to encode
+-- its value)
+newtype YiOptions = YiOptions { _yiOptions :: [YiOptionDescr] }
+    deriving (Default, Typeable)
+
+instance YiConfigVariable YiOptions
+
+makeLenses ''YiOptions
+
+-- | Lens for accessing the list of custom options.
+--
+-- You can pretty much create whatever types of options you want with this.
+-- But most cases are taken care of by one of the helper functions in this module.
+yiCustomOptions :: Lens' Config [YiOptionDescr]
+yiCustomOptions = configVariable . yiOptions
+
+-- | Includes an extra option in the configuration. Small wrapper around 'yiCustomOptions'
+consYiOption :: YiOptionDescr -> Config -> Config
+consYiOption opt = over yiCustomOptions (opt:)
+
+-- | Like 'consYiOption' but supports multiple options. Convenient for keymaps which might
+-- want to install lots of options.
+consYiOptions :: [YiOptionDescr] -> Config -> Config
+consYiOptions opts = over yiCustomOptions (opts++)
+
+-- | An argument which sets some configuration value to 'True'.
+yiBoolOption :: Lens' Config Bool -> ArgDescr YiOption
+yiBoolOption lens = NoArg $ Right . set lens True
+
+-- | An argument which applies a function transforming some inner value of
+-- the configuration.
+yiFlagOption :: Lens' Config a -> (a -> a) -> ArgDescr YiOption
+yiFlagOption lens f = NoArg $ Right . over lens f
+
+-- | Flag that appends an action to the startup actions.
+yiActionFlagOption :: Action -> ArgDescr YiOption
+yiActionFlagOption action = NoArg f
+    where f config = Right $ over startActionsA (++[action]) config
+ 
+-- | Sets the value of an option which is any string type (hopefully text...)
+--
+-- This is not meant to be fully applied. By only passing in the lens you
+-- will obtain a value suitable for use in OptDescr.
+yiStringOption :: IsString a => Lens' Config a -> String -> ArgDescr YiOption
+yiStringOption lens desc = ReqArg f desc
+    where f string config = Right $ set lens (fromString string) config
+
+-- | Just like 'yiStringOption', except it applies a 'Just'. Useful for setting
+-- string-like values whose default is 'None'.
+yiStringOption' :: IsString a => Lens' Config (Maybe a) -> String -> ArgDescr YiOption
+yiStringOption' lens desc = ReqArg f desc
+    where f string config = Right $ set lens (Just $ fromString string) config
+
+-- | Option that appends a parameterized action to the startup actions.
+yiActionOption :: IsString a => (a -> Action) -> String -> ArgDescr YiOption
+yiActionOption action desc = ReqArg f desc
+    where f string config = Right $ over startActionsA (++[action (fromString string)]) config
+
+yiActionOption' :: IsString a => (a -> Either OptionError Action) -> String -> ArgDescr YiOption
+yiActionOption' action desc = ReqArg f desc
+    where f string config = do
+            action' <- action (fromString string)
+            return $ over startActionsA (++[action']) config
diff --git a/src/Yi/Paths.hs b/src/Yi/Paths.hs
new file mode 100644
--- /dev/null
+++ b/src/Yi/Paths.hs
@@ -0,0 +1,76 @@
+{-# LANGUAGE FlexibleContexts          #-}
+{-# LANGUAGE NoMonomorphismRestriction #-}
+
+module Yi.Paths ( getEvaluatorContextFilename
+                , getConfigFilename
+                , getConfigModules
+                , getArticleDbFilename
+                , getPersistentStateFilename
+                , getConfigDir
+                , getConfigPath
+                , getCustomConfigPath
+                , getDataPath
+                ) where
+
+import           Control.Monad.Base             (MonadBase, liftBase)
+import           System.Directory               (createDirectoryIfMissing,
+                                                 doesDirectoryExist,
+                                                 getAppUserDataDirectory)
+import qualified System.Environment.XDG.BaseDir as XDG (getUserConfigDir, getUserDataDir)
+import           System.FilePath                ((</>))
+
+appUserDataCond ::(MonadBase IO m) => (String -> IO FilePath) -> m FilePath
+appUserDataCond dirQuery = liftBase $
+  do oldDir <- getAppUserDataDirectory "yi"
+     newDir <- dirQuery "yi"
+     oldDirExists <- doesDirectoryExist oldDir
+     newDirExists <- doesDirectoryExist newDir
+     if newDirExists -- overrides old-style
+        then return newDir
+        else if oldDirExists -- old-style exists, use it
+               then return oldDir
+               else do createDirectoryIfMissing True newDir -- none exists, use new style, but create it
+                       return newDir
+
+getConfigDir ::(MonadBase IO m) => m FilePath
+getConfigDir = appUserDataCond XDG.getUserConfigDir
+
+getDataDir ::(MonadBase IO m) => m FilePath
+getDataDir = appUserDataCond XDG.getUserDataDir
+
+-- | Given a path relative to application data directory,
+--   this function finds a path to a given data file.
+getDataPath :: (MonadBase IO m) => FilePath -> m FilePath
+getDataPath fp = fmap (</> fp) getDataDir
+
+-- | Given a path relative to application configuration directory,
+--   this function finds a path to a given configuration file.
+getConfigPath :: MonadBase IO m => FilePath -> m FilePath
+getConfigPath = getCustomConfigPath getConfigDir
+
+-- | Given an action that retrieves config path, and a path relative to it,
+-- this function joins the two together to create a config file path.
+getCustomConfigPath :: MonadBase IO m => m FilePath -> FilePath -> m FilePath
+getCustomConfigPath cd fp = (</> fp) `fmap` cd
+
+-- Note: Dyre also uses XDG cache dir - that would be:
+--getCachePath = getPathHelper XDG.getUserCacheDirectory
+
+-- Below are all points that are used in Yi code (to keep it clean.)
+getEvaluatorContextFilename, getConfigFilename, getConfigModules,
+    getArticleDbFilename, getPersistentStateFilename :: (MonadBase IO m) => m FilePath
+
+-- | Get Yi master configuration script.
+getConfigFilename = getConfigPath "yi.hs"
+
+getConfigModules = getConfigPath "modules"
+
+-- | Get articles.db database of locations to visit (for Yi.IReader.)
+getArticleDbFilename = getConfigPath "articles.db"
+
+-- | Get path to Yi history that stores state between runs.
+getPersistentStateFilename = getDataPath "history"
+
+-- | Get path to environment file that defines namespace used by Yi
+--   command evaluator.
+getEvaluatorContextFilename = getConfigPath $ "local" </> "Env.hs"
diff --git a/src/Yi/PersistentState.hs b/src/Yi/PersistentState.hs
new file mode 100644
--- /dev/null
+++ b/src/Yi/PersistentState.hs
@@ -0,0 +1,127 @@
+{-# LANGUAGE CPP                        #-}
+{-# LANGUAGE DeriveDataTypeable         #-}
+{-# LANGUAGE DeriveGeneric              #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE Haskell2010                #-}
+{-# LANGUAGE NoMonomorphismRestriction  #-}
+{-# LANGUAGE ScopedTypeVariables        #-}
+{-# LANGUAGE TemplateHaskell            #-}
+
+-- | This module implements persistence across different Yi runs.
+--   It includes minibuffer command history, marks etc.
+--   Warning: Current version will _not_ check whether two or more instances
+--   of Yi are run at the same time.
+
+module Yi.PersistentState(loadPersistentState,
+                          savePersistentState,
+                          maxHistoryEntries,
+                          persistentSearch)
+where
+
+import GHC.Generics (Generic)
+
+import           Control.Exc            (ignoringException)
+import           Lens.Micro.Platform    ((.=), makeLenses, use)
+import           Control.Monad          (when)
+import           Data.Binary            (Binary, decodeFile, encodeFile)
+import           Data.Default           (Default, def)
+import qualified Data.Map               as M (map)
+import           Data.Typeable          (Typeable)
+import           System.Directory       (doesFileExist)
+import           Yi.Config.Simple.Types (Field, customVariable)
+import           Yi.Editor
+import           Yi.History             (Histories (..), History (..))
+import           Yi.Keymap              (YiM)
+import           Yi.KillRing            (Killring (..))
+import           Yi.Paths               (getPersistentStateFilename)
+import           Yi.Regex               (SearchExp (..))
+import           Yi.Search.Internal     (getRegexE, setRegexE)
+import           Yi.Types               (YiConfigVariable)
+import           Yi.Utils               (io)
+
+data PersistentState = PersistentState
+    { histories     :: !Histories
+    , aKillring     :: !Killring
+    , aCurrentRegex :: Maybe SearchExp
+    } deriving (Generic)
+
+instance Binary PersistentState
+
+newtype MaxHistoryEntries = MaxHistoryEntries { _unMaxHistoryEntries :: Int }
+  deriving(Typeable, Binary)
+
+instance Default MaxHistoryEntries where
+  def = MaxHistoryEntries 1000
+
+instance YiConfigVariable MaxHistoryEntries
+
+makeLenses ''MaxHistoryEntries
+
+maxHistoryEntries :: Field Int
+maxHistoryEntries = customVariable . unMaxHistoryEntries
+
+newtype PersistentSearch = PersistentSearch { _unPersistentSearch :: Bool }
+  deriving(Typeable, Binary)
+
+instance Default PersistentSearch where
+  def = PersistentSearch True
+
+instance YiConfigVariable PersistentSearch
+
+makeLenses ''PersistentSearch
+
+persistentSearch :: Field Bool
+persistentSearch = customVariable . unPersistentSearch
+
+-- | Trims per-command histories to contain at most N completions each.
+trimHistories :: Int -> Histories -> Histories
+trimHistories maxHistory (Histories m) = Histories $ M.map trimH m
+  where
+    trimH (History cur content prefix) = History cur (trim content) prefix
+    trim content = drop (max 0 (length content - maxHistory)) content
+
+-- | Here is a persistent history saving part.
+--   We assume each command is a single line.
+--   To add new components, one has to:
+--
+--   * add new field in @PersistentState@ structure,
+--   * add write and read parts in @loadPersistentState@/@savePersistentState@,
+--   * add a trimming code in @savePersistentState@ to prevent blowing up
+--     of save file.
+savePersistentState :: YiM ()
+savePersistentState = do
+    MaxHistoryEntries histLimit <- withEditor askConfigVariableA
+    pStateFilename      <- getPersistentStateFilename
+    (hist :: Histories) <- withEditor   getEditorDyn
+    kr                  <- withEditor $ use killringA
+    curRe               <- withEditor   getRegexE
+    let pState = PersistentState {
+                   histories     = trimHistories histLimit hist
+                 , aKillring     = kr    -- trimmed during normal operation
+                 , aCurrentRegex = curRe -- just a single value -> no need to trim
+                 }
+    io $ encodeFile pStateFilename pState
+
+-- | Reads and decodes a persistent state in both strict, and exception robust
+--   way.
+readPersistentState :: YiM (Maybe PersistentState)
+readPersistentState = do pStateFilename <- getPersistentStateFilename
+                         pStateExists <- io $ doesFileExist pStateFilename
+                         if not pStateExists
+                           then return Nothing
+                           else io $ ignoringException $ strictDecoder pStateFilename
+  where
+    strictDecoder filename = do (state :: PersistentState) <- decodeFile filename
+                                state `seq` return (Just state)
+
+-- | Loads a persistent state, and sets Yi state variables accordingly.
+loadPersistentState :: YiM ()
+loadPersistentState = do
+    maybePState <- readPersistentState
+    case maybePState of
+      Nothing     -> return ()
+      Just pState -> do putEditorDyn                $ histories     pState
+                        (.=) killringA            $ aKillring     pState
+                        PersistentSearch keepSearch <- askConfigVariableA
+                        when keepSearch . withEditor $
+                            maybe (return ()) setRegexE $ aCurrentRegex pState
diff --git a/src/Yi/Process.hs b/src/Yi/Process.hs
new file mode 100644
--- /dev/null
+++ b/src/Yi/Process.hs
@@ -0,0 +1,107 @@
+{-# LANGUAGE CPP                 #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+
+module Yi.Process (runProgCommand, runShellCommand, shellFileName,
+                   createSubprocess, readAvailable, SubprocessInfo(..), SubprocessId) where
+
+import           Control.Exc             (orException)
+import qualified Data.ListLike as L      (empty)
+import           Foreign.C.String        (peekCStringLen)
+import           Foreign.Marshal.Alloc   (allocaBytes)
+import           System.Directory        (findExecutable)
+import           System.Environment      (getEnv)
+import           System.Exit             (ExitCode (ExitFailure))
+import           System.IO               (BufferMode (NoBuffering), Handle, hSetBuffering, hGetBufNonBlocking)
+import           System.Process          (ProcessHandle, runProcess)
+import           System.Process.ListLike (ListLikeProcessIO, readProcessWithExitCode)
+import           Yi.Buffer.Basic         (BufferRef)
+import           Yi.Monad                (repeatUntilM)
+
+#ifndef mingw32_HOST_OS
+import System.Posix.IO (createPipe, fdToHandle)
+#endif
+
+runProgCommand :: ListLikeProcessIO a c => String -> [String] -> IO (ExitCode, a, a)
+runProgCommand prog args = do loc <- findExecutable prog
+                              case loc of
+                                  Nothing -> return (ExitFailure 1, L.empty, L.empty)
+                                  Just fp -> readProcessWithExitCode fp args L.empty
+
+------------------------------------------------------------------------
+-- | Run a command using the system shell, returning stdout, stderr and exit code
+
+shellFileName :: IO String
+shellFileName = orException (getEnv "SHELL") (return "/bin/sh")
+
+shellCommandSwitch :: String
+shellCommandSwitch = "-c"
+
+runShellCommand :: ListLikeProcessIO a c => String -> IO (ExitCode, a, a)
+runShellCommand cmd = do
+      sh <- shellFileName
+      readProcessWithExitCode sh [shellCommandSwitch, cmd] L.empty
+
+
+--------------------------------------------------------------------------------
+-- Subprocess support (ie. async processes whose output goes to a buffer)
+
+type SubprocessId = Integer
+
+data SubprocessInfo = SubprocessInfo {
+      procCmd :: FilePath,
+      procArgs :: [String],
+      procHandle :: ProcessHandle,
+      hIn  :: Handle,
+      hOut :: Handle,
+      hErr :: Handle,
+      bufRef :: BufferRef,
+      separateStdErr :: Bool
+      }
+
+{-
+Simon Marlow said this:
+
+ It turns out to be dead easy to bind stderr and stdout to the same pipe. After a couple of minor tweaks the following now works:
+
+ createProcess (proc cmd args){ std_out = CreatePipe,
+                                std_err = UseHandle stdout }
+
+Therefore it should be possible to simplifiy the following greatly with the new process package.
+
+-}
+createSubprocess :: FilePath -> [String] -> BufferRef -> IO SubprocessInfo
+createSubprocess cmd args bufref = do
+
+#ifdef mingw32_HOST_OS
+    (inp,out,err,handle) <- runInteractiveProcess cmd args Nothing Nothing
+    let separate = True
+#else
+    (inpReadFd,inpWriteFd) <- System.Posix.IO.createPipe
+    (outReadFd,outWriteFd) <- System.Posix.IO.createPipe
+    [inpRead,inpWrite,outRead,outWrite] <- mapM fdToHandle [inpReadFd,inpWriteFd,outReadFd,outWriteFd]
+
+    handle <- runProcess cmd args Nothing Nothing (Just inpRead) (Just outWrite) (Just outWrite)
+    let inp = inpWrite
+        out = outRead
+        err = outRead
+        separate = False
+#endif
+    hSetBuffering inp NoBuffering
+    hSetBuffering out NoBuffering
+    hSetBuffering err NoBuffering
+    return SubprocessInfo { procCmd=cmd, procArgs=args, procHandle=handle, hIn=inp, hOut=out, hErr=err, bufRef=bufref, separateStdErr=separate }
+
+
+-- Read as much as possible from handle without blocking
+readAvailable :: Handle -> IO String
+readAvailable handle = fmap concat $ repeatUntilM $ readChunk handle
+
+-- Read a chunk from a handle, bool indicates if there is potentially more data available
+readChunk :: Handle -> IO (Bool, String)
+readChunk handle = do
+    let bufferSize = 1024
+    allocaBytes bufferSize $ \buffer -> do
+                 bytesRead <- hGetBufNonBlocking handle buffer bufferSize
+                 s <- peekCStringLen (buffer,bytesRead)
+                 let mightHaveMore = bytesRead == bufferSize
+                 return (mightHaveMore, s)
diff --git a/src/Yi/Rectangle.hs b/src/Yi/Rectangle.hs
new file mode 100644
--- /dev/null
+++ b/src/Yi/Rectangle.hs
@@ -0,0 +1,107 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE PackageImports #-}
+{-# OPTIONS_HADDOCK show-extensions #-}
+
+-- |
+-- Module      :  Yi.Rectangle
+-- License     :  GPL-2
+-- Maintainer  :  yi-devel@googlegroups.com
+-- Stability   :  experimental
+-- Portability :  portable
+--
+-- emacs-style rectangle manipulation functions.
+
+module Yi.Rectangle where
+
+import           Control.Monad       (forM_)
+import           Data.List           (sort, transpose)
+import           Data.Monoid         ((<>))
+import qualified Data.Text           as T (Text, concat, justifyLeft, length)
+import qualified Data.Text.ICU       as ICU (regex, find, unfold, group)
+import           Yi.Buffer
+import           Yi.Editor           (EditorM, getRegE, setRegE, withCurrentBuffer)
+import qualified Yi.Rope             as R
+import           Yi.String           (lines', mapLines, unlines')
+
+alignRegion :: T.Text -> BufferM ()
+alignRegion str = do
+  s <- getSelectRegionB >>= unitWiseRegion Line
+  modifyRegionB (R.fromText . alignText str . R.toText) s
+  where
+    regexSplit :: T.Text -> T.Text -> [T.Text]
+    regexSplit pattern l = case ICU.find (ICU.regex [] pattern) l of
+        Nothing -> error "regexSplit: text does not match"
+        Just m  -> drop 1 $ ICU.unfold ICU.group m
+
+    alignText :: T.Text -> T.Text -> T.Text
+    alignText regex text = unlines' ls'
+      where ls, ls' :: [T.Text]
+            ls = lines' text
+            columns :: [[T.Text]]
+            columns = regexSplit regex <$> ls
+
+            columnsWidth :: [Int]
+            columnsWidth = fmap (maximum . fmap T.length) $ transpose columns
+
+            columns' :: [[T.Text]]
+            columns' = fmap (zipWith (`T.justifyLeft` ' ') columnsWidth) columns
+
+            ls' = T.concat <$> columns'
+
+-- | Align each line of the region on the given regex.
+-- Fails if it is not found in any line.
+alignRegionOn :: T.Text -> BufferM ()
+alignRegionOn s = alignRegion $ "^(.*)(" <> s <> ")(.*)"
+
+-- | Get the selected region as a rectangle.
+-- Returns the region extended to lines, plus the start and end columns of the rectangle.
+getRectangle :: BufferM (Region, Int, Int)
+getRectangle = do
+    r <- getSelectRegionB
+    extR <- unitWiseRegion Line r
+    [lowCol,highCol] <- sort <$> mapM colOf [regionStart r, regionEnd r]
+    return (extR, lowCol, highCol)
+
+-- | Split text at the boundaries given
+multiSplit :: [Int] -> R.YiString -> [R.YiString]
+multiSplit [] l = [l]
+multiSplit (x:xs) l = left : multiSplit (fmap (subtract x) xs) right
+    where (left, right) = R.splitAt x l
+
+onRectangle :: (Int -> Int -> R.YiString -> R.YiString) -> BufferM ()
+onRectangle f = do
+  (reg, l, r) <- getRectangle
+  modifyRegionB (mapLines (f l r)) reg
+
+openRectangle :: BufferM ()
+openRectangle = onRectangle openLine
+  where
+    openLine l r line =
+      left <> R.replicateChar (r - l) ' ' <> right
+          where (left, right) = R.splitAt l line
+
+stringRectangle :: R.YiString -> BufferM ()
+stringRectangle inserted = onRectangle stringLine
+  where stringLine l r line = left <> inserted <> right
+          where [left,_,right] = multiSplit [l,r] line
+
+killRectangle :: EditorM ()
+killRectangle = do
+  cutted <- withCurrentBuffer $ do
+      (reg, l, r) <- getRectangle
+      text <- readRegionB reg
+      let (cutted, rest) = unzip $ fmap cut $ R.lines' text
+
+          cut :: R.YiString -> (R.YiString, R.YiString)
+          cut line = let [left,mid,right] = multiSplit [l,r] line
+                     in (mid, left <> right)
+      replaceRegionB reg (R.unlines rest)
+      return cutted
+  setRegE (R.unlines cutted)
+
+yankRectangle :: EditorM ()
+yankRectangle = do
+  text <- R.lines' <$> getRegE
+  withCurrentBuffer $ forM_ text $ \t -> do
+    savingPointB $ insertN t
+    lineDown
diff --git a/src/Yi/Search.hs b/src/Yi/Search.hs
new file mode 100644
--- /dev/null
+++ b/src/Yi/Search.hs
@@ -0,0 +1,441 @@
+{-# LANGUAGE DeriveDataTypeable         #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE LambdaCase                 #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# OPTIONS_HADDOCK show-extensions #-}
+
+-- |
+-- Module      :  Yi.Search
+-- License     :  GPL-2
+-- Maintainer  :  yi-devel@googlegroups.com
+-- Stability   :  experimental
+-- Portability :  portable
+--
+-- Search/Replace functions
+
+module Yi.Search (
+        setRegexE,      -- :: SearchExp -> EditorM ()
+        resetRegexE,    -- :: EditorM ()
+        getRegexE,      -- :: EditorM (Maybe SearchExp)
+        SearchMatch,
+        SearchResult(..),
+        SearchOption(..),
+        doSearch,            -- :: (Maybe String) -> [SearchOption]
+                            -- -> Direction -> YiM ()
+        searchInit,        -- :: String
+                            -- -> [SearchOption]
+                            -- -> IO SearchExp
+        continueSearch,          -- :: SearchExp
+                            -- -> IO SearchResult
+        makeSimpleSearch,
+
+        -- * Batch search-replace
+        searchReplaceRegionB,
+        searchReplaceSelectionB,
+        replaceString,
+        searchAndRepRegion,
+        searchAndRepRegion0,
+        searchAndRepUnit, -- :: String -> String -> Bool -> TextUnit -> EditorM Bool
+
+        -- * Incremental Search
+        isearchInitE,
+        isearchIsEmpty,
+        isearchAddE,
+        isearchPrevE,
+        isearchNextE,
+        isearchWordE,
+        isearchHistory,
+        isearchDelE,
+        isearchCancelE,
+        isearchFinishE,
+        isearchCancelWithE,
+        isearchFinishWithE,
+
+        -- * Replace
+        qrNext,
+        qrReplaceAll,
+        qrReplaceOne,
+        qrFinish
+                 ) where
+
+import           Lens.Micro.Platform          ((.=))
+import           Control.Monad       (void, when)
+import           Data.Binary         (Binary, get, put)
+import           Data.Char           (isAlpha, isUpper)
+import           Data.Default        (Default, def)
+import           Data.Maybe          (listToMaybe)
+import           Data.Monoid         ((<>))
+import qualified Data.Text           as T (Text, any, break, empty, length, null, takeWhile, unpack)
+import qualified Data.Text.Encoding  as E (decodeUtf8, encodeUtf8)
+import           Data.Typeable       (Typeable)
+import           Yi.Buffer
+import           Yi.Editor
+import           Yi.History          (historyFinishGen, historyMoveGen, historyStartGen)
+import           Yi.Regex
+import qualified Yi.Rope             as R (YiString, null, toString, toText)
+import           Yi.Search.Internal  (getRegexE, resetRegexE, setRegexE)
+import           Yi.String           (showT)
+import           Yi.Types            (YiVariable)
+import           Yi.Utils            (fst3)
+import           Yi.Window           (Window)
+
+-- ---------------------------------------------------------------------
+--
+-- | Global searching. Search for regex and move point to that position.
+-- @Nothing@ means reuse the last regular expression. @Just s@ means use
+-- @s@ as the new regular expression. Direction of search can be
+-- specified as either @Backward@ or @Forward@ (forwards in the buffer).
+-- Arguments to modify the compiled regular expression can be supplied
+-- as well.
+--
+
+type SearchMatch = Region
+data SearchResult = PatternFound
+                  | PatternNotFound
+                  | SearchWrapped
+  deriving Eq
+
+doSearch :: Maybe String        -- ^ @Nothing@ means used previous
+                                -- pattern, if any. Complain otherwise.
+                                -- Use getRegexE to check for previous patterns
+        -> [SearchOption]            -- ^ Flags to modify the compiled regex
+        -> Direction            -- ^ @Backward@ or @Forward@
+        -> EditorM SearchResult
+
+doSearch (Just re) fs d = searchInit re d fs >>= withCurrentBuffer . continueSearch
+doSearch Nothing   _  d = do
+  mre <- getRegexE
+  case mre of
+    Nothing -> fail "No previous search pattern" -- NB
+    Just r  -> withCurrentBuffer (continueSearch (r,d))
+
+-- | Set up a search.
+searchInit :: String -> Direction -> [SearchOption] -> EditorM (SearchExp, Direction)
+searchInit re d fs = do
+    let Right c_re = makeSearchOptsM fs re
+    setRegexE c_re
+    searchDirectionA .= d
+    return (c_re,d)
+
+-- | Do a search, placing cursor at first char of pattern, if found.
+-- Keymaps may implement their own regex language. How do we provide for this?
+-- Also, what's happening with ^ not matching sol?
+continueSearch :: (SearchExp, Direction) -> BufferM SearchResult
+continueSearch (c_re, dir) = do
+  mp <- savingPointB $ do
+    moveB Character dir  -- start immed. after cursor
+    rs <- regexB dir c_re
+    moveB Document (reverseDir dir) -- wrap around
+    ls <- regexB dir c_re
+    return $ listToMaybe $ fmap Right rs ++ fmap Left ls
+  maybe (return ()) (moveTo . regionStart . either id id) mp
+  return $ f mp
+    where
+        f (Just (Right _)) = PatternFound
+        f (Just (Left  _)) = SearchWrapped
+        f Nothing          = PatternNotFound
+
+------------------------------------------------------------------------
+-- Batch search and replace
+--
+
+-- | Search and Replace all within the current region.
+-- Note the region is the final argument since we might perform
+-- the same search and replace over multiple regions however we are
+-- unlikely to perform several search and replaces over the same region
+-- since the first such may change the bounds of the region.
+searchReplaceRegionB :: R.YiString -- ^ The string to search for
+                     -> R.YiString -- ^ The string to replace it with
+                     -> Region -- ^ The region to perform this over
+                     -> BufferM Int
+searchReplaceRegionB from to =
+  searchAndRepRegion0 (makeSimpleSearch from) to True
+
+
+-- | Peform a search and replace on the selection
+searchReplaceSelectionB :: R.YiString  -- ^ text to search for
+                        -> R.YiString  -- ^ text to replace it with
+                        -> BufferM Int
+searchReplaceSelectionB from to =
+  getSelectRegionB >>= searchReplaceRegionB from to
+
+-- | Replace a string by another everywhere in the document
+replaceString :: R.YiString -> R.YiString -> BufferM Int
+replaceString a b = regionOfB Document >>= searchReplaceRegionB a b
+
+------------------------------------------------------------------------
+-- | Search and replace in the given region.
+--
+-- If the input boolean is True, then the replace is done globally,
+-- otherwise only the first match is replaced. Returns the number of
+-- replacements done.
+searchAndRepRegion0 :: SearchExp -> R.YiString -> Bool -> Region -> BufferM Int
+searchAndRepRegion0 c_re str globally region = do
+    mp <- (if globally then id else take 1) <$> regexRegionB c_re region  -- find the regex
+    -- mp' is a maybe not reversed version of mp, the goal
+    -- is to avoid replaceRegionB to mess up the next regions.
+    -- So we start from the end.
+    let mp' = mayReverse (reverseDir $ regionDirection region) mp
+    mapM_ (`replaceRegionB` str) mp'
+    return (length mp)
+
+searchAndRepRegion :: R.YiString -> R.YiString -> Bool -> Region -> EditorM Bool
+searchAndRepRegion s str globally region = case R.null s of
+  False -> return False
+  True -> do
+    let c_re = makeSimpleSearch s
+    setRegexE c_re     -- store away for later use
+    searchDirectionA .= Forward
+    withCurrentBuffer $ (/= 0) <$> searchAndRepRegion0 c_re str globally region
+
+------------------------------------------------------------------------
+-- | Search and replace in the region defined by the given unit.
+-- The rest is as in 'searchAndRepRegion'.
+searchAndRepUnit :: R.YiString -> R.YiString -> Bool -> TextUnit -> EditorM Bool
+searchAndRepUnit re str g unit =
+  withCurrentBuffer (regionOfB unit) >>= searchAndRepRegion re str g
+
+--------------------------
+-- Incremental search
+
+
+newtype Isearch = Isearch [(T.Text, Region, Direction)]
+  deriving (Typeable, Show)
+
+instance Binary Isearch where
+  put (Isearch ts) = put (map3 E.encodeUtf8 ts)
+  get = Isearch . map3 E.decodeUtf8 <$> get
+
+map3 :: (a -> d) -> [(a, b, c)] -> [(d, b, c)]
+map3 _ [] = []
+map3 f ((a, b, c):xs) = (f a, b, c) : map3 f xs
+
+
+-- This contains: (string currently searched, position where we
+-- searched it, direction, overlay for highlighting searched text)
+
+-- Note that this info cannot be embedded in the Keymap state: the state
+-- modification can depend on the state of the editor.
+
+instance Default Isearch where
+    def = Isearch []
+
+instance YiVariable Isearch
+
+isearchInitE :: Direction -> EditorM ()
+isearchInitE dir = do
+  historyStartGen iSearch
+  p <- withCurrentBuffer pointB
+  putEditorDyn (Isearch [(T.empty ,mkRegion p p, dir)])
+  printMsg "I-search: "
+
+isearchIsEmpty :: EditorM Bool
+isearchIsEmpty = do
+  Isearch s <- getEditorDyn
+  return . not . T.null . fst3 $ head s
+
+isearchAddE :: T.Text -> EditorM ()
+isearchAddE inc = isearchFunE (<> inc)
+
+-- | Create a SearchExp that matches exactly its argument
+makeSimpleSearch :: R.YiString -> SearchExp
+makeSimpleSearch s = se
+    where Right se = makeSearchOptsM [QuoteRegex] (R.toString s)
+
+makeISearch :: T.Text -> SearchExp
+makeISearch s = case makeSearchOptsM opts (T.unpack s) of
+                  Left _ -> SearchExp (T.unpack s) emptyRegex emptyRegex []
+                  Right search -> search
+   where opts = QuoteRegex : if T.any isUpper s then [] else [IgnoreCase]
+
+isearchFunE :: (T.Text -> T.Text) -> EditorM ()
+isearchFunE fun = do
+  Isearch s <- getEditorDyn
+  case s of
+    [_] -> resetRegexE
+    _ -> return ()
+  let (previous,p0,direction) = head s
+      current = fun previous
+      srch = makeISearch current
+  printMsg $ "I-search: " <> current
+  setRegexE srch
+  prevPoint <- withCurrentBuffer pointB
+  matches <- withCurrentBuffer $ do
+      moveTo $ regionStart p0
+      when (direction == Backward) $
+         moveN $ T.length current
+      regexB direction srch
+
+  let onSuccess p = do withCurrentBuffer $ moveTo (regionEnd p)
+                       putEditorDyn $ Isearch ((current, p, direction) : s)
+
+  case matches of
+    (p:_) -> onSuccess p
+    [] -> do matchesAfterWrap <- withCurrentBuffer $ do
+               case direction of
+                 Forward -> moveTo 0
+                 Backward -> do
+                   bufferLength <- sizeB
+                   moveTo bufferLength
+               regexB direction srch
+
+             case matchesAfterWrap of
+               (p:_) -> onSuccess p
+               [] -> do withCurrentBuffer $ moveTo prevPoint -- go back to where we were
+                        putEditorDyn $ Isearch ((current, p0, direction) : s)
+                        printMsg $ "Failing I-search: " <> current
+
+isearchDelE :: EditorM ()
+isearchDelE = do
+  Isearch s <- getEditorDyn
+  case s of
+    (_:(text,p,dir):rest) -> do
+      withCurrentBuffer $
+        moveTo $ regionEnd p
+      putEditorDyn $ Isearch ((text,p,dir):rest)
+      setRegexE $ makeISearch text
+      printMsg $ "I-search: " <> text
+    _ -> return () -- if the searched string is empty, don't try to remove chars from it.
+
+isearchHistory :: Int -> EditorM ()
+isearchHistory delta = do
+  Isearch ((current,_p0,_dir):_) <- getEditorDyn
+  h <- historyMoveGen iSearch delta (return current)
+  isearchFunE (const h)
+
+isearchPrevE :: EditorM ()
+isearchPrevE = isearchNext0 Backward
+
+isearchNextE :: EditorM ()
+isearchNextE = isearchNext0 Forward
+
+isearchNext0 :: Direction -> EditorM ()
+isearchNext0 newDir = do
+  Isearch ((current,_p0,_dir):_rest) <- getEditorDyn
+  if T.null current
+    then isearchHistory 1
+    else isearchNext newDir
+
+
+isearchNext :: Direction -> EditorM ()
+isearchNext direction = do
+  Isearch ((current, p0, _dir) : rest) <- getEditorDyn
+  withCurrentBuffer $ moveTo (regionStart p0 + startOfs)
+  mp <- withCurrentBuffer $
+    regexB direction (makeISearch current)
+  case mp of
+    [] -> do
+      endPoint <- withCurrentBuffer $ do
+              moveTo (regionEnd p0) -- revert to offset we were before.
+              sizeB
+      printMsg "isearch: end of document reached"
+      let wrappedOfs = case direction of
+                         Forward -> mkRegion 0 0
+                         Backward -> mkRegion endPoint endPoint
+      putEditorDyn $ Isearch ((current,wrappedOfs,direction):rest) -- prepare to wrap around.
+    (p:_) -> do
+      withCurrentBuffer $
+        moveTo (regionEnd p)
+      printMsg $ "I-search: " <> current
+      putEditorDyn $ Isearch ((current,p,direction):rest)
+ where startOfs = case direction of
+                      Forward  ->  1
+                      Backward -> -1
+
+isearchWordE :: EditorM ()
+isearchWordE = do
+   -- add maximum 32 chars at a time.
+  text <- R.toText <$> withCurrentBuffer (pointB >>= nelemsB 32)
+
+  let (prefix, rest) = T.break isAlpha text
+      word = T.takeWhile isAlpha rest
+  isearchAddE $ prefix <> word
+
+-- | Succesfully finish a search. Also see 'isearchFinishWithE'.
+isearchFinishE :: EditorM ()
+isearchFinishE = isearchEnd True
+
+-- | Cancel a search. Also see 'isearchCancelWithE'.
+isearchCancelE :: EditorM ()
+isearchCancelE = isearchEnd False
+
+-- | Wrapper over 'isearchEndWith' that passes through the action and
+-- accepts the search as successful (i.e. when the user wants to stay
+-- at the result).
+isearchFinishWithE :: EditorM a -> EditorM ()
+isearchFinishWithE act = isearchEndWith act True
+
+-- | Wrapper over 'isearchEndWith' that passes through the action and
+-- marks the search as unsuccessful (i.e. when the user wants to
+-- jump back to where the search started).
+isearchCancelWithE :: EditorM a -> EditorM ()
+isearchCancelWithE act = isearchEndWith act False
+
+iSearch :: T.Text
+iSearch = "isearch"
+
+-- | Editor action describing how to end finish incremental search.
+-- The @act@ parameter allows us to specify an extra action to run
+-- before finishing up the search. For Vim, we don't want to do
+-- anything so we use 'isearchEnd' which just does nothing. For emacs,
+-- we want to cancel highlighting and stay where we are.
+isearchEndWith :: EditorM a -> Bool -> EditorM ()
+isearchEndWith act accept = getEditorDyn >>= \case
+  Isearch [] -> return ()
+  Isearch s@((lastSearched, _, dir):_) -> do
+    let (_,p0,_) = last s
+    historyFinishGen iSearch (return lastSearched)
+    searchDirectionA .= dir
+    if accept
+       then do void act
+               printMsg "Quit"
+       else do resetRegexE
+               withCurrentBuffer $ moveTo $ regionStart p0
+
+-- | Specialised 'isearchEndWith' to do nothing as the action.
+isearchEnd :: Bool -> EditorM ()
+isearchEnd = isearchEndWith (return ())
+
+-----------------
+-- Query-Replace
+
+-- | Find the next match and select it.
+-- Point is end, mark is beginning.
+qrNext :: Window -> BufferRef -> SearchExp -> EditorM ()
+qrNext win b what = do
+  mp <- withGivenBufferAndWindow win b $ regexB Forward what
+  case mp of
+    [] -> do
+      printMsg "String to search not found"
+      qrFinish
+    (r:_) -> withGivenBufferAndWindow win b $ setSelectRegionB r
+
+-- | Replace all the remaining occurrences.
+qrReplaceAll :: Window -> BufferRef -> SearchExp -> R.YiString -> EditorM ()
+qrReplaceAll win b what replacement = do
+  n <- withGivenBufferAndWindow win b $ do
+    exchangePointAndMarkB -- so we replace the current occurence too
+    searchAndRepRegion0 what replacement True =<< regionOfPartB Document Forward
+  printMsg $ "Replaced " <> showT n <> " occurrences"
+  qrFinish
+
+-- | Exit from query/replace.
+qrFinish :: EditorM ()
+qrFinish = do
+  currentRegexA .= Nothing
+  closeBufferAndWindowE  -- the minibuffer.
+
+-- | We replace the currently selected match and then move to the next
+-- match.
+qrReplaceOne :: Window -> BufferRef -> SearchExp -> R.YiString -> EditorM ()
+qrReplaceOne win b reg replacement = do
+  qrReplaceCurrent win b replacement
+  qrNext win b reg
+
+-- | This may actually be a bit more general it replaces the current
+-- selection with the given replacement string in the given window and
+-- buffer.
+qrReplaceCurrent :: Window -> BufferRef -> R.YiString -> EditorM ()
+qrReplaceCurrent win b replacement =
+  withGivenBufferAndWindow win b $
+   flip replaceRegionB replacement =<< getRawestSelectRegionB
diff --git a/src/Yi/Search/Internal.hs b/src/Yi/Search/Internal.hs
new file mode 100644
--- /dev/null
+++ b/src/Yi/Search/Internal.hs
@@ -0,0 +1,36 @@
+{-# OPTIONS_HADDOCK show-extensions #-}
+
+-- |
+-- Module      :  Yi.Search.Internal
+-- License     :  GPL-2
+-- Maintainer  :  yi-devel@googlegroups.com
+-- Stability   :  experimental
+-- Portability :  portable
+--
+-- Internal use for Yi.Search.
+
+module Yi.Search.Internal where
+
+import           Lens.Micro.Platform   ((.=), use)
+import           Yi.Editor    (EditorM, currentRegexA)
+import           Yi.Regex     (SearchExp)
+
+-- ---------------------------------------------------------------------
+-- Searching and substitutions with regular expressions
+--
+-- The most recent regex is held by the editor. You can get at it with
+-- getRegeE. This is useful to determine if there was a previous
+-- pattern.
+--
+
+-- | Put regex into regex 'register'
+setRegexE :: SearchExp -> EditorM ()
+setRegexE re = currentRegexA .= (Just re)
+
+-- | Clear the regex 'register'
+resetRegexE :: EditorM ()
+resetRegexE = currentRegexA .= Nothing
+
+-- | Return contents of regex register
+getRegexE :: EditorM (Maybe SearchExp)
+getRegexE = use currentRegexA
diff --git a/src/Yi/Snippets.hs b/src/Yi/Snippets.hs
new file mode 100644
--- /dev/null
+++ b/src/Yi/Snippets.hs
@@ -0,0 +1,419 @@
+{-# LANGUAGE CPP                        #-}
+{-# LANGUAGE DeriveDataTypeable         #-}
+{-# LANGUAGE DeriveGeneric              #-}
+{-# LANGUAGE FlexibleContexts           #-}
+{-# LANGUAGE FlexibleInstances          #-}
+{-# LANGUAGE FunctionalDependencies     #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE LambdaCase                 #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE TemplateHaskell            #-}
+{-# OPTIONS_HADDOCK show-extensions #-}
+
+-- |
+-- Module      :  Yi.Snippets
+-- License     :  GPL-2
+-- Maintainer  :  yi-devel@googlegroups.com
+-- Stability   :  experimental
+-- Portability :  portable
+
+module Yi.Snippets where
+
+import           GHC.Generics (Generic)
+
+import           Control.Applicative (some)
+import           Control.Arrow       (second)
+import           Lens.Micro.Platform          (use, (.=))
+import           Control.Monad.RWS   (MonadPlus (mplus), MonadReader (ask),
+                                      MonadState, MonadTrans (..),
+                                      MonadWriter (tell), RWST, evalRWST,
+                                      filterM, forM, forM_, liftM2, unless,
+                                      when, (<>))
+import           Data.Binary         (Binary)
+import           Data.Char           (isSpace)
+import           Data.Default        (Default, def)
+import           Data.Foldable       (find)
+import           Data.List           (foldl', groupBy, intersperse, nub, sort)
+import           Data.Maybe          (catMaybes)
+import qualified Data.Text           as T (Text)
+import           Data.Typeable       (Typeable)
+import           Yi.Buffer
+import           Yi.Editor           (withCurrentBuffer)
+import           Yi.Keymap           (Action)
+import           Yi.Keymap.Keys
+import qualified Yi.Rope             as R
+import           Yi.TextCompletion   (resetComplete, wordCompleteString')
+import           Yi.Types            (YiVariable)
+
+type SnippetCmd = RWST (Int, Int) [MarkInfo] () BufferM
+
+data SnippetMark = SimpleMark !Int
+                 | ValuedMark !Int R.YiString
+                 | DependentMark !Int
+
+
+data MarkInfo = SimpleMarkInfo { userIndex :: !Int
+                               , startMark :: !Mark }
+              | ValuedMarkInfo { userIndex :: !Int
+                               , startMark :: !Mark
+                               , endMark :: !Mark }
+              | DependentMarkInfo { userIndex :: !Int
+                                  , startMark :: !Mark
+                                  , endMark :: !Mark }
+  deriving (Eq, Show, Generic)
+
+instance Binary MarkInfo
+
+newtype BufferMarks = BufferMarks { bufferMarks :: [MarkInfo] }
+  deriving (Eq, Show, Monoid, Typeable, Binary)
+
+newtype DependentMarks = DependentMarks { marks :: [[MarkInfo]] }
+  deriving (Eq, Show, Monoid, Typeable, Binary)
+
+instance Default BufferMarks where
+  def = BufferMarks []
+
+instance Default DependentMarks where
+  def = DependentMarks []
+
+instance YiVariable BufferMarks
+instance YiVariable DependentMarks
+
+instance Ord MarkInfo where
+  a `compare` b = userIndex a `compare` userIndex b
+
+cursor :: Int -> SnippetMark
+cursor = SimpleMark
+
+cursorWith :: Int -> R.YiString -> SnippetMark
+cursorWith = ValuedMark
+
+dep :: Int -> SnippetMark
+dep = DependentMark
+
+isDependentMark :: MarkInfo -> Bool
+isDependentMark (SimpleMarkInfo{})    = False
+isDependentMark (ValuedMarkInfo{})    = False
+isDependentMark (DependentMarkInfo{}) = True
+
+bufferMarkers :: MarkInfo -> [Mark]
+bufferMarkers (SimpleMarkInfo _ s) = [s]
+bufferMarkers m = [startMark m, endMark m]
+
+-- used to translate a datatype into a snippet cmd for
+-- freely combining data with '&'
+class MkSnippetCmd a b | a -> b where
+  mkSnippetCmd :: a -> SnippetCmd b
+
+instance MkSnippetCmd String () where
+  mkSnippetCmd = text . R.fromString
+
+instance MkSnippetCmd R.YiString () where
+  mkSnippetCmd = text
+
+instance MkSnippetCmd T.Text () where
+  mkSnippetCmd = text . R.fromText
+
+instance MkSnippetCmd (SnippetCmd a) a where
+  mkSnippetCmd = id
+
+-- mkSnippetCmd for 'cursor...'-functions
+instance MkSnippetCmd SnippetMark () where
+  mkSnippetCmd (SimpleMark i) = do
+      mk <- mkMark
+      tell [SimpleMarkInfo i mk]
+
+  mkSnippetCmd (ValuedMark i str) = do
+      start <- mkMark
+      lift $ insertN str
+      end <- mkMark
+      tell [ValuedMarkInfo i start end]
+
+  mkSnippetCmd (DependentMark i) = do
+      start <- mkMark
+      end <- mkMark
+      tell [DependentMarkInfo i start end]
+
+-- create a mark at current position
+mkMark :: MonadTrans t => t BufferM Mark
+mkMark = lift $ do p <- pointB
+                   newMarkB $ MarkValue p Backward
+
+-- Indentation support has been temporarily removed
+text :: R.YiString -> SnippetCmd ()
+text txt = do
+  (_, indent) <- ask
+  indentSettings <- lift indentSettingsB
+  lift . foldl' (>>) (return ()) .
+      intersperse (newlineB >> indentToB indent) .
+      map (if expandTabs indentSettings
+           then insertN . expand indentSettings ""
+           else insertN) $ lines' txt
+  where
+    lines' txt' = case R.last txt' of
+      Just '\n' -> R.lines txt' <> [mempty]
+      _         -> R.lines txt
+
+    expand :: IndentSettings -> R.YiString -> R.YiString -> R.YiString
+    expand is str rst = case R.head rst of
+      Nothing -> R.reverse str
+      Just '\t' -> let t = R.replicateChar (tabSize is) ' ' <> str
+                   in expand is t (R.drop 1 rst)
+      Just s -> expand is (s `R.cons` str) rst
+
+-- unfortunatelly data converted to snippets are no monads, but '&' is
+-- very similar to '>>' and '&>' is similar to '>>=', since
+-- SnippetCmd's can be used monadically
+infixr 5 &
+(&) :: (MkSnippetCmd a any , MkSnippetCmd b c) => a -> b -> SnippetCmd c
+str & rst = mkSnippetCmd str >> mkSnippetCmd rst
+
+(&>) :: (MkSnippetCmd a b, MkSnippetCmd c d) => a -> (b -> c) -> SnippetCmd d
+str &> rst = mkSnippetCmd str >>= mkSnippetCmd . rst
+
+runSnippet :: Bool -> SnippetCmd a -> BufferM a
+runSnippet deleteLast s = do
+    line <- lineOf =<< pointB
+    indent <- indentOfCurrentPosB
+    (a, markInfo) <- evalRWST s (line, indent) ()
+    unless (null markInfo) $ do
+        let newMarks = sort $ filter (not . isDependentMark) markInfo
+        let newDepMarks = filter (not . len1) $
+                            groupBy belongTogether $
+                              sort markInfo
+        getBufferDyn >>= putBufferDyn.(BufferMarks newMarks `mappend`)
+        unless (null newDepMarks) $
+            getBufferDyn >>= putBufferDyn.(DependentMarks newDepMarks `mappend`)
+        moveToNextBufferMark deleteLast
+    return a
+  where
+    len1 (_:[]) = True
+    len1 _      = False
+
+    belongTogether a b = userIndex a == userIndex b
+
+updateUpdatedMarks :: [Update] -> BufferM ()
+updateUpdatedMarks upds = findEditedMarks upds >>=
+                          mapM_ updateDependents
+
+findEditedMarks :: [Update] -> BufferM [MarkInfo]
+findEditedMarks upds = fmap (nub . concat) (mapM findEditedMarks' upds)
+  where
+    findEditedMarks' :: Update -> BufferM [MarkInfo]
+    findEditedMarks' upd = do
+        let p = updatePoint upd
+        ms <- return . nub . concat . marks =<< getBufferDyn
+        ms' <- forM ms $ \m ->do
+                 r <- adjMarkRegion m
+                 return $ if (updateIsDelete upd && p `nearRegion` r)
+                             || p `inRegion` r
+                          then Just m
+                          else Nothing
+        return . catMaybes $ ms'
+
+dependentSiblings :: MarkInfo -> [[MarkInfo]] -> [MarkInfo]
+dependentSiblings mark deps =
+  case find (elem mark) deps of
+    Nothing -> []
+    Just lst -> filter (not . (mark==)) lst
+
+updateDependents :: MarkInfo -> BufferM ()
+updateDependents m = getBufferDyn >>= updateDependents' m . marks
+
+updateDependents' :: MarkInfo -> [[MarkInfo]] -> BufferM ()
+updateDependents' mark deps =
+  case dependentSiblings mark deps of
+    []   -> return ()
+    deps' -> do
+      txt <- markText mark
+      forM_ deps' $ \d -> do
+        dTxt <- markText d
+        when (txt /= dTxt) $ setMarkText txt d
+
+markText :: MarkInfo -> BufferM R.YiString
+markText m = markRegion m >>= readRegionB
+
+setMarkText :: R.YiString -> MarkInfo -> BufferM ()
+setMarkText txt (SimpleMarkInfo _ start) = do
+  p <- use $ markPointA start
+  c <- readAtB p
+  if isSpace c
+    then insertNAt txt p
+    else do
+      r <- regionOfPartNonEmptyAtB unitViWordOnLine Forward p
+      modifyRegionB (const txt) r
+
+setMarkText txt mi = do
+    start <- use $ markPointA $ startMark mi
+    end   <- use $ markPointA $ endMark mi
+    let r = mkRegion start end
+    modifyRegionB (const txt) r
+    when (start == end) $
+      markPointA (endMark mi) .= end + Point (R.length txt)
+
+withSimpleRegion :: MarkInfo -> (Region -> BufferM Region) -> BufferM Region
+withSimpleRegion (SimpleMarkInfo _ s) f = do
+    p <- use $ markPointA s
+    c <- readAtB p
+    if isSpace c
+      then return $ mkRegion p p  -- return empty region
+      else f =<< regionOfPartNonEmptyAtB unitViWordOnLine Forward p
+withSimpleRegion r _ = error $ "withSimpleRegion: " <> show r
+
+markRegion :: MarkInfo -> BufferM Region
+markRegion m@SimpleMarkInfo{} = withSimpleRegion m $ \r -> do
+    os <- findOverlappingMarksWith safeMarkRegion concat True r m
+    rOs <- mapM safeMarkRegion os
+    return . mkRegion (regionStart r) $ foldl' minEnd (regionEnd r) rOs
+  where
+    minEnd end r = if regionEnd r < end
+                   then end
+                   else min end $ regionStart r
+
+markRegion m = liftM2 mkRegion
+                   (use $ markPointA $ startMark m)
+                   (use $ markPointA $ endMark m)
+
+safeMarkRegion :: MarkInfo -> BufferM Region
+safeMarkRegion m@(SimpleMarkInfo _ _) = withSimpleRegion m return
+safeMarkRegion m = markRegion m
+
+adjMarkRegion :: MarkInfo -> BufferM Region
+adjMarkRegion s@(SimpleMarkInfo _ _) = markRegion s
+
+adjMarkRegion m = do
+    s <- use $ markPointA $ startMark m
+    e <- use $ markPointA $ endMark m
+    c <- readAtB e
+    when (isWordChar c) $ do adjustEnding e
+                             repairOverlappings e
+    e' <- use $ markPointA $ endMark m
+    s' <- adjustStart s e'
+    return $ mkRegion s' e'
+  where
+    adjustEnding end = do
+        r' <- regionOfPartNonEmptyAtB unitViWordOnLine Forward end
+        markPointA (endMark m) .= (regionEnd r')
+
+    adjustStart s e = do
+        txt <- readRegionB (mkRegion s e)
+        let sP = s + (Point . R.length $ R.takeWhile isSpace txt)
+        when (sP > s) $
+            markPointA (startMark m) .= sP
+        return sP
+
+    -- test if we generated overlappings and repair
+    repairOverlappings origEnd = do overlappings <- allOverlappingMarks True m
+                                    unless (null overlappings) $
+                                        markPointA (endMark m) .= origEnd
+
+findOverlappingMarksWith :: (MarkInfo -> BufferM Region)
+                         -> ([[MarkInfo]] -> [MarkInfo])
+                         -> Bool -> Region -> MarkInfo -> BufferM [MarkInfo]
+findOverlappingMarksWith fMarkRegion flattenMarks border r m =
+  let markFilter = filter (m /=) . flattenMarks . marks
+      regOverlap = fmap (regionsOverlap border r) . fMarkRegion
+  in fmap markFilter getBufferDyn >>= filterM regOverlap
+
+findOverlappingMarks :: ([[MarkInfo]] -> [MarkInfo]) -> Bool -> Region ->
+                        MarkInfo -> BufferM [MarkInfo]
+findOverlappingMarks = findOverlappingMarksWith markRegion
+
+regionsOverlappingMarks :: Bool -> Region -> MarkInfo -> BufferM [MarkInfo]
+regionsOverlappingMarks = findOverlappingMarks concat
+
+overlappingMarks :: Bool -> Bool -> MarkInfo -> BufferM [MarkInfo]
+overlappingMarks border belongingTogether mark = do
+    r <- markRegion mark
+    findOverlappingMarks (if belongingTogether
+                          then dependentSiblings mark
+                          else concat)
+                         border
+                         r
+                         mark
+
+allOverlappingMarks :: Bool -> MarkInfo -> BufferM [MarkInfo]
+allOverlappingMarks border = overlappingMarks border False
+
+dependentOverlappingMarks :: Bool -> MarkInfo -> BufferM [MarkInfo]
+dependentOverlappingMarks border = overlappingMarks border True
+
+nextBufferMark :: Bool -> BufferM (Maybe MarkInfo)
+nextBufferMark deleteLast = do
+  BufferMarks ms <- getBufferDyn
+  if null ms
+    then return Nothing
+    else do
+      let mks = if deleteLast then const $ tail ms else (tail ms <>)
+      putBufferDyn . BufferMarks . mks $ [head ms]
+      return . Just $ head ms
+
+isDependentMarker :: (MonadState FBuffer m, Functor m) => Mark -> m Bool
+isDependentMarker bMark = do
+    DependentMarks ms <- getBufferDyn
+    return . elem bMark . concatMap bufferMarkers . concat $ ms
+
+safeDeleteMarkB :: Mark -> BufferM ()
+safeDeleteMarkB m = do
+    b <- isDependentMarker m
+    unless b (deleteMarkB m)
+
+moveToNextBufferMark :: Bool -> BufferM ()
+moveToNextBufferMark deleteLast = nextBufferMark deleteLast >>= \case
+  Just p  -> mv p
+  Nothing -> return ()
+  where
+    mv (SimpleMarkInfo _ m)   = do
+        moveTo =<< use (markPointA m)
+        when deleteLast $ safeDeleteMarkB m
+
+    mv (ValuedMarkInfo _ s e) = do
+        sp <- use $ markPointA s
+        ep <- use $ markPointA e
+        deleteRegionB (mkRegion sp ep)
+        moveTo sp
+        when deleteLast $ do
+            safeDeleteMarkB s
+            safeDeleteMarkB e
+
+    mv r = error $ "moveToNextBufferMark.mv: " <> show r
+
+-- Keymap support
+
+newtype SupertabExt = Supertab (R.YiString -> Maybe (BufferM ()))
+
+instance Monoid SupertabExt where
+  mempty = Supertab $ const Nothing
+
+  (Supertab f) `mappend` (Supertab g) =
+    Supertab $ \s -> f s `mplus` g s
+
+superTab :: (MonadInteract m Action Event) => Bool -> SupertabExt -> m ()
+superTab caseSensitive (Supertab expander) =
+    some (spec KTab ?>>! doSuperTab) >> deprioritize >>! resetComplete
+  where
+    doSuperTab = do canExpand <- withCurrentBuffer $ do
+                                   sol <- atSol
+                                   ws  <- hasWhiteSpaceBefore
+                                   return $ sol || ws
+                    if canExpand
+                      then insertTab
+                      else runCompleter
+
+    insertTab = withCurrentBuffer $ mapM_ insertB =<< tabB
+
+    runCompleter = do w <- withCurrentBuffer readPrevWordB
+                      case expander w of
+                        Just cmd -> withCurrentBuffer $ bkillWordB >> cmd
+                        _        -> autoComplete
+
+    autoComplete = wordCompleteString' caseSensitive >>=
+                   withCurrentBuffer . (bkillWordB >>) . (insertN . R.fromText)
+
+-- | Convert snippet description list into a SuperTab extension
+fromSnippets :: Bool -> [(R.YiString, SnippetCmd ())] -> SupertabExt
+fromSnippets deleteLast snippets =
+  Supertab $ \str -> lookup str $ map (second $ runSnippet deleteLast) snippets
+
+snippet :: MkSnippetCmd a b => a -> SnippetCmd b
+snippet = mkSnippetCmd
diff --git a/src/Yi/Snippets/Haskell.hs b/src/Yi/Snippets/Haskell.hs
new file mode 100644
--- /dev/null
+++ b/src/Yi/Snippets/Haskell.hs
@@ -0,0 +1,22 @@
+{-# OPTIONS_HADDOCK show-extensions #-}
+
+-- |
+-- Module      :  Yi.Snippets.Haskell
+-- License     :  GPL-2
+-- Maintainer  :  yi-devel@googlegroups.com
+-- Stability   :  experimental
+-- Portability :  portable
+
+module Yi.Snippets.Haskell where
+
+import qualified Yi.Rope     as R (singleton)
+import           Yi.Snippets (SnippetCmd, cursor, cursorWith, snippet, (&))
+
+hsFunction :: SnippetCmd ()
+hsFunction = snippet $
+  cursorWith 1 (R.singleton 'f') & " :: " & cursor 2 & "\n" &
+  cursor 3 & " = " & cursor 4 & "\n"
+
+hsClass :: SnippetCmd ()
+hsClass = snippet $
+  "class " & cursor 1 & " " & cursor 2 & " where\n  " & cursor 3
diff --git a/src/Yi/String.hs b/src/Yi/String.hs
new file mode 100644
--- /dev/null
+++ b/src/Yi/String.hs
@@ -0,0 +1,147 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# OPTIONS_HADDOCK show-extensions #-}
+
+-- |
+-- Module      :  Yi.String
+-- License     :  GPL-2
+-- Maintainer  :  yi-devel@googlegroups.com
+-- Stability   :  experimental
+-- Portability :  portable
+--
+-- String manipulation utilities
+
+module Yi.String (isBlank,
+                  chomp,
+                  capitalize,
+                  capitalizeFirst,
+                  dropSpace,
+                  fillText,
+                  onLines,
+                  mapLines,
+                  lines',
+                  unlines',
+                  padLeft, padRight,
+                  commonTPrefix,
+                  commonTPrefix',
+                  listify,
+                  showT,
+                  overInit, overTail
+                 ) where
+
+import           Data.Char   (isAlphaNum, isSpace, toLower, toUpper)
+import           Data.List   (isSuffixOf)
+import           Data.Maybe  (fromMaybe)
+import           Data.Monoid ((<>))
+import qualified Data.Text   as T (Text, break, commonPrefixes, empty,
+                                   intercalate, pack, splitAt, splitOn, toUpper)
+import qualified Yi.Rope     as R (YiString, all, cons, head, init, intercalate,
+                                   last, length, lines', snoc, tail, unwords,
+                                   withText, words)
+
+-- | Helper that shows then packs the 'Text', for all those cases
+-- where we use 'show'.
+showT :: Show a => a -> T.Text
+showT = T.pack . show
+
+-- | This is kind of like the default Show instance for lists except
+-- over 'T.Text'. It does not leave the elements in extra quotes and
+-- should not be attempted to be 'show'n and 'read' back.
+listify :: [R.YiString] -> R.YiString
+listify t = '[' `R.cons` R.intercalate ", " t `R.snoc` ']'
+
+-- | Works by resupplying the found prefix back into the list,
+-- eventually either finding the prefix or not matching.
+commonTPrefix :: [T.Text] -> Maybe T.Text
+commonTPrefix (x:y:xs) = case T.commonPrefixes x y of
+  Nothing -> Nothing
+  Just (p, _, _) -> commonTPrefix (p : xs)
+commonTPrefix [x] = Just x
+commonTPrefix _ = Nothing
+
+-- | Like 'commonTPrefix' but returns empty text on failure.
+commonTPrefix' :: [T.Text] -> T.Text
+commonTPrefix' = fromMaybe T.empty . commonTPrefix
+
+capitalize :: String -> String
+capitalize [] = []
+capitalize (c:cs) = toUpper c : map toLower cs
+
+capitalizeFirst :: R.YiString -> R.YiString
+capitalizeFirst = R.withText go
+  where
+    go x = case T.break isAlphaNum x of
+      (f, b) -> f <> case T.splitAt 1 b of
+        (h, hs) -> T.toUpper h <> hs
+
+-- | Remove any trailing strings matching /irs/ (input record separator)
+-- from input string. Like perl's chomp(1).
+chomp :: String -> String -> String
+chomp irs st
+    | irs `isSuffixOf` st
+    = let st' = reverse $ drop (length irs) (reverse st) in chomp irs st'
+    | otherwise = st
+{-# INLINE chomp #-}
+
+
+-- | Trim spaces at beginning /and/ end
+dropSpace :: String -> String
+dropSpace = let f = reverse . dropWhile isSpace in f . f
+
+isBlank :: R.YiString -> Bool
+isBlank = R.all isSpace
+
+-- | Fills lines up to the given length, splitting the text up if
+-- necessary.
+fillText :: Int -> R.YiString -> [R.YiString]
+fillText margin = map (R.unwords . reverse) . fill 0 [] . R.words
+  where
+    fill _ acc [] = [acc]
+    fill n acc (w:ws)
+      | n + R.length w >= margin = acc : fill (R.length w) [w] ws
+      | otherwise = fill (n + 1 + R.length w) (w:acc) ws
+
+-- | @overInit f@ runs f over the 'R.init' of the input if possible,
+-- preserving the 'R.last' element as-is. If given a string with
+-- length ≤ 1, it effectively does nothing.
+--
+-- Also see 'overTail'.
+overInit :: (R.YiString -> R.YiString) -> R.YiString -> R.YiString
+overInit f t = case (R.init t, R.last t) of
+  (Just xs, Just x) -> f xs `R.snoc` x
+  _ -> t
+
+-- | @overInit f@ runs f over the 'R.tail' of the input if possible,
+-- preserving the 'R.head' element as-is. If given a string with
+-- length ≤ 1, it effectively does nothing.
+--
+-- Also see 'overInit'.
+overTail :: (R.YiString -> R.YiString) -> R.YiString -> R.YiString
+overTail f t = case (R.head t, R.tail t) of
+  (Just x, Just xs) -> x `R.cons` f xs
+  _ -> t
+
+-- | Inverse of 'lines''. In contrast to 'Prelude.unlines', this does
+-- not add an empty line at the end.
+unlines' :: [T.Text] -> T.Text
+unlines' = T.intercalate "\n"
+
+-- | Split a Text in lines. Unlike 'Prelude.lines', this does not
+-- remove any empty line at the end.
+lines' :: T.Text -> [T.Text]
+lines' = T.splitOn "\n"
+
+-- | A helper function for creating functions suitable for
+-- 'modifySelectionB' and 'modifyRegionB'.
+-- To be used when the desired function should map across
+-- the lines of a region.
+mapLines :: (R.YiString -> R.YiString) -> R.YiString -> R.YiString
+mapLines f = onLines $ fmap f
+
+onLines :: ([R.YiString] -> [R.YiString]) -> R.YiString -> R.YiString
+onLines f = mconcat . f . R.lines'
+
+padLeft, padRight :: Int -> String -> String
+padLeft n [] = replicate n ' '
+padLeft n (x:xs) = x : padLeft (n-1) xs
+
+padRight n = reverse . padLeft n . reverse
diff --git a/src/Yi/Syntax/Driver.hs b/src/Yi/Syntax/Driver.hs
new file mode 100644
--- /dev/null
+++ b/src/Yi/Syntax/Driver.hs
@@ -0,0 +1,58 @@
+{-# LANGUAGE ScopedTypeVariables, ExistentialQuantification, MultiParamTypeClasses #-}
+{-# OPTIONS_GHC -fno-warn-name-shadowing #-}
+
+-- | This module defines implementations of syntax-awareness drivers.
+
+module Yi.Syntax.Driver where
+
+import           Data.Map        (Map)
+import qualified Data.Map        as M (Map, assocs, empty, findWithDefault, fromList)
+import           Yi.Buffer.Basic (WindowRef)
+import           Yi.Lexer.Alex   (Tok)
+import           Yi.Syntax       hiding (Cache)
+import           Yi.Syntax.Tree  (IsTree, fromNodeToFinal)
+
+type Path = [Int]
+
+data Cache state tree tt = Cache {
+                                   path :: M.Map WindowRef Path,
+                                   cachedStates :: [state],
+                                   root :: tree (Tok tt),
+                                   focused :: !(M.Map WindowRef (tree (Tok tt)))
+                                 }
+
+mkHighlighter :: forall state tree tt. (IsTree tree, Show state) =>
+                 (Scanner Point Char -> Scanner state (tree (Tok tt))) ->
+                     Highlighter (Cache state tree tt) (tree (Tok tt))
+mkHighlighter scanner =
+  Yi.Syntax.SynHL
+        { hlStartState   = Cache M.empty [] emptyResult M.empty
+        , hlRun          = updateCache
+        , hlGetTree      = \(Cache _ _ _ focused) w -> M.findWithDefault emptyResult w focused
+        , hlFocus        = focus
+        }
+    where startState :: state
+          startState = scanInit    (scanner emptyFileScan)
+          emptyResult = scanEmpty (scanner emptyFileScan)
+          updateCache :: Scanner Point Char -> Point -> Cache state tree tt -> Cache state tree tt
+          updateCache newFileScan dirtyOffset (Cache path cachedStates oldResult _) = Cache path newCachedStates newResult M.empty
+            where newScan = scanner newFileScan
+                  reused :: [state]
+                  reused = takeWhile ((< dirtyOffset) . scanLooked (scanner newFileScan)) cachedStates
+                  resumeState :: state
+                  resumeState = if null reused then startState else last reused
+
+                  newCachedStates = reused ++ fmap fst recomputed
+                  recomputed = scanRun newScan resumeState
+                  newResult :: tree (Tok tt)
+                  newResult = if null recomputed then oldResult else snd $ head recomputed
+          focus r (Cache path states root _focused) =
+              Cache path' states root focused
+              where (path', focused) = unzipFM $ zipWithFM (\newpath oldpath -> fromNodeToFinal newpath (oldpath,root)) [] r path
+
+unzipFM :: Ord k => [(k,(u,v))] -> (Map k u, Map k v)
+unzipFM l = (M.fromList mu, M.fromList mv)
+    where (mu, mv) = unzip [((k,u),(k,v)) | (k,(u,v)) <- l]
+
+zipWithFM :: Ord k => (u -> v -> w) -> v -> Map k u -> Map k v -> [(k,w)]
+zipWithFM f v0 mu mv = [ (k,f u (M.findWithDefault v0 k mv) ) | (k,u) <- M.assocs mu]
diff --git a/src/Yi/Syntax/Layout.hs b/src/Yi/Syntax/Layout.hs
new file mode 100644
--- /dev/null
+++ b/src/Yi/Syntax/Layout.hs
@@ -0,0 +1,147 @@
+{-# LANGUAGE PatternGuards       #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+
+-- Note: If the first line of the file has wrong indentation, some of the
+-- code might be left outside of the blocks
+module Yi.Syntax.Layout (layoutHandler, State) where
+
+import           Data.List     (find)
+import           Data.Maybe    (isJust)
+import           Yi.Lexer.Alex (AlexState (..), Posn (Posn), Tok (Tok, tokPosn, tokT), startPosn)
+import           Yi.Syntax     (Scanner (..))
+
+data BlockOpen t = Indent Int -- block opened because of indentation; parameter is the column of it.
+                 | Paren t      -- block opened because of parentheses
+                 deriving Show
+
+isParen :: BlockOpen t -> Bool
+isParen (Paren _) = True
+isParen _ = False
+
+data IState t = IState [BlockOpen t]  -- current block nesting
+                     Bool   -- should we open a compound now ?
+                     Int    -- last line number
+  deriving Show
+type State t lexState = (IState t, AlexState lexState)
+
+
+-- | Transform a scanner into a scanner that also adds opening,
+-- closing and "next" tokens to indicate layout.
+
+-- @isSpecial@ predicate indicates a token that starts a compound,
+-- like "where", "do", ...
+
+-- @isIgnore@ predicate indicates a token that is to be ignored for
+-- layout. (eg. pre-processor directive...)
+
+-- @parens@ is a list of couple of matching parenthesis-like tokens
+-- "()[]{}...".
+
+
+layoutHandler :: forall t lexState. (Show t, Eq t) => (t -> Bool) -> [(t,t)] ->
+            (Tok t -> Bool) ->
+            (t,t,t) -> (Tok t -> Bool) ->
+            Scanner (AlexState lexState) (Tok t) -> Scanner (State t lexState) (Tok t)
+layoutHandler isSpecial parens isIgnored (openT, closeT, nextT) isGroupOpen lexSource = Scanner
+  {
+   scanLooked = scanLooked lexSource . snd,
+   scanEmpty = error "layoutHandler: scanEmpty",
+   scanInit = (IState [] True (-1), scanInit lexSource),
+   scanRun  = \st -> let result = parse (fst st) (scanRun lexSource (snd st))
+                     in --trace ("toks = " ++ show (fmap snd result)) $
+                        result
+  }
+    where dummyAlexState = AlexState
+              {
+               stLexer = error "dummyAlexState: should not be reused for restart!",
+               lookedOffset = maxBound, -- setting this to maxBound ensures nobody ever uses it.
+               stPosn = startPosn
+              }
+
+          deepestIndent [] = -1
+          deepestIndent (Indent i:_) = i
+          deepestIndent (_:levs) = deepestIndent levs
+
+          deepestParen _ [] = False
+          deepestParen p (Paren t:levs) = p == t || deepestParen p levs
+          deepestParen p (_:levs) = deepestParen p levs
+
+          findParen f t = find ((== t) . f) parens
+
+          parse :: IState t -> [(AlexState lexState, Tok t)] -> [(State t lexState, Tok t)]
+          parse iSt@(IState levels doOpen lastLine)
+                toks@((aSt, tok @ Tok {tokPosn = Posn _nextOfs line col}) : tokss)
+
+            -- ignore this token
+            | isIgnored tok
+              = (st, tok) : parse (IState levels doOpen line) tokss
+
+            -- start a compound if the rest of the line is empty then skip to it!
+            | doOpen
+              = if isGroupOpen tok -- check so that the do is not followed by a {
+                  then parse (IState levels False lastLine) toks
+                  else (st', tt openT) : parse (IState (Indent col : levels) False line) toks
+                  -- if it's a block opening, we ignore the layout, and just let the "normal" rule
+                  -- handle the creation of another level.
+
+            -- close, or prepare to close, a paren block
+            | Just (openTok,_) <- findParen snd $ tokT tok,
+              deepestParen openTok levels
+
+              = case levels of
+                      Indent _:levs -> (st',tt closeT) : parse (IState levs False lastLine) toks
+                      -- close an indent level inside the paren block
+                      Paren openTok' :levs
+                          | openTok == openTok' -> (st', tok) : parse (IState levs False line) tokss
+                          | otherwise           ->              parse (IState levs False line) toks
+                      -- close one level of nesting.
+                      [] -> error $ "Parse: " ++ show iSt
+
+            -- pop an indent block
+            | col < deepestIndent levels
+              = let (_lev:levs) = dropWhile isParen levels
+                in (st', tt closeT) : parse (IState levs doOpen lastLine) toks
+                  -- drop all paren levels inside the indent
+
+            -- next item
+            | line > lastLine &&
+              col == deepestIndent levels
+                = (st', tt nextT) : parse (IState (dropWhile isParen levels) doOpen line) toks
+                  -- drop all paren levels inside the indent
+
+            -- open a paren block
+            | isJust $ findParen fst $ tokT tok
+              = (st', tok) : parse (IState (Paren (tokT tok):levels) (isSpecial (tokT tok)) line) tokss
+              -- important note: the the token can be both special and an opening. This is the case of the
+              -- haskell 'let' (which is closed by 'in'). In that case the inner block is that of the indentation.
+
+            -- prepare to open a compound
+            | isSpecial (tokT tok)
+                = (st', tok) : parse (IState levels True   line) tokss
+
+            | otherwise
+                = (st', tok) : parse (IState levels doOpen line) tokss
+                  where st = (iSt, aSt)
+                        st' = (iSt, aSt {lookedOffset = max peeked (lookedOffset aSt)})
+                        tt t = Tok t 0 (tokPosn tok)
+                        peeked = case tokss of
+                                   [] -> maxBound
+                                   (AlexState {lookedOffset = p},_):_ -> p
+                        -- This function checked the position and kind of the
+                        -- next token.  We peeked further, and so must
+                        -- update the lookedOffset accordingly.
+
+          -- finish by closing all the indent states.
+          parse iSt@(IState (Indent _:levs) doOpen posn) []
+              = ((iSt,dummyAlexState), Tok closeT 0 maxPosn) : parse (IState levs doOpen posn) []
+          parse (IState (Paren _:levs) doOpen posn) []
+              = parse (IState levs doOpen posn) []
+          parse (IState [] _ _) [] = []
+
+
+maxPosn :: Posn
+maxPosn = Posn (-1) (-1) 0
+-- HACK! here we have collusion between using (-1) here and the parsing of
+-- OnlineTrees, which relies on the position of the last token to stop
+-- the parsing.
+
diff --git a/src/Yi/Syntax/OnlineTree.hs b/src/Yi/Syntax/OnlineTree.hs
new file mode 100644
--- /dev/null
+++ b/src/Yi/Syntax/OnlineTree.hs
@@ -0,0 +1,43 @@
+{-# LANGUAGE DeriveFoldable    #-}
+{-# LANGUAGE DeriveFunctor     #-}
+{-# LANGUAGE DeriveTraversable #-}
+{-# OPTIONS_GHC -fno-warn-incomplete-patterns #-} -- uniplate patterns
+{-# OPTIONS_HADDOCK show-extensions #-}
+
+-- |
+-- Module      :  Yi.Syntax.OnlineTree
+-- License     :  GPL-2
+-- Maintainer  :  yi-devel@googlegroups.com
+-- Stability   :  experimental
+-- Portability :  portable
+--
+-- Module defining the 'Tree' used as part of many 'Mode's.
+
+module Yi.Syntax.OnlineTree (Tree(..), manyToks,
+                             tokAtOrBefore) where
+
+import Yi.IncrementalParse (P, Parser (Look), symbol)
+import Yi.Lexer.Alex       (Tok)
+import Yi.Syntax.Tree      (IsTree (emptyNode, uniplate), tokAtOrBefore)
+
+data Tree a = Bin (Tree a) (Tree a)
+            | Leaf a
+            | Tip
+              deriving (Show, Functor, Foldable, Traversable)
+
+instance IsTree Tree where
+    emptyNode = Tip
+    uniplate (Bin l r) = ([l,r],\[l',r'] -> Bin l' r')
+    uniplate t = ([],const t)
+
+manyToks :: P (Tok t) (Tree (Tok t))
+manyToks = manyToks' 1
+
+manyToks' :: Int -> P a (Tree a)
+manyToks' n = Look (pure Tip) (\_ -> Bin <$> subTree n <*> manyToks' (n * 2))
+
+subTree :: Int -> P a (Tree a)
+subTree n = Look (pure Tip) . const $ case n of
+  0 -> pure Tip
+  1 -> Leaf <$> symbol (const True)
+  _ -> let m = n `div` 2 in Bin <$> subTree m <*> subTree m
diff --git a/src/Yi/Syntax/Tree.hs b/src/Yi/Syntax/Tree.hs
new file mode 100644
--- /dev/null
+++ b/src/Yi/Syntax/Tree.hs
@@ -0,0 +1,424 @@
+{-# LANGUAGE CPP                 #-}
+{-# LANGUAGE DeriveFoldable      #-}
+{-# LANGUAGE FlexibleInstances   #-}
+{-# LANGUAGE OverloadedStrings   #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+
+ -- the CPP seems to confuse GHC; we have uniplate patterns
+{-# OPTIONS_GHC -fno-warn-unused-binds -fno-warn-incomplete-patterns #-}
+{-# OPTIONS_HADDOCK show-extensions #-}
+
+-- |
+-- Module      :  Yi.Syntax.Tree
+-- License     :  GPL-2
+-- Maintainer  :  yi-devel@googlegroups.com
+-- Stability   :  experimental
+-- Portability :  portable
+
+-- Generic syntax tree handling functions
+module Yi.Syntax.Tree (IsTree(..), toksAfter, allToks, tokAtOrBefore,
+                       toksInRegion, sepBy, sepBy1,
+                       getLastOffset, getFirstOffset,
+                       getFirstElement, getLastElement,
+                       getLastPath,
+                       getAllSubTrees,
+                       tokenBasedAnnots, tokenBasedStrokes,
+                       subtreeRegion,
+                       fromLeafToLeafAfter, fromNodeToFinal)
+  where
+
+-- Some of this might be replaced by a generic package
+-- such as multirec, uniplace, emgm, ...
+
+import           Prelude             hiding (concatMap, error)
+
+import           Control.Applicative (Alternative ((<|>), many))
+import           Control.Arrow       (first)
+import           Data.Foldable       (concatMap, toList)
+import           Data.List.NonEmpty  (NonEmpty (..))
+import qualified Data.List.NonEmpty  as NE (reverse, toList, (<|))
+import           Data.Maybe          (catMaybes, listToMaybe)
+import           Data.Monoid         (First (First, getFirst), Last (Last, getLast), (<>))
+import           Yi.Buffer.Basic     (Point)
+import           Yi.Debug            (error, trace)
+import           Yi.Lexer.Alex       (Posn (Posn, posnLine, posnOfs),
+                                      Tok (Tok, tokPosn), tokBegin, tokEnd)
+import           Yi.Region           (Region (regionEnd, regionStart),
+                                      includedRegion, mkRegion)
+import           Yi.String           (showT)
+
+#ifdef TESTING
+import           Test.QuickCheck
+import           Test.QuickCheck.Property (unProperty)
+#endif
+
+-- Fundamental types
+type Path = [Int]
+type Node t = (Path, t)
+
+class Foldable tree => IsTree tree where
+    -- | Direct subtrees of a tree
+    subtrees :: tree t -> [tree t]
+    subtrees = fst . uniplate
+    uniplate :: tree t -> ([tree t], [tree t] -> tree t)
+    emptyNode :: tree t
+
+toksAfter :: Foldable t1 => t -> t1 a -> [a]
+toksAfter _begin = allToks
+
+allToks :: Foldable t => t a -> [a]
+allToks = toList
+
+tokAtOrBefore :: Foldable t => Point -> t (Tok t1) -> Maybe (Tok t1)
+tokAtOrBefore p res =
+  listToMaybe $ reverse $ toksInRegion (mkRegion 0 (p+1)) res
+
+toksInRegion :: Foldable t1 => Region -> t1 (Tok t) -> [Tok t]
+toksInRegion reg = takeWhile (\t -> tokBegin t <= regionEnd   reg)
+                   . dropWhile (\t -> tokEnd t < regionStart reg)
+                   . toksAfter (regionStart reg)
+
+tokenBasedAnnots :: (Foldable t1) => (a1 -> Maybe a) -> t1 a1 -> t -> [a]
+tokenBasedAnnots tta t begin = catMaybes (tta <$> toksAfter begin t)
+
+tokenBasedStrokes :: (Foldable t3) => (a -> b) -> t3 a -> t -> t2 -> t1 -> [b]
+tokenBasedStrokes tts t _point begin _end = tts <$> toksAfter begin t
+
+-- | Prune the nodes before the given point.
+-- The path is used to know which nodes we can force or not.
+pruneNodesBefore :: IsTree tree => Point -> Path -> tree (Tok a) -> tree (Tok a)
+pruneNodesBefore _ [] t = t
+pruneNodesBefore p (x:xs) t = rebuild $ left' <> (pruneNodesBefore p xs c : rs)
+    where (children,rebuild) = uniplate t
+          (left,c:rs) = splitAt x children
+          left' = fmap replaceEmpty left
+          replaceEmpty s = if getLastOffset s < p then emptyNode else s
+
+-- | Given an approximate path to a leaf at the end of the region,
+-- return: (path to leaf at the end of the region,path from focused
+-- node to the leaf, small node encompassing the region)
+fromNodeToFinal :: IsTree tree => Region -> Node (tree (Tok a))
+                -> Node (tree (Tok a))
+fromNodeToFinal r (xs,root) =
+    trace ("r = " <> showT r) $
+    trace ("focused ~ " <> showT (subtreeRegion focused) ) $
+    trace ("pathFromFocusedToLeaf = " <> showT focusedToLeaf) $
+    trace ("pruned ~ " <> showT (subtreeRegion focused)) (xs', pruned)
+
+    where n@(xs',_) = fromLeafToLeafAfter (regionEnd r) (xs,root)
+          (_,(focusedToLeaf,focused)) = fromLeafAfterToFinal p0 n
+          p0 = regionStart r
+          pruned = pruneNodesBefore p0 focusedToLeaf focused
+
+-- | Return the first element that matches the predicate, or the last
+-- of the list if none matches.
+firstThat :: (a -> Bool) -> NonEmpty a -> a
+firstThat _ (x :| []) = x
+firstThat p (x :| [y]) = if p x then x else y
+firstThat p (x :| y : xs) = if p x then x else firstThat p (y :| xs)
+
+-- | Return the element before first element that violates the
+-- predicate, or the first of the list if that one violates the
+-- predicate.
+lastThat :: (a -> Bool) -> NonEmpty a -> a
+lastThat p (x :| xs) = if p x then work x xs else x
+    where work x0 [] = x0
+          work x0 (y:ys) = if p y then work y ys else x0
+
+-- | Given a path to a node, return a path+node which node that
+-- encompasses the given node + a point before it.
+fromLeafAfterToFinal :: IsTree tree => Point -> Node (tree (Tok a))
+                     -> (Path, Node (tree (Tok a)))
+fromLeafAfterToFinal p n =
+    -- trace ("reg = " <> showT (fmap (subtreeRegion . snd) nsPth)) $
+      firstThat (\(_,(_,s)) -> getFirstOffset s <= p) ns
+    where ns = NE.reverse (nodesOnPath n)
+
+-- | Search the tree in pre-order starting at a given node, until
+-- finding a leaf which is at or after the given point. An effort is
+-- also made to return a leaf as close as possible to @p@.
+--
+-- TODO: rename to fromLeafToLeafAt
+fromLeafToLeafAfter :: IsTree tree => Point
+                    -> Node (tree (Tok a))
+                    -> Node (tree (Tok a))
+fromLeafToLeafAfter p (xs, root) =
+  trace "fromLeafToLeafAfter:" $
+  trace ("xs = " <> showT xs) $
+  trace ("xsValid = " <> showT xsValid) $
+  trace ("p = " <> showT p) $
+  trace ("leafBeforeP = " <> showT leafBeforeP) $
+  trace ("leaf ~ " <> showT (subtreeRegion leaf)) $
+  trace ("xs' = " <> showT xs') result
+  where
+    xs' = case candidateLeaves of
+      [] -> []
+      c:cs -> fst $ firstOrLastThat (\(_,s) -> getFirstOffset s >= p) (c :| cs)
+    candidateLeaves = allLeavesRelative relChild n
+    (firstOrLastThat,relChild) = if leafBeforeP then (firstThat,afterChild)
+                                                else (lastThat,beforeChild)
+    (xsValid,leaf) = wkDown (xs,root)
+    leafBeforeP = getFirstOffset leaf <= p
+    n = (xsValid,root)
+    result = (xs',root)
+
+allLeavesRelative :: IsTree tree => (Int -> [(Int, tree a)] -> [(Int, tree a)])
+                  -> Node (tree a)
+                  -> [Node (tree a)]
+allLeavesRelative select
+   = filter (not . nullSubtree . snd) . allLeavesRelative' select
+     . NE.toList . NE.reverse . nodesAndChildIndex
+     -- we remove empty subtrees because their region is [0,0].
+
+-- | Takes a list of (node, index of already inspected child), and
+-- return all leaves in this node after the said child).
+allLeavesRelative' :: IsTree tree => (Int -> [(Int, tree a)] -> [(Int, tree a)])
+                   -> [(Node (tree a), Int)] -> [Node (tree a)]
+allLeavesRelative' select l =
+  [(xs <> xs', t') | ((xs,t),c) <- l
+                   , (xs',t') <- allLeavesRelativeChild select c t]
+
+-- | Given a root, return all the nodes encountered along it, their
+-- paths, and the index of the child which comes next.
+nodesAndChildIndex :: IsTree tree => Node (tree a)
+                   -> NonEmpty (Node (tree a), Int)
+nodesAndChildIndex ([],t) = return (([],t),negate 1)
+nodesAndChildIndex (x:xs, t) = case index x (subtrees t) of
+  Just c' -> (([],t), x)
+             NE.<| fmap (first $ first (x:)) (nodesAndChildIndex (xs,c'))
+  Nothing -> return (([],t),negate 1)
+
+nodesOnPath :: IsTree tree => Node (tree a) -> NonEmpty (Path, Node (tree a))
+nodesOnPath ([],t) = return ([],([],t))
+nodesOnPath (x:xs,t) = ([],(x:xs,t)) NE.<| case index x (subtrees t) of
+  Nothing -> error "nodesOnPath: non-existent path"
+  Just c -> fmap (first (x:)) (nodesOnPath (xs,c))
+
+
+beforeChild :: Int -> [a] -> [a]
+
+beforeChild (-1) = reverse -- (-1) indicates that all children should be taken.
+beforeChild c = reverse . take (c-1)
+
+afterChild :: Int -> [a] -> [a]
+afterChild c = drop (c+1)
+
+-- | Return all leaves after or before child depending on the relation
+-- which is given.
+allLeavesRelativeChild :: IsTree tree => (Int -> [(Int, tree a)]
+                                          -> [(Int, tree a)])
+                       -> Int
+                       -> tree a -> [Node (tree a)]
+allLeavesRelativeChild select c t
+  | null ts = return ([], t)
+  | otherwise = [(x:xs,t') | (x,ct) <- select c (zip [0..] ts),
+                 (xs, t') <- allLeavesIn select ct]
+ where ts = subtrees t
+
+
+-- | Return all leaves (with paths) inside a given root.
+allLeavesIn :: (IsTree tree) => (Int -> [(Int, tree a)] -> [(Int, tree a)])
+            -> tree a -> [Node (tree a)]
+allLeavesIn select = allLeavesRelativeChild select (-1)
+
+-- | Return all subtrees in a tree; each element of the return list
+-- contains paths to nodes. (Root is at the start of each path)
+getAllPaths :: IsTree tree => tree t -> [[tree t]]
+getAllPaths t = fmap (<>[t]) ([] : concatMap getAllPaths (subtrees t))
+
+goDown :: IsTree tree => Int -> tree t -> Maybe (tree t)
+goDown i = index i . subtrees
+
+index :: Int -> [a] -> Maybe a
+index _ [] = Nothing
+index 0 (h:_) = Just h
+index n (_:t) = index (n-1) t
+
+walkDown :: IsTree tree => Node (tree t) -> Maybe (tree t)
+walkDown ([],t) = return t
+walkDown (x:xs,t) = goDown x t >>= curry walkDown xs
+
+wkDown :: IsTree tree => Node (tree a) -> Node (tree a)
+wkDown ([],t) = ([],t)
+wkDown (x:xs,t) = case goDown x t of
+    Nothing -> ([],t)
+    Just t' -> first (x:) $ wkDown (xs,t')
+
+-- | Search the given list, and return the last tree before the given
+-- point; with path to the root. (Root is at the start of the path)
+getLastPath :: IsTree tree => [tree (Tok t)] -> Point -> Maybe [tree (Tok t)]
+getLastPath roots offset =
+    case takeWhile ((< offset) . posnOfs . snd) allSubPathPosn of
+      [] -> Nothing
+      xs -> Just $ fst $ last xs
+    where
+      allSubPathPosn = [ (p,posn) | root <- roots
+                                  , p@(t':_) <- getAllPaths root
+                                  , Just tok <- [getFirstElement t']
+                                  , let posn = tokPosn tok
+                                  ]
+
+-- | Return all subtrees in a tree, in preorder.
+getAllSubTrees :: IsTree tree => tree t -> [tree t]
+getAllSubTrees t = t : concatMap getAllSubTrees (subtrees t)
+
+-- | Return the 1st token of a subtree.
+getFirstElement :: Foldable t => t a -> Maybe a
+getFirstElement tree = getFirst $ foldMap (First . Just) tree
+
+nullSubtree :: Foldable t => t a -> Bool
+nullSubtree = null . toList
+
+getFirstTok, getLastTok :: Foldable t => t a -> Maybe a
+
+getFirstTok = getFirstElement
+getLastTok = getLastElement
+
+-- | Return the last token of a subtree.
+getLastElement :: Foldable t => t a -> Maybe a
+getLastElement tree = getLast $ foldMap (Last . Just) tree
+
+getFirstOffset, getLastOffset :: Foldable t => t (Tok t1) -> Point
+getFirstOffset = maybe 0 tokBegin . getFirstTok
+getLastOffset = maybe 0 tokEnd . getLastTok
+
+subtreeRegion :: Foldable t => t (Tok t1) -> Region
+subtreeRegion t = mkRegion (getFirstOffset t) (getLastOffset t)
+
+-- | Given a tree, return (first offset, number of lines).
+getSubtreeSpan :: (Foldable tree) => tree (Tok t) -> (Point, Int)
+getSubtreeSpan tree = (posnOfs firstOff, lastLine - firstLine)
+    where bounds@[firstOff, _last] = fmap (tokPosn . assertJust)
+                                     [getFirstElement tree, getLastElement tree]
+          [firstLine, lastLine] = fmap posnLine bounds
+          assertJust (Just x) = x
+          assertJust _ = error "assertJust: Just expected"
+
+-------------------------------------
+-- Should be in Control.Applicative.?
+
+sepBy :: (Alternative f) => f a -> f v -> f [a]
+sepBy p s   = sepBy1 p s <|> pure []
+
+sepBy1 :: (Alternative f) => f a -> f v -> f [a]
+sepBy1 p s  = (:) <$> p <*> many (s *> p)
+
+
+----------------------------------------------------
+-- Testing code.
+
+#ifdef TESTING
+
+nodeRegion :: IsTree tree => Node (tree (Tok a)) -> Region
+nodeRegion n = subtreeRegion t
+    where Just t = walkDown n
+
+data Test a = Empty | Leaf a | Bin (Test a) (Test a) deriving (Show, Eq, Foldable)
+
+instance IsTree Test where
+    uniplate (Bin l r) = ([l,r],\[l',r'] -> Bin l' r')
+    uniplate t = ([],\[] -> t)
+    emptyNode = Empty
+
+type TT = Tok ()
+
+instance Arbitrary (Test TT) where
+    arbitrary = sized $ \size -> do
+      arbitraryFromList [1..size+1]
+    shrink (Leaf _) = []
+    shrink (Bin l r) = [l,r] <>  (Bin <$> shrink l <*> pure r) <>  (Bin <$> pure l <*> shrink r)
+
+tAt :: Point -> TT
+tAt idx =  Tok () 1 (Posn (idx * 2) 0 0)
+
+arbitraryFromList :: [Int] -> Gen (Test TT)
+arbitraryFromList [] = error "arbitraryFromList expects non empty lists"
+arbitraryFromList [x] = pure (Leaf (tAt (fromIntegral x)))
+arbitraryFromList xs = do
+  m <- choose (1,length xs - 1)
+  let (l,r) = splitAt m xs
+  Bin <$> arbitraryFromList l <*> arbitraryFromList r
+
+newtype NTTT = N (Node (Test TT)) deriving Show
+
+instance Arbitrary NTTT where
+    arbitrary = do
+      t <- arbitrary
+      p <- arbitraryPath t
+      return $ N (p,t)
+
+arbitraryPath :: Test t -> Gen Path
+arbitraryPath (Leaf _) = return []
+arbitraryPath (Bin l r) = do
+  c <- choose (0,1)
+  let Just n' = index c [l,r]
+  (c :) <$> arbitraryPath n'
+
+regionInside :: Region -> Gen Region
+regionInside r = do
+  b :: Int <- choose (fromIntegral $ regionStart r, fromIntegral $ regionEnd r)
+  e :: Int <- choose (b, fromIntegral $ regionEnd r)
+  return $ mkRegion (fromIntegral b) (fromIntegral e)
+
+pointInside :: Region -> Gen Point
+pointInside r = do
+  p :: Int <- choose (fromIntegral $ regionStart r, fromIntegral $ regionEnd r)
+  return (fromIntegral p)
+
+prop_fromLeafAfterToFinal :: NTTT -> Property
+prop_fromLeafAfterToFinal (N n) = let
+    fullRegion = subtreeRegion $ snd n
+ in forAll (pointInside fullRegion) $ \p -> do
+   let final@(_, (_, finalSubtree)) = fromLeafAfterToFinal p n
+       finalRegion = subtreeRegion finalSubtree
+       initialRegion = nodeRegion n
+
+   whenFail (do putStrLn $ "final = " <> show final
+                putStrLn $ "final reg = " <> show finalRegion
+                putStrLn $ "initialReg = " <> show initialRegion
+                putStrLn $ "p = " <> show p
+            )
+     ((regionStart finalRegion <= p) && (initialRegion `includedRegion` finalRegion))
+
+prop_allLeavesAfter :: NTTT -> Property
+prop_allLeavesAfter (N n@(xs,t)) = property $ do
+  let after = allLeavesRelative afterChild n
+  (xs',t') <- elements after
+  let t'' = walkDown (xs',t)
+  unProperty $ whenFail (do
+      putStrLn $ "t' = " <> show t'
+      putStrLn $ "t'' = " <> show t''
+      putStrLn $ "xs' = " <> show xs'
+    ) (Just t' == t'' && xs <= xs')
+
+prop_allLeavesBefore :: NTTT -> Property
+prop_allLeavesBefore (N n@(xs,t)) = property $ do
+  let after = allLeavesRelative beforeChild n
+  (xs',t') <- elements after
+  let t'' = walkDown (xs',t)
+  unProperty $ whenFail (do
+      putStrLn $ "t' = " <> show t'
+      putStrLn $ "t'' = " <> show t''
+      putStrLn $ "xs' = " <> show xs'
+    ) (Just t' == t'' && xs' <= xs)
+
+prop_fromNodeToLeafAfter :: NTTT -> Property
+prop_fromNodeToLeafAfter (N n) = forAll (pointInside (subtreeRegion $ snd n)) $ \p -> do
+   let after = fromLeafToLeafAfter p n
+       afterRegion = nodeRegion after
+   whenFail (do putStrLn $ "after = " <> show after
+                putStrLn $ "after reg = " <> show afterRegion
+            )
+     (regionStart afterRegion >= p)
+
+prop_fromNodeToFinal :: NTTT -> Property
+prop_fromNodeToFinal  (N t) = forAll (regionInside (subtreeRegion $ snd t)) $ \r -> do
+   let final@(_, finalSubtree) = fromNodeToFinal r t
+       finalRegion = subtreeRegion finalSubtree
+   whenFail (do putStrLn $ "final = " <> show final
+                putStrLn $ "final reg = " <> show finalRegion
+                putStrLn $ "leaf after = " <> show (fromLeafToLeafAfter (regionEnd r) t)
+            ) $ do
+     r `includedRegion` finalRegion
+
+#endif
diff --git a/src/Yi/Tab.hs b/src/Yi/Tab.hs
new file mode 100644
--- /dev/null
+++ b/src/Yi/Tab.hs
@@ -0,0 +1,113 @@
+{-# LANGUAGE DeriveDataTypeable #-}
+{-# LANGUAGE Rank2Types         #-}
+
+module Yi.Tab
+ (
+  Tab,
+  TabRef,
+  tabWindowsA,
+  tabLayoutManagerA,
+  tabDividerPositionA,
+  tkey,
+  tabMiniWindows,
+  tabFocus,
+  forceTab,
+  mapWindows,
+  tabLayout,
+  tabFoldl,
+  makeTab,
+  makeTab1,
+ )  where
+
+import           Prelude               hiding (foldl, foldr)
+
+import           Lens.Micro.Platform            (Lens', lens, over, (^.))
+import qualified Data.Binary           as Binary (Binary, get, put)
+import           Data.Default          (def)
+import           Data.Foldable         (foldl, foldr, toList)
+import qualified Data.List.PointedList as PL (PointedList, singleton, _focus)
+import           Data.Typeable         (Typeable)
+import           Yi.Buffer.Basic       (WindowRef)
+import           Yi.Layout
+import           Yi.Window             (Window, isMini, wkey)
+
+type TabRef = Int
+
+-- | A tab, containing a collection of windows.
+data Tab = Tab {
+  tkey             :: !TabRef,                  -- ^ For UI sync; fixes #304
+  tabWindows       :: !(PL.PointedList Window), -- ^ Visible windows
+  tabLayout        :: !(Layout WindowRef),      -- ^ Current layout. Invariant: must be the layout generated by 'tabLayoutManager', up to changing the 'divPos's.
+  tabLayoutManager :: !AnyLayoutManager -- ^ layout manager (for regenerating the layout when we add/remove windows)
+  }
+ deriving Typeable
+
+tabFocus :: Tab -> Window
+tabFocus = PL._focus . tabWindows
+
+-- | Returns a list of all mini windows associated with the given tab
+tabMiniWindows :: Tab -> [Window]
+tabMiniWindows = Prelude.filter isMini . toList . tabWindows
+
+-- | Accessor for the windows. If the windows (but not the focus) have changed when setting, then a relayout will be triggered to preserve the internal invariant.
+tabWindowsA :: Functor f =>
+    (PL.PointedList Window -> f (PL.PointedList Window)) -> Tab -> f Tab
+tabWindowsA f s = (`setter` s) <$> f (getter s)
+  where
+    setter ws t = relayoutIf (toList ws /= toList (tabWindows t)) (t { tabWindows = ws})
+    getter = tabWindows
+
+-- | Accessor for the layout manager. When setting, will trigger a relayout if the layout manager has changed.
+tabLayoutManagerA :: Functor f =>
+    (AnyLayoutManager -> f AnyLayoutManager) -> Tab -> f Tab
+tabLayoutManagerA f s = (`setter` s) <$> f (getter s)
+  where
+    setter lm t = relayoutIf (lm /= tabLayoutManager t) (t { tabLayoutManager = lm })
+    getter = tabLayoutManager
+
+-- | Gets / sets the position of the divider with the given reference. The caller must ensure that the DividerRef is valid, otherwise an error will (might!) occur.
+tabDividerPositionA :: DividerRef -> Lens' Tab DividerPosition
+tabDividerPositionA ref = lens tabLayout (\ t l -> t{tabLayout = l}) . dividerPositionA ref
+
+relayoutIf :: Bool -> Tab -> Tab
+relayoutIf False t = t
+relayoutIf True t = relayout t
+
+relayout :: Tab -> Tab
+relayout t = t { tabLayout = buildLayout (tabWindows t) (tabLayoutManager t) (tabLayout t) }
+
+instance Binary.Binary Tab where
+  put (Tab tk ws _ _) = Binary.put tk >> Binary.put ws
+  get = makeTab <$> Binary.get <*> Binary.get
+
+
+-- | Equality on tab identity (the 'tkey')
+instance Eq Tab where
+  (==) t1 t2 = tkey t1 == tkey t2
+
+instance Show Tab where
+  show t = "Tab " ++ show (tkey t)
+
+-- | A specialised version of "fmap".
+mapWindows :: (Window -> Window) -> Tab -> Tab
+mapWindows f = over tabWindowsA (fmap f)
+
+-- | Forces all windows in the tab
+forceTab :: Tab -> Tab
+forceTab t = foldr seq t (t ^. tabWindowsA)
+
+-- | Folds over the windows in the tab
+tabFoldl :: (a -> Window -> a) -> a -> Tab -> a
+tabFoldl f z t = foldl f z (t ^. tabWindowsA)
+
+-- | Run the layout on the given tab, for the given aspect ratio
+buildLayout :: PL.PointedList Window -> AnyLayoutManager -> Layout WindowRef -> Layout WindowRef
+buildLayout ws m l = pureLayout m l . fmap wkey . Prelude.filter (not . isMini) . toList $ ws
+
+-- | Make a tab from multiple windows
+makeTab :: TabRef -> PL.PointedList Window -> Tab
+makeTab key ws = Tab key ws (buildLayout ws def def) def
+
+-- | Make a tab from one window
+makeTab1 :: TabRef -> Window -> Tab
+makeTab1 key win = makeTab key (PL.singleton win)
diff --git a/src/Yi/Tag.hs b/src/Yi/Tag.hs
new file mode 100644
--- /dev/null
+++ b/src/Yi/Tag.hs
@@ -0,0 +1,152 @@
+{-# LANGUAGE CPP                #-}
+{-# LANGUAGE DeriveDataTypeable #-}
+{-# LANGUAGE DeriveGeneric      #-}
+{-# LANGUAGE FlexibleContexts   #-}
+{-# LANGUAGE OverloadedStrings  #-}
+{-# LANGUAGE TemplateHaskell    #-}
+{-# OPTIONS_HADDOCK show-extensions #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+
+-- |
+-- Module      :  Yi.Tag
+-- License     :  GPL-2
+-- Maintainer  :  yi-devel@googlegroups.com
+-- Stability   :  experimental
+-- Portability :  portable
+--
+-- A module for CTags integration. Note that this reads the ‘tags’
+-- file produced by @hasktags@, not the ‘TAGS’ file which uses a
+-- different format (etags).
+
+module Yi.Tag ( lookupTag
+              , importTagTable
+              , hintTags
+              , completeTag
+              , Tag(..)
+              , unTag'
+              , TagTable(..)
+              , getTags
+              , setTags
+              , resetTags
+              , tagsFileList
+              , readCTags
+              ) where
+
+import           GHC.Generics (Generic)
+
+import           Lens.Micro.Platform             (makeLenses)
+import           Data.Binary            (Binary)
+import qualified Data.ByteString        as BS (readFile)
+import           Data.Default           (Default, def)
+import qualified Data.Foldable          as F (concat)
+import           Data.Map               (Map, fromListWith, keys, lookup)
+import           Data.Maybe             (mapMaybe)
+import qualified Data.Text              as T (Text, append, isPrefixOf, lines, pack, unpack, words)
+import qualified Data.Text.Encoding     as E (decodeUtf8, encodeUtf8)
+import qualified Data.Text.Read         as R (decimal)
+import qualified Yi.CompletionTree      as CT
+import           System.FilePath        (takeDirectory, takeFileName, (</>))
+import           System.FriendlyPath    (expandTilda)
+import           Yi.Config.Simple.Types (Field, customVariable)
+import           Yi.Editor              (EditorM, getEditorDyn, putEditorDyn)
+import           Yi.Types               (YiConfigVariable, YiVariable)
+
+newtype TagsFileList  = TagsFileList { _unTagsFileList :: [FilePath] }
+
+instance Default TagsFileList where
+    def = TagsFileList ["tags"]
+
+instance YiConfigVariable TagsFileList
+
+makeLenses ''TagsFileList
+
+tagsFileList :: Field [FilePath]
+tagsFileList = customVariable . unTagsFileList
+
+newtype Tags = Tags (Maybe TagTable) deriving (Binary)
+
+instance Default Tags where
+    def = Tags Nothing
+
+instance YiVariable Tags
+
+newtype Tag = Tag { _unTag :: T.Text } deriving (Show, Eq, Ord, Binary)
+
+unTag' :: Tag -> T.Text
+unTag' =  _unTag
+
+data TagTable = TagTable
+    { tagFileName :: FilePath
+    -- ^ local name of the tag file
+    -- TODO: reload if this file is changed
+    , tagBaseDir :: FilePath
+    -- ^ path to the tag file directory
+    -- tags are relative to this path
+    , tagFileMap :: Map Tag [(FilePath, Int)]
+    -- ^ map from tags to files
+    , tagCompletionTree :: CT.CompletionTree T.Text
+    -- ^ trie to speed up tag hinting
+    } deriving (Generic)
+
+-- | Find the location of a tag using the tag table.
+-- Returns a full path and line number
+lookupTag :: Tag -> TagTable -> [(FilePath, Int)]
+lookupTag tag tagTable = do
+  (file, line) <- F.concat . Data.Map.lookup tag $ tagFileMap tagTable
+  return (tagBaseDir tagTable </> file, line)
+
+-- | Super simple parsing CTag format 1 parsing algorithm
+-- TODO: support search patterns in addition to lineno
+readCTags :: T.Text -> Map Tag [(FilePath, Int)]
+readCTags =
+    fromListWith (++) . mapMaybe (parseTagLine . T.words) . T.lines
+    where parseTagLine (tag:tagfile:lineno:_) =
+              -- remove ctag control lines
+              if "!_TAG_" `T.isPrefixOf` tag then Nothing
+              else Just (Tag tag, [(T.unpack tagfile, getLineNumber lineno)])
+              where getLineNumber = (\(Right x) -> x) . fmap fst . R.decimal
+          parseTagLine _ = Nothing
+
+-- | Read in a tag file from the system
+importTagTable :: FilePath -> IO TagTable
+importTagTable filename = do
+  friendlyName <-  expandTilda filename
+  tagStr <- E.decodeUtf8 <$> BS.readFile friendlyName
+  let cts = readCTags tagStr
+  return TagTable { tagFileName = takeFileName filename
+                  , tagBaseDir  = takeDirectory filename
+                  , tagFileMap  = cts
+                  , tagCompletionTree = CT.fromList . map (_unTag) $ keys cts
+                  }
+
+-- | Gives all the possible expanded tags that could match a given @prefix@
+hintTags :: TagTable -> T.Text -> [T.Text]
+hintTags tags prefix = map (prefix `T.append`) sufs
+  where
+    sufs :: [T.Text]
+    sufs = CT.toList (CT.update (tagCompletionTree tags) prefix)
+
+-- | Extends the string to the longest certain length
+completeTag :: TagTable -> T.Text -> T.Text
+completeTag tags prefix =
+  prefix `T.append` fst (CT.complete (CT.update (tagCompletionTree tags) prefix))
+
+
+-- ---------------------------------------------------------------------
+-- Direct access interface to TagTable.
+
+-- | Set a new TagTable
+setTags :: TagTable -> EditorM ()
+setTags = putEditorDyn . Tags . Just
+
+-- | Reset the TagTable
+resetTags :: EditorM ()
+resetTags = putEditorDyn $ Tags Nothing
+
+-- | Get the currently registered tag table
+getTags :: EditorM (Maybe TagTable)
+getTags = do
+  Tags t <- getEditorDyn
+  return t
+
+instance Binary TagTable
diff --git a/src/Yi/TextCompletion.hs b/src/Yi/TextCompletion.hs
new file mode 100644
--- /dev/null
+++ b/src/Yi/TextCompletion.hs
@@ -0,0 +1,211 @@
+{-# LANGUAGE DeriveDataTypeable         #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# OPTIONS_HADDOCK show-extensions #-}
+
+-- |
+-- Module      :  Yi.TextCompletion
+-- License     :  GPL-2
+-- Maintainer  :  yi-devel@googlegroups.com
+-- Stability   :  experimental
+-- Portability :  portable
+--
+-- Module providing text completion functions.
+
+module Yi.TextCompletion (
+        -- * Word completion
+        wordComplete,
+        wordComplete',
+        wordCompleteString,
+        wordCompleteString',
+        mkWordComplete,
+        resetComplete,
+        completeWordB,
+        CompletionScope(..)
+) where
+
+import           Control.Monad       (forM)
+import           Data.Binary         (Binary, get, put)
+import           Data.Char           (GeneralCategory (..), generalCategory)
+import           Data.Default        (Default, def)
+import           Data.Function       (on)
+import           Data.List           (findIndex)
+import           Data.List.NonEmpty  (NonEmpty (..))
+import           Data.Maybe          (isJust)
+import qualified Data.Text           as T (Text, drop, groupBy, head, isPrefixOf, length, null)
+import qualified Data.Text.Encoding  as E (decodeUtf8, encodeUtf8)
+import           Data.Typeable       (Typeable)
+import           Yi.Buffer
+import           Yi.Completion       (completeInList, isCasePrefixOf)
+import           Yi.Editor
+import           Yi.Keymap           (YiM)
+import qualified Yi.Rope             as R (fromText, toText)
+import           Yi.Types            (YiVariable)
+import           Yi.Utils            (nubSet)
+
+-- ---------------------------------------------------------------------
+-- | Word completion
+--
+-- when doing keyword completion, we need to keep track of the word
+-- we're trying to complete.
+
+
+newtype Completion = Completion
+      [T.Text] -- the list of all possible things we can complete to.
+               -- (this seems very inefficient; but we use laziness to
+               -- our advantage)
+    deriving (Typeable, Show, Eq)
+
+instance Binary Completion where
+  put (Completion ts) = put (E.encodeUtf8 <$> ts)
+  get = Completion . map E.decodeUtf8 <$> get
+
+-- TODO: put this in keymap state instead
+instance Default Completion where
+    def = Completion []
+
+instance YiVariable Completion
+
+-- | Switch out of completion mode.
+resetComplete :: EditorM ()
+resetComplete = putEditorDyn (Completion [])
+
+-- | Try to complete the current word with occurences found elsewhere in the
+-- editor. Further calls try other options.
+mkWordComplete :: YiM T.Text -- ^ Extract function
+               -> (T.Text -> YiM [T.Text]) -- ^ Source function
+               -> ([T.Text] -> YiM ()) -- ^ Message function
+               -> (T.Text -> T.Text -> Bool) -- ^ Predicate matcher
+               -> YiM T.Text
+mkWordComplete extractFn sourceFn msgFn predMatch = do
+  Completion complList <- withEditor getEditorDyn
+  case complList of
+    (x:xs) -> do -- more alternatives, use them.
+       msgFn (x:xs)
+       withEditor . putEditorDyn $ Completion xs
+       return x
+    [] -> do -- no alternatives, build them.
+      w <- extractFn
+      ws <- sourceFn w
+      let comps = nubSet (filter (matches w) ws) ++ [w]
+      putEditorDyn $ Completion comps
+      -- We put 'w' back at the end so we go back to it after seeing
+      -- all possibilities.
+
+      -- to pick the 1st possibility.
+      mkWordComplete extractFn sourceFn msgFn predMatch
+
+  where matches x y = x `predMatch` y && x/=y
+
+wordCompleteString' :: Bool -> YiM T.Text
+wordCompleteString' caseSensitive =
+  mkWordComplete (withCurrentBuffer $
+                   textRegion =<< regionOfPartB unitWord Backward)
+                 (\_ -> withEditor wordsForCompletion)
+                 (\_ -> return ())
+                 (isCasePrefixOf caseSensitive)
+  where
+    textRegion = fmap R.toText . readRegionB
+
+wordCompleteString :: YiM T.Text
+wordCompleteString = wordCompleteString' True
+
+wordComplete' :: Bool -> YiM ()
+wordComplete' caseSensitive = do
+  x <- R.fromText <$> wordCompleteString' caseSensitive
+  withEditor $ withCurrentBuffer $
+    flip replaceRegionB x =<< regionOfPartB unitWord Backward
+
+wordComplete :: YiM ()
+wordComplete = wordComplete' True
+
+----------------------------
+-- Alternative Word Completion
+
+{-
+  'completeWordB' is an alternative to 'wordCompleteB'.
+
+  'completeWordB' offers a slightly different interface. The user
+  completes the word using the mini-buffer in the same way a user
+  completes a buffer or file name when switching buffers or opening a
+  file. This means that it never guesses and completes only as much as
+  it can without guessing.
+
+  I think there is room for both approaches. The 'wordCompleteB' approach
+  which just guesses the completion from a list of possible completion
+  and then re-hitting the key-binding will cause it to guess again.
+  I think this is very nice for things such as completing a word within
+  a TeX-buffer. However using the mini-buffer might be nicer when we allow
+  syntax knowledge to allow completion for example we may complete from
+  a Hoogle database.
+-}
+completeWordB :: CompletionScope -> EditorM ()
+completeWordB = veryQuickCompleteWord
+
+data CompletionScope = FromCurrentBuffer | FromAllBuffers
+  deriving (Eq, Show)
+
+{-
+  This is a very quick and dirty way to complete the current word.
+  It works in a similar way to the completion of words in the mini-buffer
+  it uses the message buffer to give simple feedback such as,
+  "Matches:" and "Complete, but not unique:"
+
+  It is by no means perfect but it's also not bad, pretty usable.
+-}
+veryQuickCompleteWord :: CompletionScope -> EditorM ()
+veryQuickCompleteWord scope = do
+  (curWord, curWords) <- withCurrentBuffer wordsAndCurrentWord
+  allWords <- fmap concat $ withEveryBuffer $ words' <$> (R.toText <$> elemsB)
+  let match :: T.Text -> Maybe T.Text
+      match x = if (curWord `T.isPrefixOf` x) && (x /= curWord)
+                then Just x
+                else Nothing
+      wordsToChooseFrom = if scope == FromCurrentBuffer
+                          then curWords
+                          else allWords
+  preText             <- completeInList curWord match wordsToChooseFrom
+  if T.null curWord
+    then printMsg "No word to complete"
+    else withCurrentBuffer . insertN . R.fromText $ T.drop (T.length curWord) preText
+
+wordsAndCurrentWord :: BufferM (T.Text, [T.Text])
+wordsAndCurrentWord =
+  do curText          <- R.toText <$> elemsB
+     curWord          <-
+       fmap R.toText $ readRegionB =<< regionOfPartB unitWord Backward
+     return (curWord, words' curText)
+
+wordsForCompletionInBuffer :: BufferM [T.Text]
+wordsForCompletionInBuffer = do
+  let readTextRegion = fmap R.toText . readRegionB
+  above <- readTextRegion =<< regionOfPartB Document Backward
+  below <- readTextRegion =<< regionOfPartB Document Forward
+  return $ reverse (words' above) ++ words' below
+
+wordsForCompletion :: EditorM [T.Text]
+wordsForCompletion = do
+    _ :| bs <- fmap bkey <$> getBufferStack
+    w0 <- withCurrentBuffer wordsForCompletionInBuffer
+    contents <- forM bs $ \b -> withGivenBuffer b (R.toText <$> elemsB)
+    return $ w0 ++ concatMap words' contents
+
+words' :: T.Text -> [T.Text]
+words' = filter (isJust . charClass . T.head) . T.groupBy ((==) `on` charClass)
+
+charClass :: Char -> Maybe Int
+charClass c = findIndex (generalCategory c `elem`)
+                [ [ UppercaseLetter, LowercaseLetter, TitlecaseLetter
+                  , ModifierLetter, OtherLetter
+                  , ConnectorPunctuation
+                  , NonSpacingMark, SpacingCombiningMark, EnclosingMark
+                  , DecimalNumber, LetterNumber, OtherNumber
+                  ]
+                , [ MathSymbol, CurrencySymbol, ModifierSymbol, OtherSymbol ]
+                ]
+
+{-
+  Finally obviously we wish to have a much more sophisticated completeword.
+  One which spawns a mini-buffer and allows searching in Hoogle databases
+  or in other files etc.
+-}
diff --git a/src/Yi/Types.hs b/src/Yi/Types.hs
new file mode 100644
--- /dev/null
+++ b/src/Yi/Types.hs
@@ -0,0 +1,464 @@
+{-# LANGUAGE CPP                        #-}
+{-# LANGUAGE DeriveDataTypeable         #-}
+{-# LANGUAGE DeriveFoldable             #-}
+{-# LANGUAGE DeriveFunctor              #-}
+{-# LANGUAGE DeriveTraversable          #-}
+{-# LANGUAGE ExistentialQuantification  #-}
+{-# LANGUAGE FlexibleContexts           #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE LambdaCase                 #-}
+{-# LANGUAGE MultiParamTypeClasses      #-}
+{-# LANGUAGE ScopedTypeVariables        #-}
+{-# OPTIONS_HADDOCK show-extensions #-}
+
+-- |
+-- Module      :  Yi.Types
+-- License     :  GPL-2
+-- Maintainer  :  yi-devel@googlegroups.com
+-- Stability   :  experimental
+-- Portability :  portable
+--
+-- This module is the host of the most prevalent types throughout Yi.
+-- It is unfortunately a necessary evil to avoid use of bootfiles.
+--
+-- You're encouraged to import from more idiomatic modules which will
+-- re-export these where appropriate.
+
+module Yi.Types where
+
+import           Control.Concurrent             (MVar, modifyMVar, modifyMVar_, readMVar)
+import           Control.Monad.Base             (MonadBase, liftBase)
+import           Control.Monad.Reader           (MonadReader, ReaderT, ask, asks, runReaderT)
+import           Control.Monad.RWS.Strict       (MonadWriter, RWS, ap, liftM3, void)
+import           Control.Monad.State            (MonadState (..), State, forever, runState)
+import           Data.Binary                    (Binary)
+import qualified Data.Binary                    as B (get, put)
+import           Data.Default                   (Default, def)
+import qualified Data.DelayList                 as DelayList (DelayList)
+import qualified Data.DynamicState              as ConfigState (DynamicState)
+import qualified Data.DynamicState.Serializable as DynamicState (DynamicState)
+import           Data.Function                  (on)
+import           Data.List.NonEmpty             (NonEmpty)
+import           Data.List.PointedList          (PointedList)
+import qualified Data.Map                       as M (Map)
+import qualified Data.Text                      as T (Text)
+import qualified Data.Text.Encoding             as E (decodeUtf8, encodeUtf8)
+import           Data.Time                      (UTCTime (..))
+import           Data.Typeable                  (Typeable)
+import           Data.Word                      (Word8)
+import           Yi.Buffer.Basic                (BufferRef, WindowRef)
+import           Yi.Buffer.Implementation
+import           Yi.Buffer.Undo                 (URList)
+import           Yi.Config.Misc                 (ScrollStyle)
+import           Yi.Event                       (Event)
+import qualified Yi.Interact                    as I (I, P (End))
+import           Yi.KillRing                    (Killring)
+import           Yi.Layout                      (AnyLayoutManager)
+import           Yi.Monad                       (getsAndModify)
+import           Yi.Process                     (SubprocessId, SubprocessInfo)
+import qualified Yi.Rope                        as R (ConverterName, YiString)
+import           Yi.Style                       (StyleName)
+import           Yi.Style.Library               (Theme)
+import           Yi.Syntax                      (ExtHL, Stroke)
+import           Yi.Tab                         (Tab)
+import           Yi.UI.Common                   (UI)
+import           Yi.Window                      (Window)
+
+
+-- Yi.Keymap
+
+-- TODO: refactor this!
+
+data Action = forall a. Show a => YiA (YiM a)
+            | forall a. Show a => EditorA (EditorM a)
+            | forall a. Show a => BufferA (BufferM a)
+            deriving Typeable
+
+emptyAction :: Action
+emptyAction = BufferA (return ())
+
+class (Default a, Binary a, Typeable a) => YiVariable a
+class (Default a, Typeable a) => YiConfigVariable a
+
+instance Eq Action where
+    _ == _ = False
+
+instance Show Action where
+    show (YiA _) = "@Y"
+    show (EditorA _) = "@E"
+    show (BufferA _) = "@B"
+
+type Interact ev a = I.I ev Action a
+
+type KeymapM a = Interact Event a
+
+type Keymap = KeymapM ()
+
+type KeymapEndo = Keymap -> Keymap
+
+type KeymapProcess = I.P Event Action
+
+data IsRefreshNeeded = MustRefresh | NoNeedToRefresh
+    deriving (Show, Eq)
+
+data Yi = Yi { yiUi          :: UI Editor
+             , yiInput       :: [Event] -> IO ()    -- ^ input stream
+             , yiOutput      :: IsRefreshNeeded -> [Action] -> IO ()   -- ^ output stream
+             , yiConfig      :: Config
+               -- TODO: this leads to anti-patterns and seems like one itself
+               -- too coarse for actual concurrency, otherwise pointless
+               -- And MVars can be empty so this causes soundness problems
+               -- Also makes code a bit opaque
+             , yiVar         :: MVar YiVar           -- ^ The only mutable state in the program
+             }
+             deriving Typeable
+
+data YiVar = YiVar { yiEditor             :: !Editor
+                   , yiSubprocessIdSupply :: !SubprocessId
+                   , yiSubprocesses       :: !(M.Map SubprocessId SubprocessInfo)
+                   }
+
+-- | The type of user-bindable functions
+-- TODO: doc how these are actually user-bindable
+-- are they?
+newtype YiM a = YiM {runYiM :: ReaderT Yi IO a}
+    deriving (Monad, Applicative, MonadReader Yi, MonadBase IO, Typeable, Functor)
+
+instance MonadState Editor YiM where
+    get = yiEditor <$> (liftBase . readMVar =<< yiVar <$> ask)
+    put v = liftBase . flip modifyMVar_ (\x -> return $ x {yiEditor = v}) =<< yiVar <$> ask
+
+instance MonadEditor YiM where
+    askCfg = yiConfig <$> ask
+    withEditor f = do
+      r <- asks yiVar
+      cfg <- asks yiConfig
+      liftBase $ unsafeWithEditor cfg r f
+
+
+unsafeWithEditor :: Config -> MVar YiVar -> EditorM a -> IO a
+unsafeWithEditor cfg r f = modifyMVar r $ \var -> do
+  let e = yiEditor var
+  let (e',a) = runEditor cfg f e
+  -- Make sure that the result of runEditor is evaluated before
+  -- replacing the editor state. Otherwise, we might replace e
+  -- with an exception-producing thunk, which makes it impossible
+  -- to look at or update the editor state.
+  -- Maybe this could also be fixed by -fno-state-hack flag?
+  -- TODO: can we simplify this?
+  e' `seq` a `seq` return (var {yiEditor = e'}, a)
+
+
+data KeymapSet = KeymapSet
+    { topKeymap :: Keymap         -- ^ Content of the top-level loop.
+    , insertKeymap :: Keymap      -- ^ For insertion-only modes
+    }
+
+extractTopKeymap :: KeymapSet -> Keymap
+extractTopKeymap kms = forever (topKeymap kms)
+    -- Note the use of "forever": this has quite subtle implications, as it means that
+    -- failures in one iteration can yield to jump to the next iteration seamlessly.
+    -- eg. in emacs keybinding, failures in incremental search, like <left>, will "exit"
+    -- incremental search and immediately move to the left.
+
+-- Yi.Buffer.Misc
+
+-- | The BufferM monad writes the updates performed.
+newtype BufferM a = BufferM { fromBufferM :: RWS Window [Update] FBuffer a }
+    deriving (Monad, Functor, MonadWriter [Update], MonadState FBuffer, MonadReader Window, Typeable)
+
+-- | Currently duplicates some of Vim's indent settings. Allowing a
+-- buffer to specify settings that are more dynamic, perhaps via
+-- closures, could be useful.
+data IndentSettings = IndentSettings
+  { expandTabs :: Bool -- ^ Insert spaces instead of tabs as possible
+  , tabSize    :: Int  -- ^ Size of a Tab
+  , shiftWidth :: Int  -- ^ Indent by so many columns
+  } deriving (Eq, Show, Typeable)
+
+instance Applicative BufferM where
+    pure = return
+    (<*>) = ap
+
+data FBuffer = forall syntax.
+        FBuffer { bmode  :: !(Mode syntax)
+                , rawbuf :: !(BufferImpl syntax)
+                , attributes :: !Yi.Types.Attributes
+               }
+        deriving Typeable
+
+instance Eq FBuffer where
+    (==) = (==) `on` bkey__ . attributes
+
+type WinMarks = MarkSet Mark
+
+data MarkSet a = MarkSet { fromMark, insMark, selMark :: !a }
+               deriving (Traversable, Foldable, Functor)
+
+instance Binary a => Binary (MarkSet a) where
+  put (MarkSet f i s) = B.put f >> B.put i >> B.put s
+  get = liftM3 MarkSet B.get B.get B.get
+
+data Attributes
+    = Attributes
+    { ident :: !BufferId
+    , bkey__   :: !BufferRef -- ^ immutable unique key
+    , undos  :: !URList -- ^ undo/redo list
+    , bufferDynamic :: !DynamicState.DynamicState -- ^ dynamic components
+    , preferCol :: !(Maybe Int)
+    -- ^ prefered column to arrive at when we do a lineDown / lineUp
+    , preferVisCol :: !(Maybe Int)
+    -- ^ prefered column to arrive at visually (ie, respecting wrap)
+    , stickyEol :: !Bool
+    -- ^ stick to the end of line (used by vim bindings mostly)
+    , pendingUpdates :: ![UIUpdate]
+    -- ^ updates that haven't been synched in the UI yet
+    , selectionStyle :: !SelectionStyle
+    , keymapProcess :: !KeymapProcess
+    , winMarks :: !(M.Map WindowRef WinMarks)
+    , lastActiveWindow :: !Window
+    , lastSyncTime :: !UTCTime
+    -- ^ time of the last synchronization with disk
+    , readOnly :: !Bool -- ^ read-only flag
+    , inserting :: !Bool -- ^ the keymap is ready for insertion into this buffer
+    , directoryContent :: !Bool -- ^ does buffer contain directory contents
+    , pointFollowsWindow :: !(WindowRef -> Bool)
+    , updateTransactionInFlight :: !Bool
+    , updateTransactionAccum :: ![Update]
+    , fontsizeVariation :: !Int
+    , encodingConverterName :: Maybe R.ConverterName
+      -- ^ How many points (frontend-specific) to change
+      -- the font by in this buffer
+    } deriving Typeable
+
+
+instance Binary Yi.Types.Attributes where
+    put (Yi.Types.Attributes n b u bd pc pv se pu selectionStyle_
+         _proc wm law lst ro ins _dc _pfw isTransacPresent transacAccum fv cn) = do
+      let putTime (UTCTime x y) = B.put (fromEnum x) >> B.put (fromEnum y)
+      B.put n >> B.put b >> B.put u >> B.put bd
+      B.put pc >> B.put pv >> B.put se >> B.put pu >> B.put selectionStyle_ >> B.put wm
+      B.put law >> putTime lst >> B.put ro >> B.put ins >> B.put _dc
+      B.put isTransacPresent >> B.put transacAccum >> B.put fv >> B.put cn
+    get = Yi.Types.Attributes <$> B.get <*> B.get <*> B.get <*> B.get <*>
+          B.get <*> B.get <*> B.get <*> B.get <*> B.get <*> pure I.End <*> B.get <*> B.get
+          <*> getTime <*> B.get <*> B.get <*> B.get
+          <*> pure (const False) <*> B.get <*> B.get <*> B.get <*> B.get
+      where
+        getTime = UTCTime <$> (toEnum <$> B.get) <*> (toEnum <$> B.get)
+
+data BufferId = MemBuffer T.Text
+              | FileBuffer FilePath
+              deriving (Show, Eq, Ord)
+
+instance Binary BufferId where
+  get = B.get >>= \case
+    (0 :: Word8) -> MemBuffer . E.decodeUtf8 <$> B.get
+    1 -> FileBuffer <$> B.get
+    x -> fail $ "Binary failed on BufferId, tag: " ++ show x
+  put (MemBuffer t) = B.put (0 :: Word8) >> B.put (E.encodeUtf8 t)
+  put (FileBuffer t) = B.put (1 :: Word8) >> B.put t
+
+
+data SelectionStyle = SelectionStyle
+  { highlightSelection :: !Bool
+  , rectangleSelection :: !Bool
+  } deriving Typeable
+
+instance Binary SelectionStyle where
+  put (SelectionStyle h r) = B.put h >> B.put r
+  get = SelectionStyle <$> B.get <*> B.get
+
+
+data AnyMode = forall syntax. AnyMode (Mode syntax)
+  deriving Typeable
+
+-- | A Mode customizes the Yi interface for editing a particular data
+-- format. It specifies when the mode should be used and controls
+-- file-specific syntax highlighting and command input, among other
+-- things.
+data Mode syntax = Mode
+  { modeName :: T.Text
+    -- ^ so this can be serialized, debugged.
+  , modeApplies :: FilePath -> R.YiString -> Bool
+    -- ^ What type of files does this mode apply to?
+  , modeHL :: ExtHL syntax
+    -- ^ Syntax highlighter
+  , modePrettify :: syntax -> BufferM ()
+    -- ^ Prettify current \"paragraph\"
+  , modeKeymap :: KeymapSet -> KeymapSet
+    -- ^ Buffer-local keymap modification
+  , modeIndent :: syntax -> IndentBehaviour -> BufferM ()
+    -- ^ emacs-style auto-indent line
+  , modeAdjustBlock :: syntax -> Int -> BufferM ()
+    -- ^ adjust the indentation after modification
+  , modeFollow :: syntax -> Action
+    -- ^ Follow a \"link\" in the file. (eg. go to location of error message)
+  , modeIndentSettings :: IndentSettings
+  , modeToggleCommentSelection :: Maybe (BufferM ())
+  , modeGetStrokes :: syntax -> Point -> Point -> Point -> [Stroke]
+    -- ^ Strokes that should be applied when displaying a syntax element
+    -- should this be an Action instead?
+  , modeOnLoad :: BufferM ()
+    -- ^ An action that is to be executed when this mode is set
+  , modeModeLine :: [T.Text] -> BufferM T.Text
+    -- ^ buffer-local modeline formatting method
+  , modeGotoDeclaration :: BufferM ()
+    -- ^ go to the point where the variable is declared
+  }
+
+-- | Used to specify the behaviour of the automatic indent command.
+data IndentBehaviour =
+    IncreaseCycle -- ^ Increase the indentation to the next higher indentation
+                  --   hint. If we are currently at the highest level of
+                  --   indentation then cycle back to the lowest.
+  | DecreaseCycle -- ^ Decrease the indentation to the next smaller indentation
+                  --   hint. If we are currently at the smallest level then
+                  --   cycle back to the largest
+  | IncreaseOnly  -- ^ Increase the indentation to the next higher hint
+                  --   if no such hint exists do nothing.
+  | DecreaseOnly  -- ^ Decrease the indentation to the next smaller indentation
+                  --   hint, if no such hint exists do nothing.
+    deriving (Eq, Show)
+
+-- Yi.Editor
+
+type Status = ([T.Text], StyleName)
+type Statuses = DelayList.DelayList Status
+
+-- | The Editor state
+data Editor = Editor
+  { bufferStack     :: !(NonEmpty BufferRef)
+    -- ^ Stack of all the buffers.
+    -- Invariant: first buffer is the current one.
+  , buffers         :: !(M.Map BufferRef FBuffer)
+  , refSupply       :: !Int  -- ^ Supply for buffer, window and tab ids.
+  , tabs_           :: !(PointedList Tab)
+    -- ^ current tab contains the visible windows pointed list.
+  , dynamic         :: !DynamicState.DynamicState -- ^ dynamic components
+  , statusLines     :: !Statuses
+  , maxStatusHeight :: !Int
+  , killring        :: !Killring
+  , currentRegex    :: !(Maybe SearchExp)
+    -- ^ currently highlighted regex (also most recent regex for use
+    -- in vim bindings)
+  , searchDirection :: !Direction
+  , pendingEvents   :: ![Event]
+    -- ^ Processed events that didn't yield any action yet.
+  , onCloseActions  :: !(M.Map BufferRef (EditorM ()))
+    -- ^ Actions to be run when the buffer is closed; should be scrapped.
+  } deriving Typeable
+
+
+newtype EditorM a = EditorM {fromEditorM :: ReaderT Config (State Editor) a}
+    deriving (Monad, Applicative, MonadState Editor,
+              MonadReader Config, Functor, Typeable)
+
+instance MonadEditor EditorM where
+    askCfg = ask
+    withEditor = id
+
+class (Monad m, MonadState Editor m) => MonadEditor m where
+  askCfg :: m Config
+
+  withEditor :: EditorM a -> m a
+  withEditor f = do
+    cfg <- askCfg
+    getsAndModify (runEditor cfg f)
+
+  withEditor_ :: EditorM a -> m ()
+  withEditor_ = withEditor . void
+
+
+runEditor :: Config -> EditorM a -> Editor -> (Editor, a)
+runEditor cfg f e = let (a, e') = runState (runReaderT (fromEditorM f) cfg) e
+                    in (e',a)
+
+-- Yi.Config
+
+data UIConfig = UIConfig {
+   configFontName :: Maybe String,  -- ^ Font name, for the UI that support it.
+   configFontSize :: Maybe Int,     -- ^ Font size, for the UI that support it.
+   configScrollStyle :: Maybe ScrollStyle,
+   -- ^ Style of scroll
+   configScrollWheelAmount :: Int,  -- ^ Amount to move the buffer when using the scroll wheel
+   configLeftSideScrollBar :: Bool, -- ^ Should the scrollbar be shown on the left side?
+   configAutoHideScrollBar :: Bool, -- ^ Hide scrollbar automatically if text fits on one page.
+   configAutoHideTabBar :: Bool,    -- ^ Hide the tabbar automatically if only one tab is present
+   configLineWrap :: Bool,          -- ^ Wrap lines at the edge of the window if too long to display.
+   configCursorStyle :: CursorStyle,
+   configWindowFill :: Char,
+   -- ^ The char with which to fill empty window space.  Usually '~' for vi-like
+   -- editors, ' ' for everything else.
+   configTheme :: Theme             -- ^ UI colours
+  }
+
+
+type UIBoot = Config -> ([Event] -> IO ())
+              -> ([Action] -> IO ()) ->  Editor -> IO (UI Editor)
+
+-- | When should we use a "fat" cursor (i.e. 2 pixels wide, rather than 1)? Fat
+-- cursors have only been implemented for the Pango frontend.
+data CursorStyle = AlwaysFat
+                 | NeverFat
+                 | FatWhenFocused
+                 | FatWhenFocusedAndInserting
+
+-- | Configuration record. All Yi hooks can be set here.
+data Config = Config {startFrontEnd :: UIBoot,
+                      -- ^ UI to use.
+                      configUI :: UIConfig,
+                      -- ^ UI-specific configuration.
+                      startActions :: [Action],
+                      -- ^ Actions to run when the editor is started.
+                      initialActions :: [Action],
+                      -- ^ Actions to run after startup (after startActions) or reload.
+                      defaultKm :: KeymapSet,
+                      -- ^ Default keymap to use.
+                      configInputPreprocess :: I.P Event Event,
+                      modeTable :: [AnyMode],
+                      -- ^ List modes by order of preference.
+                      debugMode :: Bool,
+                      -- ^ Produce a .yi.dbg file with a lot of debug information.
+                      configRegionStyle :: RegionStyle,
+                      -- ^ Set to 'Exclusive' for an emacs-like behaviour.
+                      configKillringAccumulate :: Bool,
+                      -- ^ Set to 'True' for an emacs-like behaviour, where
+                      -- all deleted text is accumulated in a killring.
+                      configCheckExternalChangesObsessively :: Bool,
+                      bufferUpdateHandler :: [[Update] -> BufferM ()],
+                      layoutManagers :: [AnyLayoutManager],
+                      -- ^ List of layout managers for 'cycleLayoutManagersNext'
+                      configVars :: ConfigState.DynamicState
+                      -- ^ Custom configuration, containing the 'YiConfigVariable's. Configure with 'configVariableA'.
+                     }
+
+
+-- Yi.Buffer.Normal
+
+-- Region styles are relative to the buffer contents.
+-- They likely should be considered a TextUnit.
+data RegionStyle = LineWise
+                 | Inclusive
+                 | Exclusive
+                 | Block
+  deriving (Eq, Typeable, Show)
+
+instance Binary RegionStyle where
+  put LineWise = B.put (0 :: Word8)
+  put Inclusive = B.put (1 :: Word8)
+  put Exclusive = B.put (2 :: Word8)
+  put Block = B.put (3 :: Word8)
+
+  get = B.get >>= \case
+    (0 :: Word8) -> return LineWise
+    1 -> return Inclusive
+    2 -> return Exclusive
+    3 -> return Block
+    n -> fail $ "Binary RegionStyle fail with " ++ show n
+
+-- TODO: put in the buffer state proper.
+instance Default RegionStyle where
+  def = Inclusive
+
+instance YiVariable RegionStyle
diff --git a/src/Yi/UI/Common.hs b/src/Yi/UI/Common.hs
new file mode 100644
--- /dev/null
+++ b/src/Yi/UI/Common.hs
@@ -0,0 +1,65 @@
+{-# LANGUAGE Rank2Types #-}
+
+module Yi.UI.Common where
+
+{- | Record presenting a frontend's interface.
+
+The functions 'layout' and 'refresh' are both run by the editor's main loop,
+in response to user actions and so on. Their relation is a little subtle, and
+is discussed here:
+
+  * to see some code, look at the function @refreshEditor@ in "Yi.Core".
+    This is the only place where 'layout' and 'refresh' are used.
+
+  * the function 'layout' is responsible for updating the 'Editor' with the
+    width and height of the windows. Some frontends, such as Pango, need to
+    modify their internal state to do this, and will consequently change
+    their display. This is expected.
+
+  * the function 'refresh' should cause the UI to update its display with
+    the information given in the 'Editor'.
+
+  * the functionalities of 'layout' and 'refresh' overlap to some extent, in
+    the sense that both may cause the frontend to update its display. The Yi core
+    provides the following guarantees which the frontend may take advantage of:
+
+      * in the main editor loop (i.e. in the @refreshEditor@ function),
+        'layout' will be run (possibly multiple times) and then 'refresh' will
+        be run. This guarantee will hold even in the case of threading (the
+        function @refreshEditor@ will always be run atomically, using @MVar@s).
+
+      * between the last run of 'layout' and the run of 'refresh', some changes
+        may be made to the 'Editor'. However, the text, text attributes, and
+        (displayed) window region of all windows will remain the same. However,
+        the cursor location may change.
+
+        This guarantee allows frontends which calculate rendering of the text
+        during the 'layout' stage to avoid recalculating the render again during
+        'refresh'. Pango is an example of such a frontend.
+
+The Yi core provides no guarantee about the OS thread from which the functions
+'layout' and 'refresh' are called from. In particular, subprocesses (e.g. compilation,
+ghci) will run 'layout' and 'refresh' from new OS threads (see @startSubprocessWatchers@
+in "Yi.Core"). The frontend must be preparaed for this: for instance, Gtk-based frontends
+should wrap GUI updates in @postGUIAsync@.
+-}
+data UI e = UI
+    { main                  :: IO ()               -- ^ Main loop
+    , end                   :: Bool -> IO ()       -- ^ Clean up, and also terminate if given 'true'
+    , suspend               :: IO ()               -- ^ Suspend (or minimize) the program
+    , refresh               :: e -> IO ()     -- ^ Refresh the UI with the given state
+    , userForceRefresh      :: IO ()               -- ^ User force-refresh (in case the screen has been messed up from outside)
+    , layout                :: e -> IO e -- ^ Set window width and height
+    , reloadProject         :: FilePath -> IO ()   -- ^ Reload cabal project views
+    }
+
+dummyUI :: UI e
+dummyUI = UI
+  { main             = return ()
+  , end              = const (return ())
+  , suspend          = return ()
+  , refresh          = const (return ())
+  , userForceRefresh = return ()
+  , layout           = return
+  , reloadProject    = const (return ())
+  }
diff --git a/src/Yi/UI/SimpleLayout.hs b/src/Yi/UI/SimpleLayout.hs
new file mode 100644
--- /dev/null
+++ b/src/Yi/UI/SimpleLayout.hs
@@ -0,0 +1,177 @@
+{-# LANGUAGE BangPatterns      #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE ViewPatterns      #-}
+
+module Yi.UI.SimpleLayout
+    ( Rect (..)
+    , Layout (..)
+    , Point2D (..)
+    , Size2D (..)
+    , coordsOfCharacterB
+    , layout
+    , verticalOffsetsForWindows
+    ) where
+
+import           Prelude                        hiding (concatMap, mapM)
+
+import           Lens.Micro.Platform                     (use, (.~), (&), (^.), to, _1)
+import           Control.Monad.State            (evalState, get, put)
+import           Data.Foldable                  (find, toList)
+import qualified Data.List.PointedList.Circular as PL (PointedList, focus)
+import qualified Data.Map.Strict                as M (Map, fromList)
+import           Data.Maybe                     (fromMaybe)
+import           Data.Monoid                    ((<>))
+import qualified Data.Text                      as T (uncons)
+import           Data.Traversable               (mapM)
+import           Yi.Buffer
+import           Yi.Editor
+import qualified Yi.Rope                        as R (take, toString, toText)
+import           Yi.UI.Utils                    (arrangeItems)
+import           Yi.Window
+import           Yi.Tab                         (tabLayout)
+import           Yi.Layout                      (Rectangle(..), HasNeighborWest,
+                                                 layoutToRectangles)
+
+data Layout = Layout
+    { tabbarRect :: !Rect
+    , windowRects :: !(M.Map WindowRef (Rect, HasNeighborWest))
+    , promptRect :: !Rect
+    }
+
+data Rect = Rect
+    { offsetX :: !Int
+    , offsetY :: !Int
+    , sizeX :: !Int
+    , sizeY :: !Int
+    }
+
+data Point2D = Point2D
+    { pointCol :: !Int
+    , pointRow :: !Int
+    }
+
+data Size2D = Size2D
+    { sizeWidth :: !Int
+    , sizeHeight :: !Int
+    }
+
+layout :: Int -> Int -> Editor -> (Editor, Layout)
+layout colCount rowCount e =
+    ( e & windowsA .~ newWs
+    , Layout tabRect winRects cmdRect
+    )
+    where
+      lt = e ^. tabsA . PL.focus . to tabLayout
+      miniWs = filter isMini . toList $ windows e
+      tabHeight = 1
+      tabRect = Rect 0 0 colCount tabHeight
+      cmdHeight = length $ arrangeItems (fst $ statusLineInfo e) colCount (maxStatusHeight e)
+      miniHeight = if null miniWs then 0 else 1
+      cmdRect = Rect 0 (rowCount - cmdHeight - miniHeight) colCount cmdHeight
+      bounds = rectToRectangle $ Rect 0 tabHeight colCount $
+                   rowCount - (max 1 $ cmdHeight + miniHeight) - tabHeight
+      bigRects = layoutToRectangles False bounds lt & map (\(wr, r, nb) ->
+                   let r' = rectangleToRect r
+                       sx = sizeX r' - if nb then 1 else 0
+                       w' = layoutWindow (findWindowWith wr e) e sx (sizeY r')
+                   in (w', r', nb))
+      miniRects = miniWs & map (\w ->
+                    let r' = Rect 0 (rowCount - 1) colCount 1
+                        w' = layoutWindow w e (sizeX r') (sizeY r')
+                    in (w', r', False))
+      rects = bigRects <> miniRects
+      winRects = rects & M.fromList . map (\(w, r, nb) -> (wkey w, (r, nb)))
+      updWs = rects & map (^. _1)
+      newWs = windows e & fmap (\w -> fromMaybe w $ find ((== wkey w) . wkey) updWs)
+
+rectToRectangle :: Rect -> Rectangle
+rectToRectangle (Rect x y sx sy) = Rectangle (fromIntegral x)  (fromIntegral y)
+                                             (fromIntegral sx) (fromIntegral sy)
+
+rectangleToRect :: Rectangle -> Rect
+rectangleToRect (Rectangle x y sx sy) = Rect (truncate x) (truncate y)
+                                             (truncate (x + sx) - truncate x)
+                                             (truncate (y + sy) - truncate y)
+
+layoutWindow :: Window -> Editor -> Int -> Int -> Window
+layoutWindow win e w h = win
+    { height = h
+    , width = w
+    , winRegion = mkRegion fromMarkPoint toMarkPoint
+    , actualLines = dispLnCount
+    }
+    where
+        b = findBufferWith (bufkey win) e
+        evalBuffer action = fst (runBuffer win b action)
+
+        -- Mini windows don't have a mode line.
+        h' = h - if isMini win then 0 else 1
+
+        -- Work around a problem with the mini window never displaying it's contents due to a
+        -- fromMark that is always equal to the end of the buffer contents.
+        Just (MarkSet fromM _ _) = evalBuffer (getMarks win)
+        fromMarkPoint = if isMini win
+                        then Point 0
+                        else evalBuffer $ use $ markPointA fromM
+
+        -- TODO: I suspect that this costs quite a lot of CPU in the "dry run" which determines the window size;
+        -- In that case, since attributes are also useless there, it might help to replace the call by a dummy value.
+        -- This is also approximately valid of the call to "indexedAnnotatedStreamB".
+        (toMarkPoint, wrapCount) = evalBuffer
+            (lastVisiblePointAndWrapCountB (Size2D w h') fromMarkPoint)
+
+        dispLnCount = h' - wrapCount
+
+
+coordsOfCharacterB :: Size2D -> Point -> Point -> BufferM (Maybe Point2D)
+coordsOfCharacterB _ topLeft char | topLeft > char = return Nothing
+coordsOfCharacterB (Size2D w h) (Point topLeft) (Point char)
+    | char - topLeft >= w * h = return Nothing
+coordsOfCharacterB (Size2D w h) (Point topLeft) (Point char) = savingPointB $ do
+    ts <- fmap tabSize indentSettingsB
+    text <- fmap (R.toString . R.take (w * h)) (streamB Forward (Point topLeft))
+    let go _  !y _ _ | y >= h = Nothing
+        go !x !y 0 _ = Just (Point2D x y)
+        go !x !y !n (c : d : t) =
+            case (c, d, compare x wOffset) of
+                ('\t',  _ , _) -> go (x + ts) y (n - 1) (d:t)
+                ('\n',  _ , _) -> go 0 (y + 1) (n - 1) (d:t)
+                (  _ ,'\n',EQ) -> go x y (n - 1) (d:t)
+                (  _ ,  _ ,EQ) -> go (x - wOffset) (y + 1) (n - 1) (d:t)
+                (  _ ,  _ , _) -> go (x + 1) y (n - 1) (d:t)
+            where wOffset = w - 1
+        go !x !y !n [c] =
+            case (c, compare x wOffset) of
+                ('\n', _) -> go 0 (y + 1) (n - 1) [c]
+                (  _ , _) -> go (x + 1) y (n - 1) [c]
+            where wOffset = w - 1
+        go !x !y _ _ = Just (Point2D x y)
+    return (go 0 0 (char - topLeft) text)
+
+lastVisiblePointAndWrapCountB :: Size2D -> Point -> BufferM (Point, Int)
+lastVisiblePointAndWrapCountB (Size2D w h) (Point topLeft) = savingPointB $ do
+    ts <- fmap tabSize indentSettingsB
+    text <- fmap (R.toText . R.take (w * h))
+                 (streamB Forward (Point topLeft))
+    let go !x !y !wc !n t | x > w = go (x - w) (y + 1) (wc + 1) n t
+        go _  !y !wc !n _ | y >= h = (Point (n - 1), wc)
+        go !x !y !wc !n (T.uncons -> Just (c, t)) =
+            case c of
+                '\t' -> go (x + ts) y wc (n + 1) t
+                '\n' -> go 0 (y + 1) wc (n + 1) t
+                _ -> go (x + 1) y wc (n + 1) t
+        go _ _ !wc !n _ = (Point n, wc)
+    return (go 0 0 0 topLeft text)
+
+verticalOffsetsForWindows :: Int -> PL.PointedList Window -> PL.PointedList Int
+verticalOffsetsForWindows startY ws =
+    scanrT (+) startY (fmap (\w -> if isMini w then 0 else height w) ws)
+
+-- As scanr, but generalized to a traversable (TODO)
+scanrT :: (Int -> Int -> Int) -> Int -> PL.PointedList Int -> PL.PointedList Int
+scanrT (+*+) k t = evalState (mapM f t) k
+    where f x = do s <- get
+                   let s' = s +*+ x
+                   put s'
+                   return s
diff --git a/src/Yi/UI/TabBar.hs b/src/Yi/UI/TabBar.hs
new file mode 100644
--- /dev/null
+++ b/src/Yi/UI/TabBar.hs
@@ -0,0 +1,55 @@
+{-# OPTIONS_HADDOCK show-extensions #-}
+
+-- |
+-- Module      :  Yi.UI.TabBar
+-- License     :  GPL-2
+-- Maintainer  :  yi-devel@googlegroups.com
+-- Stability   :  experimental
+-- Portability :  portable
+--
+-- Tabs.
+
+module Yi.UI.TabBar where
+
+import           Lens.Micro.Platform                     ((^.))
+import qualified Data.List.PointedList.Circular as PL (PointedList (_focus), withFocus)
+import qualified Data.Text                      as T (Text, pack, unpack)
+import           System.FilePath                (isValid, splitPath)
+import           Yi.Buffer                      (shortIdentString)
+import           Yi.Editor                      (Editor (..), commonNamePrefix, findBufferWith, tabsA)
+import           Yi.Tab                         (tabWindowsA)
+import           Yi.Window                      (Window (bufkey))
+
+-- | A TabDescr describes the properties of a UI tab independent of
+-- the particular GUI in use.
+data TabDescr = TabDescr
+    { tabText :: T.Text
+    , tabInFocus :: Bool
+    } deriving (Show, Eq)
+
+type TabBarDescr = PL.PointedList TabDescr
+
+tabBarDescr :: Editor -> TabBarDescr
+tabBarDescr editor = tabDescr <$> PL.withFocus (editor ^. tabsA)
+  where
+    prefix = commonNamePrefix editor
+    shorten = tabAbbrevTitle . shortIdentString (length prefix)
+    mkHintWith f = shorten $ findBufferWith f editor
+    hintForTab tab = mkHintWith (bufkey $ PL._focus (tab ^. tabWindowsA))
+
+    tabDescr (tab, True)  = TabDescr (hintForTab tab) True
+    tabDescr (tab, False) = TabDescr (hintForTab tab) False
+
+
+-- FIXME: it seems that using splitDirectories can abstract the '/'
+-- handling away. (Making it win32 friendly and simpler)
+tabAbbrevTitle :: T.Text -> T.Text
+tabAbbrevTitle title = if isValid fp
+                       then T.pack $ concatMap abbrev (splitPath fp)
+                       else title
+  where
+    fp = T.unpack title
+    abbrev "/" = "/"
+    abbrev path | head path == '.' && last path == '/' = take 2 path ++ "/"
+                | last path == '/' = head path : "/"
+                | otherwise        = path
diff --git a/src/Yi/UI/Utils.hs b/src/Yi/UI/Utils.hs
new file mode 100644
--- /dev/null
+++ b/src/Yi/UI/Utils.hs
@@ -0,0 +1,116 @@
+{-# LANGUAGE FlexibleContexts #-}
+{-# OPTIONS_HADDOCK show-extensions #-}
+
+-- |
+-- Module      :  Yi.UI.Utils
+-- License     :  GPL-2
+-- Maintainer  :  yi-devel@googlegroups.com
+-- Stability   :  experimental
+-- Portability :  portable
+--
+-- Utilities shared by various UIs
+
+module Yi.UI.Utils where
+
+import           Prelude                   hiding (mapM)
+
+import           Control.Arrow             (second)
+import           Lens.Micro.Platform                (use)
+import           Control.Monad.State       (evalState, modify)
+import           Control.Monad.State.Class (gets)
+import           Data.Foldable             (maximumBy)
+import           Data.Function             (on)
+import           Data.List                 (transpose)
+import           Data.List.Split           (chunksOf)
+import           Data.Monoid               (Endo (appEndo))
+import qualified Data.Text                 as T (Text, null, pack, unpack)
+import           Data.Traversable          (mapM)
+import           Yi.Buffer
+import           Yi.String                 (padLeft)
+import           Yi.Style                  (Attributes, StyleName, UIStyle (baseAttributes, selectedStyle))
+import           Yi.Syntax                 (Span (..))
+import           Yi.Window                 (Window (height, isMini))
+
+applyHeights :: Traversable t => [Int] -> t Window -> t Window
+applyHeights heights ws = evalState (mapM distribute ws) heights
+    where
+      distribute win = if isMini win
+          then return win{height = 1}
+          else (do h <- gets head
+                   modify tail
+                   return win{height = h})
+
+spliceAnnots :: [(Point,Char)] -> [Span String] -> [(Point,Char)]
+spliceAnnots text [] = text
+spliceAnnots text (Span start x stop:anns) = l ++ zip (repeat start) x ++ spliceAnnots r anns
+    where (l,rest) =  span ((start >) . fst) text
+          (_,r) = span ((stop >) . fst) rest
+
+-- | Turn a sequence of (from,style,to) strokes into a sequence
+--   of picture points (from,style), taking special care to
+--   ensure that the points are strictly increasing and introducing
+--   padding segments where neccessary.
+--   Precondition: Strokes are ordered and not overlapping.
+strokePicture :: [Span (Endo a)] -> [(Point,a -> a)]
+strokePicture [] = []
+strokePicture wholeList@(Span leftMost _ _:_) = helper leftMost wholeList
+    where helper :: Point -> [Span (Endo a)] -> [(Point,a -> a)]
+          helper prev [] = [(prev,id)]
+          helper prev (Span l f r:xs)
+              | prev < l  = (prev, id) : (l,appEndo f) : helper r xs
+              | otherwise = (l,appEndo f) : helper r xs
+
+-- | Paint the given stroke-picture on top of an existing picture
+paintStrokes :: (a -> a) -> a -> [(Point,a -> a)] -> [(Point,a)] -> [(Point,a)]
+paintStrokes f0 _  [] lx = fmap (second f0)     lx
+paintStrokes _  x0 lf [] = fmap (second ($ x0)) lf
+paintStrokes f0 x0 lf@((pf,f):tf) lx@((px,x):tx) =
+  case pf `compare` px of
+    LT -> (pf, f  x0):paintStrokes f  x0 tf lx
+    EQ -> (pf, f  x ):paintStrokes f  x  tf tx
+    GT -> (px, f0 x ):paintStrokes f0 x  lf tx
+
+
+
+paintPicture :: a -> [[Span (Endo a)]] -> [(Point,a)]
+paintPicture a = foldr (paintStrokes id a . strokePicture) []
+
+attributesPictureB :: UIStyle -> Maybe SearchExp -> Region -> [[Span StyleName]]
+    -> BufferM [(Point,Attributes)]
+attributesPictureB sty mexp region extraLayers =
+  paintPicture (baseAttributes sty) <$>
+    fmap (fmap (fmap ($ sty))) <$>
+    (extraLayers ++) <$>
+    strokesRangesB mexp region
+
+attributesPictureAndSelB :: UIStyle -> Maybe SearchExp -> Region -> BufferM [(Point,Attributes)]
+attributesPictureAndSelB sty mexp region = do
+    selReg <- getSelectRegionB
+    showSel <- use highlightSelectionA
+    rectSel <- use rectangleSelectionA
+    let styliseReg reg = Span (regionStart reg) selectedStyle (regionEnd reg)
+        extraLayers | rectSel && showSel = (:[]) . fmap styliseReg <$> blockifyRegion selReg
+                    | showSel            = return [[styliseReg selReg]]
+                    | otherwise          = return []
+    attributesPictureB sty mexp region =<< extraLayers
+
+
+-- | Arrange a list of items in columns over maximum @maxNumberOfLines@ lines
+arrangeItems :: [T.Text] -> Int -> Int -> [T.Text]
+arrangeItems items _ _ | all T.null items = []
+arrangeItems items maxWidth maxNumberOfLines = take maxNumberOfLines $ snd choice
+    where choice = maximumBy (compare `on` fst) arrangements
+          arrangements = fmap (arrangeItems' items maxWidth) (reverse [1..maxNumberOfLines])
+
+-- | Arrange a list of items in columns over @numberOfLines@ lines.
+--
+-- TODO: proper Text/YiString implementation
+arrangeItems' :: [T.Text] -> Int -> Int -> (Int, [T.Text])
+arrangeItems' items' maxWidth numberOfLines = (fittedItems,theLines)
+    where items = T.unpack <$> items'
+          columns = chunksOf numberOfLines items
+          columnsWidth = fmap (maximum . fmap length) columns
+          totalWidths = scanl (\x y -> 1 + x + y) 0 columnsWidth
+          shownItems = scanl (+) 0 (fmap length columns)
+          fittedItems = snd $ last $ takeWhile ((<= maxWidth) . fst) $ zip totalWidths shownItems
+          theLines = T.pack . unwords . zipWith padLeft columnsWidth <$> transpose columns
diff --git a/src/Yi/Window.hs b/src/Yi/Window.hs
new file mode 100644
--- /dev/null
+++ b/src/Yi/Window.hs
@@ -0,0 +1,80 @@
+{-# LANGUAGE DeriveDataTypeable #-}
+{-# LANGUAGE TemplateHaskell    #-}
+{-# OPTIONS_HADDOCK show-extensions #-}
+
+-- |
+-- Module      :  Yi.Window
+-- License     :  GPL-2
+-- Maintainer  :  yi-devel@googlegroups.com
+-- Stability   :  experimental
+-- Portability :  portable
+--
+-- Operations on 'Window's, in the emacs sense of the word.
+
+module Yi.Window where
+
+import Data.Binary         (Binary (..))
+import Data.Default        (Default (def))
+import Data.Typeable       (Typeable)
+import Yi.Buffer.Basic     (BufferRef, WindowRef)
+import Yi.JumpList         (JumpList)
+import Yi.Region           (Region, emptyRegion)
+import Yi.Utils            (makeLensesWithSuffix)
+
+------------------------------------------------------------------------
+-- | A window onto a buffer.
+
+data Window = Window
+    { isMini    :: !Bool -- ^ regular or mini window?
+    , bufkey    :: !BufferRef -- ^ the buffer this window opens to
+    , bufAccessList :: ![BufferRef]
+      -- ^ list of last accessed buffers (former bufKeys). Last
+      -- accessed one is first element
+    , height    :: !Int -- ^ height of the window (in number of screen
+                       -- lines displayed)
+    , width     :: !Int -- ^ width of the window (in number of chars)
+    , winRegion :: !Region -- ^ view area. note that the top point is
+                          -- also available as a buffer mark.
+    , wkey      :: !WindowRef -- ^ identifier for the window (for UI sync)
+    -- This is required for accurate scrolling.
+    -- Scrolling depends on the actual number of buffer
+    -- lines displayed. Line wrapping changes that number
+    -- relative to the height so we can't use height for that
+    -- purpose.
+    , actualLines :: !Int
+      -- ^ The actual number of buffer lines displayed. Taking into
+      -- account line wrapping
+    , jumpList :: !JumpList
+    } deriving (Typeable)
+
+makeLensesWithSuffix "A" ''Window
+
+instance Binary Window where
+    put (Window mini bk bl _w _h _rgn key lns jl) =
+        put mini >> put bk >> put bl >> put key >> put lns >> put jl
+    get = Window <$> get <*> get <*> get
+                   <*> return 0 <*> return 0 <*> return emptyRegion
+                   <*> get <*> get <*> get
+
+
+-- | Get the identification of a window.
+winkey :: Window -> (Bool, BufferRef)
+winkey w = (isMini w, bufkey w)
+
+instance Show Window where
+    show w = "Window to " ++ show (bufkey w)
+             -- ++ "{" ++ show (tospnt w) ++ "->" ++ show (bospnt w) ++ "}"
+             ++ "(" ++ show (height w) ++ ")"
+
+instance Eq Window where
+    (==) w1 w2 = wkey w1 == wkey w2
+
+{-
+-- | Is a given point within tospnt / bospnt?
+pointInWindow :: Point -> Window -> Bool
+pointInWindow point win = tospnt win <= point && point <= bospnt win
+-}
+
+-- | Return a "fake" window onto a buffer.
+dummyWindow :: BufferRef -> Window
+dummyWindow b = Window False b [] 0 0 emptyRegion def 0 Nothing
diff --git a/test/Spec.hs b/test/Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec.hs
@@ -0,0 +1,15 @@
+import Test.Tasty
+
+import qualified Yi.CompletionTreeTests as CompletionTree (testSuite)
+import qualified Yi.TagTests            as Tag            (testSuite)
+import qualified Yi.Mode.CommonTests    as Mode.Common    (testSuite)
+
+main :: IO ()
+main = defaultMain tests
+
+tests :: TestTree
+tests = testGroup "all" 
+  [ CompletionTree.testSuite
+  , Tag.testSuite
+  , Mode.Common.testSuite
+  ]
diff --git a/test/Yi/CompletionTreeTests.hs b/test/Yi/CompletionTreeTests.hs
new file mode 100644
--- /dev/null
+++ b/test/Yi/CompletionTreeTests.hs
@@ -0,0 +1,52 @@
+module Yi.CompletionTreeTests (testSuite) where
+
+import Test.Tasty.HUnit
+import Test.Tasty.QuickCheck
+import Test.Tasty
+
+import Yi.CompletionTree as CT
+
+import qualified Data.Map as M
+import Data.List (sort,nub)
+
+testSuite :: TestTree
+testSuite = testGroup "CompletionTree" [propertyTests, unitTests]
+
+propertyTests :: TestTree
+propertyTests = testGroup "properties"
+  [ testProperty "CT.toList . CT.fromList == nub . sort" $
+      \list -> CT.toList (CT.fromList list) == sort (nub (list :: [String]))
+  , testProperty "update (fromList [a]) a == fromList [mempty] (a is a non-empty string)" $
+      \string -> null string || CT.update (CT.fromList [string :: String]) string == CT.fromList [mempty]
+  , testProperty "\"\" `elem` update (fromList [a,...]) a" $
+      \listOfStrings -> null listOfStrings || null (head listOfStrings) || "" `elem` CT.toList (CT.update (CT.fromList listOfStrings) (head listOfStrings))
+  , testProperty "complete (fromList [a]) == (a, fromList [\"\"])" $
+      \string -> CT.complete (CT.fromList [string]) == (string,CT.fromList [""])
+  ]
+
+unitTests :: TestTree
+unitTests = testGroup "unit tests"
+  [ testGroup "fromList"
+      [ testCase "returns an empty CompletionTree when given an empty list" $
+          CT.fromList [] @?= (mempty :: CT.CompletionTree String)
+      , testCase "returns a map with one key when given a list with one item" $
+          CT.fromList ["a"] @?= CT.CompletionTree (M.fromList [("a",mempty)])
+      , testCase "groups elements with the same prefix" $
+          CT.fromList ["aa","ab"] @?= CT.CompletionTree (M.fromList [("a",CT.CompletionTree $ M.fromList [("a",mempty),("b",mempty)])])
+      ]
+  , testGroup "update"
+    -- toList is covered by the SmallCheck and QuickCheck
+      [ testCase "strips its argument from a matching key" $
+          CT.update (CT.fromList ["abc"]) "a" @?= CT.fromList ["bc"]
+      , testCase "descends the tree if a substring of its input is found in the CompletionTree" $
+          CT.update (CT.fromList ["put","putStr"]) "putS" @?= CT.fromList ["tr"]
+      , testCase "returns an empty list if it can't find a matching key" $
+          CT.update (CT.fromList ["put"]) "list" @?= CT.fromList []
+      ]
+  , testGroup "complete"
+      [ testCase "Returns the common prefix" $
+        CT.complete (CT.fromList ["put","putStr","putStrLn"]) @?= ("put",CT.fromList ["","Str","StrLn"])
+      , testCase "Returns an empty string if there's no common prefix" $
+        CT.complete (CT.fromList ["put","putStr","abc"]) @?= ("",CT.fromList ["put","putStr","abc"])
+      ]
+  ]
diff --git a/test/Yi/Mode/CommonTests.hs b/test/Yi/Mode/CommonTests.hs
new file mode 100644
--- /dev/null
+++ b/test/Yi/Mode/CommonTests.hs
@@ -0,0 +1,28 @@
+{-# LANGUAGE OverloadedStrings #-}
+module Yi.Mode.CommonTests (testSuite) where
+
+import Test.Tasty
+import Test.Tasty.QuickCheck
+import Test.Tasty.HUnit
+
+import Yi.Mode.Common
+import Data.Attoparsec.Text (parseOnly)
+import Control.Applicative ((<|>))
+import Data.Either (isRight)
+
+testSuite :: TestTree
+testSuite = testGroup "Mode.Common" [unitTests]
+
+unitTests :: TestTree
+unitTests = testGroup "unit tests"
+  [ testGroup "shebangParser" $
+      [ testCase "matches a simple shebang" $
+          parseOnly (shebangParser "runhaskell") "#!/usr/bin/env runhaskell\n" @?= Right ()
+      , testCase "matches a complex shebang" $
+          map (parseOnly (shebangParser ("python" *> ("3" <|> "2" <|> "")))) ["#!/usr/bin/env python\n", "#!/usr/bin/env python2\n", "#!/usr/bin/env python3\n"] @?= [Right (), Right (), Right ()]
+      , testCase "ignores noise and spaces" $
+          parseOnly (shebangParser "runhaskell") "\n#!abcdefg\r\nABCdefG\n#!   /usr/bin/env  runhaskell    \r\n\n/AbcDe#!/fg\n" @?= Right ()
+      , testCase "parser fails correctly" $
+          isRight (parseOnly (shebangParser "runhaskell") "#!/usr/bin/env abc\n") @?= False
+      ]
+  ]
diff --git a/test/Yi/TagTests.hs b/test/Yi/TagTests.hs
new file mode 100644
--- /dev/null
+++ b/test/Yi/TagTests.hs
@@ -0,0 +1,73 @@
+{-# LANGUAGE OverloadedStrings #-}
+module Yi.TagTests (testSuite) where
+
+import Test.Tasty
+import Test.Tasty.QuickCheck
+import Test.Tasty.HUnit
+
+import Yi.Tag
+import qualified Yi.CompletionTree as CT
+
+import Data.List (sort)
+import Data.Map (keys)
+
+testSuite :: TestTree
+testSuite = testGroup "Tag" [unitTests]
+
+exampleTagTable :: TagTable
+exampleTagTable = TagTable 
+  { tagFileName = "exampleFileName"
+  , tagBaseDir = "."
+  , tagFileMap = cts
+  , tagCompletionTree = CT.fromList . map (_unTag) $ keys cts
+  }
+  where 
+    cts = readCTags $ 
+      "!_TAG_FILE_SORTED        1        ~\n\
+      \Tag               src/Yi/Tag.hs        73;\"        t\n\
+      \Tag               src/Yi/Tag.hs        73;\"        C\n\
+      \Tag               src/Yi/Tag.hs        20;\"        m\n\
+      \TagTable          src/Yi/Tag.hs        82;\"        t\n\
+      \TagTable          src/Yi/Tag.hs        82;\"        C\n\
+      \Tags              src/Yi/Tag.hs        66;\"        t\n\
+      \Tags              src/Yi/Tag.hs        66;\"        C\n\
+      \TagsFileList      src/Yi/Tag.hs        53;\"        t\n\
+      \TagsFileList      src/Yi/Tag.hs        53;\"        C\n\
+      \_unTag            src/Yi/Tag.hs        73;\"        f\n\
+      \_unTagsFileList   src/Yi/Tag.hs        53;\"        f\n\
+      \completeTag       src/Yi/Tag.hs        134;\"       f\n\
+      \getTags           src/Yi/Tag.hs        151;\"       f\n\
+      \hintTags          src/Yi/Tag.hs        127;\"       f\n\
+      \importTagTable    src/Yi/Tag.hs        115;\"       f\n\
+      \lookupTag         src/Yi/Tag.hs        97;\"        f\n\
+      \readCTags         src/Yi/Tag.hs        104;\"       f\n\
+      \resetTags         src/Yi/Tag.hs        147;\"       f\n\
+      \setTags           src/Yi/Tag.hs        143;\"       f\n\
+      \tagBaseDir        src/Yi/Tag.hs        86;\"        f\n\
+      \tagCompletionTree src/Yi/Tag.hs        91;\"        f\n\
+      \tagFileMap        src/Yi/Tag.hs        89;\"        f\n\
+      \tagFileName       src/Yi/Tag.hs        83;\"        f\n\
+      \tagsFileList      src/Yi/Tag.hs        63;\"        f\n\
+      \unTag'            src/Yi/Tag.hs        75;\"        f"
+
+unitTests :: TestTree
+unitTests = testGroup "unit tests"
+  [ testGroup "lookupTag" $ 
+      [ testCase "finds a tag in a taglist" $
+          lookupTag (Tag "Tag") exampleTagTable @?= sort [("./src/Yi/Tag.hs", 73),("./src/Yi/Tag.hs",73),("./src/Yi/Tag.hs",20)]
+      , testCase "returns an empty list if no tag is found" $
+          lookupTag (Tag "") exampleTagTable @?= []
+      ]
+  , testGroup "hintTags" $
+      [ testCase "completes the input with all possible tags" $
+          hintTags exampleTagTable "Tag" @?= ["Tag", "TagTable", "Tags", "TagsFileList"]
+      , testCase "returns an empty list if the input is not a prefix of any tag" $
+          hintTags exampleTagTable "FooBar" @?= []
+      ]
+  , testGroup "completeTag" $
+      [ testCase "extends the input to the longest certain length" $
+          completeTag exampleTagTable "_" @?= "_unTag"
+      , testCase "returns the input when there are no completions possible" $
+          completeTag exampleTagTable "FooBar" @?= "FooBar"
+      ]
+  ]
diff --git a/yi-core.cabal b/yi-core.cabal
new file mode 100644
--- /dev/null
+++ b/yi-core.cabal
@@ -0,0 +1,242 @@
+-- This file has been generated from package.yaml by hpack version 0.14.0.
+--
+-- see: https://github.com/sol/hpack
+
+name:           yi-core
+version:        0.13
+synopsis:       Yi editor core library
+category:       Yi
+homepage:       https://github.com/yi-editor/yi#readme
+bug-reports:    https://github.com/yi-editor/yi/issues
+maintainer:     Yi developers <yi-devel@googlegroups.com>
+license:        GPL-2
+build-type:     Simple
+cabal-version:  >= 1.10
+
+source-repository head
+  type: git
+  location: https://github.com/yi-editor/yi
+
+flag eventlog
+  description: Runtime binary will be compiled with eventlog and RTS options enabled.
+  manual: True
+  default: False
+
+flag hint
+  description: Include hint (haskell interpreter) in yi
+  manual: True
+  default: False
+
+flag profiling
+  description: Runtime binary will be compiled with profiling and RTS options enabled.
+  manual: True
+  default: False
+
+flag testing
+  description: bake-in the self-checks
+  manual: True
+  default: False
+
+library
+  hs-source-dirs:
+      src
+  ghc-options: -Wall -ferror-spans -Wall -fno-warn-orphans -ferror-spans
+  build-depends:
+      base >= 4.8 && < 5
+    , array
+    , attoparsec
+    , binary >= 0.7
+    , bytestring >= 0.9.1
+    , containers
+    , data-default
+    , directory
+    , dlist >= 0.4.1
+    , dynamic-state >= 0.1.0.5
+    , dyre >= 0.8.11
+    , exceptions
+    , filepath >= 1.1
+    , hashable >= 1.1.2.5
+    , Hclip >= 3
+    , ListLike >= 4.5
+    , microlens-platform
+    , mtl >= 0.1.0.1
+    , old-locale
+    , oo-prototypes
+    , parsec >= 3.0
+    , pointedlist >= 0.5
+    , process >= 1.0.1.1
+    , process-extras >= 0.3.3.8
+    , safe >= 0.3.4
+    , semigroups
+    , split >= 0.2
+    , text >= 1.1.1.3
+    , text-icu >= 0.7
+    , time >= 1.1
+    , transformers-base
+    , unix-compat >= 0.1
+    , unordered-containers >= 0.1.3
+    , word-trie >= 0.2.0.4
+    , xdg-basedir >= 0.2.1
+    , yi-language >= 0.1.1.0
+    , yi-rope >= 0.7.0.0
+  if os(win32)
+    build-depends:
+        Win32
+  else
+    build-depends:
+        unix
+  if flag(profiling)
+    cpp-options: -DPROFILING
+  if flag(eventlog)
+    ghc-options: -Wall -fno-warn-orphans -eventlog
+    cpp-options: -DEVENTLOG
+  if flag(hint)
+    cpp-options: -DHINT
+    build-depends:
+        hint > 0.3.1
+  if flag(testing)
+    cpp-options: -DTESTING
+    build-depends:
+        QuickCheck >= 2.7
+      , random
+  exposed-modules:
+      Yi
+      Yi.Boot
+      Yi.Boot.Internal
+      Yi.Buffer
+      Yi.Buffer.Adjusted
+      Yi.Buffer.HighLevel
+      Yi.Buffer.Indent
+      Yi.Buffer.Normal
+      Yi.Buffer.Misc
+      Yi.Buffer.Region
+      Yi.Buffer.TextUnit
+      Yi.Buffer.Undo
+      Yi.Command
+      Yi.Command.Help
+      Yi.Completion
+      Yi.CompletionTree
+      Yi.Config
+      Yi.Config.Default
+      Yi.Config.Misc
+      Yi.Config.Lens
+      Yi.Config.Simple
+      Yi.Config.Simple.Types
+      Yi.Core
+      Yi.Debug
+      Yi.Dired
+      Yi.Editor
+      Yi.Eval
+      Yi.Event
+      Yi.File
+      Yi.History
+      Yi.Hoogle
+      Yi.Hooks
+      Yi.IncrementalParse
+      Yi.Interact
+      Yi.JumpList
+      Yi.Keymap
+      Yi.Keymap.Keys
+      Yi.KillRing
+      Yi.Layout
+      Yi.Main
+      Yi.MiniBuffer
+      Yi.Misc
+      Yi.Mode.Common
+      Yi.Mode.Compilation
+      Yi.Mode.Interactive
+      Yi.Monad
+      Yi.Option
+      Yi.Paths
+      Yi.PersistentState
+      Yi.Process
+      Yi.Rectangle
+      Yi.Snippets
+      Yi.Snippets.Haskell
+      Yi.Search
+      Yi.Search.Internal
+      Yi.String
+      Yi.Syntax.Driver
+      Yi.Syntax.Layout
+      Yi.Syntax.OnlineTree
+      Yi.Syntax.Tree
+      Yi.Tab
+      Yi.Tag
+      Yi.TextCompletion
+      Yi.Types
+      Yi.UI.Common
+      Yi.UI.SimpleLayout
+      Yi.UI.TabBar
+      Yi.UI.Utils
+      Yi.Window
+      System.FriendlyPath
+  other-modules:
+      Control.Exc
+      Data.DelayList
+      Parser.Incremental
+      System.CanonicalizePath
+      Yi.Buffer.Implementation
+      Paths_yi_core
+  default-language: Haskell2010
+
+test-suite tasty
+  type: exitcode-stdio-1.0
+  main-is: Spec.hs
+  hs-source-dirs:
+      test
+  ghc-options: -Wall -ferror-spans
+  build-depends:
+      base >= 4.8 && < 5
+    , array
+    , attoparsec
+    , binary >= 0.7
+    , bytestring >= 0.9.1
+    , containers
+    , data-default
+    , directory
+    , dlist >= 0.4.1
+    , dynamic-state >= 0.1.0.5
+    , dyre >= 0.8.11
+    , exceptions
+    , filepath >= 1.1
+    , hashable >= 1.1.2.5
+    , Hclip >= 3
+    , ListLike >= 4.5
+    , microlens-platform
+    , mtl >= 0.1.0.1
+    , old-locale
+    , oo-prototypes
+    , parsec >= 3.0
+    , pointedlist >= 0.5
+    , process >= 1.0.1.1
+    , process-extras >= 0.3.3.8
+    , safe >= 0.3.4
+    , semigroups
+    , split >= 0.2
+    , text >= 1.1.1.3
+    , text-icu >= 0.7
+    , time >= 1.1
+    , transformers-base
+    , unix-compat >= 0.1
+    , unordered-containers >= 0.1.3
+    , word-trie >= 0.2.0.4
+    , xdg-basedir >= 0.2.1
+    , yi-language >= 0.1.1.0
+    , yi-rope >= 0.7.0.0
+    , tasty
+    , tasty-hunit
+    , tasty-quickcheck
+    , yi-core
+    , text
+    , containers
+  if os(win32)
+    build-depends:
+        Win32
+  else
+    build-depends:
+        unix
+  other-modules:
+      Yi.CompletionTreeTests
+      Yi.Mode.CommonTests
+      Yi.TagTests
+  default-language: Haskell2010
