packages feed

cgrep-9.0.0: src/CGrep/Strategy/BoyerMoore.hs

-- Copyright (c) 2013-2025 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.
--

module CGrep.Strategy.BoyerMoore (search) where

import Control.Monad.Trans.Reader (ask)

-- expandMultiline,

import CGrep.Common (ignoreCase, runSearch)
import CGrep.Semantic.ContextFilter (mkContextFilter)
import CGrep.FileType (FileType)
import CGrep.FileTypeMap (FileTypeInfo)
import CGrep.FileTypeMapTH (mkContextFilterFn)
import CGrep.Match (Match, mkMatches)

import CGrep.Parser.Chunk
import Options (Options (..))
import PutMessage (putMessageLnVerb)
import Reader (Env (..), ReaderIO)

import System.IO (stderr)
import System.OsPath (OsPath)

import CGrep.Line (buildIndex, getLineByOffset, getLineOffsets)
import CGrep.Text (textContainsOneOf, textIndices, textSlice)
import Control.Concurrent (MVar)
import qualified Data.Text as T
import qualified Data.Text.Unsafe as TU
import qualified Data.Vector.Unboxed as UV

search :: MVar () -> Maybe (FileType, FileTypeInfo) -> OsPath -> T.Text -> [T.Text] -> Bool -> ReaderIO [Match]
search lock info filename text patterns _strict = do
    Env{..} <- ask
    let lindex = buildIndex text

    -- transform text...
    let !contextFilter = mkContextFilterFn (fst <$> info) (mkContextFilter opt) False

    let text' = ignoreCase opt text
    let text'' = contextFilter $ text'

    -- make shallow search
    let !eligibleForSearch = textContainsOneOf patterns text'

    -- search for matching tokens
    putMessageLnVerb 3 lock stderr $ "---\n" <> text'' <> "\n---"
    putMessageLnVerb 1 lock stderr $ "strategy  : running Boyer-Moore search on " <> show filename

    runSearch lindex filename eligibleForSearch $ do
        let lineOffsets = getLineOffsets text''
        let indices = textIndices patterns text''

        let !chunks =
                concat $
                    zipWith
                        ( \p offsets ->
                            let blen = TU.lengthWord8 p
                             in map (\offset -> Chunk ChunkUnspec (textSlice text'' offset blen)) offsets
                        )
                        patterns
                        indices

        let !chunks' =
                if word_match opt || prefix_match opt || suffix_match opt
                    then filter (filterChunk opt lineOffsets (snd <$> info) text'') chunks
                    else chunks

        putMessageLnVerb 2 lock stderr $ "matches   : " <> show chunks'
        putMessageLnVerb 2 lock stderr $ "lindex    : " <> show lindex
        mkMatches lindex filename text'' chunks'

filterChunk :: Options -> UV.Vector Int -> Maybe FileTypeInfo -> T.Text -> Chunk -> Bool
filterChunk opts loff info text chunk
    | word_match opts =
        let (# line', _ #) = getLineByOffset ((cOffset chunk)) text loff
            !cs = parseChunks info line'
            !off = cOffset chunk
         in any (\chunk' -> cOffset chunk' == off && cToken chunk' == cToken chunk) $ cs
    | prefix_match opts =
        let !(# line', _ #) = getLineByOffset (cOffset chunk) text loff
            !cs = parseChunks info line'
         in any
                ( \chunk' ->
                    cToken chunk `T.isPrefixOf` cToken chunk'
                        && cOffset chunk' == cOffset chunk
                )
                cs
    | suffix_match opts =
        let !(# line', _ #) = getLineByOffset (cOffset chunk) text loff
            !cs = parseChunks info line'
            !tokLen = T.length (cToken chunk)
         in any
                ( \chunk' ->
                    let !tokLen' = T.length (cToken chunk')
                     in cToken chunk `T.isSuffixOf` cToken chunk'
                            && cOffset chunk' + (tokLen' - tokLen) == cOffset chunk
                )
                cs
    | otherwise = undefined