Skip to content

Commit a47b398

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 a47b398

File tree

13 files changed

+110
-15
lines changed

13 files changed

+110
-15
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

+47-5
Original file line numberDiff line numberDiff line change
@@ -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 (SessionLoadingConfig (..),
84+
sessionLoading)
8285
import Language.LSP.Protocol.Message
8386
import Language.LSP.Server
8487
import System.Directory
@@ -127,6 +130,7 @@ import GHC.Unit.State
127130
#endif
128131

129132
import GHC.ResponseFile
133+
import qualified Control.Monad.Extra as Extra
130134

131135
data Log
132136
= LogSettingInitialDynFlags
@@ -147,6 +151,7 @@ data Log
147151
| LogNoneCradleFound FilePath
148152
| LogNewComponentCache !(([FileDiagnostic], Maybe HscEnvEq), DependencyInfo)
149153
| LogHieBios HieBios.Log
154+
| LogSessionLoadingChanged
150155
deriving instance Show Log
151156

152157
instance Pretty Log where
@@ -217,6 +222,8 @@ instance Pretty Log where
217222
LogNewComponentCache componentCache ->
218223
"New component cache HscEnvEq:" <+> viaShow componentCache
219224
LogHieBios msg -> pretty msg
225+
LogSessionLoadingChanged ->
226+
"Session Loading Config change, reload the full session."
220227

221228
-- | Bump this version number when making changes to the format of the data stored in hiedb
222229
hiedbDataVersion :: String
@@ -447,6 +454,7 @@ loadSessionWithOptions recorder SessionLoadingOptions{..} dir = do
447454
filesMap <- newVar HM.empty :: IO (Var FilesMap)
448455
-- Version of the mappings above
449456
version <- newVar 0
457+
biosSessionLoadingVar <- newVar Nothing :: IO (Var (Maybe SessionLoadingConfig))
450458
let returnWithVersion fun = IdeGhcSession fun <$> liftIO (readVar version)
451459
-- This caches the mapping from Mod.hs -> hie.yaml
452460
cradleLoc <- liftIO $ memoIO $ \v -> do
@@ -461,6 +469,7 @@ loadSessionWithOptions recorder SessionLoadingOptions{..} dir = do
461469
runningCradle <- newVar dummyAs :: IO (Var (Async (IdeResult HscEnvEq,[FilePath])))
462470

463471
return $ do
472+
clientConfig <- getClientConfigAction
464473
extras@ShakeExtras{restartShakeSession, ideNc, knownTargetsVar, lspEnv
465474
} <- getShakeExtras
466475
let invalidateShakeCache :: IO ()
@@ -651,7 +660,7 @@ loadSessionWithOptions recorder SessionLoadingOptions{..} dir = do
651660
withTrace "Load cradle" $ \addTag -> do
652661
addTag "file" lfp
653662
old_files <- readIORef cradle_files
654-
res <- cradleToOptsAndLibDir recorder cradle cfp old_files
663+
res <- cradleToOptsAndLibDir recorder (sessionLoading clientConfig) cradle cfp old_files
655664
addTag "result" (show res)
656665
return res
657666

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

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

715752
returnWithVersion $ \file -> do
716-
opts <- liftIO $ join $ mask_ $ modifyVar runningCradle $ \as -> do
753+
opts <- join $ mask_ $ modifyVar runningCradle $ \as -> do
717754
-- If the cradle is not finished, then wait for it to finish.
718755
void $ wait as
719756
asyncRes <- async $ getOptions file
@@ -723,14 +760,14 @@ loadSessionWithOptions recorder SessionLoadingOptions{..} dir = do
723760
-- | Run the specific cradle on a specific FilePath via hie-bios.
724761
-- This then builds dependencies or whatever based on the cradle, gets the
725762
-- GHC options/dynflags needed for the session and the GHC library directory
726-
cradleToOptsAndLibDir :: Recorder (WithPriority Log) -> Cradle Void -> FilePath -> [FilePath]
763+
cradleToOptsAndLibDir :: Recorder (WithPriority Log) -> SessionLoadingConfig -> Cradle Void -> FilePath -> [FilePath]
727764
-> IO (Either [CradleError] (ComponentOptions, FilePath))
728-
cradleToOptsAndLibDir recorder cradle file old_files = do
765+
cradleToOptsAndLibDir recorder loadConfig cradle file old_fps = do
729766
-- let noneCradleFoundMessage :: FilePath -> T.Text
730767
-- noneCradleFoundMessage f = T.pack $ "none cradle found for " <> f <> ", ignoring the file"
731768
-- Start off by getting the session options
732769
logWith recorder Debug $ LogCradle cradle
733-
cradleRes <- HieBios.getCompilerOptions file old_files cradle
770+
cradleRes <- HieBios.getCompilerOptions file loadStyle cradle
734771
case cradleRes of
735772
CradleSuccess r -> do
736773
-- Now get the GHC lib dir
@@ -748,6 +785,11 @@ cradleToOptsAndLibDir recorder cradle file old_files = do
748785
logWith recorder Info $ LogNoneCradleFound file
749786
return (Left [])
750787

788+
where
789+
loadStyle = case loadConfig of
790+
SessionLoadSingleComponent -> LoadFile
791+
SessionLoadMultipleComponents -> LoadWithContext old_fps
792+
751793
#if MIN_VERSION_ghc(9,3,0)
752794
emptyHscEnv :: NameCache -> FilePath -> IO HscEnv
753795
#else

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

+37-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(..), SessionLoadingConfig(..)
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 :: !SessionLoadingConfig
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 = SessionLoadSingleComponent
197201
, plugins = mempty
198202
}
199203

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

213+
214+
data SessionLoadingConfig
215+
= SessionLoadSingleComponent
216+
-- ^ Always load only a single component when a new component
217+
-- is discovered.
218+
| SessionLoadMultipleComponents
219+
-- ^ Always allow the cradle to load multiple components
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.
224+
deriving stock (Eq, Ord, Show, Generic)
225+
226+
instance Pretty SessionLoadingConfig where
227+
pretty SessionLoadSingleComponent = "Single Component Session Loading"
228+
pretty SessionLoadMultipleComponents = "Multiple Components Session Loading"
229+
230+
instance ToJSON SessionLoadingConfig where
231+
toJSON SessionLoadSingleComponent =
232+
String "Single Component"
233+
toJSON SessionLoadMultipleComponents =
234+
String "Multiple Components"
235+
236+
instance FromJSON SessionLoadingConfig where
237+
parseJSON (String val) = case val of
238+
"Single Component" -> pure SessionLoadSingleComponent
239+
"Multiple Components" -> pure SessionLoadMultipleComponents
240+
_ -> A.prependFailure "parsing SessionLoadingConfig failed, "
241+
(A.parseFail $ "Expected one of \"Single Component\" or \"Multiple Components\" but got " <> T.unpack val )
242+
parseJSON o = A.prependFailure "parsing SessionLoadingConfig failed, "
243+
(A.typeMismatch "String" o)
244+
209245
-- | A PluginConfig is a generic configuration for a given HLS plugin. It
210246
-- provides a "big switch" to turn it on or off as a whole, as well as small
211247
-- 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": "Single Component"
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": "Single Component"
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": "Single Component"
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": "Single Component"
155156
}

0 commit comments

Comments
 (0)