Skip to content
This repository was archived by the owner on Oct 7, 2020. It is now read-only.

HaRe hie plugin api #1215

Merged
merged 23 commits into from
May 5, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
82ae176
Add Haskell.Ide.Engine.PluginApi.
alanz Apr 14, 2019
d71de15
Expose ghc-mod features currently used in HaRe tests
alanz Apr 14, 2019
d645924
Add the HIE Monads
alanz Apr 14, 2019
8cb0bca
Merge branch 'master' into hare-hie-plugin-api
alanz Apr 20, 2019
ecced4f
Suport HaRe running in IdeGhcM
alanz Apr 20, 2019
f5ea4b3
Move setTypecheckedModule into hie-plugin-api
alanz Apr 21, 2019
0455f9e
Tweaks
alanz Apr 21, 2019
f9137a3
Add missing module
alanz Apr 21, 2019
e47d1dc
WIP, includes list of things actually used by HaRe currently
alanz Apr 22, 2019
e1da17d
More narrowing of the plugin api
alanz Apr 22, 2019
416b2c8
Merge remote-tracking branch 'haskell/master' into hare-hie-plugin-api
alanz Apr 27, 2019
e113322
Start using ghc-project-types for the plugin API
alanz Apr 27, 2019
fbd6860
Only ghc-mod Options and defaultOptions left to decouple
alanz Apr 27, 2019
1d4a52c
Use hie data type BiosOptions instead of ghc-mod Options
alanz Apr 28, 2019
43c3a3b
Update submodules for all GHC versions
alanz Apr 28, 2019
5dcc7f2
Sort out some compiler warnings
alanz Apr 28, 2019
9c51e77
Use hie-bios version of HaRe
alanz Apr 28, 2019
c16e343
Bump HaRe submodule to work with GHC 8.2.x
alanz Apr 28, 2019
54a8b61
Update comment.
alanz Apr 28, 2019
eb77dc1
Merge branch 'master' into hare-hie-plugin-api
alanz May 4, 2019
d50a1b0
Apply progress reporting to setTypecheckedModule in its new home
alanz May 4, 2019
84b0405
Fix build for GHC 8.2
alanz May 4, 2019
bf612fc
Fix GHC 8.6.5 build too
alanz May 4, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 5 additions & 7 deletions app/MainHie.hs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ module Main where
import Control.Monad
import Data.Monoid ((<>))
import Data.Version (showVersion)
import qualified GhcMod.Types as GM
import Haskell.Ide.Engine.MonadFunctions
import Haskell.Ide.Engine.MonadTypes
import Haskell.Ide.Engine.Options
Expand Down Expand Up @@ -121,13 +120,12 @@ run opts = do
d <- getCurrentDirectory
logm $ "Current directory:" ++ d

let vomitOptions = GM.defaultOptions { GM.optOutput = oo { GM.ooptLogLevel = GM.GmVomit}}
oo = GM.optOutput GM.defaultOptions
let defaultOpts = if optGhcModVomit opts then vomitOptions else GM.defaultOptions
let vomitOptions = defaultOptions { boLogging = BlVomit}
let defaultOpts = if optGhcModVomit opts then vomitOptions else defaultOptions
-- Running HIE on projects with -Werror breaks most of the features since all warnings
-- will be treated with the same severity of type errors. In order to offer a more useful
-- experience, we make sure warnings are always reported as warnings by setting -Wwarn
ghcModOptions = defaultOpts { GM.optGhcUserOptions = ["-Wwarn"] }
biosOptions = defaultOpts { boGhcUserOptions = ["-Wwarn"] }

when (optGhcModVomit opts) $
logm "Enabling --vomit for ghc-mod. Output will be on stderr"
Expand All @@ -139,8 +137,8 @@ run opts = do

-- launch the dispatcher.
if optJson opts then do
scheduler <- newScheduler plugins' ghcModOptions
scheduler <- newScheduler plugins' biosOptions
jsonStdioTransport scheduler
else do
scheduler <- newScheduler plugins' ghcModOptions
scheduler <- newScheduler plugins' biosOptions
lspStdioTransport scheduler origDir plugins' (optCaptureFile opts)
1 change: 1 addition & 0 deletions cabal.project
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ packages:
./submodules/floskell
./submodules/ghc-mod/
./submodules/ghc-mod/core/
./submodules/ghc-mod/ghc-project-types

