Skip to content

Reload .cabal files when they are modified #4630

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ghcide/session-loader/Development/IDE/Session.hs
Original file line number Diff line number Diff line change
Expand Up @@ -499,7 +499,7 @@
hscEnv <- emptyHscEnv ideNc libDir
newTargetDfs <- evalGhcEnv hscEnv $ setOptions optHaddockParse cfp opts (hsc_dflags hscEnv) rootDir
let deps = componentDependencies opts ++ maybeToList hieYaml
dep_info <- getDependencyInfo deps
dep_info <- getDependencyInfo (fmap toAbsolutePath deps)
-- Now lookup to see whether we are combining with an existing HscEnv
-- or making a new one. The lookup returns the HscEnv and a list of
-- information about other components loaded into the HscEnv
Expand Down Expand Up @@ -629,7 +629,7 @@
[] -> error $ "GHC version could not be parsed: " <> version
((runTime, _):_)
| compileTime == runTime -> do
atomicModifyIORef' cradle_files (\xs -> (cfp:xs,()))

Check warning on line 632 in ghcide/session-loader/Development/IDE/Session.hs

View workflow job for this annotation

GitHub Actions / Hlint check run

Warning in loadSessionWithOptions in module Development.IDE.Session: Use atomicModifyIORef'_ ▫︎ Found: "atomicModifyIORef' cradle_files (\\ xs -> (cfp : xs, ()))" ▫︎ Perhaps: "atomicModifyIORef'_ cradle_files ((:) cfp)"
session (hieYaml, toNormalizedFilePath' cfp, opts, libDir)
| otherwise -> return (([renderPackageSetupException cfp GhcVersionMismatch{..}], Nothing),[])
-- Failure case, either a cradle error or the none cradle
Expand Down Expand Up @@ -896,7 +896,7 @@
x <- map errMsgDiagnostic closure_errs
DriverHomePackagesNotClosed us <- pure x
pure us
isBad ci = (homeUnitId_ (componentDynFlags ci)) `OS.member` bad_units

Check warning on line 899 in ghcide/session-loader/Development/IDE/Session.hs

View workflow job for this annotation

GitHub Actions / Hlint check run

Suggestion in newComponentCache in module Development.IDE.Session: Redundant bracket ▫︎ Found: "(homeUnitId_ (componentDynFlags ci)) `OS.member` bad_units" ▫︎ Perhaps: "homeUnitId_ (componentDynFlags ci) `OS.member` bad_units"
-- Whenever we spin up a session on Linux, dynamically load libm.so.6
-- in. We need this in case the binary is statically linked, in which
-- case the interactive session will fail when trying to load
Expand Down
31 changes: 29 additions & 2 deletions ghcide/src/Development/IDE/Core/FileStore.hs
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,6 @@ import System.FilePath
import System.IO.Error
import System.IO.Unsafe


data Log
= LogCouldNotIdentifyReverseDeps !NormalizedFilePath
| LogTypeCheckingReverseDeps !NormalizedFilePath !(Maybe [NormalizedFilePath])
Expand Down Expand Up @@ -147,6 +146,29 @@ getModificationTimeImpl missingFileDiags file = do
then return (Nothing, ([], Nothing))
else return (Nothing, ([diag], Nothing))


getPhysicalModificationTimeRule :: Recorder (WithPriority Log) -> Rules ()
getPhysicalModificationTimeRule recorder = defineEarlyCutoff (cmapWithPrio LogShake recorder) $ Rule $ \GetPhysicalModificationTime file ->
getPhysicalModificationTimeImpl file

getPhysicalModificationTimeImpl
:: NormalizedFilePath
-> Action (Maybe BS.ByteString, ([FileDiagnostic], Maybe FileVersion))
getPhysicalModificationTimeImpl file = do
let file' = fromNormalizedFilePath file
let wrap time = (Just $ LBS.toStrict $ B.encode $ toRational time, ([], Just $ ModificationTime time))

alwaysRerun

liftIO $ fmap wrap (getModTime file')
`catch` \(e :: IOException) -> do
let err | isDoesNotExistError e = "File does not exist: " ++ file'
| otherwise = "IO error while reading " ++ file' ++ ", " ++ displayException e
diag = ideErrorText file (T.pack err)
if isDoesNotExistError e
then return (Nothing, ([], Nothing))
else return (Nothing, ([diag], Nothing))

-- | Interface files cannot be watched, since they live outside the workspace.
-- But interface files are private, in that only HLS writes them.
-- So we implement watching ourselves, and bypass the need for alwaysRerun.
Expand All @@ -170,7 +192,11 @@ resetFileStore ideState changes = mask $ \_ -> do
case c of
LSP.FileChangeType_Changed
-- already checked elsewhere | not $ HM.member nfp fois
-> atomically $ deleteValue (shakeExtras ideState) GetModificationTime nfp
->
atomically $ do
ks <- deleteValue (shakeExtras ideState) GetModificationTime nfp
vs <- deleteValue (shakeExtras ideState) GetPhysicalModificationTime nfp
pure $ ks ++ vs
_ -> pure []


Expand Down Expand Up @@ -233,6 +259,7 @@ getVersionedTextDoc doc = do
fileStoreRules :: Recorder (WithPriority Log) -> (NormalizedFilePath -> Action Bool) -> Rules ()
fileStoreRules recorder isWatched = do
getModificationTimeRule recorder
getPhysicalModificationTimeRule recorder
getFileContentsRule recorder
addWatchedFileRule recorder isWatched

Expand Down
8 changes: 8 additions & 0 deletions ghcide/src/Development/IDE/Core/RuleTypes.hs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
-- Copyright (c) 2019 The DAML Authors. All rights reserved.
-- SPDX-License-Identifier: Apache-2.0

{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE PatternSynonyms #-}
Expand Down Expand Up @@ -319,6 +320,13 @@ instance Hashable GetModificationTime where

instance NFData GetModificationTime

data GetPhysicalModificationTime = GetPhysicalModificationTime
deriving (Generic, Show, Eq)
deriving anyclass (Hashable, NFData)

-- | Get the modification time of a file on disk, ignoring any version in the VFS.
type instance RuleResult GetPhysicalModificationTime = FileVersion

pattern GetModificationTime :: GetModificationTime
pattern GetModificationTime = GetModificationTime_ {missingFileDiagnostics=True}

Expand Down
9 changes: 8 additions & 1 deletion ghcide/src/Development/IDE/Core/Rules.hs
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@
| LogLoadingHieFileFail !FilePath !SomeException
| LogLoadingHieFileSuccess !FilePath
| LogTypecheckedFOI !NormalizedFilePath
| LogDependencies !NormalizedFilePath [FilePath]
deriving Show

instance Pretty Log where
Expand All @@ -207,6 +208,11 @@
<+> "the HLS version being used, the plugins enabled, and if possible the codebase and file which"
<+> "triggered this warning."
]
LogDependencies nfp deps ->
vcat
[ "Add dependency" <+> pretty (fromNormalizedFilePath nfp)
, nest 2 $ pretty deps
]

templateHaskellInstructions :: T.Text
templateHaskellInstructions = "https://haskell-language-server.readthedocs.io/en/latest/troubleshooting.html#static-binaries"
Expand Down Expand Up @@ -715,7 +721,8 @@
let nfp = toNormalizedFilePath' fp
itExists <- getFileExists nfp
when itExists $ void $ do
use_ GetModificationTime nfp
use_ GetPhysicalModificationTime nfp
logWith recorder Logger.Info $ LogDependencies file deps
mapM_ addDependency deps

let cutoffHash = LBS.toStrict $ B.encode (hash (snd val))
Expand Down Expand Up @@ -802,7 +809,7 @@
{ source_version = ver
, old_value = m_old
, get_file_version = use GetModificationTime_{missingFileDiagnostics = False}
, get_linkable_hashes = \fs -> map (snd . fromJust . hirCoreFp) <$> uses_ GetModIface fs

Check warning on line 812 in ghcide/src/Development/IDE/Core/Rules.hs

View workflow job for this annotation

GitHub Actions / Hlint check run

Suggestion in getModIfaceFromDiskRule in module Development.IDE.Core.Rules: Use fmap ▫︎ Found: "\\ fs -> map (snd . fromJust . hirCoreFp) <$> uses_ GetModIface fs" ▫︎ Perhaps: "fmap (map (snd . fromJust . hirCoreFp)) . uses_ GetModIface"
, get_module_graph = useWithSeparateFingerprintRule_ GetModuleGraphTransDepsFingerprints GetModuleGraph f
, regenerate = regenerateHiFile session f ms
}
Expand Down
1 change: 1 addition & 0 deletions haskell-language-server.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,7 @@ test-suite hls-cabal-plugin-tests
, haskell-language-server:hls-cabal-plugin
, hls-test-utils == 2.11.0.0
, lens
, lsp
, lsp-types
, text

Expand Down
14 changes: 12 additions & 2 deletions plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal.hs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE TypeFamilies #-}

module Ide.Plugin.Cabal (descriptor, haskellInteractionDescriptor, Log (..)) where
Expand Down Expand Up @@ -145,7 +146,7 @@ descriptor recorder plId =
\ide vfs _ (DidSaveTextDocumentParams TextDocumentIdentifier{_uri} _) -> liftIO $ do
whenUriFile _uri $ \file -> do
log' Debug $ LogDocSaved _uri
restartCabalShakeSession (shakeExtras ide) vfs file "(saved)" $
restartCabalShakeSessionPhysical (shakeExtras ide) vfs file "(saved)" $
OfInterest.addFileOfInterest ofInterestRecorder ide file OnDisk
, mkPluginNotificationHandler LSP.SMethod_TextDocumentDidClose $
\ide vfs _ (DidCloseTextDocumentParams TextDocumentIdentifier{_uri}) -> liftIO $ do
Expand Down Expand Up @@ -180,7 +181,16 @@ restartCabalShakeSession :: ShakeExtras -> VFS.VFS -> NormalizedFilePath -> Stri
restartCabalShakeSession shakeExtras vfs file actionMsg actionBetweenSession = do
restartShakeSession shakeExtras (VFSModified vfs) (fromNormalizedFilePath file ++ " " ++ actionMsg) [] $ do
keys <- actionBetweenSession
return (toKey GetModificationTime file : keys)
return (toKey GetModificationTime file:keys)

-- | Just like 'restartCabalShakeSession', but records that the 'file' has been changed on disk.
-- So, any action that can only work with on-disk modifications may depend on the 'GetPhysicalModificationTime'
-- rule to get re-run if the file changes on disk.
restartCabalShakeSessionPhysical :: ShakeExtras -> VFS.VFS -> NormalizedFilePath -> String -> IO [Key] -> IO ()
restartCabalShakeSessionPhysical shakeExtras vfs file actionMsg actionBetweenSession = do
restartShakeSession shakeExtras (VFSModified vfs) (fromNormalizedFilePath file ++ " " ++ actionMsg) [] $ do
keys <- actionBetweenSession
return (toKey GetModificationTime file:toKey GetPhysicalModificationTime file:keys)

-- ----------------------------------------------------------------
-- Code Actions
Expand Down
73 changes: 68 additions & 5 deletions plugins/hls-cabal-plugin/test/Main.hs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
{-# LANGUAGE CPP #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DisambiguateRecordFields #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}

module Main (
main,
Expand All @@ -17,14 +19,19 @@ import qualified Data.ByteString as BS
import Data.Either (isRight)
import Data.List.Extra (nubOrdOn)
import qualified Data.Maybe as Maybe
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.IO as Text
import Definition (gotoDefinitionTests)
import Development.IDE.Test
import Ide.Plugin.Cabal.LicenseSuggest (licenseErrorSuggestion)
import qualified Ide.Plugin.Cabal.Parse as Lib
import qualified Language.LSP.Protocol.Lens as L
import qualified Language.LSP.Protocol.Message as L
import Outline (outlineTests)
import System.FilePath
import Test.Hls
import Test.Hls.FileSystem
import Utils

main :: IO ()
Expand All @@ -40,6 +47,7 @@ main = do
, codeActionTests
, gotoDefinitionTests
, hoverTests
, reloadOnCabalChangeTests
]

-- ------------------------------------------------------------------------
Expand Down Expand Up @@ -128,11 +136,6 @@ pluginTests =
_ <- applyEdit doc $ TextEdit (Range (Position 3 20) (Position 4 0)) "BSD-3-Clause\n"
newDiags <- cabalCaptureKick
liftIO $ newDiags @?= []
, runCabalTestCaseSession "No Diagnostics in .hs files from valid .cabal file" "simple-cabal" $ do
hsDoc <- openDoc "A.hs" "haskell"
expectNoMoreDiagnostics 1 hsDoc "typechecking"
cabalDoc <- openDoc "simple-cabal.cabal" "cabal"
expectNoMoreDiagnostics 1 cabalDoc "parsing"
]
]
-- ----------------------------------------------------------------------------
Expand Down Expand Up @@ -262,3 +265,63 @@ hoverOnDependencyTests = testGroup "Hover Dependency"
h <- getHover doc pos
liftIO $ assertBool ("Found hover `" <> show h <> "`") $ Maybe.isNothing h
closeDoc doc

-- ----------------------------------------------------------------------------
-- Reloading of Haskell files on .cabal changes
-- ----------------------------------------------------------------------------

simpleCabalVft :: [FileTree]
simpleCabalVft =
[ copy "hie.yaml"
, copy "simple-reload.cabal"
, copy "Main.hs"
]

simpleCabalFs :: VirtualFileTree
simpleCabalFs = mkVirtualFileTree
(testDataDir </> "simple-reload")
simpleCabalVft

-- Slow tests
reloadOnCabalChangeTests :: TestTree
reloadOnCabalChangeTests = testGroup "Reload on .cabal changes"
[ runCabalTestCaseSessionVft "Change warnings when .cabal file changes" simpleCabalFs $ do
_ <- openDoc "Main.hs" "haskell"
expectDiagnostics [("Main.hs", [(DiagnosticSeverity_Warning, (8, 0), "Top-level binding with no type signature", Just "GHC-38417")])]
waitForAllProgressDone
cabalDoc <- openDoc "simple-reload.cabal" "cabal"
skipManyTill anyMessage cabalKickDone
saveDoc cabalDoc
[trimming|
cabal-version: 3.4
name: simple-reload
version: 0.1.0.0
-- copyright:
build-type: Simple

common warnings
ghc-options: -Wall -Wno-missing-signatures

executable simple-reload
import: warnings
main-is: Main.hs
build-depends: base
default-language: Haskell2010
|]

expectDiagnostics [("Main.hs", [(DiagnosticSeverity_Warning, (2, 0), "The import of \8216Data.List\8217 is redundant", Nothing)])]
]

-- | Persists the given contents to the 'TextDocumentIdentifier' on disk
-- and sends the @textDocument/didSave@ notification.
saveDoc :: TextDocumentIdentifier -> Text -> Session ()
saveDoc docId t = do
-- I couldn't figure out how to get the virtual file contents, so we write it
-- to disk and send the 'SMethod_TextDocumentDidSave' notification
case uriToFilePath (docId ^. L.uri) of
Nothing -> pure ()
Just fp -> do
liftIO $ Text.writeFile fp t

let params = DidSaveTextDocumentParams docId Nothing
sendNotification L.SMethod_TextDocumentDidSave params
9 changes: 9 additions & 0 deletions plugins/hls-cabal-plugin/test/Utils.hs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import qualified Ide.Plugin.Cabal
import Ide.Plugin.Cabal.Completion.Types
import System.FilePath
import Test.Hls
import Test.Hls.FileSystem (VirtualFileTree)


cabalPlugin :: PluginTestDescriptor Ide.Plugin.Cabal.Log
Expand Down Expand Up @@ -57,6 +58,13 @@ runCabalSession :: FilePath -> Session a -> IO a
runCabalSession subdir =
failIfSessionTimeout . runSessionWithServer def cabalPlugin (testDataDir </> subdir)

runCabalTestCaseSessionVft :: TestName -> VirtualFileTree -> Session () -> TestTree
runCabalTestCaseSessionVft title vft = testCase title . runCabalSessionVft vft

runCabalSessionVft :: VirtualFileTree -> Session a -> IO a
runCabalSessionVft vft =
failIfSessionTimeout . runSessionWithServerInTmpDir def cabalPlugin vft

runHaskellAndCabalSession :: FilePath -> Session a -> IO a
runHaskellAndCabalSession subdir =
failIfSessionTimeout . runSessionWithServer def (cabalPlugin <> cabalHaskellPlugin) (testDataDir </> subdir)
Expand All @@ -82,3 +90,4 @@ cabalCaptureKick = captureKickDiagnostics cabalKickStart cabalKickDone
-- | list comparison where the order in the list is irrelevant
(@?==) :: (HasCallStack, Ord a, Show a) => [a] -> [a] -> Assertion
(@?==) l1 l2 = sort l1 @?= sort l2

Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module Main where

import Data.List -- Intentionally unused import, used in the testcase

main :: IO ()
main = foo

-- Missing signature
foo = putStrLn "Hello, World"
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
packages: .
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
cradle:
cabal:
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
cabal-version: 3.4
name: simple-reload
version: 0.1.0.0
-- copyright:
build-type: Simple

common warnings
ghc-options: -Wall -Wno-unused-imports

executable simple-reload
import: warnings
main-is: Main.hs
build-depends: base
default-language: Haskell2010
Loading