module IDE.Leksah (
leksah
) where
import Graphics.UI.Gtk
import Control.Concurrent
import Data.IORef
import Data.Maybe
import qualified Data.Map as Map
import System.Console.GetOpt
import System.Environment
import Data.Version
import Prelude hiding(catch)
import qualified IDE.OSX as OSX
import qualified IDE.YiConfig as Yi
#ifdef LEKSAH_WITH_YI_DYRE
import System.Directory (getAppUserDataDirectory)
import System.FilePath ((</>))
import Control.Applicative ((<$>))
import qualified Config.Dyre as Dyre
#endif
import Paths_leksah
import IDE.Session
import IDE.Core.State
import Control.Event
import IDE.SourceCandy
import IDE.Utils.FileUtils
import Graphics.UI.Editor.MakeEditor
import Graphics.UI.Editor.Parameters
import IDE.Command
import IDE.Pane.Preferences
import IDE.Keymap
import IDE.Pane.SourceBuffer
import IDE.Find
import Graphics.UI.Editor.Composite (filesEditor, maybeEditor)
import Graphics.UI.Editor.Simple
(enumEditor, stringEditor)
import IDE.Metainfo.Provider (initInfo)
import IDE.Workspaces
(workspaceAddPackage', workspaceTryQuiet, workspaceNewHere,
workspaceOpenThis, backgroundMake)
import IDE.Utils.GUIUtils
import Network (withSocketsDo)
import Control.Exception
import System.Exit(exitFailure)
import qualified IDE.StrippedPrefs as SP
import IDE.Utils.Tool (runTool, toolline, waitForProcess)
import System.Log
import System.Log.Logger
(getLevel, getRootLogger, debugM, updateGlobalLogger,
rootLoggerName, setLevel)
import Data.List (stripPrefix)
import System.Directory
(doesDirectoryExist, copyFile, createDirectoryIfMissing,
getHomeDirectory, doesFileExist)
import System.FilePath (dropExtension, splitExtension, (</>))
import qualified Data.Enumerator as E
import qualified Data.Enumerator.List as EL
import Data.Enumerator (($$))
import Control.Monad (when, unless, liftM)
import Control.Monad.IO.Class (MonadIO(..))
import Control.Applicative ((<$>))
data Flag = VersionF | SessionN String | EmptySession | DefaultSession | Help | Verbosity String
deriving (Show,Eq)
options :: [OptDescr Flag]
options = [Option ['e'] ["emptySession"] (NoArg EmptySession)
"Start with empty session"
, Option ['d'] ["defaultSession"] (NoArg DefaultSession)
"Start with default session (can be used together with a source file)"
, Option ['l'] ["loadSession"] (ReqArg SessionN "NAME")
"Load session"
, Option ['h'] ["help"] (NoArg Help)
"Display command line options"
, Option ['v'] ["version"] (NoArg VersionF)
"Show the version number of ide"
, Option ['e'] ["verbosity"] (ReqArg Verbosity "Verbosity")
"One of DEBUG, INFO, NOTICE, WARNING, ERROR, CRITICAL, ALERT, EMERGENCY"]
header = "Usage: leksah [OPTION...] [file(.lkshs|.lkshw|.hs|.lhs)]"
ideOpts :: [String] -> IO ([Flag], [String])
ideOpts argv =
case getOpt Permute options argv of
(o,n,[] ) -> return (o,n)
(_,_,errs) -> ioError $ userError $ concat errs ++ usageInfo header options
#ifdef LEKSAH_WITH_YI_DYRE
leksahDriver = Dyre.wrapMain Dyre.defaultParams
{ Dyre.projectName = "yi"
, Dyre.realMain = \(config, mbError) -> do
case mbError of
Just error -> putStrLn $ "Error in yi configuration file : " ++ error
Nothing -> return ()
realMain config
, Dyre.showError = \(config, _) error -> (config, Just error)
, Dyre.configDir = Just . getAppUserDataDirectory $ "yi"
, Dyre.cacheDir = Just $ ((</> "leksah") <$> (getAppUserDataDirectory "cache"))
, Dyre.hidePackages = ["mtl"]
, Dyre.ghcOpts = ["-DLEKSAH"]
}
leksah yiConfig = leksahDriver (yiConfig, Nothing)
#else
leksah = realMain
#endif
realMain yiConfig = do
withSocketsDo $ handleExceptions $ do
dataDir <- getDataDir
args <- getArgs
(o,files) <- ideOpts args
isFirstStart <- liftM not $ hasSavedConfigFile standardPreferencesFilename
let sessions = catMaybes $ map (\x -> case x of
SessionN s -> Just s
_ -> Nothing) o
let sessionFPs = filter (\f -> snd (splitExtension f) == leksahSessionFileExtension) files
let workspaceFPs = filter (\f -> snd (splitExtension f) == leksahWorkspaceFileExtension) files
let sourceFPs = filter (\f -> let (_,s) = splitExtension f
in s == ".hs" || s == ".lhs" || s == ".chs") files
let mbWorkspaceFP'= case workspaceFPs of
[] -> Nothing
w:_ -> Just w
(mbWSessionFP, mbWorkspaceFP) <-
case mbWorkspaceFP' of
Nothing -> return (Nothing,Nothing)
Just fp -> let spath = dropExtension fp ++ leksahSessionFileExtension
in do
exists <- liftIO $ doesFileExist spath
if exists
then return (Just spath,Nothing)
else return (Nothing,Just fp)
let ssession = case sessions of
(s:_) -> s ++ leksahSessionFileExtension
_ -> if null sourceFPs
then standardSessionFilename
else emptySessionFilename
sessionFP <- if elem EmptySession o
then getConfigFilePathForLoad
emptySessionFilename Nothing dataDir
else if elem DefaultSession o
then getConfigFilePathForLoad
standardSessionFilename Nothing dataDir
else case mbWSessionFP of
Just fp -> return fp
Nothing -> getConfigFilePathForLoad
ssession Nothing dataDir
let verbosity' = catMaybes $
map (\x -> case x of
Verbosity s -> Just s
_ -> Nothing) o
let verbosity = case verbosity' of
[] -> INFO
h:_ -> read h
updateGlobalLogger rootLoggerName (\ l -> setLevel verbosity l)
when (elem VersionF o)
(sysMessage Normal $ "Leksah the Haskell IDE, version " ++ showVersion version)
when (elem Help o)
(sysMessage Normal $ "Leksah the Haskell IDE " ++ usageInfo header options)
prefsPath <- getConfigFilePathForLoad standardPreferencesFilename Nothing dataDir
prefs <- readPrefs prefsPath
when (not (elem VersionF o) && not (elem Help o))
(startGUI yiConfig sessionFP mbWorkspaceFP sourceFPs prefs isFirstStart)
handleExceptions inner =
catch inner (\(exception :: SomeException) -> do
sysMessage Normal ("leksah: internal IDE error: " ++ show exception)
exitFailure
)
startGUI :: Yi.Config -> FilePath -> Maybe FilePath -> [FilePath] -> Prefs -> Bool -> IO ()
startGUI yiConfig sessionFP mbWorkspaceFP sourceFPs iprefs isFirstStart = do
Yi.start yiConfig $ \yiControl -> do
st <- unsafeInitGUIForThreadedRTS
when rtsSupportsBoundThreads
(sysMessage Normal "Linked with -threaded")
timeoutAddFull (yield >> return True) priorityHigh 10
mapM_ (sysMessage Normal) st
initGtkRc
dataDir <- getDataDir
mbStartupPrefs <- if not isFirstStart
then return $ Just iprefs
else do
firstStartOK <- firstStart iprefs
if not firstStartOK
then return Nothing
else do
prefsPath <- getConfigFilePathForLoad standardPreferencesFilename Nothing dataDir
prefs <- readPrefs prefsPath
return $ Just prefs
case mbStartupPrefs of
Nothing -> return ()
Just startupPrefs -> startMainWindow yiControl sessionFP mbWorkspaceFP sourceFPs
startupPrefs isFirstStart
startMainWindow :: Yi.Control -> FilePath -> Maybe FilePath -> [FilePath] ->
Prefs -> Bool -> IO ()
startMainWindow yiControl sessionFP mbWorkspaceFP sourceFPs startupPrefs isFirstStart = do
osxApp <- OSX.applicationNew
uiManager <- uiManagerNew
newIcons
dataDir <- getDataDir
candyPath <- getConfigFilePathForLoad
(case sourceCandy startupPrefs of
(_,name) -> name ++ leksahCandyFileExtension) Nothing dataDir
candySt <- parseCandy candyPath
keysPath <- getConfigFilePathForLoad (keymapName startupPrefs ++ leksahKeymapFileExtension) Nothing dataDir
keyMap <- parseKeymap keysPath
let accelActions = setKeymap (keyMap :: KeymapI) mkActions
specialKeys <- buildSpecialKeys keyMap accelActions
win <- windowNew
widgetSetName win "Leksah Main Window"
let fs = FrameState
{ windows = [win]
, uiManager = uiManager
, panes = Map.empty
, activePane = Nothing
, paneMap = Map.empty
, layout = (TerminalP Map.empty Nothing (1) Nothing Nothing)
, panePathFromNB = Map.empty
}
let ide = IDE
{ frameState = fs
, recentPanes = []
, specialKeys = specialKeys
, specialKey = Nothing
, candy = candySt
, prefs = startupPrefs
, workspace = Nothing
, activePack = Nothing
, bufferProjCache = Map.empty
, allLogRefs = []
, currentHist = 0
, currentEBC = (Nothing, Nothing, Nothing)
, systemInfo = Nothing
, packageInfo = Nothing
, workspaceInfo = Nothing
, workspInfoCache = Map.empty
, handlers = Map.empty
, currentState = IsStartingUp
, guiHistory = (False,[],1)
, findbar = (False,Nothing)
, toolbar = (True,Nothing)
, recentFiles = []
, recentWorkspaces = []
, runningTool = Nothing
, debugState = Nothing
, completion = ((750,400),Nothing)
, yiControl = yiControl
, server = Nothing
}
ideR <- newIORef ide
menuDescription' <- menuDescription
reflectIDE (makeMenu uiManager accelActions menuDescription') ideR
nb <- reflectIDE (newNotebook []) ideR
afterSwitchPage nb (\i -> reflectIDE (handleNotebookSwitch nb i) ideR)
widgetSetName nb $"root"
win `onDelete` (\ _ -> do reflectIDE quit ideR; return True)
reflectIDE (instrumentWindow win startupPrefs (castToWidget nb)) ideR
reflectIDE (do
setCandyState (fst (sourceCandy startupPrefs))
setBackgroundBuildToggled (backgroundBuild startupPrefs)
setRunUnitTests (runUnitTests startupPrefs)
setMakeModeToggled (makeMode startupPrefs)) ideR
let (x,y) = defaultSize startupPrefs
windowSetDefaultSize win x y
(tbv,fbv) <- reflectIDE (do
registerLeksahEvents
pair <- recoverSession sessionFP
workspaceOpenThis False mbWorkspaceFP
mapM fileOpenThis sourceFPs
wins <- getWindows
mapM_ instrumentSecWindow (tail wins)
return pair
) ideR
widgetShowAll win
reflectIDE (do
triggerEventIDE UpdateRecent
if tbv
then showToolbar
else hideToolbar
if fbv
then showFindbar
else hideFindbar
OSX.updateMenu osxApp uiManager) ideR
OSX.applicationReady osxApp
configDir <- getConfigDir
let welcomePath = configDir</>"leksah-welcome"
welcomeExists <- doesDirectoryExist welcomePath
unless welcomeExists $ do
let welcomeSource = dataDir</>"data"</>"leksah-welcome"
welcomeCabal = welcomePath</>"leksah-welcome.cabal"
welcomeMain = welcomePath</>"src"</>"Main.hs"
createDirectoryIfMissing True $ welcomePath</>"src"
copyFile (welcomeSource</>"Setup.lhs") (welcomePath</>"Setup.lhs")
copyFile (welcomeSource</>"leksah-welcome.cabal") (welcomeCabal)
copyFile (welcomeSource</>"src"</>"Main.hs") (welcomeMain)
defaultWorkspace <- liftIO $ (</> "leksah.lkshw") <$> getHomeDirectory
defaultExists <- liftIO $ doesFileExist defaultWorkspace
reflectIDE (do
if defaultExists
then workspaceOpenThis False (Just defaultWorkspace)
else workspaceNewHere defaultWorkspace
workspaceTryQuiet $ workspaceAddPackage' welcomeCabal
fileOpenThis welcomeMain) ideR
reflectIDE (initInfo (modifyIDE_ (\ide -> ide{currentState = IsRunning}))) ideR
timeoutAddFull (do
reflectIDE (do
currentPrefs <- readIDE prefs
when (backgroundBuild currentPrefs) $ backgroundMake) ideR
return True) priorityDefaultIdle 1000
reflectIDE (triggerEvent ideR (Sensitivity [(SensitivityInterpreting, False)])) ideR
mainGUI
fDescription :: FilePath -> FieldDescription Prefs
fDescription configPath = VFD emptyParams [
mkField
(paraName <<<- ParaName "Paths under which haskell sources for packages may be found"
$ paraDirection <<<- ParaDirection Vertical
$ emptyParams)
sourceDirectories
(\b a -> a{sourceDirectories = b})
(filesEditor Nothing FileChooserActionSelectFolder "Select folders")
, mkField
(paraName <<<- ParaName "Unpack source for cabal packages to" $ emptyParams)
unpackDirectory
(\b a -> a{unpackDirectory = b})
(maybeEditor (stringEditor (const True) True,emptyParams) True "")
, mkField
(paraName <<<- ParaName "URL from which to download prebuilt metadata" $ emptyParams)
retrieveURL
(\b a -> a{retrieveURL = b})
(stringEditor (\ _ -> True) True)
, mkField
(paraName <<<- ParaName "Strategy for downloading prebuilt metadata" $ emptyParams)
retrieveStrategy
(\b a -> a{retrieveStrategy = b})
(enumEditor ["Try to download and then build locally if that fails","Try to build locally and then download if that fails","Never download (just try to build locally)"])]
firstStart :: Prefs -> IO Bool
firstStart prefs = do
dataDir <- getDataDir
prefsPath <- getConfigFilePathForLoad standardPreferencesFilename Nothing dataDir
prefs <- readPrefs prefsPath
configDir <- getConfigDir
dialog <- dialogNew
setLeksahIcon dialog
set dialog [
windowTitle := "Welcome to Leksah, the Haskell IDE",
windowWindowPosition := WinPosCenter]
dialogAddButton dialog "gtk-ok" ResponseOk
dialogAddButton dialog "gtk-cancel" ResponseCancel
vb <- dialogGetUpper dialog
label <- labelNew (Just (
"Before you start using Leksah it will collect and download metadata about your installed Haskell packages.\n" ++
"You can add folders under which you have sources for Haskell packages not available from Hackage."))
(widget, setInj, getExt,notifier) <- buildEditor (fDescription configDir) prefs
boxPackStart vb label PackNatural 7
sw <- scrolledWindowNew Nothing Nothing
scrolledWindowAddWithViewport sw widget
scrolledWindowSetPolicy sw PolicyNever PolicyAutomatic
boxPackStart vb sw PackGrow 7
windowSetDefaultSize dialog 800 630
widgetShowAll dialog
response <- dialogRun dialog
widgetHide dialog
case response of
ResponseOk -> do
mbNewPrefs <- extract prefs [getExt]
widgetDestroy dialog
case mbNewPrefs of
Nothing -> do
sysMessage Normal "No dialog results"
return False
Just newPrefs -> do
fp <- getConfigFilePathForSave standardPreferencesFilename
writePrefs fp newPrefs
fp2 <- getConfigFilePathForSave strippedPreferencesFilename
SP.writeStrippedPrefs fp2
(SP.Prefs {SP.sourceDirectories = sourceDirectories newPrefs,
SP.unpackDirectory = unpackDirectory newPrefs,
SP.retrieveURL = retrieveURL newPrefs,
SP.retrieveStrategy = retrieveStrategy newPrefs,
SP.serverPort = serverPort newPrefs,
SP.endWithLastConn = endWithLastConn newPrefs})
firstBuild newPrefs
return True
_ -> do
widgetDestroy dialog
return False
setLeksahIcon :: (WindowClass self) => self -> IO ()
setLeksahIcon window = do
dataDir <- getDataDir
let iconPath = dataDir </> "pics" </> "leksah.png"
iconExists <- doesFileExist iconPath
when iconExists $
windowSetIconFromFile window iconPath
firstBuild newPrefs = do
dialog <- dialogNew
setLeksahIcon dialog
set dialog [
windowTitle := "Leksah: Updating Metadata",
windowWindowPosition := WinPosCenter,
windowDeletable := False]
vb <- dialogGetUpper dialog
progressBar <- progressBarNew
progressBarSetText progressBar "Please wait while Leksah collects information about Haskell packages on your system"
progressBarSetFraction progressBar 0.0
boxPackStart vb progressBar PackGrow 7
forkIO $ do
logger <- getRootLogger
let verbosity = case getLevel logger of
Just level -> ["--verbosity=" ++ show level]
Nothing -> []
(output, pid) <- runTool "leksah-server" (["-sbo", "+RTS", "-N2", "-RTS"] ++ verbosity) Nothing
E.run_ $ output $$ EL.mapM_ (update progressBar)
waitForProcess pid
postGUIAsync (dialogResponse dialog ResponseOk)
widgetShowAll dialog
dialogRun dialog
widgetHide dialog
widgetDestroy dialog
return ()
where
update pb to = do
let str = toolline to
case stripPrefix "update_toolbar " str of
Just rest -> postGUIAsync $ progressBarSetFraction pb (read rest)
Nothing -> liftIO $ debugM "leksah" str