ghcide-1.3.0.0: src/Development/IDE/Core/ProgressReporting.hs
{-# LANGUAGE RankNTypes #-}
module Development.IDE.Core.ProgressReporting
( ProgressEvent(..)
, ProgressReporting(..)
, noProgressReporting
, delayedProgressReporting
-- utilities, reexported for use in Core.Shake
, mRunLspT
, mRunLspTCallback
-- for tests
, recordProgress
, InProgress(..)
)
where
import Control.Concurrent.Async
import Control.Concurrent.Strict
import Control.Monad.Extra
import Control.Monad.IO.Class
import Control.Monad.Trans.Class (lift)
import Data.Foldable (for_)
import Data.Functor (($>))
import qualified Data.HashMap.Strict as HMap
import qualified Data.Text as T
import Data.Unique
import Development.IDE.GHC.Orphans ()
import Development.IDE.Graph hiding (ShakeValue)
import Development.IDE.Types.Location
import Development.IDE.Types.Options
import qualified Language.LSP.Server as LSP
import Language.LSP.Types
import qualified Language.LSP.Types as LSP
import System.Time.Extra
import UnliftIO.Exception (bracket_)
data ProgressEvent
= KickStarted
| KickCompleted
data ProgressReporting = ProgressReporting
{ progressUpdate :: ProgressEvent -> IO ()
, inProgress :: forall a. NormalizedFilePath -> Action a -> Action a
, progressStop :: IO ()
}
noProgressReporting :: IO ProgressReporting
noProgressReporting = return $ ProgressReporting
{ progressUpdate = const $ pure ()
, inProgress = const id
, progressStop = pure ()
}
-- | State used in 'delayedProgressReporting'
data State
= NotStarted
| Stopped
| Running (Async ())
-- | State transitions used in 'delayedProgressReporting'
data Transition = Event ProgressEvent | StopProgress
updateState :: IO () -> Transition -> State -> IO State
updateState _ _ Stopped = pure Stopped
updateState start (Event KickStarted) NotStarted = Running <$> async start
updateState start (Event KickStarted) (Running a) = cancel a >> Running <$> async start
updateState _ (Event KickCompleted) (Running a) = cancel a $> NotStarted
updateState _ (Event KickCompleted) st = pure st
updateState _ StopProgress (Running a) = cancel a $> Stopped
updateState _ StopProgress st = pure st
-- | Data structure to track progress across the project
data InProgress = InProgress
{ todo :: !Int -- ^ Number of files to do
, done :: !Int -- ^ Number of files done
, current :: !(HMap.HashMap NormalizedFilePath Int)
}
recordProgress :: NormalizedFilePath -> (Int -> Int) -> InProgress -> InProgress
recordProgress file shift InProgress{..} = case HMap.alterF alter file current of
((prev, new), m') ->
let (done',todo') =
case (prev,new) of
(Nothing,0) -> (done+1, todo+1)
(Nothing,_) -> (done, todo+1)
(Just 0, 0) -> (done , todo)
(Just 0, _) -> (done-1, todo)
(Just _, 0) -> (done+1, todo)
(Just _, _) -> (done , todo)
in InProgress todo' done' m'
where
alter x = let x' = maybe (shift 0) shift x in ((x,x'), Just x')
-- | A 'ProgressReporting' that enqueues Begin and End notifications in a new
-- thread, with a grace period (nothing will be sent if 'KickCompleted' arrives
-- before the end of the grace period).
delayedProgressReporting
:: Seconds -- ^ Grace period before starting
-> Seconds -- ^ sampling delay
-> Maybe (LSP.LanguageContextEnv c)
-> ProgressReportingStyle
-> IO ProgressReporting
delayedProgressReporting before after lspEnv optProgressStyle = do
inProgressVar <- newVar $ InProgress 0 0 mempty
progressState <- newVar NotStarted
let progressUpdate event = updateStateVar $ Event event
progressStop = updateStateVar StopProgress
updateStateVar = modifyVar_ progressState . updateState (mRunLspT lspEnv $ lspShakeProgress inProgressVar)
inProgress :: NormalizedFilePath -> Action a -> Action a
inProgress = withProgressVar inProgressVar
return ProgressReporting{..}
where
lspShakeProgress inProgress = do
-- first sleep a bit, so we only show progress messages if it's going to take
-- a "noticable amount of time" (we often expect a thread kill to arrive before the sleep finishes)
liftIO $ sleep before
u <- ProgressTextToken . T.pack . show . hashUnique <$> liftIO newUnique
b <- liftIO newBarrier
void $ LSP.sendRequest LSP.SWindowWorkDoneProgressCreate
LSP.WorkDoneProgressCreateParams { _token = u } $ liftIO . signalBarrier b
ready <- liftIO $ waitBarrier b
for_ ready $ const $ bracket_ (start u) (stop u) (loop u 0)
where
start id = LSP.sendNotification LSP.SProgress $
LSP.ProgressParams
{ _token = id
, _value = LSP.Begin $ WorkDoneProgressBeginParams
{ _title = "Processing"
, _cancellable = Nothing
, _message = Nothing
, _percentage = Nothing
}
}
stop id = LSP.sendNotification LSP.SProgress
LSP.ProgressParams
{ _token = id
, _value = LSP.End WorkDoneProgressEndParams
{ _message = Nothing
}
}
loop _ _ | optProgressStyle == NoProgress =
forever $ liftIO $ threadDelay maxBound
loop id prev = do
InProgress{..} <- liftIO $ readVar inProgress
liftIO $ sleep after
if todo == 0 then loop id 0 else do
let next = 100 * fromIntegral done / fromIntegral todo
when (next /= prev) $
LSP.sendNotification LSP.SProgress $
LSP.ProgressParams
{ _token = id
, _value = LSP.Report $ case optProgressStyle of
Explicit -> LSP.WorkDoneProgressReportParams
{ _cancellable = Nothing
, _message = Just $ T.pack $ show done <> "/" <> show todo
, _percentage = Nothing
}
Percentage -> LSP.WorkDoneProgressReportParams
{ _cancellable = Nothing
, _message = Nothing
, _percentage = Just next
}
NoProgress -> error "unreachable"
}
loop id next
withProgressVar var file = actionBracket (f succ) (const $ f pred) . const
-- This functions are deliberately eta-expanded to avoid space leaks.
-- Do not remove the eta-expansion without profiling a session with at
-- least 1000 modifications.
where
f shift = modifyVar' var $ recordProgress file shift
mRunLspT :: Applicative m => Maybe (LSP.LanguageContextEnv c ) -> LSP.LspT c m () -> m ()
mRunLspT (Just lspEnv) f = LSP.runLspT lspEnv f
mRunLspT Nothing _ = pure ()
mRunLspTCallback :: Monad m
=> Maybe (LSP.LanguageContextEnv c)
-> (LSP.LspT c m a -> LSP.LspT c m a)
-> m a
-> m a
mRunLspTCallback (Just lspEnv) f g = LSP.runLspT lspEnv $ f (lift g)
mRunLspTCallback Nothing _ g = g