1-- Copyright (c) 2019 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
2-- SPDX-License-Identifier: Apache-2.0
4{-# LANGUAGE ExistentialQuantification #-}
5{-# LANGUAGE TypeFamilies #-}
6{-# LANGUAGE ConstraintKinds #-}
7{-# LANGUAGE OverloadedStrings #-}
9-- | A Shake implementation of the compiler service.
11-- There are two primary locations where data lives, and both of
12-- these contain much the same data:
14-- * The Shake database (inside 'shakeDb') stores a map of shake keys
15-- to shake values. In our case, these are all of type 'Q' to 'A'.
16-- During a single run all the values in the Shake database are consistent
17-- so are used in conjunction with each other, e.g. in 'uses'.
19-- * The 'Values' type stores a map of keys to values. These values are
20-- always stored as real Haskell values, whereas Shake serialises all 'A' values
21-- between runs. To deserialise a Shake value, we just consult Values.
22module Development.IDE.Core.Shake(
24 IdeRule, IdeResult, GetModificationTime(..),
29 use_, useNoFile_, uses_,
30 define, defineEarlyCutoff,
31 getDiagnostics, unsafeClearDiagnostics,
32 IsIdeGlobal, addIdeGlobal, getIdeGlobalState, getIdeGlobalAction,
42import Development.Shake
43import Development.Shake.Database
44import Development.Shake.Classes
45import Development.Shake.Rule
46import qualified Data.HashMap.Strict as HMap
47import qualified Data.Map as Map
48import qualified Data.ByteString.Char8 as BS
51import Data.Map.Strict (Map)
52import Data.Either.Extra
54import qualified Data.Text as T
56import Development.IDE.Core.Debouncer
57import Development.IDE.Types.Logger hiding (Priority)
58import Language.Haskell.LSP.Diagnostics
59import qualified Data.SortedList as SL
60import Development.IDE.Types.Diagnostics
61import Development.IDE.Types.Location
62import Control.Concurrent.Extra
63import Control.Exception
65import System.Time.Extra
67import qualified Language.Haskell.LSP.Messages as LSP
68import qualified Language.Haskell.LSP.Types as LSP
69import System.FilePath hiding (makeRelative)
70import qualified Development.Shake as Shake
71import Control.Monad.Extra
74import System.IO.Unsafe
76import Language.Haskell.LSP.Types
79-- information we stash inside the shakeExtra field
80data ShakeExtras = ShakeExtras
81 {eventer :: LSP.FromServerMessage -> IO ()
82 ,debouncer :: Debouncer NormalizedUri
84 ,globals :: Var (HMap.HashMap TypeRep Dynamic)
86 ,diagnostics :: Var DiagnosticStore
87 ,publishedDiagnostics :: Var (Map NormalizedUri [Diagnostic])
88 -- ^ This represents the set of diagnostics that we have published.
89 -- Due to debouncing not every change might get published.
92getShakeExtras :: Action ShakeExtras
94 Just x <- getShakeExtra @ShakeExtras
97getShakeExtrasRules :: Rules ShakeExtras
98getShakeExtrasRules = do
99 Just x <- getShakeExtraRules @ShakeExtras
104class Typeable a => IsIdeGlobal a where
106addIdeGlobal :: IsIdeGlobal a => a -> Rules ()
107addIdeGlobal x@(typeOf -> ty) = do
108 ShakeExtras{globals} <- getShakeExtrasRules
109 liftIO $ modifyVar_ globals $ \mp -> case HMap.lookup ty mp of
110 Just _ -> error $ "Can't addIdeGlobal twice on the same type, got " ++ show ty
111 Nothing -> return $! HMap.insert ty (toDyn x) mp
114getIdeGlobalExtras :: forall a . IsIdeGlobal a => ShakeExtras -> IO a
115getIdeGlobalExtras ShakeExtras{globals} = do
116 Just x <- HMap.lookup (typeRep (Proxy :: Proxy a)) <$> readVar globals
117 return $ fromDyn x $ error "Serious error, corrupt globals"
119getIdeGlobalAction :: forall a . IsIdeGlobal a => Action a
120getIdeGlobalAction = liftIO . getIdeGlobalExtras =<< getShakeExtras
122getIdeGlobalState :: forall a . IsIdeGlobal a => IdeState -> IO a
123getIdeGlobalState = getIdeGlobalExtras . shakeExtras
126-- | The state of the all values.
127type Values = HMap.HashMap (NormalizedFilePath, Key) (Value Dynamic)
130data Key = forall k . (Typeable k, Hashable k, Eq k, Show k) => Key k
132instance Show Key where
133 show (Key k) = show k
136 Key k1 == Key k2 | Just k2' <- cast k2 = k1 == k2'
139instance Hashable Key where
140 hashWithSalt salt (Key key) = hashWithSalt salt key
142-- | The result of an IDE operation. Warnings and errors are in the Diagnostic,
143-- and a value is in the Maybe. For operations that throw an error you
144-- expect a non-empty list of diagnostics, at least one of which is an error,
145-- and a Nothing. For operations that succeed you expect perhaps some warnings
146-- and a Just. For operations that depend on other failing operations you may
147-- get empty diagnostics and a Nothing, to indicate this phase throws no fresh
148-- errors but still failed.
150-- A rule on a file should only return diagnostics for that given file. It should
151-- not propagate diagnostic errors through multiple phases.
152type IdeResult v = ([FileDiagnostic], Maybe v)
157 deriving (Functor, Generic, Show)
159instance NFData v => NFData (Value v)
161-- | Convert a Value to a Maybe. This will only return `Just` for
162-- up2date results not for stale values.
163valueToMaybe :: Value v -> Maybe v
164valueToMaybe (Succeeded v) = Just v
165valueToMaybe Failed = Nothing
167-- | Convert a Value to a Maybe. A `Just` will be treated as a
168-- succesful run rather than a stale result
169maybeToValue :: Maybe v -> Value v
170maybeToValue (Just v) = Succeeded v
171maybeToValue Nothing = Failed
174 ( Shake.RuleResult k ~ v
186-- | A Shake database plus persistent store. Can be thought of as storing
187-- mappings from @(FilePath, k)@ to @RuleResult k@.
188data IdeState = IdeState
189 {shakeDb :: ShakeDatabase
190 ,shakeAbort :: Var (IO ()) -- close whoever was running last
192 ,shakeExtras :: ShakeExtras
195profileDir :: Maybe FilePath
196profileDir = Nothing -- set to Just the directory you want profile reports to appear in
199-- This is debugging code that generates a series of profiles, if the Boolean is true
200shakeRunDatabaseProfile :: ShakeDatabase -> [Action a] -> IO [a]
201shakeRunDatabaseProfile shakeDb acts = do
202 (time, (res,_)) <- duration $ shakeRunDatabase shakeDb acts
203 whenJust profileDir $ \dir -> do
204 count <- modifyVar profileCounter $ \x -> let y = x+1 in return (y,y)
205 let file = "ide-" ++ profileStartTime ++ "-" ++ takeEnd 5 ("0000" ++ show count) ++ "-" ++ showDP 2 time <.> "html"
206 shakeProfileDatabase shakeDb $ dir </> file
210{-# NOINLINE profileStartTime #-}
211profileStartTime :: String
212profileStartTime = unsafePerformIO $ formatTime defaultTimeLocale "%Y%m%d-%H%M%S" <$> getCurrentTime
214{-# NOINLINE profileCounter #-}
215profileCounter :: Var Int
216profileCounter = unsafePerformIO $ newVar 0
218setValues :: IdeRule k v
221 -> NormalizedFilePath
224setValues state key file val = modifyVar_ state $
225 pure . HMap.insert (file, Key key) (fmap toDyn val)
227-- | We return Nothing if the rule has not run and Just Failed if it has failed to produce a value.
228getValues :: forall k v. IdeRule k v => Var Values -> k -> NormalizedFilePath -> IO (Maybe (Value v))
229getValues state key file = do
232 v <- HMap.lookup (file, Key key) vs
233 pure $ fmap (fromJust . fromDynamic @v) v
235-- | Open a 'IdeState', should be shut using 'shakeShut'.
236shakeOpen :: (LSP.FromServerMessage -> IO ()) -- ^ diagnostic handler
241shakeOpen eventer logger opts rules = do
243 globals <- newVar HMap.empty
244 state <- newVar HMap.empty
245 diagnostics <- newVar mempty
246 publishedDiagnostics <- newVar mempty
247 debouncer <- newDebouncer
249 (shakeDb, shakeClose) <-
252 { shakeExtra = addShakeExtra shakeExtras $ shakeExtra opts
253 , shakeProgress = lspShakeProgress eventer
256 shakeAbort <- newVar $ return ()
260lspShakeProgress :: (LSP.FromServerMessage -> IO ()) -> IO Progress -> IO ()
261lspShakeProgress sendMsg prog = do
262 u <- T.pack . show . hashUnique <$> newUnique
263 bracket_ (start u) (stop u) (loop u)
265 start id = sendMsg $ LSP.NotProgressStart $ LSP.fmServerProgressStartNotification
268 , _title = "Processing"
269 , _cancellable = Nothing
271 , _percentage = Nothing
273 stop id = sendMsg $ LSP.NotProgressDone $ LSP.fmServerProgressDoneNotification
278 loop id = forever $ do
281 let done = countSkipped p + countBuilt p
282 let todo = done + countUnknown p + countTodo p
283 sendMsg $ LSP.NotProgressReport $ LSP.fmServerProgressReportNotification
286 , _message = Just $ T.pack $ show done <> "/" <> show todo
287 , _percentage = Nothing
290shakeProfile :: IdeState -> FilePath -> IO ()
291shakeProfile IdeState{..} = shakeProfileDatabase shakeDb
293shakeShut :: IdeState -> IO ()
294shakeShut IdeState{..} = withVar shakeAbort $ \stop -> do
295 -- Shake gets unhappy if you try to close when there is a running
296 -- request so we first abort that.
300-- | Spawn immediately. If you are already inside a call to shakeRun that will be aborted with an exception.
301shakeRun :: IdeState -> [Action a] -> IO (IO [a])
302-- FIXME: If there is already a shakeRun queued up and waiting to send me a kill, I should probably
303-- not even start, which would make issues with async exceptions less problematic.
304shakeRun IdeState{shakeExtras=ShakeExtras{..}, ..} acts = modifyVar shakeAbort $ \stop -> do
305 (stopTime,_) <- duration stop
306 logDebug logger $ T.pack $ "Starting shakeRun (aborting the previous one took " ++ showDuration stopTime ++ ")"
309 thread <- forkFinally (shakeRunDatabaseProfile shakeDb acts) $ \res -> do
310 signalBarrier bar res
312 logDebug logger $ T.pack $
313 "Finishing shakeRun (took " ++ showDuration runTime ++ ", " ++ (if isLeft res then "exception" else "completed") ++ ")"
314 -- important: we send an async exception to the thread, then wait for it to die, before continuing
315 return (do killThread thread; void $ waitBarrier bar, either throwIO return =<< waitBarrier bar)
317getDiagnostics :: IdeState -> IO [FileDiagnostic]
318getDiagnostics IdeState{shakeExtras = ShakeExtras{diagnostics}} = do
319 val <- readVar diagnostics
320 return $ getAllDiagnostics val
322-- | FIXME: This function is temporary! Only required because the files of interest doesn't work
323unsafeClearDiagnostics :: IdeState -> IO ()
324unsafeClearDiagnostics IdeState{shakeExtras = ShakeExtras{diagnostics}} =
325 writeVar diagnostics mempty
327-- | Clear the results for all files that do not match the given predicate.
328garbageCollect :: (NormalizedFilePath -> Bool) -> Action ()
329garbageCollect keep = do
330 ShakeExtras{state, diagnostics} <- getShakeExtras
332 do modifyVar_ state $ return . HMap.filterWithKey (\(file, _) _ -> keep file)
333 modifyVar_ diagnostics $ return . filterDiagnostics keep
337 => (k -> NormalizedFilePath -> Action (IdeResult v)) -> Rules ()
338define op = defineEarlyCutoff $ \k v -> (Nothing,) <$> op k v
341 => k -> NormalizedFilePath -> Action (Maybe v)
342use key file = head <$> uses key [file]
344useNoFile :: IdeRule k v => k -> Action (Maybe v)
345useNoFile key = use key ""
347use_ :: IdeRule k v => k -> NormalizedFilePath -> Action v
348use_ key file = head <$> uses_ key [file]
350useNoFile_ :: IdeRule k v => k -> Action v
351useNoFile_ key = use_ key ""
353uses_ :: IdeRule k v => k -> [NormalizedFilePath] -> Action [v]
355 res <- uses key files
357 Nothing -> liftIO $ throwIO BadDependency
361-- | When we depend on something that reported an error, and we fail as a direct result, throw BadDependency
362-- which short-circuits the rest of the action
363data BadDependency = BadDependency deriving Show
364instance Exception BadDependency
366isBadDependency :: SomeException -> Bool
368 | Just (x :: ShakeException) <- fromException x = isBadDependency $ shakeExceptionInner x
369 | Just (_ :: BadDependency) <- fromException x = True
372newtype Q k = Q (k, NormalizedFilePath)
373 deriving (Eq,Hashable,NFData)
375-- Using Database we don't need Binary instances for keys
376instance Binary (Q k) where
378 get = fail "Binary.get not defined for type Development.IDE.Core.Shake.Q"
380instance Show k => Show (Q k) where
381 show (Q (k, file)) = show k ++ "; " ++ fromNormalizedFilePath file
383-- | Invariant: the 'v' must be in normal form (fully evaluated).
384-- Otherwise we keep repeatedly 'rnf'ing values taken from the Shake database
385data A v = A (Value v) (Maybe BS.ByteString)
388instance NFData (A v) where rnf (A v x) = v `seq` rnf x
390-- In the Shake database we only store one type of key/result pairs,
391-- namely Q (question) / A (answer).
392type instance RuleResult (Q k) = A (RuleResult k)
395-- | Compute the value
397 => k -> [NormalizedFilePath] -> Action [Maybe v]
398uses key files = map (\(A value _) -> valueToMaybe value) <$> apply (map (Q . (key,)) files)
402 => (k -> NormalizedFilePath -> Action (Maybe BS.ByteString, IdeResult v))
404defineEarlyCutoff op = addBuiltinRule noLint noIdentity $ \(Q (key, file)) old mode -> do
405 extras@ShakeExtras{state} <- getShakeExtras
407 Just old | mode == RunDependenciesSame -> do
408 v <- liftIO $ getValues state key file
410 Just v -> return $ Just $ RunResult ChangedNothing old $ A v (unwrap old)
414 Just res -> return res
416 (bs, (diags, res)) <- actionCatch
417 (do v <- op key file; liftIO $ evaluate $ force $ v) $
418 \(e :: SomeException) -> pure (Nothing, ([ideErrorText file $ T.pack $ show e | not $ isBadDependency e],Nothing))
419 res <- pure $ maybeToValue res
421 liftIO $ setValues state key file res
422 updateFileDiagnostics file (Key key) extras $ map snd diags
423 let eq = case (bs, fmap unwrap old) of
424 (Just a, Just (Just b)) -> a == b
427 (if eq then ChangedRecomputeSame else ChangedRecomputeDiff)
431 wrap = maybe BS.empty (BS.cons '_')
432 unwrap x = if BS.null x then Nothing else Just $ BS.tail x
434updateFileDiagnostics ::
438 -> [Diagnostic] -- ^ current results
440updateFileDiagnostics fp k ShakeExtras{diagnostics, publishedDiagnostics, state, debouncer, eventer} current = liftIO $ do
441 modTime <- join . fmap valueToMaybe <$> getValues state GetModificationTime fp
443 -- Mask async exceptions to ensure that updated diagnostics are always
444 -- published. Otherwise, we might never publish certain diagnostics if
445 -- an exception strikes between modifyVar but before
446 -- publishDiagnosticsNotification.
447 newDiags <- modifyVar diagnostics $ \old -> do
448 let newDiagsStore = setStageDiagnostics fp (vfsVersion =<< modTime) (T.pack $ show k) current old
449 let newDiags = getFileDiagnostics fp newDiagsStore
450 pure (newDiagsStore, newDiags)
451 let uri = filePathToUri' fp
452 let delay = if null newDiags then 0.1 else 0
453 registerEvent debouncer delay uri $ do
454 mask_ $ modifyVar_ publishedDiagnostics $ \published -> do
455 let lastPublish = Map.findWithDefault [] uri published
456 when (lastPublish /= newDiags) $
457 eventer $ publishDiagnosticsNotification (fromNormalizedUri uri) newDiags
458 pure (Map.insert uri newDiags published)
460publishDiagnosticsNotification :: Uri -> [Diagnostic] -> LSP.FromServerMessage
461publishDiagnosticsNotification uri diags =
462 LSP.NotPublishDiagnostics $
463 LSP.NotificationMessage "2.0" LSP.TextDocumentPublishDiagnostics $
464 LSP.PublishDiagnosticsParams uri (List diags)
466newtype Priority = Priority Double
468setPriority :: Priority -> Action ()
469setPriority (Priority p) = deprioritize p
471sendEvent :: LSP.FromServerMessage -> Action ()
473 ShakeExtras{eventer} <- getShakeExtras
476ideLogger :: IdeState -> Logger
477ideLogger IdeState{shakeExtras=ShakeExtras{logger}} = logger
479actionLogger :: Action Logger
481 ShakeExtras{logger} <- getShakeExtras
485data GetModificationTime = GetModificationTime
486 deriving (Eq, Show, Generic)
487instance Hashable GetModificationTime
488instance NFData GetModificationTime
490-- | Get the modification time of a file.
491type instance RuleResult GetModificationTime = FileVersion
493data FileVersion = VFSVersion Int | ModificationTime UTCTime
494 deriving (Show, Generic)
496instance NFData FileVersion
498vfsVersion :: FileVersion -> Maybe Int
499vfsVersion (VFSVersion i) = Just i
500vfsVersion (ModificationTime _) = Nothing
504getDiagnosticsFromStore :: StoreItem -> [Diagnostic]
505getDiagnosticsFromStore (StoreItem _ diags) = concatMap SL.fromSortedList $ Map.elems diags
508-- | Sets the diagnostics for a file and compilation step
509-- if you want to clear the diagnostics call this with an empty list
511 :: NormalizedFilePath
512 -> TextDocumentVersion -- ^ the time that the file these diagnostics originate from was last edited
517setStageDiagnostics fp timeM stage diags ds =
518 updateDiagnostics ds uri timeM diagsBySource
520 diagsBySource = Map.singleton (Just stage) (SL.toSortedList diags)
521 uri = filePathToUri' fp
527 concatMap (\(k,v) -> map (fromUri k,) $ getDiagnosticsFromStore v) . Map.toList
530 NormalizedFilePath ->
533getFileDiagnostics fp ds =
534 maybe [] getDiagnosticsFromStore $
535 Map.lookup (filePathToUri' fp) ds
538 (NormalizedFilePath -> Bool) ->
541filterDiagnostics keep =
542 Map.filterWithKey (\uri _ -> maybe True (keep . toNormalizedFilePath) $ uriToFilePath' $ fromNormalizedUri uri)
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list