module IDE.Pane.Errors (
ErrorsPane
, ErrorsState
, fillErrorList
, getErrors
, addErrorToList
, selectMatchingErrors
) where
import Prelude ()
import Prelude.Compat
import Graphics.UI.Gtk
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,
treeStoreGetForest)
import Data.Text (dropWhileEnd, Text)
import Control.Applicative (Alternative(..))
import Control.Monad (filterM, foldM_, unless, void, when)
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 System.Glib.Properties (newAttrFromMaybeStringProperty)
import Data.Char (isSpace)
import Data.Tree (Forest, Tree(..), Tree)
import Data.Function.Compat ((&))
import System.Log.Logger (debugM)
import Data.Foldable (forM_)
data ErrorsPane = ErrorsPane {
vbox :: VBox
, scrolledView :: ScrolledWindow
, treeView :: TreeView
, errorStore :: TreeStore ErrorRecord
, autoClose :: IORef Bool
, errorsButton :: ToggleButton
, warningsButton :: ToggleButton
, suggestionsButton :: ToggleButton
, testFailsButton :: ToggleButton
} deriving Typeable
data ErrorRecord = ERLogRef LogRef
| ERPackage IDEPackage Text
| ERIDE Text
| ERFullMessage Text (Maybe LogRef)
deriving (Eq)
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 = castToWidget . vbox
paneId _b = "*Errors"
instance RecoverablePane ErrorsPane ErrorsState IDEM where
saveState ErrorsPane{..} = liftIO $ do
showErrors <- get errorsButton toggleButtonActive
showWarnings <- get warningsButton toggleButtonActive
showSuggestions <- get suggestionsButton toggleButtonActive
showTestFails <- get testFailsButton toggleButtonActive
return (Just ErrorsState{..})
recoverState pp ErrorsState{..} = do
nb <- getNotebook pp
mbErrors <- buildPane pp nb builder
forM_ mbErrors $ \ErrorsPane{..} -> liftIO $ do
set errorsButton [toggleButtonActive := showErrors]
set warningsButton [toggleButtonActive := showWarnings]
set suggestionsButton [toggleButtonActive := showSuggestions]
set testFailsButton [toggleButtonActive := showTestFails]
return mbErrors
builder = builder'
builder' :: PanePath ->
Notebook ->
Window ->
IDEM (Maybe ErrorsPane, Connections)
builder' _pp _nb _windows = reifyIDE $ \ ideR -> do
errorStore <- treeStoreNew []
vbox <- vBoxNew False 0
hbox <- hBoxNew False 0
boxPackStart vbox hbox PackNatural 0
errorsButton <- toggleButtonNewWithLabel (__ "Errors")
warningsButton <- toggleButtonNewWithLabel (__ "Warnings")
suggestionsButton <- toggleButtonNewWithLabel (__ "Suggestions")
testFailsButton <- toggleButtonNewWithLabel (__ "Test Failures")
set suggestionsButton [toggleButtonActive := False]
forM_ [errorsButton, warningsButton, suggestionsButton, testFailsButton] $ \b -> do
set b [toggleButtonActive := True]
boxPackStart hbox b PackNatural 3
b `on` toggled $ reflectIDE (fillErrorList False) ideR
boxPackStart vbox hbox PackNatural 0
treeView <- treeViewNew
treeViewSetModel treeView errorStore
set treeView
[ treeViewLevelIndentation := 20
, treeViewRulesHint := True
, treeViewHeadersVisible := False]
column <- treeViewColumnNew
iconRenderer <- cellRendererPixbufNew
cellLayoutPackStart column iconRenderer False
cellLayoutSetAttributes column iconRenderer errorStore
$ \row -> [ newAttrFromMaybeStringProperty "icon-name" := toIcon row]
treeViewColumnSetSizing column TreeViewColumnAutosize
renderer <- cellRendererTextNew
cellLayoutPackStart column renderer False
cellLayoutSetAttributeFunc column renderer errorStore $ \iter -> do
path <- treeModelGetPath errorStore iter
row <- treeModelGetRow errorStore iter
expanded <- treeViewRowExpanded treeView path
set renderer [cellText := toDescription expanded row]
treeViewAppendColumn treeView column
selB <- treeViewGetSelection treeView
treeSelectionSetMode selB SelectionMultiple
scrolledView <- scrolledWindowNew Nothing Nothing
scrolledWindowSetShadowType scrolledView ShadowIn
containerAdd scrolledView treeView
scrolledWindowSetPolicy scrolledView PolicyAutomatic PolicyAutomatic
boxPackStart vbox scrolledView PackGrow 0
autoClose <- newIORef False
let pane = ErrorsPane {..}
cid1 <- after treeView focusInEvent $ do
liftIO $ reflectIDE (makeActive pane) ideR
return True
(cid2, cid3) <- flip reflectIDE ideR $
treeViewContextMenu' treeView errorStore contextMenuItems
cid4 <- treeView `on` rowActivated $ \path col -> do
record <- treeStoreGetValue errorStore path
case record of
ERLogRef logRef -> errorsSelect ideR errorStore path col
ERFullMessage _ ref -> errorsSelect ideR errorStore path col
_ -> return ()
reflectIDE (fillErrorList' pane) ideR
return (Just pane, map ConnectC [cid1, cid2, cid3, cid4])
toIcon :: ErrorRecord -> Maybe Text
toIcon (ERLogRef logRef) =
case logRefType logRef of
ErrorRef -> Just "ide_error"
WarningRef -> Just "ide_warning"
LintRef -> Just "ide_suggestion"
TestFailureRef -> Just "software-update-urgent"
_ -> Nothing
toIcon (ERPackage _ _) = Just "dialog-error"
toIcon (ERIDE _) = Just "dialog-error"
toIcon (ERFullMessage _ _) = Nothing
toDescription :: Bool -> ErrorRecord -> Text
toDescription expanded errorRec =
case errorRec of
(ERLogRef logRef) -> formatExpandableMessage (T.pack $ logRefFilePath logRef) (refDescription logRef)
(ERIDE msg) -> formatExpandableMessage "" msg
(ERPackage pkg msg) -> formatExpandableMessage (packageIdentifierToString (ipdPackageId pkg))
(packageIdentifierToString (ipdPackageId pkg) <> ": \n" <> msg)
(ERFullMessage msg _) -> removeIndentation msg
where
formatExpandableMessage location msg
| expanded = location
| otherwise = location <> ": " <> msg & removeIndentation
& T.lines
& map removeTrailingWhiteSpace
& T.intercalate " "
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)) <> "..."
getErrors :: Maybe PanePath -> IDEM ErrorsPane
getErrors Nothing = forceGetPane (Right "*Errors")
getErrors (Just pp) = forceGetPane (Left pp)
fillErrorList :: Bool
-> IDEAction
fillErrorList False = getPane >>= maybe (return ()) fillErrorList'
fillErrorList True = getErrors Nothing >>= \ p -> fillErrorList' p >> displayPane p False
fillErrorList' :: ErrorsPane -> IDEAction
fillErrorList' pane = do
liftIO $ debugM "leksah" "fillErrorList'"
refs <- F.toList <$> readIDE errorRefs
visibleRefs <- liftIO $ filterM (isRefVisible pane) refs
ac <- liftIO $ readIORef (autoClose pane)
when (null refs && ac) . void $ closePane pane
updateFilterButtons pane
liftIO $ do
let store = errorStore pane
let view = treeView pane
treeStoreClear store
forM_ (zip visibleRefs [0..]) $ \(ref, n) -> do
treeStoreInsert store [] n (ERLogRef ref)
when (length (T.lines (refDescription ref)) > 1) $ do
treeStoreInsert store [n] 0 (ERFullMessage (refDescription ref) (Just ref))
treeViewExpandToPath view [n,0]
isRefVisible :: ErrorsPane -> LogRef -> IO 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
addErrorToList :: Bool
-> Int
-> 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
addErrorToList' :: Int -> LogRef -> ErrorsPane -> IDEAction
addErrorToList' unfilteredIndex ref pane = do
liftIO $ debugM "leksah" "addErrorToList'"
visible <- liftIO $ isRefVisible pane ref
updateFilterButtons pane
when visible $ do
refs <- F.toList <$> readIDE errorRefs
index <- liftIO $ length <$> filterM (isRefVisible pane) (take unfilteredIndex refs)
ac <- liftIO $ readIORef (autoClose pane)
liftIO $ do
let store = errorStore pane
let view = treeView pane
treeStoreInsert store [] index (ERLogRef ref)
when (length (T.lines (refDescription ref)) > 1) $ do
treeStoreInsert store [index] 0 (ERFullMessage (refDescription ref) (Just ref))
treeViewExpandToPath view [index,0]
updateFilterButtons :: ErrorsPane -> IDEAction
updateFilterButtons pane = do
liftIO $ debugM "leksah" "updateFilterButtons"
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
liftIO $ do
setLabel "Errors" numErrors (errorsButton pane)
setLabel "Warnings" numWarnings (warningsButton pane)
setLabel "Suggestions" numSuggestions (suggestionsButton pane)
setLabel "Test Failures" numTestFails (testFailsButton pane)
widgetShowAll (vbox pane)
getSelectedError :: TreeView
-> TreeStore ErrorRecord
-> IO (Maybe LogRef)
getSelectedError treeView store = do
liftIO $ debugM "leksah" "getSelectedError"
treeSelection <- treeViewGetSelection treeView
paths <- treeSelectionGetSelectedRows treeSelection
case paths of
path:_ -> do
val <- treeStoreGetValue store path
case val of
ERLogRef logRef -> return (Just logRef)
_ -> return Nothing
_ -> return Nothing
selectError :: Maybe LogRef
-> 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)
case mbLogRef of
Nothing -> do
empty <- null <$> treeStoreGetTree (errorStore errors) []
unless empty $
treeViewScrollToCell (treeView errors) (Just [0]) Nothing Nothing
treeSelectionUnselectAll selection
Just lr -> do
let store = errorStore errors
empty <- isNothing <$> treeModelGetIterFirst store
unless empty $ do
forest <- treeStoreGetForest store
let mbPath = forestFind forest (ERLogRef lr)
forM_ mbPath $ \path -> do
treeViewScrollToCell (treeView errors) (Just path) Nothing Nothing
treeSelectionSelectPath selection path
where
forestFind :: Eq a => Forest a -> a -> Maybe TreePath
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 -> TreeStore 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]
ERPackage _ msg -> [clipboardItem msg]
_ -> []
]
where clipboardItem str = ("Copy message to clipboard", liftIO $ clipboardGet selectionClipboard >>= flip clipboardSetText str)
errorsSelect :: IDERef
-> TreeStore ErrorRecord
-> TreePath
-> TreeViewColumn
-> IO ()
errorsSelect ideR store path _ = do
liftIO $ debugM "leksah" "errorsSelect"
record <- treeStoreGetValue store path
case record of
ERLogRef logRef -> reflectIDE (setCurrentError (Just logRef)) ideR
ERFullMessage _ (Just ref) -> reflectIDE (setCurrentError (Just ref)) ideR
_ -> return ()
selectMatchingErrors :: Maybe SrcSpan
-> IDEAction
selectMatchingErrors mbSpan = do
liftIO $ debugM "leksah" "selectMatchingErrors"
mbErrors <- getPane
forM_ mbErrors $ \pane -> do
treeSel <- liftIO $ treeViewGetSelection (treeView pane)
liftIO $ 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 =
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'
insideOf (SrcSpan _ lStart cStart lEnd cEnd) (SrcSpan _ lStart' cStart' lEnd' cEnd')
= (lStart, cStart) <= (lEnd', cEnd')
&& (lEnd, cEnd) >= (lStart', cStart')