|
| 1 | +{-# LANGUAGE DataKinds #-} |
| 2 | +{-# LANGUAGE DuplicateRecordFields #-} |
| 3 | +{-# LANGUAGE LambdaCase #-} |
| 4 | +{-# LANGUAGE OverloadedStrings #-} |
| 5 | +{-# LANGUAGE TypeFamilies #-} |
| 6 | + |
| 7 | +module Ide.Plugin.CabalProject where |
| 8 | + |
| 9 | +import Control.Concurrent.Strict |
| 10 | +import Control.DeepSeq |
| 11 | +import Control.Lens ((^.)) |
| 12 | +import Control.Monad.Extra |
| 13 | +import Control.Monad.IO.Class |
| 14 | +import Control.Monad.Trans.Class (lift) |
| 15 | +import Control.Monad.Trans.Maybe (runMaybeT) |
| 16 | +import qualified Data.ByteString as BS |
| 17 | +import Data.Hashable |
| 18 | +import Data.HashMap.Strict (HashMap) |
| 19 | +import qualified Data.HashMap.Strict as HashMap |
| 20 | +import qualified Data.List as List |
| 21 | +import qualified Data.List.NonEmpty as NE |
| 22 | +import qualified Data.Maybe as Maybe |
| 23 | +import Data.Proxy |
| 24 | +import qualified Data.Text () |
| 25 | +import qualified Data.Text as T |
| 26 | +import qualified Data.Text.Encoding as Encoding |
| 27 | +import Data.Text.Utf16.Rope.Mixed as Rope |
| 28 | +import Development.IDE as D |
| 29 | +import Development.IDE.Core.FileStore (getVersionedTextDoc) |
| 30 | +import Development.IDE.Core.PluginUtils |
| 31 | +import Development.IDE.Core.Shake (restartShakeSession) |
| 32 | +import qualified Development.IDE.Core.Shake as Shake |
| 33 | +import Development.IDE.Graph (Key, |
| 34 | + alwaysRerun) |
| 35 | +import Development.IDE.LSP.HoverDefinition (foundHover) |
| 36 | +import qualified Development.IDE.Plugin.Completions.Logic as Ghcide |
| 37 | +import Development.IDE.Types.Shake (toKey) |
| 38 | +import qualified Distribution.CabalSpecVersion as Cabal |
| 39 | +import qualified Distribution.Fields as Syntax |
| 40 | +import Distribution.Package (Dependency) |
| 41 | +import Distribution.PackageDescription (allBuildDepends, |
| 42 | + depPkgName, |
| 43 | + unPackageName) |
| 44 | +import Distribution.PackageDescription.Configuration (flattenPackageDescription) |
| 45 | +import Distribution.Parsec.Error |
| 46 | +import qualified Distribution.Parsec.Position as Syntax |
| 47 | +import GHC.Generics |
| 48 | +import Ide.Plugin.Error |
| 49 | +import Ide.Types |
| 50 | +import qualified Language.LSP.Protocol.Lens as JL |
| 51 | +import qualified Language.LSP.Protocol.Message as LSP |
| 52 | +import Language.LSP.Protocol.Types |
| 53 | +import qualified Language.LSP.VFS as VFS |
| 54 | +import Text.Regex.TDFA |
| 55 | + |
| 56 | +data Log |
| 57 | + = LogModificationTime NormalizedFilePath FileVersion |
| 58 | + | LogShake Shake.Log |
| 59 | + | LogDocOpened Uri |
| 60 | + | LogDocModified Uri |
| 61 | + | LogDocSaved Uri |
| 62 | + | LogDocClosed Uri |
| 63 | + | LogFOI (HashMap NormalizedFilePath FileOfInterestStatus) |
| 64 | + deriving (Show) |
| 65 | + |
| 66 | +instance Pretty Log where |
| 67 | + pretty = \case |
| 68 | + LogShake log' -> pretty log' |
| 69 | + LogModificationTime nfp modTime -> |
| 70 | + "Modified:" <+> pretty (fromNormalizedFilePath nfp) <+> pretty (show modTime) |
| 71 | + LogDocOpened uri -> |
| 72 | + "Opened text document:" <+> pretty (getUri uri) |
| 73 | + LogDocModified uri -> |
| 74 | + "Modified text document:" <+> pretty (getUri uri) |
| 75 | + LogDocSaved uri -> |
| 76 | + "Saved text document:" <+> pretty (getUri uri) |
| 77 | + LogDocClosed uri -> |
| 78 | + "Closed text document:" <+> pretty (getUri uri) |
| 79 | + LogFOI files -> |
| 80 | + "Set files of interest to:" <+> viaShow files |
| 81 | + |
| 82 | +descriptor :: Recorder (WithPriority Log) -> PluginId -> PluginDescriptor IdeState |
| 83 | +descriptor recorder plId = |
| 84 | + (defaultCabalProjectPluginDescriptor plId "Provides a variety of IDE features in cabal files") |
| 85 | + { pluginRules = cabalRules recorder plId |
| 86 | + , pluginHandlers = |
| 87 | + mconcat |
| 88 | + [] |
| 89 | + , pluginNotificationHandlers = |
| 90 | + mconcat |
| 91 | + [ mkPluginNotificationHandler LSP.SMethod_TextDocumentDidOpen $ |
| 92 | + \ide vfs _ (DidOpenTextDocumentParams TextDocumentItem{_uri, _version}) -> liftIO $ do |
| 93 | + whenUriFile _uri $ \file -> do |
| 94 | + log' Debug $ LogDocOpened _uri |
| 95 | + restartCabalShakeSession (shakeExtras ide) vfs file "(opened)" $ |
| 96 | + addFileOfInterest recorder ide file Modified{firstOpen = True} |
| 97 | + , mkPluginNotificationHandler LSP.SMethod_TextDocumentDidChange $ |
| 98 | + \ide vfs _ (DidChangeTextDocumentParams VersionedTextDocumentIdentifier{_uri} _) -> liftIO $ do |
| 99 | + whenUriFile _uri $ \file -> do |
| 100 | + log' Debug $ LogDocModified _uri |
| 101 | + restartCabalShakeSession (shakeExtras ide) vfs file "(changed)" $ |
| 102 | + addFileOfInterest recorder ide file Modified{firstOpen = False} |
| 103 | + , mkPluginNotificationHandler LSP.SMethod_TextDocumentDidSave $ |
| 104 | + \ide vfs _ (DidSaveTextDocumentParams TextDocumentIdentifier{_uri} _) -> liftIO $ do |
| 105 | + whenUriFile _uri $ \file -> do |
| 106 | + log' Debug $ LogDocSaved _uri |
| 107 | + restartCabalShakeSession (shakeExtras ide) vfs file "(saved)" $ |
| 108 | + addFileOfInterest recorder ide file OnDisk |
| 109 | + , mkPluginNotificationHandler LSP.SMethod_TextDocumentDidClose $ |
| 110 | + \ide vfs _ (DidCloseTextDocumentParams TextDocumentIdentifier{_uri}) -> liftIO $ do |
| 111 | + whenUriFile _uri $ \file -> do |
| 112 | + log' Debug $ LogDocClosed _uri |
| 113 | + restartCabalShakeSession (shakeExtras ide) vfs file "(closed)" $ |
| 114 | + deleteFileOfInterest recorder ide file |
| 115 | + ] |
| 116 | + , pluginConfigDescriptor = defaultConfigDescriptor |
| 117 | + { configHasDiagnostics = True |
| 118 | + } |
| 119 | + } |
| 120 | + where |
| 121 | + log' = logWith recorder |
| 122 | + |
| 123 | + whenUriFile :: Uri -> (NormalizedFilePath -> IO ()) -> IO () |
| 124 | + whenUriFile uri act = whenJust (uriToFilePath uri) $ act . toNormalizedFilePath' |
| 125 | + |
| 126 | +cabalRules :: Recorder (WithPriority Log) -> PluginId -> Rules () |
| 127 | +cabalRules recorder _ = do |
| 128 | + ofInterestRules recorder |
| 129 | + |
| 130 | +{- | Helper function to restart the shake session, specifically for modifying .cabal files. |
| 131 | +No special logic, just group up a bunch of functions you need for the base |
| 132 | +Notification Handlers. |
| 133 | +
|
| 134 | +To make sure diagnostics are up to date, we need to tell shake that the file was touched and |
| 135 | +needs to be re-parsed. That's what we do when we record the dirty key that our parsing |
| 136 | +rule depends on. |
| 137 | +Then we restart the shake session, so that changes to our virtual files are actually picked up. |
| 138 | +-} |
| 139 | +restartCabalShakeSession :: ShakeExtras -> VFS.VFS -> NormalizedFilePath -> String -> IO [Key] -> IO () |
| 140 | +restartCabalShakeSession shakeExtras vfs file actionMsg actionBetweenSession = do |
| 141 | + restartShakeSession shakeExtras (VFSModified vfs) (fromNormalizedFilePath file ++ " " ++ actionMsg) [] $ do |
| 142 | + keys <- actionBetweenSession |
| 143 | + return (toKey GetModificationTime file:keys) |
| 144 | + |
| 145 | +-- ---------------------------------------------------------------- |
| 146 | +-- Cabal file of Interest rules and global variable |
| 147 | +-- ---------------------------------------------------------------- |
| 148 | + |
| 149 | +{- | Cabal files that are currently open in the lsp-client. |
| 150 | +Specific actions happen when these files are saved, closed or modified, |
| 151 | +such as generating diagnostics, re-parsing, etc... |
| 152 | +
|
| 153 | +We need to store the open files to parse them again if we restart the shake session. |
| 154 | +Restarting of the shake session happens whenever these files are modified. |
| 155 | +-} |
| 156 | +newtype OfInterestCabalVar = OfInterestCabalVar (Var (HashMap NormalizedFilePath FileOfInterestStatus)) |
| 157 | + |
| 158 | +instance Shake.IsIdeGlobal OfInterestCabalVar |
| 159 | + |
| 160 | +data IsCabalFileOfInterest = IsCabalFileOfInterest |
| 161 | + deriving (Eq, Show, Generic) |
| 162 | +instance Hashable IsCabalFileOfInterest |
| 163 | +instance NFData IsCabalFileOfInterest |
| 164 | + |
| 165 | +type instance RuleResult IsCabalFileOfInterest = CabalFileOfInterestResult |
| 166 | + |
| 167 | +data CabalFileOfInterestResult = NotCabalFOI | IsCabalFOI FileOfInterestStatus |
| 168 | + deriving (Eq, Show, Generic) |
| 169 | +instance Hashable CabalFileOfInterestResult |
| 170 | +instance NFData CabalFileOfInterestResult |
| 171 | + |
| 172 | +{- | The rule that initialises the files of interest state. |
| 173 | +
|
| 174 | +Needs to be run on start-up. |
| 175 | +-} |
| 176 | +ofInterestRules :: Recorder (WithPriority Log) -> Rules () |
| 177 | +ofInterestRules recorder = do |
| 178 | + Shake.addIdeGlobal . OfInterestCabalVar =<< liftIO (newVar HashMap.empty) |
| 179 | + Shake.defineEarlyCutoff (cmapWithPrio LogShake recorder) $ RuleNoDiagnostics $ \IsCabalFileOfInterest f -> do |
| 180 | + alwaysRerun |
| 181 | + filesOfInterest <- getCabalFilesOfInterestUntracked |
| 182 | + let foi = maybe NotCabalFOI IsCabalFOI $ f `HashMap.lookup` filesOfInterest |
| 183 | + fp = summarize foi |
| 184 | + res = (Just fp, Just foi) |
| 185 | + return res |
| 186 | + where |
| 187 | + summarize NotCabalFOI = BS.singleton 0 |
| 188 | + summarize (IsCabalFOI OnDisk) = BS.singleton 1 |
| 189 | + summarize (IsCabalFOI (Modified False)) = BS.singleton 2 |
| 190 | + summarize (IsCabalFOI (Modified True)) = BS.singleton 3 |
| 191 | + |
| 192 | +getCabalFilesOfInterestUntracked :: Action (HashMap NormalizedFilePath FileOfInterestStatus) |
| 193 | +getCabalFilesOfInterestUntracked = do |
| 194 | + OfInterestCabalVar var <- Shake.getIdeGlobalAction |
| 195 | + liftIO $ readVar var |
| 196 | + |
| 197 | +addFileOfInterest :: Recorder (WithPriority Log) -> IdeState -> NormalizedFilePath -> FileOfInterestStatus -> IO [Key] |
| 198 | +addFileOfInterest recorder state f v = do |
| 199 | + OfInterestCabalVar var <- Shake.getIdeGlobalState state |
| 200 | + (prev, files) <- modifyVar var $ \dict -> do |
| 201 | + let (prev, new) = HashMap.alterF (,Just v) f dict |
| 202 | + pure (new, (prev, new)) |
| 203 | + if prev /= Just v |
| 204 | + then do |
| 205 | + log' Debug $ LogFOI files |
| 206 | + return [toKey IsCabalFileOfInterest f] |
| 207 | + else return [] |
| 208 | + where |
| 209 | + log' = logWith recorder |
| 210 | + |
| 211 | +deleteFileOfInterest :: Recorder (WithPriority Log) -> IdeState -> NormalizedFilePath -> IO [Key] |
| 212 | +deleteFileOfInterest recorder state f = do |
| 213 | + OfInterestCabalVar var <- Shake.getIdeGlobalState state |
| 214 | + files <- modifyVar' var $ HashMap.delete f |
| 215 | + log' Debug $ LogFOI files |
| 216 | + return [toKey IsFileOfInterest f] |
| 217 | + where |
| 218 | + log' = logWith recorder |
0 commit comments