Skip to content

Emit holes as diagnostics #1653

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
Apr 9, 2021
Merged
21 changes: 20 additions & 1 deletion ghcide/src/Development/IDE/Core/Rules.hs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ module Development.IDE.Core.Rules(
getParsedModule,
getParsedModuleWithComments,
getClientConfigAction,
usePropertyAction,
-- * Rules
CompiledLinkables(..),
IsHiFileStable(..),
Expand Down Expand Up @@ -139,7 +140,12 @@ import Language.LSP.Types (SMethod (SCustomM
import Language.LSP.VFS
import Module
import TcRnMonad (tcg_dependent_files)
import Control.Applicative

import Ide.Plugin.Properties (HasProperty, KeyNameProxy, Properties, ToHsType, useProperty)
import Ide.Types (PluginId)
import Data.Default (def)
import Ide.PluginUtils (configForPlugin)
import Control.Applicative

-- | This is useful for rules to convert rules that can only produce errors or
-- a result into the more general IdeResult type that supports producing
Expand Down Expand Up @@ -940,6 +946,19 @@ getClientConfigAction defValue = do
Just (Success c) -> return c
_ -> return defValue

usePropertyAction ::
(HasProperty s k t r) =>
KeyNameProxy s ->
PluginId ->
Properties r ->
Action (ToHsType t)
usePropertyAction kn plId p = do
config <- getClientConfigAction def
let pluginConfig = configForPlugin config plId
pure $ useProperty kn p $ plcConfig pluginConfig

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

-- | For now we always use bytecode unless something uses unboxed sums and tuples along with TH
getLinkableType :: NormalizedFilePath -> Action (Maybe LinkableType)
getLinkableType f = use_ NeedsCompilation f
Expand Down
1 change: 1 addition & 0 deletions plugins/hls-tactics-plugin/hls-tactics-plugin.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ library
, text
, transformers
, deepseq
, unordered-containers

default-language: Haskell2010
default-extensions:
Expand Down
80 changes: 77 additions & 3 deletions plugins/hls-tactics-plugin/src/Wingman/LanguageServer.hs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{-# LANGUAGE CPP #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeFamilies #-}

module Wingman.LanguageServer where

Expand All @@ -12,25 +13,30 @@ import Data.Coerce
import Data.Functor ((<&>))
import Data.Generics.Aliases (mkQ)
import Data.Generics.Schemes (everything)
import qualified Data.HashMap.Strict as Map
import Data.IORef (readIORef)
import qualified Data.Map as M
import Data.Maybe
import Data.Monoid
import qualified Data.Set as S
import qualified Data.Text as T
import Data.Traversable
import Development.IDE (getFilesOfInterest, ShowDiagnostic (ShowDiag), srcSpanToRange)
import Development.IDE (hscEnv)
import Development.IDE.Core.PositionMapping
import Development.IDE.Core.RuleTypes
import Development.IDE.Core.Rules (usePropertyAction)
import Development.IDE.Core.Service (runAction)
import Development.IDE.Core.Shake (IdeState (..), use)
import Development.IDE.Core.Shake (IdeState (..), uses, define, use)
import qualified Development.IDE.Core.Shake as IDE
import Development.IDE.Core.UseStale
import Development.IDE.GHC.Compat
import Development.IDE.GHC.Error (realSrcSpanToRange)
import Development.IDE.Spans.LocalBindings (Bindings, getDefiningBindings)
import Development.Shake (Action, RuleResult)
import Development.Shake (Action, RuleResult, Rules, action)
import Development.Shake.Classes (Typeable, Binary, Hashable, NFData)
import qualified FastString
import GHC.Generics (Generic)
import GhcPlugins (tupleDataCon, consDataCon, substTyAddInScope, ExternalPackageState, HscEnv (hsc_EPS), liftIO)
import qualified Ide.Plugin.Config as Plugin
import Ide.Plugin.Properties
Expand Down Expand Up @@ -109,7 +115,8 @@ unsafeRunStaleIde state nfp a = do
------------------------------------------------------------------------------

properties :: Properties
'[ 'PropertyKey "max_use_ctor_actions" 'TInteger
'[ 'PropertyKey "hole_severity" ('TEnum (Maybe DiagnosticSeverity))
, 'PropertyKey "max_use_ctor_actions" 'TInteger
, 'PropertyKey "features" 'TString
, 'PropertyKey "timeout_duration" 'TInteger
]
Expand All @@ -120,6 +127,15 @@ properties = emptyProperties
"Feature set used by Wingman" ""
& defineIntegerProperty #max_use_ctor_actions
"Maximum number of `Use constructor <x>` code actions that can appear" 5
& defineEnumProperty #hole_severity
"The severity to use when showing hole diagnostics. These are noisy, but some editors don't allow jumping to all severities."
[ (Just DsError, "error")
, (Just DsWarning, "warning")
, (Just DsInfo, "info")
, (Just DsHint, "hint")
, (Nothing, "none")
]
Nothing


-- | Get the the plugin config
Expand Down Expand Up @@ -421,3 +437,61 @@ mkShowMessageParams ufm = ShowMessageParams (ufmSeverity ufm) $ T.pack $ show uf
showLspMessage :: MonadLsp cfg m => ShowMessageParams -> m ()
showLspMessage = sendNotification SWindowShowMessage


-- This rule only exists for generating file diagnostics
-- so the RuleResult is empty
data WriteDiagnostics = WriteDiagnostics
deriving (Eq, Show, Typeable, Generic)

instance Hashable WriteDiagnostics
instance NFData WriteDiagnostics
instance Binary WriteDiagnostics

type instance RuleResult WriteDiagnostics = ()

wingmanRules :: PluginId -> Rules ()
wingmanRules plId = do
define $ \WriteDiagnostics nfp ->
usePropertyAction #hole_severity plId properties >>= \case
Nothing -> pure (mempty, Just ())
Just severity ->
use GetParsedModule nfp >>= \case
Nothing ->
pure ([], Nothing)
Just pm -> do
let holes :: [Range]
holes =
everything (<>)
(mkQ mempty $ \case
L span (HsVar _ (L _ name))
| isHole (occName name) ->
maybeToList $ srcSpanToRange span
L span (HsUnboundVar _ (TrueExprHole occ))
| isHole occ ->
maybeToList $ srcSpanToRange span
#if __GLASGOW_HASKELL__ <= 808
L span (EWildPat _) ->
maybeToList $ srcSpanToRange span
#endif
(_ :: LHsExpr GhcPs) -> mempty
) $ pm_parsed_source pm
pure
( fmap (\r -> (nfp, ShowDiag, mkDiagnostic severity r)) holes
, Just ()
)

action $ do
files <- getFilesOfInterest
void $ uses WriteDiagnostics $ Map.keys files
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 really necessary? It will be executed on every single keystroke, so please be very sure about it.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Happy to get suggestions here; this is just cargo culted from hlint.

Copy link
Collaborator

Choose a reason for hiding this comment

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

What is the desired purpose? Do you want to refresh the code action diagnostics on every keystroke, or only when code actions are invoked?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I must admit I'm in over my head here. I'd like a workflow that allows for refining holes by hand and via Wingman, that makes it easy to get to the next hole.

My original thought was to just automatically move to the next hole after running a code action, but LSP doesn't support that. And so if users need to do the movement themselves, it'd be nice if it worked under all circumstances.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Have you seen this: haskell/ghcide#889

The idea is that you can hook into GHC to record all the holes in the program while its being typechecked, no need to do a potentially slow SYB style traversal afterwards.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

That's embarrassing. I had them disabled in my test project for some reason. Thanks. How can I get the existing diagnostics?

Copy link
Collaborator

Choose a reason for hiding this comment

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

You might want this:

getDiagnostics :: IdeState -> IO [FileDiagnostic]
getDiagnostics IdeState{shakeExtras = ShakeExtras{diagnostics}} = do
val <- readVar diagnostics
return $ getAllDiagnostics val

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I'm confused about the flow here, and worried about races.

getDiagnostics above requires an IdeState, which AFAICT isn't available inside of the shake Action where the new diagnostics need to be generated. Easy enough to work around this by mucking with the internals, but then getDiagnostics just reads a mutable variable. Without a proper rule that only fires when the diagnostics have changed, to ensure the other diagnostics get generated before I look at them, it seems like I'm just inviting race conditions.

But since diagnostics are generated (exclusively?) by rules, and that this new one will produce new diagnostics, it feels like this approach won't work. Unless I'm missing something obvious?

Copy link
Collaborator

Choose a reason for hiding this comment

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

No, you are right. The diagnostic store is mutable, and getDiagnostics will only read whatever is in there.

But that said, I think you could add a dependency on the Typecheck rule, and then I believe getDiagnostics will find the Typecheck diagnostics in there.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

That makes sense, thanks!



mkDiagnostic :: DiagnosticSeverity -> Range -> Diagnostic
mkDiagnostic severity r =
Diagnostic r
(Just severity)
(Just $ InR "hole")
(Just "wingman")
"Hole"
(Just $ List [DtUnnecessary])
Nothing

1 change: 1 addition & 0 deletions plugins/hls-tactics-plugin/src/Wingman/Plugin.hs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ descriptor plId = (defaultPluginDescriptor plId)
[minBound .. maxBound]
, pluginHandlers =
mkPluginHandler STextDocumentCodeAction codeActionProvider
, pluginRules = wingmanRules plId
, pluginCustomConfig =
mkCustomConfig properties
}
Expand Down