packages feed

leksah-0.16.2.2: src/IDE/Find.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# OPTIONS_GHC -fno-warn-warnings-deprecations #-}
-----------------------------------------------------------------------------
--
-- Module      :  IDE.Find
-- Copyright   :  (c) Juergen Nicklisch-Franken, Hamish Mackenzie
-- License     :  GNU-GPL
--
-- Maintainer  :  <maintainer at leksah.org>
-- Stability   :  provisional
-- Portability :  portable
--
-- | The toolbar for searching and replacing in a text buffer
--
-------------------------------------------------------------------------------

module IDE.Find (
    toggleFindbar
,   constructFindReplace
,   hideFindbar
,   showFindbar
,   focusFindEntry
,   editFindInc
,   editGotoLine
,   FindState(..)
,   getFindState
,   setFindState
,   editFind
,   showToolbar
,   hideToolbar
,   toggleToolbar
) where

import Control.Applicative (Applicative)
import IDE.Core.State
import IDE.Utils.GUIUtils
import IDE.TextEditor hiding(afterFocusIn)
import IDE.Pane.SourceBuffer
import Data.Char (digitToInt, isDigit, toLower, isAlphaNum)
import Text.Regex.TDFA hiding (caseSensitive, after)
import qualified Text.Regex.TDFA as Regex
import Text.Regex.TDFA.Text (compile)
import Data.List (find, isPrefixOf)
import Data.Array (bounds, (!), inRange)
import Data.Maybe (fromJust)
import IDE.Pane.Grep (grepWorkspace)
import IDE.Workspaces (workspaceTry, packageTry)
import Control.Monad.IO.Class (MonadIO(..))
import Control.Monad.Trans.Reader (ask)
import Control.Monad.Trans.Class (MonadTrans(..))
import Control.Monad (liftM, filterM, when, unless, void)
import Foreign.C.Types (CInt(..))
import Foreign.Ptr (Ptr(..))
import Foreign.ForeignPtr (withForeignPtr)
import Data.Text (Text)
import qualified Data.Text as T
       (pack, unpack, singleton, isPrefixOf, length, null, toLower)
import Data.Monoid ((<>))
import GI.Gtk.Objects.SpinButton
       (spinButtonSetRange, spinButtonNewWithRange, spinButtonSetValue,
        SpinButton(..), spinButtonGetValueAsInt)
import Data.GI.Base (set, unsafeCastTo, nullToNothing)
import GI.Gtk.Objects.Entry
       (afterEntryActivate, onEntryActivate, entrySetCompletion,
        entrySetPlaceholderText, entryNew, entrySetText, entryGetText,
        Entry(..))
import Data.GI.Gtk.ModelView.SeqStore
       (seqStoreGetValue, SeqStore(..), seqStoreNew, seqStoreAppend,
        seqStoreClear, seqStoreToList)
import GI.Gtk.Objects.Widget
       (widgetGetName, widgetModifyText, widgetModifyBase,
        afterWidgetFocusInEvent, onWidgetKeyPressEvent,
        onWidgetFocusInEvent, setWidgetTooltipText, Widget(..), widgetSetName,
        widgetGrabFocus, widgetShowAll, widgetHide)
import GI.Gtk.Objects.Toolbar
       (toolbarInsert, toolbarSetIconSize, toolbarSetStyle, toolbarNew,
        Toolbar(..))
import GI.Gtk.Enums (StateType(..), IconSize(..), ToolbarStyle(..))
import GI.Gtk.Objects.ToolButton
       (toolButtonSetLabel, onToolButtonClicked, toolButtonNew,
        toolButtonNewFromStock)
import GI.Gtk.Objects.ToolItem (toolItemSetExpand, toolItemNew)
import GI.Gtk.Objects.Container
       (containerGetChildren, containerChildSetProperty, containerAdd)
import GI.Gtk.Objects.Label (labelNew)
import GI.Gtk.Objects.SeparatorToolItem (separatorToolItemNew)
import GI.Gtk.Objects.ToggleToolButton
       (ToggleToolButton(..), toggleToolButtonSetActive,
        toggleToolButtonGetActive, toggleToolButtonNew)
import Data.GI.Gtk.ModelView.TreeModel (makeColumnIdString, treeModelGetPath, treeModelGetValue)
import Data.GI.Gtk.ModelView.CustomStore (customStoreSetColumn)
import GI.Gtk.Objects.EntryCompletion
       (EntryCompletion(..), onEntryCompletionMatchSelected,
        entryCompletionSetMatchFunc, setEntryCompletionModel,
        entryCompletionNew)
