packages feed

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

{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE FlexibleInstances, TypeSynonymInstances,
   MultiParamTypeClasses, DeriveDataTypeable, OverloadedStrings #-}
-----------------------------------------------------------------------------
--
-- Module      :  IDE.Pane.Errors
-- Copyright   :  2007-2011 Juergen Nicklisch-Franken, Hamish Mackenzie
-- License     :  GPL
--
-- Maintainer  :  maintainer@leksah.org
-- Stability   :  provisional
-- Portability :
--
-- | A pane which displays a list of errors
--
-----------------------------------------------------------------------------

module IDE.Pane.Errors (
    ErrorsPane
,   ErrorsState
,   fillErrorList
,   getErrors
,   addErrorToList
,   removeErrorsFromList
,   selectMatchingErrors
) where

import Prelude ()
import Prelude.Compat
import Data.Typeable (Typeable)
import IDE.Core.State
import IDE.ImportTool
       (resolveErrors, resolveMenuItems)
import Data.List (groupBy, sortBy, elemIndex)
import IDE.LogRef (showSourceSpan)
import Control.Monad.IO.Class (MonadIO(..))
import IDE.Utils.GUIUtils
       (treeViewContextMenu', treeViewContextMenu, __, treeViewToggleRow)
import Data.Text (dropWhileEnd, Text)
import Control.Applicative (Alternative(..))
import Control.Monad (filterM, foldM_, unless, void, when, forever)
import Control.Concurrent (forkIO, threadDelay, MVar, newEmptyMVar, takeMVar, tryPutMVar)
import qualified Data.Text as T
       (unlines, dropWhileEnd, unpack, pack, intercalate, lines,
        takeWhile, length, drop)
import Data.IORef (writeIORef, readIORef, newIORef, IORef)
import Data.Maybe (isJust, isNothing)
import qualified Data.Foldable as F (toList)
import qualified Data.Sequence as Seq (null, elemIndexL)
import Data.Monoid ((<>))
import Data.Ord (comparing)
import Data.Char (isSpace)
import Data.Tree (Forest, Tree(..), Tree)
import Data.Function.Compat ((&))
import System.Log.Logger (debugM)
import Data.Foldable (forM_)
import GI.Gtk.Objects.Box (boxNew, Box(..))
import GI.Gtk.Objects.ScrolledWindow
       (scrolledWindowSetPolicy, scrolledWindowSetShadowType,
        scrolledWindowNew, ScrolledWindow(..))
import GI.Gtk.Objects.TreeView
       (treeViewScrollToCell, treeViewExpandToPath,
        onTreeViewRowActivated, treeViewGetSelection, treeViewAppendColumn,
        treeViewRowExpanded, setTreeViewHeadersVisible, setTreeViewRulesHint,
        setTreeViewLevelIndentation, treeViewSetModel, treeViewNew,
        TreeView(..))
import GI.Gtk.Objects.ToggleButton
       (toggleButtonGetActive, onToggleButtonToggled,
        toggleButtonNewWithLabel, setToggleButtonActive, ToggleButton(..))
import GI.Gtk.Objects.Widget
       (widgetShowAll, afterWidgetFocusInEvent, toWidget)
import Data.GI.Base (set, get)
import GI.Gtk.Objects.Notebook (Notebook(..))
import GI.Gtk.Objects.Window (Window(..))
import Graphics.UI.Editor.Parameters (Packing(..), boxPackStart')
import GI.Gtk.Objects.TreeViewColumn
       (noTreeViewColumn, TreeViewColumn(..), treeViewColumnSetSizing,
        treeViewColumnNew)
import GI.Gtk.Objects.CellRendererPixbuf
       (setCellRendererPixbufIconName, cellRendererPixbufNew)
import GI.Gtk.Interfaces.CellLayout (cellLayoutPackStart)
import Data.GI.Gtk.ModelView.CellLayout
       (cellLayoutSetDataFunc', cellLayoutSetDataFunction)
import GI.Gtk.Enums
       (PolicyType(..), ShadowType(..), SelectionMode(..),
        TreeViewColumnSizing(..), Orientation(..))
import GI.Gtk.Objects.CellRendererText
       (setCellRendererTextText, cellRendererTextNew)
import GI.Gtk.Interfaces.TreeModel
       (treeModelGetIterFirst, treeModelGetPath)
import Data.GI.Gtk.ModelView.CustomStore (customStoreGetRow)
import GI.Gtk.Objects.TreeSelection
       (treeSelectionSelectPath, treeSelectionUnselectAll,
        treeSelectionSetMode)
import GI.Gtk.Objects.Adjustment (noAdjustment)
import GI.Gtk.Objects.Container (containerAdd)
import Control.Monad.Reader (MonadReader(..))
import Control.Monad.Trans.Class (MonadTrans(..))
import Data.GI.Gtk.ModelView.ForestStore
       (forestStoreRemove, forestStoreInsert, forestStoreClear,
        forestStoreNew, ForestStore(..), forestStoreGetTree,
        forestStoreGetValue, forestStoreGetForest)
import GI.Gtk.Objects.Button (buttonSetLabel)
import GI.Gtk.Structs.TreePath
       (TreePath(..))
import GI.Gtk.Objects.Clipboard (clipboardSetText, clipboardGet)
import GI.Gdk.Structs.Atom (atomIntern)
import Data.Int (Int32)
import Data.GI.Gtk.ModelView.Types
       (treeSelectionGetSelectedRows', treePathNewFromIndices')
import GI.Gtk (getToggleButtonActive)
import Data.Time.Clock (getCurrentTime, diffUTCTime)


-- | The representation of the Errors pane
data ErrorsPane      =   ErrorsPane {
    vbox              :: Box
,   scrolledView      :: ScrolledWindow
,   treeView          :: TreeView
,   errorStore        :: ForestStore ErrorRecord
,   autoClose         :: IORef Bool -- ^ If the pane was only displayed to show current error
,   errorsButton      :: ToggleButton
,   warningsButton    :: ToggleButton
,   suggestionsButton :: ToggleButton
,   testFailsButton   :: ToggleButton
,   updateButtons     :: MVar ()
} deriving Typeable


-- | The data for a single row in the Errors pane
data ErrorRecord = ERLogRef LogRef
                 | ERIDE Text
                 | ERFullMessage Text (Maybe LogRef)
    deriving (Eq)

-- | The additional state used when recovering the pane
data ErrorsState = ErrorsState
    {
      showErrors :: Bool
    , showWarnings :: Bool
    , showSuggestions :: Bool
    , showTestFails :: Bool
    }
   deriving (Eq,Ord,Read,Show,Typeable)


instance Pane ErrorsPane IDEM
    where
    primPaneName _  =   __ "Errors"
    getTopWidget    =   liftIO . toWidget . vbox
    paneId _b       =   "*Errors"


instance RecoverablePane ErrorsPane ErrorsState IDEM where
    saveState ErrorsPane{..} = do
        showErrors      <- getToggleButtonActive errorsButton
        showWarnings    <- getToggleButtonActive warningsButton
        showSuggestions <- getToggleButtonActive suggestionsButton
        showTestFails   <- getToggleButtonActive testFailsButton
        return (Just ErrorsState{..})

    recoverState pp ErrorsState{..} = do
        nb <- getNotebook pp
        mbErrors <- buildPane pp nb builder
        forM_ mbErrors $ \ErrorsPane{..} -> do
            setToggleButtonActive errorsButton      showErrors
            setToggleButtonActive warningsButton    showWarnings
            setToggleButtonActive suggestionsButton showSuggestions
            setToggleButtonActive testFailsButton   showTestFails
        return mbErrors


    builder = builder'

-- | Builds an 'ErrorsPane' pane together with a list of
--   event 'Connections'
builder' :: PanePath ->
    Notebook ->
    Window ->
    IDEM (Maybe ErrorsPane, Connections)
builder' _pp _nb _windows = do
    ideR <- ask
    errorStore   <- forestStoreNew []

    vbox         <- boxNew OrientationVertical 0

    -- Top box with buttons
    hbox <- boxNew OrientationHorizontal 0
    boxPackStart' vbox hbox PackNatural 0


    errorsButton <- toggleButtonNewWithLabel (__ "Errors")
    warningsButton <- toggleButtonNewWithLabel (__ "Warnings")
    suggestionsButton <- toggleButtonNewWithLabel (__ "Suggestions")
    testFailsButton <- toggleButtonNewWithLabel (__ "Test Failures")
    setToggleButtonActive suggestionsButton False

    forM_ [errorsButton, warningsButton, suggestionsButton, testFailsButton] $ \b -> do
        setToggleButtonActive b True
        boxPackStart' hbox b PackNatural 3
        onToggleButtonToggled b $ reflectIDE (fillErrorList False) ideR

    -- TreeView for bottom part of vbox

    treeView     <- treeViewNew
    treeViewSetModel treeView (Just errorStore)
    setTreeViewLevelIndentation treeView 20
    setTreeViewRulesHint        treeView True
    setTreeViewHeadersVisible   treeView False

    column       <- treeViewColumnNew
    iconRenderer <- cellRendererPixbufNew

    cellLayoutPackStart column iconRenderer False
    cellLayoutSetDataFunction column iconRenderer errorStore
        $ setCellRendererPixbufIconName iconRenderer . toIcon


    treeViewColumnSetSizing column TreeViewColumnSizingAutosize

    renderer <- cellRendererTextNew
    cellLayoutPackStart column renderer False

    cellLayoutSetDataFunc' column renderer errorStore $ \iter -> do
        path <- treeModelGetPath errorStore iter
        row <- customStoreGetRow errorStore iter
        expanded <- treeViewRowExpanded treeView path
        setCellRendererTextText renderer $ toDescription expanded row

    treeViewAppendColumn treeView column


    selB <- treeViewGetSelection treeView
    treeSelectionSetMode selB SelectionModeMultiple
    scrolledView <- scrolledWindowNew noAdjustment noAdjustment
    scrolledWindowSetShadowType scrolledView ShadowTypeIn
    containerAdd scrolledView treeView
    scrolledWindowSetPolicy scrolledView PolicyTypeAutomatic PolicyTypeAutomatic
    boxPackStart' vbox scrolledView PackGrow 0

    autoClose <- liftIO $ newIORef False

    updateButtons <- liftIO newEmptyMVar

    let pane = ErrorsPane {..}
    cid1 <- onIDE afterWidgetFocusInEvent treeView $ do
        liftIDE $ makeActive pane
        return True
    cids2 <- treeViewContextMenu' treeView errorStore contextMenuItems
    cid4 <- ConnectC treeView <$> onTreeViewRowActivated treeView (\path col -> do
        record <- forestStoreGetValue errorStore path
        case record of
            ERLogRef logRef -> errorsSelect ideR errorStore path col
            ERFullMessage _ ref -> errorsSelect ideR errorStore path col
            _        -> return ())

    fillErrorList' pane
    liftIO . forkIO . forever $ do
        takeMVar updateButtons
        reflectIDE (postSyncIDE (doUpdateFilterButtons pane)) ideR
        threadDelay 200000
    return (Just pane, [cid1, cid4] ++ cids2)


toIcon :: ErrorRecord -> Text
toIcon (ERLogRef logRef) =
    case logRefType logRef of
        ErrorRef       -> "ide_error"
        WarningRef     -> "ide_warning"
        LintRef        -> "ide_suggestion"
        TestFailureRef -> "software-update-urgent"
        _              -> ""
toIcon (ERIDE _) = "dialog-error"
toIcon (ERFullMessage _ _) = ""


toDescription :: Bool -> ErrorRecord -> Text
toDescription expanded errorRec =
    case errorRec of
        (ERLogRef logRef)   -> formatExpandableMessage (T.pack $ logRefFilePath logRef) (refDescription logRef)
        (ERIDE msg)         -> formatExpandableMessage "" msg
        (ERFullMessage msg _) -> removeIndentation (cutOffAt 8192 msg)

    where
        formatExpandableMessage location msg
            | expanded  = location
            | otherwise = location <> ": " <> msg & cutOffAt 2048
                                                  & removeIndentation
                                                  & T.lines
                                                  & map removeTrailingWhiteSpace
                                                  & T.intercalate " "


-- | Removes the unnecessary indentation
removeIndentation :: Text -> Text
removeIndentation t = T.intercalate "\n" $ map (T.drop minIndent) l
  where
    l = T.lines t
    minIndent = minimum $ map (T.length . T.takeWhile (== ' ')) l

removeTrailingWhiteSpace :: Text -> Text
removeTrailingWhiteSpace = T.dropWhileEnd isSpace

cutOffAt :: Int -> Text -> Text
cutOffAt n t | T.length t < n = t
             | otherwise      = T.pack (take n (T.unpack t)) <> "..."

-- | Get the Errors pane
getErrors :: Maybe PanePath -> IDEM ErrorsPane
getErrors Nothing    = forceGetPane (Right "*Errors")
getErrors (Just pp)  = forceGetPane (Left pp)

timeIt :: String -> IDEM a -> IDEM a
timeIt name f = do
    liftIO $ debugM "leksah" name
    start <- liftIO getCurrentTime
    result <- f
    end <- liftIO getCurrentTime
    liftIO $ debugM "leksah" $ name <> " took " <> show (diffUTCTime end start)
    return result

-- | Repopulates the Errors pane
fillErrorList :: Bool -- ^ Whether to display the Errors pane
              -> IDEAction
fillErrorList False = getPane >>= maybe (return ()) fillErrorList'
fillErrorList True = getErrors Nothing  >>= \ p -> fillErrorList' p >> displayPane p False

-- | Fills the pane with the error list from the IDE state
fillErrorList' :: ErrorsPane -> IDEAction
fillErrorList' pane = timeIt "fillErrorList'" $ do
    refs <- F.toList <$> readIDE errorRefs
    visibleRefs <- filterM (isRefVisible pane) refs

    ac   <- liftIO $ readIORef (autoClose pane)
    when (null refs && ac) . void $ closePane pane

    updateFilterButtons pane
    let store = errorStore pane
    let view  = treeView pane
    forestStoreClear store
    forM_ (zip visibleRefs [0..]) $ \(ref, n) -> do
        emptyPath <- treePathNewFromIndices' []
        forestStoreInsert store emptyPath n (ERLogRef ref)
        when (length (T.lines (refDescription ref)) > 1) $ do
            p <- treePathNewFromIndices' [fromIntegral n]
            forestStoreInsert store p 0 (ERFullMessage (refDescription ref) (Just ref))
            collapse <- collapseErrors <$> readIDE prefs
            unless collapse $ do
                treeViewExpandToPath view =<< treePathNewFromIndices' [fromIntegral n,0]

-- | Returns whether the `LogRef` should be visible in the errors pane
isRefVisible :: MonadIO m => ErrorsPane -> LogRef -> m Bool
isRefVisible pane ref =
    case logRefType ref of
        ErrorRef       -> toggleButtonGetActive (errorsButton pane)
        WarningRef     -> toggleButtonGetActive (warningsButton pane)
        LintRef        -> toggleButtonGetActive (suggestionsButton pane)
        TestFailureRef -> toggleButtonGetActive (testFailsButton pane)
        _              -> return False

-- | Add any LogRef to the Errors pane at a given index
addErrorToList :: Bool -- ^ Whether to display the pane
               -> Int  -- ^ The index to insert at
               -> LogRef
               -> IDEAction
addErrorToList False index lr = getPane >>= maybe (return ()) (addErrorToList' index lr)
addErrorToList True  index lr = getErrors Nothing  >>= \ p -> addErrorToList' index lr p >> displayPane p False

-- | Add a 'LogRef' at a specific index to the Errors pane
addErrorToList' :: Int -> LogRef -> ErrorsPane -> IDEAction
addErrorToList' unfilteredIndex ref pane = timeIt "addErrorToList'" $ do
    visible <- isRefVisible pane ref
    updateFilterButtons pane
    when visible $ do
        refs <- F.toList <$> readIDE errorRefs
        index <- length <$> filterM (isRefVisible pane) (take unfilteredIndex refs)
        ac   <- liftIO $ readIORef (autoClose pane)
        let store = errorStore pane
        let view  = treeView pane
        emptyPath <- treePathNewFromIndices' []
        forestStoreInsert store emptyPath index (ERLogRef ref)
        when (length (T.lines (refDescription ref)) > 1) $ do
            p <- treePathNewFromIndices' [fromIntegral index]
            forestStoreInsert store p 0 (ERFullMessage (refDescription ref) (Just ref))
            collapse <- collapseErrors <$> readIDE prefs
            unless collapse $
                treeViewExpandToPath view =<< treePathNewFromIndices' [fromIntegral index,0]
        when (index == 0) $ do
            path <- treePathNewFromIndices' [0]
            treeViewScrollToCell view (Just path) noTreeViewColumn False 0 0

-- | Add any LogRef to the Errors pane at a given index
removeErrorsFromList :: Bool -- ^ Whether to display the pane
                     -> (LogRef -> Bool)
                     -> IDEAction
removeErrorsFromList False toRemove = getPane >>= maybe (return ()) (removeErrorsFromList' toRemove)
removeErrorsFromList True  toRemove = getErrors Nothing  >>= \ p -> removeErrorsFromList' toRemove p >> displayPane p False


-- | Add a 'LogRef' at a specific index to the Errors pane
removeErrorsFromList' :: (LogRef -> Bool) -> ErrorsPane -> IDEAction
removeErrorsFromList' toRemove pane = timeIt "removeErrorsFromList" $ do
    let store = errorStore pane
    trees <- forestStoreGetForest store
    updateFilterButtons pane
    let refsToRemove = filter (treeToRemove . snd) $ zip [0..] trees
    forM_ (map fst $ reverse refsToRemove) $ \index ->
        forestStoreRemove store =<< treePathNewFromIndices' [fromIntegral index]
  where
    treeToRemove (Node (ERLogRef ref) _) = toRemove ref
    treeToRemove _ = False

updateFilterButtons :: ErrorsPane -> IDEAction
updateFilterButtons pane = void . liftIO $ tryPutMVar (updateButtons pane) ()

-- | Updates the filter buttons in the Error Pane
doUpdateFilterButtons :: ErrorsPane -> IDEAction
doUpdateFilterButtons pane = timeIt "updateFilterButtons" $ do
    let numRefs refType = length . filter ((== refType) . logRefType) . F.toList <$> readIDE errorRefs
    let setLabel name amount button = buttonSetLabel button (name <> " (" <> T.pack (show amount) <> ")" )

    numErrors      <- numRefs ErrorRef
    numWarnings    <- numRefs WarningRef
    numSuggestions <- numRefs LintRef
    numTestFails   <- numRefs TestFailureRef

    setLabel "Errors"        numErrors      (errorsButton      pane)
    setLabel "Warnings"      numWarnings    (warningsButton    pane)
    setLabel "Suggestions"   numSuggestions (suggestionsButton pane)
    setLabel "Test Failures" numTestFails   (testFailsButton   pane)


-- | Get the currently selected error
getSelectedError ::  TreeView
    -> ForestStore ErrorRecord
    -> IO (Maybe LogRef)
getSelectedError treeView store = do
    liftIO $ debugM "leksah" "getSelectedError"
    treeSelection   <-  treeViewGetSelection treeView
    paths           <-  treeSelectionGetSelectedRows' treeSelection
    case paths of
        path:_ ->  do
            val     <-  forestStoreGetValue store path
            case val of
                ERLogRef logRef -> return (Just logRef)
                _ -> return Nothing
        _  ->  return Nothing

-- | Select a 'LogRef' in the Errors pane if it is visible
selectError :: Maybe LogRef -- ^ When @Nothing@, the first row in the list is selected
            -> IDEAction
selectError mbLogRef = do
    liftIO $ debugM "leksah" "selectError"
    (mbPane :: Maybe ErrorsPane) <- getPane
    errors     <- getErrors Nothing
    when (isNothing mbPane) $ do
        liftIO $ writeIORef (autoClose errors) True
        displayPane errors False
    reifyIDE $ \ideR -> do
        selection <- treeViewGetSelection (treeView errors)
        forest <- forestStoreGetForest (errorStore errors)
        case mbLogRef of
            Nothing -> do
                unless (null forest) $ do
                    childPath <- treePathNewFromIndices' [0]
                    treeViewScrollToCell (treeView errors) (Just childPath) noTreeViewColumn False 0.0 0.0
                treeSelectionUnselectAll selection
            Just lr -> do
                let mbPath = forestFind forest (ERLogRef lr)
                forM_ mbPath $ \path' -> do
                    path <- treePathNewFromIndices' path'
                    treeViewScrollToCell (treeView errors) (Just path) noTreeViewColumn False 0.0 0.0
                    treeSelectionSelectPath selection path

    where
        forestFind :: Eq a => Forest a -> a -> Maybe [Int32]
        forestFind = forestFind' [0]
            where
                forestFind' path [] _ = Nothing
                forestFind' path (Node x trees : forest) y
                    | x == y    = Just path
                    | otherwise = forestFind' (path ++ [0]) trees y
                                      <|> forestFind' (sibling path) forest y

                sibling [n] = [n+1]
                sibling (x:xs) = x:sibling xs
                sibling [] = error "Error in selectError sibling function"

contextMenuItems :: ErrorRecord -> TreePath -> ForestStore ErrorRecord -> IDEM [[(Text, IDEAction)]]
contextMenuItems record path store = return
    [("Resolve Errors", resolveErrors) :
        case record of
               ERLogRef logRef -> resolveMenuItems logRef ++ [clipboardItem (refDescription logRef)]
               ERIDE msg       -> [clipboardItem msg]
               _               -> []
    ]
  where
    clipboardItem str = ("Copy message to clipboard",
            atomIntern "CLIBPOARD" False >>= clipboardGet >>= (\c -> clipboardSetText c str (-1)))


-- | Highlight an error refered to by the 'TreePath' in the given 'TreeViewColumn'
errorsSelect :: IDERef
                -> ForestStore ErrorRecord
                -> TreePath
                -> TreeViewColumn
                -> IO ()
errorsSelect ideR store path _ = do
    liftIO $ debugM "leksah" "errorsSelect"
    record <- forestStoreGetValue store path
    case record of
        ERLogRef logRef -> reflectIDE (setCurrentError (Just logRef)) ideR
        ERFullMessage _ (Just ref) -> reflectIDE (setCurrentError (Just ref)) ideR
        _ -> return ()


-- | Select the matching errors for a 'SrcSpan' in the Errors
--   pane, or none at all
selectMatchingErrors :: Maybe SrcSpan -- ^ When @Nothing@, unselects any errors in the pane
                     -> IDEAction
selectMatchingErrors mbSpan = do
    liftIO $ debugM "leksah" "selectMatchingErrors"
    mbErrors <- getPane
    forM_ mbErrors $ \pane -> do
        treeSel <- treeViewGetSelection (treeView pane)
        treeSelectionUnselectAll treeSel
        forM_ mbSpan $ \span -> do
            spans <- map logRefSrcSpan . F.toList <$> readIDE errorRefs
            matches <- matchingRefs span . F.toList <$> readIDE errorRefs
            forM_ matches $ \ref ->
                selectError (Just ref)

matchingRefs :: SrcSpan -> [LogRef] -> [LogRef]
matchingRefs span refs =
    -- the path of the SrcSpan in the LogRef absolute, so comparison with the given SrcSpan goes right
    let toAbsolute ref =  ref {logRefSrcSpan = (logRefSrcSpan ref) {srcSpanFilename = logRefFullFilePath ref}}
    in filter (\ref -> filesMatch (logRefSrcSpan (toAbsolute ref)) span && span `insideOf` logRefSrcSpan (toAbsolute ref)) refs
    where
        filesMatch span span' = srcSpanFilename span == srcSpanFilename span'

        -- Test whether the first span is inside of the second
        insideOf (SrcSpan _ lStart cStart lEnd cEnd) (SrcSpan _ lStart' cStart' lEnd' cEnd')
            =  (lStart, cStart) <= (lEnd', cEnd')
            && (lEnd, cEnd)     >= (lStart', cStart')