{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE NamedFieldPuns #-} -- | Integration with Liquid Haskell with diagnostics and hover information module Haskell.Ide.Engine.Plugin.Liquid where import Control.Concurrent.Async.Lifted import Control.Monad import Control.Monad.IO.Class import Control.Monad.Trans.Class import Control.Exception (bracket) import Data.Aeson import qualified Data.ByteString.Lazy as BS import qualified Data.Map as Map import qualified Data.Set as S import qualified Data.Text as T import qualified Data.Text.IO as T import GHC.Generics import Haskell.Ide.Engine.MonadFunctions import Haskell.Ide.Engine.MonadTypes hiding (_range) import Haskell.Ide.Engine.PluginUtils import qualified Language.Haskell.LSP.Types as J import System.Directory import System.Environment import System.Exit import System.FilePath import System.Process import Text.Parsec import Text.Parsec.Text -- --------------------------------------------------------------------- liquidDescriptor :: PluginId -> PluginDescriptor liquidDescriptor plId = PluginDescriptor { pluginId = plId , pluginName = "Liquid Haskell" , pluginDesc = "Integration with Liquid Haskell" , pluginCommands = [] , pluginCodeActionProvider = Nothing , pluginDiagnosticProvider = Just (DiagnosticProvider (S.singleton DiagnosticOnSave) (DiagnosticProviderAsync diagnosticProvider)) , pluginHoverProvider = Just hoverProvider , pluginSymbolProvider = Nothing , pluginFormattingProvider = Nothing } -- --------------------------------------------------------------------- data LiquidJson = LJ { status :: T.Text , types :: Value , errors :: [LiquidError] } deriving (Show,Generic) data LiquidPos = LP { line :: Int , column :: Int } deriving (Show,Generic,Eq) data LiquidError = LE { start :: LiquidPos , stop :: LiquidPos , message :: T.Text } deriving (Show,Generic,Eq) instance FromJSON LiquidJson instance ToJSON LiquidJson instance FromJSON LiquidPos instance ToJSON LiquidPos instance FromJSON LiquidError instance ToJSON LiquidError -- --------------------------------------------------------------------- newtype LiquidData = LiquidData { tid :: Maybe (Async ()) } instance ExtensionClass LiquidData where initialValue = LiquidData Nothing -- --------------------------------------------------------------------- -- diagnosticProvider :: DiagnosticTrigger -> Uri -> IdeM (IdeResult (Map.Map Uri (S.Set Diagnostic))) diagnosticProvider :: DiagnosticProviderFuncAsync diagnosticProvider DiagnosticOnSave uri cb = pluginGetFile "Liquid.diagnosticProvider:" uri $ \file -> withCachedModuleAndData file (IdeResultOk ()) $ \_tm _info () -> do -- New save, kill any prior instance that was running LiquidData mtid <- get mapM_ (liftIO . cancel) mtid let progTitle = "Running Liquid Haskell on " <> T.pack (takeFileName file) tid <- lift $ async $ withIndefiniteProgress progTitle NotCancellable $ liftIO $ generateDiagnosics cb uri file put (LiquidData (Just tid)) return $ IdeResultOk () diagnosticProvider _ _ _ = return (IdeResultOk ()) -- --------------------------------------------------------------------- generateDiagnosics :: (Map.Map Uri (S.Set Diagnostic) -> IO ()) -> Uri -> FilePath -> IO () generateDiagnosics cb uri file = do r <- runLiquidHaskell file case r of Nothing -> do -- TODO: return an LSP warning logm "Liquid.generateDiagnostics:no liquid exe found" return () Just (_ec,_) -> do me <- liftIO $ readJsonAnnot uri case me of Nothing -> do logm "Liquid.generateDiagnostics:no liquid results parsed" return () Just es -> do logm "Liquid.generateDiagnostics:liquid results parsed" cb m where m = Map.fromList [(uri,S.fromList (map liquidErrorToDiagnostic es))] return () -- --------------------------------------------------------------------- -- | Find and run the liquid haskell executable runLiquidHaskell :: FilePath -> IO (Maybe (ExitCode,[String])) runLiquidHaskell fp = do mexe <- findExecutable "liquid" case mexe of Nothing -> return Nothing Just lh -> do -- Putting quotes around the fp to help windows users with -- spaces in paths let cmd = lh ++ " --json \"" ++ fp ++ "\"" dir = takeDirectory fp cp = (shell cmd) { cwd = Just dir } -- logm $ "runLiquidHaskell:cmd=[" ++ cmd ++ "]" mpp <- lookupEnv "GHC_PACKAGE_PATH" mge <- lookupEnv "GHC_ENVIRONMENT" -- logm $ "runLiquidHaskell:mpp=[" ++ show mpp ++ "]" -- env <- getEnvironment -- logm $ "runLiquidHaskell:env=[" ++ show env ++ "]" (ec,o,e) <- bracket (do unsetEnv "GHC_ENVIRONMENT" unsetEnv "GHC_PACKAGE_PATH" ) (\_ -> do mapM_ (setEnv "GHC_PACKAGE_PATH") mpp mapM_ (setEnv "GHC_ENVIRONMENT" ) mge ) (\_ -> readCreateProcessWithExitCode cp "") -- logm $ "runLiquidHaskell:v=" ++ show (ec,o,e) return $ Just (ec,[o,e]) -- --------------------------------------------------------------------- liquidErrorToDiagnostic :: LiquidError -> Diagnostic liquidErrorToDiagnostic (LE f t msg) = Diagnostic { _range = Range (lpToPos f) (lpToPos t) , _severity = Just DsError , _code = Nothing , _source = Just "liquid" , _message = msg , _relatedInformation = Nothing } lpToPos :: LiquidPos -> Position lpToPos (LP r c) = Position (r - 1) (c - 1) -- --------------------------------------------------------------------- -- | Pull the errors out of the JSON annotation file, if it exists readJsonAnnot :: Uri -> IO (Maybe [LiquidError]) readJsonAnnot uri = do let fileName = jsonAnnotFile uri exists <- doesFileExist fileName if exists then do jf <- BS.readFile fileName case decode jf :: Maybe LiquidJson of Nothing -> return Nothing Just j -> return (Just (errors j)) else return Nothing -- | Pull the errors out of the JSON annotation file, if it exists readVimAnnot :: Uri -> IO (Maybe [LiquidError]) readVimAnnot uri = do let fileName = vimAnnotFile uri exists <- doesFileExist fileName if exists then do vf <- T.readFile fileName return $ Just (parseType vf) else return Nothing -- --------------------------------------------------------------------- -- | For a Uri representing -- "path/to/file/Foo.hs" return -- "path/to/file/.liquid/Foo.hs.json" jsonAnnotFile :: Uri -> FilePath jsonAnnotFile uri = liquidFileFor uri "json" -- | For a Uri representing -- "path/to/file/Foo.hs" return -- "path/to/file/.liquid/Foo.hs.vim.annot" vimAnnotFile :: Uri -> FilePath vimAnnotFile uri = liquidFileFor uri "vim.annot" -- | For a Uri representing -- "path/to/file/Foo.hs" return -- "path/to/file/.liquid/Foo.hs.EXT" liquidFileFor :: Uri -> String -> FilePath liquidFileFor uri ext = case uriToFilePath uri of Nothing -> error $ " Liquid.vimAnnotFile:bad uri:" ++ show uri Just fp -> r where d = takeDirectory fp f = takeFileName fp r = d ".liquid" f <.> ext -- --------------------------------------------------------------------- -- type HoverProvider = Uri -> Position -> IdeM (IdeResponse Hover) hoverProvider :: HoverProvider hoverProvider uri pos = pluginGetFile "Liquid.hoverProvider: " uri $ \file -> ifCachedModuleAndData file (IdeResultOk []) $ \_ info () -> do merrs <- liftIO $ readVimAnnot uri case merrs of Nothing -> return (IdeResultOk []) Just lerrs -> do let perrs = map (\le@(LE s e _) -> (lpToPos s,lpToPos e,le)) lerrs ls = getThingsAtPos info pos perrs hs <- forM ls $ \(r,LE _s _e msg) -> do let msgs = T.splitOn "\\n" msg msgm = J.markedUpContent "haskell" (T.unlines msgs) return $ J.Hover (J.HoverContents msgm) (Just r) return (IdeResultOk hs) -- --------------------------------------------------------------------- parseType :: T.Text -> [LiquidError] parseType str = case parse parseTypes "" str of -- Left _ -> [] Left err -> error $ show err Right les -> les -- --------------------------------------------------------------------- parseTypes :: Parser [LiquidError] parseTypes = parseTypeFromVim `sepBy` string "\n" -- | Parse a line of the form -- 6:1-6:10::Main.weAreEven :: "[{v : GHC.Types.Int | v mod 2 == 0}]" parseTypeFromVim :: Parser LiquidError parseTypeFromVim = do sr <- number _ <- char ':' sc <- number _ <- char '-' er <- number _ <- char ':' ec <- number _ <- string "::" _ <- manyTill anyChar (try (string "::")) _ <- string " \"" msg <- manyTill anyChar (try (string "\"")) return $ LE (LP sr sc) (LP er ec) (T.pack msg) number :: Parser Int number = do s <- many1 digit return (read s) -- ---------------------------------------------------------------------