tests: true
238 changes: 238 additions & 0 deletions hie-plugin-api/Haskell/Ide/Engine/Ghc.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
{-# LANGUAGE CPP #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeFamilies #-}
-- | This module provides the interface to GHC, mainly for loading
-- modules while updating the module cache.

module Haskell.Ide.Engine.Ghc
(
setTypecheckedModule
, Diagnostics
, AdditionalErrs
, cabalModuleGraphs
, makeRevRedirMapFunc
) where

import Bag
import Control.Monad.IO.Class
import Data.IORef
import qualified Data.Map.Strict as Map
import qualified Data.Set as Set
import qualified Data.Text as T
import ErrUtils
import qualified GhcMod.DynFlags as GM
import qualified GhcMod.Error as GM
import qualified GhcMod.Gap as GM
import qualified GhcMod.ModuleLoader as GM
import qualified GhcMod.Monad as GM
import Data.Monoid ((<>))
import qualified GhcMod.Target as GM
import qualified GhcMod.Types as GM
import qualified GhcMod.Utils as GM
import Haskell.Ide.Engine.MonadFunctions
import Haskell.Ide.Engine.MonadTypes
import Haskell.Ide.Engine.PluginUtils
import System.FilePath

import DynFlags
import GHC
import IOEnv as G
import HscTypes
import Outputable (renderWithStyle)

-- ---------------------------------------------------------------------

type Diagnostics = Map.Map Uri (Set.Set Diagnostic)
type AdditionalErrs = [T.Text]

-- ---------------------------------------------------------------------

lspSev :: Severity -> DiagnosticSeverity
lspSev SevWarning = DsWarning
lspSev SevError = DsError
lspSev SevFatal = DsError
lspSev SevInfo = DsInfo
lspSev _ = DsInfo

-- ---------------------------------------------------------------------
-- type LogAction = DynFlags -> WarnReason -> Severity -> SrcSpan -> PprStyle -> MsgDoc -> IO ()
logDiag :: (FilePath -> FilePath) -> IORef AdditionalErrs -> IORef Diagnostics -> LogAction
logDiag rfm eref dref df _reason sev spn style msg = do
eloc <- srcSpan2Loc rfm spn
let msgTxt = T.pack $ renderWithStyle df msg style
case eloc of
Right (Location uri range) -> do
let update = Map.insertWith Set.union uri l
where l = Set.singleton diag
diag = Diagnostic range (Just $ lspSev sev) Nothing (Just "ghcmod") msgTxt Nothing
modifyIORef' dref update
Left _ -> do
modifyIORef' eref (msgTxt:)
return ()

-- ---------------------------------------------------------------------

-- unhelpfulSrcSpanErr :: T.Text -> IdeError
-- unhelpfulSrcSpanErr err =
-- IdeError PluginError
-- ("Unhelpful SrcSpan" <> ": \"" <> err <> "\"")
-- Null

-- ---------------------------------------------------------------------

srcErrToDiag :: MonadIO m
=> DynFlags
-> (FilePath -> FilePath)
-> SourceError -> m (Diagnostics, AdditionalErrs)
srcErrToDiag df rfm se = do
debugm "in srcErrToDiag"
let errMsgs = bagToList $ srcErrorMessages se
processMsg err = do
let sev = Just DsError
unqual = errMsgContext err
st = GM.mkErrStyle' df unqual
msgTxt = T.pack $ renderWithStyle df (pprLocErrMsg err) st
eloc <- srcSpan2Loc rfm $ errMsgSpan err
case eloc of
Right (Location uri range) ->
return $ Right (uri, Diagnostic range sev Nothing (Just "ghcmod") msgTxt Nothing)
Left _ -> return $ Left msgTxt
processMsgs [] = return (Map.empty,[])
processMsgs (x:xs) = do
res <- processMsg x
(m,es) <- processMsgs xs
case res of
Right (uri, diag) ->
return (Map.insertWith Set.union uri (Set.singleton diag) m, es)
Left e -> return (m, e:es)
processMsgs errMsgs

-- ---------------------------------------------------------------------

myWrapper :: GM.IOish m
=> (FilePath -> FilePath)
-> GM.GmlT m ()
-> GM.GmlT m (Diagnostics, AdditionalErrs)
myWrapper rfm action = do
env <- getSession
diagRef <- liftIO $ newIORef Map.empty
errRef <- liftIO $ newIORef []
let setLogger df = df { log_action = logDiag rfm errRef diagRef }
setDeferTypedHoles = setGeneralFlag' Opt_DeferTypedHoles
ghcErrRes msg = (Map.empty, [T.pack msg])
handlers = errorHandlers ghcErrRes (srcErrToDiag (hsc_dflags env) rfm )
action' = do
GM.withDynFlags (setLogger . setDeferTypedHoles) action
diags <- liftIO $ readIORef diagRef
errs <- liftIO $ readIORef errRef
return (diags,errs)
GM.gcatches action' handlers

-- ---------------------------------------------------------------------

errorHandlers :: (Monad m) => (String -> a) -> (SourceError -> m a) -> [GM.GHandler m a]
errorHandlers ghcErrRes renderSourceError = handlers
where
-- ghc throws GhcException, SourceError, GhcApiError and
-- IOEnvFailure. ghc-mod-core throws GhcModError.
handlers =
[ GM.GHandler $ \(ex :: GM.GhcModError) ->
return $ ghcErrRes (show ex)
, GM.GHandler $ \(ex :: IOEnvFailure) ->
return $ ghcErrRes (show ex)
, GM.GHandler $ \(ex :: GhcApiError) ->
return $ ghcErrRes (show ex)
, GM.GHandler $ \(ex :: SourceError) ->
renderSourceError ex
, GM.GHandler $ \(ex :: GhcException) ->
return $ ghcErrRes $ GM.renderGm $ GM.ghcExceptionDoc ex
, GM.GHandler $ \(ex :: IOError) ->
return $ ghcErrRes (show ex)
-- , GM.GHandler $ \(ex :: GM.SomeException) ->
-- return $ ghcErrRes (show ex)
]

-- ---------------------------------------------------------------------

setTypecheckedModule :: Uri -> IdeGhcM (IdeResult (Diagnostics, AdditionalErrs))
setTypecheckedModule uri =
pluginGetFile "setTypecheckedModule: " uri $ \fp -> do
fileMap <- GM.getMMappedFiles
debugm $ "setTypecheckedModule: file mapping state is: " ++ show fileMap
rfm <- GM.mkRevRedirMapFunc
let
ghcErrRes msg = ((Map.empty, [T.pack msg]),Nothing,Nothing)
progTitle = "Typechecking " <> T.pack (takeFileName fp)
debugm "setTypecheckedModule: before ghc-mod"
-- TODO:AZ: loading this one module may/should trigger loads of any
-- other modules which currently have a VFS entry. Need to make
-- sure that their diagnostics are reported, and their module
-- cache entries are updated.
-- TODO: Are there any hooks we can use to report back on the progress?
((diags', errs), mtm, mpm) <- withIndefiniteProgress progTitle NotCancellable $ GM.gcatches
(GM.getModulesGhc' (myWrapper rfm) fp)
(errorHandlers ghcErrRes (return . ghcErrRes . show))
debugm "setTypecheckedModule: after ghc-mod"

canonUri <- canonicalizeUri uri
let diags = Map.insertWith Set.union canonUri Set.empty diags'
diags2 <- case (mpm,mtm) of
(Just pm, Nothing) -> do
debugm $ "setTypecheckedModule: Did get parsed module for: " ++ show fp
cacheModule fp (Left pm)
debugm "setTypecheckedModule: done"
return diags

(_, Just tm) -> do
debugm $ "setTypecheckedModule: Did get typechecked module for: " ++ show fp
sess <- fmap GM.gmgsSession . GM.gmGhcSession <$> GM.gmsGet

-- set the session before we cache the module, so that deferred
-- responses triggered by cacheModule can access it
modifyMTS (\s -> s {ghcSession = sess})
cacheModule fp (Right tm)
debugm "setTypecheckedModule: done"
return diags

_ -> do
debugm $ "setTypecheckedModule: Didn't get typechecked or parsed module for: " ++ show fp
debugm $ "setTypecheckedModule: errs: " ++ show errs

failModule fp

let sev = Just DsError
range = Range (Position 0 0) (Position 1 0)
msgTxt = T.unlines errs
let d = Diagnostic range sev Nothing (Just "ghcmod") msgTxt Nothing
return $ Map.insertWith Set.union canonUri (Set.singleton d) diags

return $ IdeResultOk (diags2,errs)

-- ---------------------------------------------------------------------

cabalModuleGraphs :: IdeGhcM [GM.GmModuleGraph]
cabalModuleGraphs = doCabalModuleGraphs
where
doCabalModuleGraphs :: (GM.IOish m) => GM.GhcModT m [GM.GmModuleGraph]
doCabalModuleGraphs = do
crdl <- GM.cradle
case GM.cradleCabalFile crdl of
Just _ -> do
mcs <- GM.cabalResolvedComponents
let graph = map GM.gmcHomeModuleGraph $ Map.elems mcs
return graph
Nothing -> return []

-- ---------------------------------------------------------------------

makeRevRedirMapFunc :: IdeGhcM (FilePath -> FilePath)
makeRevRedirMapFunc = make
where
make :: (GM.IOish m) => GM.GhcModT m (FilePath -> FilePath)
make = GM.mkRevRedirMapFunc

-- ---------------------------------------------------------------------
7 changes: 4 additions & 3 deletions hie-plugin-api/Haskell/Ide/Engine/ModuleCache.hs
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,8 @@ withCachedInfo fp def callback = deferIfNotCached fp go
-- If you need custom data, see also 'ifCachedModuleAndData'.
-- If you are in IdeDeferM and would like to wait until a cached module is available,
-- see also 'withCachedModule'.
ifCachedModule :: (HasGhcModuleCache m, GM.MonadIO m, CacheableModule b) => FilePath -> a -> (b -> CachedInfo -> m a) -> m a
ifCachedModule :: (HasGhcModuleCache m, MonadIO m, CacheableModule b)
=> FilePath -> a -> (b -> CachedInfo -> m a) -> m a
ifCachedModule fp def callback = do
muc <- getUriCache fp
let x = do
Expand Down Expand Up @@ -177,7 +178,7 @@ withCachedModuleAndData fp def callback = deferIfNotCached fp go
go (UriCacheSuccess (UriCache _ _ Nothing _)) = wrap (Defer fp go)
go UriCacheFailed = return def

getUriCache :: (HasGhcModuleCache m, GM.MonadIO m) => FilePath -> m (Maybe UriCacheResult)
getUriCache :: (HasGhcModuleCache m, MonadIO m) => FilePath -> m (Maybe UriCacheResult)
getUriCache fp = do
uri' <- liftIO $ canonicalizePath fp
fmap (Map.lookup uri' . uriCaches) getModuleCache
Expand Down Expand Up @@ -211,7 +212,7 @@ lookupCachedData fp tm info dat = do

-- | Saves a module to the cache and executes any deferred
-- responses waiting on that module.
cacheModule :: FilePath -> (Either GHC.ParsedModule GHC.TypecheckedModule) -> IdeGhcM ()
cacheModule :: FilePath -> Either GHC.ParsedModule GHC.TypecheckedModule -> IdeGhcM ()
cacheModule uri modul = do
uri' <- liftIO $ canonicalizePath uri
rfm <- GM.mkRevRedirMapFunc
Expand Down
71 changes: 71 additions & 0 deletions hie-plugin-api/Haskell/Ide/Engine/PluginApi.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
-- | This module provides an API that software intented to be
-- integrated into HIE can use, so that they can make use of the
-- shared BIOS features.

{-
-- Stuff used in HaRe currently
Options(..)
defaultOptions
GmModuleGraph(..)
ModulePath(..)
GmComponent(..)
GmComponentType(..)

CachedInfo(..)
HasGhcModuleCache(..)
IdeGhcM

cabalModuleGraphs
filePathToUri
makeRevRedirMapFunc

MonadIO(..)
ifCachedModule
runIdeGhcMBare
setTypecheckedModule
-}


module Haskell.Ide.Engine.PluginApi
(
-- ** Re-exported from ghc-mod via ghc-project-types
GP.GmModuleGraph(..)
, GP.ModulePath(..)
, GP.GmComponent(..)
, GP.GmComponentType(..)

-- * IDE monads
, HIE.IdeState(..)
, HIE.IdeGhcM
, HIE.runIdeGhcM
, HIE.runIdeGhcMBare
, HIE.IdeM
, HIE.runIdeM
, HIE.IdeDeferM
, HIE.MonadIde
, HIE.iterT
, HIE.LiftsToGhc(..)
, HIE.HasGhcModuleCache(..)
, HIE.cabalModuleGraphs
, HIE.makeRevRedirMapFunc
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this meant to be a one-stop import for all plugins? If so should this also export the withCachedModule etc. functions and other helpers?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bubba the initial intention is to create as narrow as possible an API to support what HaRe needs, as a way to end up with a real world requirement on hie-bios.

And I think there possibly still needs to be a discussion about what the layers are for a plugin.

My mental model for an external tool XXX currently has it something like

  • hie
  • plugin within hie to intergrate XXX to hie/LSP
  • XXX as a standalone tool
  • part of hie exposed to interact with GHC etc, and allow the tool to be a good citizen in hie, but also to be able to run standalone.

So this file is currently aiming at the last of those, which is effectively the hie-bios. I think.

But this whole layering is something that needs discussion, I think.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps we could view HIE as having two parts:

  • A library for running long-lived sessions that provide access to processed information from GHC, in a relatively bulletproof fashion (i.e. using the hie-bios to work out how to talk to GHC)
    • Plugins could code against this library, and it would then also provide a way to run those tools in a relatively robust way. But they wouldn't have to have a dependency on a LSP client.
  • A Haskell LSP client which uses the session library and manages plugins.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@michaelpj Do you mean LSP server in your comment above? The client is the IDE that talks to hie.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Er yes, sorry 🤦‍♂️

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And the intended breakdown you describe is the driver behind having hie-plugin-api which should play the role described in your first bullet point, and does so for HaRe using this branch and https://github.com/alanz/HaRe/tree/hie-plugin-api.

But I can also envisage another layer, where a third party could package something like HaRe for hie, which can then be simply included in the plugin list in app/MainHie.hs and be usable.

So my original discussion point is whether we need to distinguish between the 'upper' and 'lower' APIs. Especially as I think there is likely to be some level of crossover in it.


-- * Using the HIE module cache etc
, HIE.setTypecheckedModule
, HIE.Diagnostics
, HIE.AdditionalErrs
, LSP.filePathToUri
, HIE.ifCachedModule
, HIE.CachedInfo(..)

-- * used for tests in HaRe
, HIE.BiosLogLevel(..)
, HIE.BiosOptions(..)
, HIE.defaultOptions
) where

import qualified GhcProject.Types as GP
import qualified Haskell.Ide.Engine.Ghc as HIE
import qualified Haskell.Ide.Engine.GhcModuleCache as HIE (CachedInfo(..),HasGhcModuleCache(..))
import qualified Haskell.Ide.Engine.ModuleCache as HIE (ifCachedModule)
import qualified Haskell.Ide.Engine.PluginsIdeMonads as HIE
import qualified Language.Haskell.LSP.Types as LSP ( filePathToUri )
4 changes: 3 additions & 1 deletion hie-plugin-api/Haskell/Ide/Engine/PluginUtils.hs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ srcSpan2Range :: SrcSpan -> Either T.Text Range
srcSpan2Range spn =
realSrcSpan2Range <$> getRealSrcSpan spn



reverseMapFile :: MonadIO m => (FilePath -> FilePath) -> FilePath -> m FilePath
reverseMapFile rfm fp = do
fp' <- liftIO $ canonicalizePath fp
Expand Down Expand Up @@ -288,4 +290,4 @@ rangeLinesFromVfs (VirtualFile _ yitext) (Range (Position lf _cf) (Position lt _
where
(_ ,s1) = Yi.splitAtLine lf yitext
(s2, _) = Yi.splitAtLine (lt - lf) s1
r = Yi.toText s2
r = Yi.toText s2
Loading