Skip to content

Commit 76c74e0

Browse files
committed
[Fix #5005] Add whole-project loading support
renamed "sessionLoading" config to "componentsLoading" deprecated old values in favor of: "single", "multi: needed-only", "multi: whole-project"
1 parent 765b727 commit 76c74e0

15 files changed

Lines changed: 191 additions & 97 deletions

File tree

docs/configuration.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ Here is a list of the additional settings currently supported by `haskell-langua
4545
- Max completions (`haskell.maxCompletions`, default 40): maximum number of completions sent to the LSP client.
4646
- Check project (`haskell.checkProject`, default true): whether to typecheck the entire project on initial load. As it is activated by default could drive to bad performance in large projects.
4747
- Check parents (`haskell.checkParents`, default `CheckOnSave`): when to typecheck reverse dependencies of a file; one of `NeverCheck`, `CheckOnSave` (means dependent/parent modules will only be checked when you save), or `AlwaysCheck` (means re-typechecking them on every change).
48-
- Session loading preference (`haskell.sessionLoading`, default `multipleComponents`): how to load sessions; one of `singleComponent` (means always loading only a single component when a new component is discovered) or `multipleComponents` (means always preferring loading multiple components in the cradle at once). `multipleComponents` might not be always possible, if the tool doesn't support multiple components loading. The cradle can decide how to handle these situations, and whether to honour the preference at all.
48+
- Components loading preference (`haskell.componentsLoading`, default `multi: needed-only`): which components to choose when loading sessions; one of `single` (means always loading only a single component when a new component is discovered), `multi: needed-only` (means preferring loading multiple components in the cradle at once, but only those needed for documents opened), `multi: whole-project` (means preferring loading all the components discovered from the cradle/project; see also `componentsToLoad` cradle config field). `multi: ...` might not be always possible, if the tool doesn't support multiple components loading. The cradle can decide how to handle these situations, and whether to honour the preference at all.
4949

5050
#### Generic plugin configuration
5151

ghcide-test/exe/CradleTests.hs

Lines changed: 114 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -37,28 +37,36 @@ import Language.LSP.Test
3737
import System.FilePath
3838
import Test.Hls (TestConfig (..), def,
3939
runSessionWithTestConfig,
40-
waitForBuildQueue)
40+
waitForBuildQueue, setHlsConfig)
4141
import Test.Hls.FileSystem
4242
import Test.Hls.Util (EnvSpec (..), OS (..),
4343
ignoreInEnv)
4444
import Test.Tasty
4545
import Test.Tasty.HUnit
46+
import Control.Monad (when)
4647

4748

4849
tests :: TestTree
4950
tests = testGroup "cradle"
50-
[testGroup "dependencies" [sessionDepsArePickedUp]
51-
,testGroup "ignore-fatal" [ignoreFatalWarning]
52-
,testGroup "loading" [loadCradleOnlyonce, retryFailedCradle]
51+
[testGroup "dependencies" $ bothLoadings1 sessionDepsArePickedUp
52+
,testGroup "ignore-fatal" $ bothLoadings1 ignoreFatalWarning
53+
,testGroup "loading" $ bothLoadings $ \initM -> [loadCradleOnlyonce initM, retryFailedCradle initM]
5354
,testGroup "regression.batch" batchLoadRegressionTests
5455
,testGroup "multi" (multiTests "multi")
5556
,testGroup "multi-unit" (multiTests "multi-unit")
56-
,testGroup "sub-directory" [simpleSubDirectoryTest]
57-
,testGroup "multi-unit-rexport" [multiRexportTest]
57+
,testGroup "sub-directory" $ bothLoadings1 simpleSubDirectoryTest
58+
,testGroup "multi-unit-rexport" $ bothLoadings1 multiRexportTest
5859
]
5960

60-
loadCradleOnlyonce :: TestTree
61-
loadCradleOnlyonce = testGroup "load cradle only once"
61+
-- | Test both with the default `componentsLoading` and WholeProject.
62+
bothLoadings :: (Session () -> [TestTree]) -> [TestTree]
63+
bothLoadings m = [testGroup "default" (m (return ()))
64+
, testGroup "whole-project" (m setWholeProjectLoading)]
65+
bothLoadings1 :: (Session () -> TestTree) -> [TestTree]
66+
bothLoadings1 m = bothLoadings $ \ initM -> [m initM]
67+
68+
loadCradleOnlyonce :: Session () -> TestTree
69+
loadCradleOnlyonce initM = testGroup "load cradle only once"
6270
[ testWithDummyPluginEmpty' "implicit" implicit
6371
, testWithDummyPluginEmpty' "direct" direct
6472
]
@@ -69,6 +77,7 @@ loadCradleOnlyonce = testGroup "load cradle only once"
6977
test dir
7078
implicit dir = test dir
7179
test _dir = do
80+
initM
7281
doc <- createDoc "B.hs" "haskell" "module B where\nimport Data.Foo"
7382
msgs <- someTill (skipManyTill anyMessage cradleLoadedMessage) (skipManyTill anyMessage (message SMethod_TextDocumentPublishDiagnostics))
7483
liftIO $ length msgs @?= 1
@@ -79,8 +88,9 @@ loadCradleOnlyonce = testGroup "load cradle only once"
7988
msgs <- manyTill (skipManyTill anyMessage cradleLoadedMessage) (skipManyTill anyMessage (message SMethod_TextDocumentPublishDiagnostics))
8089
liftIO $ length msgs @?= 0
8190

82-
retryFailedCradle :: TestTree
83-
retryFailedCradle = testWithDummyPluginEmpty' "retry failed" $ \dir -> do
91+
retryFailedCradle :: Session () -> TestTree
92+
retryFailedCradle initM = testWithDummyPluginEmpty' "retry failed" $ \dir -> do
93+
initM
8494
-- The false cradle always fails
8595
let hieContents = "cradle: {bios: {shell: \"false\"}}"
8696
hiePath = dir </> "hie.yaml"
@@ -108,16 +118,18 @@ cradleLoadedMessage = satisfy $ \case
108118
cradleLoadedMethod :: String
109119
cradleLoadedMethod = "ghcide/cradle/loaded"
110120

111-
ignoreFatalWarning :: TestTree
112-
ignoreFatalWarning = testCase "ignore-fatal-warning" $ runWithExtraFiles "ignore-fatal" $ \dir -> do
121+
ignoreFatalWarning :: Session () -> TestTree
122+
ignoreFatalWarning initM = testCase "ignore-fatal-warning" $ runWithExtraFiles "ignore-fatal" $ \dir -> do
123+
initM
113124
let srcPath = dir </> "IgnoreFatal.hs"
114125
src <- liftIO $ readFileUtf8 srcPath
115126
_ <- createDoc srcPath "haskell" src
116127
expectNoMoreDiagnostics 5
117128

118-
simpleSubDirectoryTest :: TestTree
119-
simpleSubDirectoryTest =
129+
simpleSubDirectoryTest :: Session () -> TestTree
130+
simpleSubDirectoryTest initM =
120131
testCase "simple-subdirectory" $ runWithExtraFiles "cabal-exe" $ \dir -> do
132+
initM
121133
let mainPath = dir </> "a/src/Main.hs"
122134
mainSource <- liftIO $ readFileUtf8 mainPath
123135
_mdoc <- createDoc mainPath "haskell" mainSource
@@ -127,18 +139,31 @@ simpleSubDirectoryTest =
127139
expectNoMoreDiagnostics 0.5
128140

129141
multiTests :: FilePath -> [TestTree]
130-
multiTests dir =
131-
[ simpleMultiTest dir
132-
, simpleMultiTest2 dir
133-
, simpleMultiTest3 dir
134-
, simpleMultiDefTest dir
135-
]
142+
multiTests odir =
143+
[ testGroup "default" $ tests (return ())
144+
, testGroup "whole-project" $ tests setWholeProjectLoading
145+
]
146+
where
147+
tests initM =
148+
[ ignoreForWindows testName $ testCase testName $ runWithExtraFiles odir $ \dir -> initM >> test dir
149+
| (name,test) <-
150+
[ ("test",simpleMultiTest)
151+
, ("test2",simpleMultiTest2)
152+
, ("test3",simpleMultiTest3)
153+
, ("def-test",simpleMultiDefTest)
154+
]
155+
,
156+
let testName = multiTestName odir name
157+
]
158+
ignoreForWindows testName
159+
| testName == "simple-multi-def-test" = ignoreInEnv [HostOS Windows] "Test is flaky on Windows, see #4270"
160+
| otherwise = id
136161

137162
multiTestName :: FilePath -> String -> String
138163
multiTestName dir name = "simple-" ++ dir ++ "-" ++ name
139164

140-
simpleMultiTest :: FilePath -> TestTree
141-
simpleMultiTest variant = testCase (multiTestName variant "test") $ runWithExtraFiles variant $ \dir -> do
165+
simpleMultiTest :: FilePath -> Session ()
166+
simpleMultiTest = \dir -> do
142167
let aPath = dir </> "a/A.hs"
143168
bPath = dir </> "b/B.hs"
144169
adoc <- openDoc aPath "haskell"
@@ -153,8 +178,8 @@ simpleMultiTest variant = testCase (multiTestName variant "test") $ runWithExtra
153178
expectNoMoreDiagnostics 0.5
154179

155180
-- Like simpleMultiTest but open the files in the other order
156-
simpleMultiTest2 :: FilePath -> TestTree
157-
simpleMultiTest2 variant = testCase (multiTestName variant "test2") $ runWithExtraFiles variant $ \dir -> do
181+
simpleMultiTest2 :: FilePath -> Session ()
182+
simpleMultiTest2 = \dir -> do
158183
let aPath = dir </> "a/A.hs"
159184
bPath = dir </> "b/B.hs"
160185
bdoc <- openDoc bPath "haskell"
@@ -167,9 +192,8 @@ simpleMultiTest2 variant = testCase (multiTestName variant "test2") $ runWithExt
167192
expectNoMoreDiagnostics 0.5
168193

169194
-- Now with 3 components
170-
simpleMultiTest3 :: FilePath -> TestTree
171-
simpleMultiTest3 variant =
172-
testCase (multiTestName variant "test3") $ runWithExtraFiles variant $ \dir -> do
195+
simpleMultiTest3 :: FilePath -> Session ()
196+
simpleMultiTest3 = \ dir -> do
173197
let aPath = dir </> "a/A.hs"
174198
bPath = dir </> "b/B.hs"
175199
cPath = dir </> "c/C.hs"
@@ -273,12 +297,31 @@ batchLoadRegressionTests =
273297
, testCase "s1-no-stale-outcomes-across-restart-paths" $
274298
runWithExtraFilesMultiComponent "multi" regressionNoStaleOutcomesOnRestart
275299
]
300+
++ [ testGroup "whole-project"
301+
[ testCase "m1-open-a-then-b-batch-pending-and-success" $
302+
runWithExtraFilesMultiComponent' PreferMultiWholeProjectLoading "multi" runRegressionMultiOpenAThenB
303+
, testCase "m2-open-b-then-a-batch-pending-and-success" $
304+
runWithExtraFilesMultiComponent' PreferMultiWholeProjectLoading "multi" runRegressionMultiOpenBThenA
305+
, testCase "m3-open-b-then-a-then-c-batch-pending-and-success" $
306+
runWithExtraFilesMultiComponent' PreferMultiWholeProjectLoading "multi" runRegressionMultiOpenBThenAThenC
307+
, testCase "f1-batch-pending-failure-does-not-isolate-broken-component" $
308+
runWithExtraFilesMultiComponent' PreferMultiWholeProjectLoading "multi" regressionBatchFailureDoesNotIsolateBrokenComponent
309+
, testCase "f2-failed-file-keeps-failing-until-cradle-fix" $
310+
runWithExtraFilesMultiComponent' PreferMultiWholeProjectLoading "multi" $ regressionFailedFileKeepsFailingUntilFix' False
311+
, testCase "r1-failed-file-recovers-after-cradle-fix" $
312+
runWithExtraFilesMultiComponent' PreferMultiWholeProjectLoading "multi" regressionFailedFileRecoversAfterFix
313+
, testCase "s1-no-stale-outcomes-across-restart-paths" $
314+
runWithExtraFilesMultiComponent' PreferMultiWholeProjectLoading "multi" $ regressionNoStaleOutcomesOnRestart' False
315+
]]
276316

277317
runWithExtraFilesMultiComponent :: String -> (FilePath -> Session a) -> IO a
278-
runWithExtraFilesMultiComponent dirName action = do
318+
runWithExtraFilesMultiComponent = runWithExtraFilesMultiComponent' PreferMultiComponentLoading
319+
320+
runWithExtraFilesMultiComponent' :: SessionLoadingPreferenceConfig -> String -> (FilePath -> Session a) -> IO a
321+
runWithExtraFilesMultiComponent' sesLoading dirName action = do
279322
let vfs = mkIdeTestFs [copyDir dirName]
280323
lspConfig :: Config
281-
lspConfig = def { sessionLoading = PreferMultiComponentLoading }
324+
lspConfig = def { componentsLoading = sesLoading }
282325
conf :: TestConfig ()
283326
conf = def
284327
{ testPluginDescriptor = dummyPlugin
@@ -321,6 +364,11 @@ assertTypeCheckFailure doc msg = do
321364
WaitForIdeRuleResult {..} <- waitForAction "TypeCheck" doc
322365
liftIO $ assertBool msg (not ideResultSuccess)
323366

367+
setWholeProjectLoading :: Session ()
368+
setWholeProjectLoading = do
369+
setIgnoringConfigurationRequests False
370+
setHlsConfig def{componentsLoading = PreferMultiWholeProjectLoading}
371+
324372
regressionBatchFailureIsolatesBrokenFile :: FilePath -> Session ()
325373
regressionBatchFailureIsolatesBrokenFile dir = do
326374
writeBrokenMultiHieYaml dir
@@ -333,8 +381,24 @@ regressionBatchFailureIsolatesBrokenFile dir = do
333381
liftIO $ assertBool "A should typecheck when B cradle mapping is broken" (ideResultSuccess aRes)
334382
liftIO $ assertBool "B should fail with a broken cradle mapping" (not $ ideResultSuccess bRes)
335383

384+
-- | With whole-project loading a failed component blocks the whole session.
385+
regressionBatchFailureDoesNotIsolateBrokenComponent :: FilePath -> Session ()
386+
regressionBatchFailureDoesNotIsolateBrokenComponent dir = do
387+
writeBrokenMultiHieYaml dir
388+
let aPath = dir </> "a/A.hs"
389+
bPath = dir </> "b/B.hs"
390+
adoc <- openDoc aPath "haskell"
391+
bdoc <- openDoc bPath "haskell"
392+
_ <- waitForBuildQueue
393+
[aRes, bRes] <- waitForTypeChecksBatched [adoc, bdoc]
394+
liftIO $ assertBool "A should not typecheck when B cradle mapping is broken" (not $ ideResultSuccess aRes)
395+
liftIO $ assertBool "B should fail with a broken cradle mapping" (not $ ideResultSuccess bRes)
396+
336397
regressionFailedFileKeepsFailingUntilFix :: FilePath -> Session ()
337-
regressionFailedFileKeepsFailingUntilFix dir = do
398+
regressionFailedFileKeepsFailingUntilFix = regressionFailedFileKeepsFailingUntilFix' True
399+
400+
regressionFailedFileKeepsFailingUntilFix' :: Bool -> FilePath -> Session ()
401+
regressionFailedFileKeepsFailingUntilFix' isolated dir = do
338402
writeBrokenMultiHieYaml dir
339403
let aPath = dir </> "a/A.hs"
340404
bPath = dir </> "b/B.hs"
@@ -347,10 +411,11 @@ regressionFailedFileKeepsFailingUntilFix dir = do
347411
[TextDocumentContentChangeEvent . InR . TextDocumentContentChangeWholeDocument $ bSource <> "\n"]
348412
assertTypeCheckFailure bdoc "B should keep failing until the cradle is fixed"
349413

350-
adoc <- openDoc aPath "haskell"
351-
cdoc <- openDoc cPath "haskell"
352-
assertTypeCheckSuccess adoc "A should still typecheck while B remains broken"
353-
assertTypeCheckSuccess cdoc "C should still typecheck while B remains broken"
414+
when isolated $ do
415+
adoc <- openDoc aPath "haskell"
416+
cdoc <- openDoc cPath "haskell"
417+
assertTypeCheckSuccess adoc "A should still typecheck while B remains broken"
418+
assertTypeCheckSuccess cdoc "C should still typecheck while B remains broken"
354419

355420
regressionFailedFileRecoversAfterFix :: FilePath -> Session ()
356421
regressionFailedFileRecoversAfterFix dir = do
@@ -371,7 +436,10 @@ regressionFailedFileRecoversAfterFix dir = do
371436
assertTypeCheckSuccess bdoc "B should recover after restoring the cradle"
372437

373438
regressionNoStaleOutcomesOnRestart :: FilePath -> Session ()
374-
regressionNoStaleOutcomesOnRestart dir = do
439+
regressionNoStaleOutcomesOnRestart = regressionNoStaleOutcomesOnRestart' True
440+
441+
regressionNoStaleOutcomesOnRestart' :: Bool -> FilePath -> Session ()
442+
regressionNoStaleOutcomesOnRestart' isolated dir = do
375443
let hiePath = dir </> "hie.yaml"
376444
aPath = dir </> "a/A.hs"
377445
bPath = dir </> "b/B.hs"
@@ -382,8 +450,9 @@ regressionNoStaleOutcomesOnRestart dir = do
382450
bdoc <- openDoc bPath "haskell"
383451
assertTypeCheckFailure bdoc "B should fail before cradle fix"
384452

385-
adoc <- openDoc aPath "haskell"
386-
assertTypeCheckSuccess adoc "A should remain healthy while B is broken"
453+
when isolated $ do
454+
adoc <- openDoc aPath "haskell"
455+
assertTypeCheckSuccess adoc "A should remain healthy while B is broken"
387456

388457
liftIO $ atomicFileWriteStringUTF8 hiePath (T.unpack validHie)
389458
notifyHieYamlChanged dir
@@ -397,9 +466,8 @@ regressionNoStaleOutcomesOnRestart dir = do
397466
assertTypeCheckSuccess bdoc "B should not keep stale failure after cradle restart"
398467

399468
-- Like simpleMultiTest but open the files in component 'a' in a separate session
400-
simpleMultiDefTest :: FilePath -> TestTree
401-
simpleMultiDefTest variant = ignoreForWindows $ testCase testName $
402-
runWithExtraFiles variant $ \dir -> do
469+
simpleMultiDefTest :: FilePath -> Session ()
470+
simpleMultiDefTest = \dir -> do
403471
let aPath = dir </> "a/A.hs"
404472
bPath = dir </> "b/B.hs"
405473
adoc <- openDoc aPath "haskell"
@@ -411,15 +479,11 @@ simpleMultiDefTest variant = ignoreForWindows $ testCase testName $
411479
let fooL = mkL (adoc ^. L.uri) 2 0 2 3
412480
checkDefs locs (pure [fooL])
413481
expectNoMoreDiagnostics 0.5
414-
where
415-
testName = multiTestName variant "def-test"
416-
ignoreForWindows
417-
| testName == "simple-multi-def-test" = ignoreInEnv [HostOS Windows] "Test is flaky on Windows, see #4270"
418-
| otherwise = id
419482

420-
multiRexportTest :: TestTree
421-
multiRexportTest =
483+
multiRexportTest :: Session () -> TestTree
484+
multiRexportTest initM =
422485
testCase "multi-unit-reexport-test" $ runWithExtraFiles "multi-unit-reexport" $ \dir -> do
486+
initM
423487
let cPath = dir </> "c/C.hs"
424488
cdoc <- openDoc cPath "haskell"
425489
WaitForIdeRuleResult {} <- waitForAction "TypeCheck" cdoc
@@ -429,10 +493,11 @@ multiRexportTest =
429493
checkDefs locs (pure [fooL])
430494
expectNoMoreDiagnostics 0.5
431495

432-
sessionDepsArePickedUp :: TestTree
433-
sessionDepsArePickedUp = testWithDummyPluginEmpty'
496+
sessionDepsArePickedUp :: Session () -> TestTree
497+
sessionDepsArePickedUp initM = testWithDummyPluginEmpty'
434498
"session-deps-are-picked-up"
435499
$ \dir -> do
500+
initM
436501
liftIO $
437502
atomicFileWriteStringUTF8
438503
(dir </> "hie.yaml")

ghcide/ghcide.cabal

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ library
7474
, Glob
7575
, haddock-library >=1.8 && <1.12
7676
, hashable
77-
, hie-bios ^>= 0.19.0
77+
, hie-bios ^>= 0.20.0
7878
, hiedb ^>= 0.8.0.0
7979
, hls-graph == 2.14.0.0
8080
, hls-plugin-api == 2.14.0.0

ghcide/session-loader/Development/IDE/Session.hs

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ import Ide.Logger (Pretty (pretty),
7171
vcat, viaShow, (<+>))
7272
import Ide.Types (Config,
7373
SessionLoadingPreferenceConfig (..),
74-
sessionLoading)
74+
componentsLoading)
7575
import Language.LSP.Protocol.Message
7676
import Language.LSP.Server
7777
import System.Directory
@@ -599,11 +599,11 @@ didSessionLoadingPreferenceConfigChange s = do
599599
mLoadingConfig <- liftIO $ readVar biosSessionLoadingVar
600600
case mLoadingConfig of
601601
Nothing -> do
602-
liftIO $ writeVar biosSessionLoadingVar (Just (sessionLoading clientConfig))
602+
liftIO $ writeVar biosSessionLoadingVar (Just (componentsLoading clientConfig))
603603
pure False
604604
Just loadingConfig -> do
605-
liftIO $ writeVar biosSessionLoadingVar (Just (sessionLoading clientConfig))
606-
pure (loadingConfig /= sessionLoading clientConfig)
605+
liftIO $ writeVar biosSessionLoadingVar (Just (componentsLoading clientConfig))
606+
pure (loadingConfig /= componentsLoading clientConfig)
607607

608608
newSessionState :: IO SessionState
609609
newSessionState = do
@@ -1018,7 +1018,7 @@ loadCradleWithNotifications recorder sessionState hieYaml cfp = do
10181018
let progMsg = "Setting up " <> T.pack (takeBaseName (cradleRootDir cradle))
10191019
<> " (for " <> T.pack lfpLog <> ")"
10201020

1021-
sessionPref <- asks (sessionLoading . sessionClientConfig)
1021+
sessionPref <- asks (componentsLoading . sessionClientConfig)
10221022
extraToLoads <- liftIO $ getExtraFilesToLoad sessionState cfp
10231023
-- Start loading the file!
10241024
eopts <- mRunLspTCallback lspEnv (\act -> withIndefiniteProgress progMsg Nothing NotCancellable (const act)) $
@@ -1040,7 +1040,7 @@ cradleToOptsAndLibDir recorder loadConfig cradle file old_fps = do
10401040
-- noneCradleFoundMessage f = T.pack $ "none cradle found for " <> f <> ", ignoring the file"
10411041
-- Start off by getting the session options
10421042
logWith recorder Debug $ LogCradle cradle
1043-
cradleRes <- HieBios.getCompilerOptions file loadStyle cradle
1043+
cradleRes <- HieBios.getCompilerOptions (TargetWithContext file old_fps) loadStyle cradle
10441044
case cradleRes of
10451045
CradleSuccess r -> do
10461046
-- Now get the GHC lib dir
@@ -1061,8 +1061,9 @@ cradleToOptsAndLibDir recorder loadConfig cradle file old_fps = do
10611061

10621062
where
10631063
loadStyle = case loadConfig of
1064-
PreferSingleComponentLoading -> LoadFile
1065-
PreferMultiComponentLoading -> LoadWithContext old_fps
1064+
PreferSingleComponentLoading -> LoadFile
1065+
PreferMultiComponentLoading -> LoadFileWithContext
1066+
PreferMultiWholeProjectLoading -> LoadUnitsFromCradle
10661067

10671068
-- ----------------------------------------------------------------------------
10681069
-- Utilities

0 commit comments

Comments
 (0)