|
| 1 | +{-# LANGUAGE DeriveAnyClass #-} |
| 2 | +{-# LANGUAGE DeriveGeneric #-} |
| 3 | +{-# LANGUAGE DuplicateRecordFields #-} |
| 4 | +{-# LANGUAGE LambdaCase #-} |
| 5 | +{-# LANGUAGE NamedFieldPuns #-} |
| 6 | +{-# LANGUAGE OverloadedStrings #-} |
| 7 | +{-# LANGUAGE RecordWildCards #-} |
| 8 | +{-# LANGUAGE ScopedTypeVariables #-} |
| 9 | +{-# LANGUAGE TupleSections #-} |
| 10 | + |
| 11 | +-- | A plugin inspired by the REPLoid feature of Dante[1] which allows |
| 12 | +-- to evaluate code in comment prompts and splice the results right below: |
| 13 | +-- |
| 14 | +-- > example :: [String] |
| 15 | +-- > example = ["This is an example", "of", "interactive", "evaluation"] |
| 16 | +-- > |
| 17 | +-- > -- >>> intercalate " " example |
| 18 | +-- > -- "This is an example of interactive evaluation" |
| 19 | +-- > -- |
| 20 | +-- |
| 21 | +-- [1] - https://github.com/jyp/dante |
| 22 | +module Ide.Plugin.Eval where |
| 23 | + |
| 24 | +import Control.Monad (void) |
| 25 | +import Control.Monad.Catch (finally) |
| 26 | +import Control.Monad.IO.Class (MonadIO (liftIO)) |
| 27 | +import Control.Monad.Trans.Class (MonadTrans (lift)) |
| 28 | +import Control.Monad.Trans.Except (ExceptT (..), runExceptT, |
| 29 | + throwE) |
| 30 | +import Data.Aeson (FromJSON, ToJSON, Value (Null), |
| 31 | + toJSON) |
| 32 | +import Data.Bifunctor (Bifunctor (first)) |
| 33 | +import qualified Data.HashMap.Strict as Map |
| 34 | +import qualified Data.Rope.UTF16 as Rope |
| 35 | +import Data.String (IsString (fromString)) |
| 36 | +import Data.Text (Text) |
| 37 | +import qualified Data.Text as T |
| 38 | +import Data.Time (getCurrentTime) |
| 39 | +import Development.IDE.Core.Rules (runAction) |
| 40 | +import Development.IDE.Core.RuleTypes (GetModSummary (..), |
| 41 | + GhcSessionDeps (..)) |
| 42 | +import Development.IDE.Core.Shake (use_) |
| 43 | +import Development.IDE.GHC.Util (evalGhcEnv, hscEnv, |
| 44 | + textToStringBuffer) |
| 45 | +import Development.IDE.Types.Location (toNormalizedFilePath', |
| 46 | + uriToFilePath') |
| 47 | +import DynamicLoading (initializePlugins) |
| 48 | +import GHC |
| 49 | +import GHC.Generics (Generic) |
| 50 | +import GhcMonad (modifySession) |
| 51 | +import GhcPlugins (defaultLogActionHPutStrDoc, |
| 52 | + gopt_set, gopt_unset, |
| 53 | + interpWays, updateWays, |
| 54 | + wayGeneralFlags, |
| 55 | + wayUnsetGeneralFlags) |
| 56 | +import HscTypes |
| 57 | +import Ide.Plugin |
| 58 | +import Ide.Types |
| 59 | +import Language.Haskell.LSP.Core (LspFuncs (getVirtualFileFunc)) |
| 60 | +import Language.Haskell.LSP.Types |
| 61 | +import Language.Haskell.LSP.VFS (VirtualFile (..)) |
| 62 | +import PrelNames (pRELUDE) |
| 63 | +import System.IO (IOMode (WriteMode), hClose, openFile) |
| 64 | +import System.IO.Extra (newTempFile) |
| 65 | + |
| 66 | +descriptor :: PluginId -> PluginDescriptor |
| 67 | +descriptor plId = |
| 68 | + (defaultPluginDescriptor plId) |
| 69 | + { pluginId = plId, |
| 70 | + pluginCodeLensProvider = Just provider, |
| 71 | + pluginCommands = [evalCommand] |
| 72 | + } |
| 73 | + |
| 74 | +extractMatches :: Maybe Text -> [([(Text, Int)], Range)] |
| 75 | +extractMatches = goSearch 0 . maybe [] T.lines |
| 76 | + where |
| 77 | + checkMatch = T.stripPrefix "-- >>> " |
| 78 | + looksLikeSplice l |
| 79 | + | Just l' <- T.stripPrefix "--" l |
| 80 | + = not (" >>>" `T.isPrefixOf` l') |
| 81 | + | otherwise |
| 82 | + = False |
| 83 | + |
| 84 | + goSearch _ [] = [] |
| 85 | + goSearch line (l : ll) |
| 86 | + | Just match <- checkMatch l = |
| 87 | + goAcc (line + 1) [(match, line)] ll |
| 88 | + | otherwise = |
| 89 | + goSearch (line + 1) ll |
| 90 | + |
| 91 | + goAcc line acc [] = [(reverse acc,Range p p)] where p = Position line 0 |
| 92 | + goAcc line acc (l:ll) |
| 93 | + | Just match <- checkMatch l = |
| 94 | + goAcc (line + 1) ([(match, line)] <> acc) ll |
| 95 | + | otherwise = |
| 96 | + (reverse acc,r) : goSearch (line + 1) ll |
| 97 | + where |
| 98 | + r = Range p p' |
| 99 | + p = Position line 0 |
| 100 | + p' = Position (line + spliceLength) 0 |
| 101 | + spliceLength = length (takeWhile looksLikeSplice (l:ll)) |
| 102 | + |
| 103 | +provider :: CodeLensProvider |
| 104 | +provider lsp _state plId CodeLensParams {_textDocument} = response $ do |
| 105 | + let TextDocumentIdentifier uri = _textDocument |
| 106 | + contents <- liftIO $ getVirtualFileFunc lsp $ toNormalizedUri uri |
| 107 | + let text = Rope.toText . (_text :: VirtualFile -> Rope.Rope) <$> contents |
| 108 | + let matches = extractMatches text |
| 109 | + |
| 110 | + cmd <- liftIO $ mkLspCommand plId evalCommandName "Evaluate..." (Just []) |
| 111 | + |
| 112 | + let lenses = |
| 113 | + [ CodeLens range (Just cmd') Nothing |
| 114 | + | (m, r) <- matches, |
| 115 | + let (_, startLine) = head m |
| 116 | + (_, endLine) = last m |
| 117 | + range = Range start end |
| 118 | + start = Position startLine 0 |
| 119 | + end = Position endLine 1000 |
| 120 | + args = EvalParams m r _textDocument, |
| 121 | + let cmd' = (cmd :: Command) |
| 122 | + {_arguments = Just (List [toJSON args]) |
| 123 | + ,_title = if trivial r then "Evaluate..." else "Refresh..." |
| 124 | + } |
| 125 | + ] |
| 126 | + |
| 127 | + return $ List lenses |
| 128 | + where |
| 129 | + trivial (Range p p') = p == p' |
| 130 | + |
| 131 | +evalCommandName :: CommandId |
| 132 | +evalCommandName = "evalCommand" |
| 133 | + |
| 134 | +evalCommand :: PluginCommand |
| 135 | +evalCommand = |
| 136 | + PluginCommand evalCommandName "evaluate" runEvalCmd |
| 137 | + |
| 138 | +data EvalParams = EvalParams |
| 139 | + { statements :: [(Text, Int)], |
| 140 | + editTarget :: !Range, |
| 141 | + module_ :: !TextDocumentIdentifier |
| 142 | + } |
| 143 | + deriving (Eq, Show, Generic, FromJSON, ToJSON) |
| 144 | + |
| 145 | +runEvalCmd :: CommandFunction EvalParams |
| 146 | +runEvalCmd lsp state EvalParams {..} = response' $ do |
| 147 | + let TextDocumentIdentifier {_uri} = module_ |
| 148 | + fp <- handleMaybe "uri" $ uriToFilePath' _uri |
| 149 | + contents <- liftIO $ getVirtualFileFunc lsp $ toNormalizedUri _uri |
| 150 | + text <- handleMaybe "contents" $ Rope.toText . (_text :: VirtualFile -> Rope.Rope) <$> contents |
| 151 | + |
| 152 | + session <- |
| 153 | + liftIO |
| 154 | + $ runAction "runEvalCmd.ghcSession" state |
| 155 | + $ use_ GhcSessionDeps |
| 156 | + $ toNormalizedFilePath' |
| 157 | + $ fp |
| 158 | + |
| 159 | + ms <- |
| 160 | + liftIO |
| 161 | + $ runAction "runEvalCmd.getModSummary" state |
| 162 | + $ use_ GetModSummary |
| 163 | + $ toNormalizedFilePath' |
| 164 | + $ fp |
| 165 | + |
| 166 | + now <- liftIO getCurrentTime |
| 167 | + |
| 168 | + (temp, clean) <- liftIO newTempFile |
| 169 | + (tempLog, cleanLog) <- liftIO newTempFile |
| 170 | + hLog <- liftIO $ openFile tempLog WriteMode |
| 171 | + flip finally (liftIO $ hClose hLog >> cleanLog >> clean) $ do |
| 172 | + let modName = moduleName $ ms_mod ms |
| 173 | + thisModuleTarget = Target (TargetFile fp Nothing) False (Just (textToStringBuffer text, now)) |
| 174 | + |
| 175 | + hscEnv' <- ExceptT $ evalGhcEnv (hscEnv session) $ do |
| 176 | + df <- getSessionDynFlags |
| 177 | + env <- getSession |
| 178 | + df <- liftIO $ setupDynFlagsForGHCiLike env df |
| 179 | + _lp <- setSessionDynFlags df |
| 180 | + |
| 181 | + -- copy the package state to the interactive DynFlags |
| 182 | + idflags <- getInteractiveDynFlags |
| 183 | + df <- getSessionDynFlags |
| 184 | + setInteractiveDynFlags |
| 185 | + idflags |
| 186 | + { pkgState = pkgState df, |
| 187 | + pkgDatabase = pkgDatabase df, |
| 188 | + packageFlags = packageFlags df |
| 189 | + } |
| 190 | + |
| 191 | + -- set up a custom log action |
| 192 | + setLogAction $ \_df _wr _sev _span _style _doc -> |
| 193 | + defaultLogActionHPutStrDoc _df hLog _doc _style |
| 194 | + |
| 195 | + -- load the module in the interactive environment |
| 196 | + setTargets [thisModuleTarget] |
| 197 | + loadResult <- load LoadAllTargets |
| 198 | + case loadResult of |
| 199 | + Failed -> liftIO $ do |
| 200 | + hClose hLog |
| 201 | + Left <$> readFile tempLog |
| 202 | + Succeeded -> do |
| 203 | + setContext [IIDecl (simpleImportDecl $ moduleName pRELUDE), IIModule modName] |
| 204 | + Right <$> getSession |
| 205 | + |
| 206 | + df <- liftIO $ evalGhcEnv hscEnv' getSessionDynFlags |
| 207 | + let eval (stmt, l) |
| 208 | + | isStmt df stmt = do |
| 209 | + |
| 210 | + -- set up a custom interactive print function |
| 211 | + ctxt <- getContext |
| 212 | + setContext [IIDecl (simpleImportDecl $ moduleName pRELUDE)] |
| 213 | + let printFun = "let ghcideCustomShow x = Prelude.writeFile " <> show temp <> " (Prelude.show x)" |
| 214 | + interactivePrint <- execStmt printFun execOptions >>= \case |
| 215 | + ExecComplete (Right [interactivePrint]) _ -> pure interactivePrint |
| 216 | + _ -> error "internal error binding print function" |
| 217 | + modifySession $ \hsc -> hsc {hsc_IC = setInteractivePrintName (hsc_IC hsc) interactivePrint} |
| 218 | + setContext ctxt |
| 219 | + |
| 220 | + let opts = |
| 221 | + execOptions |
| 222 | + { execSourceFile = fp, |
| 223 | + execLineNumber = l |
| 224 | + } |
| 225 | + res <- execStmt stmt opts |
| 226 | + str <- case res of |
| 227 | + ExecComplete (Left err) _ -> pure $ pad $ show err |
| 228 | + ExecComplete (Right _) _ -> liftIO $ pad <$> readFile temp |
| 229 | + ExecBreak {} -> pure $ pad "breakpoints are not supported" |
| 230 | + |
| 231 | + let changes = [TextEdit editTarget $ T.pack str] |
| 232 | + return changes |
| 233 | + |
| 234 | + | isImport df stmt = do |
| 235 | + ctxt <- getContext |
| 236 | + idecl <- parseImportDecl stmt |
| 237 | + setContext $ IIDecl idecl : ctxt |
| 238 | + return [] |
| 239 | + |
| 240 | + | otherwise = do |
| 241 | + void $ runDecls stmt |
| 242 | + return [] |
| 243 | + |
| 244 | + edits <- liftIO $ evalGhcEnv hscEnv' $ traverse (eval . first T.unpack) statements |
| 245 | + |
| 246 | + let workspaceEditsMap = Map.fromList [(_uri, List $ concat edits)] |
| 247 | + let workspaceEdits = WorkspaceEdit (Just workspaceEditsMap) Nothing |
| 248 | + |
| 249 | + return (WorkspaceApplyEdit, ApplyWorkspaceEditParams workspaceEdits) |
| 250 | + |
| 251 | +pad :: String -> String |
| 252 | +pad = unlines . map ("-- " <>) . lines |
| 253 | + |
| 254 | +------------------------------------------------------------------------------- |
| 255 | + |
| 256 | +handleMaybe :: Monad m => e -> Maybe b -> ExceptT e m b |
| 257 | +handleMaybe msg = maybe (throwE msg) return |
| 258 | + |
| 259 | +handleMaybeM :: Monad m => e -> m (Maybe b) -> ExceptT e m b |
| 260 | +handleMaybeM msg act = maybe (throwE msg) return =<< lift act |
| 261 | + |
| 262 | +response :: ExceptT String IO a -> IO (Either ResponseError a) |
| 263 | +response = |
| 264 | + fmap (first (\msg -> ResponseError InternalError (fromString msg) Nothing)) |
| 265 | + . runExceptT |
| 266 | + |
| 267 | +response' :: ExceptT String IO a -> IO (Either ResponseError Value, Maybe a) |
| 268 | +response' act = do |
| 269 | + res <- runExceptT act |
| 270 | + case res of |
| 271 | + Left e -> |
| 272 | + return (Left (ResponseError InternalError (fromString e) Nothing), Nothing) |
| 273 | + Right a -> return (Right Null, Just a) |
| 274 | + |
| 275 | +setupDynFlagsForGHCiLike :: HscEnv -> DynFlags -> IO DynFlags |
| 276 | +setupDynFlagsForGHCiLike env dflags = do |
| 277 | + let dflags3 = |
| 278 | + dflags |
| 279 | + { hscTarget = HscInterpreted, |
| 280 | + ghcMode = CompManager, |
| 281 | + ghcLink = LinkInMemory |
| 282 | + } |
| 283 | + platform = targetPlatform dflags3 |
| 284 | + dflags3a = updateWays $ dflags3 {ways = interpWays} |
| 285 | + dflags3b = |
| 286 | + foldl gopt_set dflags3a $ |
| 287 | + concatMap |
| 288 | + (wayGeneralFlags platform) |
| 289 | + interpWays |
| 290 | + dflags3c = |
| 291 | + foldl gopt_unset dflags3b $ |
| 292 | + concatMap |
| 293 | + (wayUnsetGeneralFlags platform) |
| 294 | + interpWays |
| 295 | + dflags4 = |
| 296 | + dflags3c `gopt_set` Opt_ImplicitImportQualified |
| 297 | + `gopt_set` Opt_IgnoreOptimChanges |
| 298 | + `gopt_set` Opt_IgnoreHpcChanges |
| 299 | + initializePlugins env dflags4 |
0 commit comments