packages feed

leksah-0.8.0.4: src/IDE/Workspaces.hs

{-# OPTIONS_GHC -XScopedTypeVariables #-}
-----------------------------------------------------------------------------
--
-- Module      :  IDE.Workspace
-- Copyright   :  2007-2010 Juergen Nicklisch-Franken, Hamish Mackenzie
-- License     :  GPL
--
-- Maintainer  :  maintainer@leksah.org
-- Stability   :  provisional
-- Portability :
--
-- | Represents a workspace, a work unit, which can be composed of multiple packages
--
-----------------------------------------------------------------------------

module IDE.Workspaces (
    workspaceNew
,   workspaceOpen
,   workspaceOpenThis
,   workspaceClose
,   workspaceClean
,   workspaceMake
,   workspaceActivatePackage
,   workspaceAddPackage
,   workspaceAddPackage'
,   workspaceRemovePackage
,   workspacePackageNew

,   makePackage
,   backgroundMake
,   calculateReverseDependencies
) where

import IDE.Core.State
import Control.Monad.Trans (liftIO)
import Graphics.UI.Editor.Parameters
    (Parameter(..), (<<<-), paraName, emptyParams)
import Control.Monad (unless, when)
import Data.Maybe (isJust,fromJust )
import IDE.Utils.GUIUtils
    (chooseFile, chooseSaveFile)
import System.FilePath
       ((</>), isAbsolute, dropFileName, makeRelative, dropExtension,
        takeBaseName, addExtension, takeExtension)
import Text.PrinterParser
    (readFields,
     writeFields,
     readParser,
     stringParser,
     intParser,
     mkFieldS,
     FieldDescriptionS(..))
import qualified Text.PrettyPrint as  PP (text)
import Graphics.UI.Gtk
    (widgetDestroy, dialogRun, messageDialogNew, Window(..))
import IDE.Pane.PackageEditor (packageNew', choosePackageFile)
import Data.List ((\\), foldl', nub, delete)
import IDE.Package
       (packageConfig', getModuleTemplate, getPackageDescriptionAndPath,
        buildPackage, packageInstall', packageClean, activatePackage,
        deactivatePackage, idePackageFromPath)
import System.Directory
       (createDirectoryIfMissing, doesFileExist, canonicalizePath)
import System.Time (getClockTime)
import Graphics.UI.Gtk.Windows.MessageDialog
    (ButtonsType(..), MessageType(..))
import Graphics.UI.Gtk.General.Structs (ResponseId(..))
import Control.Exception (SomeException(..))
import Control.Monad.Reader.Class (ask)
import Data.Map (Map(..))
import qualified Data.Map as  Map
    (lookup, empty, mapWithKey, fromList, map)
import Distribution.Package (pkgVersion, pkgName, Dependency(..))
import Distribution.Version (withinRange)
import qualified GHC.List as  List (or)
import IDE.Pane.SourceBuffer
       (fileOpenThis, belongsToPackage, fileCheckAll)
import qualified System.IO.UTF8 as UTF8 (writeFile)
import Debug.Trace (trace)


setWorkspace :: Maybe Workspace -> IDEAction
setWorkspace mbWs = trace "setWorkspace" $ do
    let mbRealWs = case mbWs of
                        Nothing -> Nothing
                        Just ws -> Just ws{wsReverseDeps = calculateReverseDependencies ws}
    mbOldWs <- readIDE workspace
    modifyIDE_ (\ide -> ide{workspace = mbRealWs})
    let pack =  case mbRealWs of
                    Nothing -> Nothing
                    Just ws -> wsActivePack ws
    let oldPack = case mbOldWs of
                    Nothing -> Nothing
                    Just ws -> wsActivePack ws
    when (pack /= oldPack) $
            case pack of
                Nothing -> deactivatePackage
                Just p  -> activatePackage pack >> return ()
    mbPack <- readIDE activePack
    let wsStr = case mbRealWs of
                    Nothing -> ""
                    Just ws -> wsName ws
    let txt = wsStr ++ " > " ++
                    (case mbPack of
                            Nothing -> ""
                            Just p  -> packageIdentifierToString (ipdPackageId p))
    triggerEventIDE (StatusbarChanged [CompartmentPackage txt])
    triggerEventIDE (WorkspaceChanged True True)
    triggerEventIDE UpdateWorkspaceInfo
    return ()


-- ---------------------------------------------------------------------
-- This needs to be incremented, when the workspace format changes
--
workspaceVersion :: Int
workspaceVersion = 1

-- | Constructs a new workspace and makes it the current workspace
workspaceNew :: IDEAction
workspaceNew = do
    window <- getMainWindow
    mbFile <- liftIO $ do
        chooseSaveFile window "New file for wokspace" Nothing
    case mbFile of
        Nothing -> return ()
        Just filePath ->
            let realPath = if takeExtension filePath == leksahWorkspaceFileExtension
                                    then filePath
                                    else addExtension filePath leksahWorkspaceFileExtension
            in do
                cPath <- liftIO $ canonicalizePath realPath
                let newWorkspace = emptyWorkspace {
                                    wsName = takeBaseName cPath,
                                    wsFile = cPath}
                liftIO $ writeFields cPath newWorkspace workspaceDescr
                workspaceOpenThis False (Just cPath)
                return ()

workspaceOpen :: IDEAction
workspaceOpen = do
    window     <- getMainWindow
    mbFilePath <- liftIO $ chooseWorkspaceFile window
    workspaceOpenThis True mbFilePath
    return ()


chooseWorkspaceFile :: Window -> IO (Maybe FilePath)
chooseWorkspaceFile window = chooseFile window "Select leksah workspace file (.lkshw)" Nothing

workspaceOpenThis :: Bool -> Maybe FilePath -> IDEAction
workspaceOpenThis askForSession mbFilePath =
    case mbFilePath of
        Nothing -> return ()
        Just filePath -> do
            let spath =  dropExtension filePath ++ leksahSessionFileExtension
            workspaceClose
            exists <- liftIO $ doesFileExist spath
            wantToLoadSession <-
                if exists && askForSession
                    then liftIO $ do
                        md  <- messageDialogNew Nothing [] MessageQuestion ButtonsYesNo
                                $ "Load the session settings stored with this workspace?"
                        rid <- dialogRun md
                        widgetDestroy md
                        case rid of
                            ResponseYes ->  return True
                            otherwise   ->  return False
                    else return False
            if wantToLoadSession
                then triggerEventIDE (LoadSession spath) >> return ()
                else do
                    ideR <- ask
                    catchIDE (do
                        workspace <- readWorkspace filePath
                        setWorkspace (Just workspace {wsFile = filePath})
                        return ())
                           (\ (e :: SomeException) -> reflectIDE
                                (ideMessage Normal ("Can't load workspace file " ++ filePath ++ "\n" ++ show e)) ideR)


-- | Closes a workspace
workspaceClose :: IDEAction
workspaceClose = do
    oldWorkspace <- readIDE workspace
    case oldWorkspace of
        Nothing -> return ()
        Just ws -> do
            let oldActivePack = wsActivePack ws
            triggerEventIDE (SaveSession ((dropExtension (wsFile ws))
                                ++  leksahSessionFileExtension))
            addRecentlyUsedWorkspace (wsFile ws)
            setWorkspace Nothing
            when (isJust oldActivePack) $ do
                triggerEventIDE (Sensitivity [(SensitivityProjectActive, False),
                    (SensitivityWorkspaceOpen, False)])
                return ()
            return ()
    return ()

workspacePackageNew :: IDEAction
workspacePackageNew = do
    mbWs <- readIDE workspace
    case mbWs of
        Nothing -> ideMessage Normal "Needs an open workspace"
        Just ws -> do
            let path = dropFileName (wsFile ws)
            packageNew' (Just path) (\fp -> do
                window     <-  getMainWindow
                workspaceAddPackage' fp >> return ()
                mbPack <- idePackageFromPath fp
                constructAndOpenMainModule mbPack
                triggerEventIDE UpdateWorkspaceInfo >> return ())

constructAndOpenMainModule :: Maybe IDEPackage -> IDEAction
constructAndOpenMainModule Nothing = return ()
constructAndOpenMainModule (Just idePackage) =
    case (ipdMain idePackage, ipdSrcDirs idePackage) of
        ([target],[path]) -> do
            mbPD <- getPackageDescriptionAndPath
            case mbPD of
                Just (pd,_) -> do
                    liftIO $ createDirectoryIfMissing True path
                    alreadyExists <- liftIO $ doesFileExist (path </> target)
                    if alreadyExists
                        then ideMessage Normal "Main file already exists"
                        else do
                            template <- liftIO $ getModuleTemplate pd (dropExtension target)
                            liftIO $ UTF8.writeFile (path </> target) template
                            fileOpenThis (path </> target)
                Nothing     -> ideMessage Normal "No package description"
        _ -> return ()


workspaceAddPackage :: IDEAction
workspaceAddPackage = do
    mbWs <- readIDE workspace
    case mbWs of
        Nothing -> ideMessage Normal "Needs an open workspace"
        Just ws -> do
            let path = dropFileName (wsFile ws)
            window <-  getMainWindow
            mbFilePath <- liftIO $ choosePackageFile window (Just path)
            case mbFilePath of
                Nothing -> return ()
                Just fp -> workspaceAddPackage' fp >> return ()

workspaceAddPackage' :: FilePath -> IDEM (Maybe IDEPackage)
workspaceAddPackage' fp = do
    mbWs <- readIDE workspace
    cfp <-  liftIO $ canonicalizePath fp
    case mbWs of
        Nothing -> return Nothing
        Just ws -> do
            mbPack <- idePackageFromPath cfp
            case mbPack of
                Just pack -> do
                    unless (elem cfp (map ipdCabalFile (wsPackages ws))) $
                        writeWorkspace $ ws {wsPackages =  pack : wsPackages ws,
                                            wsActivePack =  Just pack}
                    return (Just pack)
                Nothing -> return Nothing


workspaceRemovePackage :: IDEPackage -> IDEAction
workspaceRemovePackage pack = do
    mbWs <- readIDE workspace
    case mbWs of
        Nothing -> return ()
        Just ws -> do
            when (elem pack (wsPackages ws)) $
                writeWorkspace ws {wsPackages =  delete pack (wsPackages ws)}
            return ()

workspaceActivatePackage :: IDEPackage -> IDEAction
workspaceActivatePackage pack = do
    activatePackage (Just pack)
    mbWs <- readIDE workspace
    case mbWs of
        Nothing -> return ()
        Just ws -> do
            when (elem pack (wsPackages ws)) $ do
                writeWorkspace ws {wsActivePack =  Just pack}
                return ()
            return ()

writeWorkspace :: Workspace -> IDEAction
writeWorkspace ws = do
    timeNow      <- liftIO getClockTime
    let newWs    =  ws {wsSaveTime = show timeNow,
                         wsVersion = workspaceVersion,
                         wsActivePackFile = case wsActivePack ws of
                                                Nothing -> Nothing
                                                Just pack -> Just (ipdCabalFile pack),
                         wsPackagesFiles = map ipdCabalFile (wsPackages ws)}
    setWorkspace $ Just newWs
    newWs' <- liftIO $ makePathesRelative newWs
    liftIO $ writeFields (wsFile newWs') (newWs' {wsFile = ""}) workspaceDescr

readWorkspace :: FilePath -> IDEM Workspace
readWorkspace fp = do
    ws <- liftIO $ readFields fp workspaceDescr emptyWorkspace
    ws' <- liftIO $ makePathesAbsolute ws fp
    activePack <- case wsActivePackFile ws' of
                        Nothing ->  return Nothing
                        Just fp ->  idePackageFromPath fp
    packages <- mapM idePackageFromPath (wsPackagesFiles ws')
    return ws'{ wsActivePack = activePack, wsPackages = map fromJust $ filter isJust $ packages}

makePathesRelative :: Workspace -> IO Workspace
makePathesRelative ws = do
    wsFile'                     <-  canonicalizePath (wsFile ws)
    wsActivePackFile'           <-  case wsActivePackFile ws of
                                        Nothing -> return Nothing
                                        Just fp -> do
                                            nfp <- canonicalizePath fp
                                            return (Just (makeRelative (dropFileName wsFile') nfp))
    wsPackagesFiles'            <-  mapM canonicalizePath (wsPackagesFiles ws)
    let relativePathes          =   map (\p -> makeRelative (dropFileName wsFile') p) wsPackagesFiles'
    return ws {wsActivePackFile = wsActivePackFile', wsFile = wsFile', wsPackagesFiles = relativePathes}

makePathesAbsolute :: Workspace -> FilePath -> IO Workspace
makePathesAbsolute ws bp = do
    wsFile'                     <-  canonicalizePath bp
    wsActivePackFile'           <-  case wsActivePackFile ws of
                                        Nothing -> return Nothing
                                        Just fp -> do
                                            fp' <- makeAbsolute (dropFileName wsFile') fp
                                            return (Just fp')
    wsPackagesFiles'            <-  mapM (makeAbsolute (dropFileName wsFile')) (wsPackagesFiles ws)
    return ws {wsActivePackFile = wsActivePackFile', wsFile = wsFile', wsPackagesFiles = wsPackagesFiles'}
    where
        makeAbsolute basePath relativePath  =
            if isAbsolute relativePath
                then canonicalizePath relativePath
                else canonicalizePath (basePath </> relativePath)

emptyWorkspace =  Workspace {
    wsVersion       =   workspaceVersion
,   wsSaveTime      =   ""
,   wsName          =   ""
,   wsFile          =   ""
,   wsPackages      =   []
,   wsActivePack    =   Nothing
,   wsPackagesFiles =   []
,   wsActivePackFile =   Nothing
,   wsReverseDeps   =   Map.empty
}

workspaceDescr :: [FieldDescriptionS Workspace]
workspaceDescr = [
        mkFieldS
            (paraName <<<- ParaName "Version of workspace file format" $ emptyParams)
            (PP.text . show)
            intParser
            wsVersion
            (\ b a -> a{wsVersion = b})
    ,   mkFieldS
            (paraName <<<- ParaName "Time of storage" $ emptyParams)
            (PP.text . show)
            stringParser
            wsSaveTime
            (\ b a -> a{wsSaveTime = b})
    ,   mkFieldS
            (paraName <<<- ParaName "Name of the workspace" $ emptyParams)
            (PP.text . show)
            stringParser
            wsName
            (\ b a -> a{wsName = b})
    ,   mkFieldS
            (paraName <<<- ParaName "File paths of contained packages" $ emptyParams)
            (PP.text . show)
            readParser
            wsPackagesFiles
            (\b a -> a{wsPackagesFiles = b})
    ,   mkFieldS
            (paraName <<<- ParaName "Maybe file path of an active package" $ emptyParams)
            (PP.text . show)
            readParser
            wsActivePackFile
            (\fp a -> a{wsActivePackFile = fp})]


addRecentlyUsedWorkspace :: FilePath -> IDEAction
addRecentlyUsedWorkspace fp = do
    state <- readIDE currentState
    when (not $ isStartingOrClosing state) $ do
        recentWorkspaces' <- readIDE recentWorkspaces
        unless (elem fp recentWorkspaces') $
            modifyIDE_ (\ide -> ide{recentWorkspaces = take 12 (fp : recentWorkspaces')})
        triggerEventIDE UpdateRecent
        return ()

removeRecentlyUsedWorkspace :: FilePath -> IDEAction
removeRecentlyUsedWorkspace fp = do
    state <- readIDE currentState
    when (not $ isStartingOrClosing state) $ do
        recentWorkspaces' <- readIDE recentWorkspaces
        when (elem fp recentWorkspaces') $
            modifyIDE_ (\ide -> ide{recentWorkspaces = filter (\e -> e /= fp) recentWorkspaces'})
        triggerEventIDE UpdateRecent
        return ()

------------------------
-- Workspace make

-- | Calculates for every package a list of packages, that depends directly on it
-- This is the reverse info from cabal, were we specify what a package depends on
calculateReverseDependencies :: Workspace -> Map IDEPackage [IDEPackage]
calculateReverseDependencies ws = Map.map nub $ foldl' calc mpacks packages
    where
    packages = wsPackages ws
    mpacks   = Map.fromList (map (\p -> (p,[])) packages)

    calc :: Map IDEPackage [IDEPackage] -> IDEPackage -> Map IDEPackage [IDEPackage]
    calc theMap pack = Map.mapWithKey (addDeps (ipdDepends pack) pack ) theMap

    addDeps :: [Dependency] -> IDEPackage -> IDEPackage -> [IDEPackage] -> [IDEPackage]
    addDeps dependencies pack pack2 revDeps =  if depsMatch then pack : revDeps else revDeps
        where
        depsMatch = List.or (map (matchOne pack2) dependencies)
        matchOne thePack (Dependency name versionRange) =
            name == pkgName (ipdPackageId thePack)
            &&  withinRange (pkgVersion (ipdPackageId thePack)) versionRange


-- | Select a package which is not direct or indirect dependent on any other package
selectFirstReasonableTarget :: [IDEPackage] -> Map IDEPackage [IDEPackage] -> IDEPackage
selectFirstReasonableTarget [] _      = error "Workspaces>>selectFirstReasonableTarget: empty package list"
selectFirstReasonableTarget [p] _     = p
selectFirstReasonableTarget l revDeps =
    case foldl' removeDeps l l of
        hd:_ -> hd
        []   -> error "Workspaces>>selectFirstReasonableTarget: no remaining"
    where
    removeDeps :: [IDEPackage] -> IDEPackage -> [IDEPackage]
    removeDeps packs p = packs \\ allDeps p

    allDeps :: IDEPackage -> [IDEPackage]
    allDeps p = case Map.lookup p revDeps of
                            Nothing   -> []
                            Just list -> list ++ concatMap allDeps list

makePackages :: Bool -> Bool -> [IDEPackage] -> IDEAction
makePackages _ _ []     = return ()
makePackages isBackgroundBuild needConfigure packages = do
    mbWs <- readIDE workspace
    prefs' <- readIDE prefs
    case mbWs of
        Nothing -> return ()
        Just ws -> do
            let package = selectFirstReasonableTarget packages (wsReverseDeps ws)
            if needConfigure
                then packageConfig' package (\b ->
                    if b
                        then buildPackage isBackgroundBuild package (cont prefs' package ws)
                        else return ())
                else buildPackage isBackgroundBuild package (cont prefs' package ws)
    where
    cont prefs' package ws res =
        if res
            then do
                let deps = case Map.lookup package (wsReverseDeps ws) of
                                        Nothing   -> []
                                        Just list -> list
                -- install if backgroundLink is set or it is not a background build and either
                -- AlwaysInstall is set or
                -- deps are not empty
                when ((not isBackgroundBuild || backgroundLink prefs') &&
                    ((not (null deps) && autoInstall prefs' == InstallLibs)
                        || autoInstall prefs' == InstallAlways))
                    $ packageInstall' package (cont2 deps package)
            else return () -- don't continue, when their was an error
    cont2 deps package res = do
        when res $ do
            let nextTargets = delete package $ nub $ packages ++ deps
            makePackages isBackgroundBuild True nextTargets


backgroundMake :: IDEAction
backgroundMake = catchIDE (do
    ideR        <- ask
    prefs       <- readIDE prefs
    mbPackage   <- readIDE activePack
    case mbPackage of
        Nothing         -> return ()
        Just package    -> do
            modifiedPacks <- if saveAllBeforeBuild prefs
                                then fileCheckAll belongsToPackage
                                else return []
            let isModified = not (null modifiedPacks)
            when isModified $ do
            makePackages True False modifiedPacks
    )
    (\(e :: SomeException) -> sysMessage Normal (show e))

makePackage :: IDEAction
makePackage = do
    activePack' <- readIDE activePack
    case activePack' of
        Nothing -> return ()
        Just p -> makePackages False False [p]

workspaceClean = do
    mbWs <-  readIDE workspace
    case mbWs of
        Nothing -> return ()
        Just ws -> mapM_ (packageClean . Just) (wsPackages ws)

workspaceMake = do
    mbWs <-  readIDE workspace
    case mbWs of
        Nothing -> return ()
        Just ws -> makePackages False True (wsPackages ws)