Skip to content

Commit 8d700ea

Browse files
committed
Allow users to specify whether to use cabal's multi-repl feature
We add an option to `Config` that allows clients to specify how HLS should load components. We support two loading strategies: * SessionLoadSingleComponent: Always load only a single component when a new component is discovered. * SessionLoadMultipleComponents: Always allow the cradle to load multiple components at once. This might not be always possible, e.g., if the tool doesn't support multiple components loading. The cradle decides how to handle these situations. By default, we use the conservative `SessionLoadSingleComponent` mode. Additionally, changing the config at run-time leads to a reload of the GHC session, allowing users to switch between the modes without restarting the full server.
1 parent 9b0699d commit 8d700ea

File tree

13 files changed

+112
-17
lines changed

13 files changed

+112
-17
lines changed

cabal.project

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ packages:
88
./hls-plugin-api
99
./hls-test-utils
1010

11-
index-state: 2024-03-09T08:17:00Z
11+
index-state: 2024-04-23T12:00:00Z
1212

1313
tests: True
1414
test-show-details: direct

ghcide/ghcide.cabal

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ library
7878
, Glob
7979
, haddock-library >=1.8 && <1.12
8080
, hashable
81-
, hie-bios ==0.13.1
81+
, hie-bios ^>=0.14.0
8282
, hie-compat ^>=0.3.0.0
8383
, hiedb ^>= 0.6.0.0
8484
, hls-graph == 2.7.0.0

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

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import Control.Concurrent.Async
2525
import Control.Concurrent.Strict
2626
import Control.Exception.Safe as Safe
2727
import Control.Monad
28-
import Control.Monad.Extra
28+
import Control.Monad.Extra as Extra
2929
import Control.Monad.IO.Class
3030
import qualified Crypto.Hash.SHA1 as H
3131
import Data.Aeson hiding (Error)
@@ -69,6 +69,7 @@ import Development.IDE.Types.Location
6969
import Development.IDE.Types.Options
7070
import GHC.Check
7171
import qualified HIE.Bios as HieBios
72+
import qualified HIE.Bios.Cradle as HieBios
7273
import HIE.Bios.Environment hiding (getCacheDir)
7374
import HIE.Bios.Types hiding (Log)
7475
import qualified HIE.Bios.Types as HieBios
@@ -79,6 +80,8 @@ import Ide.Logger (Pretty (pretty),
7980
nest,
8081
toCologActionWithPrio,
8182
vcat, viaShow, (<+>))
83+
import Ide.Types (SessionLoadingPreferenceConfig (..),
84+
sessionLoading)
8285
import Language.LSP.Protocol.Message
8386
import Language.LSP.Server
8487
import System.Directory
@@ -147,6 +150,7 @@ data Log
147150
| LogNoneCradleFound FilePath
148151
| LogNewComponentCache !(([FileDiagnostic], Maybe HscEnvEq), DependencyInfo)
149152
| LogHieBios HieBios.Log
153+
| LogSessionLoadingChanged
150154
deriving instance Show Log
151155

152156
instance Pretty Log where
@@ -217,6 +221,8 @@ instance Pretty Log where
217221
LogNewComponentCache componentCache ->
218222
"New component cache HscEnvEq:" <+> viaShow componentCache
219223
LogHieBios msg -> pretty msg
224+
LogSessionLoadingChanged ->
225+
"Session Loading config changed, reloading the full session."
220226

221227
-- | Bump this version number when making changes to the format of the data stored in hiedb
222228
hiedbDataVersion :: String
@@ -447,6 +453,7 @@ loadSessionWithOptions recorder SessionLoadingOptions{..} dir = do
447453
filesMap <- newVar HM.empty :: IO (Var FilesMap)
448454
-- Version of the mappings above
449455
version <- newVar 0
456+
biosSessionLoadingVar <- newVar Nothing :: IO (Var (Maybe SessionLoadingPreferenceConfig))
450457
let returnWithVersion fun = IdeGhcSession fun <$> liftIO (readVar version)
451458
-- This caches the mapping from Mod.hs -> hie.yaml
452459
cradleLoc <- liftIO $ memoIO $ \v -> do
@@ -461,6 +468,7 @@ loadSessionWithOptions recorder SessionLoadingOptions{..} dir = do
461468
runningCradle <- newVar dummyAs :: IO (Var (Async (IdeResult HscEnvEq,[FilePath])))
462469