import GI.Gtk.Objects.CellRendererText
       (setCellRendererTextText, cellRendererTextNew)
import Data.GI.Gtk.ModelView.CellLayout
       (cellLayoutSetDataFunction, cellLayoutPackStart)
import GI.Gtk.Interfaces.Editable
       (afterEditableDeleteText, afterEditableInsertText)
import GI.Gdk.Structs.EventKey
       (getEventKeyState, getEventKeyKeyval)
import GI.Gdk.Functions (keyvalName)
import GI.Gdk.Flags (ModifierType(..))
import Data.GI.Base.GValue (IsGValue(..))
import GI.Gtk.Structs.TreeIter (TreeIter(..))
import GI.Gtk.Objects.Bin (Bin(..), binGetChild)
import IDE.Core.Types (toGdkColor)
import GI.Gtk.Structs.TreePath (treePathGetIndices)
import GI.Gtk (noWidget, imageNewFromIconName)

data FindState = FindState {
            entryStr        ::    Text
        ,   entryHist       ::    [Text]
        ,   replaceStr      ::    Text
        ,   replaceHist     ::    [Text]
        ,   caseSensitive   ::    Bool
        ,   entireWord      ::    Bool
        ,   wrapAround      ::    Bool
        ,   regex           ::    Bool
        ,   lineNr          ::    Int}
    deriving(Eq,Ord,Show,Read)

getFindState :: IDEM FindState
getFindState = do
    (fb,ls) <- needFindbar
    do
        lineNr        <- getLineEntry fb >>= spinButtonGetValueAsInt
        replaceStr    <- getReplaceEntry fb >>= entryGetText
        entryStr      <- getFindEntry fb >>= entryGetText
        entryHist     <- seqStoreToList ls
        entireWord    <- getEntireWord fb
        wrapAround    <- getWrapAround fb
        caseSensitive <- getCaseSensitive fb
        regex         <- getRegex fb
        return FindState{
                entryStr        =   entryStr
            ,   entryHist       =   entryHist
            ,   replaceStr      =   replaceStr
            ,   replaceHist     =   []
            ,   caseSensitive   =   caseSensitive
            ,   entireWord      =   entireWord
            ,   wrapAround      =   wrapAround
            ,   regex           =   regex
            ,   lineNr          =   fromIntegral lineNr}

setFindState :: FindState -> IDEAction
setFindState fs = do
    (fb,ls)      <- needFindbar
    getLineEntry fb >>= (\sb -> spinButtonSetValue sb (fromIntegral (lineNr fs)))
    getReplaceEntry fb >>= (\e -> entrySetText e (replaceStr fs))
    getFindEntry fb >>= (\e -> entrySetText e (entryStr fs))
    seqStoreClear ls
    mapM_ (seqStoreAppend ls) (entryHist fs)
    setEntireWord fb (entireWord fs)
    setWrapAround fb (wrapAround fs)
    setCaseSensitive fb (caseSensitive fs)
    setRegex fb (regex fs)

hideToolbar :: IDEAction
hideToolbar = do
    (_,mbtb) <- readIDE toolbar
    case mbtb of
        Nothing -> return ()
        Just tb -> do
            modifyIDE_ (\ide -> ide{toolbar = (False,snd (toolbar ide))})
            widgetHide tb

showToolbar :: IDEAction
showToolbar = do
    (_,mbtb) <- readIDE toolbar
    case mbtb of
        Nothing -> return ()
        Just tb -> do
            modifyIDE_ (\ide -> ide{toolbar = (True,snd (toolbar ide))})
            widgetShowAll tb

toggleToolbar :: IDEAction
toggleToolbar = do
    toolbar' <- readIDE toolbar
    if fst toolbar'
        then hideToolbar
        else showToolbar

hideFindbar :: IDEAction
hideFindbar = do
    (_,mbfb) <- readIDE findbar
    modifyIDE_ (\ide -> ide{findbar = (False,mbfb)})
    case mbfb of
        Nothing -> return ()
        Just (fb,_) -> do
            widgetHide fb
            void $ inActiveBufContext False $ \_ sv ebuf _ _ -> do
                removeTagByName ebuf "search-match"
                return True

