module Main where import Codec.Sarsi (Event (..), Level (..), Location (..), Message (..)) import Control.Concurrent.Async (async, cancel) import Control.Concurrent.STM (atomically) import Control.Concurrent.STM.TBQueue (newTBQueue, readTBQueue) import Control.Concurrent.STM.TVar (TVar, modifyTVar', newTVar, readTVar, readTVarIO, stateTVar, writeTVar) import Control.Monad (when) import Data.Machine (ProcessT, asParts, auto, autoM, final, runT, runT_, scan, sinkPart_, (<~)) import Data.Machine.Fanout (fanout) import Data.MessagePack (Object (..)) import qualified Data.MessagePack.RPC as RPC import Data.Text (Text) import qualified Data.Text as Text import Data.Vector (Vector) import qualified Data.Vector as Vector import NVIM.Client (CommandQueue, ask', mkConnection, send) import NVIM.Command (Command (..)) import NVIM.QuickFix (toQuickFix) import Sarsi (Topic (..), getBroker, getTopic, title) import Sarsi.Consumer (consumeOrWait) import System.Environment (getArgs) import System.Exit (ExitCode (..), exitWith) import System.IO (Handle, IOMode (WriteMode)) import qualified System.IO as IO import System.IO.Machine (sinkIO, sourceIO) data BuildStatus = Starting | Building | Done deriving (Show, Eq) data PluginAction = Focus | Next | Previous deriving (Bounded, Show, Enum, Eq, Ord, Read) pluginActions :: [PluginAction] pluginActions = [minBound ..] data PluginState = PluginState { buildStatus :: BuildStatus, buildErrors :: Vector (Location, [Text]), buildWarnings :: Vector (Location, [Text]), focus :: Maybe (Level, Int), buffer :: Object, window :: Maybe Object } deriving (Show) locationLast :: PluginState -> Maybe Location locationLast s = if Vector.null $ buildErrors s then if Vector.null $ buildWarnings s then Nothing else Just . fst $ Vector.last (buildWarnings s) else Just . fst $ Vector.last (buildErrors s) echo :: String -> Command echo str = NvimCommand [ObjectStr . Text.pack $ concat ["echo \"", str, "\""]] echom :: String -> Command echom str = NvimCommand [ObjectStr . Text.pack $ concat ["echom \"", title, ": ", str, "\""]] jumpTo :: Location -> [Command] jumpTo loc = (\x -> NvimCommand [ObjectStr . Text.pack $ x]) <$> [ concat ["drop +", show $ line loc, " ", Text.unpack $ filePath loc], concat ["call cursor(", show $ line loc, ", ", show $ column loc, ")"], "normal zz" ] openLogFile :: Topic -> IO Handle openLogFile (Topic _ fp) = IO.openFile (concat [fp, "-nvim.log"]) WriteMode parseAction :: Maybe Handle -> (Text, [Object]) -> IO [PluginAction] parseAction _ (m, params) | m == method = return $ cmd =<< unpack =<< params where cmd (ObjectStr c) | Text.isPrefixOf c (Text.pack "cfirst") = [Focus] cmd (ObjectStr c) | Text.isPrefixOf c (Text.pack "cnext") = [Next] cmd (ObjectStr c) | Text.isPrefixOf c (Text.pack "cprevious") = [Previous] cmd _ = [] method = Text.pack "CmdlineLeave" unpack (ObjectArray xs) = Vector.toList xs unpack _ = [] parseAction hLog x = do putLogLn hLog $ concat ["unsupported notification: ", show x] return [] parseArgs :: [String] -> Either String Bool parseArgs [] = Right False parseArgs ["--log"] = Right True parseArgs _ = Left "usage: [--log]" pluginStateInit :: Object -> PluginState pluginStateInit b = PluginState Done Vector.empty Vector.empty Nothing b Nothing putLogLn :: Maybe Handle -> String -> IO () putLogLn Nothing _ = return () putLogLn (Just h) s = IO.hPutStrLn h s >> IO.hFlush h update :: Monoid a => Maybe Handle -> CommandQueue -> TVar PluginState -> Event -> IO a update h q s' e = do display e case e of (Start _) -> updateState (\s -> s {buildStatus = Starting}) (Finish _ _) -> do emptyErrors <- atomically $ stateTVar s' ( \s -> case buildStatus s of Building -> (Vector.null $ buildErrors s, s {buildStatus = Done}) _ -> (True, s {buildStatus = Done, buildErrors = Vector.empty, buildWarnings = Vector.empty}) ) when emptyErrors $ windowClose q s' (Notify msg) -> updateState ( \s -> if buildStatus s /= Building then s {buildStatus = Building, focus = Nothing, buildErrors = Vector.empty, buildWarnings = Vector.empty} else s ) >> updateMsg msg trace h return mempty where display (Start _) = nvim_ h q $ echom $ show e display (Finish _ _) = nvim_ h q $ echom $ show e display (Notify (Message loc lvl _)) = nvim_ h q $ echo $ concat [show loc, " ", show lvl] trace Nothing = return () trace _ = do s <- readTVarIO s' putLogLn h $ show s updateMsg msg = atomically $ modifyTVar' s' (f msg) where f x s = g x where g (Message loc Error txts) = s {buildErrors = Vector.snoc es (loc, txts)} g (Message loc Warning txts) = s {buildWarnings = Vector.snoc ws (loc, txts)} (es, ws) = case buildStatus s of Starting -> (Vector.empty, Vector.empty) _ -> (buildErrors s, buildWarnings s) updateState f = atomically $ modifyTVar' s' f -- TODO Wrap this in an appropriate transformer nvim :: Maybe Handle -> CommandQueue -> Command -> IO (Maybe Object) nvim hLog q cmd = do r <- ask' q cmd case r of RPC.Success a -> return $ Just a RPC.Error err -> do putLogLn hLog $ show err return Nothing nvim_ :: Maybe Handle -> CommandQueue -> Command -> IO () nvim_ h q c = nvim h q c >> return () -- TODO Important: could they all be into STM? how to avoid unnecessary readTVarIO? -- There must be a useful `Async + STM` atomic layer bufferSetLines :: CommandQueue -> TVar PluginState -> [Text] -> IO () bufferSetLines q s' txts = do s <- readTVarIO s' let b = buffer s (RPC.Success _) <- ask' q $ NvimBufSetLines b 0 64 False txts return () windowClose :: CommandQueue -> TVar PluginState -> IO () windowClose q s' = do s <- readTVarIO s' case window s of Nothing -> return () Just w -> do -- Tolerate failure if window was closed manually by user _ <- ask' q $ NvimWinClose w False atomically . modifyTVar' s' $ \x -> x {window = Nothing} return () bufferShow :: CommandQueue -> TVar PluginState -> Int -> IO () bufferShow q s' height = do windowClose q s' (RPC.Success (ObjectInt rows)) <- ask' q (NvimWinGetHeight $ ObjectInt 0) (RPC.Success (ObjectInt cols)) <- ask' q (NvimWinGetWidth $ ObjectInt 0) s <- readTVarIO s' (RPC.Success w) <- ask' q $ openWin (buffer s) rows cols atomically . modifyTVar' s' $ \x -> x {window = Just w} return () where openWin b rows cols = NvimOpenWin b False ( ObjectMap $ ( Vector.fromList [ (ObjectStr $ Text.pack "style", ObjectStr $ Text.pack "minimal"), (ObjectStr $ Text.pack "relative", ObjectStr $ Text.pack "win"), (ObjectStr $ Text.pack "row", ObjectInt $ rows - height), (ObjectStr $ Text.pack "col", ObjectInt 0), (ObjectStr $ Text.pack "width", ObjectInt cols), (ObjectStr $ Text.pack "height", ObjectInt height) ] ) ) actionFocus :: Maybe Handle -> CommandQueue -> TVar PluginState -> Level -> Int -> IO () actionFocus hLog q s' lvl rank = do s <- readTVarIO s' active <- fixingIsActive hLog q s let (loc, txts) = focusContent lvl rank s if not active then return () else do _ <- traverse (nvim_ hLog q) $ jumpTo loc bufferSetLines q s' txts bufferShow q s' $ length txts nvim_ hLog q $ echo $ concat [show lvl, ": ", show (rank + 1), "/", show . length $ messagesSelect s lvl] return () actionMove :: Maybe Handle -> CommandQueue -> TVar PluginState -> (PluginState -> PluginState) -> IO () actionMove hLog q s' f = do fcs <- atomically $ do s <- readTVar s' let s'' = f s writeTVar s' s'' return $ focus s'' case fcs of Nothing -> return () Just (lvl, rank) -> actionFocus hLog q s' lvl rank fixingIsActive :: Maybe Handle -> CommandQueue -> PluginState -> IO Bool fixingIsActive hLog q s = do qfLast <- nvim hLog q $ NvimCommandOutput [ObjectStr . Text.pack $ "clist -1"] case (qfLast, (locationString <$> (locationLast s))) of (Just (ObjectStr ln), Just loc) -> return $ not (Text.null . snd $ Text.breakOn (Text.pack loc) ln) _ -> return False where locationString :: Location -> String locationString (Location fp c l) = concat [Text.unpack fp, ":", show l, " col ", show c] focusContent :: Level -> Int -> PluginState -> (Location, [Text]) focusContent lvl rank s = Vector.unsafeIndex xs rank where xs = case lvl of Warning -> buildWarnings s Error -> buildErrors s focusDefault :: PluginState -> Maybe (Level, Int) focusDefault s = select (Vector.null $ buildErrors s) (Vector.null $ buildWarnings s) where select False _ = Just (Error, 0) select True False = Just (Warning, 0) select True True = Nothing focusMove :: Int -> PluginState -> PluginState focusMove i s = case focus s of Nothing -> s {focus = focusDefault s} Just (lvl, rank) -> s {focus = Just $ f lvl (rank + i)} where f lvl rank | rank < 0 = f (toggle lvl) ((length $ select lvl) + rank) f lvl rank | rank >= (length $ select lvl) = f (toggle lvl) (rank - (length $ select lvl)) f lvl rank = (lvl, rank) toggle lvl | Vector.null $ select (toggle' lvl) = lvl toggle lvl = toggle' lvl toggle' Warning = Error toggle' Error = Warning select = messagesSelect s messagesSelect :: PluginState -> Level -> Vector (Location, [Text]) messagesSelect s Warning = buildWarnings s messagesSelect s Error = buildErrors s -- TODO How to make it shudown gracefully? currently it's probably killed by nvim while blocking in `consumerOrWait` main :: IO () main = do args <- getArgs case parseArgs args of Left err -> do putStrLn err exitWith $ ExitFailure 1 Right logging -> do IO.hSetBuffering IO.stdin IO.NoBuffering IO.hSetBuffering IO.stdout IO.NoBuffering b <- getBroker t <- getTopic b "." hLog <- if logging then Just <$> (openLogFile t) else return Nothing qCmds <- atomically $ newTBQueue 8 qNotifs <- atomically $ newTBQueue 8 connClose <- mkConnection IO.stdin IO.stdout qCmds qNotifs (errHandler hLog) nvim_ hLog qCmds $ NvimCommand [ObjectStr . Text.pack $ "au CmdlineLeave * call rpcnotify(g:sarsi, 'CmdlineLeave', [getcmdline()])"] (Just buf) <- nvim hLog qCmds $ NvimCreateBuf False True state <- atomically $ newTVar $ pluginStateInit buf notifier <- async . runT_ $ autoM (notify hLog qCmds state) <~ asParts <~ (autoM $ parseAction hLog) <~ (sourceIO . atomically $ readTBQueue qNotifs) putLogLn hLog "ready" _ <- consumeOrWait t (consumer hLog state qCmds) cancel notifier connClose _ <- traverse IO.hClose hLog return () where errHandler hLog err = do putLogLn hLog $ show err notify hLog q s' Focus = do s <- readTVarIO s' case focus s of Nothing -> case focusDefault s of Nothing -> return () Just (lvl, rank) -> do atomically . modifyTVar' s' $ \x -> x {focus = Just (lvl, rank)} actionFocus hLog q s' lvl rank Just (lvl, rank) -> actionFocus hLog q s' lvl rank notify hLog q s' Next = actionMove hLog q s' (focusMove 1) notify hLog q s' Previous = actionMove hLog q s' (focusMove (-1)) consumer h s q Nothing src = consumer h s q (Just 0) src consumer h s q (Just i) src = do i' <- runT $ final <~ asParts <~ fanout [quickFixes, pluginUpdate] <~ src return (Left $ head i') where quickFixes = auto (\x -> [x]) <~ sinkPart_ id (sinkIO (send q) <~ asParts) <~ toQuickFixes i pluginUpdate = autoM (update h q s) toQuickFixes :: Int -> ProcessT IO Event (Int, [Command]) toQuickFixes acc = scan f (acc, []) where f (i, _) event = toQuickFix i event