packages feed

tricorder-0.2.0.1: src/Tricorder/Session/Command.hs

module Tricorder.Session.Command
    ( Command (..)
    , Repl (..)
    , render
    , resolveCommand
    ) where

import Atelier.Effects.FileSystem (FileSystem)
import Data.Default (Default (..))
import Effectful.NonDet (NonDet, OnEmptyPolicy (..), emptyEff, plusEff, runNonDet)
import System.FilePath ((</>))

import Atelier.Effects.FileSystem qualified as FileSystem
import Data.List qualified as List

import Tricorder.Runtime (ProjectRoot (..))
import Tricorder.Session.Config (Config, command, replBuildDir)
import Tricorder.Session.Target (Target (..))
import Tricorder.Session.TestTarget (TestTarget, getTestTarget)

import Tricorder.Session.Target qualified as Target


data Command = Command
    { repl :: Repl
    , arguments :: [Text]
    , targets :: [Target]
    }
    deriving stock (Eq, Generic, Show)


data Repl = StackMulti | Stack | Cabal | Unknown
    deriving stock (Eq, Generic, Show)


render :: Command -> Text
render command = unwords $ renderRepl command.repl <> command.arguments <> tgts
  where
    tgts = case command.repl of
        Stack -> List.nub $ Target.componentName <$> command.targets
        StackMulti -> List.nub $ Target.renderTarget <$> command.targets
        Cabal -> Target.renderTarget <$> command.targets
        Unknown -> Target.renderTarget <$> command.targets


renderRepl :: Repl -> [Text]
renderRepl StackMulti = ["stack", "ghci"]
renderRepl Stack = ["stack", "ghci"]
renderRepl Cabal = ["cabal", "repl"]
renderRepl Unknown = []


instance Default Command where
    def = Command Unknown [] []


-- | Resolve the GHCi command, using config if set or autodetecting otherwise.
--
-- The @testTargets@ are the discovered @test:@ components; they are appended to
-- the auto-detected @all@ target (see 'detectCommand'). They are ignored when
-- the user has pinned an explicit @command@ or explicit @targets@ in config.
resolveCommand :: (FileSystem :> es) => ProjectRoot -> Config -> [Target] -> [TestTarget] -> Eff es Command
resolveCommand projectRoot@(ProjectRoot root) cfg targets testTargets =
    case cfg.command of
        Just cmd -> case words cmd of
            "stack" : "repl" : args -> detectStackKind args
            "stack" : "ghci" : args -> detectStackKind args
            "cabal" : "repl" : args -> pure $ Command Cabal args []
            args -> pure $ Command Unknown args []
        Nothing ->
            detectCommand targets testTargets cfg.replBuildDir projectRoot
  where
    detectStackKind args = do
        hasCabalFileInRoot <- any (".cabal" `List.isSuffixOf`) <$> FileSystem.listDirectory root
        let repl =
                if hasCabalFileInRoot then
                    Stack
                else
                    StackMulti
        pure $ Command repl args []


detectCommand :: (FileSystem :> es) => [Target] -> [TestTarget] -> FilePath -> ProjectRoot -> Eff es Command
detectCommand targets testTargets replBuildDir projectRoot = do
    cmd <-
        fmap (fromMaybe (fallback replBuildDir) . rightToMaybe)
            $ runNonDet OnEmptyKeep
            $ useStack projectRoot
                `plusEff` useMultiCabal projectRoot replBuildDir
    pure
        $ cmd
            { targets =
                if not (null targets) then
                    targets
                else
                    Bare "all" : (getTestTarget <$> testTargets)
            }


useStack :: (FileSystem :> es, NonDet :> es) => ProjectRoot -> Eff es Command
useStack (ProjectRoot projectRoot) = do
    hasStack <- FileSystem.doesFileExist $ projectRoot </> "stack.yaml"
    if hasStack then
        pure $ Command Stack [] []
    else
        emptyEff


useMultiCabal :: (FileSystem :> es, NonDet :> es) => ProjectRoot -> FilePath -> Eff es Command
useMultiCabal (ProjectRoot projectRoot) replBuildDir = do
    hasCabalProject <- FileSystem.doesFileExist $ projectRoot </> "cabal.project"
    hasCabalFiles <- any (".cabal" `List.isSuffixOf`) <$> FileSystem.listDirectory projectRoot
    if hasCabalFiles || hasCabalProject then
        pure
            $ Command
                { repl = Cabal
                , arguments = ["--enable-multi-repl"] <> buildDirFlag replBuildDir
                , targets = []
                }
    else
        emptyEff


fallback :: FilePath -> Command
fallback replBuildDir =
    Command
        { repl = Cabal
        , arguments = buildDirFlag replBuildDir
        , targets = [Bare "all"]
        }


buildDirFlag :: FilePath -> [Text]
buildDirFlag replBuildDir = ["--builddir", toText replBuildDir]