showFindbar :: IDEAction
showFindbar = do
    (_,mbfb) <- readIDE findbar
    modifyIDE_ (\ide -> ide{findbar = (True,mbfb)})
    case mbfb of
        Nothing -> return ()
        Just (fb,_) -> widgetShowAll fb

focusFindEntry :: IDEAction
focusFindEntry = do
    (fb,_) <- needFindbar
    do
        entry <- getFindEntry fb
        widgetGrabFocus entry

toggleFindbar :: IDEAction
toggleFindbar = do
    findbar <- readIDE findbar
    if fst findbar
        then hideFindbar
        else showFindbar

constructFindReplace :: IDEM Toolbar
constructFindReplace = do
    ideR <- ask
    toolbar <- toolbarNew
    toolbarSetStyle toolbar ToolbarStyleIcons
    toolbarSetIconSize toolbar IconSizeSmallToolbar
    let newButtonFromIconName name = imageNewFromIconName
                                        (Just name)
                                        (fromIntegral $ fromEnum IconSizeSmallToolbar)
                                    >>= (`toolButtonNew` Nothing) . Just
    closeButton <- newButtonFromIconName "window-close"
    toolbarInsert toolbar closeButton 0

    spinTool <- toolItemNew
    spinL <- spinButtonNewWithRange 1.0 1000.0 10.0
    widgetSetName spinL "gotoLineEntry"
    containerAdd spinTool spinL
    widgetSetName spinTool "gotoLineEntryTool"
    toolbarInsert toolbar spinTool 0

    labelTool3 <- toolItemNew
    label3 <- labelNew (Just (__"Goto Line :"))
    containerAdd labelTool3 label3
    toolbarInsert toolbar labelTool3 0

    sep1 <- separatorToolItemNew
    toolbarInsert toolbar sep1 0

    let performGrep = reflectIDE (workspaceTry $ doGrep toolbar) ideR
    grepButton <- toolButtonNew noWidget (Just (__"Grep"))
    toolbarInsert toolbar grepButton 0
    onToolButtonClicked grepButton performGrep
    setWidgetTooltipText grepButton $ __"Search in multiple files"

    sep1 <- separatorToolItemNew
    toolbarInsert toolbar sep1 0

    replaceAllButton <- toolButtonNew noWidget (Just (__"Replace All"))
    toolbarInsert toolbar replaceAllButton 0

    replaceButton <- newButtonFromIconName "edit-find-replace"
    toolbarInsert toolbar replaceButton 0

    replaceTool <- toolItemNew
    rentry <- entryNew
    widgetSetName rentry "replaceEntry"
    entrySetPlaceholderText rentry (Just "Replace with")
    containerAdd replaceTool rentry
    widgetSetName replaceTool "replaceTool"
    toolbarInsert toolbar replaceTool 0

    sep2 <- separatorToolItemNew
    toolbarInsert toolbar sep2 0

    nextButton <- newButtonFromIconName "go-next"
    toolbarInsert toolbar nextButton 0
    setWidgetTooltipText nextButton $ __"Search for the next match in the current file"
    nextButton `onToolButtonClicked` doSearch toolbar Forward ideR

    wrapAroundButton <- toggleToolButtonNew
    toolButtonSetLabel wrapAroundButton (Just (__"Wrap"))
    widgetSetName wrapAroundButton "wrapAroundButton"
    toolbarInsert toolbar wrapAroundButton 0
    setWidgetTooltipText wrapAroundButton $ __"When selected searching will continue from the top when no more matches are found"

    previousButton <- newButtonFromIconName "go-previous"
    toolbarInsert toolbar previousButton 0
    setWidgetTooltipText previousButton $ __"Search for the previous match in the current file"
    previousButton `onToolButtonClicked` doSearch toolbar Backward ideR

    entryTool <- toolItemNew
    entry <- entryNew
    widgetSetName entry "searchEntry"
    entrySetPlaceholderText entry (Just "Find")
    containerAdd entryTool entry
    widgetSetName entryTool "searchEntryTool"
    toolItemSetExpand entryTool True
    toolbarInsert toolbar entryTool 0

    let column0 = makeColumnIdString 0
    store <- seqStoreNew []
    customStoreSetColumn store column0 id

    completion <- entryCompletionNew
    entrySetCompletion entry (Just completion)

    setEntryCompletionModel completion store
    cell <- cellRendererTextNew
    cellLayoutPackStart completion cell True
    cellLayoutSetDataFunction completion cell store
        (setCellRendererTextText cell)
    entryCompletionSetMatchFunc completion (matchFunc store)
    onEntryCompletionMatchSelected completion $ \ model iter -> do
        txt <- treeModelGetValue model iter column0
        entrySetText entry txt
        doSearch toolbar Forward ideR
        return True

    regexButton <- toggleToolButtonNew
    toolButtonSetLabel regexButton (Just (__"Regex"))
    widgetSetName regexButton "regexButton"
    toolbarInsert toolbar regexButton 0
    onToolButtonClicked regexButton $ doSearch toolbar Insert ideR
    setWidgetTooltipText regexButton $ __"When selected the search string is used as a regular expression"

    entireWordButton <- toggleToolButtonNew
    toolButtonSetLabel entireWordButton (Just (__"Words"))
    widgetSetName entireWordButton "entireWordButton"
    toolbarInsert toolbar entireWordButton 0
    entireWordButton `onToolButtonClicked` doSearch toolbar Insert ideR
    setWidgetTooltipText entireWordButton $ __"When selected only entire words are matched"

    caseSensitiveButton <- toggleToolButtonNew
    toolButtonSetLabel caseSensitiveButton (Just (__"Case"))
    widgetSetName caseSensitiveButton "caseSensitiveButton"
    toolbarInsert toolbar caseSensitiveButton 0
    caseSensitiveButton `onToolButtonClicked`
       doSearch toolbar Insert ideR
    setWidgetTooltipText caseSensitiveButton $ __"When selected the search is case sensitive"

    afterEditableInsertText entry (\ t _ i -> do
        doSearch toolbar Insert ideR
        return i)
    afterEditableDeleteText entry (\ _ _ -> doSearch toolbar Delete ideR)

    onEntryActivate entry $ doSearch toolbar Forward ideR
    onIDE onWidgetFocusInEvent entry $ do
        liftIDE $ triggerEventIDE (Sensitivity [(SensitivityEditor, False)])
        return False

    onToolButtonClicked replaceButton $ replace toolbar Forward ideR
    let performReplaceAll = replaceAll toolbar Initial ideR
    onToolButtonClicked replaceAllButton performReplaceAll

    let ctrl "c" = toggleToolButton caseSensitiveButton >> return True
        ctrl "e" = toggleToolButton regexButton >> return True
        ctrl "w" = toggleToolButton entireWordButton >> return True
        ctrl "p" = toggleToolButton wrapAroundButton >> return True
        ctrl "r" = performReplaceAll >> return True
        ctrl "g" = performGrep >> return True
        ctrl _ = return False
        toggleToolButton btn = do
            old <- toggleToolButtonGetActive btn
            toggleToolButtonSetActive btn $ not old

    onWidgetKeyPressEvent entry $ \e -> do
        mbName <- getEventKeyKeyval e >>= keyvalName
        mods <- getEventKeyState e
        case mbName of
            Just "Return" | ModifierTypeShiftMask `elem`  mods ->
                                doSearch toolbar Backward ideR >> return True
            Just "Down"   -> doSearch toolbar Forward ideR >> return True
            Just "Up"     -> doSearch toolbar Backward ideR >> return True
            Just "Escape" -> getOut ideR >> return True
            Just "Tab"    -> do
                re <- getReplaceEntry toolbar
                widgetGrabFocus re
                --- widgetAc
                return True
            Just name | mapControlCommand ModifierTypeControlMask `elem` mods -> ctrl $ T.toLower name
            _        -> return False

    onWidgetKeyPressEvent rentry $ \e -> do
        mbName <- getEventKeyKeyval e >>= keyvalName
        mods <- getEventKeyState e
        case mbName of
           Just name
             | name == "Tab" || name == "ISO_Left_Tab" -> do
                    fe <- getFindEntry toolbar
                    widgetGrabFocus fe
                    return True
             | mapControlCommand ModifierTypeControlMask `elem` mods ->
                        ctrl $ T.toLower name
           _ -> return False

    onIDE afterWidgetFocusInEvent spinL . liftIDE $ inActiveBufContext True $ \ _ _ ebuf _ _ -> do
        max <- getLineCount ebuf
        spinButtonSetRange spinL 1.0 (fromIntegral max)
        return True

    onWidgetKeyPressEvent spinL $ \e -> do
        mbName <- getEventKeyKeyval e >>= keyvalName
        mods <- getEventKeyState e
        case mbName of
            Just "Escape" -> getOut ideR >> return True
            Just "Tab"    -> do
                re <- getFindEntry toolbar
                widgetGrabFocus re
                return True
            Just name | mapControlCommand ModifierTypeControlMask `elem` mods -> ctrl $ T.toLower name
            _ -> return False

    afterEntryActivate spinL . (`reflectIDE` ideR) $ inActiveBufContext () $ \ _ sv ebuf _ _ -> do
        line <- spinButtonGetValueAsInt spinL
        iter <- getIterAtLine ebuf (fromIntegral line - 1)
        placeCursor ebuf iter
        scrollToIter sv iter 0.2 Nothing
        getOut ideR
        return ()

    onToolButtonClicked closeButton $ reflectIDE hideFindbar ideR

    liftIO $ do
        containerChildSetProperty toolbar spinTool "homogeneous" =<< toGValue False
        containerChildSetProperty toolbar wrapAroundButton "homogeneous" =<< toGValue False
        containerChildSetProperty toolbar entireWordButton "homogeneous" =<< toGValue False
        containerChildSetProperty toolbar caseSensitiveButton "homogeneous" =<< toGValue False
        containerChildSetProperty toolbar regexButton "homogeneous" =<< toGValue False
        containerChildSetProperty toolbar replaceAllButton "homogeneous" =<< toGValue False
        containerChildSetProperty toolbar labelTool3 "homogeneous" =<< toGValue False

    modifyIDE_ (\ ide -> ide{findbar = (False, Just (toolbar, store))})
    return toolbar
  where
    getOut ideR = liftIO $ reflectIDE (do
        hideFindbar
        maybeActiveBuf ?>>= makeActive) ideR


