|
| 1 | +{-# LANGUAGE DeriveAnyClass #-} |
| 2 | +{-# LANGUAGE DeriveGeneric #-} |
| 3 | +{-# LANGUAGE LambdaCase #-} |
| 4 | +{-# LANGUAGE OverloadedStrings #-} |
| 5 | +{-# LANGUAGE TupleSections #-} |
| 6 | +{-# LANGUAGE TypeApplications #-} |
| 7 | +{-# LANGUAGE ViewPatterns #-} |
| 8 | + |
| 9 | +-- | A plugin that uses tactics to synthesize code |
| 10 | +module Ide.Plugin.Tactic |
| 11 | + ( descriptor |
| 12 | + , tacticTitle |
| 13 | + , TacticCommand (..) |
| 14 | + ) where |
| 15 | + |
| 16 | +import Control.Monad |
| 17 | +import Control.Monad.Trans |
| 18 | +import Control.Monad.Trans.Maybe |
| 19 | +import Data.Aeson |
| 20 | +import Data.Coerce |
| 21 | +import qualified Data.Map as M |
| 22 | +import qualified Data.Set as S |
| 23 | +import Data.Maybe |
| 24 | +import qualified Data.Text as T |
| 25 | +import Data.Traversable |
| 26 | +import Development.IDE.Core.PositionMapping |
| 27 | +import Development.IDE.Core.RuleTypes |
| 28 | +import Development.IDE.Core.Service (runAction) |
| 29 | +import Development.IDE.Core.Shake (useWithStale, IdeState (..)) |
| 30 | +import Development.IDE.GHC.Compat |
| 31 | +import Development.IDE.GHC.Error (realSrcSpanToRange) |
| 32 | +import Development.IDE.GHC.Util (hscEnv) |
| 33 | +import Development.Shake (Action) |
| 34 | +import GHC.Generics (Generic) |
| 35 | +import HscTypes (hsc_dflags) |
| 36 | +import Ide.Plugin (mkLspCommand) |
| 37 | +import Ide.Plugin.Tactic.Machinery |
| 38 | +import Ide.Plugin.Tactic.Tactics |
| 39 | +import Ide.Plugin.Tactic.Types |
| 40 | +import Ide.TreeTransform (transform, graft, useAnnotatedSource) |
| 41 | +import Ide.Types |
| 42 | +import Language.Haskell.LSP.Core (clientCapabilities) |
| 43 | +import Language.Haskell.LSP.Types |
| 44 | +import OccName |
| 45 | +import qualified FastString |
| 46 | + |
| 47 | + |
| 48 | +descriptor :: PluginId -> PluginDescriptor |
| 49 | +descriptor plId = (defaultPluginDescriptor plId) |
| 50 | + { pluginCommands |
| 51 | + = fmap (\tc -> |
| 52 | + PluginCommand |
| 53 | + (tcCommandId tc) |
| 54 | + (tacticDesc $ tcCommandName tc) |
| 55 | + (tacticCmd $ commandTactic tc)) |
| 56 | + enabledTactics |
| 57 | + , pluginCodeActionProvider = Just codeActionProvider |
| 58 | + } |
| 59 | + |
| 60 | +tacticDesc :: T.Text -> T.Text |
| 61 | +tacticDesc name = "fill the hole using the " <> name <> " tactic" |
| 62 | + |
| 63 | +------------------------------------------------------------------------------ |
| 64 | + |
| 65 | +enabledTactics :: [TacticCommand] |
| 66 | +enabledTactics = [Intros, Destruct, Homomorphism] |
| 67 | + |
| 68 | +------------------------------------------------------------------------------ |
| 69 | +-- | A 'TacticProvider' is a way of giving context-sensitive actions to the LS |
| 70 | +-- UI. |
| 71 | +type TacticProvider = PluginId -> Uri -> Range -> Judgement -> IO [CAResult] |
| 72 | + |
| 73 | + |
| 74 | +------------------------------------------------------------------------------ |
| 75 | +-- | Construct a 'CommandId' |
| 76 | +tcCommandId :: TacticCommand -> CommandId |
| 77 | +tcCommandId c = coerce $ T.pack $ "tactics" <> show c <> "Command" |
| 78 | + |
| 79 | + |
| 80 | +------------------------------------------------------------------------------ |
| 81 | +-- | The name of the command for the LS. |
| 82 | +tcCommandName :: TacticCommand -> T.Text |
| 83 | +tcCommandName = T.pack . show |
| 84 | + |
| 85 | +------------------------------------------------------------------------------ |
| 86 | +-- | Mapping from tactic commands to their contextual providers. See 'provide', |
| 87 | +-- 'filterGoalType' and 'filterBindingType' for the nitty gritty. |
| 88 | +commandProvider :: TacticCommand -> TacticProvider |
| 89 | +commandProvider Auto = provide Auto "" |
| 90 | +commandProvider Split = provide Split "" |
| 91 | +commandProvider Intro = |
| 92 | + filterGoalType isFunction $ |
| 93 | + provide Intro "" |
| 94 | +commandProvider Intros = |
| 95 | + filterGoalType isFunction $ |
| 96 | + provide Intros "" |
| 97 | +commandProvider Destruct = |
| 98 | + filterBindingType destructFilter $ \occ _ -> |
| 99 | + provide Destruct $ T.pack $ occNameString occ |
| 100 | +commandProvider Homomorphism = |
| 101 | + filterBindingType homoFilter $ \occ _ -> |
| 102 | + provide Homomorphism $ T.pack $ occNameString occ |
| 103 | + |
| 104 | + |
| 105 | +------------------------------------------------------------------------------ |
| 106 | +-- | A mapping from tactic commands to actual tactics for refinery. |
| 107 | +commandTactic :: TacticCommand -> OccName -> TacticsM () |
| 108 | +commandTactic Auto = const auto |
| 109 | +commandTactic Split = const split |
| 110 | +commandTactic Intro = const intro |
| 111 | +commandTactic Intros = const intros |
| 112 | +commandTactic Destruct = destruct |
| 113 | +commandTactic Homomorphism = homo |
| 114 | + |
| 115 | + |
| 116 | +------------------------------------------------------------------------------ |
| 117 | +-- | We should show homos only when the goal type is the same as the binding |
| 118 | +-- type, and that both are usual algebraic types. |
| 119 | +homoFilter :: Type -> Type -> Bool |
| 120 | +homoFilter (algebraicTyCon -> Just t1) (algebraicTyCon -> Just t2) = t1 == t2 |
| 121 | +homoFilter _ _ = False |
| 122 | + |
| 123 | + |
| 124 | +------------------------------------------------------------------------------ |
| 125 | +-- | We should show destruct for bindings only when those bindings have usual |
| 126 | +-- algebraic types. |
| 127 | +destructFilter :: Type -> Type -> Bool |
| 128 | +destructFilter _ (algebraicTyCon -> Just _) = True |
| 129 | +destructFilter _ _ = False |
| 130 | + |
| 131 | + |
| 132 | +runIde :: IdeState -> Action a -> IO a |
| 133 | +runIde state = runAction "tactic" state |
| 134 | + |
| 135 | + |
| 136 | +codeActionProvider :: CodeActionProvider |
| 137 | +codeActionProvider _conf state plId (TextDocumentIdentifier uri) range _ctx |
| 138 | + | Just nfp <- uriToNormalizedFilePath $ toNormalizedUri uri = |
| 139 | + fromMaybeT (Right $ List []) $ do |
| 140 | + (_, span, jdg) <- MaybeT $ judgementForHole state nfp range |
| 141 | + actions <- lift $ |
| 142 | + -- This foldMap is over the function monoid. |
| 143 | + foldMap commandProvider enabledTactics |
| 144 | + plId |
| 145 | + uri |
| 146 | + span |
| 147 | + jdg |
| 148 | + pure $ Right $ List actions |
| 149 | +codeActionProvider _ _ _ _ _ _ = pure $ Right $ codeActions [] |
| 150 | + |
| 151 | + |
| 152 | +codeActions :: [CodeAction] -> List CAResult |
| 153 | +codeActions = List . fmap CACodeAction |
| 154 | + |
| 155 | + |
| 156 | +------------------------------------------------------------------------------ |
| 157 | +-- | Terminal constructor for providing context-sensitive tactics. Tactics |
| 158 | +-- given by 'provide' are always available. |
| 159 | +provide :: TacticCommand -> T.Text -> TacticProvider |
| 160 | +provide tc name plId uri range _ = do |
| 161 | + let title = tacticTitle tc name |
| 162 | + params = TacticParams { file = uri , range = range , var_name = name } |
| 163 | + cmd <- mkLspCommand plId (tcCommandId tc) title (Just [toJSON params]) |
| 164 | + pure |
| 165 | + $ pure |
| 166 | + $ CACodeAction |
| 167 | + $ CodeAction title (Just CodeActionQuickFix) Nothing Nothing |
| 168 | + $ Just cmd |
| 169 | + |
| 170 | + |
| 171 | +------------------------------------------------------------------------------ |
| 172 | +-- | Restrict a 'TacticProvider', making sure it appears only when the given |
| 173 | +-- predicate holds for the goal. |
| 174 | +filterGoalType :: (Type -> Bool) -> TacticProvider -> TacticProvider |
| 175 | +filterGoalType p tp plId uri range jdg@(Judgement _ (CType g)) = |
| 176 | + case p g of |
| 177 | + True -> tp plId uri range jdg |
| 178 | + False -> pure [] |
| 179 | + |
| 180 | + |
| 181 | +------------------------------------------------------------------------------ |
| 182 | +-- | Multiply a 'TacticProvider' for each binding, making sure it appears only |
| 183 | +-- when the given predicate holds over the goal and binding types. |
| 184 | +filterBindingType |
| 185 | + :: (Type -> Type -> Bool) -- ^ Goal and then binding types. |
| 186 | + -> (OccName -> Type -> TacticProvider) |
| 187 | + -> TacticProvider |
| 188 | +filterBindingType p tp plId uri range jdg@(Judgement hys (CType g)) = |
| 189 | + fmap join $ for (M.toList hys) $ \(occ, CType ty) -> |
| 190 | + case p g ty of |
| 191 | + True -> tp occ ty plId uri range jdg |
| 192 | + False -> pure [] |
| 193 | + |
| 194 | + |
| 195 | +data TacticParams = TacticParams |
| 196 | + { file :: Uri -- ^ Uri of the file to fill the hole in |
| 197 | + , range :: Range -- ^ The range of the hole |
| 198 | + , var_name :: T.Text |
| 199 | + } |
| 200 | + deriving (Show, Eq, Generic, ToJSON, FromJSON) |
| 201 | + |
| 202 | + |
| 203 | +------------------------------------------------------------------------------ |
| 204 | +-- | Find the last typechecked module, and find the most specific span, as well |
| 205 | +-- as the judgement at the given range. |
| 206 | +judgementForHole |
| 207 | + :: IdeState |
| 208 | + -> NormalizedFilePath |
| 209 | + -> Range |
| 210 | + -> IO (Maybe (PositionMapping, Range, Judgement)) |
| 211 | +judgementForHole state nfp range = runMaybeT $ do |
| 212 | + (asts, amapping) <- MaybeT $ runIde state $ useWithStale GetHieAst nfp |
| 213 | + range' <- liftMaybe $ fromCurrentRange amapping range |
| 214 | + |
| 215 | + (binds, _) <- MaybeT $ runIde state $ useWithStale GetBindings nfp |
| 216 | + |
| 217 | + (rss, goal) <- liftMaybe $ join $ listToMaybe $ M.elems $ flip M.mapWithKey (getAsts $ hieAst asts) $ \fs ast -> |
| 218 | + case selectSmallestContaining (rangeToRealSrcSpan (FastString.unpackFS fs) range') ast of |
| 219 | + Nothing -> Nothing |
| 220 | + Just ast' -> do |
| 221 | + let info = nodeInfo ast' |
| 222 | + ty <- listToMaybe $ nodeType info |
| 223 | + guard $ ("HsUnboundVar","HsExpr") `S.member` nodeAnnotations info |
| 224 | + pure (nodeSpan ast', ty) |
| 225 | + |
| 226 | + resulting_range <- liftMaybe $ toCurrentRange amapping $ realSrcSpanToRange rss |
| 227 | + |
| 228 | + let hyps = hypothesisFromBindings rss binds |
| 229 | + pure (amapping, resulting_range, Judgement hyps $ CType goal) |
| 230 | + |
| 231 | + |
| 232 | +tacticCmd :: (OccName -> TacticsM ()) -> CommandFunction TacticParams |
| 233 | +tacticCmd tac lf state (TacticParams uri range var_name) |
| 234 | + | Just nfp <- uriToNormalizedFilePath $ toNormalizedUri uri = |
| 235 | + fromMaybeT (Right Null, Nothing) $ do |
| 236 | + (pos, _, jdg) <- MaybeT $ judgementForHole state nfp range |
| 237 | + -- Ok to use the stale 'ModIface', since all we need is its 'DynFlags' |
| 238 | + -- which don't change very often. |
| 239 | + (hscenv, _) <- MaybeT $ runIde state $ useWithStale GhcSession nfp |
| 240 | + let dflags = hsc_dflags $ hscEnv hscenv |
| 241 | + pm <- MaybeT $ useAnnotatedSource "tacticsCmd" state nfp |
| 242 | + case runTactic jdg |
| 243 | + $ tac |
| 244 | + $ mkVarOcc |
| 245 | + $ T.unpack var_name of |
| 246 | + Left err -> |
| 247 | + pure $ (, Nothing) |
| 248 | + $ Left |
| 249 | + $ ResponseError InvalidRequest (T.pack $ show err) Nothing |
| 250 | + Right res -> do |
| 251 | + range' <- liftMaybe $ toCurrentRange pos range |
| 252 | + let span = rangeToSrcSpan (fromNormalizedFilePath nfp) range' |
| 253 | + g = graft span res |
| 254 | + let response = transform dflags (clientCapabilities lf) uri g pm |
| 255 | + pure $ case response of |
| 256 | + Right res -> (Right Null , Just (WorkspaceApplyEdit, ApplyWorkspaceEditParams res)) |
| 257 | + Left err -> (Left $ ResponseError InternalError (T.pack err) Nothing, Nothing) |
| 258 | +tacticCmd _ _ _ _ = |
| 259 | + pure ( Left $ ResponseError InvalidRequest (T.pack "Bad URI") Nothing |
| 260 | + , Nothing |
| 261 | + ) |
| 262 | + |
| 263 | + |
| 264 | +fromMaybeT :: Functor m => a -> MaybeT m a -> m a |
| 265 | +fromMaybeT def = fmap (fromMaybe def) . runMaybeT |
| 266 | + |
| 267 | +liftMaybe :: Monad m => Maybe a -> MaybeT m a |
| 268 | +liftMaybe a = MaybeT $ pure a |
| 269 | + |
0 commit comments