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(
17import Development.IDE.GHC.Orphans()
18import Development.IDE.Core.Shake
19import Control.Concurrent.Extra
20import qualified Data.Map.Strict as Map
22import qualified Data.Text as T
23import Control.Monad.Extra
24import Development.Shake
25import Development.Shake.Classes
26import Control.Exception
28import Data.Either.Extra
30import qualified Data.ByteString.Char8 as BS
31import Development.IDE.Types.Diagnostics
32import Development.IDE.Types.Location
33import qualified Data.Rope.UTF16 as Rope
37import qualified System.Directory as Dir
40import Foreign.C.String
42import Foreign.Marshal (alloca)
43import Foreign.Storable
44import qualified System.Posix.Error as Posix
47import Language.Haskell.LSP.Core
48import Language.Haskell.LSP.VFS
50-- | haskell-lsp manages the VFS internally and automatically so we cannot use
51-- the builtin VFS without spawning up an LSP server. To be able to test things
52-- like `setBufferModified` we abstract over the VFS implementation.
53data VFSHandle = VFSHandle
54 { getVirtualFile :: NormalizedUri -> IO (Maybe VirtualFile)
55 -- ^ get the contents of a virtual file
56 , setVirtualFileContents :: Maybe (NormalizedUri -> Maybe T.Text -> IO ())
57 -- ^ set a specific file to a value. If Nothing then we are ignoring these
58 -- signals anyway so can just say something was modified
61instance IsIdeGlobal VFSHandle
63makeVFSHandle :: IO VFSHandle
65 vfsVar <- newVar (1, Map.empty)
67 { getVirtualFile = \uri -> do
68 (_nextVersion, vfs) <- readVar vfsVar
69 pure $ Map.lookup uri vfs
70 , setVirtualFileContents = Just $ \uri content ->
71 modifyVar_ vfsVar $ \(nextVersion, vfs) -> pure $ (nextVersion + 1, ) $
73 Nothing -> Map.delete uri vfs
74 -- The second version number is only used in persistFileVFS which we do not use so we set it to 0.
75 Just content -> Map.insert uri (VirtualFile nextVersion 0 (Rope.fromText content)) vfs
78makeLSPVFSHandle :: LspFuncs c -> VFSHandle
79makeLSPVFSHandle lspFuncs = VFSHandle
80 { getVirtualFile = getVirtualFileFunc lspFuncs
81 , setVirtualFileContents = Nothing
85-- | Get the contents of a file, either dirty (if the buffer is modified) or Nothing to mean use from disk.
86type instance RuleResult GetFileContents = (FileVersion, Maybe T.Text)
88data GetFileContents = GetFileContents
89 deriving (Eq, Show, Generic)
90instance Hashable GetFileContents
91instance NFData GetFileContents
92instance Binary GetFileContents
94getModificationTimeRule :: VFSHandle -> Rules ()
95getModificationTimeRule vfs =
96 defineEarlyCutoff $ \GetModificationTime file -> do
97 let file' = fromNormalizedFilePath file
98 let wrap time@(l,s) = (Just $ BS.pack $ show time, ([], Just $ ModificationTime l s))
100 mbVirtual <- liftIO $ getVirtualFile vfs $ filePathToUri' file
102 Just (virtualFileVersion -> ver) ->
103 pure (Just $ BS.pack $ show ver, ([], Just $ VFSVersion ver))
104 Nothing -> liftIO $ fmap wrap (getModTime file')
105 `catch` \(e :: IOException) -> do
106 let err | isDoesNotExistError e = "File does not exist: " ++ file'
107 | otherwise = "IO error while reading " ++ file' ++ ", " ++ displayException e
108 return (Nothing, ([ideErrorText file $ T.pack err], Nothing))
110 -- Dir.getModificationTime is surprisingly slow since it performs
111 -- a ton of conversions. Since we do not actually care about
112 -- the format of the time, we can get away with something cheaper.
113 -- For now, we only try to do this on Unix systems where it seems to get the
114 -- time spent checking file modifications (which happens on every change)
115 -- from > 0.5s to ~0.15s.
116 -- We might also want to try speeding this up on Windows at some point.
117 -- TODO leverage DidChangeWatchedFile lsp notifications on clients that
118 -- support them, as done for GetFileExists
119 getModTime :: FilePath -> IO (Int,Int)
121#ifdef mingw32_HOST_OS
122 do time <- Dir.getModificationTime f
123 let !day = fromInteger $ toModifiedJulianDay $ utctDay time
124 !dayTime = fromInteger $ diffTimeToPicoseconds $ utctDayTime time
127 withCString f $ \f' ->
129 alloca $ \nsecPtr -> do
130 Posix.throwErrnoPathIfMinus1Retry_ "getmodtime" f $ c_getModTime f' secPtr nsecPtr
133 pure (fromEnum sec, fromIntegral nsec)
135-- Sadly even unix’s getFileStatus + modificationTimeHiRes is still about twice as slow
136-- as doing the FFI call ourselves :(.
137foreign import ccall "getmodtime" c_getModTime :: CString -> Ptr CTime -> Ptr CLong -> IO Int
140getFileContentsRule :: VFSHandle -> Rules ()
141getFileContentsRule vfs =
142 define $ \GetFileContents file -> do
143 -- need to depend on modification time to introduce a dependency with Cutoff
144 time <- use_ GetModificationTime file
145 res <- liftIO $ ideTryIOException file $ do
146 mbVirtual <- getVirtualFile vfs $ filePathToUri' file
147 pure $ Rope.toText . _text <$> mbVirtual
149 Left err -> return ([err], Nothing)
150 Right contents -> return ([], Just (time, contents))
152ideTryIOException :: NormalizedFilePath -> IO a -> IO (Either FileDiagnostic a)
153ideTryIOException fp act =
155 (\(e :: IOException) -> ideErrorText fp $ T.pack $ show e)
159getFileContents :: NormalizedFilePath -> Action (FileVersion, Maybe T.Text)
160getFileContents = use_ GetFileContents
162fileStoreRules :: VFSHandle -> Rules ()
163fileStoreRules vfs = do
165 getModificationTimeRule vfs
166 getFileContentsRule vfs
169-- | Notify the compiler service that a particular file has been modified.
170-- Use 'Nothing' to say the file is no longer in the virtual file system
171-- but should be sourced from disk, or 'Just' to give its new value.
172setBufferModified :: IdeState -> NormalizedFilePath -> Maybe T.Text -> IO ()
173setBufferModified state absFile contents = do
174 VFSHandle{..} <- getIdeGlobalState state
175 whenJust setVirtualFileContents $ \set ->
176 set (filePathToUri' absFile) contents
177 void $ shakeRun state []
179-- | Note that some buffer somewhere has been modified, but don't say what.
180-- Only valid if the virtual file system was initialised by LSP, as that
181-- independently tracks which files are modified.
182setSomethingModified :: IdeState -> IO ()
183setSomethingModified state = do
184 VFSHandle{..} <- getIdeGlobalState state
185 when (isJust setVirtualFileContents) $
186 fail "setSomethingModified can't be called on this type of VFSHandle"
187 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