463470
return $ do
471+
clientConfig <- getClientConfigAction
464472
extras@ShakeExtras{restartShakeSession, ideNc, knownTargetsVar, lspEnv
465473
} <- getShakeExtras
466474
let invalidateShakeCache :: IO ()
@@ -651,7 +659,7 @@ loadSessionWithOptions recorder SessionLoadingOptions{..} dir = do
651659
withTrace "Load cradle" $ \addTag -> do
652660
addTag "file" lfp
653661
old_files <- readIORef cradle_files
654-
res <- cradleToOptsAndLibDir recorder cradle cfp old_files
662+
res <- cradleToOptsAndLibDir recorder (sessionLoading clientConfig) cradle cfp old_files
655663
addTag "result" (show res)
656664
return res
657665

@@ -679,11 +687,38 @@ loadSessionWithOptions recorder SessionLoadingOptions{..} dir = do
679687
void $ modifyVar' filesMap $ HM.insert ncfp hieYaml
680688
return (res, maybe [] pure hieYaml ++ concatMap cradleErrorDependencies err)
681689

690+
let
691+
-- | We allow users to specify a loading strategy.
692+
-- Check whether this config was changed since the last time we have loaded
693+
-- a session.
694+
--
695+
-- If the loading configuration changed, we likely should restart the session
696+
-- in its entirety.
697+
didSessionLoadingPreferenceConfigChange :: IO Bool
698+
didSessionLoadingPreferenceConfigChange = do
699+
mLoadingConfig <- readVar biosSessionLoadingVar
700+
case mLoadingConfig of
701+
Nothing -> do
702+
writeVar biosSessionLoadingVar (Just (sessionLoading clientConfig))
703+
pure False
704+
Just loadingConfig -> do
705+
writeVar biosSessionLoadingVar (Just (sessionLoading clientConfig))
706+
pure (loadingConfig /= sessionLoading clientConfig)
707+
682708
-- This caches the mapping from hie.yaml + Mod.hs -> [String]
683709
-- Returns the Ghc session and the cradle dependencies
684710
let sessionOpts :: (Maybe FilePath, FilePath)
685711
-> IO (IdeResult HscEnvEq, [FilePath])
686712
sessionOpts (hieYaml, file) = do
713+
Extra.whenM didSessionLoadingPreferenceConfigChange $ do
714+
logWith recorder Info LogSessionLoadingChanged
715+
-- If the dependencies are out of date then clear both caches and start
716+
-- again.
717+
modifyVar_ fileToFlags (const (return Map.empty))
718+
modifyVar_ filesMap (const (return HM.empty))
719+
-- Don't even keep the name cache, we start from scratch here!
720+
modifyVar_ hscEnvs (const (return Map.empty))
721+
687722
v <- Map.findWithDefault HM.empty hieYaml <$> readVar fileToFlags
688723
cfp <- makeAbsolute file
689724
case HM.lookup (toNormalizedFilePath' cfp) v of
@@ -694,6 +729,7 @@ loadSessionWithOptions recorder SessionLoadingOptions{..} dir = do
694729
-- If the dependencies are out of date then clear both caches and start
695730
-- again.
696731
modifyVar_ fileToFlags (const (return Map.empty))
732+
modifyVar_ filesMap (const (return HM.empty))
697733
-- Keep the same name cache
698734
modifyVar_ hscEnvs (return . Map.adjust (const []) hieYaml )
699735
consultCradle hieYaml cfp
@@ -713,7 +749,7 @@ loadSessionWithOptions recorder SessionLoadingOptions{..} dir = do
713749
return (([renderPackageSetupException file e], Nothing), maybe [] pure hieYaml)
714750

715751
returnWithVersion $ \file -> do
716-
opts <- liftIO $ join $ mask_ $ modifyVar runningCradle $ \as -> do
752+
opts <- join $ mask_ $ modifyVar runningCradle $ \as -> do
717753
-- If the cradle is not finished, then wait for it to finish.
718754
void $ wait as
719755
asyncRes <- async $ getOptions file
@@ -723,14 +759,14 @@ loadSessionWithOptions recorder SessionLoadingOptions{..} dir = do
723759
-- | Run the specific cradle on a specific FilePath via hie-bios.
724760
-- This then builds dependencies or whatever based on the cradle, gets the
725761
-- GHC options/dynflags needed for the session and the GHC library directory
726-
cradleToOptsAndLibDir :: Recorder (WithPriority Log) -> Cradle Void -> FilePath -> [FilePath]
762+
cradleToOptsAndLibDir :: Recorder (WithPriority Log) -> SessionLoadingPreferenceConfig -> Cradle Void -> FilePath -> [FilePath]
727763
-> IO (Either [CradleError] (ComponentOptions, FilePath))
728-
cradleToOptsAndLibDir recorder cradle file old_files = do
764+
cradleToOptsAndLibDir recorder loadConfig cradle file old_fps = do
729765
-- let noneCradleFoundMessage :: FilePath -> T.Text
730766
-- noneCradleFoundMessage f = T.pack $ "none cradle found for " <> f <> ", ignoring the file"
731767
-- Start off by getting the session options
732768
logWith recorder Debug $ LogCradle cradle
733-
cradleRes <- HieBios.getCompilerOptions file old_files cradle
769+
cradleRes <- HieBios.getCompilerOptions file loadStyle cradle
734770
case cradleRes of
735771
CradleSuccess r -> do
736772
-- Now get the GHC lib dir
@@ -748,6 +784,11 @@ cradleToOptsAndLibDir recorder cradle file old_files = do
748784
logWith recorder Info $ LogNoneCradleFound file
749785
return (Left [])
750786

787+
where
788+
loadStyle = case loadConfig of
789+
PreferSingleComponentLoading -> LoadFile
790+
PreferMultiComponentLoading -> LoadWithContext old_fps
791+
751792
#if MIN_VERSION_ghc(9,3,0)
752793
emptyHscEnv :: NameCache -> FilePath -> IO HscEnv
753794
#else
@@ -1093,7 +1134,7 @@ setOptions cfp (ComponentOptions theOpts compRoot _) dflags = do
10931134
-- component to be created. In case the cradle doesn't list all the targets for
10941135
-- the component, in which case things will be horribly broken anyway.
10951136
--
1096-
-- When we have a single component that is caused to be loaded due to a
1137+
-- When we have a singleComponent that is caused to be loaded due to a
10971138
-- file, we assume the file is part of that component. This is useful
10981139
-- for bare GHC sessions, such as many of the ones used in the testsuite
10991140
--

ghcide/src/Development/IDE/Core/Rules.hs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -701,9 +701,20 @@ loadGhcSession recorder ghcSessionDepsConfig = do
701701
defineEarlyCutOffNoFile (cmapWithPrio LogShake recorder) $ \GhcSessionIO -> do
702702
alwaysRerun
703703
opts <- getIdeOptions
704+
config <- getClientConfigAction
704705
res <- optGhcSession opts
705706

706-
let fingerprint = LBS.toStrict $ B.encode $ hash (sessionVersion res)
707+
let fingerprint = LBS.toStrict $ LBS.concat
708+
[ B.encode (hash (sessionVersion res))
709+
-- When the session version changes, reload all session
710+
-- hsc env sessions
711+
, B.encode (show (sessionLoading config))
712+
-- The loading config affects session loading.
713+
-- Invalidate all build nodes.
714+
-- Changing the session loading config will increment
715+
-- the 'sessionVersion', thus we don't generate the same fingerprint
716+
-- twice by accident.
717+
]
707718
return (fingerprint, res)
708719

709720
defineEarlyCutoff (cmapWithPrio LogShake recorder) $ Rule $ \GhcSession file -> do

haskell-language-server.cabal

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -822,7 +822,7 @@ test-suite hls-stan-plugin-tests
822822
, lens
823823
, lsp-types
824824
, text
825-
default-extensions:
825+
default-extensions:
826826
OverloadedStrings
827827

828828
-----------------------------

hls-plugin-api/src/Ide/Plugin/Config.hs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ parseConfig idePlugins defValue = A.withObject "settings" $ \o ->
4242
<*> o .:? "formattingProvider" .!= formattingProvider defValue
4343
<*> o .:? "cabalFormattingProvider" .!= cabalFormattingProvider defValue
4444
<*> o .:? "maxCompletions" .!= maxCompletions defValue
45+
<*> o .:? "sessionLoading" .!= sessionLoading defValue
4546
<*> A.explicitParseFieldMaybe (parsePlugins idePlugins) o "plugin" .!= plugins defValue
4647

4748
-- | Parse the 'PluginConfig'.

hls-plugin-api/src/Ide/Types.hs

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ module Ide.Types
2222
, IdeNotification(..)
2323
, IdePlugins(IdePlugins, ipMap)
2424
, DynFlagsModifications(..)
25-
, Config(..), PluginConfig(..), CheckParents(..)
25+
, Config(..), PluginConfig(..), CheckParents(..), SessionLoadingPreferenceConfig(..)
2626
, ConfigDescriptor(..), defaultConfigDescriptor, configForPlugin
2727
, CustomConfig(..), mkCustomConfig
2828
, FallbackCodeActionParams(..)
@@ -65,6 +65,7 @@ import Control.Monad.Error.Class (MonadError (throwError))
6565
import Control.Monad.Trans.Class (MonadTrans (lift))
6666
import Control.Monad.Trans.Except (ExceptT, runExceptT)
6767
import Data.Aeson hiding (Null, defaultOptions)
68+
import qualified Data.Aeson.Types as A
6869
import Data.Default
6970
import Data.Dependent.Map (DMap)
7071
import qualified Data.Dependent.Map as DMap
@@ -170,6 +171,7 @@ data Config =
170171
, formattingProvider :: !T.Text
171172
, cabalFormattingProvider :: !T.Text
172173
, maxCompletions :: !Int
174+
, sessionLoading :: !SessionLoadingPreferenceConfig
173175
, plugins :: !(Map.Map PluginId PluginConfig)
174176
} deriving (Show,Eq)
175177

