{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ViewPatterns #-} {-# OPTIONS_HADDOCK show-extensions #-} -- | -- Module : Yi.Keymap.Vim.VisualMap -- License : GPL-2 -- Maintainer : yi-devel@googlegroups.com -- Stability : experimental -- Portability : portable -- -- I'm a module waiting for some kind soul to give me a commentary! module Yi.Keymap.Vim.VisualMap ( defVisualMap ) where import Lens.Micro.Platform ((.=)) import Control.Monad (forM_, void, when) import Data.Char (ord) import Data.List (group) import Data.Maybe (fromJust, fromMaybe) import qualified Data.Text as T (unpack) import Yi.Buffer hiding (Insert) import Yi.Editor import Yi.Keymap.Vim.Common import Yi.Keymap.Vim.Operator (VimOperator (..), opDelete, stringToOperator) import Yi.Keymap.Vim.StateUtils import Yi.Keymap.Vim.StyledRegion (StyledRegion (StyledRegion), transformCharactersInRegionB) import Yi.Keymap.Vim.Tag (gotoTag) import Yi.Keymap.Vim.TextObject import Yi.Keymap.Vim.Utils (matchFromBool, mkChooseRegisterBinding, mkMotionBinding, addNewLineIfNecessary, pasteInclusiveB) import Yi.MiniBuffer (spawnMinibufferE) import Yi.Monad (whenM) import qualified Yi.Rope as R (toText, countNewLines) import Yi.Tag (Tag (Tag)) import Yi.Utils (SemiNum ((-~))) defVisualMap :: [VimOperator] -> [VimBinding] defVisualMap operators = [escBinding, motionBinding, textObjectBinding, changeVisualStyleBinding, setMarkBinding] ++ [chooseRegisterBinding, pasteBinding] ++ operatorBindings operators ++ digitBindings ++ [replaceBinding, switchEdgeBinding] ++ [insertBinding, exBinding, shiftDBinding] ++ [tagJumpBinding] escAction :: EditorM RepeatToken escAction = do resetCountE clrStatus withCurrentBuffer $ do setVisibleSelection False putRegionStyle Inclusive switchModeE Normal return Drop escBinding :: VimBinding escBinding = VimBindingE f where f evs (VimState { vsMode = (Visual _) }) = escAction <$ matchFromBool (evs `elem` ["", ""]) f _ _ = NoMatch exBinding :: VimBinding exBinding = VimBindingE f where f ":" (VimState { vsMode = (Visual _) }) = WholeMatch $ do void $ spawnMinibufferE ":" id withCurrentBuffer $ writeN "'<,'>" switchModeE Ex return Finish f _ _ = NoMatch digitBindings :: [VimBinding] digitBindings = zeroBinding : fmap mkDigitBinding ['1' .. '9'] zeroBinding :: VimBinding zeroBinding = VimBindingE f where f "0" (VimState { vsMode = (Visual _) }) = WholeMatch $ do currentState <- getEditorDyn case vsCount currentState of Just c -> do setCountE (10 * c) return Continue Nothing -> do withCurrentBuffer moveToSol resetCountE withCurrentBuffer $ stickyEolA .= False return Continue f _ _ = NoMatch setMarkBinding :: VimBinding setMarkBinding = VimBindingE (f . T.unpack . _unEv) where f "m" (VimState { vsMode = (Visual _) }) = PartialMatch f ('m':c:[]) (VimState { vsMode = (Visual _) }) = WholeMatch $ do withCurrentBuffer $ setNamedMarkHereB [c] return Continue f _ _ = NoMatch changeVisualStyleBinding :: VimBinding changeVisualStyleBinding = VimBindingE f where f evs (VimState { vsMode = (Visual _) }) | evs `elem` ["v", "V", ""] = WholeMatch $ do currentMode <- fmap vsMode getEditorDyn let newStyle = case evs of "v" -> Inclusive "V" -> LineWise "" -> Block _ -> error "Just silencing false positive warning." newMode = Visual newStyle if newMode == currentMode then escAction else do modifyStateE $ \s -> s { vsMode = newMode } withCurrentBuffer $ do putRegionStyle newStyle rectangleSelectionA .= (Block == newStyle) setVisibleSelection True return Finish f _ _ = NoMatch mkDigitBinding :: Char -> VimBinding mkDigitBinding c = VimBindingE (f . T.unpack . _unEv) where f [c'] (VimState { vsMode = (Visual _) }) | c == c' = WholeMatch $ do modifyStateE mutate return Continue f _ _ = NoMatch mutate vs@(VimState {vsCount = Nothing}) = vs { vsCount = Just d } mutate vs@(VimState {vsCount = Just count}) = vs { vsCount = Just $ count * 10 + d } d = ord c - ord '0' motionBinding :: VimBinding motionBinding = mkMotionBinding Continue $ \m -> case m of Visual _ -> True _ -> False textObjectBinding :: VimBinding textObjectBinding = VimBindingE (f . T.unpack . _unEv) where f (stringToTextObject -> PartialMatch) (VimState {vsMode = Visual _}) = PartialMatch f (stringToTextObject -> WholeMatch to) (VimState {vsMode = Visual _, vsCount = mbCount}) = let count = fromMaybe 1 mbCount in WholeMatch $ do withCurrentBuffer $ do StyledRegion _ reg <- regionOfTextObjectB (CountedTextObject count to) setSelectionMarkPointB (regionStart reg) moveTo (regionEnd reg -~ 1) return Continue f _ _ = NoMatch regionOfSelectionB :: BufferM Region regionOfSelectionB = savingPointB $ do start <- getSelectionMarkPointB stop <- pointB return $! mkRegion start stop operatorBindings :: [VimOperator] -> [VimBinding] operatorBindings operators = fmap mkOperatorBinding $ operators ++ visualOperators where visualOperators = fmap synonymOp [ ("x", "d") , ("s", "c") , ("S", "c") , ("C", "c") , ("~", "g~") , ("Y", "y") , ("u", "gu") , ("U", "gU") ] synonymOp (newName, existingName) = VimOperator newName . operatorApplyToRegionE . fromJust . stringToOperator operators $ existingName chooseRegisterBinding :: VimBinding chooseRegisterBinding = mkChooseRegisterBinding $ \s -> case s of (VimState { vsMode = (Visual _) }) -> True _ -> False shiftDBinding :: VimBinding shiftDBinding = VimBindingE (f . T.unpack . _unEv) where f "D" (VimState { vsMode = (Visual _) }) = WholeMatch $ do (Visual style) <- vsMode <$> getEditorDyn reg <- withCurrentBuffer regionOfSelectionB case style of Block -> withCurrentBuffer $ do (start, lengths) <- shapeOfBlockRegionB reg moveTo start startCol <- curCol forM_ (reverse [0 .. length lengths - 1]) $ \l -> do moveTo start void $ lineMoveRel l whenM (fmap (== startCol) curCol) deleteToEol leftOnEol _ -> do reg' <- withCurrentBuffer $ convertRegionToStyleB reg LineWise reg'' <- withCurrentBuffer $ mkRegionOfStyleB (regionStart reg') (regionEnd reg' -~ Size 1) Exclusive void $ operatorApplyToRegionE opDelete 1 $ StyledRegion LineWise reg'' resetCountE switchModeE Normal return Finish f _ _ = NoMatch mkOperatorBinding :: VimOperator -> VimBinding mkOperatorBinding op = VimBindingE f where f evs (VimState { vsMode = (Visual _) }) = action <$ evs `matchesString` Ev (_unOp $ operatorName op) f _ _ = NoMatch action = do (Visual style) <- vsMode <$> getEditorDyn region <- withCurrentBuffer regionOfSelectionB count <- getCountE token <- operatorApplyToRegionE op count $ StyledRegion style region resetCountE clrStatus withCurrentBuffer $ do setVisibleSelection False putRegionStyle Inclusive return token replaceBinding :: VimBinding replaceBinding = VimBindingE (f . T.unpack . _unEv) where f evs (VimState { vsMode = (Visual _) }) = case evs of "r" -> PartialMatch ('r':c:[]) -> WholeMatch $ do (Visual style) <- vsMode <$> getEditorDyn region <- withCurrentBuffer regionOfSelectionB withCurrentBuffer $ transformCharactersInRegionB (StyledRegion style region) (\x -> if x == '\n' then x else c) void escAction return Finish _ -> NoMatch f _ _ = NoMatch data VisualPaste = PasteBefore | ReplaceSelection deriving (Eq, Ord, Show) pasteBinding :: VimBinding pasteBinding = VimBindingE (f . T.unpack . _unEv) where f "P" VimState { vsMode = (Visual style) } = pasteMatch style PasteBefore f "p" VimState { vsMode = (Visual style) } = pasteMatch style ReplaceSelection f _ _ = NoMatch pasteMatch :: RegionStyle -> VisualPaste -> MatchResult (EditorM RepeatToken) pasteMatch style p = WholeMatch $ do register <- getRegisterE . vsActiveRegister =<< getEditorDyn maybe (pure ()) (paste style p) register void escAction return Finish paste :: RegionStyle -> VisualPaste -> Register -> EditorM () paste LineWise = linePaste paste Block = blockPaste paste Inclusive = otherPaste paste Exclusive = otherPaste linePaste :: VisualPaste -> Register -> EditorM () linePaste p (Register _style rope) = withCurrentBuffer $ do region <- regionOfSelectionB when (p == ReplaceSelection) . void $ deleteRegionWithStyleB region LineWise insertRopeWithStyleB (addNewLineIfNecessary rope) LineWise blockPaste :: VisualPaste -> Register -> EditorM () blockPaste p (Register _style rope) = withCurrentBuffer $ do here <- pointB there <- getSelectionMarkPointB (here', there') <- flipRectangleB here there reg <- regionOfSelectionB moveTo (minimum [here, there, here', there']) when (p == ReplaceSelection) . void $ deleteRegionWithStyleB reg Block if R.countNewLines rope == 0 then actionOnLeft reg $ maybe (pure ()) (\_ -> insertRopeWithStyleB rope _style) else pasteInclusiveB rope _style where -- Taken from deleteRegionWithStyleB actionOnLeft :: Region -> (Maybe Point -> BufferM ()) -> BufferM () actionOnLeft reg action = savingPointB $ do (start, lengths) <- shapeOfBlockRegionB reg moveTo start forM_ (zip [0..] lengths) $ \(i, l) -> do p' <- pointB moveTo start void $ lineMoveRel i action (if l == 0 then Nothing else Just p') otherPaste :: VisualPaste -> Register -> EditorM () otherPaste _ (Register _style rope) = withCurrentBuffer $ do region <- regionOfSelectionB region' <- convertRegionToStyleB region Inclusive replaceRegionB region' rope switchEdgeBinding :: VimBinding switchEdgeBinding = VimBindingE (f . T.unpack . _unEv) where f [c] (VimState { vsMode = (Visual _) }) | c `elem` ['o', 'O'] = WholeMatch $ do (Visual style) <- vsMode <$> getEditorDyn withCurrentBuffer $ do here <- pointB there <- getSelectionMarkPointB (here', there') <- case (c, style) of ('O', Block) -> flipRectangleB here there (_, _) -> return (there, here) moveTo here' setSelectionMarkPointB there' return Continue f _ _ = NoMatch insertBinding :: VimBinding insertBinding = VimBindingE (f . T.unpack . _unEv) where f evs (VimState { vsMode = (Visual _) }) | evs `elem` group "IA" = WholeMatch $ do (Visual style) <- vsMode <$> getEditorDyn region <- withCurrentBuffer regionOfSelectionB cursors <- withCurrentBuffer $ case evs of "I" -> leftEdgesOfRegionB style region "A" -> rightEdgesOfRegionB style region _ -> error "Just silencing ghc's false positive warning." case cursors of [] -> error "No cursor to move to (in Yi.Keymap.Vim.VisualMap.insertBinding)" (mainCursor : _) -> withCurrentBuffer (moveTo mainCursor) modifyStateE $ \s -> s { vsSecondaryCursors = drop 1 cursors } withCurrentBuffer $ setVisibleSelection False switchModeE $ Insert (head evs) return Continue f _ _ = NoMatch tagJumpBinding :: VimBinding tagJumpBinding = VimBindingY (f . T.unpack . _unEv) where f "" (VimState { vsMode = (Visual _) }) = WholeMatch $ do tag <- Tag . R.toText <$> withCurrentBuffer (regionOfSelectionB >>= readRegionB) void $ withEditor escAction gotoTag tag 0 Nothing return Finish f _ _ = NoMatch