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')