@@ -180,6 +182,7 @@ instance ToJSON Config where
180182
, "formattingProvider" .= formattingProvider
181183
, "cabalFormattingProvider" .= cabalFormattingProvider
182184
, "maxCompletions" .= maxCompletions
185+
, "sessionLoading" .= sessionLoading
183186
, "plugin" .= Map.mapKeysMonotonic (\(PluginId p) -> p) plugins
184187
]
185188

@@ -194,6 +197,7 @@ instance Default Config where
194197
-- , cabalFormattingProvider = "cabal-fmt"
195198
-- this string value needs to kept in sync with the value provided in HlsPlugins
196199
, maxCompletions = 40
200+
, sessionLoading = PreferSingleComponentLoading
197201
, plugins = mempty
198202
}
199203

@@ -206,6 +210,39 @@ data CheckParents
206210
deriving stock (Eq, Ord, Show, Generic)
207211
deriving anyclass (FromJSON, ToJSON)
208212

213+
214+
data SessionLoadingPreferenceConfig
215+
= PreferSingleComponentLoading
216+
-- ^ Always load only a singleComponent when a new component
217+
-- is discovered.
218+
| PreferMultiComponentLoading
219+
-- ^ Always prefer loading multiple components in the cradle
220+
-- at once. This might not be always possible, if the tool doesn't
221+
-- support multiple components loading.
222+
--
223+
-- The cradle can decide how to handle these situations, and whether
224+
-- to honour the preference at all.
225+
deriving stock (Eq, Ord, Show, Generic)
226+
227+
instance Pretty SessionLoadingPreferenceConfig where
228+
pretty PreferSingleComponentLoading = "Prefer Single Component Loading"
229+
pretty PreferMultiComponentLoading = "Prefer Multiple Components Loading"
230+
231+
instance ToJSON SessionLoadingPreferenceConfig where
232+
toJSON PreferSingleComponentLoading =
233+
String "singleComponent"
234+
toJSON PreferMultiComponentLoading =
235+
String "multipleComponents"
236+
237+
instance FromJSON SessionLoadingPreferenceConfig where
238+
parseJSON (String val) = case val of
239+
"singleComponent" -> pure PreferSingleComponentLoading
240+
"multipleComponents" -> pure PreferMultiComponentLoading
241+
_ -> A.prependFailure "parsing SessionLoadingPreferenceConfig failed, "
242+
(A.parseFail $ "Expected one of \"singleComponent\" or \"multipleComponents\" but got " <> T.unpack val )
243+
parseJSON o = A.prependFailure "parsing SessionLoadingPreferenceConfig failed, "
244+
(A.typeMismatch "String" o)
245+
209246
-- | A PluginConfig is a generic configuration for a given HLS plugin. It
210247
-- provides a "big switch" to turn it on or off as a whole, as well as small
211248
-- switches per feature, and a slot for custom config.