doSearch :: Toolbar -> SearchHint -> IDERef -> IO ()
doSearch fb hint ideR   = do
    entry         <- getFindEntry fb
    search        <- entryGetText entry
    entireWord    <- getEntireWord fb
    caseSensitive <- getCaseSensitive fb
    wrapAround    <- getWrapAround fb
    regex         <- getRegex fb
    mbExpAndMatchIndex <- regexAndMatchIndex caseSensitive entireWord regex search
    case mbExpAndMatchIndex of
        Just (exp, matchIndex) -> do
            res           <- reflectIDE (editFind entireWord caseSensitive wrapAround regex search "" hint) ideR
            if res || T.null search
                then do
                    widgetModifyBase entry StateTypeNormal . Just =<< toGdkColor white
                    widgetModifyText entry StateTypeNormal . Just =<< toGdkColor black
                else do
                    widgetModifyBase entry StateTypeNormal . Just =<< toGdkColor red
                    widgetModifyText entry StateTypeNormal . Just =<< toGdkColor white
        Nothing ->
            if T.null search
                then do
                    widgetModifyBase entry StateTypeNormal . Just =<< toGdkColor white
                    widgetModifyText entry StateTypeNormal . Just =<< toGdkColor black
                else do
                    widgetModifyBase entry StateTypeNormal . Just =<< toGdkColor orange
                    widgetModifyText entry StateTypeNormal . Just =<< toGdkColor black
    reflectIDE (addToHist search) ideR
    return ()

