module DiffRender.DiffRender where import Common import Control.Monad.IO.Class import Data.List as DL import Data.Text as T import Data.Vector.Mutable (IOVector) import qualified Data.Vector.Mutable as MV import System.Console.ANSI (Color(..), ColorIntensity(..), ConsoleLayer(..), SGR(..), Underlining(..), setSGRCode) import Test.Common data DiffRender = DiffRender { dfScreenStateBack :: ScreenState , dfScreenState :: ScreenState , dfDebug :: Bool } emptyDiffRender :: Dimensions -> IO DiffRender emptyDiffRender Dimensions { diH = rows, diW = cols } = do ss <- emptyScreenState rows cols ssBack <- emptyScreenState rows cols pure $ DiffRender { dfScreenStateBack = ssBack, dfScreenState = ss, dfDebug = False } emptyScreenState :: Int -> Int -> IO ScreenState emptyScreenState rows cols = do stLines <- MV.generate rows (\_ -> [Plain (T.replicate cols " ")]) pure (ScreenState stLines (ScreenPos 0 0) cols True) dfGetCursorPosition :: DiffRender -> ScreenPos dfGetCursorPosition dfr = ssCursorPos $ dfScreenStateBack dfr dfSetCursorPosition :: (Int -> Int) -> (Int -> Int) -> DiffRender -> DiffRender dfSetCursorPosition fx fy dfr = let screenState = dfScreenStateBack dfr screenLines = ssLines screenState screenColumns = ssColumns screenState x = fx $ sX $ ssCursorPos $ dfScreenStateBack dfr y = fy $ sY $ ssCursorPos $ dfScreenStateBack dfr in if (x >= 0 && x < screenColumns) && (y >= 0 && y < (MV.length screenLines)) then dfr { dfScreenStateBack = screenState { ssCursorOverflow = False, ssCursorPos = ScreenPos x y }} else dfr { dfScreenStateBack = screenState { ssCursorOverflow = True }} dfPutText :: MonadIO m => StyledText -> DiffRender -> m () dfPutText t (DiffRender { dfDebug = _, dfScreenStateBack = ScreenState {ssLines = ssLns, ssCursorOverflow = cursorOverflow, ssCursorPos = ScreenPos cx cy} }) = do -- Write stuff to the backbuffer. If the cursor is in an overflow position, then do nothing. if cursorOverflow then pure () else liftIO $ flip (MV.modify ssLns) cy $ \l -> stInsert l cx t data ScreenState = ScreenState { ssLines :: IOVector [StyledText] , ssCursorPos :: ScreenPos , ssColumns :: Int , ssCursorOverflow :: Bool } screenStateDimension :: ScreenState -> Dimensions screenStateDimension ScreenState {..} = Dimensions ssColumns (MV.length ssLines) copyScreenState :: ScreenState -> ScreenState -> IO () copyScreenState sss ssd = MV.copy (ssLines ssd) (ssLines sss) diffRenderToDimension :: DiffRender -> Dimensions diffRenderToDimension DiffRender {..} = screenStateDimension dfScreenState data Style = FgBg Color Color| Fg Color | Bg Color | TextUnderline | NoStyle deriving (Eq, Show) data StyledText = StyledText Style [StyledText] | Plain Text deriving (Show, Eq) renderLines :: [[StyledText]] -> Text renderLines st = T.intercalate "\n" $ (\i -> (T.concat $ stRender <$> i)) <$> st instance HasGen StyledText where getGen = recursive choice [Plain . T.pack <$> txGen] [StyledText NoStyle <$> list (linear 1 100) getGen] where txGen = list (linear 1 100) (choice [lower, upper]) -- Punch a hole starting from offset of size length. Columns start -- from 0. Returns chunks left and right of the hole. punchHole :: (Int, Int) -> [StyledText] -> ([StyledText], [StyledText]) punchHole (tk, ln) sts = (stTake tk sts, stDrop (tk + ln) sts) stInsert :: [StyledText] -> Int -> StyledText -> [StyledText] stInsert sts cx t = let (lr, rg) = punchHole (cx, stLength t) sts in (lr <> [t] <> rg) stDrop :: Int -> [StyledText] -> [StyledText] stDrop l stsIn = snd $ DL.foldl' stDrop' (l, []) stsIn where stDrop' :: (Int, [StyledText]) -> StyledText -> (Int, [StyledText]) stDrop' (0, sts) st = (0, sts <> [st]) stDrop' (s, sts) (Plain t) = let r = T.drop s t rlen = T.length r in if rlen > 0 then (s - (T.length t - rlen), sts <> [Plain r]) else (s - T.length t, sts) stDrop' (s, sts) (StyledText st sts') = case DL.foldl' stDrop' (s, []) sts' of (s', sts''@(_:_)) -> (s', sts <> [StyledText st sts'']) (s', _) -> (s', sts) stTake :: Int -> [StyledText] -> [StyledText] stTake l stsIn = snd $ DL.foldl' stTake' (l, []) stsIn where stTake' :: (Int, [StyledText]) -> StyledText -> (Int, [StyledText]) stTake' (0, sts) _ = (0, sts) stTake' (s, sts) (Plain t) = let r = T.take s t in (s - T.length r, sts <> [Plain r]) stTake' (s, sts) (StyledText st sts') = let (s', sts'') = DL.foldl' stTake' (s, []) sts' in (s', sts <> [StyledText st sts'']) applyStyleToRange :: (StyledText -> StyledText) -> Int -> (Int, Int) -> [StyledText] -> [StyledText] applyStyleToRange fn sStart (rStart, rEnd) segments = let sEnd = sStart + stTotalLength segments - 1 in if rEnd >= sStart && rStart <= sEnd then let r1 = max 0 (rStart - sStart) r2 = rEnd - (max sStart rStart) + 1 in (stTake r1 segments) <> (fn <$> (stTake r2 (stDrop r1 segments))) <> (stDrop (r1 + r2) segments) else segments stLength :: StyledText -> Int stLength (Plain t) = T.length t stLength (StyledText _ sts) = sum (stLength <$> sts) stTotalLength :: [StyledText] -> Int stTotalLength sts = sum $ stLength <$> sts -- This implimentation is not optimal and does not correctly -- render nested styles. But this appear to be good enough for now. styleToPrefix :: Style -> Text styleToPrefix st = T.pack $ case st of FgBg fg bg -> setSGRCode [SetColor Foreground Vivid fg] <> setSGRCode [SetColor Background Vivid bg] Bg bg -> setSGRCode [SetColor Background Vivid bg] Fg fg -> setSGRCode [SetColor Foreground Vivid fg] TextUnderline -> setSGRCode [SetUnderlining SingleUnderline] NoStyle -> mempty stRender :: StyledText -> Text stRender st = stRender' NoStyle st mergeStyles :: Style -> Style -> Style mergeStyles NoStyle a = a mergeStyles a NoStyle = a mergeStyles (Fg a) (Bg b) = (FgBg a b) mergeStyles (Bg a) (Fg b) = (FgBg b a) mergeStyles _ a = a stRender' :: Style -> StyledText -> Text stRender' _ (Plain t) = T.replace "\n" " " t stRender' pst (StyledText st sts) = let suffix = T.pack $ setSGRCode [] prefix = styleToPrefix (mergeStyles pst st) in T.concat (((\x -> prefix <> x <> suffix) . stRender' st) <$> sts)