module Proteome.Grep.Process where

import qualified Data.Text as Text
import Data.Text (isInfixOf)
import Path (Abs, Dir, File, Path, parseAbsFile, relfile, toFilePath)
import Path.IO (findExecutable, isLocationOccupied)
import Ribosome (pathText)
import Ribosome.Final (inFinal_)
import Ribosome.Menu (MenuItem)
import qualified Streamly.Data.Fold as Fold
import qualified Streamly.Internal.Unicode.Stream as Stream
import qualified Streamly.Prelude as Stream
import Streamly.Prelude (IsStream)
import qualified Streamly.System.Process as Process
import Streamly.System.Process (ProcessFailure)

import qualified Proteome.Data.GrepError as GrepError (GrepError (..))
import Proteome.Data.GrepError (GrepError)
import Proteome.Data.GrepOutputLine (GrepOutputLine)
import Proteome.Grep.Parse (parseGrepOutput)
import Proteome.System.Path (findExe)

defaultGrepCmdline :: Text
defaultGrepCmdline :: Text
defaultGrepCmdline =
  Text
"grep -Hnr {pattern} {path}"

defaultRgCmdline :: Text
defaultRgCmdline :: Text
defaultRgCmdline =
  Text
"rg --vimgrep --no-heading -e {pattern} {path}"

defaultCmdline ::
  Member (Embed IO) r =>
  Sem r Text
defaultCmdline :: forall (r :: EffectRow). Member (Embed IO) r => Sem r Text
defaultCmdline =
  Path Rel File -> Sem r (Maybe (Path Abs File))
forall (m :: * -> *).
MonadIO m =>
Path Rel File -> m (Maybe (Path Abs File))
findExecutable [relfile|rg|] Sem r (Maybe (Path Abs File))
-> (Maybe (Path Abs File) -> Text) -> Sem r Text
forall (f :: * -> *) a b. Functor f => f a -> (a -> b) -> f b
<&> \case
    Just Path Abs File
_ ->
      Text
defaultRgCmdline
    Maybe (Path Abs File)
_ ->
      Text
defaultGrepCmdline

patternPlaceholder :: Text
patternPlaceholder :: Text
patternPlaceholder =
  Text
"{pattern}"

pathPlaceholder :: Text
pathPlaceholder :: Text
pathPlaceholder =
  Text
"{path}"

replaceOrAppend :: Text -> Text -> [Text] -> [Text]
replaceOrAppend :: Text -> Text -> [Text] -> [Text]
replaceOrAppend Text
placeholder Text
target [Text]
segments | (Text -> Bool) -> [Text] -> Bool
forall (t :: * -> *) a. Foldable t => (a -> Bool) -> t a -> Bool
any (Text
placeholder Text -> Text -> Bool
`isInfixOf`) [Text]
segments =
  Text -> Text -> Text -> Text
Text.replace Text
placeholder Text
target (Text -> Text) -> [Text] -> [Text]
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> [Text]
segments
replaceOrAppend Text
_ Text
target [Text]
segments =
  [Text]
segments [Text] -> [Text] -> [Text]
forall a. Semigroup a => a -> a -> a
<> [Text
Item [Text]
target]

parseAbsExe ::
  Member (Stop GrepError) r =>
  Text ->
  Sem r (Path Abs File)
parseAbsExe :: forall (r :: EffectRow).
Member (Stop GrepError) r =>
Text -> Sem r (Path Abs File)
parseAbsExe Text
exe =
  GrepError