doGrep :: Toolbar -> WorkspaceAction
doGrep fb   = do
    package       <- ask
    ideR          <- lift ask
    entry         <- getFindEntry fb
    search        <- entryGetText entry
    entireWord    <- getEntireWord fb
    caseSensitive <- getCaseSensitive fb
    wrapAround    <- getWrapAround fb
    regex         <- getRegex fb
    let (regexString, _) = regexStringAndMatchIndex entireWord regex search
    liftIDE $ workspaceTry $ grepWorkspace regexString caseSensitive

matchFunc :: (Applicative m, MonadIO m) => SeqStore Text -> EntryCompletion -> Text -> TreeIter -> m Bool
matchFunc model completion str iter = do
  tp <- treeModelGetPath model iter >>= treePathGetIndices
  case tp of
         (i:_) -> do row <- seqStoreGetValue model i
                     return (T.isPrefixOf (T.toLower str) (T.toLower row) && T.length str < T.length row)
         _     -> return False

addToHist :: Text -> IDEAction
addToHist str =
    unless (T.null str) $ do
        (_, ls) <- needFindbar
        entryHist <- seqStoreToList ls
        unless (any (str `T.isPrefixOf`) entryHist) $ do
            let newList = take 12 (str : filter (\ e -> not (e `T.isPrefixOf` str)) entryHist)
            seqStoreClear ls
            mapM_ (seqStoreAppend ls) newList