stack-lts21.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ allow-newer: true
1818
extra-deps:
1919
- floskell-0.11.1
2020
- hiedb-0.6.0.0
21-
- hie-bios-0.13.1
21+
- hie-bios-0.14.0
2222
- implicit-hie-0.1.4.0
2323
- monad-dijkstra-0.1.1.3
2424
- retrie-1.2.2

stack.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ extra-deps:
1818
- floskell-0.11.1
1919
- retrie-1.2.2
2020
- hiedb-0.6.0.0
21+
- hie-bios-0.14.0
2122
- implicit-hie-0.1.4.0
2223
- lsp-2.4.0.0
2324
- lsp-test-0.17.0.0

test/testdata/schema/ghc92/default-config.golden.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,5 +148,6 @@
148148
"splice": {
149149
"globalOn": true
150150
}
151-
}
151+
},
152+
"sessionLoading": "singleComponent"
152153
}

test/testdata/schema/ghc94/default-config.golden.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,5 +151,6 @@
151151
"stan": {
152152
"globalOn": false
153153
}
154-
}
154+
},
155+
"sessionLoading": "singleComponent"
155156
}

test/testdata/schema/ghc96/default-config.golden.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,5 +151,6 @@
151151
"stan": {
152152
"globalOn": false
153153
}
154-
}
154+
},
155+
"sessionLoading": "singleComponent"
155156
}

test/testdata/schema/ghc98/default-config.golden.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,5 +151,6 @@
151151
"stan": {
152152
"globalOn": false
153153
}
154-
}
154+
},
155+
"sessionLoading": "singleComponent"
155156
}

0 commit comments

Comments
 (0)