packages feed

leksah-0.16.2.2: src/IDE/Pane/Grep.hs

{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE TypeSynonymInstances #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE OverloadedStrings #-}
-----------------------------------------------------------------------------
--
-- Module      :  IDE.Pane.Grep
-- Copyright   :  (c) Juergen Nicklisch-Franken, Hamish Mackenzie
-- License     :  GNU-GPL
--
-- Maintainer  :  <maintainer at leksah.org>
-- Stability   :  provisional
-- Portability  :  portable
--
-- | The pane of the ide where grep results are displayed
--
-------------------------------------------------------------------------------

module IDE.Pane.Grep (
    IDEGrep(..)
,   grepWorkspace
,   GrepState(..)
,   getGrep
) where

import Prelude ()
import Prelude.Compat
import Control.Applicative (Applicative)
import Text.ParserCombinators.Parsec.Language
import Text.ParserCombinators.Parsec hiding(Parser)
import qualified Text.ParserCombinators.Parsec.Token as P
import Data.Maybe
import Data.Typeable
import Data.List (isPrefixOf)
import IDE.Core.State
import IDE.BufferMode
import IDE.Utils.Tool (runTool, ToolOutput(..), getProcessExitCode, interruptProcessGroupOf)
import Control.Concurrent
       (forkOS, newEmptyMVar, isEmptyMVar, takeMVar, putMVar, MVar,
        forkIO)
import IDE.LogRef (logOutput, defaultLineLogger)
import IDE.Pane.SourceBuffer
    (goToSourceDefinition, maybeActiveBuf, IDEBuffer(..))
import IDE.TextEditor (grabFocus)
import System.FilePath ((</>), dropFileName)
import System.Exit (ExitCode(..))
import IDE.Pane.Log (getLog, getDefaultLogLaunch)
import Control.DeepSeq
import qualified Data.Conduit as C
       (Sink)
import qualified Data.Conduit.List as CL
       (foldM, head, isolate, sinkNull)
import Data.Conduit (($$), (=$))
import Control.Monad (void, foldM, when)
import Control.Monad.Trans.Reader (ask)
import Control.Monad.Trans.Class (MonadTrans(..))
import Control.Monad.IO.Class (MonadIO(..))
import IDE.Utils.GUIUtils (__, treeViewToggleRow)
import System.Directory (getDirectoryContents)
import qualified Data.Text as T (pack, take, unpack)
import System.Log.Logger (debugM)
import Control.Exception (SomeException, catch)
import Data.Text (Text)
import GI.Gtk.Objects.ScrolledWindow
       (scrolledWindowSetPolicy, scrolledWindowSetShadowType,
        scrolledWindowNew, ScrolledWindow(..))
import GI.Gtk.Objects.TreeView
       (treeViewExpandAll, treeViewGetSelection,
        treeViewSetHeadersVisible, treeViewAppendColumn, treeViewSetModel,
        treeViewNew, TreeView(..), onTreeViewRowActivated)
import Data.GI.Gtk.ModelView.ForestStore
       (forestStoreChange, forestStoreGetValue, ForestStore(..),
        forestStoreNew, forestStoreClear, forestStoreInsert)
import GI.Gtk.Objects.CellRendererText
       (setCellRendererTextText, cellRendererTextNew)
import GI.Gtk.Objects.TreeViewColumn
       (treeViewColumnSetReorderable, treeViewColumnSetResizable,
        treeViewColumnSetSizing, treeViewColumnSetTitle, treeViewColumnNew)
import GI.Gtk.Enums
       (PolicyType(..), ShadowType(..), SelectionMode(..),
        TreeViewColumnSizing(..))
import GI.Gtk.Interfaces.CellLayout (cellLayoutPackStart)
import Data.GI.Gtk.ModelView.CellLayout
       (cellLayoutSetDataFunction)
import GI.Gtk.Objects.TreeSelection
       (onTreeSelectionChanged, treeSelectionSetMode)
import GI.Gtk.Objects.Adjustment (noAdjustment)
import GI.Gtk.Objects.Container (containerAdd)
import GI.Gtk.Objects.Widget
       (widgetGrabFocus, onWidgetKeyPressEvent, afterWidgetFocusInEvent,
        toWidget)
import GI.Gdk.Structs.EventKey (getEventKeyKeyval)
import GI.Gdk.Functions (keyvalName)
import Data.GI.Gtk.ModelView.TreeModel
       (treeModelGetIter, treeModelIterNChildren)
import Data.GI.Gtk.ModelView.Types
       (treeSelectionGetSelectedRows', treePathNewFromIndices')
import qualified Data.Function as F (on)

-- | Represents a single search result
data GrepRecord = GrepRecord {
            file        :: FilePath
        ,   line        :: Int
        ,   context     :: Text
        ,   parDir      :: Maybe FilePath
        }


-- | Determines whether a 'GrepRecord' is a directory
isDir :: GrepRecord -> Bool
isDir GrepRecord{parDir = Nothing}  = True
isDir otherwies                     = False


-- | The representation of the Grep result pane
data IDEGrep        =   IDEGrep {
    scrolledView    ::   ScrolledWindow
,   treeView        ::   TreeView
,   grepStore       ::   ForestStore GrepRecord
,   waitingGrep     ::   MVar Bool
,   activeGrep      ::   MVar Bool
} deriving Typeable


-- | The additional state used when recovering the pane
data GrepState      =   GrepState
    deriving(Eq,Ord,Read,Show,Typeable)


instance Pane IDEGrep IDEM
    where
    primPaneName _  =   __ "Grep"
    getAddedIndex _ =   0
    getTopWidget    =   liftIO . toWidget . scrolledView
    paneId b        =   "*Grep"


instance RecoverablePane IDEGrep GrepState IDEM where
    saveState p     =   return (Just GrepState)
    recoverState pp GrepState =   do
        nb      <-  getNotebook pp
        buildPane pp nb builder
    builder pp nb windows = do
        grepStore   <-  forestStoreNew []
        treeView    <-  treeViewNew
        treeViewSetModel treeView (Just grepStore)

        renderer1    <- cellRendererTextNew
        col1         <- treeViewColumnNew
        treeViewColumnSetTitle col1 (__ "File")
        treeViewColumnSetSizing col1 TreeViewColumnSizingAutosize
        treeViewColumnSetResizable col1 True
        treeViewColumnSetReorderable col1 True
        treeViewAppendColumn treeView col1
        cellLayoutPackStart col1 renderer1 True
        cellLayoutSetDataFunction col1 renderer1 grepStore
            $ \row -> setCellRendererTextText renderer1 . T.pack $ file row

        renderer2   <- cellRendererTextNew
        col2        <- treeViewColumnNew
        treeViewColumnSetTitle col2 (__ "Line")
        treeViewColumnSetSizing col2 TreeViewColumnSizingAutosize
        treeViewColumnSetResizable col2 True
        treeViewColumnSetReorderable col2 True
        treeViewAppendColumn treeView col2
        cellLayoutPackStart col2 renderer2 True
        cellLayoutSetDataFunction col2 renderer2 grepStore
            $ setCellRendererTextText renderer2 . T.pack . show . line

        renderer3    <- cellRendererTextNew
        col3         <- treeViewColumnNew
        treeViewColumnSetTitle col3 (__ "Context")
        treeViewColumnSetSizing col3 TreeViewColumnSizingAutosize
        treeViewColumnSetResizable col3 True
        treeViewColumnSetReorderable col3 True
        treeViewAppendColumn treeView col3
        cellLayoutPackStart col3 renderer3 True
        cellLayoutSetDataFunction col3 renderer3 grepStore
            $ setCellRendererTextText renderer3 . T.take 2048 . context


        treeViewSetHeadersVisible treeView True
        sel <- treeViewGetSelection treeView
        treeSelectionSetMode sel SelectionModeSingle

        onTreeViewRowActivated treeView $ \path col ->
            void $ treeViewToggleRow treeView path

        scrolledView <- scrolledWindowNew noAdjustment noAdjustment
        scrolledWindowSetShadowType scrolledView ShadowTypeIn
        containerAdd scrolledView treeView
        scrolledWindowSetPolicy scrolledView PolicyTypeAutomatic PolicyTypeAutomatic

        waitingGrep <- liftIO newEmptyMVar
        activeGrep <- liftIO newEmptyMVar
        let grep = IDEGrep {..}
        let
            gotoSource :: Bool -> IDEM Bool
            gotoSource focus = do
                sel <- getSelectionGrepRecord treeView grepStore
                case sel of
                    Just record ->
                        case record of
                            GrepRecord {file=f, line=l, parDir=Just pp} ->
                                goToSourceDefinition pp (Location f l 0 l 0) ?>>=
                                   (\ IDEBuffer{sourceView = sv} -> when focus $ grabFocus sv)
                            _ -> return ()
                    Nothing -> return ()
                return True
        cid1 <- onIDE afterWidgetFocusInEvent treeView $ do
            liftIDE $ makeActive grep
            return True
        cid2 <- onIDE onWidgetKeyPressEvent treeView $ do
            e <- lift ask
            name <- getEventKeyKeyval e >>= keyvalName
            liftIDE $
                case name of
                    Just "Return" -> gotoSource True
                    Just "Escape" -> do
                        lastActiveBufferPane ?>>= \paneName -> do
                            (PaneC pane) <- paneFromName paneName
                            makeActive pane
                            return ()
                        triggerEventIDE StartFindInitial
                        return True
                        -- gotoSource True
                    _ -> return False
        ideR <- ask
        onTreeSelectionChanged sel (void (reflectIDE (gotoSource False) ideR))

        return (Just grep,[cid1, cid2])

-- | Get the Grep panel
getGrep :: Maybe PanePath -> IDEM IDEGrep
getGrep Nothing    = forceGetPane (Right "*Grep")
getGrep (Just pp)  = forceGetPane (Left pp)


-- | Parses a result line of grep output into a 'GrepRecord'
grepLineParser :: CharParser () GrepRecord
grepLineParser = try (do
        file <- many (noneOf ":")
        char ':'
        line <- int
        char ':'
        context <- T.pack <$> many anyChar
        let parDir = Nothing
        return GrepRecord {..}
    <?> "grepLineParser")

lexer = P.makeTokenParser emptyDef
lexeme = P.lexeme lexer
whiteSpace = P.whiteSpace lexer
hexadecimal = P.hexadecimal lexer
symbol = P.symbol lexer
identifier = P.identifier lexer
colon = P.colon lexer
int = fromInteger <$> P.integer lexer


-- | Tries to get the currently selected record in the pane
getSelectionGrepRecord :: (Applicative m, MonadIO m)
                       => TreeView
                       -> ForestStore GrepRecord
                       -> m (Maybe GrepRecord)
getSelectionGrepRecord treeView grepStore = do
    treeSelection   <-  treeViewGetSelection treeView
    paths           <-  treeSelectionGetSelectedRows' treeSelection
    case paths of
        p:_ ->  Just <$> forestStoreGetValue grepStore p
        _   ->  return Nothing


--TODO srp use default loglaunch probably
-- | Greps the current workspace
grepWorkspace :: Text -- ^ The regex string
              -> Bool -- ^ Case sensitive
              -> WorkspaceAction
grepWorkspace "" caseSensitive = return ()
grepWorkspace regexString caseSensitive = do
    ws <- ask
    maybeActive <- lift $ readIDE activePack
    let packages = case maybeActive of
            Just active -> active : filter (((/=) `F.on` ipdCabalFile) active) (wsAllPackages ws)
            Nothing     -> wsAllPackages ws
    lift $ grepDirectories regexString caseSensitive $
            map ipdPackageDir packages


-- | Greps a list of directories
grepDirectories :: Text -- ^ The regex string
                -> Bool -- ^ Case sensitive
                -> [FilePath]
                -> IDEAction
grepDirectories regexString caseSensitive dirs = do
    grep <- getGrep Nothing
    let store = grepStore grep
    ideRef <- ask
    bringPaneToFront grep
    liftIO . forkIO . (`reflectIDE` ideRef) $ do
        liftIO $ do
            putMVar (waitingGrep grep) True
            putMVar (activeGrep grep) True
            takeMVar (waitingGrep grep)

        postSyncIDE $ forestStoreClear store

        totalFound <- foldM (\a dir -> do
            subDirs <- liftIO $ filter (\f ->
                   not ("." `isPrefixOf` f)
                && f `notElem` ["_darcs", "dist", "dist-newstyle", "vendor"]) <$> getDirectoryContents dir
            nooneWaiting <- liftIO $ isEmptyMVar (waitingGrep grep)
            found <- if nooneWaiting
                then do
                    (output, pid) <- liftIO $ runTool "grep" ((if caseSensitive then [] else ["-i"])
                        ++ ["-R", "-E", "-n", "-I",
                            "--exclude=*~",
                            "--exclude-dir=.svn",
                            "--exclude-dir=_darcs",
                            "--exclude-dir=.git",
                            regexString] ++ map T.pack subDirs) (Just dir) Nothing
                    output $$ do
                        let max = 1000
                        CL.isolate max =$ do
                            n <- setGrepResults dir
                            when (n >= max) $ do
                                liftIO $ debugM "leksah" "interrupting grep process"
                                liftIO $ interruptProcessGroupOf pid
                                  `catch` \(_::SomeException) -> return ()
                                lift $ postSyncIDE $ do
                                    nDir <- treeModelIterNChildren store Nothing
                                    p <- treePathNewFromIndices' [nDir-1]
                                    forestStoreChange store p (\r -> r{ context = __ "(Stoped Searching)" })
                                    return ()
                            CL.sinkNull
                            return n
                else return 0
            return $ a + found) 0 dirs

        nooneWaiting <- liftIO $ isEmptyMVar (waitingGrep grep)
        when nooneWaiting $ postSyncIDE $ do
            nDir <- treeModelIterNChildren store Nothing
            p <- treePathNewFromIndices' []
            forestStoreInsert store p (fromIntegral nDir) $ GrepRecord (T.unpack $ __ "Search Complete") totalFound "" Nothing

        void $ liftIO $ takeMVar (activeGrep grep)
    return ()


-- | A Sink for processing lines of grep output and counting them
setGrepResults :: FilePath -> C.Sink ToolOutput IDEM Int
setGrepResults dir = do
    ideRef <- lift ask
    grep <- lift $ getGrep Nothing
    log <- lift getLog
    defaultLogLaunch <- lift getDefaultLogLaunch
    let store = grepStore grep
        view  = treeView grep
    nDir <- lift $ postSyncIDE $ do
        nDir <- treeModelIterNChildren store Nothing
        p <- treePathNewFromIndices' []
        forestStoreInsert store p (fromIntegral nDir) $ GrepRecord dir 0 "" Nothing
        when (nDir == 0) (void $ widgetGrabFocus view)
        return nDir
    CL.foldM (\count line ->
        if isError line
            then do
                postSyncIDE $ void $ defaultLineLogger log defaultLogLaunch line
                return count
            else
                case process dir line of
                    Nothing     -> return count
                    Just record -> do
                        nooneWaiting <- liftIO $ isEmptyMVar (waitingGrep grep)
                        when nooneWaiting . postSyncIDE $ do
                            p <- treePathNewFromIndices' [nDir]
                            Just parent <- treeModelGetIter store p
                            n <- treeModelIterNChildren store (Just parent)
                            forestStoreInsert store p (fromIntegral n) record
                            forestStoreChange store p (\r -> r{ line = fromIntegral n+1 })
                            when (nDir == 0 && n == 0) $
                                treeViewExpandAll view
                        return (count+1)) 0
    where
        process pp (ToolOutput line) =
            case parse grepLineParser "" $ T.unpack line of
                Right record -> Just record{parDir = Just pp}
                _ -> Nothing
        process _ _ = Nothing

        isError (ToolExit ExitSuccess) = False
        isError (ToolExit (ExitFailure 1)) = False
        isError o = isNothing (process "" o)