Skip to content

Commit a6f0008

Browse files
authored
Allow users to specify whether to use cabal's multi-repl feature (#4179)
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 d33f5f0 commit a6f0008

File tree

13 files changed

+115
-19
lines changed

13 files changed

+115
-19
lines changed

cabal.project

+1-1
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

+1-1
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

+51-9
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)
@@ -52,13 +52,13 @@ import Development.IDE.Core.RuleTypes
5252
import Development.IDE.Core.Shake hiding (Log, Priority,
5353
knownTargets, withHieDb)
5454
import qualified Development.IDE.GHC.Compat as Compat
55-
import qualified Development.IDE.GHC.Compat.Util as Compat
5655
import Development.IDE.GHC.Compat.Core hiding (Target,
5756
TargetFile, TargetModule,
5857
Var, Warning, getOptions)
5958
import qualified Development.IDE.GHC.Compat.Core as GHC
6059
import Development.IDE.GHC.Compat.Env hiding (Logger)
6160
import Development.IDE.GHC.Compat.Units (UnitId)
61+
import qualified Development.IDE.GHC.Compat.Util as Compat
6262
import Development.IDE.GHC.Util
6363
import Development.IDE.Graph (Action)
6464
import Development.IDE.Session.VersionCheck
@@ -70,6 +70,7 @@ import Development.IDE.Types.Location
7070
import Development.IDE.Types.Options
7171
import GHC.Check
7272
import qualified HIE.Bios as HieBios
73+
import qualified HIE.Bios.Cradle as HieBios
7374
import HIE.Bios.Environment hiding (getCacheDir)
7475
import HIE.Bios.Types hiding (Log)
7576
import qualified HIE.Bios.Types as HieBios
@@ -80,6 +81,8 @@ import Ide.Logger (Pretty (pretty),
8081
nest,
8182
toCologActionWithPrio,
8283
vcat, viaShow, (<+>))
84+
import Ide.Types (SessionLoadingPreferenceConfig (..),
85+
sessionLoading)
8386
import Language.LSP.Protocol.Message
8487
import Language.LSP.Server
8588
import System.Directory
@@ -123,7 +126,8 @@ import GHC.Data.Bag
123126
import GHC.Driver.Env (hsc_all_home_unit_ids)
124127
import GHC.Driver.Errors.Types
125128
import GHC.Driver.Make (checkHomeUnitsClosed)
126-
import GHC.Types.Error (errMsgDiagnostic, singleMessage)
129+
import GHC.Types.Error (errMsgDiagnostic,
130+
singleMessage)
127131
import GHC.Unit.State
128132
#endif
129133

@@ -149,6 +153,7 @@ data Log
149153
| LogNoneCradleFound FilePath
150154
| LogNewComponentCache !(([FileDiagnostic], Maybe HscEnvEq), DependencyInfo)
151155
| LogHieBios HieBios.Log
156+
| LogSessionLoadingChanged
152157
deriving instance Show Log
153158

154159
instance Pretty Log where
@@ -219,6 +224,8 @@ instance Pretty Log where
219224
LogNewComponentCache componentCache ->
220225
"New component cache HscEnvEq:" <+> viaShow componentCache
221226
LogHieBios msg -> pretty msg
227+
LogSessionLoadingChanged ->
228+
"Session Loading config changed, reloading the full session."
222229

223230
-- | Bump this version number when making changes to the format of the data stored in hiedb
224231
hiedbDataVersion :: String
@@ -449,6 +456,7 @@ loadSessionWithOptions recorder SessionLoadingOptions{..} dir = do
449456
filesMap <- newVar HM.empty :: IO (Var FilesMap)
450457
-- Version of the mappings above
451458
version <- newVar 0
459+
biosSessionLoadingVar <- newVar Nothing :: IO (Var (Maybe SessionLoadingPreferenceConfig))
452460
let returnWithVersion fun = IdeGhcSession fun <$> liftIO (readVar version)
453461
-- This caches the mapping from Mod.hs -> hie.yaml
454462
cradleLoc <- liftIO $ memoIO $ \v -> do
@@ -463,6 +471,7 @@ loadSessionWithOptions recorder SessionLoadingOptions{..} dir = do
463471
runningCradle <- newVar dummyAs :: IO (Var (Async (IdeResult HscEnvEq,[FilePath])))
464472

465473
return $ do
474+
clientConfig <- getClientConfigAction
466475
extras@ShakeExtras{restartShakeSession, ideNc, knownTargetsVar, lspEnv
467476
} <- getShakeExtras
468477
let invalidateShakeCache :: IO ()
@@ -653,7 +662,7 @@ loadSessionWithOptions recorder SessionLoadingOptions{..} dir = do
653662
withTrace "Load cradle" $ \addTag -> do
654663
addTag "file" lfp
655664
old_files <- readIORef cradle_files
656-
res <- cradleToOptsAndLibDir recorder cradle cfp old_files
665+
res <- cradleToOptsAndLibDir recorder (sessionLoading clientConfig) cradle cfp old_files
657666
addTag "result" (show res)
658667
return res
659668

@@ -681,11 +690,38 @@ loadSessionWithOptions recorder SessionLoadingOptions{..} dir = do
681690
void $ modifyVar' filesMap $ HM.insert ncfp hieYaml
682691
return (res, maybe [] pure hieYaml ++ concatMap cradleErrorDependencies err)
683692

