packages feed

cgrep-8.0.0: src/CGrep/Output.hs

--
-- Copyright (c) 2013-2023 Nicola Bonelli <nicola@larthia.com>
--
-- This program is free software; you can redistribute it and/or modify
-- it under the terms of the GNU General Public License as published by
-- the Free Software Foundation; either version 2 of the License, or
-- (at your option) any later version.
--
-- This program is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-- GNU General Public License for more details.
--
-- You should have received a copy of the GNU General Public License
-- along with this program; if not, write to the Free Software
-- Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
--

{-# LANGUAGE ExistentialQuantification #-}

module CGrep.Output ( Output(..)
                    , mkOutputElements
                    , putOutputElements
                    , runSearch
                    , showFileName
                    , showBold) where

import qualified Data.ByteString as B
import qualified Data.ByteString.Builder as B

import qualified Data.ByteString.Char8 as C
import qualified Data.ByteString.Lazy.Char8 as LC
import qualified Data.ByteString.Unsafe as BU

import qualified Data.Vector.Unboxed as UV

import Data.Vector.Unboxed ( (!) )

import System.Console.ANSI
    ( setSGRCode,
      ConsoleIntensity(BoldIntensity),
      SGR(SetConsoleIntensity) )

import Control.Monad.Trans.Reader ( ask, reader )
import Control.Monad.IO.Class ( MonadIO(liftIO) )

import Data.List
    ( foldl', sortBy, groupBy, isPrefixOf, nub, sort, genericLength, intersperse )
import Data.Function ( on )

import CGrep.Types ( Text8, Offset )
import CGrep.Parser.Chunk ( Chunk(..), MatchLine(..) )

import Config ( Config(configColorFile, configColorMatch) )
import Reader ( ReaderIO, Env(..) )
import Data.Int ( Int64 )
import Data.Word ( Word8 )
import Data.ByteString.Internal (c2w)
import qualified Data.Vector.Fusion.Util as VU (Box(..))

import System.Posix.FilePath (RawFilePath)

import qualified Data.Vector.Generic as GV
import CGrep.Parser.Line ( getLineOffsets )
import Options
    ( Options(Options, invert_match, json, filename_only, no_shallow,
              no_filename, count, no_color, no_numbers, no_column, show_match,
              color) )

data Output = Output
    { outFilePath :: RawFilePath
    , outLineNumb :: {-# UNPACK #-} !Int64
    , outLine     :: {-# UNPACK #-} !Text8
    , outChunks   :: ![Chunk]
    }

outTokens :: Output -> [Text8]
outTokens (Output fp ln l cs) = cToken <$> cs
{-# INLINE  outTokens #-}


insertIndex :: UV.Vector Offset -> Offset -> Int
insertIndex vs x = search vs 0 (UV.length vs)
    where search xs !lo !hi
            | lo == hi = lo
            | otherwise = let !mid = (lo + hi) `quot` 2
                    in if x < VU.unBox(xs `GV.basicUnsafeIndexM` mid)
                        then search xs lo mid
                        else search xs (mid+1) hi


getLineNumberAndOffset :: UV.Vector Offset -> Offset -> (# Int, Offset #)
getLineNumberAndOffset xs x =
    let idx = insertIndex xs x
    in (# idx, x - xs `UV.unsafeIndex` (idx-1) #)
{-# INLINE getLineNumberAndOffset #-}


mkOutputElements :: UV.Vector Int64 -> RawFilePath -> Text8 -> Text8 -> [Chunk] -> ReaderIO [Output]
mkOutputElements lineOffsets f text multi ts = do
    invert <- invert_match <$> reader opt
    return $ if invert then map (\(MatchLine n xs) -> Output f n (ls !! fromIntegral (n-1)) xs) . invertLines (length ls) $ mkMatchLines lineOffsets multi ts
                       else map (\(MatchLine n xs) -> Output f n (ls !! fromIntegral (n-1)) xs) $ mkMatchLines lineOffsets multi ts
    where ls = C.lines text
{-# INLINE mkOutputElements #-}


mkMatchLines :: UV.Vector Int64 -> Text8 -> [Chunk] -> [MatchLine]
mkMatchLines lineOffsets _ [] = []
mkMatchLines lineOffsets text ts = map mergeGroup $ groupBy ((==) `on` lOffset) . sortBy (compare `on` lOffset) $
    (\chunk -> let (# r, c #) = getLineNumberAndOffset lineOffsets (cOffset chunk) in MatchLine (fromIntegral r) [Chunk (cTyp chunk) (cToken chunk) c]) <$> ts
        where mergeGroup :: [MatchLine] -> MatchLine
              mergeGroup ls = MatchLine ((lOffset . head) ls) (foldl' (\l m -> l <> lChunks m) [] ls)


invertLines :: Int -> [MatchLine] -> [MatchLine]
invertLines n xs =  filter (\(MatchLine i _) ->  i `notElem` idx ) $ take n [ MatchLine i [] | i <- [1..]]
    where idx = lOffset <$> xs
{-# INLINE invertLines #-}


putOutputElements :: [Output] -> ReaderIO (Maybe B.Builder)
putOutputElements [] = pure Nothing
putOutputElements out = do
    Env{..} <- ask
    if  | json opt          -> Just <$> jsonOutput out
        | filename_only opt -> Just <$> filenameOutput out
        | otherwise         -> Just <$> defaultOutput out

runSearch :: Options
          -> RawFilePath
          -> Bool
          -> ReaderIO [Output]
          -> ReaderIO [Output]
runSearch opt filename eligible doSearch =
    if eligible || no_shallow opt
        then doSearch
        else mkOutputElements UV.empty filename C.empty C.empty ([] :: [Chunk])


defaultOutput :: [Output] -> ReaderIO B.Builder
defaultOutput xs = do
    Env{..} <- ask
    if  |  Options{ no_filename = False, no_numbers = False , count = False } <- opt
                -> pure $ mconcat . intersperse (B.char8 '\n') $ map (\out -> buildFileName conf opt out <> B.char8 ':' <> buildLineCol opt out <> B.char8 ':' <> buildTokens opt out <> buildLine conf opt out) xs

        |  Options{ no_filename = False, no_numbers = True  , count = False } <- opt
                -> pure $ mconcat . intersperse (B.char8 '\n') $ map (\out -> buildFileName conf opt out <> B.char8 ':' <> buildTokens opt out <> buildLine conf opt out) xs

        |  Options{ no_filename = True , no_numbers = False , count = False } <- opt
                -> pure $ mconcat . intersperse (B.char8 '\n') $ map (\out -> buildTokens opt out <> buildLine conf opt out) xs

        |  Options{ no_filename = True , no_numbers = True  , count = False } <- opt
                -> pure $ mconcat . intersperse (B.char8 '\n') $ map (\out -> buildTokens opt out <> buildLine conf opt out) xs

        |  Options{ no_filename = False, count = True } <- opt
                -> do
                    let gs = groupBy (\(Output f1 _ _ _) (Output f2 _ _ _) -> f1 == f2) xs
                    pure $ mconcat . intersperse (B.char8 '\n') $ (\ys@(y:_) -> buildFileName conf opt y <> B.char8 ':' <> B.intDec (length ys)) <$> gs
        |  Options{ count = True } <- opt
                -> do
                    let gs = groupBy (\(Output f1 _ _ _) (Output f2 _ _ _) -> f1 == f2) xs
                    pure $ mconcat . intersperse (B.char8 '\n') $ (\ys@(y:_) ->  B.intDec (length ys)) <$> gs


jsonOutput :: [Output] -> ReaderIO B.Builder
jsonOutput [] = pure mempty
jsonOutput outs = pure $ mconcat . intersperse (B.char8 '\n') $
        [B.byteString "{ \"file\":\"" <> B.byteString fname <> B.byteString "\", \"matches\":["] <>
        [ mconcat $ intersperse (B.char8 ',') (foldl mkMatch [] outs) ] <> [B.byteString "]}"]
     where fname | (Output f _ _ _) <- head outs = f
           mkJToken chunk = B.byteString "{ \"col\":" <> B.int64Dec (cOffset chunk) <> B.byteString ", \"token\":\"" <> B.byteString (cToken chunk) <> B.byteString "\" }"
           mkMatch xs (Output _ n _ ts) =
               xs <> [B.byteString "{ \"row\": " <> B.int64Dec n <> B.byteString ", \"tokens\":[" <>
                        mconcat (intersperse (B.byteString ",") (map mkJToken ts)) <> B.byteString "] }" ]


filenameOutput :: [Output] -> ReaderIO B.Builder
filenameOutput outs = return $ mconcat . intersperse (B.char8 '\n') $ B.byteString <$> nub ((\(Output fname _ _ _) -> fname) <$> outs)
{-# INLINE filenameOutput #-}


bold, reset :: C.ByteString
bold  = C.pack $ setSGRCode [SetConsoleIntensity BoldIntensity]
reset = C.pack $ setSGRCode []
{-# NOINLINE bold #-}
{-# NOINLINE reset #-}

boldBuilder, resetBuilder :: B.Builder
boldBuilder = B.byteString bold
resetBuilder = B.byteString reset
{-# NOINLINE boldBuilder #-}
{-# NOINLINE resetBuilder #-}

type ColorString = C.ByteString


buildFileName :: Config -> Options -> Output -> B.Builder
buildFileName conf opt = buildFileName' conf opt . outFilePath
    where buildFileName' :: Config -> Options -> B.ByteString -> B.Builder
          buildFileName' conf opt = buildColoredAs opt $ C.pack (setSGRCode (configColorFile conf))
{-# INLINE buildFileName #-}


buildColoredAs :: Options -> ColorString -> B.ByteString -> B.Builder
buildColoredAs Options { color = c, no_color = c'} colorCode str
    | c && not c'= B.byteString colorCode <> B.byteString str <> resetBuilder
    | otherwise  = B.byteString str
{-# INLINE buildColoredAs #-}


buildLineCol :: Options -> Output -> B.Builder
buildLineCol Options{no_numbers = True } _ = mempty
buildLineCol Options{no_numbers = False, no_column = True  } (Output _ n _ _)  = B.int64Dec n
buildLineCol Options{no_numbers = False, no_column = False } (Output _ n _ []) = B.int64Dec n
buildLineCol Options{no_numbers = False, no_column = False } (Output _ n _ ts) = B.int64Dec n <> B.char8 ':' <> B.int64Dec ((+1) . cOffset . head $ ts)
{-# INLINE buildLineCol #-}


buildTokens :: Options -> Output -> B.Builder
buildTokens Options { show_match = st } out
    | st        = boldBuilder <> mconcat (B.byteString <$> outTokens out) <> resetBuilder <> B.char8 ':'
    | otherwise = mempty


buildLine :: Config -> Options -> Output -> B.Builder
buildLine conf Options { color = c, no_color = c' } out
    | c && not c'= highlightLine conf (sortBy (flip compare `on` (C.length . cToken)) (outChunks out)) (outLine out)
    | otherwise  = B.byteString $ outLine out
{-# INLINE buildLine #-}


showFileName :: Config -> Options -> RawFilePath -> RawFilePath
showFileName conf opt = showColoredAs opt $ C.pack (setSGRCode (configColorFile conf))
{-# INLINE showFileName #-}


showBold :: Options -> C.ByteString -> C.ByteString
showBold opt = showColoredAs opt bold
{-# INLINE showBold #-}


showColoredAs :: Options -> C.ByteString -> C.ByteString -> C.ByteString
showColoredAs Options { color = c, no_color = c'} colorCode str
    | c && not c'= colorCode <> str <> reset
    | otherwise  = str
{-# INLINE showColoredAs #-}


highlightLine :: Config -> [Chunk] -> Text8 -> B.Builder
highlightLine conf ts =  highlightLine' (highlightIndexes ts, 0, 0)
    where highlightLine' :: ([(Int64, Int64)], Int64, Int) -> C.ByteString -> B.Builder
          highlightLine'  _ (C.uncons -> Nothing) = mempty
          highlightLine' (ns, !n, !bs) s@(C.uncons -> Just (x,_)) =
                (if | check && bs' == 0 -> if fst stack > 0 then B.string8 colorMatch <> B.char8 x <> resetBuilder else B.char8 x <> resetBuilder
                    | check && bs' > 0 -> B.string8 colorMatch <> B.char8 x
                    | otherwise -> B.byteString next) <> highlightLine' (ns, n + nn, bs') rest
            where stack = foldr (\(a, b) (c, d) -> (c + fromEnum (a == n), d + fromEnum (b == n))) (0, 0) ns
                  check = fst stack > 0 || snd stack > 0
                  colorMatch = setSGRCode (configColorMatch conf)
                  bs' = bs + fst stack - snd stack
                  plain = nub . sort $ foldr (\(a, b) acc -> a : b : acc) [] ns
                  nn | check = 1
                     | null plain' = fromIntegral (C.length s)
                     | otherwise = head plain' - n
                         where plain' = dropWhile (<=n) plain
                  (next, rest) = C.splitAt (fromIntegral nn) s
          highlightLine'  _ _ = undefined


highlightIndexes :: [Chunk] -> [(Int64, Int64)]
highlightIndexes = foldr (\chunk a -> let b = cOffset chunk in (fromIntegral b, b + fromIntegral (C.length (cToken chunk)) - 1) : a) [] . filter (not. B.null . cToken)
{-# INLINE highlightIndexes #-}