99{-# LANGUAGE TupleSections #-}
1010{-# LANGUAGE TypeFamilies #-}
1111
12- module Ide.Plugin.Cabal (descriptor , Log (.. )) where
12+ module Ide.Plugin.Cabal (descriptor , Log (.. )) where
1313
1414import Control.Concurrent.STM
1515import Control.Concurrent.Strict
1616import Control.DeepSeq
1717import Control.Monad.Extra
1818import Control.Monad.IO.Class
19- import qualified Data.ByteString as BS
19+ import qualified Data.ByteString as BS
2020import Data.Hashable
21- import Data.HashMap.Strict (HashMap )
22- import qualified Data.HashMap.Strict as HashMap
23- import qualified Data.List.NonEmpty as NE
24- import qualified Data.Text as T
25- import qualified Data.Text.Encoding as Encoding
26- import qualified Data.Text.Utf16.Rope as Rope
21+ import Data.HashMap.Strict (HashMap )
22+ import qualified Data.HashMap.Strict as HashMap
23+ import qualified Data.List.NonEmpty as NE
24+ import qualified Data.Text.Encoding as Encoding
25+ import qualified Data.Text.Utf16.Rope as Rope
2726import Data.Typeable
28- import Development.IDE as D
29- import Development.IDE.Core.Shake (restartShakeSession )
30- import qualified Development.IDE.Core.Shake as Shake
31- import Development.IDE.Graph (alwaysRerun )
32- import Distribution.Compat.Lens ((^.) )
33- import Distribution.Simple.PackageDescription (readGenericPackageDescription )
34- import Distribution.Verbosity (silent )
27+ import Development.IDE as D
28+ import Development.IDE.Core.Shake (restartShakeSession )
29+ import qualified Development.IDE.Core.Shake as Shake
30+ import Development.IDE.Graph (alwaysRerun )
31+ import Distribution.Compat.Lens ((^.) )
3532import GHC.Generics
3633import Ide.Plugin.Cabal.Completions
37- import qualified Ide.Plugin.Cabal.Diagnostics as Diagnostics
38- import qualified Ide.Plugin.Cabal.LicenseSuggest as LicenseSuggest
39- import qualified Ide.Plugin.Cabal.Parse as Parse
34+ import qualified Ide.Plugin.Cabal.Diagnostics as Diagnostics
35+ import qualified Ide.Plugin.Cabal.LicenseSuggest as LicenseSuggest
36+ import qualified Ide.Plugin.Cabal.Parse as Parse
4037import Ide.Types
41- import qualified Language.LSP.Server as LSP
38+ import qualified Language.LSP.Server as LSP
4239import Language.LSP.Types
43- import qualified Language.LSP.Types as J
44- import qualified Language.LSP.Types as LSP
45- import qualified Language.LSP.Types.Lens as JL
46- import Language.LSP.VFS (VirtualFile )
47- import qualified Language.LSP.VFS as VFS
40+ import qualified Language.LSP.Types as J
41+ import qualified Language.LSP.Types as LSP
42+ import qualified Language.LSP.Types.Lens as JL
43+ import Language.LSP.VFS (VirtualFile )
44+ import qualified Language.LSP.VFS as VFS
45+
4846data Log
4947 = LogModificationTime NormalizedFilePath FileVersion
5048 | LogShake Shake. Log
@@ -53,12 +51,12 @@ data Log
5351 | LogDocSaved Uri
5452 | LogDocClosed Uri
5553 | LogFOI (HashMap NormalizedFilePath FileOfInterestStatus )
56- deriving Show
54+ deriving ( Show )
5755
5856instance Pretty Log where
5957 pretty = \ case
6058 LogShake log' -> pretty log'
61- LogModificationTime nfp modTime ->
59+ LogModificationTime nfp modTime ->
6260 " Modified:" <+> pretty (fromNormalizedFilePath nfp) <+> pretty (show modTime)
6361 LogDocOpened uri ->
6462 " Opened text document:" <+> pretty (getUri uri)
@@ -71,56 +69,56 @@ instance Pretty Log where
7169 LogFOI files ->
7270 " Set files of interest to:" <+> viaShow files
7371
74-
7572descriptor :: Recorder (WithPriority Log ) -> PluginId -> PluginDescriptor IdeState
76- descriptor recorder plId = (defaultCabalPluginDescriptor plId)
77- { pluginRules = cabalRules recorder
78- , pluginHandlers = mkPluginHandler STextDocumentCodeAction licenseSuggestCodeAction
79- <> mkPluginHandler J. STextDocumentCompletion completion
80- , pluginNotificationHandlers = mconcat
81- [ mkPluginNotificationHandler LSP. STextDocumentDidOpen $
82- \ ide vfs _ (DidOpenTextDocumentParams TextDocumentItem {_uri,_version}) -> liftIO $ do
83- whenUriFile _uri $ \ file -> do
84- log' Debug $ LogDocOpened _uri
85- addFileOfInterest recorder ide file Modified {firstOpen= True }
86- restartCabalShakeSession (shakeExtras ide) vfs file " (opened)"
87-
88- , mkPluginNotificationHandler LSP. STextDocumentDidChange $
89- \ ide vfs _ (DidChangeTextDocumentParams VersionedTextDocumentIdentifier {_uri} _) -> liftIO $ do
90- whenUriFile _uri $ \ file -> do
91- log' Debug $ LogDocModified _uri
92- addFileOfInterest recorder ide file Modified {firstOpen= False }
93- restartCabalShakeSession (shakeExtras ide) vfs file " (changed)"
94-
95- , mkPluginNotificationHandler LSP. STextDocumentDidSave $
96- \ ide vfs _ (DidSaveTextDocumentParams TextDocumentIdentifier {_uri} _) -> liftIO $ do
97- whenUriFile _uri $ \ file -> do
98- log' Debug $ LogDocSaved _uri
99- addFileOfInterest recorder ide file OnDisk
100- restartCabalShakeSession (shakeExtras ide) vfs file " (saved)"
101-
102- , mkPluginNotificationHandler LSP. STextDocumentDidClose $
103- \ ide vfs _ (DidCloseTextDocumentParams TextDocumentIdentifier {_uri}) -> liftIO $ do
104- whenUriFile _uri $ \ file -> do
105- log' Debug $ LogDocClosed _uri
106- deleteFileOfInterest recorder ide file
107- restartCabalShakeSession (shakeExtras ide) vfs file " (closed)"
108- ]
109- }
110- where
111- log' = logWith recorder
112-
113- whenUriFile :: Uri -> (NormalizedFilePath -> IO () ) -> IO ()
114- whenUriFile uri act = whenJust (LSP. uriToFilePath uri) $ act . toNormalizedFilePath'
115-
116- -- | Helper function to restart the shake session, specifically for modifying .cabal files.
117- -- No special logic, just group up a bunch of functions you need for the base
118- -- Notification Handlers.
119- --
120- -- To make sure diagnostics are up to date, we need to tell shake that the file was touched and
121- -- needs to be re-parsed. That's what we do when we record the dirty key that our parsing
122- -- rule depends on.
123- -- Then we restart the shake session, so that changes to our virtual files are actually picked up.
73+ descriptor recorder plId =
74+ (defaultCabalPluginDescriptor plId)
75+ { pluginRules = cabalRules recorder
76+ , pluginHandlers =
77+ mkPluginHandler STextDocumentCodeAction licenseSuggestCodeAction
78+ <> mkPluginHandler J. STextDocumentCompletion completion
79+ , pluginNotificationHandlers =
80+ mconcat
81+ [ mkPluginNotificationHandler LSP. STextDocumentDidOpen $
82+ \ ide vfs _ (DidOpenTextDocumentParams TextDocumentItem {_uri, _version}) -> liftIO $ do
83+ whenUriFile _uri $ \ file -> do
84+ log' Debug $ LogDocOpened _uri
85+ addFileOfInterest recorder ide file Modified {firstOpen = True }
86+ restartCabalShakeSession (shakeExtras ide) vfs file " (opened)"
87+ , mkPluginNotificationHandler LSP. STextDocumentDidChange $
88+ \ ide vfs _ (DidChangeTextDocumentParams VersionedTextDocumentIdentifier {_uri} _) -> liftIO $ do
89+ whenUriFile _uri $ \ file -> do
90+ log' Debug $ LogDocModified _uri
91+ addFileOfInterest recorder ide file Modified {firstOpen = False }
92+ restartCabalShakeSession (shakeExtras ide) vfs file " (changed)"
93+ , mkPluginNotificationHandler LSP. STextDocumentDidSave $
94+ \ ide vfs _ (DidSaveTextDocumentParams TextDocumentIdentifier {_uri} _) -> liftIO $ do
95+ whenUriFile _uri $ \ file -> do
96+ log' Debug $ LogDocSaved _uri
97+ addFileOfInterest recorder ide file OnDisk
98+ restartCabalShakeSession (shakeExtras ide) vfs file " (saved)"
99+ , mkPluginNotificationHandler LSP. STextDocumentDidClose $
100+ \ ide vfs _ (DidCloseTextDocumentParams TextDocumentIdentifier {_uri}) -> liftIO $ do
101+ whenUriFile _uri $ \ file -> do
102+ log' Debug $ LogDocClosed _uri
103+ deleteFileOfInterest recorder ide file
104+ restartCabalShakeSession (shakeExtras ide) vfs file " (closed)"
105+ ]
106+ }
107+ where
108+ log' = logWith recorder
109+
110+ whenUriFile :: Uri -> (NormalizedFilePath -> IO () ) -> IO ()
111+ whenUriFile uri act = whenJust (LSP. uriToFilePath uri) $ act . toNormalizedFilePath'
112+
113+ {- | Helper function to restart the shake session, specifically for modifying .cabal files.
114+ No special logic, just group up a bunch of functions you need for the base
115+ Notification Handlers.
116+
117+ To make sure diagnostics are up to date, we need to tell shake that the file was touched and
118+ needs to be re-parsed. That's what we do when we record the dirty key that our parsing
119+ rule depends on.
120+ Then we restart the shake session, so that changes to our virtual files are actually picked up.
121+ -}
124122restartCabalShakeSession :: ShakeExtras -> VFS. VFS -> NormalizedFilePath -> String -> IO ()
125123restartCabalShakeSession shakeExtras vfs file actionMsg = do
126124 join $ atomically $ Shake. recordDirtyKeys shakeExtras GetModificationTime [file]
@@ -131,9 +129,9 @@ restartCabalShakeSession shakeExtras vfs file actionMsg = do
131129-- ----------------------------------------------------------------
132130
133131data ParseCabal = ParseCabal
134- deriving (Eq , Show , Typeable , Generic )
132+ deriving (Eq , Show , Typeable , Generic )
135133instance Hashable ParseCabal
136- instance NFData ParseCabal
134+ instance NFData ParseCabal
137135
138136type instance RuleResult ParseCabal = ()
139137
@@ -168,15 +166,16 @@ cabalRules recorder = do
168166 -- Must be careful to not impede the performance too much. Crucial to
169167 -- a snappy IDE experience.
170168 kick
171- where
172- log' = logWith recorder
173-
174- -- | This is the kick function for the cabal plugin.
175- -- We run this action, whenever we shake session us run/restarted, which triggers
176- -- actions to produce diagnostics for cabal files.
177- --
178- -- It is paramount that this kick-function can be run quickly, since it is a blocking
179- -- function invocation.
169+ where
170+ log' = logWith recorder
171+
172+ {- | This is the kick function for the cabal plugin.
173+ We run this action, whenever we shake session us run/restarted, which triggers
174+ actions to produce diagnostics for cabal files.
175+
176+ It is paramount that this kick-function can be run quickly, since it is a blocking
177+ function invocation.
178+ -}
180179kick :: Action ()
181180kick = do
182181 files <- HashMap. keys <$> getCabalFilesOfInterestUntracked
@@ -186,84 +185,86 @@ kick = do
186185-- Code Actions
187186-- ----------------------------------------------------------------
188187
189- licenseSuggestCodeAction
190- :: IdeState
191- -> PluginId
192- -> CodeActionParams
193- -> LSP. LspM Config (Either ResponseError (ResponseResult 'TextDocumentCodeAction))
194- licenseSuggestCodeAction _ _ (CodeActionParams _ _ (TextDocumentIdentifier uri) _range CodeActionContext {_diagnostics= List diags}) =
188+ licenseSuggestCodeAction ::
189+ IdeState ->
190+ PluginId ->
191+ CodeActionParams ->
192+ LSP. LspM Config (Either ResponseError (ResponseResult 'TextDocumentCodeAction))
193+ licenseSuggestCodeAction _ _ (CodeActionParams _ _ (TextDocumentIdentifier uri) _range CodeActionContext {_diagnostics = List diags}) =
195194 pure $ Right $ List $ diags >>= (fmap InR . (LicenseSuggest. licenseErrorAction uri))
196195
197196-- ----------------------------------------------------------------
198197-- Cabal file of Interest rules and global variable
199198-- ----------------------------------------------------------------
200199
201- -- | Cabal files that are currently open in the lsp-client.
202- -- Specific actions happen when these files are saved, closed or modified,
203- -- such as generating diagnostics, re-parsing, etc...
204- --
205- -- We need to store the open files to parse them again if we restart the shake session.
206- -- Restarting of the shake session happens whenever these files are modified.
200+ {- | Cabal files that are currently open in the lsp-client.
201+ Specific actions happen when these files are saved, closed or modified,
202+ such as generating diagnostics, re-parsing, etc...
203+
204+ We need to store the open files to parse them again if we restart the shake session.
205+ Restarting of the shake session happens whenever these files are modified.
206+ -}
207207newtype OfInterestCabalVar = OfInterestCabalVar (Var (HashMap NormalizedFilePath FileOfInterestStatus ))
208208
209209instance Shake. IsIdeGlobal OfInterestCabalVar
210210
211211data IsCabalFileOfInterest = IsCabalFileOfInterest
212- deriving (Eq , Show , Typeable , Generic )
212+ deriving (Eq , Show , Typeable , Generic )
213213instance Hashable IsCabalFileOfInterest
214- instance NFData IsCabalFileOfInterest
214+ instance NFData IsCabalFileOfInterest
215215
216216type instance RuleResult IsCabalFileOfInterest = CabalFileOfInterestResult
217217
218218data CabalFileOfInterestResult = NotCabalFOI | IsCabalFOI FileOfInterestStatus
219219 deriving (Eq , Show , Typeable , Generic )
220220instance Hashable CabalFileOfInterestResult
221- instance NFData CabalFileOfInterestResult
221+ instance NFData CabalFileOfInterestResult
222222
223- -- | The rule that initialises the files of interest state.
224- --
225- -- Needs to be run on start-up.
223+ {- | The rule that initialises the files of interest state.
224+
225+ Needs to be run on start-up.
226+ -}
226227ofInterestRules :: Recorder (WithPriority Log ) -> Rules ()
227228ofInterestRules recorder = do
228- Shake. addIdeGlobal . OfInterestCabalVar =<< liftIO (newVar HashMap. empty)
229- Shake. defineEarlyCutoff (cmapWithPrio LogShake recorder) $ RuleNoDiagnostics $ \ IsCabalFileOfInterest f -> do
230- alwaysRerun
231- filesOfInterest <- getCabalFilesOfInterestUntracked
232- let foi = maybe NotCabalFOI IsCabalFOI $ f `HashMap.lookup` filesOfInterest
233- fp = summarize foi
234- res = (Just fp, Just foi)
235- return res
236- where
237- summarize NotCabalFOI = BS. singleton 0
238- summarize (IsCabalFOI OnDisk ) = BS. singleton 1
239- summarize (IsCabalFOI (Modified False )) = BS. singleton 2
240- summarize (IsCabalFOI (Modified True )) = BS. singleton 3
229+ Shake. addIdeGlobal . OfInterestCabalVar =<< liftIO (newVar HashMap. empty)
230+ Shake. defineEarlyCutoff (cmapWithPrio LogShake recorder) $ RuleNoDiagnostics $ \ IsCabalFileOfInterest f -> do
231+ alwaysRerun
232+ filesOfInterest <- getCabalFilesOfInterestUntracked
233+ let foi = maybe NotCabalFOI IsCabalFOI $ f `HashMap.lookup` filesOfInterest
234+ fp = summarize foi
235+ res = (Just fp, Just foi)
236+ return res
237+ where
238+ summarize NotCabalFOI = BS. singleton 0
239+ summarize (IsCabalFOI OnDisk ) = BS. singleton 1
240+ summarize (IsCabalFOI (Modified False )) = BS. singleton 2
241+ summarize (IsCabalFOI (Modified True )) = BS. singleton 3
241242
242243getCabalFilesOfInterestUntracked :: Action (HashMap NormalizedFilePath FileOfInterestStatus )
243244getCabalFilesOfInterestUntracked = do
244- OfInterestCabalVar var <- Shake. getIdeGlobalAction
245- liftIO $ readVar var
245+ OfInterestCabalVar var <- Shake. getIdeGlobalAction
246+ liftIO $ readVar var
246247
247248addFileOfInterest :: Recorder (WithPriority Log ) -> IdeState -> NormalizedFilePath -> FileOfInterestStatus -> IO ()
248249addFileOfInterest recorder state f v = do
249- OfInterestCabalVar var <- Shake. getIdeGlobalState state
250- (prev, files) <- modifyVar var $ \ dict -> do
251- let (prev, new) = HashMap. alterF (, Just v) f dict
252- pure (new, (prev, new))
253- when (prev /= Just v) $ do
254- join $ atomically $ Shake. recordDirtyKeys (shakeExtras state) IsFileOfInterest [f]
255- log' Debug $ LogFOI files
256- where
257- log' = logWith recorder
250+ OfInterestCabalVar var <- Shake. getIdeGlobalState state
251+ (prev, files) <- modifyVar var $ \ dict -> do
252+ let (prev, new) = HashMap. alterF (,Just v) f dict
253+ pure (new, (prev, new))
254+ when (prev /= Just v) $ do
255+ join $ atomically $ Shake. recordDirtyKeys (shakeExtras state) IsFileOfInterest [f]
256+ log' Debug $ LogFOI files
257+ where
258+ log' = logWith recorder
258259
259260deleteFileOfInterest :: Recorder (WithPriority Log ) -> IdeState -> NormalizedFilePath -> IO ()
260261deleteFileOfInterest recorder state f = do
261- OfInterestCabalVar var <- Shake. getIdeGlobalState state
262- files <- modifyVar' var $ HashMap. delete f
263- join $ atomically $ Shake. recordDirtyKeys (shakeExtras state) IsFileOfInterest [f]
264- log' Debug $ LogFOI files
265- where
266- log' = logWith recorder
262+ OfInterestCabalVar var <- Shake. getIdeGlobalState state
263+ files <- modifyVar' var $ HashMap. delete f
264+ join $ atomically $ Shake. recordDirtyKeys (shakeExtras state) IsFileOfInterest [f]
265+ log' Debug $ LogFOI files
266+ where
267+ log' = logWith recorder
267268
268269-- ----------------------------------------------------------------
269270-- Completion
@@ -279,22 +280,17 @@ completion _ide _ complParams = do
279280 pref <- VFS. getCompletionPrefix position cnts
280281 liftIO $ result pref path cnts
281282 _ -> return $ J. List []
282- where
283- result :: Maybe VFS. PosPrefixInfo -> FilePath -> VirtualFile -> IO (J. List CompletionItem )
284- result Nothing _ _ = pure $ J. List []
285- result (Just pfix) fp cnts
286- | Just ctx <- context = do
283+ where
284+ result :: Maybe VFS. PosPrefixInfo -> FilePath -> VirtualFile -> IO (J. List CompletionItem )
285+ result Nothing _ _ = pure $ J. List []
286+ result (Just prefix) _fp cnts
287+ | Just ctx <- context = do
287288 let completer = contextToCompleter " " ctx
288- completions <- completer filePathPfix
289+ completions <- completer completionContext
289290 -- genPkgDesc <- readGenericPackageDescription silent fp
290- pure $ J. List $ makeCompletionItems editRange completions
291- | otherwise = pure $ J. List []
292- where
293- (Position linePos charPos) = VFS. cursorPos pfix
294- context = getContext (Position linePos charPos) (Rope. lines $ cnts ^. VFS. file_text)
295- filePathPfix = getFilePathCursorPrefix pfix
296- editRange =
297- Range
298- (Position linePos (fromIntegral charPos - fromIntegral (T. length filePathPfix)))
299- (Position linePos charPos)
300-
291+ pure $ J. List $ makeCompletionItems completions
292+ | otherwise = pure $ J. List []
293+ where
294+ (Position linePos charPos) = VFS. cursorPos prefix
295+ context = getContext (Position linePos charPos) (Rope. lines $ cnts ^. VFS. file_text)
296+ completionContext = getFilePathCompletionContext prefix
0 commit comments