{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE ViewPatterns #-}
{-# LANGUAGE DeriveGeneric #-}

-- |
-- Module      : Clod.IgnorePatterns
-- Description : Functions for handling ignore patterns (.gitignore, .clodignore)
-- Copyright   : (c) Fuzz Leonard, 2025
-- License     : MIT
-- Maintainer  : cyborg@bionicfuzz.com
-- Stability   : experimental
--
-- This module provides functionality for parsing and matching .gitignore and
-- .clodignore patterns to determine which files should be excluded from processing.
--
-- The module supports common gitignore patterns including:
--
-- * Simple file patterns: @README.md@, @LICENSE@
-- * Directory patterns: @node_modules/@, @dist/@
-- * Extension patterns: @*.js@, @*.svg@
-- * Path patterns: @src/components/@
-- * Patterns with wildcards: @**\/node_modules@, @src\/**\/*.js@
-- * Negation patterns: @!important.txt@ (to exclude a file from a broader pattern)
-- * Character classes: @[abc]file.txt@, @file[0-9].txt@
--
-- === Default Patterns
--
-- This module provides default .clodignore patterns that are embedded directly into the
-- executable at compile time using Template Haskell.
--
-- === Pattern Matching Rules
--
-- 1. File extension patterns (@*.ext@) match any file with that extension
-- 2. Directory patterns match at any level in the directory tree
-- 3. Patterns with leading slash (@\/dist@) are anchored to the repository root
-- 4. Patterns with trailing slash are treated as directories
-- 5. Patterns with wildcards use simplified glob matching
-- 6. Negation patterns (@!pattern@) re-include a previously excluded file
-- 7. Later patterns take precedence over earlier ones
--
-- === Usage
--
-- @
-- -- Read patterns from a .clodignore file
-- patterns <- readClodIgnore "/path/to/repo"
--
-- -- Check if a file matches any pattern
-- if matchesIgnorePattern patterns "src/components/Button.jsx"
--   then -- Skip the file
--   else -- Process the file
-- @

module Clod.IgnorePatterns
  ( -- * Pattern reading functions
    readClodIgnore
  , readGitIgnore
  , readNestedGitIgnores
  , createDefaultClodIgnore
    -- * Pattern matching functions
  , matchesIgnorePattern
  , simpleGlobMatch
  , makePatternMatcher
    -- * Pattern types and utilities
  , PatternType(..)
  , categorizePatterns
    -- * Embedded content
  , defaultClodIgnoreContent
  , defaultClodIgnoreContentStr
  ) where

import Control.Monad (filterM)
import Control.Monad.IO.Class (liftIO)
import Control.Exception (try, SomeException)
import Control.Monad.Except (throwError)
import qualified Data.List as L
import Data.Char (toLower)
import qualified Data.Map.Strict as Map
import System.Directory (doesDirectoryExist, doesFileExist, getDirectoryContents)
import System.FilePath (splitDirectories, takeExtension, takeFileName, takeDirectory, (</>))
import Data.FileEmbed (embedStringFile)
import qualified Data.ByteString.Char8 as BS
import qualified Data.Text as T
import Data.Text (Text)
import GHC.Generics (Generic)
import qualified Dhall

import Clod.Types (ClodM, IgnorePattern(..), ClodError(..))
import Clod.Config (clodIgnoreFile)

-- | Pattern synonyms for common ignore pattern structures
pattern FileExtension :: String -> String
pattern $mFileExtension :: forall {r}. [Char] -> ([Char] -> r) -> ((# #) -> r) -> r
$bFileExtension :: [Char] -> [Char]
FileExtension ext <- ('*':'.':ext@(_:_)) where
  FileExtension [Char]
ext = Char
'*'Char -> [Char] -> [Char]
forall a. a -> [a] -> [a]
:Char
'.'Char -> [Char] -> [Char]
forall a. a -> [a] -> [a]
:[Char]
ext

pattern DirectoryWildcard :: String -> String
pattern $mDirectoryWildcard :: forall {r}. [Char] -> ([Char] -> r) -> ((# #) -> r) -> r
$bDirectoryWildcard :: [Char] -> [Char]
DirectoryWildcard rest <- ('*':'*':'/':rest) where
  DirectoryWildcard [Char]
rest = Char
'*'Char -> [Char] -> [Char]
forall a. a -> [a] -> [a]
:Char
'*'Char -> [Char] -> [Char]
forall a. a -> [a] -> [a]
:Char
'/'Char -> [Char] -> [Char]
forall a. a -> [a] -> [a]
:[Char]
rest

pattern MultiLevelWildcard :: String -> String
pattern $mMultiLevelWildcard :: forall {r}. [Char] -> ([Char] -> r) -> ((# #) -> r) -> r
$bMultiLevelWildcard :: [Char] -> [Char]
MultiLevelWildcard rest <- ('*':'*':rest) where
  MultiLevelWildcard [Char]
rest = Char
'*'Char -> [Char] -> [Char]
forall a. a -> [a] -> [a]
:Char
'*'Char -> [Char] -> [Char]
forall a. a -> [a] -> [a]
:[Char]
rest

pattern SingleLevelWildcard :: String -> String
pattern $mSingleLevelWildcard :: forall {r}. [Char] -> ([Char] -> r) -> ((# #) -> r) -> r
$bSingleLevelWildcard :: [Char] -> [Char]
SingleLevelWildcard rest <- ('*':rest) where
  SingleLevelWildcard [Char]
rest = Char
'*'Char -> [Char] -> [Char]
forall a. a -> [a] -> [a]
:[Char]
rest

-- Leading slash pattern for paths
pattern CharClassStart :: String -> String
pattern $mCharClassStart :: forall {r}. [Char] -> ([Char] -> r) -> ((# #) -> r) -> r
$bCharClassStart :: [Char] -> [Char]
CharClassStart rest <- ('[':rest) where
  CharClassStart [Char]
rest = Char
'['Char -> [Char] -> [Char]
forall a. a -> [a] -> [a]
:[Char]
rest

-- | Types of ignore patterns
data PatternType 
  = Inclusion  -- ^ Normal inclusion pattern (e.g., "*.js")
  | Negation   -- ^ Negation pattern to re-include files (e.g., "!important.js")
  deriving (Int -> PatternType -> [Char] -> [Char]
[PatternType] -> [Char] -> [Char]
PatternType -> [Char]
(Int -> PatternType -> [Char] -> [Char])
-> (PatternType -> [Char])
-> ([PatternType] -> [Char] -> [Char])
-> Show PatternType
forall a.
(Int -> a -> [Char] -> [Char])
-> (a -> [Char]) -> ([a] -> [Char] -> [Char]) -> Show a
$cshowsPrec :: Int -> PatternType -> [Char] -> [Char]
showsPrec :: Int -> PatternType -> [Char] -> [Char]
$cshow :: PatternType -> [Char]
show :: PatternType -> [Char]
$cshowList :: [PatternType] -> [Char] -> [Char]
showList :: [PatternType] -> [Char] -> [Char]
Show, PatternType -> PatternType -> Bool
(PatternType -> PatternType -> Bool)
-> (PatternType -> PatternType -> Bool) -> Eq PatternType
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: PatternType -> PatternType -> Bool
== :: PatternType -> PatternType -> Bool
$c/= :: PatternType -> PatternType -> Bool
/= :: PatternType -> PatternType -> Bool
Eq)

-- | Data type to parse ignore patterns from Dhall
data ClodIgnorePatterns = ClodIgnorePatterns
  { ClodIgnorePatterns -> [Text]
textPatterns :: [Text]  -- ^ List of patterns to ignore
  } deriving (Int -> ClodIgnorePatterns -> [Char] -> [Char]
[ClodIgnorePatterns] -> [Char] -> [Char]
ClodIgnorePatterns -> [Char]
(Int -> ClodIgnorePatterns -> [Char] -> [Char])
-> (ClodIgnorePatterns -> [Char])
-> ([ClodIgnorePatterns] -> [Char] -> [Char])
-> Show ClodIgnorePatterns
forall a.
(Int -> a -> [Char] -> [Char])
-> (a -> [Char]) -> ([a] -> [Char] -> [Char]) -> Show a
$cshowsPrec :: Int -> ClodIgnorePatterns -> [Char] -> [Char]
showsPrec :: Int -> ClodIgnorePatterns -> [Char] -> [Char]
$cshow :: ClodIgnorePatterns -> [Char]
show :: ClodIgnorePatterns -> [Char]
$cshowList :: [ClodIgnorePatterns] -> [Char] -> [Char]
showList :: [ClodIgnorePatterns] -> [Char] -> [Char]
Show, ClodIgnorePatterns -> ClodIgnorePatterns -> Bool
(ClodIgnorePatterns -> ClodIgnorePatterns -> Bool)
-> (ClodIgnorePatterns -> ClodIgnorePatterns -> Bool)
-> Eq ClodIgnorePatterns
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: ClodIgnorePatterns -> ClodIgnorePatterns -> Bool
== :: ClodIgnorePatterns -> ClodIgnorePatterns -> Bool
$c/= :: ClodIgnorePatterns -> ClodIgnorePatterns -> Bool
/= :: ClodIgnorePatterns -> ClodIgnorePatterns -> Bool
Eq, (forall x. ClodIgnorePatterns -> Rep ClodIgnorePatterns x)
-> (forall x. Rep ClodIgnorePatterns x -> ClodIgnorePatterns)
-> Generic ClodIgnorePatterns
forall x. Rep ClodIgnorePatterns x -> ClodIgnorePatterns
forall x. ClodIgnorePatterns -> Rep ClodIgnorePatterns x
forall a.
(forall x. a -> Rep a x) -> (forall x. Rep a x -> a) -> Generic a
$cfrom :: forall x. ClodIgnorePatterns -> Rep ClodIgnorePatterns x
from :: forall x. ClodIgnorePatterns -> Rep ClodIgnorePatterns x
$cto :: forall x. Rep ClodIgnorePatterns x -> ClodIgnorePatterns
to :: forall x. Rep ClodIgnorePatterns x -> ClodIgnorePatterns
Generic)

instance Dhall.FromDhall ClodIgnorePatterns where
  autoWith :: InputNormalizer -> Decoder ClodIgnorePatterns
autoWith InputNormalizer
_ = RecordDecoder ClodIgnorePatterns -> Decoder ClodIgnorePatterns
forall a. RecordDecoder a -> Decoder a
Dhall.record (RecordDecoder ClodIgnorePatterns -> Decoder ClodIgnorePatterns)
-> RecordDecoder ClodIgnorePatterns -> Decoder ClodIgnorePatterns
forall a b. (a -> b) -> a -> b
$ [Text] -> ClodIgnorePatterns
ClodIgnorePatterns ([Text] -> ClodIgnorePatterns)
-> RecordDecoder [Text] -> RecordDecoder ClodIgnorePatterns
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Text -> Decoder [Text] -> RecordDecoder [Text]
forall a. Text -> Decoder a -> RecordDecoder a
Dhall.field Text
"textPatterns" Decoder [Text]
forall a. FromDhall a => Decoder a
Dhall.auto

-- | Default clodignore pattern content embedded at compile time (Dhall format)
defaultClodIgnoreContent :: Text
defaultClodIgnoreContent :: Text
defaultClodIgnoreContent = [Char] -> Text
T.pack ([Char] -> Text) -> [Char] -> Text
forall a b. (a -> b) -> a -> b
$ ByteString -> [Char]
BS.unpack $(embedStringFile "resources/default_clodignore.dhall")

-- | Default clodignore pattern content as a string (for testing)
defaultClodIgnoreContentStr :: String
defaultClodIgnoreContentStr :: [Char]
defaultClodIgnoreContentStr = ByteString -> [Char]
BS.unpack $(embedStringFile "resources/default_clodignore.dhall")

-- | Map used to cache compiled pattern matchers for performance
type PatternCache = Map.Map String (FilePath -> Bool)

-- | Categorize patterns by type (inclusion or negation)
categorizePatterns :: [IgnorePattern] -> ([IgnorePattern], [IgnorePattern])
categorizePatterns :: [IgnorePattern] -> ([IgnorePattern], [IgnorePattern])
categorizePatterns = (IgnorePattern -> Bool)
-> [IgnorePattern] -> ([IgnorePattern], [IgnorePattern])
forall a. (a -> Bool) -> [a] -> ([a], [a])
L.partition IgnorePattern -> Bool
isInclusion
  where 
    isInclusion :: IgnorePattern -> Bool
isInclusion (IgnorePattern [Char]
p) = Bool -> Bool
not ([Char]
"!" [Char] -> [Char] -> Bool
forall a. Eq a => [a] -> [a] -> Bool
`L.isPrefixOf` [Char]
p)

-- | Create a default .clodignore file using the embedded template
--
-- This function creates a new .clodignore file in the specified directory
-- using the default patterns embedded in the executable at compile time.
--
-- @
-- createDefaultClodIgnore "/path/to/repo" ".clodignore"
-- @
createDefaultClodIgnore :: FilePath -> String -> ClodM ()
createDefaultClodIgnore :: [Char] -> [Char] -> ClodM ()
createDefaultClodIgnore [Char]
projectPath [Char]
ignoreFileName = do
  let ignorePath :: [Char]
ignorePath = [Char]
projectPath [Char] -> [Char] -> [Char]
</> [Char]
ignoreFileName
  
  -- Parse the Dhall content to extract patterns
  result <- IO (Either SomeException ClodIgnorePatterns)
-> ReaderT
     ClodConfig
     (ExceptT ClodError IO)
     (Either SomeException ClodIgnorePatterns)
forall a. IO a -> ReaderT ClodConfig (ExceptT ClodError IO) a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (IO (Either SomeException ClodIgnorePatterns)
 -> ReaderT
      ClodConfig
      (ExceptT ClodError IO)
      (Either SomeException ClodIgnorePatterns))
-> IO (Either SomeException ClodIgnorePatterns)
-> ReaderT
     ClodConfig
     (ExceptT ClodError IO)
     (Either SomeException ClodIgnorePatterns)
forall a b. (a -> b) -> a -> b
$ IO ClodIgnorePatterns
-> IO (Either SomeException ClodIgnorePatterns)
forall e a. Exception e => IO a -> IO (Either e a)
try (IO ClodIgnorePatterns
 -> IO (Either SomeException ClodIgnorePatterns))
-> IO ClodIgnorePatterns
-> IO (Either SomeException ClodIgnorePatterns)
forall a b. (a -> b) -> a -> b
$ Decoder ClodIgnorePatterns -> Text -> IO ClodIgnorePatterns
forall a. Decoder a -> Text -> IO a
Dhall.input Decoder ClodIgnorePatterns
forall a. FromDhall a => Decoder a
Dhall.auto Text
defaultClodIgnoreContent
  case result of
    Left (SomeException
e :: SomeException) -> 
      ClodError -> ClodM ()
forall a. ClodError -> ReaderT ClodConfig (ExceptT ClodError IO) a
forall e (m :: * -> *) a. MonadError e m => e -> m a
throwError (ClodError -> ClodM ()) -> ClodError -> ClodM ()
forall a b. (a -> b) -> a -> b
$ [Char] -> ClodError
ConfigError ([Char] -> ClodError) -> [Char] -> ClodError
forall a b. (a -> b) -> a -> b
$ [Char]
"Failed to parse default clodignore patterns: " [Char] -> [Char] -> [Char]
forall a. [a] -> [a] -> [a]
++ SomeException -> [Char]
forall a. Show a => a -> [Char]
show SomeException
e
    Right (ClodIgnorePatterns [Text]
patterns) -> do
      -- Convert to plain text format with comments
      let fileContent :: [Char]
fileContent = [Char]
"# Default .clodignore file for Claude uploader\n# Add patterns to ignore files when uploading to Claude\n\n" [Char] -> [Char] -> [Char]
forall a. [a] -> [a] -> [a]
++
                       [[Char]] -> [Char]
unlines ((Text -> [Char]) -> [Text] -> [[Char]]
forall a b. (a -> b) -> [a] -> [b]
map Text -> [Char]
T.unpack [Text]
patterns)
      
      -- Write the file
      IO () -> ClodM ()
forall a. IO a -> ReaderT ClodConfig (ExceptT ClodError IO) a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (IO () -> ClodM ()) -> IO () -> ClodM ()
forall a b. (a -> b) -> a -> b
$ [Char] -> [Char] -> IO ()
writeFile [Char]
ignorePath [Char]
fileContent

-- | Read and parse .clodignore file
-- 
-- This function reads patterns from a .clodignore file in the specified directory.
-- If the file doesn't exist, a default one is created using the template in resources/default_clodignore.dhall
-- which is a proper Dhall configuration file that is parsed and converted to a plain text .clodignore file.
-- Comments (lines starting with '#') and empty lines are ignored.
--
-- Uses the CLODIGNORE environment variable or defaults to ".clodignore".
--
-- @
-- patterns <- readClodIgnore "/path/to/repo"
-- @
readClodIgnore :: FilePath -> ClodM [IgnorePattern]
readClodIgnore :: [Char] -> ClodM [IgnorePattern]
readClodIgnore [Char]
projectPath = do
  ignoreFileName <- IO [Char] -> ReaderT ClodConfig (ExceptT ClodError IO) [Char]
forall a. IO a -> ReaderT ClodConfig (ExceptT ClodError IO) a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO IO [Char]
clodIgnoreFile
  let ignorePath = [Char]
projectPath [Char] -> [Char] -> [Char]
</> [Char]
ignoreFileName
  exists <- liftIO $ doesFileExist ignorePath
  if exists
    then do
      content <- liftIO $ readFile ignorePath
      return $ map IgnorePattern $ filter isValidPattern $ lines content
    else do
      -- Create a default .clodignore file if one doesn't exist
      createDefaultClodIgnore projectPath ignoreFileName
      -- Now read the newly created file
      content <- liftIO $ readFile ignorePath
      return $ map IgnorePattern $ filter isValidPattern $ lines content
  where
    isValidPattern :: [Char] -> Bool
isValidPattern [Char]
line = Bool -> Bool
not ([Char] -> Bool
forall a. [a] -> Bool
forall (t :: * -> *) a. Foldable t => t a -> Bool
null [Char]
line) Bool -> Bool -> Bool
&& Bool -> Bool
not ([Char]
"#" [Char] -> [Char] -> Bool
forall a. Eq a => [a] -> [a] -> Bool
`L.isPrefixOf` [Char]
line)

-- | Read and parse .gitignore file
--
-- This function reads patterns from a .gitignore file in the specified directory.
-- If the file doesn't exist, an empty list is returned.
-- Comments (lines starting with '#') and empty lines are ignored.
-- Negation patterns (lines starting with '!') are properly processed.
--
-- @
-- patterns <- readGitIgnore "/path/to/repo"
-- @
readGitIgnore :: FilePath -> ClodM [IgnorePattern]
readGitIgnore :: [Char] -> ClodM [IgnorePattern]
readGitIgnore [Char]
projectPath = do
  let gitIgnorePath :: [Char]
gitIgnorePath = [Char]
projectPath [Char] -> [Char] -> [Char]
</> [Char]
".gitignore"
  exists <- IO Bool -> ReaderT ClodConfig (ExceptT ClodError IO) Bool
forall a. IO a -> ReaderT ClodConfig (ExceptT ClodError IO) a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (IO Bool -> ReaderT ClodConfig (ExceptT ClodError IO) Bool)
-> IO Bool -> ReaderT ClodConfig (ExceptT ClodError IO) Bool
forall a b. (a -> b) -> a -> b
$ [Char] -> IO Bool
doesFileExist [Char]
gitIgnorePath
  if exists
    then do
      content <- liftIO $ readFile gitIgnorePath
      let lines' = [Char] -> [[Char]]
lines [Char]
content
      -- Process each line to handle standard git patterns
      let validPatterns = ([Char] -> Bool) -> [[Char]] -> [[Char]]
forall a. (a -> Bool) -> [a] -> [a]
filter [Char] -> Bool
isValidPattern [[Char]]
lines'
      return $ map IgnorePattern validPatterns
    else return []
  where
    isValidPattern :: [Char] -> Bool
isValidPattern [Char]
line = Bool -> Bool
not ([Char] -> Bool
forall a. [a] -> Bool
forall (t :: * -> *) a. Foldable t => t a -> Bool
null [Char]
line) Bool -> Bool -> Bool
&& Bool -> Bool
not ([Char]
"#" [Char] -> [Char] -> Bool
forall a. Eq a => [a] -> [a] -> Bool
`L.isPrefixOf` [Char]
line)

-- | Find and read all .gitignore files in a directory tree
--
-- This function recursively searches for all .gitignore files in a directory
-- and its subdirectories, and combines their patterns. Patterns in deeper
-- directories take precedence over ones in higher directories.
--
-- This matches Git's behavior where each directory can have its own .gitignore
-- that applies to files within it.
--
-- @
-- patterns <- readNestedGitIgnores "/path/to/repo"
-- @
readNestedGitIgnores :: FilePath -> ClodM [IgnorePattern]
readNestedGitIgnores :: [Char] -> ClodM [IgnorePattern]
readNestedGitIgnores [Char]
rootPath = do
  -- Find all .gitignore files
  ignoreFiles <- [Char] -> ClodM [[Char]]
findGitIgnoreFiles [Char]
rootPath
  -- Read patterns from each file, maintaining order with deeper files later
  -- (later patterns take precedence in git)
  patternLists <- mapM (\[Char]
file -> do
    dir <- IO [Char] -> ReaderT ClodConfig (ExceptT ClodError IO) [Char]
forall a. IO a -> ReaderT ClodConfig (ExceptT ClodError IO) a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (IO [Char] -> ReaderT ClodConfig (ExceptT ClodError IO) [Char])
-> IO [Char] -> ReaderT ClodConfig (ExceptT ClodError IO) [Char]
forall a b. (a -> b) -> a -> b
$ [Char] -> [Char]
takeDirectory ([Char] -> [Char]) -> IO [Char] -> IO [Char]
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> [Char] -> IO [Char]
forall a. a -> IO a
forall (m :: * -> *) a. Monad m => a -> m a
return [Char]
file
    patterns <- readGitIgnoreFile file
    return (dir, patterns)
    ) ignoreFiles
  
  -- Process patterns to make paths relative to their containing directory
  let processedPatterns = (([Char], [IgnorePattern]) -> [IgnorePattern])
-> [([Char], [IgnorePattern])] -> [IgnorePattern]
forall (t :: * -> *) a b. Foldable t => (a -> [b]) -> t a -> [b]
concatMap (\([Char]
dir, [IgnorePattern]
patterns) -> 
        (IgnorePattern -> IgnorePattern)
-> [IgnorePattern] -> [IgnorePattern]
forall a b. (a -> b) -> [a] -> [b]
map ([Char] -> IgnorePattern -> IgnorePattern
makeRelativeToDir [Char]
dir) [IgnorePattern]
patterns) [([Char], [IgnorePattern])]
patternLists
  
  -- Return the combined patterns
  return processedPatterns
  where
    -- Find all .gitignore files recursively
    findGitIgnoreFiles :: FilePath -> ClodM [FilePath]
    findGitIgnoreFiles :: [Char] -> ClodM [[Char]]
findGitIgnoreFiles [Char]
dir = do
      let gitignorePath :: [Char]
gitignorePath = [Char]
dir [Char] -> [Char] -> [Char]
</> [Char]
".gitignore"
      exists <- IO Bool -> ReaderT ClodConfig (ExceptT ClodError IO) Bool
forall a. IO a -> ReaderT ClodConfig (ExceptT ClodError IO) a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (IO Bool -> ReaderT ClodConfig (ExceptT ClodError IO) Bool)
-> IO Bool -> ReaderT ClodConfig (ExceptT ClodError IO) Bool
forall a b. (a -> b) -> a -> b
$ [Char] -> IO Bool
doesFileExist [Char]
gitignorePath
      let current = if Bool
exists then [[Char]
gitignorePath] else []
      
      -- Get subdirectories
      dirExists <- liftIO $ doesDirectoryExist dir
      subdirs <- if dirExists
        then do
          contents <- liftIO $ getDirectoryContents dir
          let validDirs = ([Char] -> Bool) -> [[Char]] -> [[Char]]
forall a. (a -> Bool) -> [a] -> [a]
filter ([Char] -> [[Char]] -> Bool
forall (t :: * -> *) a. (Foldable t, Eq a) => a -> t a -> Bool
`notElem` [[Char]
".", [Char]
"..", [Char]
".git"]) [[Char]]
contents
          filterM (\[Char]
d -> IO Bool -> ReaderT ClodConfig (ExceptT ClodError IO) Bool
forall a. IO a -> ReaderT ClodConfig (ExceptT ClodError IO) a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (IO Bool -> ReaderT ClodConfig (ExceptT ClodError IO) Bool)
-> IO Bool -> ReaderT ClodConfig (ExceptT ClodError IO) Bool
forall a b. (a -> b) -> a -> b
$ [Char] -> IO Bool
doesDirectoryExist ([Char]
dir [Char] -> [Char] -> [Char]
</> [Char]
d)) validDirs
        else return []
      
      -- Recursively process subdirectories
      subResults <- mapM (\[Char]
subdir -> [Char] -> ClodM [[Char]]
findGitIgnoreFiles ([Char]
dir [Char] -> [Char] -> [Char]
</> [Char]
subdir)) subdirs
      
      -- Combine results, ordering by depth (deeper files later for precedence)
      return $ current ++ concat subResults
    
    -- Read patterns from a .gitignore file
    readGitIgnoreFile :: FilePath -> ClodM [IgnorePattern]
    readGitIgnoreFile :: [Char] -> ClodM [IgnorePattern]
readGitIgnoreFile [Char]
path = do
      content <- IO [Char] -> ReaderT ClodConfig (ExceptT ClodError IO) [Char]
forall a. IO a -> ReaderT ClodConfig (ExceptT ClodError IO) a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (IO [Char] -> ReaderT ClodConfig (ExceptT ClodError IO) [Char])
-> IO [Char] -> ReaderT ClodConfig (ExceptT ClodError IO) [Char]
forall a b. (a -> b) -> a -> b
$ [Char] -> IO [Char]
readFile [Char]
path
      return $ map IgnorePattern $ filter isValidPattern $ lines content
      where
        isValidPattern :: [Char] -> Bool
isValidPattern [Char]
line = Bool -> Bool
not ([Char] -> Bool
forall a. [a] -> Bool
forall (t :: * -> *) a. Foldable t => t a -> Bool
null [Char]
line) Bool -> Bool -> Bool
&& Bool -> Bool
not ([Char]
"#" [Char] -> [Char] -> Bool
forall a. Eq a => [a] -> [a] -> Bool
`L.isPrefixOf` [Char]
line)
    
    -- Make a pattern relative to its containing directory
    makeRelativeToDir :: FilePath -> IgnorePattern -> IgnorePattern
    makeRelativeToDir :: [Char] -> IgnorePattern -> IgnorePattern
makeRelativeToDir [Char]
dir (IgnorePattern [Char]
p) =
      let isNegation :: Bool
isNegation = [Char]
"!" [Char] -> [Char] -> Bool
forall a. Eq a => [a] -> [a] -> Bool
`L.isPrefixOf` [Char]
p
          actualPattern :: [Char]
actualPattern = if Bool
isNegation then Int -> [Char] -> [Char]
forall a. Int -> [a] -> [a]
drop Int
1 [Char]
p else [Char]
p
          isAbsolute :: Bool
isAbsolute = [Char]
"/" [Char] -> [Char] -> Bool
forall a. Eq a => [a] -> [a] -> Bool
`L.isPrefixOf` [Char]
actualPattern
          adjusted :: [Char]
adjusted = if Bool
isAbsolute 
                     then [Char]
actualPattern  -- Already absolute
                     else if [Char]
dir [Char] -> [Char] -> Bool
forall a. Eq a => a -> a -> Bool
== [Char]
rootPath
                          then [Char]
actualPattern  -- In root dir, keep as-is
                          else let relDir :: [Char]
relDir = Int -> [Char] -> [Char]
forall a. Int -> [a] -> [a]
drop ([Char] -> Int
forall a. [a] -> Int
forall (t :: * -> *) a. Foldable t => t a -> Int
length [Char]
rootPath Int -> Int -> Int
forall a. Num a => a -> a -> a
+ Int
1) [Char]
dir
                               in [Char]
relDir [Char] -> [Char] -> [Char]
</> [Char]
actualPattern
          final :: [Char]
final = if Bool
isNegation then [Char]
"!" [Char] -> [Char] -> [Char]
forall a. [a] -> [a] -> [a]
++ [Char]
adjusted else [Char]
adjusted
      in [Char] -> IgnorePattern
IgnorePattern [Char]
final

-- | Check if a file matches any ignore pattern, respecting negations
--
-- This function checks if a given file path matches any of the provided ignore patterns,
-- while properly handling negation patterns. Patterns are processed in order, with later
-- patterns taking precedence over earlier ones.
--
-- A file is ignored if it matches any inclusion pattern and doesn't match any
-- subsequent negation pattern.
--
-- === Examples
--
-- @
-- -- Check if a file should be ignored
-- matchesIgnorePattern [IgnorePattern "*.js", IgnorePattern "!important.js"] "app.js"  -- Returns True (matches *.js)
-- matchesIgnorePattern [IgnorePattern "*.js", IgnorePattern "!important.js"] "important.js"  -- Returns False (negated)
-- matchesIgnorePattern [IgnorePattern "src/*.svg"] "src/logo.svg"  -- Returns True
-- matchesIgnorePattern [IgnorePattern "node_modules"] "src/node_modules/file.js"  -- Returns True
-- @
matchesIgnorePattern :: [IgnorePattern] -> FilePath -> Bool
matchesIgnorePattern :: [IgnorePattern] -> [Char] -> Bool
matchesIgnorePattern [IgnorePattern]
patterns [Char]
filePath = 
  -- Process patterns in reverse order (later patterns take precedence)
  let ([IgnorePattern]
inclusions, [IgnorePattern]
negations) = [IgnorePattern] -> ([IgnorePattern], [IgnorePattern])
categorizePatterns [IgnorePattern]
patterns
      
      -- Process negation patterns - extract the pattern without '!'
      negationPatterns :: [IgnorePattern]
negationPatterns = (IgnorePattern -> IgnorePattern)
-> [IgnorePattern] -> [IgnorePattern]
forall a b. (a -> b) -> [a] -> [b]
map (\(IgnorePattern [Char]
p) -> 
                            [Char] -> IgnorePattern
IgnorePattern (Int -> [Char] -> [Char]
forall a. Int -> [a] -> [a]
drop Int
1 [Char]
p)) [IgnorePattern]
negations
      
      -- Check if any inclusion pattern matches
      includedByPattern :: Bool
includedByPattern = (IgnorePattern -> Bool) -> [IgnorePattern] -> Bool
forall (t :: * -> *) a. Foldable t => (a -> Bool) -> t a -> Bool
any ([Char] -> IgnorePattern -> Bool
matchesPattern [Char]
filePath) [IgnorePattern]
inclusions
      
      -- Check if any negation pattern matches
      negatedByPattern :: Bool
negatedByPattern = (IgnorePattern -> Bool) -> [IgnorePattern] -> Bool
forall (t :: * -> *) a. Foldable t => (a -> Bool) -> t a -> Bool
any ([Char] -> IgnorePattern -> Bool
matchesPattern [Char]
filePath) [IgnorePattern]
negationPatterns
  in
    -- Included by some pattern and not negated by any later pattern
    Bool
includedByPattern Bool -> Bool -> Bool
&& Bool -> Bool
not Bool
negatedByPattern
  where
    -- Use cached pattern matchers for better performance
    cache :: PatternCache
cache = PatternCache
forall k a. Map k a
Map.empty :: PatternCache
    
    -- Match a single pattern against a path
    matchesPattern :: FilePath -> IgnorePattern -> Bool
    matchesPattern :: [Char] -> IgnorePattern -> Bool
matchesPattern [Char]
path (IgnorePattern [Char]
p) =
      let (PatternCache
_, [Char] -> Bool
matcher) = PatternCache -> [Char] -> (PatternCache, [Char] -> Bool)
getCachedMatcher PatternCache
cache [Char]
p
      in [Char] -> Bool
matcher [Char]
path

-- | Get or create a cached pattern matcher
getCachedMatcher :: PatternCache -> String -> (PatternCache, FilePath -> Bool)
getCachedMatcher :: PatternCache -> [Char] -> (PatternCache, [Char] -> Bool)
getCachedMatcher PatternCache
cache [Char]
ptn = 
  case [Char] -> PatternCache -> Maybe ([Char] -> Bool)
forall k a. Ord k => k -> Map k a -> Maybe a
Map.lookup [Char]
ptn PatternCache
cache of
    Just [Char] -> Bool
matcher -> (PatternCache
cache, [Char] -> Bool
matcher)
    Maybe ([Char] -> Bool)
Nothing -> 
      let matcher :: [Char] -> Bool
matcher = [Char] -> [Char] -> Bool
makePatternMatcher [Char]
ptn
          newCache :: PatternCache
newCache = [Char] -> ([Char] -> Bool) -> PatternCache -> PatternCache
forall k a. Ord k => k -> a -> Map k a -> Map k a
Map.insert [Char]
ptn [Char] -> Bool
matcher PatternCache
cache
      in (PatternCache
newCache, [Char] -> Bool
matcher)

-- | Convert a pattern string into a function that matches paths against that pattern
makePatternMatcher :: String -> (FilePath -> Bool)
makePatternMatcher :: [Char] -> [Char] -> Bool
makePatternMatcher [Char]
ptn
  -- Skip empty patterns
  | [Char] -> Bool
forall a. [a] -> Bool
forall (t :: * -> *) a. Foldable t => t a -> Bool
null [Char]
ptn = Bool -> [Char] -> Bool
forall a b. a -> b -> a
const Bool
False
  
  -- Normalize the pattern to remove trailing slashes for consistency
  | [Char]
"/" [Char] -> [Char] -> Bool
forall a. Eq a => [a] -> [a] -> Bool
`L.isSuffixOf` [Char]
ptn = [Char] -> [Char] -> Bool
makePatternMatcher ([Char] -> [Char] -> Bool) -> [Char] -> [Char] -> Bool
forall a b. (a -> b) -> a -> b
$ [Char] -> [Char]
forall a. HasCallStack => [a] -> [a]
init [Char]
ptn
  
  -- Handle leading slash (anchored to root)
  | [Char]
"/" [Char] -> [Char] -> Bool
forall a. Eq a => [a] -> [a] -> Bool
`L.isPrefixOf` [Char]
ptn =
      let patternWithoutSlash :: [Char]
patternWithoutSlash = Int -> [Char] -> [Char]
forall a. Int -> [a] -> [a]
drop Int
1 [Char]
ptn
      in [Char] -> [Char] -> Bool
matchFromRoot [Char]
patternWithoutSlash
      
  -- File extension pattern: *.ext
  | [Char]
"*." [Char] -> [Char] -> Bool
forall a. Eq a => [a] -> [a] -> Bool
`L.isPrefixOf` [Char]
ptn = [Char] -> [Char] -> Bool
matchExtension [Char]
ptn
      
  -- Directory pattern inside path (contains slash)
  | Char
'/' Char -> [Char] -> Bool
forall a. Eq a => a -> [a] -> Bool
forall (t :: * -> *) a. (Foldable t, Eq a) => a -> t a -> Bool
`elem` [Char]
ptn = 
      if Char
'*' Char -> [Char] -> Bool
forall a. Eq a => a -> [a] -> Bool
forall (t :: * -> *) a. (Foldable t, Eq a) => a -> t a -> Bool
`elem` [Char]
ptn Bool -> Bool -> Bool
|| Char
'?' Char -> [Char] -> Bool
forall a. Eq a => a -> [a] -> Bool
forall (t :: * -> *) a. (Foldable t, Eq a) => a -> t a -> Bool
`elem` [Char]
ptn Bool -> Bool -> Bool
|| [Char] -> Bool
containsCharClass [Char]
ptn
        -- For wildcard patterns, use glob matching
        then [Char] -> [Char] -> Bool
simpleGlobMatch [Char]
ptn
        -- For non-wildcard paths, use component matching
        else [Char] -> [Char] -> Bool
matchPathComponents [Char]
ptn
      
  -- Pattern with character class like [a-z]file.txt
  | [Char] -> Bool
containsCharClass [Char]
ptn = 
      [Char] -> [Char] -> Bool
simpleGlobMatch [Char]
ptn
      
  -- Simple filename or pattern with no slashes
  | Bool
otherwise = [Char] -> [Char] -> Bool
matchSimpleName [Char]
ptn

-- | Check if a pattern contains a character class ([...])
containsCharClass :: String -> Bool
containsCharClass :: [Char] -> Bool
containsCharClass [] = Bool
False
containsCharClass (Char
'[':[Char]
_) = Bool
True
containsCharClass (Char
_:[Char]
rest) = [Char] -> Bool
containsCharClass [Char]
rest

-- | Match file extension patterns like "*.js"
matchExtension :: String -> (FilePath -> Bool)
matchExtension :: [Char] -> [Char] -> Bool
matchExtension [Char]
ptn = \[Char]
path ->
  let ext :: [Char]
ext = Int -> [Char] -> [Char]
forall a. Int -> [a] -> [a]
drop Int
2 [Char]
ptn  -- Skip "*."
      dirPattern :: [Char]
dirPattern = [Char] -> [Char]
takeDirectory [Char]
ptn
      dirParts :: [[Char]]
dirParts = if [Char]
dirPattern [Char] -> [Char] -> Bool
forall a. Eq a => a -> a -> Bool
/= [Char]
"." then [Char] -> [[Char]]
splitDirectories [Char]
dirPattern else []
      fileExt :: [Char]
fileExt = [Char] -> [Char]
takeExtension [Char]
path
      -- Get extension without the dot, safely
      extWithoutDot :: [Char]
extWithoutDot = if [Char] -> Bool
forall a. [a] -> Bool
forall (t :: * -> *) a. Foldable t => t a -> Bool
null [Char]
fileExt then [Char]
"" else Int -> [Char] -> [Char]
forall a. Int -> [a] -> [a]
drop Int
1 [Char]
fileExt
      -- For patterns like src/*.svg we need to check directory prefixes
      pathComponents :: [[Char]]
pathComponents = [Char] -> [[Char]]
splitDirectories [Char]
path
      -- Directory check based on prefix matching
      dirCheck :: Bool
dirCheck = [[Char]] -> Bool
forall a. [a] -> Bool
forall (t :: * -> *) a. Foldable t => t a -> Bool
null [[Char]]
dirParts Bool -> Bool -> Bool
|| [[Char]] -> [[Char]] -> Bool
forall a. Eq a => [a] -> [a] -> Bool
L.isPrefixOf [[Char]]
dirParts [[Char]]
pathComponents
      -- File extension check should be case-insensitive and exact match
      extensionCheck :: Bool
extensionCheck = (Char -> Char) -> [Char] -> [Char]
forall a b. (a -> b) -> [a] -> [b]
map Char -> Char
toLower [Char]
extWithoutDot [Char] -> [Char] -> Bool
forall a. Eq a => a -> a -> Bool
== (Char -> Char) -> [Char] -> [Char]
forall a b. (a -> b) -> [a] -> [b]
map Char -> Char
toLower [Char]
ext
  in Bool
dirCheck Bool -> Bool -> Bool
&& Bool
extensionCheck

-- | Match path component patterns like "src/components"
matchPathComponents :: String -> (FilePath -> Bool)
matchPathComponents :: [Char] -> [Char] -> Bool
matchPathComponents [Char]
ptn = \[Char]
path ->
  let patternComponents :: [[Char]]
patternComponents = [Char] -> [[Char]]
splitDirectories [Char]
ptn
      pathComponents :: [[Char]]
pathComponents = [Char] -> [[Char]]
splitDirectories [Char]
path
      -- Check for direct prefix match
      directMatch :: Bool
directMatch = [Char]
ptn [Char] -> [Char] -> Bool
forall a. Eq a => [a] -> [a] -> Bool
`L.isPrefixOf` [Char]
path Bool -> Bool -> Bool
|| 
                   ([Char]
"/" [Char] -> [Char] -> [Char]
forall a. [a] -> [a] -> [a]
++ [Char]
ptn) [Char] -> [Char] -> Bool
forall a. Eq a => [a] -> [a] -> Bool
`L.isPrefixOf` ([Char]
"/" [Char] -> [Char] -> [Char]
forall a. [a] -> [a] -> [a]
++ [Char]
path)
      -- Check for match at any level in the path
      multiComponentMatch :: Bool
multiComponentMatch = ([[Char]] -> Bool) -> [[[Char]]] -> Bool
forall (t :: * -> *) a. Foldable t => (a -> Bool) -> t a -> Bool
any ([[Char]] -> [[Char]] -> Bool
forall a. Eq a => [a] -> [a] -> Bool
L.isPrefixOf [[Char]]
patternComponents) ([[Char]] -> [[[Char]]]
forall a. [a] -> [[a]]
tails [[Char]]
pathComponents)
  in Bool
directMatch Bool -> Bool -> Bool
|| Bool
multiComponentMatch

-- | Match simple name patterns like "README.md" or "node_modules"
matchSimpleName :: String -> (FilePath -> Bool)
matchSimpleName :: [Char] -> [Char] -> Bool
matchSimpleName [Char]
ptn = \[Char]
path ->
  let fileName :: [Char]
fileName = [Char] -> [Char]
takeFileName [Char]
path
      pathComponents :: [[Char]]
pathComponents = [Char] -> [[Char]]
splitDirectories [Char]
path
      -- Check for exact filename match
      exactMatch :: Bool
exactMatch = [Char]
ptn [Char] -> [Char] -> Bool
forall a. Eq a => a -> a -> Bool
== [Char]
fileName
      -- Check for directory name match anywhere in path
      dirMatch :: Bool
dirMatch = [Char]
ptn [Char] -> [[Char]] -> Bool
forall a. Eq a => a -> [a] -> Bool
forall (t :: * -> *) a. (Foldable t, Eq a) => a -> t a -> Bool
`elem` [[Char]]
pathComponents
      -- Special case for trailing wildcards "dir/**"
      hasTrailingWildcard :: Bool
hasTrailingWildcard = [Char]
"/**" [Char] -> [Char] -> Bool
forall a. Eq a => [a] -> [a] -> Bool
`L.isSuffixOf` [Char]
ptn
      folderPattern :: [Char]
folderPattern = if Bool
hasTrailingWildcard
                      then Int -> [Char] -> [Char]
forall a. Int -> [a] -> [a]
take ([Char] -> Int
forall a. [a] -> Int
forall (t :: * -> *) a. Foldable t => t a -> Int
length [Char]
ptn Int -> Int -> Int
forall a. Num a => a -> a -> a
- Int
3) [Char]
ptn
                      else [Char]
ptn
      -- Component matching for wildcards
      folderMatchWithWildcard :: Bool
folderMatchWithWildcard = Bool
hasTrailingWildcard Bool -> Bool -> Bool
&& 
                              ([Char]
folderPattern [Char] -> [[Char]] -> Bool
forall a. Eq a => a -> [a] -> Bool
forall (t :: * -> *) a. (Foldable t, Eq a) => a -> t a -> Bool
`elem` [[Char]]
pathComponents) Bool -> Bool -> Bool
&&
                              Bool -> (Int -> Bool) -> Maybe Int -> Bool
forall b a. b -> (a -> b) -> Maybe a -> b
maybe Bool
False (Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
< [[Char]] -> Int
forall a. [a] -> Int
forall (t :: * -> *) a. Foldable t => t a -> Int
length [[Char]]
pathComponents Int -> Int -> Int
forall a. Num a => a -> a -> a
- Int
1) 
                                ([Char] -> [[Char]] -> Maybe Int
forall a. Eq a => a -> [a] -> Maybe Int
L.elemIndex [Char]
folderPattern [[Char]]
pathComponents)
  in Bool
exactMatch Bool -> Bool -> Bool
|| Bool
dirMatch Bool -> Bool -> Bool
|| Bool
folderMatchWithWildcard

-- | Get all tails of a list
tails :: [a] -> [[a]]
tails :: forall a. [a] -> [[a]]
tails [] = [[]]
tails xs :: [a]
xs@(a
_:[a]
xs') = [a]
xs [a] -> [[a]] -> [[a]]
forall a. a -> [a] -> [a]
: [a] -> [[a]]
forall a. [a] -> [[a]]
tails [a]
xs'

-- | Match a pattern that should start from the root
matchFromRoot :: String -> (FilePath -> Bool)
matchFromRoot :: [Char] -> [Char] -> Bool
matchFromRoot [Char]
ptn = \[Char]
path ->
  let patternComponents :: [[Char]]
patternComponents = [Char] -> [[Char]]
splitDirectories [Char]
ptn
      pathComponents :: [[Char]]
pathComponents = [Char] -> [[Char]]
splitDirectories [Char]
path
      -- Handle wildcards or character classes in the pattern
      containsSpecial :: Bool
containsSpecial = ([Char] -> Bool) -> [[Char]] -> Bool
forall (t :: * -> *) a. Foldable t => (a -> Bool) -> t a -> Bool
any [Char] -> Bool
containsSpecialChars [[Char]]
patternComponents
  in if Bool
containsSpecial
     then [Char] -> [Char] -> Bool
simpleGlobMatch [Char]
ptn [Char]
path
     else [[Char]] -> [[Char]] -> Bool
forall a. Eq a => [a] -> [a] -> Bool
L.isPrefixOf [[Char]]
patternComponents [[Char]]
pathComponents
  where
    containsSpecialChars :: [Char] -> Bool
containsSpecialChars [Char]
s = Char
'*' Char -> [Char] -> Bool
forall a. Eq a => a -> [a] -> Bool
forall (t :: * -> *) a. (Foldable t, Eq a) => a -> t a -> Bool
`elem` [Char]
s Bool -> Bool -> Bool
|| Char
'?' Char -> [Char] -> Bool
forall a. Eq a => a -> [a] -> Bool
forall (t :: * -> *) a. (Foldable t, Eq a) => a -> t a -> Bool
`elem` [Char]
s Bool -> Bool -> Bool
|| [Char] -> Bool
containsCharClass [Char]
s

-- | Simple glob pattern matching for wildcard patterns
--
-- This function implements a simplified glob pattern matching algorithm 
-- that handles the most common wildcard patterns:
--
-- * @*@ - matches any sequence of characters except /
-- * @**@ - matches any sequence of characters including /
-- * @?@ - matches any single character
-- * @[a-z]@ - matches any character in the specified range
-- * @[!a-z]@ - matches any character not in the specified range
-- * @*.ext@ - matches files with the specified extension
-- * @**/pattern@ - matches pattern at any directory level
--
-- The implementation is designed to be compatible with common .gitignore patterns.
--
-- === Examples
--
-- @
-- simpleGlobMatch "*.js" "app.js"  -- Returns True
-- simpleGlobMatch "src/*.js" "src/app.js"  -- Returns True
-- simpleGlobMatch "src/**/*.js" "src/components/Button.js"  -- Returns True
-- simpleGlobMatch "file[0-9].txt" "file5.txt"  -- Returns True
-- simpleGlobMatch "*.txt" "file.md"  -- Returns False
-- @

-- | Helper function to check if a string starts with a character
startsWith :: String -> Char -> Bool
startsWith :: [Char] -> Char -> Bool
startsWith [] Char
_ = Bool
False
startsWith (Char
x:[Char]
_) Char
c = Char
x Char -> Char -> Bool
forall a. Eq a => a -> a -> Bool
== Char
c
simpleGlobMatch :: String -> FilePath -> Bool
simpleGlobMatch :: [Char] -> [Char] -> Bool
simpleGlobMatch [Char]
ptn = \[Char]
filepath -> [Char] -> [Char] -> Bool
matchGlob [Char]
ptn [Char]
filepath
  where
    -- The core matching algorithm
    matchGlob :: String -> String -> Bool
    matchGlob :: [Char] -> [Char] -> Bool
matchGlob [Char]
pat [Char]
path = case ([Char]
pat, [Char]
path) of
      -- Base cases
      ([], []) -> Bool
True
      ([], [Char]
_)  -> Bool
False
      
      -- Pattern with characters left but no path to match
      ((SingleLevelWildcard [Char]
ps), []) -> [Char] -> [Char] -> Bool
matchGlob [Char]
ps []
      ([Char]
_, [])        -> Bool
False
      
      -- File extension special case: *.ext
      ((FileExtension [Char]
ext), [Char]
_) ->
        let fileExt :: [Char]
fileExt = [Char] -> [Char]
takeExtension [Char]
path
        in Bool -> Bool
not ([Char] -> Bool
forall a. [a] -> Bool
forall (t :: * -> *) a. Foldable t => t a -> Bool
null [Char]
fileExt) Bool -> Bool -> Bool
&& (Char -> Char) -> [Char] -> [Char]
forall a b. (a -> b) -> [a] -> [b]
map Char -> Char
toLower (Int -> [Char] -> [Char]
forall a. Int -> [a] -> [a]
drop Int
1 [Char]
fileExt) [Char] -> [Char] -> Bool
forall a. Eq a => a -> a -> Bool
== (Char -> Char) -> [Char] -> [Char]
forall a b. (a -> b) -> [a] -> [b]
map Char -> Char
toLower [Char]
ext
        
      -- Directory wildcard: **/
      ((DirectoryWildcard [Char]
ps), (Char
_:[Char]
_)) ->
        let restPath :: [Char]
restPath = (Char -> Bool) -> [Char] -> [Char]
forall a. (a -> Bool) -> [a] -> [a]
dropWhile (Char -> Char -> Bool
forall a. Eq a => a -> a -> Bool
/= Char
'/') [Char]
path
        in [Char] -> [Char] -> Bool
matchGlob ([Char] -> [Char]
DirectoryWildcard [Char]
ps) (Int -> [Char] -> [Char]
forall a. Int -> [a] -> [a]
drop Int
1 [Char]
restPath) Bool -> Bool -> Bool
|| 
           [Char] -> [Char] -> Bool
matchGlob [Char]
ps [Char]
path Bool -> Bool -> Bool
|| 
           [Char] -> [Char] -> Bool
matchGlob [Char]
ps (Int -> [Char] -> [Char]
forall a. Int -> [a] -> [a]
drop Int
1 [Char]
restPath)
           
      -- Multi-level wildcard: **
      ((MultiLevelWildcard [Char]
ps), (Char
c:[Char]
cs)) ->
        [Char] -> [Char] -> Bool
matchGlob [Char]
ps (Char
cChar -> [Char] -> [Char]
forall a. a -> [a] -> [a]
:[Char]
cs) Bool -> Bool -> Bool
|| [Char] -> [Char] -> Bool
matchGlob ([Char] -> [Char]
MultiLevelWildcard [Char]
ps) [Char]
cs
        
      -- Single-level wildcard: *
      ((SingleLevelWildcard [Char]
ps), (Char
c:[Char]
cs)) ->
        if Char
c Char -> Char -> Bool
forall a. Eq a => a -> a -> Bool
== Char
'/' 
          then [Char] -> [Char] -> Bool
matchGlob ([Char] -> [Char]
SingleLevelWildcard [Char]
ps) [Char]
cs  -- Skip the slash
          else if [Char]
ps [Char] -> [Char] -> Bool
forall a. Eq a => a -> a -> Bool
== [] Bool -> Bool -> Bool
&& (Char
'/' Char -> [Char] -> Bool
forall a. Eq a => a -> [a] -> Bool
forall (t :: * -> *) a. (Foldable t, Eq a) => a -> t a -> Bool
`elem` [Char]
cs)  
              then Bool
False  -- Don't let * cross directory boundaries for patterns like "src/*.js"
              else if [Char]
cs [Char] -> [Char] -> Bool
forall a. Eq a => a -> a -> Bool
== [] Bool -> Bool -> Bool
|| Bool -> Bool
not (Char
'/' Char -> [Char] -> Bool
forall a. Eq a => a -> [a] -> Bool
forall (t :: * -> *) a. (Foldable t, Eq a) => a -> t a -> Bool
`elem` [Char]
cs)
                  then [Char] -> [Char] -> Bool
matchGlob [Char]
ps (Char
cChar -> [Char] -> [Char]
forall a. a -> [a] -> [a]
:[Char]
cs) Bool -> Bool -> Bool
|| [Char] -> [Char] -> Bool
matchGlob ([Char] -> [Char]
SingleLevelWildcard [Char]
ps) [Char]
cs
                  else Bool
False  -- Don't match across directory boundaries for *.js pattern
                  
      -- Single character wildcard: ?
      ((Char
'?':[Char]
ps), (Char
_:[Char]
cs)) ->
        [Char] -> [Char] -> Bool
matchGlob [Char]
ps [Char]
cs
      
      -- Beginning of character class: [
      ((CharClassStart [Char]
cs), (Char
c:[Char]
path')) ->
        let ([Char]
classSpec, [Char]
rest) = (Char -> Bool) -> [Char] -> ([Char], [Char])
forall a. (a -> Bool) -> [a] -> ([a], [a])
span (Char -> Char -> Bool
forall a. Eq a => a -> a -> Bool
/= Char
']') [Char]
cs
            negated :: Bool
negated = Bool -> Bool
not ([Char] -> Bool
forall a. [a] -> Bool
forall (t :: * -> *) a. Foldable t => t a -> Bool
null [Char]
classSpec) Bool -> Bool -> Bool
&& [Char]
classSpec [Char] -> Char -> Bool
`startsWith` Char
'!'
            actualClass :: [Char]
actualClass = if Bool
negated then Int -> [Char] -> [Char]
forall a. Int -> [a] -> [a]
drop Int
1 [Char]
classSpec else [Char]
classSpec
        in if [Char] -> Bool
forall a. [a] -> Bool
forall (t :: * -> *) a. Foldable t => t a -> Bool
null [Char]
rest  -- Malformed pattern, no closing ]
           then Bool
False
           else 
             let matches :: Bool
matches = [Char] -> Char -> Bool
matchCharacterClass [Char]
actualClass Char
c
                 result :: Bool
result = if Bool
negated then Bool -> Bool
not Bool
matches else Bool
matches
             in Bool
result Bool -> Bool -> Bool
&& [Char] -> [Char] -> Bool
matchGlob (Int -> [Char] -> [Char]
forall a. Int -> [a] -> [a]
drop Int
1 [Char]
rest) [Char]
path'
        
      -- Regular character matching
      ((Char
p:[Char]
ps), (Char
c:[Char]
cs)) ->
        Char
p Char -> Char -> Bool
forall a. Eq a => a -> a -> Bool
== Char
c Bool -> Bool -> Bool
&& [Char] -> [Char] -> Bool
matchGlob [Char]
ps [Char]
cs

-- | Character class patterns for `[...]` expressions
data CharClassPattern = SingleChar Char
                      | CharRange Char Char
                      deriving (Int -> CharClassPattern -> [Char] -> [Char]
[CharClassPattern] -> [Char] -> [Char]
CharClassPattern -> [Char]
(Int -> CharClassPattern -> [Char] -> [Char])
-> (CharClassPattern -> [Char])
-> ([CharClassPattern] -> [Char] -> [Char])
-> Show CharClassPattern
forall a.
(Int -> a -> [Char] -> [Char])
-> (a -> [Char]) -> ([a] -> [Char] -> [Char]) -> Show a
$cshowsPrec :: Int -> CharClassPattern -> [Char] -> [Char]
showsPrec :: Int -> CharClassPattern -> [Char] -> [Char]
$cshow :: CharClassPattern -> [Char]
show :: CharClassPattern -> [Char]
$cshowList :: [CharClassPattern] -> [Char] -> [Char]
showList :: [CharClassPattern] -> [Char] -> [Char]
Show, CharClassPattern -> CharClassPattern -> Bool
(CharClassPattern -> CharClassPattern -> Bool)
-> (CharClassPattern -> CharClassPattern -> Bool)
-> Eq CharClassPattern
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: CharClassPattern -> CharClassPattern -> Bool
== :: CharClassPattern -> CharClassPattern -> Bool
$c/= :: CharClassPattern -> CharClassPattern -> Bool
/= :: CharClassPattern -> CharClassPattern -> Bool
Eq)

-- | Parse character class patterns from a string
parseCharClass :: String -> [CharClassPattern]
parseCharClass :: [Char] -> [CharClassPattern]
parseCharClass [] = []
parseCharClass (Char
a:Char
'-':Char
b:[Char]
rest) = Char -> Char -> CharClassPattern
CharRange Char
a Char
b CharClassPattern -> [CharClassPattern] -> [CharClassPattern]
forall a. a -> [a] -> [a]
: [Char] -> [CharClassPattern]
parseCharClass [Char]
rest
parseCharClass (Char
x:[Char]
xs) = Char -> CharClassPattern
SingleChar Char
x CharClassPattern -> [CharClassPattern] -> [CharClassPattern]
forall a. a -> [a] -> [a]
: [Char] -> [CharClassPattern]
parseCharClass [Char]
xs

-- | Match a character against a character class pattern ([a-z], [0-9], etc.)
matchCharacterClass :: String -> Char -> Bool
matchCharacterClass :: [Char] -> Char -> Bool
matchCharacterClass [Char]
spec Char
c = (CharClassPattern -> Bool) -> [CharClassPattern] -> Bool
forall (t :: * -> *) a. Foldable t => (a -> Bool) -> t a -> Bool
any (Char -> CharClassPattern -> Bool
matchesPattern Char
c) ([Char] -> [CharClassPattern]
parseCharClass [Char]
spec)
  where
    matchesPattern :: Char -> CharClassPattern -> Bool
    matchesPattern :: Char -> CharClassPattern -> Bool
matchesPattern Char
ch (SingleChar Char
x) = Char
ch Char -> Char -> Bool
forall a. Eq a => a -> a -> Bool
== Char
x
    matchesPattern Char
ch (CharRange Char
start Char
end) = Char
start Char -> Char -> Bool
forall a. Ord a => a -> a -> Bool
<= Char
ch Bool -> Bool -> Bool
&& Char
ch Char -> Char -> Bool
forall a. Ord a => a -> a -> Bool
<= Char
end