proteome-0.9.9.9: lib/Proteome/Files/Source.hs
module Proteome.Files.Source where
import Control.Concurrent.STM (atomically)
import Control.Concurrent.STM.TMChan (TMChan, closeTMChan, newTMChan, readTMChan, writeTMChan)
import Control.Lens.Regex.Text (match, regexing)
import qualified Data.List.NonEmpty as NonEmpty (toList, zip)
import qualified Data.Set as Set (fromList, toList)
import qualified Data.Text as Text
import Exon (exon)
import Path (
Abs,
Dir,
File,
Path,
Rel,
dirname,
filename,
isProperPrefixOf,
parent,
parseAbsFile,
relfile,
stripProperPrefix,
toFilePath,
)
import Path.IO (doesDirExist, findExecutable, walkDir)
import qualified Path.IO as WalkAction (WalkAction (WalkExclude))
import Ribosome (pathText)
import Ribosome.Menu (MenuItem (MenuItem))
import Ribosome.Menu.Stream.Util (takeUntilNothing)
import qualified Streamly.Prelude as Stream
import Streamly.Prelude (IsStream, SerialT)
import System.FilePattern ((?==))
import Text.Regex.PCRE.Light (Regex)
import Proteome.Data.FileScanItem (FileScanItem (FileScanItem))
import Proteome.Data.FilesConfig (FilesConfig (FilesConfig))
import Proteome.Grep.Process (processLines)
import Proteome.Path (dropSlash)
-- TODO store traversals instead of Regexes?
matchPath :: [Regex] -> Path Abs t -> Bool
matchPath excludes path =
any check excludes
where
check rgx =
pathText path & has (regexing rgx . match)
hiddenFilter ::
(Path Abs t -> Path Rel t) ->
Bool ->
Path Abs t ->
Bool
hiddenFilter lastSegment True =
Text.isPrefixOf "." . toText . toFilePath . lastSegment
hiddenFilter _ False =
const False
filterFiles ::
Bool ->
[Regex] ->
[String] ->
[Path Abs File] ->
[Path Abs File]
filterFiles excludeHidden patterns wildignore =
filter (not . cond)
where
cond path =
matchPath patterns path ||
hiddenFilter filename excludeHidden path ||
any (?== name) wildignore
where
name =
toFilePath (filename path)
filterDirs ::
Bool ->
[Regex] ->
[Path Abs Dir] ->
[Path Abs Dir]
filterDirs excludeHidden patterns =
filter pred'
where
pred' a =
matchPath patterns a || hiddenFilter dirname excludeHidden a
scan ::
Member (Embed IO) r =>
FilesConfig ->
TMChan FileScanItem ->
Path Abs Dir ->
Maybe Text ->
Sem r ()
scan (FilesConfig _ excludeHidden ignoreFiles ignoreDirs wildignore) chan dir baseIndicator =
tryAny_ do
walkDir enqueue dir
where
enqueue _ dirs files' =
exclude <$ atomically (traverse_ (writeTMChan chan) filteredFiles)
where
filteredFiles =
cons <$> filterFiles excludeHidden ignoreFiles (toString <$> wildignore) files'
exclude =
WalkAction.WalkExclude (filterDirs excludeHidden ignoreDirs dirs)
cons =
FileScanItem dir baseIndicator
runScanners ::
Members [Async, Embed IO] r =>
FilesConfig ->
TMChan FileScanItem ->
NonEmpty (Path Abs Dir, Maybe Text) ->
Sem r ()
runScanners conf chan paths = do
threads <- traverse (async . uncurry (scan conf chan)) paths
traverse_ await threads <* embed (atomically (closeTMChan chan))
withBaseIndicators ::
NonEmpty (Path Abs Dir) ->
NonEmpty (Path Abs Dir, Maybe Text)
withBaseIndicators bases@(_ :| []) =
(, Nothing) <$> bases
withBaseIndicators bases =
NonEmpty.zip bases (findSegment bases)
where
findSegment paths
| namesUnique paths = Just . dropSlash . dirname <$> paths
| allEqual next = Nothing <$ paths
| otherwise = findSegment next
where
next = parent <$> paths
namesUnique paths =
uniq names == NonEmpty.toList names
where
names = dirname <$> paths
allEqual paths =
length (uniq paths) == 1
uniq as =
Set.toList (Set.fromList (NonEmpty.toList as))
fileMenuItem ::
Path Abs Dir ->
Maybe Text ->
Path Abs File ->
MenuItem (Path Abs File)
fileMenuItem base baseIndicator path =
MenuItem path text display
where
display =
" * " <> maybe "" indicator baseIndicator <> text
text =
toText (maybe (toFilePath path) toFilePath relativePath)
relativePath =
stripProperPrefix base path
indicator name =
"[" <> name <> "] "
chanStream ::
IsStream t =>
Functor (t IO) =>
TMChan a ->
t IO a
chanStream chan =
takeUntilNothing (Stream.repeatM (liftIO (atomically (readTMChan chan))))
filesNative ::
Members [Async, Embed IO] r =>
FilesConfig ->
NonEmpty (Path Abs Dir) ->
Sem r (SerialT IO (MenuItem (Path Abs File)))
filesNative conf paths = do
chan <- embed (atomically newTMChan)
void . async $ runScanners conf chan (withBaseIndicators paths)
pure (menuItem <$> chanStream chan)
where
menuItem (FileScanItem base baseIndicator path) =
fileMenuItem base baseIndicator path
rgExcludes :: FilesConfig -> [Text]
rgExcludes (FilesConfig _ _ _ _ wilds) =
concat (wild <$> wilds)
where
wild i =
["--glob", [exon|!#{i}|]]
findBase ::
Path Abs File ->
NonEmpty (Path Abs Dir, Maybe Text) ->
Maybe (Path Abs Dir, Maybe Text)
findBase file =
find ((`isProperPrefixOf` file) . fst)
rgMenuItem ::
NonEmpty (Path Abs Dir, Maybe Text) ->
Text ->
Maybe (MenuItem (Path Abs File))
rgMenuItem bases file = do
path <- parseAbsFile (toString file)
(base, baseIndicator) <- findBase path bases
pure (fileMenuItem base baseIndicator path)
filesRg ::
Path Abs File ->
FilesConfig ->
NonEmpty (Path Abs Dir) ->
SerialT IO (MenuItem (Path Abs File))
filesRg rgExe conf paths =
Stream.mapMaybe item $
processLines rgExe ("--files" : excludes <> patterns)
where
patterns =
toText . toFilePath <$> toList paths
excludes =
rgExcludes conf
item =
rgMenuItem (withBaseIndicators paths)
files ::
Members [Async, Embed IO] r =>
FilesConfig ->
NonEmpty (Path Abs Dir) ->
Sem r (SerialT IO (MenuItem (Path Abs File)))
files conf@(FilesConfig useRg _ _ _ _) paths =
filterM doesDirExist (toList paths) >>= \case
[] ->
pure Stream.nil
p : ps ->
findExecutable [relfile|rg|] >>= \case
Just rgExe | useRg ->
pure (filesRg rgExe conf (p :| ps))
_ ->
filesNative conf (p :| ps)