{-# LANGUAGE TupleSections #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} module Haskell.Ide.Engine.Support.Hoogle where import Control.Monad.IO.Class import Control.Monad (join) import Control.Exception import Control.Applicative (liftA2) import Data.Aeson import Data.Bifunctor import Data.Maybe import qualified Data.Text as T import Data.List import Haskell.Ide.Engine.MonadTypes import Haskell.Ide.Engine.MonadFunctions import Hoogle import System.Directory import System.Environment import Text.HTML.TagSoup import Text.HTML.TagSoup.Tree -- --------------------------------------------------------------------- data HoogleError = NoDb | DbFail T.Text | NoResults deriving (Eq,Ord,Show) newtype HoogleDb = HoogleDb (Maybe FilePath) -- | Convert Hoogle Error's to Ide Error's. -- Can be used to present errors to the client. hoogleErrorToIdeError :: HoogleError -> IdeError hoogleErrorToIdeError NoResults = IdeError PluginError "No results found" Null hoogleErrorToIdeError NoDb = IdeError PluginError "Hoogle database not found. Run hoogle generate to generate" Null hoogleErrorToIdeError (DbFail msg) = IdeError PluginError ("Hoogle failed with following error: " <> msg) Null instance ExtensionClass HoogleDb where initialValue = HoogleDb Nothing -- | Initialise the Hoogle Database. -- Search for the Hoogle Database and set it in the global config if found. -- Looks first into custom hoogle database locations, then in the default location. -- Note, that the FilePath must be an absolute path, otherwise Hoogle can not -- find the database. -- -- If no hoogle database has been found, Nothing is returned -- and we will have no access to the hoogle database. -- However, it is still safe to use the hoogle API, -- e.g. either error or default values are returned. initializeHoogleDb :: IdeGhcM (Maybe FilePath) initializeHoogleDb = do explicitDbLocation <- liftIO $ lookupEnv "HIE_HOOGLE_DATABASE" db' <- maybe (liftIO defaultDatabaseLocation) pure explicitDbLocation db <- liftIO $ makeAbsolute db' exists <- liftIO $ doesFileExist db if exists then do put $ HoogleDb $ Just db return $ Just db else return Nothing info :: T.Text -> IdeM (Either HoogleError T.Text) info expr = do HoogleDb mdb <- get liftIO $ runHoogleQuery mdb expr $ \case [] -> Left NoResults h:_ -> return $ renderTargetInfo h renderTargetInfo :: Target -> T.Text renderTargetInfo t = T.intercalate "\n" $ ["```haskell\n" <> unHTML (T.pack $ targetItem t) <> "\n```"] ++ [renderDocs $ targetDocs t] ++ [T.pack $ curry annotate "More info" $ targetURL t] -- | Command to get the prettified documentation of an hoogle identifier. -- Identifier should be understandable for hoogle. -- If documentation can be found for it, the result will be rendered -- in markdown for the lsp-client. If multiple results have been found, -- only the first result will be shown. -- -- If no result can be found for the identifier, a hoogle error is returned -- that can be shown to the client by converting it -- to an IdeError with 'hoogleErrorToIdeError'. infoFancyRender :: T.Text -> IdeM (Either HoogleError T.Text) infoFancyRender expr = do HoogleDb mdb <- get liftIO $ runHoogleQuery mdb expr $ \case [] -> Left NoResults h:_ -> return $ renderTarget h -- | Render the target in valid markdown. -- Transform haddock documentation into markdown. renderTarget :: Target -> T.Text -- renderTarget t = T.intercalate "\n\n" $ renderTarget t = T.intercalate "\n" $ ["```haskell\n" <> unHTML (T.pack $ targetItem t) <> "```"] ++ [T.pack $ unwords mdl | not $ null mdl] ++ [renderDocs $ targetDocs t] ++ [T.pack $ curry annotate "More info" $ targetURL t] where mdl = map annotate $ catMaybes [targetPackage t, targetModule t] annotate :: (String, String) -> String annotate (thing,url) = "["<>thing<>"]"<>"("<>url<>")" -- | Hoogle results contain html like tags. -- We remove them with `tagsoup` here. -- So, if something hoogle related shows html tags, -- then maybe this function is responsible. unHTML :: T.Text -> T.Text unHTML = T.replace "<0>" "" . innerText . parseTags renderDocs :: String -> T.Text renderDocs = T.concat . map htmlToMarkDown . parseTree . T.pack htmlToMarkDown :: TagTree T.Text -> T.Text htmlToMarkDown (TagLeaf x) = fromMaybe "" $ maybeTagText x htmlToMarkDown (TagBranch "i" _ tree) = "*" <> T.concat (map htmlToMarkDown tree) <> "*" htmlToMarkDown (TagBranch "b" _ tree) = "**" <> T.concat (map htmlToMarkDown tree) <> "**" htmlToMarkDown (TagBranch "a" _ tree) = "`" <> T.concat (map htmlToMarkDown tree) <> "`" htmlToMarkDown (TagBranch "li" _ tree) = "- " <> T.concat (map htmlToMarkDown tree) htmlToMarkDown (TagBranch "tt" _ tree) = "`" <> innerText (flattenTree tree) <> "`" htmlToMarkDown (TagBranch "pre" _ tree) = "```haskell\n" <> T.concat (map htmlToMarkDown tree) <> "```" htmlToMarkDown (TagBranch _ _ tree) = T.concat $ map htmlToMarkDown tree ------------------------------------------------------------------------ -- | Search for modules that satisfy the given search text. -- Will return at most five, unique results. -- -- If an error occurs, such as no hoogle database has been found, -- or the search term has no match, an empty list will be returned. searchModules :: T.Text -> IdeM [T.Text] searchModules = fmap (map fst) . searchModules' -- | Just like 'searchModules', but includes the signature of the search term -- that has been found in the module. searchModules' :: T.Text -> IdeM [(T.Text, T.Text)] searchModules' = fmap (take 5 . nub) . searchTargets retrieveModuleAndSignature where retrieveModuleAndSignature :: Target -> Maybe (T.Text, T.Text) retrieveModuleAndSignature target = liftA2 (,) (packModuleName target) (packSymbolSignature target) packModuleName :: Target -> Maybe T.Text packModuleName = fmap (T.pack . fst) . targetModule packSymbolSignature :: Target -> Maybe T.Text packSymbolSignature = Just . unHTML . T.pack . targetItem -- | Search for packages that satisfy the given search text. -- Will return at most five, unique results. -- -- If an error occurs, such as no hoogle database has been found, -- or the search term has no match, an empty list will be returned. searchPackages :: T.Text -> IdeM [T.Text] searchPackages = fmap (take 5 . nub) . searchTargets (fmap (T.pack . fst) . targetPackage) -- | Search for Targets that fit to the given Text and satisfy the given predicate. -- Limits the amount of matches to at most ten. -- Applies the predicate to the first ten matches. May also return zero matches, -- although there are matches, if none of the first ten matches -- satisfies the predicate. -- -- If an error occurs, such as no hoogle database has been found, -- or the search term has no match, an empty list will be returned. searchTargets :: (Target -> Maybe a) -> T.Text -> IdeM [a] searchTargets f term = do HoogleDb mdb <- get res <- liftIO $ runHoogleQuery mdb term (Right . mapMaybe f . take 10) case bimap hoogleErrorToIdeError id res of Left _ -> return [] Right xs -> return xs ------------------------------------------------------------------------ -- | 'lookup' @n term@ looks up the given Text in the local Hoogle database. -- Takes the first @n@ matches. -- May fail with a HoogleError that can be shown to the user. lookup :: Int -> T.Text -> IdeM (Either HoogleError [T.Text]) lookup n term = do HoogleDb mdb <- get liftIO $ runHoogleQuery mdb term $ Right . map (T.pack . targetResultDisplay False) . take n ------------------------------------------------------------------------ -- | Run a query for Hoogle on the given Hoogle database. -- If no Database is given, no search is executed. -- If the Database cannot be found at the given location, an IOException will be thrown. -- Note, that the database file must be an absolute path. -- The target may be of the form: 'take', 'take :: Int -> [a] -> [a]', 'Data.List'. -- In general, it is very similar to the Web Api. -- Found targets can be consumed with the given callback function. -- You can limit the amount of results, by taking only the first ten results. -- Example call: -- -- @ -- runHoogleQuery -- (Just "/home/user/.hoogle/default-haskell-5.0.17.hoo") -- (Data.Text.pack "take :: Int -> [a] -> [a]") -- (Right . Prelude.take 10) -- @ -- This limits the results to ten and looks for a function `take` that has the given signature. -- -- HoogleError's can be translated to IdeErrors with @hoogleErrorToIdeError@ -- and shown to the client. runHoogleQuery :: Maybe FilePath -> T.Text -> ([Target] -> Either HoogleError a) -> IO (Either HoogleError a) runHoogleQuery Nothing _ _ = return $ Left NoDb runHoogleQuery (Just db) quer f = do res <- try (searchHoogle db quer) :: IO (Either ErrorCall [Target]) return . join $ bimap (DbFail . T.pack . show) f res -- | Run a query for Hoogle on the given Hoogle database. -- If the database can not be found, an IOException is thrown. -- The target may be of the form: `take`, `take :: Int -> [a] -> [a]` searchHoogle :: FilePath -> T.Text -> IO [Target] searchHoogle dbf quer = withDatabase dbf (return . flip searchDatabase (T.unpack quer)) ------------------------------------------------------------------------ docRules :: Maybe T.Text -> T.Text -> T.Text docRules (Just "base") "GHC.Base" = "Prelude" docRules (Just "base") "GHC.Enum" = "Prelude" docRules (Just "base") "GHC.Num" = "Prelude" docRules (Just "base") "GHC.Real" = "Prelude" docRules (Just "base") "GHC.Float" = "Prelude" docRules (Just "base") "GHC.Show" = "Prelude" docRules (Just "containers") modName = fromMaybe modName $ T.stripSuffix ".Base" modName docRules _ modName = modName -- | Get the Documentation for a given identifier in a given module. -- May also specify the according package, to avoid name clashes. -- Results is a prettified Text that can be sent and shown to the client. -- -- Might fail, if the identifier can not be found. getDocsForName :: T.Text -- ^ Identifier within a module. -> Maybe T.Text -- ^ Optional package name to avoid name clashes. -> T.Text -- ^ Name of the module to search in. -> IdeM (Maybe T.Text) -- ^ Prettified hoogle documentation of target. getDocsForName name pkg modName' = do let modName = docRules pkg modName' query = name <> maybe "" (T.append " package:") pkg <> " module:" <> modName <> " is:exact" debugm $ "hoogle query: " ++ T.unpack query res <- infoFancyRender query case res of Right x -> return $ Just x Left _ -> return Nothing