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.
22-- Additionally, Values can be used in an inconsistent way, for example
24module Development.IDE.Core.Shake(
26 IdeRule, IdeResult, GetModificationTime(..),
32 use_, useNoFile_, uses_,
33 define, defineEarlyCutoff,
34 getDiagnostics, unsafeClearDiagnostics,
35 IsIdeGlobal, addIdeGlobal, getIdeGlobalState, getIdeGlobalAction,
45import Development.Shake
46import Development.Shake.Database
47import Development.Shake.Classes
48import Development.Shake.Rule
49import qualified Data.HashMap.Strict as HMap
50import qualified Data.Map as Map
51import qualified Data.ByteString.Char8 as BS
54import Data.Map.Strict (Map)
55import Data.Either.Extra
57import qualified Data.Text as T
59import Development.IDE.Core.Debouncer
60import Development.IDE.Types.Logger hiding (Priority)
61import Language.Haskell.LSP.Diagnostics
62import qualified Data.SortedList as SL
63import Development.IDE.Types.Diagnostics
64import Development.IDE.Types.Location
65import Control.Concurrent.Extra
66import Control.Exception
68import System.Time.Extra
70import qualified Language.Haskell.LSP.Messages as LSP
71import qualified Language.Haskell.LSP.Types as LSP
72import System.FilePath hiding (makeRelative)
73import qualified Development.Shake as Shake
74import Control.Monad.Extra
77import System.IO.Unsafe
79import Language.Haskell.LSP.Types
82-- information we stash inside the shakeExtra field
83data ShakeExtras = ShakeExtras
84 {eventer :: LSP.FromServerMessage -> IO ()
85 ,debouncer :: Debouncer NormalizedUri
87 ,globals :: Var (HMap.HashMap TypeRep Dynamic)
89 ,diagnostics :: Var DiagnosticStore
90 ,publishedDiagnostics :: Var (Map NormalizedUri [Diagnostic])
91 -- ^ This represents the set of diagnostics that we have published.
92 -- Due to debouncing not every change might get published.
95getShakeExtras :: Action ShakeExtras
97 Just x <- getShakeExtra @ShakeExtras
100getShakeExtrasRules :: Rules ShakeExtras
101getShakeExtrasRules = do
102 Just x <- getShakeExtraRules @ShakeExtras
107class Typeable a => IsIdeGlobal a where
109addIdeGlobal :: IsIdeGlobal a => a -> Rules ()
110addIdeGlobal x@(typeOf -> ty) = do
111 ShakeExtras{globals} <- getShakeExtrasRules
112 liftIO $ modifyVar_ globals $ \mp -> case HMap.lookup ty mp of
113 Just _ -> error $ "Can't addIdeGlobal twice on the same type, got " ++ show ty
114 Nothing -> return $! HMap.insert ty (toDyn x) mp
117getIdeGlobalExtras :: forall a . IsIdeGlobal a => ShakeExtras -> IO a
118getIdeGlobalExtras ShakeExtras{globals} = do
119 Just x <- HMap.lookup (typeRep (Proxy :: Proxy a)) <$> readVar globals
120 return $ fromDyn x $ error "Serious error, corrupt globals"
122getIdeGlobalAction :: forall a . IsIdeGlobal a => Action a
123getIdeGlobalAction = liftIO . getIdeGlobalExtras =<< getShakeExtras
125getIdeGlobalState :: forall a . IsIdeGlobal a => IdeState -> IO a
126getIdeGlobalState = getIdeGlobalExtras . shakeExtras
129-- | The state of the all values - nested so you can easily find all errors at a given file.
130type Values = HMap.HashMap (NormalizedFilePath, Key) (Maybe Dynamic)
133data Key = forall k . (Typeable k, Hashable k, Eq k, Show k) => Key k
135instance Show Key where
136 show (Key k) = show k
139 Key k1 == Key k2 | Just k2' <- cast k2 = k1 == k2'
142instance Hashable Key where
143 hashWithSalt salt (Key key) = hashWithSalt salt key
145-- | The result of an IDE operation. Warnings and errors are in the Diagnostic,
146-- and a value is in the Maybe. For operations that throw an error you
147-- expect a non-empty list of diagnostics, at least one of which is an error,
148-- and a Nothing. For operations that succeed you expect perhaps some warnings
149-- and a Just. For operations that depend on other failing operations you may
150-- get empty diagnostics and a Nothing, to indicate this phase throws no fresh
151-- errors but still failed.
153-- A rule on a file should only return diagnostics for that given file. It should
154-- not propagate diagnostic errors through multiple phases.
155type IdeResult v = ([FileDiagnostic], Maybe v)
158 ( Shake.RuleResult k ~ v
170-- | A Shake database plus persistent store. Can be thought of as storing
171-- mappings from @(FilePath, k)@ to @RuleResult k@.
172data IdeState = IdeState
173 {shakeDb :: ShakeDatabase
174 ,shakeAbort :: Var (IO ()) -- close whoever was running last
176 ,shakeExtras :: ShakeExtras
179profileDir :: Maybe FilePath
180profileDir = Nothing -- set to Just the directory you want profile reports to appear in
183-- This is debugging code that generates a series of profiles, if the Boolean is true
184shakeRunDatabaseProfile :: ShakeDatabase -> [Action a] -> IO [a]
185shakeRunDatabaseProfile shakeDb acts = do
186 (time, (res,_)) <- duration $ shakeRunDatabase shakeDb acts
187 whenJust profileDir $ \dir -> do
188 count <- modifyVar profileCounter $ \x -> let y = x+1 in return (y,y)
189 let file = "ide-" ++ profileStartTime ++ "-" ++ takeEnd 5 ("0000" ++ show count) ++ "-" ++ showDP 2 time <.> "html"
190 shakeProfileDatabase shakeDb $ dir </> file
194{-# NOINLINE profileStartTime #-}
195profileStartTime :: String
196profileStartTime = unsafePerformIO $ formatTime defaultTimeLocale "%Y%m%d-%H%M%S" <$> getCurrentTime
198{-# NOINLINE profileCounter #-}
199profileCounter :: Var Int
200profileCounter = unsafePerformIO $ newVar 0
202setValues :: IdeRule k v
205 -> NormalizedFilePath
208setValues state key file val = modifyVar_ state $
209 pure . HMap.insert (file, Key key) (fmap toDyn val)
211-- | The outer Maybe is Nothing if this function hasn't been computed before
212-- the inner Maybe is Nothing if the result of the previous computation failed to produce
214getValues :: forall k v. IdeRule k v => Var Values -> k -> NormalizedFilePath -> IO (Maybe (Maybe v))
215getValues state key file = do
218 v <- HMap.lookup (file, Key key) vs
219 pure $ fmap (fromJust . fromDynamic @v) v
221-- | Open a 'IdeState', should be shut using 'shakeShut'.
222shakeOpen :: (LSP.FromServerMessage -> IO ()) -- ^ diagnostic handler
227shakeOpen eventer logger opts rules = do
229 globals <- newVar HMap.empty
230 state <- newVar HMap.empty
231 diagnostics <- newVar mempty
232 publishedDiagnostics <- newVar mempty
233 debouncer <- newDebouncer
235 (shakeDb, shakeClose) <-
238 { shakeExtra = addShakeExtra shakeExtras $ shakeExtra opts
239 , shakeProgress = lspShakeProgress eventer
242 shakeAbort <- newVar $ return ()
246lspShakeProgress :: (LSP.FromServerMessage -> IO ()) -> IO Progress -> IO ()
247lspShakeProgress sendMsg prog = do
248 u <- T.pack . show . hashUnique <$> newUnique
249 bracket_ (start u) (stop u) (loop u)
251 start id = sendMsg $ LSP.NotProgressStart $ LSP.fmServerProgressStartNotification
254 , _title = "Processing"
255 , _cancellable = Nothing
257 , _percentage = Nothing
259 stop id = sendMsg $ LSP.NotProgressDone $ LSP.fmServerProgressDoneNotification
264 loop id = forever $ do
267 let done = countSkipped p + countBuilt p
268 let todo = done + countUnknown p + countTodo p
269 sendMsg $ LSP.NotProgressReport $ LSP.fmServerProgressReportNotification
272 , _message = Just $ T.pack $ show done <> "/" <> show todo
273 , _percentage = Nothing
276shakeProfile :: IdeState -> FilePath -> IO ()
277shakeProfile IdeState{..} = shakeProfileDatabase shakeDb
279shakeShut :: IdeState -> IO ()
280shakeShut IdeState{..} = withVar shakeAbort $ \stop -> do
281 -- Shake gets unhappy if you try to close when there is a running
282 -- request so we first abort that.
286-- | Spawn immediately. If you are already inside a call to shakeRun that will be aborted with an exception.
287shakeRun :: IdeState -> [Action a] -> IO (IO [a])
288-- FIXME: If there is already a shakeRun queued up and waiting to send me a kill, I should probably
289-- not even start, which would make issues with async exceptions less problematic.
290shakeRun IdeState{shakeExtras=ShakeExtras{..}, ..} acts = modifyVar shakeAbort $ \stop -> do
291 (stopTime,_) <- duration stop
292 logDebug logger $ T.pack $ "Starting shakeRun (aborting the previous one took " ++ showDuration stopTime ++ ")"
295 thread <- forkFinally (shakeRunDatabaseProfile shakeDb acts) $ \res -> do
296 signalBarrier bar res
298 logDebug logger $ T.pack $
299 "Finishing shakeRun (took " ++ showDuration runTime ++ ", " ++ (if isLeft res then "exception" else "completed") ++ ")"
300 -- important: we send an async exception to the thread, then wait for it to die, before continuing
301 return (do killThread thread; void $ waitBarrier bar, either throwIO return =<< waitBarrier bar)
303-- | Use the last stale value, if it's ever been computed.
306 => IdeState -> k -> NormalizedFilePath -> IO (Maybe v)
307useStale IdeState{shakeExtras=ShakeExtras{state}} k fp =
308 join <$> getValues state k fp
311getDiagnostics :: IdeState -> IO [FileDiagnostic]
312getDiagnostics IdeState{shakeExtras = ShakeExtras{diagnostics}} = do
313 val <- readVar diagnostics
314 return $ getAllDiagnostics val
316-- | FIXME: This function is temporary! Only required because the files of interest doesn't work
317unsafeClearDiagnostics :: IdeState -> IO ()
318unsafeClearDiagnostics IdeState{shakeExtras = ShakeExtras{diagnostics}} =
319 writeVar diagnostics mempty
321-- | Clear the results for all files that do not match the given predicate.
322garbageCollect :: (NormalizedFilePath -> Bool) -> Action ()
323garbageCollect keep = do
324 ShakeExtras{state, diagnostics} <- getShakeExtras
326 do modifyVar_ state $ return . HMap.filterWithKey (\(file, _) _ -> keep file)
327 modifyVar_ diagnostics $ return . filterDiagnostics keep
331 => (k -> NormalizedFilePath -> Action (IdeResult v)) -> Rules ()
332define op = defineEarlyCutoff $ \k v -> (Nothing,) <$> op k v
335 => k -> NormalizedFilePath -> Action (Maybe v)
336use key file = head <$> uses key [file]
338useNoFile :: IdeRule k v => k -> Action (Maybe v)
339useNoFile key = use key ""
341use_ :: IdeRule k v => k -> NormalizedFilePath -> Action v
342use_ key file = head <$> uses_ key [file]
344useNoFile_ :: IdeRule k v => k -> Action v
345useNoFile_ key = use_ key ""
347uses_ :: IdeRule k v => k -> [NormalizedFilePath] -> Action [v]
349 res <- uses key files
351 Nothing -> liftIO $ throwIO BadDependency
355-- | When we depend on something that reported an error, and we fail as a direct result, throw BadDependency
356-- which short-circuits the rest of the action
357data BadDependency = BadDependency deriving Show
358instance Exception BadDependency
360isBadDependency :: SomeException -> Bool
362 | Just (x :: ShakeException) <- fromException x = isBadDependency $ shakeExceptionInner x
363 | Just (_ :: BadDependency) <- fromException x = True
366newtype Q k = Q (k, NormalizedFilePath)
367 deriving (Eq,Hashable,NFData)
369-- Using Database we don't need Binary instances for keys
370instance Binary (Q k) where
372 get = fail "Binary.get not defined for type Development.IDE.Core.Shake.Q"
374instance Show k => Show (Q k) where
375 show (Q (k, file)) = show k ++ "; " ++ fromNormalizedFilePath file
377-- | Invariant: the 'v' must be in normal form (fully evaluated).
378-- Otherwise we keep repeatedly 'rnf'ing values taken from the Shake database
379data A v = A (Maybe v) (Maybe BS.ByteString)
382instance NFData (A v) where rnf (A v x) = v `seq` rnf x
384-- In the Shake database we only store one type of key/result pairs,
385-- namely Q (question) / A (answer).
386type instance RuleResult (Q k) = A (RuleResult k)
389-- | Compute the value
391 => k -> [NormalizedFilePath] -> Action [Maybe v]
392uses key files = map (\(A value _) -> value) <$> apply (map (Q . (key,)) files)
396 => (k -> NormalizedFilePath -> Action (Maybe BS.ByteString, IdeResult v))
398defineEarlyCutoff op = addBuiltinRule noLint noIdentity $ \(Q (key, file)) old mode -> do
399 extras@ShakeExtras{state} <- getShakeExtras
401 Just old | mode == RunDependenciesSame -> do
402 v <- liftIO $ getValues state key file
404 Just v -> return $ Just $ RunResult ChangedNothing old $ A v (unwrap old)
408 Just res -> return res
410 (bs, (diags, res)) <- actionCatch
411 (do v <- op key file; liftIO $ evaluate $ force v) $
412 \(e :: SomeException) -> pure (Nothing, ([ideErrorText file $ T.pack $ show e | not $ isBadDependency e],Nothing))
414 liftIO $ setValues state key file res
415 updateFileDiagnostics file (Key key) extras $ map snd diags
416 let eq = case (bs, fmap unwrap old) of
417 (Just a, Just (Just b)) -> a == b
420 (if eq then ChangedRecomputeSame else ChangedRecomputeDiff)
424 wrap = maybe BS.empty (BS.cons '_')
425 unwrap x = if BS.null x then Nothing else Just $ BS.tail x
427updateFileDiagnostics ::
431 -> [Diagnostic] -- ^ current results
433updateFileDiagnostics fp k ShakeExtras{diagnostics, publishedDiagnostics, state, debouncer, eventer} current = liftIO $ do
434 modTime <- join <$> getValues state GetModificationTime fp
436 -- Mask async exceptions to ensure that updated diagnostics are always
437 -- published. Otherwise, we might never publish certain diagnostics if
438 -- an exception strikes between modifyVar but before
439 -- publishDiagnosticsNotification.
440 newDiags <- modifyVar diagnostics $ \old -> do
441 let newDiagsStore = setStageDiagnostics fp (vfsVersion =<< modTime) (T.pack $ show k) current old
442 let newDiags = getFileDiagnostics fp newDiagsStore
443 pure (newDiagsStore, newDiags)
444 let uri = filePathToUri' fp
445 let delay = if null newDiags then 0.1 else 0
446 registerEvent debouncer delay uri $ do
447 mask_ $ modifyVar_ publishedDiagnostics $ \published -> do
448 let lastPublish = Map.findWithDefault [] uri published
449 when (lastPublish /= newDiags) $
450 eventer $ publishDiagnosticsNotification (fromNormalizedUri uri) newDiags
451 pure (Map.insert uri newDiags published)
453publishDiagnosticsNotification :: Uri -> [Diagnostic] -> LSP.FromServerMessage
454publishDiagnosticsNotification uri diags =
455 LSP.NotPublishDiagnostics $
456 LSP.NotificationMessage "2.0" LSP.TextDocumentPublishDiagnostics $
457 LSP.PublishDiagnosticsParams uri (List diags)
459newtype Priority = Priority Double
461setPriority :: Priority -> Action ()
462setPriority (Priority p) = deprioritize p
464sendEvent :: LSP.FromServerMessage -> Action ()
466 ShakeExtras{eventer} <- getShakeExtras
469ideLogger :: IdeState -> Logger
470ideLogger IdeState{shakeExtras=ShakeExtras{logger}} = logger
472actionLogger :: Action Logger
474 ShakeExtras{logger} <- getShakeExtras
478data GetModificationTime = GetModificationTime
479 deriving (Eq, Show, Generic)
480instance Hashable GetModificationTime
481instance NFData GetModificationTime
483-- | Get the modification time of a file.
484type instance RuleResult GetModificationTime = FileVersion
486data FileVersion = VFSVersion Int | ModificationTime UTCTime
487 deriving (Show, Generic)
489instance NFData FileVersion
491vfsVersion :: FileVersion -> Maybe Int
492vfsVersion (VFSVersion i) = Just i
493vfsVersion (ModificationTime _) = Nothing
497getDiagnosticsFromStore :: StoreItem -> [Diagnostic]
498getDiagnosticsFromStore (StoreItem _ diags) = concatMap SL.fromSortedList $ Map.elems diags
501-- | Sets the diagnostics for a file and compilation step
502-- if you want to clear the diagnostics call this with an empty list
504 :: NormalizedFilePath
505 -> TextDocumentVersion -- ^ the time that the file these diagnostics originate from was last edited
510setStageDiagnostics fp timeM stage diags ds =
511 updateDiagnostics ds uri timeM diagsBySource
513 diagsBySource = Map.singleton (Just stage) (SL.toSortedList diags)
514 uri = filePathToUri' fp
520 concatMap (\(k,v) -> map (fromUri k,) $ getDiagnosticsFromStore v) . Map.toList
523 NormalizedFilePath ->
526getFileDiagnostics fp ds =
527 maybe [] getDiagnosticsFromStore $
528 Map.lookup (filePathToUri' fp) ds
531 (NormalizedFilePath -> Bool) ->
534filterDiagnostics keep =
535 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