replace :: Toolbar -> SearchHint -> IDERef -> IO ()
replace fb hint ideR   =  do
    entry          <- getFindEntry fb
    search         <- entryGetText entry
    rentry         <- getReplaceEntry fb
    replace        <- entryGetText rentry
    entireWord     <- getEntireWord fb
    caseSensitive  <- getCaseSensitive fb
    wrapAround     <- getWrapAround fb
    regex          <- getRegex fb
    found <- reflectIDE (editReplace entireWord caseSensitive wrapAround regex search replace hint)
                ideR
    return ()

replaceAll :: Toolbar -> SearchHint -> IDERef -> IO ()
replaceAll fb hint ideR   =  do
    entry          <- getFindEntry fb
    search         <- entryGetText entry
    rentry         <- getReplaceEntry fb
    replace        <- entryGetText rentry
    entireWord     <- getEntireWord fb
    caseSensitive  <- getCaseSensitive fb
    wrapAround     <- getWrapAround fb
    regex          <- getRegex fb
    found <- reflectIDE (editReplaceAll entireWord caseSensitive regex search replace hint)
                ideR
    return ()

editFind :: Bool -> Bool -> Bool -> Bool -> Text -> Text -> SearchHint -> IDEM Bool
editFind entireWord caseSensitive wrapAround regex search dummy hint = do
    mbExpAndMatchIndex <- regexAndMatchIndex caseSensitive entireWord regex search
    case mbExpAndMatchIndex of
        Nothing -> return False
        Just (exp, matchIndex) -> editFind' exp matchIndex wrapAround dummy hint

editFind' :: Regex -> Int -> Bool -> Text -> SearchHint -> IDEM Bool
editFind' exp matchIndex wrapAround dummy hint =
    inActiveBufContext False $ \_ sv ebuf _ _ -> do
        i1 <- getStartIter ebuf
        i2 <- getEndIter ebuf
        text <- getText ebuf i1 i2 True
        removeTagByName ebuf "search-match"
        startMark <- getInsertMark ebuf
        st1 <- getIterAtMark ebuf startMark
        mbsr2 <-
            if hint == Backward
                then do
                    st2 <- backwardCharC st1
                    st3 <- backwardCharC st2
                    mbsr <- backSearch exp matchIndex ebuf text st3
                    case mbsr of
                        Nothing ->
                            if wrapAround
                                then backSearch exp matchIndex ebuf text i2
                                else return Nothing
                        m -> return m
                else do
                    st2 <- if hint == Forward
                        then forwardCharC st1
                        else return st1
                    mbsr <- if hint == Initial
                            then initialSearch exp matchIndex ebuf text st2
                            else forwardSearch exp matchIndex ebuf text st2
                    case mbsr of
                        Nothing ->
                            if wrapAround
                                then forwardSearch exp matchIndex ebuf text i1
                                else return Nothing
                        m -> return m
        case mbsr2 of
            Just (start,end,_) -> do --found
                --widgetGrabFocus sourceView
                scrollToIter sv start 0.2 Nothing
                applyTagByName ebuf "search-match" start end
                placeCursor ebuf start
                return True
            Nothing -> return False
    where
        backSearch exp matchIndex ebuf text iter = do
            offset <- getOffset iter
            findMatch exp matchIndex ebuf text (<= offset) True

        forwardSearch exp matchIndex ebuf text iter = do
            offset <- getOffset iter
            findMatch exp matchIndex ebuf text (>= offset) False

        initialSearch exp matchIndex ebuf text iter = findMatch exp matchIndex ebuf text (>= 0) False

regexAndMatchIndex :: MonadIO m => Bool -> Bool -> Bool -> Text -> m (Maybe (Regex, Int))
regexAndMatchIndex caseSensitive entireWord regex string =
    if T.null string
        then return Nothing
        else do
            let (regexString, index) = regexStringAndMatchIndex entireWord regex string
            case compileRegex caseSensitive regexString of
                Left err -> do
                    sysMessage Normal $ T.pack err
                    return Nothing
                Right regex -> return $ Just (regex, index)