-> Either SomeException (Path Abs File) -> Sem r (Path Abs File)
forall err' (r :: EffectRow) err a.
Member (Stop err') r =>
err' -> Either err a -> Sem r a
stopEitherAs (Text -> GrepError
GrepError.NoSuchExecutable Text
exe) (Either SomeException (Path Abs File) -> Sem r (Path Abs File))
-> Either SomeException (Path Abs File) -> Sem r (Path Abs File)
forall a b. (a -> b) -> a -> b
$ FilePath -> Either SomeException (Path Abs File)
forall (m :: * -> *). MonadThrow m => FilePath -> m (Path Abs File)
parseAbsFile (Text -> FilePath
forall a. ToString a => a -> FilePath
toString Text
exe)

grepCmdline ::
  Members [Stop GrepError, Embed IO] r =>
  Text ->
  Text ->
  Path Abs Dir ->
  [Text] ->
  Sem r (Path Abs File, [Text])
grepCmdline :: forall (r :: EffectRow).
Members '[Stop GrepError, Embed IO] r =>
Text
-> Text -> Path Abs Dir -> [Text] -> Sem r (Path Abs File, [Text])
grepCmdline Text
cmdline Text
patt Path Abs Dir
destination [Text]
opt = do
  Bool -> Sem r () -> Sem r ()
forall (f :: * -> *). Applicative f => Bool -> f () -> f ()
when (Text -> Bool
Text.null Text
exe) do
    GrepError -> Sem r ()
forall e (r :: EffectRow) a. Member (Stop e) r => e -> Sem r a
stop GrepError
GrepError.Empty
  Path Abs File
absExe <- if Text -> Bool
absolute Text
exe then Text -> Sem r (Path Abs File)
forall (r :: EffectRow).
Member (Stop GrepError) r =>
Text -> Sem r (Path Abs File)
parseAbsExe Text
exe else Text -> Sem r (Path Abs File)
forall (r :: EffectRow).
Members '[Stop GrepError, Embed IO] r =>
Text -> Sem r (Path Abs File)
findExe Text
exe
  Sem r Bool -> Sem r () -> Sem r ()
forall (m :: * -> *). Monad m => m Bool -> m () -> m ()
unlessM (Path Abs Dir -> Sem r Bool
forall (m :: * -> *) b t. MonadIO m => Path b t -> m Bool
isLocationOccupied Path Abs Dir
destination) do
    GrepError -> Sem r ()
forall e (r :: EffectRow) a. Member (Stop e) r => e -> Sem r a
stop GrepError
destError
  pure (Path Abs File
absExe, Path Abs Dir -> [Text]
withDir Path Abs Dir
destination)
  where
    absolute :: Text -> Bool
absolute Text
a =
      Int -> Text -> Text
Text.take Int
1 Text
a Text -> Text -> Bool
forall a. Eq a => a -> a -> Bool
== Text
"/"
    argSegments :: [Text]
argSegments =
      [Text]
opt [Text] -> [Text] -> [Text]
forall a. Semigroup a => a -> a -> a
<> Text -> [Text]
Text.words (Text -> Text
Text.strip Text
args)
    (Text
exe, Text
args) =
      Text -> Text -> (Text, Text)
Text.breakOn Text
" " (Text -> Text
Text.strip Text
cmdline)
    withDir :: Path Abs Dir -> [Text]
withDir Path Abs Dir
dir =
      Text -> Text -> [Text] -> [Text]
replaceOrAppend Text
pathPlaceholder (Path Abs Dir -> Text
forall b t. Path b t -> Text
pathText Path Abs Dir
dir) [Text]
withPattern
    withPattern :: [Text]
withPattern =
      Text -> Text -> [Text] -> [Text]
replaceOrAppend Text
patternPlaceholder Text
patt [Text]
argSegments
    destError :: GrepError
destError =
      Path Abs Dir -> GrepError
GrepError.NoSuchDestination Path Abs Dir
destination

processOutput ::
  IsStream t =>
  String ->
  [Text] ->
  t IO Word8
processOutput :: forall (t :: (* -> *) -> * -> *).
IsStream t =>
FilePath -> [Text] -> t IO Word8
processOutput FilePath
exe [Text]
args =
  FilePath -> [FilePath] -> t IO Word8
forall (t :: (* -> *) -> * -> *) (m :: * -> *).
(IsStream t, MonadAsync m, MonadCatch m) =>
FilePath -> [FilePath] -> t m Word8
Process.toBytes FilePath
exe (Text -> FilePath
forall a. ToString a => a -> FilePath
toString (Text -> FilePath) -> [Text] -> [FilePath]
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> [Text]
args)

processLines ::
  IsStream t =>
  Path Abs File ->
  [Text] ->
  t IO Text
processLines :: forall (t :: (* -> *) -> * -> *).
IsStream t =>
Path Abs File -> [Text] -> t IO Text
processLines Path Abs File
exe [Text]
args =
  Fold IO Char Text -> t IO Char -> t IO Text
forall (m :: * -> *) (t :: (* -> *) -> * -> *) b.
(Monad m, IsStream t) =>
Fold m Char b -> t m Char -> t m b
Stream.lines (FilePath -> Text
forall a. ToText a => a -> Text
toText (FilePath -> Text) -> Fold IO Char FilePath -> Fold IO Char Text
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Fold IO Char FilePath
forall (m :: * -> *) a. Monad m => Fold m a [a]
Fold.toList) (t IO Char -> t IO Text) -> t IO Char -> t IO Text
forall a b. (a -> b) -> a -> b
$
  t IO Word8 -> t IO Char
forall (m :: * -> *) (t :: (* -> *) -> * -> *).
(Monad m, IsStream t) =>
t m Word8 -> t m Char
Stream.decodeUtf8 (t IO Word8 -> t IO Char) -> t IO Word8 -> t IO Char
forall a b. (a -> b) -> a -> b
$
  FilePath -> [Text] -> t IO Word8
forall (t :: (* -> *) -> * -> *).
IsStream t =>
FilePath -> [Text] -> t IO Word8
processOutput (Path Abs File -> FilePath
forall b t. Path b t -> FilePath
toFilePath Path Abs File
exe) [Text]
args

grepMenuItems ::
  Functor (t IO) =>
  Members [Log, Embed IO, Final IO] r =>
  IsStream t =>
  Path Abs Dir ->
  Path Abs File ->
  [Text] ->
  Sem r (t IO (MenuItem GrepOutputLine))
grepMenuItems :: forall (t :: (* -> *) -> * -> *) (r :: EffectRow).
(Functor (t IO), Members '[Log, Embed IO, Final IO] r,
 IsStream t) =>
Path Abs Dir
-> Path Abs File
-> [Text]
-> Sem r (t IO (MenuItem GrepOutputLine))
grepMenuItems Path Abs Dir
cwd Path Abs File
exe [Text]
args =
  (forall (f :: * -> *).
 Functor f =>
 (forall x. Sem r x -> IO (Maybe x))
 -> (Sem r () -> IO ())
 -> (forall x. x -> IO (f x))
 -> IO (f (t IO (MenuItem GrepOutputLine))))
-> Sem r (t IO (MenuItem GrepOutputLine))
forall (r :: EffectRow) a.
Member (Final IO) r =>
(forall (f :: * -> *).
 Functor f =>
 (forall x. Sem r x -> IO (Maybe x))
 -> (Sem r () -> IO ()) -> (forall x. x -> IO (f x)) -> IO (f a))
-> Sem r a
inFinal_ \ forall x. Sem r x -> IO (Maybe x)
lower Sem r () -> IO ()
_ forall x. x -> IO (f x)
pur ->
    t IO (MenuItem GrepOutputLine)
-> IO (f (t IO (MenuItem GrepOutputLine)))
forall x. x -> IO (f x)
pur (t IO (MenuItem GrepOutputLine)
 -> IO (f (t IO (MenuItem GrepOutputLine))))
-> t IO (MenuItem GrepOutputLine)
-> IO (f (t IO (MenuItem GrepOutputLine)))
forall a b. (a -> b) -> a -> b
$
    (ProcessFailure -> t IO (MenuItem GrepOutputLine))
-> t IO (MenuItem GrepOutputLine) -> t IO (MenuItem GrepOutputLine)
forall (t :: (* -> *) -> * -> *) (m :: * -> *) e a.
(IsStream t, MonadCatch m, Exception e) =>
(e -> t m a) -> t m a -> t m a
Stream.handle ProcessFailure -> t IO (MenuItem GrepOutputLine)
forall {t :: (* -> *) -> * -> *} {m :: * -> *} {a}.
IsStream t =>
ProcessFailure -> t m a
processFailure (t IO (MenuItem GrepOutputLine) -> t IO (MenuItem GrepOutputLine))
-> t IO (MenuItem GrepOutputLine) -> t IO (MenuItem GrepOutputLine)
forall a b. (a -> b) -> a -> b
$
    (Text -> IO (Maybe (MenuItem GrepOutputLine)))
-> t IO Text -> t IO (MenuItem GrepOutputLine)
forall (t :: (* -> *) -> * -> *) (m :: * -> *) a b.
(IsStream t, MonadAsync m, Functor (t m)) =>
(a -> m (Maybe b)) -> t m a -> t m b
Stream.mapMaybeM ((Maybe (Maybe (MenuItem GrepOutputLine))
 -> Maybe (MenuItem GrepOutputLine))
-> IO (Maybe (Maybe (MenuItem GrepOutputLine)))
-> IO (Maybe (MenuItem GrepOutputLine))
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
fmap Maybe (Maybe (MenuItem GrepOutputLine))
-> Maybe (MenuItem GrepOutputLine)
forall (m :: * -> *) a. Monad m => m (m a) -> m a
join (IO (Maybe (Maybe (MenuItem GrepOutputLine)))
 -> IO (Maybe (MenuItem GrepOutputLine)))
-> (Text -> IO (Maybe (Maybe (MenuItem GrepOutputLine))))
-> Text
-> IO (Maybe (MenuItem GrepOutputLine))
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Sem r (Maybe (MenuItem GrepOutputLine))
-> IO (Maybe (Maybe (MenuItem GrepOutputLine)))
forall x. Sem r x -> IO (Maybe x)
lower (Sem r (Maybe (MenuItem GrepOutputLine))
 -> IO (Maybe (Maybe (MenuItem GrepOutputLine))))
-> (Text -> Sem r (Maybe (MenuItem GrepOutputLine)))
-> Text
-> IO (Maybe (Maybe (MenuItem GrepOutputLine)))
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Path Abs Dir -> Text -> Sem r (Maybe (MenuItem GrepOutputLine))
forall (r :: EffectRow).
Members '[Log, Embed IO] r =>
Path Abs Dir -> Text -> Sem r (Maybe (MenuItem GrepOutputLine))
parseGrepOutput Path Abs Dir
cwd) (t IO Text -> t IO (MenuItem GrepOutputLine))
-> t IO Text -> t IO (MenuItem GrepOutputLine)
forall a b. (a -> b) -> a -> b
$
    Path Abs File -> [Text] -> t IO Text
forall (t :: (* -> *) -> * -> *).
IsStream t =>
Path Abs File -> [Text] -> t IO Text
processLines Path Abs File
exe [Text]
args
  where
    processFailure :: ProcessFailure -> t m a
processFailure (ProcessFailure
_ :: ProcessFailure) =
      t m a
forall (t :: (* -> *) -> * -> *) (m :: * -> *) a.
IsStream t =>
t m a
Stream.nil