on master help sign in
Multi Component (#522)

* Multi component support In this commit we add support for loading multiple components into one ghcide session. The current behaviour is that each component is loaded lazily into the session. When a file from an unrecognised component is loaded, the cradle is consulted again to get a new set of options for the new component. This will cause all the currently loaded files to be reloaded into a new HscEnv which is shared by all the currently known components. The result of this is that functions such as go-to definition work between components if they have been loaded into the same session but you have to open at least one file from each component before it will work. Only minimal changes are needed to the internals to ghcide to make the file searching logic look in include directories for all currently loaded components. The main changes are in exe/Main.hs which has been heavily rewritten to avoid shake indirections. A global map is created which maps a filepath to the HscEnv which should be used to compile it. When a new component is created this map is completely refreshed so each path maps to a new Which paths belong to a componenent is determined by the targets listed by the cradle. Therefore it is important that each cradle also lists all the targets for the cradle. There are some other choices here as well which are less accurate such as mapping via include directories which is the aproach that I implemented in haskell-ide-engine. The commit has been tested so far with cabal and hadrian. Also deleted the .ghci file which was causing errors during testing and seemed broken anyway. Co-authored-by: Alan Zimmerman <alan.zimm@gmail.com> Co-authored-by: fendor <power.walross@gmail.com> * Final tweaks? * Fix 8.4 build * Add multi-component test * Fix hlint * Add cabal to CI images * Modify path * Set PATH in the right place (hopefully) * Always generate interface files and hie files * Use correct DynFlags in mkImportDirs You have to use the DynFlags for the file we are currently compiling to get the right packages in the package db so that lookupPackage doesn't always fail. * Revert "Always generate interface files and hie files" This reverts commit 820aa241890c4498c566e29b0823a803fb2fd297. * remove traces * Another test * lint * Unset env vars set my stack * Fix extra-source-files As usual, stack doesn’t understand Cabal properly and doesn’t seem to like ** wildcards so I’ve enumerated it manually. * Unset env locally Co-authored-by: Alan Zimmerman <alan.zimm@gmail.com> Co-authored-by: fendor <power.walross@gmail.com> Co-authored-by: Moritz Kiefer <moritz.kiefer@purelyfunctional.org>

commit373c4060dfb4cef21fa40cc6085aa00abd040d5b authorMatthew Pickering <matthewtpickering@gmail.com> dateTue 2 Jun 2020, 13:44 (6 years ago) parent51907fe
30 files changed+855−439
.azure/linux-stack.yml+6−2 .azure/windows-stack.yml+1 .ghci−25 README.md+18−1 exe/Main.hs+520−61 exe/RuleTypes.hs−34 exe/Rules.hs−147 exe/Util.hs−62 exe/Utils.hs+9 ghcide.cabal+10−4 hie.yaml+10−1 src/Development/IDE/Core/Compile.hs+23−5 src/Development/IDE/Core/Rules.hs+39−13 src/Development/IDE/Core/Shake.hs−3 src/Development/IDE/GHC/Compat.hs+5 src/Development/IDE/GHC/Util.hs+16−8 src/Development/IDE/Import/DependencyInformation.hs+2−2 src/Development/IDE/Import/FindImports.hs+32−14 src/Development/IDE/Types/Diagnostics.hs+4 src/Development/IDE/Types/Options.hs+4−2 test/data/Bar.hs → test/data/hover/Bar.hs test/data/Foo.hs → test/data/hover/Foo.hs test/data/GotoHover.hs → test/data/hover/GotoHover.hs test/data/multi/a/A.hs+3 test/data/multi/a/a.cabal+9 test/data/multi/b/B.hs+3 test/data/multi/b/b.cabal+9 test/data/multi/cabal.project+1 test/data/multi/hie.yaml+6 test/exe/Main.hs+125−55
.azure/linux-stack.yml+6−2
@@ -32,7 +32,9 @@ jobs:
3232 ./fmt.sh
3333 displayName: "HLint via ./fmt.sh"
3434 - bash: |
35− sudo apt-get install -y g++ gcc libc6-dev libffi-dev libgmp-dev make zlib1g-dev
35+ sudo add-apt-repository ppa:hvr/ghc
36+ sudo apt-get update
37+ sudo apt-get install -y g++ gcc libc6-dev libffi-dev libgmp-dev make zlib1g-dev cabal-install-3.2
3638 if ! which stack >/dev/null 2>&1; then
3739 curl -sSL https://get.haskellstack.org/ | sh
3840 fi
@@ -41,7 +43,9 @@ jobs:
4143 displayName: 'stack setup'
4244 - bash: stack build --only-dependencies --stack-yaml=$STACK_YAML
4345 displayName: 'stack build --only-dependencies'
44− - bash: stack test --ghc-options=-Werror --stack-yaml=$STACK_YAML || stack test --ghc-options=-Werror --stack-yaml=$STACK_YAML|| stack test --ghc-options=-Werror --stack-yaml=$STACK_YAML
46+ - bash: |
47+ export PATH=/opt/cabal/bin:$PATH
48+ stack test --ghc-options=-Werror --stack-yaml=$STACK_YAML || stack test --ghc-options=-Werror --stack-yaml=$STACK_YAML|| stack test --ghc-options=-Werror --stack-yaml=$STACK_YAML
4549 # ghcide stack tests are flaky, see https://github.com/digital-asset/daml/issues/2606.
4650 displayName: 'stack test --ghc-options=-Werror'
4751 - bash: |
.azure/windows-stack.yml+1
@@ -46,6 +46,7 @@ jobs:
4646 # Installing happy and alex standalone to avoid error "strip.exe: unable to rename ../*.exe; reason: File exists"
4747 stack install happy --stack-yaml $STACK_YAML
4848 stack install alex --stack-yaml $STACK_YAML
49+ stack install cabal-install --stack-yaml $STACK_YAML
4950 stack build --only-dependencies --stack-yaml $STACK_YAML
5051 displayName: 'stack build --only-dependencies'
5152 - bash: stack test --no-run-tests --ghc-options=-Werror --stack-yaml $STACK_YAML
.ghcideleted−25
@@ -1,25 +0,0 @@
1−:set -Wunused-binds -Wunused-imports -Worphans -Wunused-matches -Wincomplete-patterns
2−
3−:set -XBangPatterns
4−:set -XDeriveFunctor
5−:set -XDeriveGeneric
6−:set -XGeneralizedNewtypeDeriving
7−:set -XLambdaCase
8−:set -XNamedFieldPuns
9−:set -XOverloadedStrings
10−:set -XRecordWildCards
11−:set -XScopedTypeVariables
12−:set -XStandaloneDeriving
13−:set -XTupleSections
14−:set -XTypeApplications
15−:set -XViewPatterns
16−
17−:set -package=ghc
18−:set -ignore-package=ghc-lib-parser
19−:set -DGHC_STABLE
20−:set -Iinclude
21−:set -idist/build/autogen
22−:set -isrc
23−:set -iexe
24−
25−:load Main
README.md+18−1
@@ -25,6 +25,23 @@ There are more details about our approach [in this blog post](https://4ta.uk/p/s
2525| Display type and source module of values | hover |
2626| Remove redundant imports, replace suggested typos for values and module imports, fill type holes, insert missing type signatures, add suggested ghc extensions | codeAction (quickfix) |
2727
28+
29+## Limitations to Multi-Component support
30+
31+`ghcide` supports loading multiple components into the same session so that
32+features such as go-to definition work across components. However, there are
33+some limitations to this.
34+
35+1. You will get much better results currently manually specifying the hie.yaml file.
36+Until tools like cabal and stack provide the right interface to support multi-component
37+projects, it is always advised to specify explicitly how your project partitions.
38+2. Cross-component features only work if you have loaded at least one file
39+from each component.
40+3. There is a known issue where if you have three components, such that A depends on B which depends on C
41+then if you load A and C into the session but not B then under certain situations you
42+can get strange errors about a type coming from two different places. See [this repo](https://github.com/fendor/ghcide-bad-interface-files) for
43+a simple reproduction of the bug.
44+
2845## Using it
2946
3047### Install `ghcide`
@@ -308,7 +325,7 @@ Now opening a `.hs` file should work with `ghcide`.
308325
309326## History and relationship to other Haskell IDE's
310327
311−The teams behind this project and the [`haskell-ide-engine`](https://github.com/haskell/haskell-ide-engine#readme) have agreed to join forces under the [`haskell-language-server` project](https://github.com/haskell/haskell-language-server), see the [original announcement](https://neilmitchell.blogspot.com/2020/01/one-haskell-ide-to-rule-them-all.html). The technical work is ongoing, with the likely model being that this project serves as the core, while plugins and integrations are kept in the [`haskell-language-server` project](https://github.com/haskell/haskell-language-server).
328+The teams behind this project and the [`haskell-ide-engine`](https://github.com/haskell/haskell-ide-engine#readme) have agreed to join forces under the [`haskell-language-server` project](https://github.com/haskell/haskell-language-server), see the [original announcement](https://neilmitchell.blogspot.com/2020/01/one-haskell-ide-to-rule-them-all.html). The technical work is ongoing, with the likely model being that this project serves as the core, while plugins and integrations are kept in the [`haskell-language-server` project](https://github.com/haskell/haskell-language-server).
312329
313330The code behind `ghcide` was originally developed by [Digital Asset](https://digitalasset.com/) as part of the [DAML programming language](https://github.com/digital-asset/daml). DAML is a smart contract language targeting distributed-ledger runtimes, based on [GHC](https://www.haskell.org/ghc/) with custom language extensions. The DAML programming language has [an IDE](https://webide.daml.com/), and work was done to separate off a reusable Haskell-only IDE (what is now `ghcide`) which the [DAML IDE then builds upon](https://github.com/digital-asset/daml/tree/master/compiler/damlc). Since that time, there have been various [non-Digital Asset contributors](https://github.com/digital-asset/ghcide/graphs/contributors), in addition to continued investment by Digital Asset. All contributions require a [Contributor License Agreement](https://cla.digitalasset.com/digital-asset/ghcide) that states you license the code under the [Apache License](LICENSE).
314331
exe/Main.hs+520−61
@@ -7,16 +7,26 @@
77
88module Main(main) where
99
10+import Linker (initDynLinker)
11+import Data.IORef
12+import NameCache
13+import Packages
14+import Module
1015import Arguments
11−import Data.Maybe
12−import Data.List.Extra
13−import System.FilePath
16+import Control.Concurrent.Async
1417import Control.Concurrent.Extra
1518import Control.Exception
1619import Control.Monad.Extra
1720import Control.Monad.IO.Class
1821import Data.Default
19−import System.Time.Extra
22+import Data.Either
23+import Data.Function
24+import Data.List.Extra
25+import Data.Maybe
26+import qualified Data.Text as T
27+import qualified Data.Text.IO as T
28+import Data.Time.Clock (UTCTime)
29+import Data.Version
2030import Development.IDE.Core.Debouncer
2131import Development.IDE.Core.FileStore
2232import Development.IDE.Core.OfInterest
@@ -33,25 +43,39 @@ import Development.IDE.GHC.Util
3343import Development.IDE.Plugin
3444import Development.IDE.Plugin.Completions as Completions
3545import Development.IDE.Plugin.CodeAction as CodeAction
36−import qualified Data.Text as T
37−import qualified Data.Text.IO as T
3846import qualified Language.Haskell.LSP.Core as LSP
3947import Language.Haskell.LSP.Messages
40−import Language.Haskell.LSP.Types (LspId(IdInt))
41−import Data.Version
48+import Language.Haskell.LSP.Types
49+import Data.Aeson (ToJSON(toJSON))
4250import Development.IDE.LSP.LanguageServer
4351import qualified System.Directory.Extra as IO
4452import System.Environment
4553import System.IO
4654import System.Exit
55+import System.FilePath
56+import System.Directory
57+import System.Time.Extra
58+import HIE.Bios.Environment (addCmdOpts, makeDynFlagsAbsolute)
4759import Paths_ghcide
4860import Development.GitRev
49−import Development.Shake (Action, Rules, action)
61+import Development.Shake (Action, action)
5062import qualified Data.HashSet as HashSet
63+import qualified Data.HashMap.Strict as HM
5164import qualified Data.Map.Strict as Map
52−import HIE.Bios
53−import Rules
54−import RuleTypes
65+import qualified Crypto.Hash.SHA1 as H
66+import qualified Data.ByteString.Char8 as B
67+import Data.ByteString.Base16 (encode)
68+import DynFlags (gopt_set, gopt_unset, updOptLevel, PackageFlag(..), PackageArg(..))
69+import GhcMonad
70+import HscTypes (HscEnv(..), ic_dflags)
71+import GHC hiding (def)
72+import GHC.Check ( VersionCheck(..), makeGhcVersionChecker )
73+import Data.Either.Extra
74+
75+import HIE.Bios.Cradle
76+import HIE.Bios.Types
77+
78+import Utils
5579
5680ghcideVersion :: IO String
5781ghcideVersion = do
@@ -97,14 +121,14 @@ main = do
97121 runLanguageServer options (pluginHandler plugins) onInitialConfiguration onConfigurationChange $ \getLspId event vfs caps -> do
98122 t <- t
99123 hPutStrLn stderr $ "Started LSP server in " ++ showDuration t
100− let options = (defaultIdeOptions $ loadSession dir)
124+ let options = (defaultIdeOptions $ loadSessionShake dir)
101125 { optReportProgress = clientSupportsProgress caps
102126 , optShakeProfiling = argsShakeProfiling
103127 , optTesting = argsTesting
104128 , optThreads = argsThreads
105129 }
106130 debouncer <- newAsyncDebouncer
107− initialise caps (cradleRules >> mainRule >> pluginRules plugins >> action kick)
131+ initialise caps (mainRule >> pluginRules plugins >> action kick)
108132 getLspId event (logger minBound) debouncer options vfs
109133 else do
110134 -- GHC produces messages with UTF8 in them, so make sure the terminal doesn't error
@@ -114,55 +138,32 @@ main = do
114138 putStrLn $ "Ghcide setup tester in " ++ dir ++ "."
115139 putStrLn "Report bugs at https://github.com/digital-asset/ghcide/issues"
116140
117− putStrLn $ "\nStep 1/6: Finding files to test in " ++ dir
141+ putStrLn $ "\nStep 1/4: Finding files to test in " ++ dir
118142 files <- expandFiles (argFiles ++ ["." | null argFiles])
119143 -- LSP works with absolute file paths, so try and behave similarly
120144 files <- nubOrd <$> mapM IO.canonicalizePath files
121145 putStrLn $ "Found " ++ show (length files) ++ " files"
122146
123− putStrLn "\nStep 2/6: Looking for hie.yaml files that control setup"
147+ putStrLn "\nStep 2/4: Looking for hie.yaml files that control setup"
124148 cradles <- mapM findCradle files
125149 let ucradles = nubOrd cradles
126150 let n = length ucradles
127151 putStrLn $ "Found " ++ show n ++ " cradle" ++ ['s' | n /= 1]
128− sessions <- forM (zipFrom (1 :: Int) ucradles) $ \(i, x) -> do
129− let msg = maybe ("Implicit cradle for " ++ dir) ("Loading " ++) x
130− putStrLn $ "\nStep 3/6, Cradle " ++ show i ++ "/" ++ show n ++ ": " ++ msg
131− cradle <- maybe (loadImplicitCradle $ addTrailingPathSeparator dir) loadCradle x
132− when (isNothing x) $ print cradle
133− putStrLn $ "\nStep 4/6, Cradle " ++ show i ++ "/" ++ show n ++ ": Loading GHC Session"
134− opts <- getComponentOptions cradle
135− createSession opts
136−
137− putStrLn "\nStep 5/6: Initializing the IDE"
152+ putStrLn "\nStep 3/4: Initializing the IDE"
138153 vfs <- makeVFSHandle
139− let cradlesToSessions = Map.fromList $ zip ucradles sessions
140− let filesToCradles = Map.fromList $ zip files cradles
141− let grab file = fromMaybe (head sessions) $ do
142− cradle <- Map.lookup file filesToCradles
143− Map.lookup cradle cradlesToSessions
144−
145− let options =
146− (defaultIdeOptions $ return $ return . grab)
147− { optShakeProfiling = argsShakeProfiling }
148− ide <- initialise def (cradleRules >> mainRule) (pure $ IdInt 0) (showEvent lock) (logger Info) noopDebouncer options vfs
149−
150− putStrLn "\nStep 6/6: Type checking the files"
154+ debouncer <- newAsyncDebouncer
155+ ide <- initialise def mainRule (pure $ IdInt 0) (showEvent lock) (logger Info) debouncer (defaultIdeOptions $ loadSessionShake dir) vfs
156+
157+ putStrLn "\nStep 4/4: Type checking the files"
151158 setFilesOfInterest ide $ HashSet.fromList $ map toNormalizedFilePath' files
152− results <- runActionSync ide $ uses TypeCheck $ map toNormalizedFilePath' files
159+ results <- runActionSync ide $ uses TypeCheck (map toNormalizedFilePath' files)
153160 let (worked, failed) = partition fst $ zip (map isJust results) files
154161 when (failed /= []) $
155162 putStr $ unlines $ "Files that failed:" : map ((++) " * " . snd) failed
156163
157164 let files xs = let n = length xs in if n == 1 then "1 file" else show n ++ " files"
158165 putStrLn $ "\nCompleted (" ++ files worked ++ " worked, " ++ files failed ++ " failed)"
159−
160− unless (null failed) exitFailure
161−
162−cradleRules :: Rules ()
163−cradleRules = do
164− loadGhcSession
165− cradleToSession
166+ return ()
166167
167168expandFiles :: [FilePath] -> IO [FilePath]
168169expandFiles = concatMapM $ \x -> do
@@ -189,23 +190,405 @@ showEvent lock (EventFileDiagnostics (toNormalizedFilePath' -> file) diags) =
189190 withLock lock $ T.putStrLn $ showDiagnosticsColored $ map (file,ShowDiag,) diags
190191showEvent lock e = withLock lock $ print e
191192
192−loadSession :: FilePath -> Action (FilePath -> Action HscEnvEq)
193−loadSession dir = liftIO $ do
194− cradleLoc <- memoIO $ \v -> do
195− res <- findCradle v
196− -- Sometimes we get C:, sometimes we get c:, and sometimes we get a relative path
197− -- try and normalise that
198− -- e.g. see https://github.com/digital-asset/ghcide/issues/126
199− res' <- traverse IO.makeAbsolute res
200− return $ normalise <$> res'
201− let session :: Maybe FilePath -> Action HscEnvEq
202− session file = do
203− -- In the absence of a cradle file, just pass the directory from where to calculate an implicit cradle
204− let cradle = toNormalizedFilePath' $ fromMaybe dir file
205− use_ LoadCradle cradle
206− return $ \file -> session =<< liftIO (cradleLoc file)
193+
194+-- | Run the specific cradle on a specific FilePath via hie-bios.
195+cradleToSessionOpts :: Cradle a -> FilePath -> IO (Either [CradleError] ComponentOptions)
196+cradleToSessionOpts cradle file = do
197+ let showLine s = putStrLn ("> " ++ s)
198+ cradleRes <- runCradle (cradleOptsProg cradle) showLine file
199+ case cradleRes of
200+ CradleSuccess r -> pure (Right r)
201+ CradleFail err -> return (Left [err])
202+ -- For the None cradle perhaps we still want to report an Info
203+ -- message about the fact that the file is being ignored.
204+ CradleNone -> return (Left [])
205+
206+emptyHscEnv :: IO HscEnv
207+emptyHscEnv = do
208+ libdir <- getLibdir
209+ env <- runGhc (Just libdir) getSession
210+ initDynLinker env
211+ pure env
212+
213+-- | Convert a target to a list of potential absolute paths.
214+-- A TargetModule can be anywhere listed by the supplied include
215+-- directories
216+-- A target file is a relative path but with a specific prefix so just need
217+-- to canonicalise it.
218+targetToFile :: [FilePath] -> TargetId -> IO [NormalizedFilePath]
219+targetToFile is (TargetModule mod) = do
220+ let fps = [i </> moduleNameSlashes mod -<.> ext | ext <- exts, i <- is ]
221+ exts = ["hs", "hs-boot", "lhs"]
222+ mapM (fmap toNormalizedFilePath' . canonicalizePath) fps
223+targetToFile _ (TargetFile f _) = do
224+ f' <- canonicalizePath f
225+ return [toNormalizedFilePath' f']
226+
227+setNameCache :: IORef NameCache -> HscEnv -> HscEnv
228+setNameCache nc hsc = hsc { hsc_NC = nc }
229+
230+loadSessionShake :: FilePath -> Action (FilePath -> Action (IdeResult HscEnvEq))
231+loadSessionShake fp = do
232+ se <- getShakeExtras
233+ IdeOptions{optTesting} <- getIdeOptions
234+ res <- liftIO $ loadSession optTesting se fp
235+ return (fmap liftIO res)
236+
237+-- | This is the key function which implements multi-component support. All
238+-- components mapping to the same hie.yaml file are mapped to the same
239+-- HscEnv which is updated as new components are discovered.
240+loadSession :: Bool -> ShakeExtras -> FilePath -> IO (FilePath -> IO (IdeResult HscEnvEq))
241+loadSession optTesting ShakeExtras{logger, eventer} dir = do
242+ -- Mapping from hie.yaml file to HscEnv, one per hie.yaml file
243+ hscEnvs <- newVar Map.empty :: IO (Var HieMap)
244+ -- Mapping from a Filepath to HscEnv
245+ fileToFlags <- newVar Map.empty :: IO (Var FlagsMap)
246+
247+ -- This caches the mapping from Mod.hs -> hie.yaml
248+ cradleLoc <- memoIO $ \v -> do
249+ res <- findCradle v
250+ -- Sometimes we get C:, sometimes we get c:, and sometimes we get a relative path
251+ -- try and normalise that
252+ -- e.g. see https://github.com/digital-asset/ghcide/issues/126
253+ res' <- traverse IO.makeAbsolute res
254+ return $ normalise <$> res'
255+
256+ -- Create a new HscEnv from a hieYaml root and a set of options
257+ -- If the hieYaml file already has an HscEnv, the new component is
258+ -- combined with the components in the old HscEnv into a new HscEnv
259+ -- which contains the union.
260+ let packageSetup :: (Maybe FilePath, NormalizedFilePath, ComponentOptions)
261+ -> IO (HscEnv, ComponentInfo, [ComponentInfo])
262+ packageSetup (hieYaml, cfp, opts) = do
263+ -- Parse DynFlags for the newly discovered component
264+ hscEnv <- emptyHscEnv
265+ (df, targets) <- evalGhcEnv hscEnv $
266+ setOptions opts (hsc_dflags hscEnv)
267+ dep_info <- getDependencyInfo (componentDependencies opts ++ maybeToList hieYaml)
268+ -- Now lookup to see whether we are combining with an existing HscEnv
269+ -- or making a new one. The lookup returns the HscEnv and a list of
270+ -- information about other components loaded into the HscEnv
271+ -- (unitId, DynFlag, Targets)
272+ modifyVar hscEnvs $ \m -> do
273+ -- Just deps if there's already an HscEnv
274+ -- Nothing is it's the first time we are making an HscEnv
275+ let oldDeps = Map.lookup hieYaml m
276+ let -- Add the raw information about this component to the list
277+ -- We will modify the unitId and DynFlags used for
278+ -- compilation but these are the true source of
279+ -- information.
280+ new_deps = RawComponentInfo (thisInstalledUnitId df) df targets cfp opts dep_info
281+ : maybe [] snd oldDeps
282+ -- Get all the unit-ids for things in this component
283+ inplace = map rawComponentUnitId new_deps
284+
285+ new_deps' <- forM new_deps $ \RawComponentInfo{..} -> do
286+ -- Remove all inplace dependencies from package flags for
287+ -- components in this HscEnv
288+ let (df2, uids) = removeInplacePackages inplace rawComponentDynFlags
289+ let prefix = show rawComponentUnitId
290+ -- See Note [Avoiding bad interface files]
291+ processed_df <- setCacheDir logger prefix (sort $ map show uids) opts df2
292+ -- The final component information, mostly the same but the DynFlags don't
293+ -- contain any packages which are also loaded
294+ -- into the same component.
295+ pure $ ComponentInfo rawComponentUnitId
296+ processed_df
297+ uids
298+ rawComponentTargets
299+ rawComponentFP
300+ rawComponentCOptions
301+ rawComponentDependencyInfo
302+ -- Make a new HscEnv, we have to recompile everything from
303+ -- scratch again (for now)
304+ -- It's important to keep the same NameCache though for reasons
305+ -- that I do not fully understand
306+ logInfo logger (T.pack ("Making new HscEnv" ++ show inplace))
307+ hscEnv <- case oldDeps of
308+ Nothing -> emptyHscEnv
309+ Just (old_hsc, _) -> setNameCache (hsc_NC old_hsc) <$> emptyHscEnv
310+ newHscEnv <-
311+ -- Add the options for the current component to the HscEnv
312+ evalGhcEnv hscEnv $ do
313+ _ <- setSessionDynFlags df
314+ getSession
315+ -- Modify the map so the hieYaml now maps to the newly created
316+ -- HscEnv
317+ -- Returns
318+ -- . the new HscEnv so it can be used to modify the
319+ -- FilePath -> HscEnv map (fileToFlags)
320+ -- . The information for the new component which caused this cache miss
321+ -- . The modified information (without -inplace flags) for
322+ -- existing packages
323+ pure (Map.insert hieYaml (newHscEnv, new_deps) m, (newHscEnv, head new_deps', tail new_deps'))
324+
325+ let session :: (Maybe FilePath, NormalizedFilePath, ComponentOptions) -> IO (IdeResult HscEnvEq)
326+ session (hieYaml, cfp, opts) = do
327+ (hscEnv, new, old_deps) <- packageSetup (hieYaml, cfp, opts)
328+ -- Make a map from unit-id to DynFlags, this is used when trying to
329+ -- resolve imports. (especially PackageImports)
330+ let uids = map (\ci -> (componentUnitId ci, componentDynFlags ci)) (new : old_deps)
331+
332+ -- For each component, now make a new HscEnvEq which contains the
333+ -- HscEnv for the hie.yaml file but the DynFlags for that component
334+
335+ -- New HscEnv for the component in question, returns the new HscEnvEq and
336+ -- a mapping from FilePath to the newly created HscEnvEq.
337+ let new_cache = newComponentCache logger hscEnv uids
338+ (cs, res) <- new_cache new
339+ -- Modified cache targets for everything else in the hie.yaml file
340+ -- which now uses the same EPS and so on
341+ cached_targets <- concatMapM (fmap fst . new_cache) old_deps
342+ modifyVar_ fileToFlags $ \var -> do
343+ pure $ Map.insert hieYaml (HM.fromList (cs ++ cached_targets)) var
344+
345+ return (fst res)
346+
347+ let consultCradle :: Maybe FilePath -> FilePath -> IO (IdeResult HscEnvEq)
348+ consultCradle hieYaml cfp = do
349+ when optTesting $ eventer $ notifyCradleLoaded cfp
350+ logInfo logger $ T.pack ("Consulting the cradle for " <> show cfp)
351+ cradle <- maybe (loadImplicitCradle $ addTrailingPathSeparator dir) loadCradle hieYaml
352+ eopts <- cradleToSessionOpts cradle cfp
353+ logDebug logger $ T.pack ("Session loading result: " <> show eopts)
354+ case eopts of
355+ -- The cradle gave us some options so get to work turning them
356+ -- into and HscEnv.
357+ Right opts -> do
358+ session (hieYaml, toNormalizedFilePath' cfp, opts)
359+ -- Failure case, either a cradle error or the none cradle
360+ Left err -> do
361+ dep_info <- getDependencyInfo (maybeToList hieYaml)
362+ let ncfp = toNormalizedFilePath' cfp
363+ let res = (map (renderCradleError ncfp) err, Nothing)
364+ modifyVar_ fileToFlags $ \var -> do
365+ pure $ Map.insertWith HM.union hieYaml (HM.singleton ncfp (res, dep_info)) var
366+ return res
367+
368+ -- This caches the mapping from hie.yaml + Mod.hs -> [String]
369+ let sessionOpts :: (Maybe FilePath, FilePath) -> IO (IdeResult HscEnvEq)
370+ sessionOpts (hieYaml, file) = do
371+ v <- fromMaybe HM.empty . Map.lookup hieYaml <$> readVar fileToFlags
372+ cfp <- canonicalizePath file
373+ case HM.lookup (toNormalizedFilePath' cfp) v of
374+ Just (opts, old_di) -> do
375+ deps_ok <- checkDependencyInfo old_di
376+ if not deps_ok
377+ then do
378+ -- If the dependencies are out of date then clear both caches and start
379+ -- again.
380+ modifyVar_ fileToFlags (const (return Map.empty))
381+ -- Keep the same name cache
382+ modifyVar_ hscEnvs (return . Map.adjust (\(h, _) -> (h, [])) hieYaml )
383+ consultCradle hieYaml cfp
384+ else return opts
385+ Nothing -> consultCradle hieYaml cfp
386+
387+ dummyAs <- async $ return (error "Uninitialised")
388+ runningCradle <- newVar dummyAs :: IO (Var (Async (IdeResult HscEnvEq)))
389+ -- The main function which gets options for a file. We only want one of these running
390+ -- at a time. Therefore the IORef contains the currently running cradle, if we try
391+ -- to get some more options then we wait for the currently running action to finish
392+ -- before attempting to do so.
393+ let getOptions :: FilePath -> IO (IdeResult HscEnvEq)
394+ getOptions file = do
395+ hieYaml <- cradleLoc file
396+ sessionOpts (hieYaml, file)
397+ return $ \file -> do
398+ join $ mask_ $ modifyVar runningCradle $ \as -> do
399+ -- If the cradle is not finished, then wait for it to finish.
400+ void $ wait as
401+ as <- async $ getOptions file
402+ return (as, wait as)
207403
208404
405+
406+-- | Create a mapping from FilePaths to HscEnvEqs
407+newComponentCache
408+ :: Logger
409+ -> HscEnv
410+ -> [(InstalledUnitId, DynFlags)]
411+ -> ComponentInfo
412+ -> IO ([(NormalizedFilePath, (IdeResult HscEnvEq, DependencyInfo))], (IdeResult HscEnvEq, DependencyInfo))
413+newComponentCache logger hsc_env uids ci = do
414+ let df = componentDynFlags ci
415+ let hscEnv' = hsc_env { hsc_dflags = df
416+ , hsc_IC = (hsc_IC hsc_env) { ic_dflags = df } }
417+
418+ versionMismatch <- checkGhcVersion
419+ henv <- case versionMismatch of
420+ Just mismatch -> return mismatch
421+ Nothing -> newHscEnvEq hscEnv' uids
422+ let res = (([], Just henv), componentDependencyInfo ci)
423+ logDebug logger ("New Component Cache HscEnvEq: " <> T.pack (show res))
424+
425+ let is = importPaths df
426+ ctargets <- concatMapM (targetToFile is . targetId) (componentTargets ci)
427+ -- A special target for the file which caused this wonderful
428+ -- component to be created. In case the cradle doesn't list all the targets for
429+ -- the component, in which case things will be horribly broken anyway.
430+ -- Otherwise, we will immediately attempt to reload this module which
431+ -- causes an infinite loop and high CPU usage.
432+ let special_target = (componentFP ci, res)
433+ let xs = map (,res) ctargets
434+ return (special_target:xs, res)
435+
436+{- Note [Avoiding bad interface files]
437+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
438+Originally, we set the cache directory for the various components once
439+on the first occurrence of the component.
440+This works fine if these components have no references to each other,
441+but you have components that depend on each other, the interface files are
442+updated for each component.
443+After restarting the session and only opening the component that depended
444+on the other, suddenly the interface files of this component are stale.
445+However, from the point of view of `ghcide`, they do not look stale,
446+thus, not regenerated and the IDE shows weird errors such as:
447+```
448+typecheckIface
449+Declaration for Rep_ClientRunFlags
450+Axiom branches Rep_ClientRunFlags:
451+ Failed to load interface for ‘Distribution.Simple.Flag’
452+ Use -v to see a list of the files searched for.
453+```
454+and
455+```
456+expectJust checkFamInstConsistency
457+CallStack (from HasCallStack):
458+ error, called at compiler\\utils\\Maybes.hs:55:27 in ghc:Maybes
459+ expectJust, called at compiler\\typecheck\\FamInst.hs:461:30 in ghc:FamInst
460+```
461+
462+To mitigate this, we set the cache directory for each component dependent
463+on the components of the current `HscEnv`, additionally to the component options
464+of the respective components.
465+Assume two components, c1, c2, where c2 depends on c1, and the options of the
466+respective components are co1, co2.
467+If we want to load component c2, followed by c1, we set the cache directory for
468+each component in this way:
469+
470+ * Load component c2
471+ * (Cache Directory State)
472+ - name of c2 + co2
473+ * Load component c1
474+ * (Cache Directory State)
475+ - name of c2 + name of c1 + co2
476+ - name of c2 + name of c1 + co1
477+
478+Overall, we created three cache directories. If we opened c1 first, then we
479+create a fourth cache directory.
480+This makes sure that interface files are always correctly updated.
481+
482+Since this causes a lot of recompilation, we only update the cache-directory,
483+if the dependencies of a component have really changed.
484+E.g. when you load two executables, they can not depend on each other. They
485+should be filtered out, such that we dont have to re-compile everything.
486+-}
487+
488+-- | Set the cache-directory based on the ComponentOptions and a list of
489+-- internal packages.
490+-- For the exact reason, see Note [Avoiding bad interface files].
491+setCacheDir :: MonadIO m => Logger -> String -> [String] -> ComponentOptions -> DynFlags -> m DynFlags
492+setCacheDir logger prefix hscComponents comps dflags = do
493+ cacheDir <- liftIO $ getCacheDir prefix (hscComponents ++ componentOptions comps)
494+ liftIO $ logInfo logger $ "Using interface files cache dir: " <> T.pack cacheDir
495+ pure $ dflags
496+ & setHiDir cacheDir
497+ & setDefaultHieDir cacheDir
498+
499+
500+renderCradleError :: NormalizedFilePath -> CradleError -> FileDiagnostic
501+renderCradleError nfp (CradleError _ec t) =
502+ ideErrorText nfp (T.unlines (map T.pack t))
503+
504+-- See Note [Multi Cradle Dependency Info]
505+type DependencyInfo = Map.Map FilePath (Maybe UTCTime)
506+type HieMap = Map.Map (Maybe FilePath) (HscEnv, [RawComponentInfo])
507+type FlagsMap = Map.Map (Maybe FilePath) (HM.HashMap NormalizedFilePath (IdeResult HscEnvEq, DependencyInfo))
508+
509+-- This is pristine information about a component
510+data RawComponentInfo = RawComponentInfo
511+ { rawComponentUnitId :: InstalledUnitId
512+ -- | Unprocessed DynFlags. Contains inplace packages such as libraries.
513+ -- We do not want to use them unprocessed.
514+ , rawComponentDynFlags :: DynFlags
515+ -- | All targets of this components.
516+ , rawComponentTargets :: [Target]
517+ -- | Filepath which caused the creation of this component
518+ , rawComponentFP :: NormalizedFilePath
519+ -- | Component Options used to load the component.
520+ , rawComponentCOptions :: ComponentOptions
521+ -- | Maps cradle dependencies, such as `stack.yaml`, or `.cabal` file
522+ -- to last modification time. See Note [Multi Cradle Dependency Info].
523+ , rawComponentDependencyInfo :: DependencyInfo
524+ }
525+
526+-- This is processed information about the component, in particular the dynflags will be modified.
527+data ComponentInfo = ComponentInfo
528+ { componentUnitId :: InstalledUnitId
529+ -- | Processed DynFlags. Does not contain inplace packages such as local
530+ -- libraries. Can be used to actually load this Component.
531+ , componentDynFlags :: DynFlags
532+ -- | Internal units, such as local libraries, that this component
533+ -- is loaded with. These have been extracted from the original
534+ -- ComponentOptions.
535+ , componentInternalUnits :: [InstalledUnitId]
536+ -- | All targets of this components.
537+ , componentTargets :: [Target]
538+ -- | Filepath which caused the creation of this component
539+ , componentFP :: NormalizedFilePath
540+ -- | Component Options used to load the component.
541+ , componentCOptions :: ComponentOptions
542+ -- | Maps cradle dependencies, such as `stack.yaml`, or `.cabal` file
543+ -- to last modification time. See Note [Multi Cradle Dependency Info]
544+ , componentDependencyInfo :: DependencyInfo
545+ }
546+
547+-- | Check if any dependency has been modified lately.
548+checkDependencyInfo :: DependencyInfo -> IO Bool
549+checkDependencyInfo old_di = do
550+ di <- getDependencyInfo (Map.keys old_di)
551+ return (di == old_di)
552+
553+-- Note [Multi Cradle Dependency Info]
554+-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
555+-- Why do we implement our own file modification tracking here?
556+-- The primary reason is that the custom caching logic is quite complicated and going into shake
557+-- adds even more complexity and more indirection. I did try for about 5 hours to work out how to
558+-- use shake rules rather than IO but eventually gave up.
559+
560+-- | Computes a mapping from a filepath to its latest modification date.
561+-- See Note [Multi Cradle Dependency Info] why we do this ourselves instead
562+-- of letting shake take care of it.
563+getDependencyInfo :: [FilePath] -> IO DependencyInfo
564+getDependencyInfo fs = Map.fromList <$> mapM do_one fs
565+
566+ where
567+ tryIO :: IO a -> IO (Either IOException a)
568+ tryIO = try
569+
570+ do_one :: FilePath -> IO (FilePath, Maybe UTCTime)
571+ do_one fp = (fp,) . eitherToMaybe <$> tryIO (getModificationTime fp)
572+
573+-- | This function removes all the -package flags which refer to packages we
574+-- are going to deal with ourselves. For example, if a executable depends
575+-- on a library component, then this function will remove the library flag
576+-- from the package flags for the executable
577+--
578+-- There are several places in GHC (for example the call to hptInstances in
579+-- tcRnImports) which assume that all modules in the HPT have the same unit
580+-- ID. Therefore we create a fake one and give them all the same unit id.
581+removeInplacePackages :: [InstalledUnitId] -> DynFlags -> (DynFlags, [InstalledUnitId])
582+removeInplacePackages us df = (df { packageFlags = ps
583+ , thisInstalledUnitId = fake_uid }, uids)
584+ where
585+ (uids, ps) = partitionEithers (map go (packageFlags df))
586+ fake_uid = toInstalledUnitId (stringToUnitId "fake_uid")
587+ go p@(ExposePackage _ (UnitIdArg u) _) = if toInstalledUnitId u `elem` us
588+ then Left (toInstalledUnitId u)
589+ else Right p
590+ go p = Right p
591+
209592-- | Memoize an IO function, with the characteristics:
210593--
211594-- * If multiple people ask for a result simultaneously, make sure you only compute it once.
@@ -222,3 +605,79 @@ memoIO op = do
222605 res <- onceFork $ op k
223606 return (Map.insert k res mp, res)
224607 Just res -> return (mp, res)
608+
609+setOptions :: GhcMonad m => ComponentOptions -> DynFlags -> m (DynFlags, [Target])
610+setOptions (ComponentOptions theOpts compRoot _) dflags = do
611+ (dflags', targets) <- addCmdOpts theOpts dflags
612+ let dflags'' =
613+ -- disabled, generated directly by ghcide instead
614+ flip gopt_unset Opt_WriteInterface $
615+ -- disabled, generated directly by ghcide instead
616+ -- also, it can confuse the interface stale check
617+ dontWriteHieFiles $
618+ setIgnoreInterfacePragmas $
619+ setLinkerOptions $
620+ disableOptimisation $
621+ makeDynFlagsAbsolute compRoot dflags'
622+ -- initPackages parses the -package flags and
623+ -- sets up the visibility for each component.
624+ (final_df, _) <- liftIO $ initPackages dflags''
625+ return (final_df, targets)
626+
627+
628+-- we don't want to generate object code so we compile to bytecode
629+-- (HscInterpreted) which implies LinkInMemory
630+-- HscInterpreted
631+setLinkerOptions :: DynFlags -> DynFlags
632+setLinkerOptions df = df {
633+ ghcLink = LinkInMemory
634+ , hscTarget = HscNothing
635+ , ghcMode = CompManager
636+ }
637+
638+setIgnoreInterfacePragmas :: DynFlags -> DynFlags
639+setIgnoreInterfacePragmas df =
640+ gopt_set (gopt_set df Opt_IgnoreInterfacePragmas) Opt_IgnoreOptimChanges
641+
642+disableOptimisation :: DynFlags -> DynFlags
643+disableOptimisation df = updOptLevel 0 df
644+
645+setHiDir :: FilePath -> DynFlags -> DynFlags
646+setHiDir f d =
647+ -- override user settings to avoid conflicts leading to recompilation
648+ d { hiDir = Just f}
649+
650+getCacheDir :: String -> [String] -> IO FilePath
651+getCacheDir prefix opts = IO.getXdgDirectory IO.XdgCache (cacheDir </> prefix ++ "-" ++ opts_hash)
652+ where
653+ -- Create a unique folder per set of different GHC options, assuming that each different set of
654+ -- GHC options will create incompatible interface files.
655+ opts_hash = B.unpack $ encode $ H.finalize $ H.updates H.init (map B.pack opts)
656+
657+-- | Sub directory for the cache path
658+cacheDir :: String
659+cacheDir = "ghcide"
660+
661+notifyCradleLoaded :: FilePath -> FromServerMessage
662+notifyCradleLoaded fp =
663+ NotCustomServer $
664+ NotificationMessage "2.0" (CustomServerMethod cradleLoadedMethod) $
665+ toJSON fp
666+
667+cradleLoadedMethod :: T.Text
668+cradleLoadedMethod = "ghcide/cradle/loaded"
669+
670+ghcVersionChecker :: IO VersionCheck
671+ghcVersionChecker = $$(makeGhcVersionChecker (pure <$> getLibdir))
672+
673+checkGhcVersion :: IO (Maybe HscEnvEq)
674+checkGhcVersion = do
675+ res <- ghcVersionChecker
676+ case res of
677+ Failure err -> do
678+ putStrLn $ "Error while checking GHC version: " ++ show err
679+ return Nothing
680+ Mismatch {..} ->
681+ return $ Just GhcVersionMismatch {..}
682+ _ ->
683+ return Nothing
exe/RuleTypes.hsdeleted−34
@@ -1,34 +0,0 @@
1−{-# LANGUAGE TypeFamilies #-}
2−module RuleTypes (GetHscEnv(..), LoadCradle(..)) where
3−
4−import Control.DeepSeq
5−import Data.Binary
6−import Data.Hashable (Hashable)
7−import Development.Shake
8−import Development.IDE.GHC.Util
9−import Data.Typeable (Typeable)
10−import GHC.Generics (Generic)
11−
12−-- Rule type for caching GHC sessions.
13−type instance RuleResult GetHscEnv = HscEnvEq
14−
15−data GetHscEnv = GetHscEnv
16− { hscenvOptions :: [String] -- componentOptions from hie-bios
17− , hscenvRoot :: FilePath -- componentRoot from hie-bios
18− , hscenvDependencies :: [FilePath] -- componentDependencies from hie-bios
19− }
20− deriving (Eq, Show, Typeable, Generic)
21−
22−instance Hashable GetHscEnv
23−instance NFData GetHscEnv
24−instance Binary GetHscEnv
25−
26−-- Rule type for caching cradle loading
27−type instance RuleResult LoadCradle = HscEnvEq
28−
29−data LoadCradle = LoadCradle
30− deriving (Eq, Show, Typeable, Generic)
31−
32−instance Hashable LoadCradle
33−instance NFData LoadCradle
34−instance Binary LoadCradle
exe/Rules.hsdeleted−147
@@ -1,147 +0,0 @@
1−{-# LANGUAGE TemplateHaskell #-}
2−module Rules
3− ( loadGhcSession
4− , cradleToSession
5− , cradleLoadedMethod
6− , createSession
7− , getComponentOptions
8− )
9−where
10−
11−import Control.Exception
12−import Control.Monad (filterM, when)
13−import qualified Crypto.Hash.SHA1 as H
14−import Data.ByteString.Base16 (encode)
15−import qualified Data.ByteString.Char8 as B
16−import Data.Functor ((<&>))
17−import Data.Text (Text, pack)
18−import Development.IDE.Core.Rules (defineNoFile)
19−import Development.IDE.Core.Service (getIdeOptions)
20−import Development.IDE.Core.Shake (actionLogger, sendEvent, define, useNoFile_)
21−import Development.IDE.GHC.Util
22−import Development.IDE.Types.Location (fromNormalizedFilePath)
23−import Development.IDE.Types.Options (IdeOptions(IdeOptions, optTesting))
24−import Development.Shake
25−import GHC
26−import GHC.Check (VersionCheck(..), makeGhcVersionChecker)
27−import HIE.Bios
28−import HIE.Bios.Cradle
29−import HIE.Bios.Environment (addCmdOpts, makeDynFlagsAbsolute)
30−import HIE.Bios.Types
31−import Linker (initDynLinker)
32−import RuleTypes
33−import qualified System.Directory.Extra as IO
34−import System.FilePath.Posix (addTrailingPathSeparator,
35− (</>))
36−import qualified Language.Haskell.LSP.Messages as LSP
37−import qualified Language.Haskell.LSP.Types as LSP
38−import Data.Aeson (ToJSON(toJSON))
39−import Development.IDE.Types.Logger (logDebug)
40−import Util
41−import System.IO (hPutStrLn, stderr)
42−
43−-- Prefix for the cache path
44−cacheDir :: String
45−cacheDir = "ghcide"
46−
47−notifyCradleLoaded :: FilePath -> LSP.FromServerMessage
48−notifyCradleLoaded fp =
49− LSP.NotCustomServer $
50− LSP.NotificationMessage "2.0" (LSP.CustomServerMethod cradleLoadedMethod) $
51− toJSON fp
52−
53−loadGhcSession :: Rules ()
54−loadGhcSession =
55− -- This rule is for caching the GHC session. E.g., even when the cabal file
56− -- changed, if the resulting flags did not change, we would continue to use
57− -- the existing session.
58− defineNoFile $ \(GetHscEnv opts optRoot deps) ->
59− liftIO $ createSession $ ComponentOptions opts optRoot deps
60−
61−cradleToSession :: Rules ()
62−cradleToSession = define $ \LoadCradle nfp -> do
63−
64− let f = fromNormalizedFilePath nfp
65−
66− IdeOptions{optTesting} <- getIdeOptions
67−
68− logger <- actionLogger
69− liftIO $ logDebug logger $ "Running cradle " <> pack (fromNormalizedFilePath nfp)
70−
71− -- If the path points to a directory, load the implicit cradle
72− mbYaml <- doesDirectoryExist f <&> \isDir -> if isDir then Nothing else Just f
73− cradle <- liftIO $ maybe (loadImplicitCradle $ addTrailingPathSeparator f) loadCradle mbYaml
74−
75− when optTesting $
76− sendEvent $ notifyCradleLoaded f
77−
78− -- Avoid interrupting `getComponentOptions` since it calls external processes
79− cmpOpts <- liftIO $ mask $ \_ -> getComponentOptions cradle
80− let opts = componentOptions cmpOpts
81− deps = componentDependencies cmpOpts
82− root = componentRoot cmpOpts
83− deps' = case mbYaml of
84− -- For direct cradles, the hie.yaml file itself must be watched.
85− Just yaml | isDirectCradle cradle -> yaml : deps
86− _ -> deps
87− existingDeps <- filterM doesFileExist deps'
88− need existingDeps
89− ([],) . pure <$> useNoFile_ (GetHscEnv opts root deps)
90−
91−cradleLoadedMethod :: Text
92−cradleLoadedMethod = "ghcide/cradle/loaded"
93−
94−getComponentOptions :: Cradle a -> IO ComponentOptions
95−getComponentOptions cradle = do
96− let showLine s = putStrLn ("> " ++ s)
97− -- WARNING 'runCradle is very expensive and must be called as few times as possible
98− cradleRes <- runCradle (cradleOptsProg cradle) showLine ""
99− case cradleRes of
100− CradleSuccess r -> pure r
101− CradleFail err -> throwIO err
102− -- TODO Rather than failing here, we should ignore any files that use this cradle.
103− -- That will require some more changes.
104− CradleNone -> fail "'none' cradle is not yet supported"
105−
106−ghcVersionChecker :: IO VersionCheck
107−ghcVersionChecker = $$(makeGhcVersionChecker (pure <$> getLibdir))
108−
109−checkGhcVersion :: IO (Maybe HscEnvEq)
110−checkGhcVersion = do
111− res <- ghcVersionChecker
112− case res of
113− Failure err -> do
114− putStrLn $ "Error while checking GHC version: " ++ show err
115− return Nothing
116− Mismatch {..} ->
117− return $ Just GhcVersionMismatch {..}
118− _ ->
119− return Nothing
120−
121−createSession :: ComponentOptions -> IO HscEnvEq
122−createSession (ComponentOptions theOpts compRoot _) = do
123− libdir <- getLibdir
124−
125− cacheDir <- getCacheDir theOpts
126−
127− hPutStrLn stderr $ "Interface files cache dir: " <> cacheDir
128−
129− runGhc (Just libdir) $ do
130− dflags <- getSessionDynFlags
131− (dflags_, _targets) <- addCmdOpts theOpts dflags
132− let dflags' = makeDynFlagsAbsolute compRoot dflags_
133− setupDynFlags cacheDir dflags'
134− versionMismatch <- liftIO checkGhcVersion
135− case versionMismatch of
136− Just mismatch -> return mismatch
137− Nothing -> do
138− env <- getSession
139− liftIO $ initDynLinker env
140− liftIO $ newHscEnvEq env
141−
142−getCacheDir :: [String] -> IO FilePath
143−getCacheDir opts = IO.getXdgDirectory IO.XdgCache (cacheDir </> opts_hash)
144− where
145− -- Create a unique folder per set of different GHC options, assuming that each different set of
146− -- GHC options will create incompatible interface files.
147− opts_hash = B.unpack $ encode $ H.finalize $ H.updates H.init (map B.pack opts)
exe/Util.hsdeleted−62
@@ -1,62 +0,0 @@
1−module Util (setupDynFlags, getLibdir) where
2−
3−-- Set the GHC libdir to the nix libdir if it's present.
4−import qualified GHC.Paths as GHCPaths
5−import DynFlags ( gopt_unset
6− , GhcMode(CompManager)
7− , HscTarget(HscNothing)
8− , GhcLink(LinkInMemory)
9− , GeneralFlag
10− ( Opt_IgnoreInterfacePragmas
11− , Opt_IgnoreOptimChanges
12− , Opt_WriteInterface
13− )
14− , gopt_set
15− , updOptLevel
16− , DynFlags(..)
17− )
18−import Data.Maybe ( fromMaybe )
19−import Development.IDE.GHC.Util ( setDefaultHieDir
20− , dontWriteHieFiles
21− )
22−import System.Environment ( lookupEnv )
23−import GHC (GhcMonad, setSessionDynFlags )
24−import Data.Functor ( void )
25−
26−setupDynFlags :: GhcMonad f => FilePath -> DynFlags -> f ()
27−setupDynFlags cacheDir =
28− void
29− . setSessionDynFlags
30− -- disabled, generated directly by ghcide instead
31− . flip gopt_unset Opt_WriteInterface
32− -- disabled, generated directly by ghcide instead
33− -- also, it can confuse the interface stale check
34− . dontWriteHieFiles
35− . setHiDir cacheDir
36− . setDefaultHieDir cacheDir
37− . setIgnoreInterfacePragmas
38− . setLinkerOptions
39− . disableOptimisation
40−
41−getLibdir :: IO FilePath
42−getLibdir = fromMaybe GHCPaths.libdir <$> lookupEnv "NIX_GHC_LIBDIR"
43−
44−-- we don't want to generate object code so we compile to bytecode
45−-- (HscInterpreted) which implies LinkInMemory
46−
47−-- HscInterpreted
48−setLinkerOptions :: DynFlags -> DynFlags
49−setLinkerOptions df =
50− df { ghcLink = LinkInMemory, hscTarget = HscNothing, ghcMode = CompManager }
51−
52−setIgnoreInterfacePragmas :: DynFlags -> DynFlags
53−setIgnoreInterfacePragmas df =
54− gopt_set (gopt_set df Opt_IgnoreInterfacePragmas) Opt_IgnoreOptimChanges
55−
56−disableOptimisation :: DynFlags -> DynFlags
57−disableOptimisation df = updOptLevel 0 df
58−
59−setHiDir :: FilePath -> DynFlags -> DynFlags
60−setHiDir f d =
61− -- override user settings to avoid conflicts leading to recompilation
62− d { hiDir = Just f }
exe/Utils.hsnew file+9
@@ -0,0 +1,9 @@
1+module Utils (getLibdir) where
2+
3+import qualified GHC.Paths
4+import System.Environment
5+import Data.Maybe
6+
7+-- Set the GHC libdir to the nix libdir if it's present.
8+getLibdir :: IO FilePath
9+getLibdir = fromMaybe GHC.Paths.libdir <$> lookupEnv "NIX_GHC_LIBDIR"
ghcide.cabal+10−4
@@ -15,7 +15,13 @@ homepage: https://github.com/digital-asset/ghcide#readme
1515bug-reports: https://github.com/digital-asset/ghcide/issues
1616tested-with: GHC==8.6.5
1717extra-source-files: include/ghc-api-version.h README.md CHANGELOG.md
18− test/data/GotoHover.hs
18+ test/data/hover/*.hs
19+ test/data/multi/cabal.project
20+ test/data/multi/hie.yaml
21+ test/data/multi/a/a.cabal
22+ test/data/multi/a/*.hs
23+ test/data/multi/b/b.cabal
24+ test/data/multi/b/*.hs
1925
2026source-repository head
2127 type: git
@@ -184,6 +190,8 @@ executable ghcide
184190 "-with-rtsopts=-I0 -qg -A128M"
185191 main-is: Main.hs
186192 build-depends:
193+ time,
194+ async,
187195 hslogger,
188196 aeson,
189197 base == 4.*,
@@ -211,11 +219,9 @@ executable ghcide
211219 text,
212220 unordered-containers
213221 other-modules:
222+ Utils
214223 Arguments
215224 Paths_ghcide
216− Rules
217− RuleTypes
218− Util
219225
220226 default-extensions:
221227 BangPatterns
hie.yaml+10−1
@@ -1 +1,10 @@
1−cradle: {stack: {component: "ghcide:lib"}}
1+cradle:
2+ cabal:
3+ - path: "./src"
4+ component: "ghcide:lib:ghcide"
5+ - path: "./exe"
6+ component: "ghcide:exe:ghcide"
7+ - path: "./test"
8+ component: "ghcide:test:ghcide-tests"
9+ - path: "./test/preprocessor"
10+ component: "ghcide:exe:ghcide-test-preprocessor"
src/Development/IDE/Core/Compile.hs+23−5
@@ -90,14 +90,15 @@ import Exception (ExceptionMonad)
9090parseModule
9191 :: IdeOptions
9292 -> HscEnv
93+ -> [PackageName]
9394 -> FilePath
9495 -> Maybe SB.StringBuffer
9596 -> IO (IdeResult (StringBuffer, ParsedModule))
96−parseModule IdeOptions{..} env filename mbContents =
97+parseModule IdeOptions{..} env comp_pkgs filename mbContents =
9798 fmap (either (, Nothing) id) $
9899 evalGhcEnv env $ runExceptT $ do
99100 (contents, dflags) <- preprocessor filename mbContents
100− (diag, modu) <- parseFileContents optPreprocessor dflags filename contents
101+ (diag, modu) <- parseFileContents optPreprocessor dflags comp_pkgs filename contents
101102 return (diag, Just (contents, modu))
102103
103104
@@ -499,10 +500,11 @@ parseFileContents
499500 :: GhcMonad m
500501 => (GHC.ParsedSource -> IdePreprocessedSource)
501502 -> DynFlags -- ^ flags to use
503+ -> [PackageName] -- ^ The package imports to ignore
502504 -> FilePath -- ^ the filename (for source locations)
503505 -> SB.StringBuffer -- ^ Haskell module source text (full Unicode is supported)
504506 -> ExceptT [FileDiagnostic] m ([FileDiagnostic], ParsedModule)
505−parseFileContents customPreprocessor dflags filename contents = do
507+parseFileContents customPreprocessor dflags comp_pkgs filename contents = do
506508 let loc = mkRealSrcLoc (mkFastString filename) 1 1
507509 case unP Parser.parseModule (mkPState dflags contents loc) of
508510#if MIN_GHC_API_VERSION(8,10,0)
@@ -534,18 +536,34 @@ parseFileContents customPreprocessor dflags filename contents = do
534536 -- Ok, we got here. It's safe to continue.
535537 let IdePreprocessedSource preproc_warns errs parsed = customPreprocessor rdr_module
536538 unless (null errs) $ throwE $ diagFromStrings "parser" DsError errs
539+ let parsed' = removePackageImports comp_pkgs parsed
537540 let preproc_warnings = diagFromStrings "parser" DsWarning preproc_warns
538− ms <- getModSummaryFromBuffer filename dflags parsed
541+ ms <- getModSummaryFromBuffer filename dflags parsed'
539542 let pm =
540543 ParsedModule {
541544 pm_mod_summary = ms
542− , pm_parsed_source = parsed
545+ , pm_parsed_source = parsed'
543546 , pm_extra_src_files=[] -- src imports not allowed
544547 , pm_annotations = hpm_annotations
545548 }
546549 warnings = diagFromErrMsgs "parser" dflags warns
547550 pure (warnings ++ preproc_warnings, pm)
548551
552+
553+-- | After parsing the module remove all package imports referring to
554+-- these packages as we have already dealt with what they map to.
555+removePackageImports :: [PackageName] -> GHC.ParsedSource -> GHC.ParsedSource
556+removePackageImports pkgs (L l h@HsModule {hsmodImports} ) = L l (h { hsmodImports = imports' })
557+ where
558+ imports' = map do_one_import hsmodImports
559+ do_one_import (L l i@ImportDecl{ideclPkgQual}) =
560+ case PackageName . sl_fs <$> ideclPkgQual of
561+ Just pn | pn `elem` pkgs -> L l (i { ideclPkgQual = Nothing })
562+ _ -> L l i
563+#if MIN_GHC_API_VERSION(8,6,0)
564+ do_one_import l = l
565+#endif
566+
549567loadHieFile :: FilePath -> IO GHC.HieFile
550568loadHieFile f = do
551569 u <- mkSplitUniqSupply 'a'
src/Development/IDE/Core/Rules.hs+39−13
@@ -59,9 +59,11 @@ import Development.IDE.GHC.Error
5959import Development.Shake hiding (Diagnostic)
6060import Development.IDE.Core.RuleTypes
6161import Development.IDE.Spans.Type
62+import qualified Data.ByteString.Char8 as BS
6263
6364import qualified GHC.LanguageExtensions as LangExt
6465import HscTypes
66+import PackageConfig
6567import DynFlags (gopt_set, xopt)
6668import GHC.Generics(Generic)
6769
@@ -141,7 +143,6 @@ getHomeHieFile f = do
141143 hie_f = ml_hie_file $ ms_location ms
142144 mbHieTimestamp <- use GetModificationTime normal_hie_f
143145 srcTimestamp <- use_ GetModificationTime f
144−
145146 let isUpToDate
146147 | Just d <- mbHieTimestamp = comparing modificationTime d srcTimestamp == GT
147148 | otherwise = False
@@ -191,12 +192,16 @@ priorityFilesOfInterest = Priority (-2)
191192
192193getParsedModuleRule :: Rules ()
193194getParsedModuleRule = defineEarlyCutoff $ \GetParsedModule file -> do
194− hsc <- hscEnv <$> use_ GhcSession file
195+ sess <- use_ GhcSession file
196+ let hsc = hscEnv sess
197+ -- These packages are used when removing PackageImports from a
198+ -- parsed module
199+ comp_pkgs = mapMaybe (fmap fst . mkImportDirs (hsc_dflags hsc)) (deps sess)
195200 opt <- getIdeOptions
196201 (_, contents) <- getFileContents file
197202
198203 let dflags = hsc_dflags hsc
199− mainParse = getParsedModuleDefinition hsc opt file contents
204+ mainParse = getParsedModuleDefinition hsc opt comp_pkgs file contents
200205
201206 -- Parse again (if necessary) to capture Haddock parse errors
202207 if gopt Opt_Haddock dflags
@@ -206,7 +211,7 @@ getParsedModuleRule = defineEarlyCutoff $ \GetParsedModule file -> do
206211 let hscHaddock = hsc{hsc_dflags = gopt_set dflags Opt_Haddock}
207212 haddockParse = do
208213 (_, (!diagsHaddock, _)) <-
209− getParsedModuleDefinition hscHaddock opt file contents
214+ getParsedModuleDefinition hscHaddock opt comp_pkgs file contents
210215 return diagsHaddock
211216
212217 ((fingerPrint, (diags, res)), diagsHaddock) <-
@@ -217,9 +222,9 @@ getParsedModuleRule = defineEarlyCutoff $ \GetParsedModule file -> do
217222
218223 return (fingerPrint, (mergeDiagnostics diags diagsHaddock, res))
219224
220−getParsedModuleDefinition :: HscEnv -> IdeOptions -> NormalizedFilePath -> Maybe T.Text -> IO (Maybe ByteString, ([FileDiagnostic], Maybe ParsedModule))
221−getParsedModuleDefinition packageState opt file contents = do
222− (diag, res) <- parseModule opt packageState (fromNormalizedFilePath file) (fmap textToStringBuffer contents)
225+getParsedModuleDefinition :: HscEnv -> IdeOptions -> [PackageName] -> NormalizedFilePath -> Maybe T.Text -> IO (Maybe ByteString, ([FileDiagnostic], Maybe ParsedModule))
226+getParsedModuleDefinition packageState opt comp_pkgs file contents = do
227+ (diag, res) <- parseModule opt packageState comp_pkgs (fromNormalizedFilePath file) (fmap textToStringBuffer contents)
223228 case res of
224229 Nothing -> pure (Nothing, (diag, Nothing))
225230 Just (contents, modu) -> do
@@ -233,11 +238,13 @@ getLocatedImportsRule =
233238 define $ \GetLocatedImports file -> do
234239 ms <- use_ GetModSummary file
235240 let imports = [(False, imp) | imp <- ms_textual_imps ms] ++ [(True, imp) | imp <- ms_srcimps ms]
236− env <- hscEnv <$> use_ GhcSession file
241+ env_eq <- use_ GhcSession file
242+ let env = hscEnv env_eq
243+ let import_dirs = deps env_eq
237244 let dflags = addRelativeImport file (moduleName $ ms_mod ms) $ hsc_dflags env
238245 opt <- getIdeOptions
239246 (diags, imports') <- fmap unzip $ forM imports $ \(isSource, (mbPkgName, modName)) -> do
240− diagOrImp <- locateModule dflags (optExtensions opt) getFileExists modName mbPkgName isSource
247+ diagOrImp <- locateModule dflags import_dirs (optExtensions opt) getFileExists modName mbPkgName isSource
241248 case diagOrImp of
242249 Left diags -> pure (diags, Left (modName, Nothing))
243250 Right (FileImport path) -> pure ([], Left (modName, Just path))
@@ -522,7 +529,7 @@ instance Hashable GhcSessionIO
522529instance NFData GhcSessionIO
523530instance Binary GhcSessionIO
524531
525−newtype GhcSessionFun = GhcSessionFun (FilePath -> Action HscEnvEq)
532+newtype GhcSessionFun = GhcSessionFun (FilePath -> Action (IdeResult HscEnvEq))
526533instance Show GhcSessionFun where show _ = "GhcSessionFun"
527534instance NFData GhcSessionFun where rnf !_ = ()
528535
@@ -532,11 +539,26 @@ loadGhcSession = do
532539 defineNoFile $ \GhcSessionIO -> do
533540 opts <- getIdeOptions
534541 GhcSessionFun <$> optGhcSession opts
542+ -- This function should always be rerun because it consults a cache to
543+ -- see what HscEnv needs to be used for the file, which can change.
544+ -- However, it should also cut-off early if it's the same HscEnv as
545+ -- last time
535546 defineEarlyCutoff $ \GhcSession file -> do
536547 GhcSessionFun fun <- useNoFile_ GhcSessionIO
548+ alwaysRerun
537549 val <- fun $ fromNormalizedFilePath file
550+
551+ -- TODO: What was this doing before?
538552 opts <- getIdeOptions
539− return ("" <$ optShakeFiles opts, ([], Just val))
553+ let cutoffHash =
554+ case optShakeFiles opts of
555+ -- optShakeFiles is only set in the DAML case.
556+ -- https://github.com/digital-asset/ghcide/pull/522#discussion_r428622915
557+ Just {} -> ""
558+ -- Hash the HscEnvEq returned so cutoff if it didn't change
559+ -- from last time
560+ Nothing -> BS.pack (show (hash (snd val)))
561+ return (Just cutoffHash, val)
540562
541563getHiFileRule :: Rules ()
542564getHiFileRule = defineEarlyCutoff $ \GetHiFile f -> do
@@ -601,12 +623,16 @@ getModIfaceRule = define $ \GetModIface f -> do
601623 -- the interface file does not exist or is out of date.
602624 -- Invoke typechecking directly to update it without incurring a dependency
603625 -- on the parsed module and the typecheck rules
604− hsc <- hscEnv <$> use_ GhcSession f
626+ sess <- use_ GhcSession f
627+ let hsc = hscEnv sess
628+ -- After parsing the module remove all package imports referring to
629+ -- these packages as we have already dealt with what they map to.
630+ comp_pkgs = mapMaybe (fmap fst . mkImportDirs (hsc_dflags hsc)) (deps sess)
605631 opt <- getIdeOptions
606632 (_, contents) <- getFileContents f
607633 -- Embed --haddocks in the interface file
608634 hsc <- pure hsc{hsc_dflags = gopt_set (hsc_dflags hsc) Opt_Haddock}
609− (_, (diags, mb_pm)) <- liftIO $ getParsedModuleDefinition hsc opt f contents
635+ (_, (diags, mb_pm)) <- liftIO $ getParsedModuleDefinition hsc opt comp_pkgs f contents
610636 case mb_pm of
611637 Nothing -> return (diags, Nothing)
612638 Just pm -> do
src/Development/IDE/Core/Shake.hs−3
@@ -171,9 +171,6 @@ instance Hashable Key where
171171-- get empty diagnostics and a Nothing, to indicate this phase throws no fresh
172172-- errors but still failed.
173173--
174−-- A rule on a file should only return diagnostics for that given file. It should
175−-- not propagate diagnostic errors through multiple phases.
176−type IdeResult v = ([FileDiagnostic], Maybe v)
177174
178175data Value v
179176 = Succeeded TextDocumentVersion v
src/Development/IDE/GHC/Compat.hs+5
@@ -26,6 +26,7 @@ module Development.IDE.GHC.Compat(
2626 includePathsQuote,
2727 addIncludePathsQuote,
2828 getModuleHash,
29+ getPackageName,
2930 pattern DerivD,
3031 pattern ForD,
3132 pattern InstD,
@@ -47,6 +48,7 @@ import DynFlags
4748import FieldLabel
4849import Fingerprint (Fingerprint)
4950import qualified Module
51+import Packages
5052
5153import qualified GHC
5254import GHC hiding (
@@ -302,3 +304,6 @@ getConArgs = GHC.getConArgs
302304#else
303305getConArgs = GHC.getConDetails
304306#endif
307+
308+getPackageName :: DynFlags -> Module.InstalledUnitId -> Maybe PackageName
309+getPackageName dfs i = packageName <$> lookupPackage dfs (Module.DefiniteUnitId (Module.DefUnitId i))
src/Development/IDE/GHC/Util.hs+16−8
@@ -8,6 +8,7 @@ module Development.IDE.GHC.Util(
88 modifyDynFlags,
99 evalGhcEnv,
1010 runGhcEnv,
11+ deps,
1112 -- * GHC wrappers
1213 prettyPrint,
1314 printRdrName,
@@ -64,7 +65,7 @@ import Packages (getPackageConfigMap, lookupPackage')
6465import SrcLoc (mkRealSrcLoc)
6566import FastString (mkFastString)
6667import DynFlags (emptyFilesToClean, unsafeGlobalDynFlags)
67−import Module (moduleNameSlashes)
68+import Module (moduleNameSlashes, InstalledUnitId)
6869import OccName (parenSymOcc)
6970import RdrName (nameRdrName, rdrNameOcc)
7071
@@ -166,6 +167,9 @@ moduleImportPath (takeDirectory . fromNormalizedFilePath -> pathDir) mn
166167-- if they are created with the same call to 'newHscEnvEq'.
167168data HscEnvEq
168169 = HscEnvEq !Unique !HscEnv
170+ [(InstalledUnitId, DynFlags)] -- In memory components for this HscEnv
171+ -- This is only used at the moment for the import dirs in
172+ -- the DynFlags
169173 | GhcVersionMismatch { compileTime :: !Version
170174 , runTime :: !Version
171175 }
@@ -175,7 +179,7 @@ hscEnv :: HscEnvEq -> HscEnv
175179hscEnv = either error id . hscEnv'
176180
177181hscEnv' :: HscEnvEq -> Either String HscEnv
178−hscEnv' (HscEnvEq _ x) = Right x
182+hscEnv' (HscEnvEq _ x _) = Right x
179183hscEnv' GhcVersionMismatch{..} = Left $
180184 unwords
181185 ["ghcide compiled against GHC"
@@ -185,25 +189,29 @@ hscEnv' GhcVersionMismatch{..} = Left $
185189 ,". This is unsupported, ghcide must be compiled with the same GHC version as the project."
186190 ]
187191
192+deps :: HscEnvEq -> [(InstalledUnitId, DynFlags)]
193+deps (HscEnvEq _ _ u) = u
194+deps GhcVersionMismatch{} = []
195+
188196-- | Wrap an 'HscEnv' into an 'HscEnvEq'.
189−newHscEnvEq :: HscEnv -> IO HscEnvEq
190−newHscEnvEq e = do u <- newUnique; return $ HscEnvEq u e
197+newHscEnvEq :: HscEnv -> [(InstalledUnitId, DynFlags)] -> IO HscEnvEq
198+newHscEnvEq e uids = do u <- newUnique; return $ HscEnvEq u e uids
191199
192200instance Show HscEnvEq where
193− show (HscEnvEq a _) = "HscEnvEq " ++ show (hashUnique a)
201+ show (HscEnvEq a _ _) = "HscEnvEq " ++ show (hashUnique a)
194202 show GhcVersionMismatch{..} = "GhcVersionMismatch " <> show (compileTime, runTime)
195203
196204instance Eq HscEnvEq where
197− HscEnvEq a _ == HscEnvEq b _ = a == b
205+ HscEnvEq a _ _ == HscEnvEq b _ _ = a == b
198206 GhcVersionMismatch a b == GhcVersionMismatch c d = a == c && b == d
199207 _ == _ = False
200208
201209instance NFData HscEnvEq where
202− rnf (HscEnvEq a b) = rnf (hashUnique a) `seq` b `seq` ()
210+ rnf (HscEnvEq a b c) = rnf (hashUnique a) `seq` b `seq` c `seq` ()
203211 rnf GhcVersionMismatch{} = rnf runTime
204212
205213instance Hashable HscEnvEq where
206− hashWithSalt salt (HscEnvEq u _) = hashWithSalt salt u
214+ hashWithSalt s (HscEnvEq a _b _c) = hashWithSalt s a
207215 hashWithSalt salt GhcVersionMismatch{..} = hashWithSalt salt (compileTime, runTime)
208216
209217-- Fake instance needed to persuade Shake to accept this type as a key.
src/Development/IDE/Import/DependencyInformation.hs+2−2
@@ -62,7 +62,7 @@ data ModuleImports = ModuleImports
6262 -- that module on disk (if we found it)
6363 , packageImports :: !(Set InstalledUnitId)
6464 -- ^ Transitive package dependencies unioned for all imports.
65− }
65+ } deriving Show
6666
6767-- | For processing dependency information, we need lots of maps and sets of
6868-- filepaths. Comparing Strings is really slow, so we work with IntMap/IntSet
@@ -128,7 +128,7 @@ data RawDependencyInformation = RawDependencyInformation
128128 -- need to add edges between .hs-boot and .hs so that the .hs files
129129 -- appear later in the sort.
130130 , rawBootMap :: !BootIdMap
131− }
131+ } deriving Show
132132
133133pkgDependencies :: RawDependencyInformation -> FilePathIdMap (Set InstalledUnitId)
134134pkgDependencies RawDependencyInformation{..} =
src/Development/IDE/Import/FindImports.hs+32−14
@@ -10,16 +10,16 @@ module Development.IDE.Import.FindImports
1010 , ArtifactsLocation(..)
1111 , modSummaryToArtifactsLocation
1212 , isBootLocation
13+ , mkImportDirs
1314 ) where
1415
1516import Development.IDE.GHC.Error as ErrUtils
1617import Development.IDE.GHC.Orphans()
1718import Development.IDE.Types.Diagnostics
1819import Development.IDE.Types.Location
20+import Development.IDE.GHC.Compat
1921-- GHC imports
20−import DynFlags
2122import FastString
22−import GHC
2323import qualified Module as M
2424import Packages
2525import Outputable (showSDoc, ppr, pprPanic)
@@ -31,6 +31,7 @@ import Control.Monad.Extra
3131import Control.Monad.IO.Class
3232import System.FilePath
3333import DriverPhases
34+import Data.Maybe
3435
3536data Import
3637 = FileImport !ArtifactsLocation
@@ -63,55 +64,72 @@ modSummaryToArtifactsLocation nfp ms = ArtifactsLocation nfp (ms_location ms) (i
6364
6465-- | locate a module in the file system. Where we go from *daml to Haskell
6566locateModuleFile :: MonadIO m
66− => DynFlags
67+ => [[FilePath]]
6768 -> [String]
6869 -> (NormalizedFilePath -> m Bool)
6970 -> Bool
7071 -> ModuleName
7172 -> m (Maybe NormalizedFilePath)
72−locateModuleFile dflags exts doesExist isSource modName = do
73− let candidates =
73+locateModuleFile import_dirss exts doesExist isSource modName = do
74+ let candidates import_dirs =
7475 [ toNormalizedFilePath' (prefix </> M.moduleNameSlashes modName <.> maybeBoot ext)
75− | prefix <- importPaths dflags, ext <- exts]
76− findM doesExist candidates
76+ | prefix <- import_dirs , ext <- exts]
77+ findM doesExist (concatMap candidates import_dirss)
7778 where
7879 maybeBoot ext
7980 | isSource = ext ++ "-boot"
8081 | otherwise = ext
8182
83+-- | This function is used to map a package name to a set of import paths.
84+-- It only returns Just for unit-ids which are possible to import into the
85+-- current module. In particular, it will return Nothing for 'main' components
86+-- as they can never be imported into another package.
87+mkImportDirs :: DynFlags -> (M.InstalledUnitId, DynFlags) -> Maybe (PackageName, [FilePath])
88+mkImportDirs df (i, DynFlags{importPaths}) = (, importPaths) <$> getPackageName df i
89+
8290-- | locate a module in either the file system or the package database. Where we go from *daml to
8391-- Haskell
8492locateModule
8593 :: MonadIO m
8694 => DynFlags
95+ -> [(M.InstalledUnitId, DynFlags)] -- Sets import directories to look in
8796 -> [String]
8897 -> (NormalizedFilePath -> m Bool)
8998 -> Located ModuleName
9099 -> Maybe FastString
91100 -> Bool
92101 -> m (Either [FileDiagnostic] Import)
93−locateModule dflags exts doesExist modName mbPkgName isSource = do
102+locateModule dflags comp_info exts doesExist modName mbPkgName isSource = do
94103 case mbPkgName of
95104 -- "this" means that we should only look in the current package
96105 Just "this" -> do
97− mbFile <- locateModuleFile dflags exts doesExist isSource $ unLoc modName
98− case mbFile of
99− Nothing -> return $ Left $ notFoundErr dflags modName $ LookupNotFound []
100− Just file -> toModLocation file
106+ lookupLocal [importPaths dflags]
101107 -- if a package name is given we only go look for a package
102− Just _pkgName -> lookupInPackageDB dflags
108+ Just pkgName
109+ | Just dirs <- lookup (PackageName pkgName) import_paths
110+ -> lookupLocal [dirs]
111+ | otherwise -> lookupInPackageDB dflags
103112 Nothing -> do
104113 -- first try to find the module as a file. If we can't find it try to find it in the package
105114 -- database.
106− mbFile <- locateModuleFile dflags exts doesExist isSource $ unLoc modName
115+ -- Here the importPaths for the current modules are added to the front of the import paths from the other components.
116+ -- This is particularly important for Paths_* modules which get generated for every component but unless you use it in
117+ -- each component will end up being found in the wrong place and cause a multi-cradle match failure.
118+ mbFile <- locateModuleFile (importPaths dflags : map snd import_paths) exts doesExist isSource $ unLoc modName
107119 case mbFile of
108120 Nothing -> lookupInPackageDB dflags
109121 Just file -> toModLocation file
110122 where
123+ import_paths = mapMaybe (mkImportDirs dflags) comp_info
111124 toModLocation file = liftIO $ do
112125 loc <- mkHomeModLocation dflags (unLoc modName) (fromNormalizedFilePath file)
113126 return $ Right $ FileImport $ ArtifactsLocation file loc (not isSource)
114127
128+ lookupLocal dirs = do
129+ mbFile <- locateModuleFile dirs exts doesExist isSource $ unLoc modName
130+ case mbFile of
131+ Nothing -> return $ Left $ notFoundErr dflags modName $ LookupNotFound []
132+ Just file -> toModLocation file
115133
116134 lookupInPackageDB dfs =
117135 case lookupModuleWithSuggestions dfs (unLoc modName) mbPkgName of
src/Development/IDE/Types/Diagnostics.hs+4
@@ -6,6 +6,7 @@ module Development.IDE.Types.Diagnostics (
66 LSP.Diagnostic(..),
77 ShowDiagnostic(..),
88 FileDiagnostic,
9+ IdeResult,
910 LSP.DiagnosticSeverity(..),
1011 DiagnosticStore,
1112 List(..),
@@ -31,6 +32,9 @@ import Data.Text.Prettyprint.Doc.Render.Terminal (Color(..), color)
3132
3233import Development.IDE.Types.Location
3334
35+-- A rule on a file should only return diagnostics for that given file. It should
36+-- not propagate diagnostic errors through multiple phases.
37+type IdeResult v = ([FileDiagnostic], Maybe v)
3438
3539ideErrorText :: NormalizedFilePath -> T.Text -> FileDiagnostic
3640ideErrorText = ideErrorWithSource (Just "compiler") (Just DsError)
src/Development/IDE/Types/Options.hs+4−2
@@ -12,6 +12,7 @@ module Development.IDE.Types.Options
1212 , clientSupportsProgress
1313 , IdePkgLocationOptions(..)
1414 , defaultIdeOptions
15+ , IdeResult
1516 ) where
1617
1718import Development.Shake
@@ -20,12 +21,13 @@ import GHC hiding (parseModule, typecheckModule)
2021import GhcPlugins as GHC hiding (fst3, (<>))
2122import qualified Language.Haskell.LSP.Types.Capabilities as LSP
2223import qualified Data.Text as T
24+import Development.IDE.Types.Diagnostics
2325
2426data IdeOptions = IdeOptions
2527 { optPreprocessor :: GHC.ParsedSource -> IdePreprocessedSource
2628 -- ^ Preprocessor to run over all parsed source trees, generating a list of warnings
2729 -- and a list of errors, along with a new parse tree.
28− , optGhcSession :: Action (FilePath -> Action HscEnvEq)
30+ , optGhcSession :: Action (FilePath -> Action (IdeResult HscEnvEq))
2931 -- ^ Setup a GHC session for a given file, e.g. @Foo.hs@.
3032 -- For the same 'ComponentOptions' from hie-bios, the resulting function will be applied once per file.
3133 -- It is desirable that many files get the same 'HscEnvEq', so that more IDE features work.
@@ -76,7 +78,7 @@ clientSupportsProgress :: LSP.ClientCapabilities -> IdeReportProgress
7678clientSupportsProgress caps = IdeReportProgress $ Just True ==
7779 (LSP._workDoneProgress =<< LSP._window (caps :: LSP.ClientCapabilities))
7880
79−defaultIdeOptions :: Action (FilePath -> Action HscEnvEq) -> IdeOptions
81+defaultIdeOptions :: Action (FilePath -> Action (IdeResult HscEnvEq)) -> IdeOptions
8082defaultIdeOptions session = IdeOptions
8183 {optPreprocessor = IdePreprocessedSource [] []
8284 ,optGhcSession = session
test/data/Bar.hs → test/data/hover/Bar.hsrenamed

The content is the same.

test/data/Foo.hs → test/data/hover/Foo.hsrenamed

The content is the same.

test/data/GotoHover.hs → test/data/hover/GotoHover.hsrenamed

The content is the same.

test/data/multi/a/A.hsnew file+3
@@ -0,0 +1,3 @@
1+module A(foo) where
2+
3+foo = ()
test/data/multi/a/a.cabalnew file+9
@@ -0,0 +1,9 @@
1+name: a
2+version: 1.0.0
3+build-type: Simple
4+cabal-version: >= 1.2
5+
6+library
7+ build-depends: base
8+ exposed-modules: A
9+ hs-source-dirs: .
test/data/multi/b/B.hsnew file+3
@@ -0,0 +1,3 @@
1+module B(module B) where
2+import A
3+qux = foo
test/data/multi/b/b.cabalnew file+9
@@ -0,0 +1,9 @@
1+name: b
2+version: 1.0.0
3+build-type: Simple
4+cabal-version: >= 1.2
5+
6+library
7+ build-depends: base, a
8+ exposed-modules: B
9+ hs-source-dirs: .
test/data/multi/cabal.projectnew file+1
@@ -0,0 +1 @@
1+packages: a b
test/data/multi/hie.yamlnew file+6
@@ -0,0 +1,6 @@
1+cradle:
2+ cabal:
3+ - path: "./a"
4+ component: "lib:a"
5+ - path: "./b"
6+ component: "lib:b"
test/exe/Main.hs+125−55
@@ -10,7 +10,7 @@
1010module Main (main) where
1111
1212import Control.Applicative.Combinators
13−import Control.Exception (catch)
13+import Control.Exception (bracket, catch)
1414import qualified Control.Lens as Lens
1515import Control.Monad
1616import Control.Monad.IO.Class (liftIO)
@@ -35,7 +35,7 @@ import Language.Haskell.LSP.Types.Capabilities
3535import qualified Language.Haskell.LSP.Types.Lens as Lsp (diagnostics, params, message)
3636import Language.Haskell.LSP.VFS (applyChange)
3737import Network.URI
38−import System.Environment.Blank (setEnv)
38+import System.Environment.Blank (getEnv, setEnv, unsetEnv)
3939import System.FilePath
4040import System.IO.Extra
4141import System.Directory
@@ -49,32 +49,35 @@ import Test.Tasty.QuickCheck
4949import Data.Maybe
5050
5151main :: IO ()
52−main = defaultMainWithRerun $ testGroup "HIE"
53− [ testSession "open close" $ do
54− doc <- createDoc "Testing.hs" "haskell" ""
55− void (skipManyTill anyMessage message :: Session WorkDoneProgressCreateRequest)
56− void (skipManyTill anyMessage message :: Session WorkDoneProgressBeginNotification)
57− closeDoc doc
58− void (skipManyTill anyMessage message :: Session WorkDoneProgressEndNotification)
59− , initializeResponseTests
60− , completionTests
61− , cppTests
62− , diagnosticTests
63− , codeActionTests
64− , codeLensesTests
65− , outlineTests
66− , findDefinitionAndHoverTests
67− , pluginTests
68− , preprocessorTests
69− , thTests
70− , safeTests
71− , unitTests
72− , haddockTests
73− , positionMappingTests
74− , watchedFilesTests
75− , cradleTests
76− , dependentFileTest
77− ]
52+main = do
53+ -- We mess with env vars so run single-threaded.
54+ setEnv "TASTY_NUM_THREADS" "1" True
55+ defaultMainWithRerun $ testGroup "HIE"
56+ [ testSession "open close" $ do
57+ doc <- createDoc "Testing.hs" "haskell" ""
58+ void (skipManyTill anyMessage message :: Session WorkDoneProgressCreateRequest)
59+ void (skipManyTill anyMessage message :: Session WorkDoneProgressBeginNotification)
60+ closeDoc doc
61+ void (skipManyTill anyMessage message :: Session WorkDoneProgressEndNotification)
62+ , initializeResponseTests
63+ , completionTests
64+ , cppTests
65+ , diagnosticTests
66+ , codeActionTests
67+ , codeLensesTests
68+ , outlineTests
69+ , findDefinitionAndHoverTests
70+ , pluginTests
71+ , preprocessorTests
72+ , thTests
73+ , safeTests
74+ , unitTests
75+ , haddockTests
76+ , positionMappingTests
77+ , watchedFilesTests
78+ , cradleTests
79+ , dependentFileTest
80+ ]
7881
7982initializeResponseTests :: TestTree
8083initializeResponseTests = withResource acquire release tests where
@@ -1293,27 +1296,19 @@ addSigLensesTests = let
12931296 ]
12941297 ]
12951298
1296−findDefinitionAndHoverTests :: TestTree
1297−findDefinitionAndHoverTests = let
1298−
1299− tst (get, check) pos targetRange title = testSession title $ do
1300− doc <- openTestDataDoc sourceFilePath
1301− found <- get doc pos
1302− check found targetRange
1303−
1304− checkDefs :: [Location] -> Session [Expect] -> Session ()
1305− checkDefs defs mkExpectations = traverse_ check =<< mkExpectations where
1299+checkDefs :: [Location] -> Session [Expect] -> Session ()
1300+checkDefs defs mkExpectations = traverse_ check =<< mkExpectations where
13061301
1307− check (ExpectRange expectedRange) = do
1308− assertNDefinitionsFound 1 defs
1309− assertRangeCorrect (head defs) expectedRange
1310− check (ExpectLocation expectedLocation) = do
1311− assertNDefinitionsFound 1 defs
1312− liftIO $ head defs @?= expectedLocation
1313− check ExpectNoDefinitions = do
1314− assertNDefinitionsFound 0 defs
1315− check ExpectExternFail = liftIO $ assertFailure "Expecting to fail to find in external file"
1316− check _ = pure () -- all other expectations not relevant to getDefinition
1302+ check (ExpectRange expectedRange) = do
1303+ assertNDefinitionsFound 1 defs
1304+ assertRangeCorrect (head defs) expectedRange
1305+ check (ExpectLocation expectedLocation) = do
1306+ assertNDefinitionsFound 1 defs
1307+ liftIO $ head defs @?= expectedLocation
1308+ check ExpectNoDefinitions = do
1309+ assertNDefinitionsFound 0 defs
1310+ check ExpectExternFail = liftIO $ assertFailure "Expecting to fail to find in external file"
1311+ check _ = pure () -- all other expectations not relevant to getDefinition
13171312
13181313 assertNDefinitionsFound :: Int -> [a] -> Session ()
13191314 assertNDefinitionsFound n defs = liftIO $ assertEqual "number of definitions" n (length defs)
@@ -1321,6 +1316,17 @@ findDefinitionAndHoverTests = let
13211316 assertRangeCorrect Location{_range = foundRange} expectedRange =
13221317 liftIO $ expectedRange @=? foundRange
13231318
1319+
1320+findDefinitionAndHoverTests :: TestTree
1321+findDefinitionAndHoverTests = let
1322+
1323+ tst (get, check) pos targetRange title = testSessionWithExtraFiles "hover" title $ \dir -> do
1324+ doc <- openTestDataDoc (dir </> sourceFilePath)
1325+ found <- get doc pos
1326+ check found targetRange
1327+
1328+
1329+
13241330 checkHover :: Maybe Hover -> Session [Expect] -> Session ()
13251331 checkHover hover expectations = traverse_ check =<< expectations where
13261332
@@ -1463,8 +1469,10 @@ findDefinitionAndHoverTests = let
14631469
14641470checkFileCompiles :: FilePath -> TestTree
14651471checkFileCompiles fp =
1466− testSessionWait ("Does " ++ fp ++ " compile") $
1467− void (openTestDataDoc fp)
1472+ testSessionWithExtraFiles "hover" ("Does " ++ fp ++ " compile") $ \dir -> do
1473+ void (openTestDataDoc (dir </> fp))
1474+ expectNoMoreDiagnostics 0.5
1475+
14681476
14691477
14701478pluginTests :: TestTree
@@ -2025,6 +2033,7 @@ cradleTests :: TestTree
20252033cradleTests = testGroup "cradle"
20262034 [testGroup "dependencies" [sessionDepsArePickedUp]
20272035 ,testGroup "loading" [loadCradleOnlyonce]
2036+ ,testGroup "multi" [simpleMultiTest, simpleMultiTest2]
20282037 ]
20292038
20302039loadCradleOnlyonce :: TestTree
@@ -2094,6 +2103,56 @@ cradleLoadedMessage = satisfy $ \case
20942103cradleLoadedMethod :: T.Text
20952104cradleLoadedMethod = "ghcide/cradle/loaded"
20962105
2106+-- Stack sets this which trips up cabal in the multi-component tests.
2107+-- However, our plugin tests rely on those env vars so we unset it locally.
2108+withoutStackEnv :: IO a -> IO a
2109+withoutStackEnv s =
2110+ bracket
2111+ (mapM getEnv vars >>= \prevState -> mapM_ unsetEnv vars >> pure prevState)
2112+ (\prevState -> mapM_ (\(var, value) -> restore var value) (zip vars prevState))
2113+ (const s)
2114+ where vars =
2115+ [ "GHC_PACKAGE_PATH"
2116+ , "GHC_ENVIRONMENT"
2117+ , "HASKELL_DIST_DIR"
2118+ , "HASKELL_PACKAGE_SANDBOX"
2119+ , "HASKELL_PACKAGE_SANDBOXES"
2120+ ]
2121+ restore var Nothing = unsetEnv var
2122+ restore var (Just val) = setEnv var val True
2123+
2124+simpleMultiTest :: TestTree
2125+simpleMultiTest = testCase "simple-multi-test" $ withoutStackEnv $ runWithExtraFiles "multi" $ \dir -> do
2126+ let aPath = dir </> "a/A.hs"
2127+ bPath = dir </> "b/B.hs"
2128+ aSource <- liftIO $ readFileUtf8 aPath
2129+ (TextDocumentIdentifier adoc) <- createDoc aPath "haskell" aSource
2130+ expectNoMoreDiagnostics 0.5
2131+ bSource <- liftIO $ readFileUtf8 bPath
2132+ bdoc <- createDoc bPath "haskell" bSource
2133+ expectNoMoreDiagnostics 0.5
2134+ locs <- getDefinitions bdoc (Position 2 7)
2135+ let fooL = mkL adoc 2 0 2 3
2136+ checkDefs locs (pure [fooL])
2137+ expectNoMoreDiagnostics 0.5
2138+
2139+-- Like simpleMultiTest but open the files in the other order
2140+simpleMultiTest2 :: TestTree
2141+simpleMultiTest2 = testCase "simple-multi-test2" $ withoutStackEnv $ runWithExtraFiles "multi" $ \dir -> do
2142+ let aPath = dir </> "a/A.hs"
2143+ bPath = dir </> "b/B.hs"
2144+ bSource <- liftIO $ readFileUtf8 bPath
2145+ bdoc <- createDoc bPath "haskell" bSource
2146+ expectNoMoreDiagnostics 5
2147+ aSource <- liftIO $ readFileUtf8 aPath
2148+ (TextDocumentIdentifier adoc) <- createDoc aPath "haskell" aSource
2149+ -- Need to have some delay here or the test fails
2150+ expectNoMoreDiagnostics 5
2151+ locs <- getDefinitions bdoc (Position 2 7)
2152+ let fooL = mkL adoc 2 0 2 3
2153+ checkDefs locs (pure [fooL])
2154+ expectNoMoreDiagnostics 0.5
2155+
20972156sessionDepsArePickedUp :: TestTree
20982157sessionDepsArePickedUp = testSession'
20992158 "session-deps-are-picked-up"
@@ -2138,6 +2197,9 @@ sessionDepsArePickedUp = testSession'
21382197testSession :: String -> Session () -> TestTree
21392198testSession name = testCase name . run
21402199
2200+testSessionWithExtraFiles :: FilePath -> String -> (FilePath -> Session ()) -> TestTree
2201+testSessionWithExtraFiles prefix name = testCase name . runWithExtraFiles prefix
2202+
21412203testSession' :: String -> (FilePath -> Session ()) -> TestTree
21422204testSession' name = testCase name . run'
21432205
@@ -2172,6 +2234,19 @@ mkRange a b c d = Range (Position a b) (Position c d)
21722234run :: Session a -> IO a
21732235run s = withTempDir $ \dir -> runInDir dir s
21742236
2237+runWithExtraFiles :: FilePath -> (FilePath -> Session a) -> IO a
2238+runWithExtraFiles prefix s = withTempDir $ \dir -> do
2239+ copyTestDataFiles dir prefix
2240+ runInDir dir (s dir)
2241+
2242+copyTestDataFiles :: FilePath -> FilePath -> IO ()
2243+copyTestDataFiles dir prefix = do
2244+ -- Copy all the test data files to the temporary workspace
2245+ testDataFiles <- getDirectoryFilesIO ("test/data" </> prefix) ["//*"]
2246+ for_ testDataFiles $ \f -> do
2247+ createDirectoryIfMissing True $ dir </> takeDirectory f
2248+ copyFile ("test/data" </> prefix </> f) (dir </> f)
2249+
21752250run' :: (FilePath -> Session a) -> IO a
21762251run' s = withTempDir $ \dir -> runInDir dir (s dir)
21772252
@@ -2183,11 +2258,6 @@ runInDir dir s = do
21832258 -- since the package import test creates "Data/List.hs", which otherwise has no physical home
21842259 createDirectoryIfMissing True $ dir ++ "/Data"
21852260
2186− -- Copy all the test data files to the temporary workspace
2187− testDataFiles <- getDirectoryFilesIO "test/data" ["//*"]
2188− for_ testDataFiles $ \f -> do
2189− createDirectoryIfMissing True $ dir </> takeDirectory f
2190− copyFile ("test/data" </> f) (dir </> f)
21912261
21922262 let cmd = unwords [ghcideExe, "--lsp", "--test", "--cwd", dir]
21932263 -- HIE calls getXgdDirectory which assumes that HOME is set.