regexStringAndMatchIndex :: Bool -> Bool -> Text -> (Text, Int)
regexStringAndMatchIndex entireWord regex string =
    -- Escape non regex string
    let regexString = if regex
                        then string
                        else foldl (\s c -> s <> if isAlphaNum c then T.singleton c else "\\"<>T.singleton c) "" $ T.unpack string in
    -- Regular expression with word filter if needed
    if entireWord
        then ("(^|[^a-zA-Z0-9])(" <> regexString <> ")($|[^a-zA-Z0-9])", 2)
        else (regexString, 0)

findMatch :: TextEditor editor => Regex -> Int -> EditorBuffer editor -> Text -> (Int -> Bool) -> Bool -> IDEM (Maybe (EditorIter editor, EditorIter editor, MatchArray))
findMatch exp matchIndex gtkbuf text offsetPred findLast = do
    let matches = (if findLast then reverse else id) (matchAll exp text)
    case find (offsetPred . fst . (!matchIndex)) matches of
        Just matches -> do
            iterStart <- getStartIter gtkbuf
            iter1     <- forwardCharsC iterStart (fst (matches!matchIndex))
            iter2     <- forwardCharsC iter1 (snd (matches!matchIndex))
            return $ Just (iter1, iter2, matches)
        Nothing -> return Nothing

editReplace :: Bool -> Bool -> Bool -> Bool -> Text -> Text -> SearchHint -> IDEM Bool
editReplace entireWord caseSensitive wrapAround regex search replace hint =
    editReplace' entireWord caseSensitive wrapAround regex search replace hint False

