mcp-hoogle 0.1.0 → 0.2.0
raw patch · 5 files changed
+276/−92 lines, 5 filesdep +stmdep +timePVP ok
version bump matches the API change (PVP)
Dependencies added: stm, time
API changes (from Hackage documentation)
- McpHoogle.Tools: [databasePath] :: RegenerateDatabaseParams -> Text
+ McpHoogle.Tools: RegenDone :: UTCTime -> Text -> RegenState
+ McpHoogle.Tools: RegenFailed :: UTCTime -> Text -> RegenState
+ McpHoogle.Tools: RegenIdle :: RegenState
+ McpHoogle.Tools: RegenRunning :: UTCTime -> RegenState
+ McpHoogle.Tools: RegenerationStatus :: HoogleTool
+ McpHoogle.Tools: ReloadDatabase :: ReloadDatabaseParams -> HoogleTool
+ McpHoogle.Tools: ReloadDatabaseParams :: Text -> ReloadDatabaseParams
+ McpHoogle.Tools: [ghcBinPath] :: RegenerateDatabaseParams -> Text
+ McpHoogle.Tools: [reloadPath] :: ReloadDatabaseParams -> Text
+ McpHoogle.Tools: data RegenState
+ McpHoogle.Tools: data ReloadDatabaseParams
- McpHoogle.Tools: handleTool :: IORef Database -> HoogleTool -> IO Text
+ McpHoogle.Tools: handleTool :: IORef (Maybe Database) -> TVar RegenState -> HoogleTool -> IO Text
Files
- app/Main.hs +3/−1
- mcp-hoogle.cabal +15/−1
- src/McpHoogle.hs +67/−48
- src/McpHoogle/Tools.hs +142/−32
- test/Test.hs +49/−10
app/Main.hs view
@@ -3,6 +3,8 @@ import McpHoogle (runServer, runServerWithDb) import Hoogle (defaultDatabaseLocation, hoogle) import Options.Applicative+import Data.Version (showVersion)+import Paths_mcp_hoogle (version) data Command = Generate GenerateOpts@@ -41,7 +43,7 @@ )) opts :: ParserInfo Command-opts = info (commandParser <**> helper)+opts = info (commandParser <**> helper <**> simpleVersioner ("mcp-hoogle " <> showVersion version)) ( fullDesc <> progDesc "MCP server exposing Hoogle search over local Haskell dependencies" <> header "mcp-hoogle - Hoogle search via Model Context Protocol"
mcp-hoogle.cabal view
@@ -1,7 +1,7 @@ cabal-version: 3.0 name: mcp-hoogle-version: 0.1.0+version: 0.2.0 synopsis: MCP server exposing Hoogle search over local project dependencies description: An MCP (Model Context Protocol) server that exposes Hoogle search over your project's local Haskell dependencies. Run from within a@@ -77,19 +77,31 @@ McpHoogle McpHoogle.Tools McpHoogle.Format+ autogen-modules:+ Paths_mcp_hoogle+ other-modules:+ Paths_mcp_hoogle hs-source-dirs: src build-depends: hoogle >=5.0 && <5.1+ -- 0.1.0.17 added protocol version negotiation; older versions reject+ -- Claude Code's 2024-11-05 handshake and tools silently don't appear. , mcp-server >=0.1.0.17 && <0.2 , text >=1.2 && <2.2 , directory >=1.3 && <1.4+ , stm >=2.5 && <2.6+ , time >=1.9 && <1.15 executable mcp-hoogle import: common-options main-is: Main.hs hs-source-dirs: app+ autogen-modules:+ Paths_mcp_hoogle+ other-modules:+ Paths_mcp_hoogle build-depends: mcp-hoogle , hoogle >=5.0 && <5.1@@ -110,3 +122,5 @@ , hoogle >=5.0 && <5.1 , text >=1.2 && <2.2 , directory >=1.3 && <1.4+ , stm >=2.5 && <2.6+ , time >=1.9 && <1.15
src/McpHoogle.hs view
@@ -4,16 +4,23 @@ -- This module wires together the Hoogle database, the tool definitions from -- "McpHoogle.Tools", and the @mcp-server@ library's stdio transport. -- It loads the Hoogle database into an 'IORef' so it can be swapped at--- runtime (via the @regenerate_database@ tool), then enters the MCP--- request\/response loop reading JSON-RPC from stdin and writing to stdout.+-- runtime (via the @regenerate_database@ or @reload_database@ tools),+-- then enters the MCP request\/response loop reading JSON-RPC from stdin+-- and writing to stdout.+--+-- The server starts even without an existing database — searches return+-- "No database loaded" until one is generated or reloaded. module McpHoogle ( runServer , runServerWithDb ) where -import Data.IORef (newIORef)+import Control.Concurrent.STM (TVar, newTVarIO)+import Data.IORef (IORef, newIORef) import Data.Text qualified as Text+import Data.Version (showVersion)+import Hoogle qualified (Database) import Hoogle (withDatabase, defaultDatabaseLocation) import MCP.Server (runMcpServerStdio) import MCP.Server.Types@@ -22,22 +29,23 @@ , Content(..) ) import MCP.Server.Derive (deriveToolHandlerWithDescription)-import McpHoogle.Tools (HoogleTool(..), handleTool, toolDescriptions)+import McpHoogle.Tools (HoogleTool(..), RegenState(..), handleTool, toolDescriptions)+import Paths_mcp_hoogle (version) import System.Directory (doesFileExist) -- | Run the MCP server using the default Hoogle database location -- (@~\/.hoogle\/default-haskell-*.hoo@). ----- Errors immediately if no database is found — the user should run--- @mcp-hoogle generate@ first.+-- If no database exists, the server starts anyway with an empty database+-- ref — the agent can call @regenerate_database@ or @reload_database@ to+-- populate it without restarting. runServer :: IO () runServer = do defaultPath <- defaultDatabaseLocation exists <- doesFileExist defaultPath if exists then runServerWithDb defaultPath- else error $ "Hoogle database not found at: " <> defaultPath- <> "\nRun 'mcp-hoogle generate' to create it, or pass --database PATH."+ else runServerEmpty -- | Run the MCP server with an explicit database path. --@@ -47,46 +55,57 @@ runServerWithDb :: FilePath -> IO () runServerWithDb databasePath = withDatabase databasePath $ \database -> do- databaseRef <- newIORef database- let serverInfo :: McpServerInfo- serverInfo = McpServerInfo- { serverName = "mcp-hoogle"- , serverVersion = "0.1.0"- , serverInstructions = Text.unlines- [ "Hoogle search for Haskell types, functions, and modules."- , ""- , "USE THESE TOOLS INSTEAD OF:"- , "- Web searching for Haskell documentation"- , "- Running `hoogle` or `mcp-hoogle` CLI commands"- , "- Fetching Hackage pages with curl/w3m"- , ""- , "AVAILABLE TOOLS:"- , "- search: Find functions by name, keyword, or type signature (e.g. \"map\", \"[a] -> Int\")"- , "- search_type: Search specifically by type signature"- , "- lookup_module: Browse all exports of a module (e.g. \"Data.Map\")"- , "- regenerate_database: Re-index after entering a different project's nix-shell"- , ""- , "DATABASE SETUP:"- , "If no database exists, run `mcp-hoogle generate` from an"- , "environment where `ghc-pkg` is on PATH (so it can discover"- , "installed packages). For nix-based projects this means running"- , "from inside the project's nix-shell:"- , " nix-shell --run 'mcp-hoogle generate'"- , "For cabal/stack projects, just run `mcp-hoogle generate` directly"- , "(GHC tools are already on PATH)."- , "This only needs to be done once per project. The database persists"- , "at ~/.hoogle/ and is reused across sessions."- ]- }+ databaseRef <- newIORef (Just database)+ regenStateVar <- newTVarIO RegenIdle+ runMcpServerStdio serverInfo (handlers databaseRef regenStateVar) - toolHandler :: HoogleTool -> IO Content- toolHandler tool = ContentText <$> handleTool databaseRef tool+-- | Run the MCP server without a database.+--+-- The server starts and exposes tools, but searches return "No database+-- loaded" until the agent calls @regenerate_database@ or @reload_database@.+runServerEmpty :: IO ()+runServerEmpty = do+ databaseRef <- newIORef Nothing+ regenStateVar <- newTVarIO RegenIdle+ runMcpServerStdio serverInfo (handlers databaseRef regenStateVar) - handlers :: McpServerHandlers IO- handlers = McpServerHandlers- { prompts = Nothing- , resources = Nothing- , tools = Just $(deriveToolHandlerWithDescription ''HoogleTool 'toolHandler toolDescriptions)- }+-- | Server metadata sent during MCP initialization.+serverInfo :: McpServerInfo+serverInfo = McpServerInfo+ { serverName = "mcp-hoogle"+ , serverVersion = Text.pack (showVersion version)+ , serverInstructions = Text.unlines+ [ "Hoogle search for Haskell types, functions, and modules."+ , ""+ , "USE THESE TOOLS INSTEAD OF:"+ , "- Web searching for Haskell documentation"+ , "- Running `hoogle` or `mcp-hoogle` CLI commands"+ , "- Fetching Hackage pages with curl/w3m"+ , ""+ , "AVAILABLE TOOLS:"+ , "- search: Find functions by name, keyword, or type signature (e.g. \"map\", \"[a] -> Int\")"+ , "- search_type: Search specifically by type signature"+ , "- lookup_module: Browse all exports of a module (e.g. \"Data.Map\")"+ , "- regenerate_database: Re-index packages. Pass ghcBinPath (find it with: nix-shell --run 'dirname $(which ghc-pkg)')"+ , "- reload_database: Reload database from disk after generating externally"+ , ""+ , "DATABASE SETUP:"+ , "If searches return 'No database loaded', call regenerate_database with"+ , "the ghcBinPath from the project's nix-shell. Find it by running:"+ , " nix-shell --run 'dirname $(which ghc-pkg)'"+ , "Then pass that path to regenerate_database."+ , "Alternatively, run `nix-shell --run 'mcp-hoogle generate'` as a bash"+ , "command, then call reload_database to pick up the new file."+ ]+ } - runMcpServerStdio serverInfo handlers+-- | Build MCP handlers from a database ref and regeneration state.+handlers :: IORef (Maybe Hoogle.Database) -> TVar RegenState -> McpServerHandlers IO+handlers databaseRef regenStateVar = McpServerHandlers+ { prompts = Nothing+ , resources = Nothing+ , tools = Just $(deriveToolHandlerWithDescription ''HoogleTool 'toolHandler toolDescriptions)+ }+ where+ toolHandler :: HoogleTool -> IO Content+ toolHandler tool = ContentText <$> handleTool databaseRef regenStateVar tool
src/McpHoogle/Tools.hs view
@@ -4,14 +4,20 @@ -- MCP client) can invoke. The @mcp-server@ library's Template Haskell -- derivation turns the ADT into a tool list + dispatcher automatically. ----- The handler reads from an 'IORef' 'Database' so the database can be--- hot-swapped via 'RegenerateDatabase' without restarting the server.+-- The handler reads from an 'IORef' holding a 'Maybe' 'Database'. The ref+-- starts as 'Nothing' when no database exists on disk and is populated+-- by 'RegenerateDatabase' or 'ReloadDatabase'.+--+-- 'RegenerateDatabase' runs asynchronously via 'forkIO' so the MCP response+-- returns immediately. Use 'RegenerationStatus' to poll progress. module McpHoogle.Tools ( HoogleTool(..) , SearchParams(..) , SearchTypeParams(..) , LookupModuleParams(..) , RegenerateDatabaseParams(..)+ , ReloadDatabaseParams(..)+ , RegenState(..) , handleTool , toolDescriptions )@@ -20,8 +26,15 @@ import Data.Text (Text) import Data.Text qualified as Text import Data.IORef (IORef, readIORef, writeIORef)-import Hoogle (Database, searchDatabase, withDatabase, hoogle)+import Data.Time.Clock (UTCTime, getCurrentTime, diffUTCTime)+import Control.Concurrent (forkIO)+import Control.Concurrent.STM (TVar, readTVarIO, atomically, writeTVar)+import Control.Exception (bracket, SomeException, try)+import GHC.IO.Handle (hDuplicate, hDuplicateTo)+import Hoogle (Database, searchDatabase, withDatabase, hoogle, defaultDatabaseLocation) import McpHoogle.Format (formatTargets)+import System.Environment (setEnv, lookupEnv)+import System.IO (stdout, stderr, hFlush) -- | Parameters for a general search (name, keyword, or type signature). data SearchParams = SearchParams@@ -39,10 +52,28 @@ } -- | Parameters for database regeneration.+--+-- Requires @ghcBinPath@ so that @ghc-pkg@ can be found even when the+-- MCP server was started outside a nix-shell. data RegenerateDatabaseParams = RegenerateDatabaseParams- { databasePath :: Text+ { ghcBinPath :: Text } +-- | Parameters for reloading the database from disk without regenerating.+data ReloadDatabaseParams = ReloadDatabaseParams+ { reloadPath :: Text+ }++-- | Tracks the state of an asynchronous database regeneration.+--+-- Stored in a 'TVar' for thread-safe access between the MCP handler+-- thread and the background regeneration thread.+data RegenState+ = RegenIdle+ | RegenRunning UTCTime -- ^ Started at this time+ | RegenDone UTCTime Text -- ^ Finished at, database path+ | RegenFailed UTCTime Text -- ^ Finished at, error message+ -- | The set of MCP tools this server exposes. -- -- Each constructor maps to one callable tool. The nested parameter records@@ -53,6 +84,8 @@ | SearchType SearchTypeParams | LookupModule LookupModuleParams | RegenerateDatabase RegenerateDatabaseParams+ | ReloadDatabase ReloadDatabaseParams+ | RegenerationStatus -- | Human-readable descriptions for each tool and its arguments. -- Fed to 'deriveToolHandlerWithDescription' so MCP clients know what@@ -62,39 +95,116 @@ [ ("Search", "Search Hoogle by function name, type signature, or keyword. Returns matching functions with their types, packages, and documentation.") , ("SearchType", "Search Hoogle specifically by type signature. Example: 'a -> [a]' or '[a] -> Int'") , ("LookupModule", "Search for all exports of a given module name. Example: 'Data.Map'")- , ("RegenerateDatabase", "Regenerate the Hoogle database from the current GHC package database. Call this after switching projects or nix-shells to re-index available packages. The databasePath should be the path to write the .hoo file.")+ , ("RegenerateDatabase", "Regenerate the Hoogle database asynchronously. Returns immediately — use regeneration_status to poll progress. Requires ghcBinPath so ghc-pkg can be found. Find it with: nix-shell --run 'dirname $(which ghc-pkg)'")+ , ("ReloadDatabase", "Reload the Hoogle database from disk without regenerating. Use after running 'mcp-hoogle generate' externally via a bash command.")+ , ("RegenerationStatus", "Check the status of an asynchronous database regeneration. Reports idle, in-progress (with elapsed time), completed, or failed.") , ("query", "The search query: a function name, keyword, or type signature") , ("typeSignature", "A Haskell type signature to search for, e.g. '[a] -> Int' or 'Map k v -> [(k,v)]'") , ("moduleName", "A Haskell module name to look up, e.g. 'Data.Map' or 'Control.Monad'")- , ("databasePath", "Path to the Hoogle database file to regenerate and reload")+ , ("ghcBinPath", "Path to GHC's bin directory containing ghc-pkg. Find with: nix-shell --run 'dirname $(which ghc-pkg)'")+ , ("reloadPath", "Path to the .hoo database file to reload. Use empty string for default (~/.hoogle/default-haskell-5.0.18.hoo).") ] -- | Dispatch a tool call to the appropriate Hoogle operation. -- -- Reads the current database from the 'IORef'. For 'RegenerateDatabase',--- shells out to @hoogle generate --local@, reloads the resulting file,--- and swaps the 'IORef' contents so subsequent searches use the new data.-handleTool :: IORef Database -> HoogleTool -> IO Text-handleTool databaseRef (Search (SearchParams searchQuery)) = do- database <- readIORef databaseRef- let results = take 20 (searchDatabase database (Text.unpack searchQuery))- pure (formatTargets results)-handleTool databaseRef (SearchType (SearchTypeParams sig)) = do- database <- readIORef databaseRef- let results = take 20 (searchDatabase database (Text.unpack sig))- pure (formatTargets results)-handleTool databaseRef (LookupModule (LookupModuleParams modName)) = do- database <- readIORef databaseRef- let queryString = "module:" <> Text.unpack modName- results = take 30 (searchDatabase database queryString)- -- If module: prefix doesn't work well, fall back to plain search- let finalResults = if null results- then take 30 (searchDatabase database (Text.unpack modName))- else results- pure (formatTargets finalResults)-handleTool databaseRef (RegenerateDatabase (RegenerateDatabaseParams path)) = do- let pathStr = Text.unpack path- hoogle ["generate", "--local", "--database=" <> pathStr]- withDatabase pathStr $ \newDb -> do- writeIORef databaseRef newDb- pure "Database regenerated and reloaded successfully."+-- spawns a background thread and returns immediately — poll with+-- 'RegenerationStatus'.+handleTool :: IORef (Maybe Database) -> TVar RegenState -> HoogleTool -> IO Text+handleTool databaseRef _regenStateVar (Search (SearchParams searchQuery)) = do+ mDatabase <- readIORef databaseRef+ case mDatabase of+ Nothing -> pure "No database loaded. Call regenerate_database with your project's ghcBinPath first."+ Just database -> do+ let results = take 20 (searchDatabase database (Text.unpack searchQuery))+ pure (formatTargets results)+handleTool databaseRef _regenStateVar (SearchType (SearchTypeParams sig)) = do+ mDatabase <- readIORef databaseRef+ case mDatabase of+ Nothing -> pure "No database loaded. Call regenerate_database with your project's ghcBinPath first."+ Just database -> do+ let results = take 20 (searchDatabase database (Text.unpack sig))+ pure (formatTargets results)+handleTool databaseRef _regenStateVar (LookupModule (LookupModuleParams modName)) = do+ mDatabase <- readIORef databaseRef+ case mDatabase of+ Nothing -> pure "No database loaded. Call regenerate_database with your project's ghcBinPath first."+ Just database -> do+ let queryString = "module:" <> Text.unpack modName+ results = take 30 (searchDatabase database queryString)+ -- If module: prefix doesn't work well, fall back to plain search+ let finalResults = if null results+ then take 30 (searchDatabase database (Text.unpack modName))+ else results+ pure (formatTargets finalResults)+handleTool databaseRef regenStateVar (RegenerateDatabase (RegenerateDatabaseParams ghcBin)) = do+ currentState <- readTVarIO regenStateVar+ case currentState of+ RegenRunning _ -> pure "Database regeneration already in progress. Use regeneration_status to check progress."+ _ -> do+ startTime <- getCurrentTime+ atomically (writeTVar regenStateVar (RegenRunning startTime))+ dbPath <- defaultDatabaseLocation+ let ghcBinStr = Text.unpack ghcBin+ _ <- forkIO $ do+ result <- try $ do+ withPrependedPath ghcBinStr $+ withSilencedStdout $+ hoogle ["generate", "--local", "--database=" <> dbPath]+ withDatabase dbPath $ \newDb ->+ writeIORef databaseRef (Just newDb)+ finishTime <- getCurrentTime+ case result of+ Right () ->+ atomically (writeTVar regenStateVar (RegenDone finishTime (Text.pack dbPath)))+ Left someException ->+ atomically (writeTVar regenStateVar (RegenFailed finishTime (Text.pack (show (someException :: SomeException)))))+ pure "Database regeneration started. Use regeneration_status to check progress."+handleTool databaseRef _regenStateVar (ReloadDatabase (ReloadDatabaseParams path)) = do+ dbPath <- if Text.null path+ then defaultDatabaseLocation+ else pure (Text.unpack path)+ withDatabase dbPath $ \newDb -> do+ writeIORef databaseRef (Just newDb)+ pure (Text.pack ("Database reloaded from: " <> dbPath))+handleTool _databaseRef regenStateVar RegenerationStatus = do+ currentState <- readTVarIO regenStateVar+ now <- getCurrentTime+ case currentState of+ RegenIdle -> pure "No regeneration in progress or completed."+ RegenRunning startTime ->+ let elapsed = floor (diffUTCTime now startTime) :: Int+ in pure (Text.pack ("Regeneration in progress (started " <> show elapsed <> "s ago)."))+ RegenDone finishTime dbPathText ->+ let ago = floor (diffUTCTime now finishTime) :: Int+ in pure (Text.pack ("Regeneration completed " <> show ago <> "s ago. Database loaded from: " <> Text.unpack dbPathText))+ RegenFailed finishTime errorText ->+ let ago = floor (diffUTCTime now finishTime) :: Int+ in pure (Text.pack ("Regeneration failed " <> show ago <> "s ago: " <> Text.unpack errorText))++-- | Temporarily prepend a directory to PATH, run an action, then restore.+withPrependedPath :: FilePath -> IO a -> IO a+withPrependedPath dir action = do+ oldPath <- lookupEnv "PATH"+ let newPath = case oldPath of+ Nothing -> dir+ Just p -> dir <> ":" <> p+ bracket+ (setEnv "PATH" newPath)+ (\_ -> maybe (setEnv "PATH" "") (setEnv "PATH") oldPath)+ (\_ -> action)++-- | Redirect stdout to \/dev\/null during an action.+--+-- Hoogle's library API writes progress text to stdout which would corrupt+-- the MCP JSON-RPC stream. We redirect stdout to stderr (so it's still+-- visible for debugging) and restore it after.+withSilencedStdout :: IO a -> IO a+withSilencedStdout action = do+ hFlush stdout+ savedStdout <- hDuplicate stdout+ hDuplicateTo stderr stdout+ result <- action+ hFlush stdout+ hDuplicateTo savedStdout stdout+ pure result
test/Test.hs view
@@ -3,12 +3,14 @@ import Test.Tasty import Test.Tasty.HUnit +import Control.Concurrent.STM (TVar, newTVarIO, atomically, writeTVar)+import Data.Time.Clock (getCurrentTime) import Control.Exception (try, SomeException) import Data.IORef (IORef, newIORef) import Data.Text qualified as Text import Hoogle (Database, Target(..), defaultDatabaseLocation, withDatabase, hoogle) import McpHoogle.Format (formatTarget, formatTargets, stripHtmlTags)-import McpHoogle.Tools (HoogleTool(..), SearchParams(..), SearchTypeParams(..), LookupModuleParams(..), handleTool)+import McpHoogle.Tools (HoogleTool(..), SearchParams(..), SearchTypeParams(..), LookupModuleParams(..), RegenState(..), handleTool) import System.Directory (doesFileExist, createDirectoryIfMissing, getTemporaryDirectory) main :: IO ()@@ -18,6 +20,7 @@ tests = testGroup "McpHoogle" [ formatTests , stripHtmlTests+ , regenStateTests , withResource acquireDb releaseDb hoogleSearchTests ] @@ -77,6 +80,41 @@ stripHtmlTags "<a><b>deep</b></a>" @?= "deep" ] +-- | Tests for the async regeneration state machine.+regenStateTests :: TestTree+regenStateTests = testGroup "Regeneration state"+ [ testCase "regeneration_status reports idle initially" $ do+ databaseRef <- newIORef Nothing+ regenStateVar <- newTVarIO RegenIdle+ result <- handleTool databaseRef regenStateVar RegenerationStatus+ assertBool "should report idle"+ (Text.isInfixOf "No regeneration" result)+ , testCase "regeneration_status reports running when state is RegenRunning" $ do+ databaseRef <- newIORef Nothing+ regenStateVar <- newTVarIO RegenIdle+ now <- getCurrentTime+ atomically (writeTVar regenStateVar (RegenRunning now))+ result <- handleTool databaseRef regenStateVar RegenerationStatus+ assertBool "should report in progress"+ (Text.isInfixOf "in progress" result)+ , testCase "regeneration_status reports failure" $ do+ databaseRef <- newIORef Nothing+ regenStateVar <- newTVarIO RegenIdle+ now <- getCurrentTime+ atomically (writeTVar regenStateVar (RegenFailed now "ghc-pkg not found"))+ result <- handleTool databaseRef regenStateVar RegenerationStatus+ assertBool "should report failure with error message"+ (Text.isInfixOf "ghc-pkg not found" result)+ , testCase "regeneration_status reports completion" $ do+ databaseRef <- newIORef Nothing+ regenStateVar <- newTVarIO RegenIdle+ now <- getCurrentTime+ atomically (writeTVar regenStateVar (RegenDone now "/tmp/test.hoo"))+ result <- handleTool databaseRef regenStateVar RegenerationStatus+ assertBool "should report completed with path"+ (Text.isInfixOf "/tmp/test.hoo" result)+ ]+ -- | Integration tests that exercise hoogle search through our handler. -- The database path is shared across all tests via withResource. -- Tests pass trivially when no database is available (e.g. cabal CI without haddocks).@@ -86,8 +124,8 @@ mDbPath <- getDbPath case mDbPath of Nothing -> pure () -- no DB available, skip- Just dbPath -> withHoogleDb dbPath $ \databaseRef -> do- result <- handleTool databaseRef (Search (SearchParams "map"))+ Just dbPath -> withHoogleDb dbPath $ \databaseRef regenStateVar -> do+ result <- handleTool databaseRef regenStateVar (Search (SearchParams "map")) assertBool "search should return results, not 'No results found'" (result /= "No results found.") assertBool "search for map should mention 'map' in results"@@ -96,26 +134,27 @@ mDbPath <- getDbPath case mDbPath of Nothing -> pure ()- Just dbPath -> withHoogleDb dbPath $ \databaseRef -> do- result <- handleTool databaseRef (SearchType (SearchTypeParams "[a] -> Int"))+ Just dbPath -> withHoogleDb dbPath $ \databaseRef regenStateVar -> do+ result <- handleTool databaseRef regenStateVar (SearchType (SearchTypeParams "[a] -> Int")) assertBool "type search should return results" (result /= "No results found.") , testCase "module lookup 'Data.Map' returns results" $ do mDbPath <- getDbPath case mDbPath of Nothing -> pure ()- Just dbPath -> withHoogleDb dbPath $ \databaseRef -> do- result <- handleTool databaseRef (LookupModule (LookupModuleParams "Data.Map"))+ Just dbPath -> withHoogleDb dbPath $ \databaseRef regenStateVar -> do+ result <- handleTool databaseRef regenStateVar (LookupModule (LookupModuleParams "Data.Map")) assertBool "module lookup should return results" (result /= "No results found.") ] -- | Load a hoogle database and run an action with it-withHoogleDb :: FilePath -> (IORef Database -> IO ()) -> IO ()+withHoogleDb :: FilePath -> (IORef (Maybe Database) -> TVar RegenState -> IO ()) -> IO () withHoogleDb path action = withDatabase path $ \database -> do- databaseRef <- newIORef database- action databaseRef+ databaseRef <- newIORef (Just database)+ regenStateVar <- newTVarIO RegenIdle+ action databaseRef regenStateVar -- | Helper to create a Target for testing mkTarget :: String -> Maybe (String, String) -> Maybe (String, String) -> Target