{-# LANGUAGE LambdaCase #-} -------------------------------------------------------------------------------- -- | This module provides you with a line-based editor. It's main feature is -- that you can specify multiple changes at the same time, e.g.: -- -- > [deleteLine 3, changeLine 4 ["Foo"]] -- -- when this is evaluated, we take into account that 4th line will become the -- 3rd line before it needs changing. module Language.Haskell.Stylish.Editor ( module Language.Haskell.Stylish.Block , Edits , apply , replace , replaceRealSrcSpan , changeLine , changeLines , insertLines ) where -------------------------------------------------------------------------------- import qualified Data.Map as M import Data.Maybe (fromMaybe) import qualified GHC.Types.SrcLoc as GHC -------------------------------------------------------------------------------- import Language.Haskell.Stylish.Block -------------------------------------------------------------------------------- data Change -- | Insert some lines. = CInsert [String] -- | Replace the block of N lines by the given lines. | CBlock Int ([String] -> [String]) -- | Replace (startCol, endCol) by the given string on this line. | CLine Int Int String -------------------------------------------------------------------------------- -- | Due to the function in CBlock we cannot write a lawful Ord instance, but -- this lets us merge-sort changes. beforeChange :: Change -> Change -> Bool beforeChange (CInsert _) _ = True beforeChange _ (CInsert _) = False beforeChange (CBlock _ _) _ = True beforeChange _ (CBlock _ _) = False beforeChange (CLine x _ _) (CLine y _ _) = x <= y -------------------------------------------------------------------------------- prettyChange :: Int -> Change -> String prettyChange l (CInsert ls) = show l ++ " insert " ++ show (length ls) ++ " lines" prettyChange l (CBlock n _) = show l ++ "-" ++ show (l + n) ++ " replace lines" prettyChange l (CLine start end x) = show l ++ ":" ++ show start ++ "-" ++ show end ++ " replace by " ++ show x -------------------------------------------------------------------------------- -- | Merge in order mergeChanges :: [Change] -> [Change] -> [Change] mergeChanges = go where go [] ys = ys go xs [] = xs go (x : xs) (y : ys) = if x `beforeChange` y then x : go xs (y : ys) else y : go (x : xs) ys -------------------------------------------------------------------------------- -- Stores sorted spans to change per line. newtype Edits = Edits {unEdits :: M.Map Int [Change]} -------------------------------------------------------------------------------- instance Show Edits where show edits = unlines $ do (line, changes) <- M.toAscList $ unEdits edits prettyChange line <$> changes -------------------------------------------------------------------------------- instance Semigroup Edits where Edits l <> Edits r = Edits $ M.unionWith mergeChanges l r -------------------------------------------------------------------------------- instance Monoid Edits where mempty = Edits mempty -------------------------------------------------------------------------------- replaceRealSrcSpan :: GHC.RealSrcSpan -> String -> Edits replaceRealSrcSpan rss repl | GHC.srcSpanStartLine rss /= GHC.srcSpanEndLine rss = mempty | otherwise = replace (GHC.srcSpanStartLine rss) (GHC.srcSpanStartCol rss) (GHC.srcSpanEndCol rss) repl -------------------------------------------------------------------------------- replace :: Int -> Int -> Int -> String -> Edits replace line startCol endCol repl | startCol > endCol = mempty | otherwise = Edits $ M.singleton line [CLine startCol endCol repl] -------------------------------------------------------------------------------- changeLine :: Int -> (String -> [String]) -> Edits changeLine start f = changeLines (Block start start) $ \ls -> case ls of l : _ -> f l _ -> f "" -------------------------------------------------------------------------------- changeLines :: Block String -> ([String] -> [String]) -> Edits changeLines (Block start end) f = Edits $ M.singleton start [CBlock (end - start + 1) f] -------------------------------------------------------------------------------- insertLines :: Int -> [String] -> Edits insertLines line ls = Edits $ M.singleton line [CInsert ls] -------------------------------------------------------------------------------- data Conflict = Conflict Int Change Int Change -------------------------------------------------------------------------------- prettyConflict :: Conflict -> String prettyConflict (Conflict l1 c1 l2 c2) = unlines [ "Conflict between edits:" , "- " ++ prettyChange l1 c1 , "- " ++ prettyChange l2 c2 ] -------------------------------------------------------------------------------- conflicts :: Edits -> [Conflict] conflicts (Edits edits) = M.toAscList edits >>= uncurry checkChanges where checkChanges _ [] = [] checkChanges i (CInsert _ : cs) = checkChanges i cs checkChanges i (c1@(CBlock _ _) : c2 : _) = [Conflict i c1 i c2] checkChanges i [c1@(CBlock n _)] = do i' <- [i + 1 .. i + n - 1] case M.lookup i' edits of Just (c2 : _) -> [Conflict i c1 i' c2] _ -> [] checkChanges i (c1@(CLine xstart xend _) : c2@(CLine ystart _ _) : cs) | xstart == ystart = [Conflict i c1 i c2] | xend > ystart = [Conflict i c1 i c2] | otherwise = checkChanges i (c2 : cs) checkChanges _ (CLine _ _ _ : _) = [] -------------------------------------------------------------------------------- apply :: Edits -> [String] -> [String] apply (Edits edits) = case conflicts (Edits edits) of c : _ -> error $ "Language.Haskell.Stylish.Editor: " ++ prettyConflict c _ -> go 1 (editsFor 1) where editsFor i = fromMaybe [] $ M.lookup i edits go _ _ [] = [] go i [] (l : ls) = l : go (i + 1) (editsFor $ i + 1) ls go i (CInsert ls' : cs) ls = ls' ++ go i cs ls go i (CBlock n f : _cs) ls = let (domain, ls') = splitAt n ls in f domain ++ go (i + n) (editsFor $ i + n) ls' go i (CLine xstart xend x : cs) (l : ls) = let l' = take (xstart - 1) l ++ x ++ drop (xend - 1) l in go i (adjust xstart xend x <$> cs) (l' : ls) adjust _ _ _ (CInsert xs) = CInsert xs adjust _ _ _ (CBlock n f) = CBlock n f adjust xstart xend x (CLine ystart yend y) | ystart >= xend = let offset = length x - (xend - xstart) in CLine (ystart + offset) (yend + offset) y | otherwise = CLine ystart yend y