packages feed

yi-0.3: Yi/Indent.hs

{-# LANGUAGE DeriveDataTypeable #-}

-- 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.Indent where

import Control.Monad

import Yi.Buffer
import Yi.Buffer.HighLevel
-- import Yi.Debug
import Yi.Dynamic

import Yi.Buffer.Region

import Data.Char
import Data.Typeable
import Data.List

{- 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
                                      , tabSize :: Int
                                      , shiftWidth :: Int
                                     }
                      deriving (Eq, Show, Typeable)

{- The default indent settings should likely be initializable from a global
 - preference.
 - Currently the initial settings reflect what's usually used in Yi's sources.
 - One aspect that is common in Yi that is not covered is that the first
 - indent is usually 4 spaces instead of 2.
 -}
instance Initializable IndentSettings where
    initial = IndentSettings True 2 2

{- Inserts either a \t or the number of spaces specified by tabSize in the
 - IndentSettings
 -}
insertTabB :: BufferM ()
insertTabB = do
  indentSettings <- indentSettingsB
  insertN $ if expandTabs indentSettings
    then replicate (tabSize indentSettings) ' '
    else ['\t']

indentSettingsB :: BufferM IndentSettings
indentSettingsB = getDynamicB

getPreviousLineB :: BufferM String
getPreviousLineB =
  savingExcursionB $ do
    lineUp
    readLnB

fetchPreviousIndentsB :: BufferM [Int]
fetchPreviousIndentsB = do
  p0 <- pointB
  lineUp
  p1 <- pointB
  l <- readLnB
  i <- indentOfB l
  if p0 == p1 || i == 0 then return [0] else do
    is <- fetchPreviousIndentsB
    return (i:is)

cycleIndentsB :: [Int] -> BufferM ()
cycleIndentsB indents = do
  l <- readLnB
  curIndent <- indentOfB l
  let (below, above) = span (< curIndent) $ indents
  -- msgEditor $ show (below, above)
  indentToB $ last (above ++ below)

autoIndentB :: BufferM ()
autoIndentB = do
  is <- savingExcursionB fetchPreviousIndentsB
  pl <- getPreviousLineB
  pli <- indentOfB pl
  let i       = lastOpenBracket pl
      indents = pli+2 : i : is
  cycleIndentsB $ sort $ nub indents


-- returns the position of the last opening bracket
-- (where bracket is parens, braces or curly braces)
-- on the given line, if there is one.
lastOpenBracket :: String -> Int
lastOpenBracket s =
  -- The 'max' is really here because if there is no opening
  -- bracket in then 'afterLastOpen' is the whole line and
  -- consequently 'fromEnd + 1' is one more than the whole
  -- of the line.
  max 0 indentation
  where
  indentation   = (length s) - (fromEnd + 1)
  fromEnd       = length afterLastOpen
  afterLastOpen = takeWhile (not . isOpening) $ reverse s

  isOpening :: Char -> Bool
  isOpening '(' = True
  isOpening '[' = True
  isOpening '{' = True
  isOpening _   = False

indentOfB :: String -> BufferM Int
indentOfB = spacingOfB . takeWhile isSpace

spacingOfB :: String -> BufferM Int
spacingOfB text = do
  indentSettings <- indentSettingsB
  let spacingOfChar '\t' = tabSize indentSettings
      spacingOfChar _ = 1
  return $ sum $ map spacingOfChar text

indentToB :: Int -> BufferM ()
indentToB level = do
  l   <- readLnB
  cur <- offsetFromSol
  moveToSol
  deleteToEol
  let (curIndent, restOfLine) = span isSpace l
      origOffsetFromIndent = cur - (length curIndent)
      newFromEol = if origOffsetFromIndent <= 0
                      then length restOfLine
                      else (length restOfLine - origOffsetFromIndent)
  insertN (replicate level ' ' ++ restOfLine)
  leftN newFromEol

-- | Indent as much as the previous line
indentAsPreviousB :: BufferM ()
indentAsPreviousB =
  do pl  <- getPreviousLineB
     pli <- indentOfB pl
     indentToB pli


-- | shiftIndent num
-- |  shifts right (or left if num is negative) num times, filling in tabs if
-- |  expandTabs is set in the buffers IndentSettings
-- TODO: This uses mkVimRegion
shiftIndentOfLine :: Int -> BufferM ()
shiftIndentOfLine numOfShifts = do
  moveToSol
  sol <- pointB
  firstNonSpaceB
  -- ptOfNonSpace <- pointB
  isAtSol <- atSol
  when (not isAtSol) leftB
  ptOfLastSpace <- pointB
  -- msgEditor ("ptOfLastSpace= " ++ (show ptOfLastSpace) ++ "-" ++ (show sol) ++ "=" ++ (show (ptOfLastSpace - sol)))
  indentSettings <- indentSettingsB
  let countSpace '\t' = tabSize indentSettings
      countSpace _ = 1 -- we'll assume nothing but tabs and spaces
  cnt <- if isAtSol then return 0
                    else readRegionB (mkVimRegion sol ptOfLastSpace) >>= return . sum . map countSpace
  if not isAtSol then deleteRegionB (mkVimRegion sol ptOfLastSpace)
                 else return ()

  let newcount = cnt + ((shiftWidth indentSettings) * numOfShifts)
  if (newcount <= 0)
     then return ()
     else do
       let tabs   = replicate (newcount `div` (tabSize indentSettings)) '\t'
           spaces = replicate (newcount `mod` (tabSize indentSettings)) ' '
       moveToSol
       insertN $ if expandTabs indentSettings then replicate newcount ' '
                               else tabs ++ spaces

       firstNonSpaceB

shiftIndentOfSelection :: Int -> BufferM ()
shiftIndentOfSelection shiftCount = do
  mark <- getSelectionMarkPointB
  (row2,_) <- getLineAndCol
  moveTo mark
  (row1,_) <- getLineAndCol
  let step = if (row2 > row1)
             then lineDown
             else lineUp
      numOfLines = 1 + (abs (row2 - row1))
  replicateM_ numOfLines (shiftIndentOfLine shiftCount >> step)