glualint-1.26.0: app/GLuaFixer/Effects/Files.hs
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
module GLuaFixer.Effects.Files where
import Control.DeepSeq (deepseq, force)
import Control.Monad.IO.Class (liftIO)
import Data.List (foldl', stripPrefix)
import Data.Maybe (fromMaybe, listToMaybe, mapMaybe)
import Effectful (Dispatch (Dynamic), DispatchOf, Eff, Effect, IOE, (:>))
import Effectful.Dispatch.Dynamic (interpose, interpret, send)
import Effectful.Dispatch.Static (HasCallStack)
import GLuaFixer.Effects.Logging (Logging, putStrLnStdError)
import qualified System.Directory as Dir
import System.FilePath (takeDirectory, (</>))
import System.FilePath.Find (FindClause, always, fileName, filePath, find, (&&?), (/~?), (~~?))
import System.FilePath.GlobPattern (GlobPattern)
import System.IO (IOMode (..), hGetContents, hPutStrLn, hSetEncoding, utf8_bom, withFile)
import Prelude hiding (readFile, writeFile)
newtype Directory = Directory {dir :: FilePath}
newtype FileNames = FileNames {names :: [String]}
newtype IgnoreFiles = IgnoreFiles {ignore :: [GlobPattern]}
-- | Interacting with the file system
data Files :: Effect where
-- | Get current working directory alias
GetCurrentDirectory :: Files m FilePath
-- | Read stdin
ReadStdIn :: Files m String
-- | Read a file
ReadFile :: FilePath -> Files m String
-- | Write to a file
WriteFile :: FilePath -> String -> Files m ()
-- | Returns whether a file exists
FileExists :: FilePath -> Files m Bool
-- | Returns whether a file exists and is a directory
IsDirectory :: FilePath -> Files m Bool
-- | Will search upwards in directories for a file, returning it if it exists
SearchUpwardsForFile :: Directory -> FileNames -> Files m (Maybe FilePath)
-- | Returns the file path of the first path that exists
FirstExists :: [FilePath] -> Files m (Maybe FilePath)
-- | Search for Lua files
FindLuaFiles :: IgnoreFiles -> FilePath -> Files m [FilePath]
-- | Get Home directory
GetHomeDirectory :: Files m FilePath
-- | Make a path absolute
MakeAbsolute :: FilePath -> Files m FilePath
type instance DispatchOf Files = Dynamic
makeAbsolute :: Files :> es => FilePath -> Eff es FilePath
makeAbsolute filepath = send $ MakeAbsolute filepath
getHomeDirectory :: Files :> es => Eff es FilePath
getHomeDirectory = send GetHomeDirectory
findLuaFiles :: Files :> es => IgnoreFiles -> FilePath -> Eff es [FilePath]
findLuaFiles ignoreFiles filepath =
send $ FindLuaFiles ignoreFiles filepath
firstExists :: Files :> es => [FilePath] -> Eff es (Maybe FilePath)
firstExists files = send $ FirstExists files
searchUpwardsForFile :: Files :> es => Directory -> FileNames -> Eff es (Maybe FilePath)
searchUpwardsForFile directory files = send $ SearchUpwardsForFile directory files
isDirectory :: Files :> es => FilePath -> Eff es Bool
isDirectory filepath = send $ IsDirectory filepath
fileExists :: Files :> es => FilePath -> Eff es Bool
fileExists filepath = send $ FileExists filepath
writeFile :: Files :> es => FilePath -> String -> Eff es ()
writeFile filepath contents = send $ WriteFile filepath contents
readFile :: Files :> es => FilePath -> Eff es String
readFile filepath = send $ ReadFile filepath
readStdIn :: Files :> es => Eff es String
readStdIn = send ReadStdIn
getCurrentDirectory
:: forall (es_a9miX :: [Effect])
. (HasCallStack, Files :> es_a9miX)
=> Eff es_a9miX FilePath
getCurrentDirectory = send (GetCurrentDirectory @(Eff es_a9miX))
runFilesIO :: forall es a. IOE :> es => Eff (Files : es) a -> Eff es a
runFilesIO = interpret $ \_ -> \case
GetCurrentDirectory -> liftIO Dir.getCurrentDirectory
ReadStdIn -> liftIO getContents
ReadFile file -> liftIO $ do
withFile file ReadMode $ \handle -> do
hSetEncoding handle utf8_bom
!contents <- force <$> hGetContents handle
pure contents
WriteFile file contents -> deepseq contents $
liftIO $
withFile file WriteMode $ \handle -> do
hSetEncoding handle utf8_bom
hPutStrLn handle contents
FileExists filepath -> liftIO $ Dir.doesFileExist filepath
IsDirectory filepath -> liftIO $ Dir.doesDirectoryExist filepath
SearchUpwardsForFile (Directory directory) (FileNames filename) ->
let
go !subdir = do
let
filepaths = fmap (subdir </>) filename
mbFoundFile <- runFilesIO $ firstExists filepaths
case mbFoundFile of
Just file -> pure $ Just file
Nothing -> do
let
up = takeDirectory subdir
dirExists <- runFilesIO $ isDirectory up
if up == subdir || not dirExists
then pure Nothing
else go up
in
go directory
FirstExists filepath -> case filepath of
(file : files) -> do
exists <- runFilesIO $ fileExists file
if exists
then pure $ Just file
else runFilesIO $ firstExists files
[] -> pure Nothing
FindLuaFiles (IgnoreFiles ignoreFiles) path ->
let
ignoredGlobs = foldl' (&&?) always $ map excludeFiles ignoreFiles
-- Exclude a path when either its relative path or its full path match the ignore globs.
excludeFiles :: GlobPattern -> FindClause Bool
excludeFiles glob = relativeFilePath /~? glob &&? filePath /~? glob
relativeFilePath :: FindClause FilePath
relativeFilePath = fmap stripFromPath filePath
-- Turn absolute path into relative path by stripping prefix
stripFromPath :: FilePath -> FilePath
stripFromPath fp =
fromMaybe fp $ listToMaybe $ mapMaybe (`stripPrefix` fp) pathsToStrip
-- Prefixes to attempt to strip, in order
pathsToStrip =
[ "./"
, path ++ "/"
, path
]
in
liftIO $ find always (fileName ~~? "*.lua" &&? ignoredGlobs) path
GetHomeDirectory -> liftIO Dir.getHomeDirectory
MakeAbsolute filepath -> liftIO $ Dir.makeAbsolute filepath
-- | Trace actions on files, useful for debugging
traceFiles :: (Files :> es, Logging :> es) => Eff es a -> Eff es a
traceFiles = interpose $ \_ -> \case
GetCurrentDirectory -> do
putStrLnStdError "GetCurrentDirectory"
send GetCurrentDirectory
ReadStdIn -> do
putStrLnStdError "ReadStdIn"
send ReadStdIn
ReadFile filepath -> do
putStrLnStdError $ "ReadFile " <> filepath
send $ ReadFile filepath
WriteFile filepath contents -> do
putStrLnStdError $ "WriteFile " <> filepath
send $ WriteFile filepath contents
FileExists filepath -> do
putStrLnStdError "FileExists"
send $ FileExists filepath
IsDirectory filepath -> do
putStrLnStdError $ "IsDirectory " <> filepath
send $ IsDirectory filepath
SearchUpwardsForFile directory filenames -> do
putStrLnStdError $ "SearchUpwardsForFile " <> directory.dir <> " " <> show filenames.names
send $ SearchUpwardsForFile directory filenames
FirstExists filepath -> do
putStrLnStdError $ "FirstExists " <> show filepath
send $ FirstExists filepath
FindLuaFiles ignores filepath -> do
putStrLnStdError $ "FindLuaFiles " <> show ignores.ignore <> " " <> filepath
send $ FindLuaFiles ignores filepath
GetHomeDirectory -> do
putStrLnStdError "GetHomeDirectory"
send GetHomeDirectory
MakeAbsolute filepath -> do
putStrLnStdError $ "MakeAbsolute " <> filepath
send $ MakeAbsolute filepath
traceFilesIfEnabled :: (Files :> es, Logging :> es) => Bool -> Eff es a -> Eff es a
traceFilesIfEnabled enabled = if enabled then traceFiles else id