693+
let
694+
-- | We allow users to specify a loading strategy.
695+
-- Check whether this config was changed since the last time we have loaded
696+
-- a session.
697+
--
698+
-- If the loading configuration changed, we likely should restart the session
699+
-- in its entirety.
700+
didSessionLoadingPreferenceConfigChange :: IO Bool
701+
didSessionLoadingPreferenceConfigChange = do
702+
mLoadingConfig <- readVar biosSessionLoadingVar
703+
case mLoadingConfig of
704+
Nothing -> do
705+
writeVar biosSessionLoadingVar (Just (sessionLoading clientConfig))
706+
pure False
707+
Just loadingConfig -> do
708+
writeVar biosSessionLoadingVar (Just (sessionLoading clientConfig))
709+
pure (loadingConfig /= sessionLoading clientConfig)
710+
684711
-- This caches the mapping from hie.yaml + Mod.hs -> [String]
685712
-- Returns the Ghc session and the cradle dependencies
686713
let sessionOpts :: (Maybe FilePath, FilePath)
687714
-> IO (IdeResult HscEnvEq, [FilePath])
688715
sessionOpts (hieYaml, file) = do
716+
Extra.whenM didSessionLoadingPreferenceConfigChange $ do
717+
logWith recorder Info LogSessionLoadingChanged
718+
-- If the dependencies are out of date then clear both caches and start
719+
-- again.
720+
modifyVar_ fileToFlags (const (return Map.empty))
721+
modifyVar_ filesMap (const (return HM.empty))
722+
-- Don't even keep the name cache, we start from scratch here!
723+
modifyVar_ hscEnvs (const (return Map.empty))
724+
689725
v <- Map.findWithDefault HM.empty hieYaml <$> readVar fileToFlags
690726
cfp <- makeAbsolute file
691727
case HM.lookup (toNormalizedFilePath' cfp) v of
@@ -696,6 +732,7 @@ loadSessionWithOptions recorder SessionLoadingOptions{..} dir = do
696732
-- If the dependencies are out of date then clear both caches and start
697733
-- again.
698734
modifyVar_ fileToFlags (const (return Map.empty))
735+
modifyVar_ filesMap (const (return HM.empty))
699736
-- Keep the same name cache
700737
modifyVar_ hscEnvs (return . Map.adjust (const []) hieYaml )
701738
consultCradle hieYaml cfp
@@ -715,7 +752,7 @@ loadSessionWithOptions recorder SessionLoadingOptions{..} dir = do
715752
return (([renderPackageSetupException file e], Nothing), maybe [] pure hieYaml)
716753

717754
returnWithVersion $ \file -> do
718-
opts <- liftIO $ join $ mask_ $ modifyVar runningCradle $ \as -> do
755+
opts <- join $ mask_ $ modifyVar runningCradle $ \as -> do
719756
-- If the cradle is not finished, then wait for it to finish.
720757
void $ wait as
721758
asyncRes <- async $ getOptions file
@@ -725,14 +762,14 @@ loadSessionWithOptions recorder SessionLoadingOptions{..} dir = do
725762
-- | Run the specific cradle on a specific FilePath via hie-bios.
726763
-- This then builds dependencies or whatever based on the cradle, gets the
727764
-- GHC options/dynflags needed for the session and the GHC library directory
728-
cradleToOptsAndLibDir :: Recorder (WithPriority Log) -> Cradle Void -> FilePath -> [FilePath]
765+
cradleToOptsAndLibDir :: Recorder (WithPriority Log) -> SessionLoadingPreferenceConfig -> Cradle Void -> FilePath -> [FilePath]
729766
-> IO (Either [CradleError] (ComponentOptions, FilePath))
730-
cradleToOptsAndLibDir recorder cradle file old_files = do
767+
cradleToOptsAndLibDir recorder loadConfig cradle file old_fps = do
731768
-- let noneCradleFoundMessage :: FilePath -> T.Text
732769
-- noneCradleFoundMessage f = T.pack $ "none cradle found for " <> f <> ", ignoring the file"
733770
-- Start off by getting the session options
734771
logWith recorder Debug $ LogCradle cradle
735-
cradleRes <- HieBios.getCompilerOptions file old_files cradle
772+
cradleRes <- HieBios.getCompilerOptions file loadStyle cradle
736773
case cradleRes of
737774
CradleSuccess r -> do
738775
-- Now get the GHC lib dir
@@ -750,6 +787,11 @@ cradleToOptsAndLibDir recorder cradle file old_files = do
750787
logWith recorder Info $ LogNoneCradleFound file
751788
return (Left [])
752789

790+
where
791+
loadStyle = case loadConfig of
792+
PreferSingleComponentLoading -> LoadFile
793+
PreferMultiComponentLoading -> LoadWithContext old_fps
794+
753795
#if MIN_VERSION_ghc(9,3,0)
754796
emptyHscEnv :: NameCache -> FilePath -> IO HscEnv
755797
#else
@@ -1150,7 +1192,7 @@ setOptions cfp (ComponentOptions theOpts compRoot _) dflags = do
11501192
-- component to be created. In case the cradle doesn't list all the targets for
11511193
-- the component, in which case things will be horribly broken anyway.
11521194
--
1153-
-- When we have a single component that is caused to be loaded due to a
1195+
-- When we have a singleComponent that is caused to be loaded due to a
11541196
-- file, we assume the file is part of that component. This is useful
11551197
-- for bare GHC sessions, such as many of the ones used in the testsuite
11561198
--

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

+12-1
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

+1-1
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

+1
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

+38-1
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

+1-1
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

+1
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

+2-1
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

+2-1
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

+2-1
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

+2-1
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)