editReplace' :: Bool -> Bool -> Bool -> Bool -> Text -> Text -> SearchHint -> Bool -> IDEM Bool
editReplace' entireWord caseSensitive wrapAround regex search replace hint fromStart =
    inActiveBufContext False $ \_ _ ebuf _ _ -> do
        insertMark <- getInsertMark ebuf
        iter       <- getIterAtMark ebuf insertMark
        offset   <- getOffset iter
        let offset' = if fromStart then 0 else offset
        mbExpAndMatchIndex <- regexAndMatchIndex caseSensitive entireWord regex search
        case mbExpAndMatchIndex of
            Just (exp, matchIndex) -> do
                iStart <- getStartIter ebuf
                iEnd   <- getEndIter ebuf
                text   <- getText ebuf iStart iEnd True
                match  <- findMatch exp matchIndex ebuf text (== offset') False
                case match of
                    Just (iterStart, iterEnd, matches) -> do
                        mbText <- replacementText regex text matchIndex matches $ T.unpack replace
                        case mbText of
                            Just text -> do
                                beginUserAction ebuf
                                delete ebuf iterStart iterEnd
                                insert ebuf iterStart (T.pack text)
                                endUserAction ebuf
                            Nothing -> do
                                sysMessage Normal
                                    "Should never happen. findMatch worked but repleacementText failed"
                                return ()
                        editFind entireWord caseSensitive wrapAround regex search "" hint
                    Nothing -> do
                        r <- editFind entireWord caseSensitive wrapAround regex search "" hint
                        if r
                            then editReplace' entireWord caseSensitive wrapAround regex search
                                    replace hint False
                            else return False
            Nothing -> return False
    where
        replacementText False _ _ _ replace = return $ Just replace
        replacementText True text matchIndex matches replace =
            case compileRegex caseSensitive search of
                Left err -> do
                    sysMessage Normal $ T.pack err
                    return Nothing
                Right exp -> return $ Just $ regexReplacement text matchIndex matches replace

regexReplacement :: Text -> Int -> MatchArray -> String -> String
regexReplacement _ _ _ [] = []
regexReplacement text matchIndex matches ('\\' : '\\' : xs) = '\\' : regexReplacement text matchIndex matches xs
regexReplacement text matchIndex matches ('\\' : n : xs) | isDigit n =
    let subIndex = matchIndex + digitToInt n
        value    = if inRange (bounds matches) subIndex
                    then
                        let subExp = matches!(matchIndex + digitToInt n) in
                        take (snd subExp) $ drop (fst subExp) $ T.unpack text
                    else ['\\', n] in
    value ++ regexReplacement text matchIndex matches xs

regexReplacement text matchIndex matches (x : xs) = x : regexReplacement text matchIndex matches xs

editReplaceAll :: Bool -> Bool -> Bool -> Text -> Text -> SearchHint -> IDEM Bool
editReplaceAll = editReplaceAll' True

editReplaceAll' :: Bool -> Bool -> Bool -> Bool -> Text -> Text -> SearchHint -> IDEM Bool
editReplaceAll' fromStart entireWord caseSensitive regex search replace hint = do
    res <- editReplace' entireWord caseSensitive False regex search replace hint fromStart
    if res
        then editReplaceAll' False entireWord caseSensitive regex search replace hint
        else return False

compileRegex :: Bool -> Text -> Either String Regex
compileRegex caseSense searchString =
    let compOption = defaultCompOpt {
                            Regex.caseSensitive = caseSense
                        ,   multiline = True } in
    compile compOption defaultExecOpt searchString

red = Color 64000 10000 10000
orange = Color 64000 48000 0
white = Color 64000 64000 64000
black = Color 0 0 0

needFindbar :: IDEM (Toolbar,SeqStore Text)
needFindbar = do
    (_,mbfb) <- readIDE findbar
    case mbfb of
        Nothing -> throwIDE "Find>>needFindbar: Findbar not initialized"
        Just p  -> return p

-- | Find action. If SearchHint==Initial then we'll use the selected text or current identifier to populate the find text box
editFindInc :: SearchHint -> IDEAction
editFindInc hint = do
    ideR <- ask
    (fb,_) <- needFindbar
    case hint of
        Initial -> do
               mbtext <- selectedText -- if no text selected, search for the last query
               case mbtext of
                 Just text -> do
                     findEntry <- getFindEntry fb
                     entrySetText findEntry text
                 Nothing -> return ()
        _ -> return ()
    showFindbar
    reifyIDE $ \ideR   -> do
        entry <- getFindEntry fb
        widgetGrabFocus entry
        case hint of
            Forward  -> doSearch fb Forward ideR
            Backward -> doSearch fb Backward ideR
            _        -> return ()

editGotoLine :: IDEAction
editGotoLine = do
    showFindbar
    (fb,_) <- needFindbar
    entry <- getLineEntry fb
    widgetGrabFocus entry

getLineEntry :: (Applicative m, MonadIO m) => Toolbar -> m SpinButton
getLineEntry tb    = getWidget "gotoLineEntryTool" tb >>= liftIO . unsafeCastTo SpinButton

getReplaceEntry, getFindEntry :: (Applicative m, MonadIO m) => Toolbar -> m Entry
getReplaceEntry tb = getWidget "replaceTool" tb >>= liftIO . unsafeCastTo Entry
getFindEntry tb    = getWidget "searchEntryTool" tb >>= liftIO . unsafeCastTo Entry

getWidget :: (Applicative m, MonadIO m) => Text -> Toolbar -> m Widget
getWidget str tb = do
    widgets <- containerGetChildren tb
    entryL <-  filterM (fmap (== str) . widgetGetName) widgets
    case entryL of
        [w] -> liftIO (unsafeCastTo Bin w) >>= (fmap fromJust . nullToNothing . binGetChild)
        _   -> throwIDE "Find>>getWidget not found(2)"

getEntireWord, getWrapAround, getCaseSensitive, getRegex :: (Applicative m, MonadIO m) => Toolbar -> m Bool
getEntireWord    = getSelection "entireWordButton"
getWrapAround    = getSelection "wrapAroundButton"
getCaseSensitive = getSelection "caseSensitiveButton"
getRegex         = getSelection "regexButton"

getSelection :: (Applicative m, MonadIO m) => Text -> Toolbar -> m Bool
getSelection str tb = do
    widgets <- containerGetChildren tb
    entryL <-  filterM (fmap (== str) . widgetGetName) widgets
    case entryL of
        [w] -> liftIO (unsafeCastTo ToggleToolButton w) >>= toggleToolButtonGetActive
        _   -> throwIDE "Find>>getIt widget not found"

setEntireWord, setWrapAround, setCaseSensitive, setRegex :: (Applicative m, MonadIO m) => Toolbar -> Bool -> m ()
setEntireWord    = setSelection "entireWordButton"
setWrapAround    = setSelection "wrapAroundButton"
setCaseSensitive = setSelection "caseSensitiveButton"
setRegex         = setSelection "regexButton"

setSelection :: (Applicative m, MonadIO m) => Text -> Toolbar -> Bool ->  m ()
setSelection str tb bool = do
    widgets <- containerGetChildren tb
    entryL <-  filterM (fmap (== str) . widgetGetName ) widgets
    case entryL of
        [w] -> liftIO (unsafeCastTo ToggleToolButton w) >>= \ttb -> toggleToolButtonSetActive ttb bool
        _   -> throwIDE "Find>>getIt widget not found"