1-- Copyright (c) 2019 The DAML Authors. All rights reserved.
2-- SPDX-License-Identifier: Apache-2.0
4{-# LANGUAGE TypeFamilies #-}
6module Development.IDE.Core.FileStore(
7 getFileExists, getFileContents,
17import Development.IDE.GHC.Orphans()
18import Development.IDE.GHC.Util
20import Control.Concurrent.Extra
21import qualified Data.Map.Strict as Map
23import qualified Data.Text as T
24import Control.Monad.Extra
25import qualified System.Directory as Dir
26import Development.Shake
27import Development.Shake.Classes
28import Development.IDE.Core.Shake
29import Control.Exception
31import Data.Either.Extra
33import qualified Data.ByteString.Char8 as BS
34import Development.IDE.Types.Diagnostics
35import Development.IDE.Types.Location
36import qualified Data.Rope.UTF16 as Rope
41import Foreign.C.String
43import Foreign.Marshal (alloca)
45import Foreign.Storable
46import qualified System.Posix.Error as Posix
49import Language.Haskell.LSP.Core
50import Language.Haskell.LSP.VFS
52-- | haskell-lsp manages the VFS internally and automatically so we cannot use
53-- the builtin VFS without spawning up an LSP server. To be able to test things
54-- like `setBufferModified` we abstract over the VFS implementation.
55data VFSHandle = VFSHandle
56 { getVirtualFile :: NormalizedUri -> IO (Maybe VirtualFile)
57 -- ^ get the contents of a virtual file
58 , setVirtualFileContents :: Maybe (NormalizedUri -> Maybe T.Text -> IO ())
59 -- ^ set a specific file to a value. If Nothing then we are ignoring these
60 -- signals anyway so can just say something was modified
63instance IsIdeGlobal VFSHandle
65makeVFSHandle :: IO VFSHandle
67 vfsVar <- newVar (1, Map.empty)
69 { getVirtualFile = \uri -> do
70 (_nextVersion, vfs) <- readVar vfsVar
71 pure $ Map.lookup uri vfs
72 , setVirtualFileContents = Just $ \uri content ->
73 modifyVar_ vfsVar $ \(nextVersion, vfs) -> pure $ (nextVersion + 1, ) $
75 Nothing -> Map.delete uri vfs
76 Just content -> Map.insert uri (VirtualFile nextVersion (Rope.fromText content) Nothing) vfs
79makeLSPVFSHandle :: LspFuncs c -> VFSHandle
80makeLSPVFSHandle lspFuncs = VFSHandle
81 { getVirtualFile = getVirtualFileFunc lspFuncs
82 , setVirtualFileContents = Nothing
86-- | Get the contents of a file, either dirty (if the buffer is modified) or Nothing to mean use from disk.
87type instance RuleResult GetFileContents = (FileVersion, Maybe StringBuffer)
89-- | Does the file exist.
90type instance RuleResult GetFileExists = Bool
93data GetFileExists = GetFileExists
94 deriving (Eq, Show, Generic)
95instance Hashable GetFileExists
96instance NFData GetFileExists
98data GetFileContents = GetFileContents
99 deriving (Eq, Show, Generic)
100instance Hashable GetFileContents
101instance NFData GetFileContents
104getFileExistsRule :: VFSHandle -> Rules ()
105getFileExistsRule vfs =
106 defineEarlyCutoff $ \GetFileExists file -> do
108 res <- liftIO $ handle (\(_ :: IOException) -> return False) $
109 (isJust <$> getVirtualFile vfs (filePathToUri' file)) ||^
110 Dir.doesFileExist (fromNormalizedFilePath file)
111 return (Just $ if res then BS.singleton '1' else BS.empty, ([], Just res))
114getModificationTimeRule :: VFSHandle -> Rules ()
115getModificationTimeRule vfs =
116 defineEarlyCutoff $ \GetModificationTime file -> do
117 let file' = fromNormalizedFilePath file
118 let wrap time = (Just time, ([], Just $ ModificationTime time))
120 mbVirtual <- liftIO $ getVirtualFile vfs $ filePathToUri' file
122 Just (VirtualFile ver _ _) -> pure (Just $ BS.pack $ show ver, ([], Just $ VFSVersion ver))
123 Nothing -> liftIO $ fmap wrap (getModTime file')
124 `catch` \(e :: IOException) -> do
125 let err | isDoesNotExistError e = "File does not exist: " ++ file'
126 | otherwise = "IO error while reading " ++ file' ++ ", " ++ displayException e
127 return (Nothing, ([ideErrorText file $ T.pack err], Nothing))
129 -- Dir.getModificationTime is surprisingly slow since it performs
130 -- a ton of conversions. Since we do not actually care about
131 -- the format of the time, we can get away with something cheaper.
132 -- For now, we only try to do this on Unix systems where it seems to get the
133 -- time spent checking file modifications (which happens on every change)
134 -- from > 0.5s to ~0.15s.
135 -- We might also want to try speeding this up on Windows at some point.
136 getModTime :: FilePath -> IO BS.ByteString
138#ifdef mingw32_HOST_OS
139 do time <- Dir.getModificationTime f
140 pure $! BS.pack $ show (toModifiedJulianDay $ utctDay time, diffTimeToPicoseconds $ utctDayTime time)
142 withCString f $ \f' ->
144 alloca $ \nsecPtr -> do
145 Posix.throwErrnoPathIfMinus1Retry_ "getmodtime" f $ c_getModTime f' secPtr nsecPtr
148 pure $! BS.pack $ show sec <> "." <> show nsec
150-- Sadly even unix’s getFileStatus + modificationTimeHiRes is still about twice as slow
151-- as doing the FFI call ourselves :(.
152foreign import ccall "getmodtime" c_getModTime :: CString -> Ptr CTime -> Ptr CLong -> IO Int
155getFileContentsRule :: VFSHandle -> Rules ()
156getFileContentsRule vfs =
157 define $ \GetFileContents file -> do
158 -- need to depend on modification time to introduce a dependency with Cutoff
159 time <- use_ GetModificationTime file
160 res <- liftIO $ ideTryIOException file $ do
161 mbVirtual <- getVirtualFile vfs $ filePathToUri' file
162 pure $ textToStringBuffer . Rope.toText . _text <$> mbVirtual
164 Left err -> return ([err], Nothing)
165 Right contents -> return ([], Just (time, contents))
167ideTryIOException :: NormalizedFilePath -> IO a -> IO (Either FileDiagnostic a)
168ideTryIOException fp act =
170 (\(e :: IOException) -> ideErrorText fp $ T.pack $ show e)
174getFileContents :: NormalizedFilePath -> Action (FileVersion, Maybe StringBuffer)
175getFileContents = use_ GetFileContents
177getFileExists :: NormalizedFilePath -> Action Bool
179 -- we deliberately and intentionally wrap the file as an FilePath WITHOUT mkAbsolute
180 -- so that if the file doesn't exist, is on a shared drive that is unmounted etc we get a properly
181 -- cached 'No' rather than an exception in the wrong place
185fileStoreRules :: VFSHandle -> Rules ()
186fileStoreRules vfs = do
188 getModificationTimeRule vfs
189 getFileContentsRule vfs
190 getFileExistsRule vfs
193-- | Notify the compiler service that a particular file has been modified.
194-- Use 'Nothing' to say the file is no longer in the virtual file system
195-- but should be sourced from disk, or 'Just' to give its new value.
196setBufferModified :: IdeState -> NormalizedFilePath -> Maybe T.Text -> IO ()
197setBufferModified state absFile contents = do
198 VFSHandle{..} <- getIdeGlobalState state
199 whenJust setVirtualFileContents $ \set ->
200 set (filePathToUri' absFile) contents
201 void $ shakeRun state []
203-- | Note that some buffer somewhere has been modified, but don't say what.
204-- Only valid if the virtual file system was initialised by LSP, as that
205-- independently tracks which files are modified.
206setSomethingModified :: IdeState -> IO ()
207setSomethingModified state = do
208 VFSHandle{..} <- getIdeGlobalState state
209 when (isJust setVirtualFileContents) $
210 fail "setSomethingModified can't be called on this type of VFSHandle"
211 void $ shakeRun state []
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list