|
1 | | -{-# LANGUAGE CPP #-} |
2 | | -{-# LANGUAGE DeriveAnyClass #-} |
3 | | -{-# LANGUAGE DeriveGeneric #-} |
4 | | -{-# LANGUAGE LambdaCase #-} |
5 | | -{-# LANGUAGE OverloadedStrings #-} |
6 | | -{-# LANGUAGE RecordWildCards #-} |
7 | | -{-# LANGUAGE TypeFamilies #-} |
8 | | -{-# LANGUAGE ViewPatterns #-} |
9 | | -module Ide.Plugin.Class |
10 | | - ( descriptor, |
11 | | - Log (..) |
12 | | - ) where |
| 1 | +module Ide.Plugin.Class (descriptor, Log(..)) where |
13 | 2 |
|
14 | | -import Control.Applicative |
15 | | -import Control.Lens hiding (List, use) |
16 | | -import Control.Monad |
17 | | -import Control.Monad.IO.Class |
18 | | -import Control.Monad.Trans.Class |
19 | | -import Control.Monad.Trans.Maybe |
20 | | -import Data.Aeson |
21 | | -import Data.Char |
22 | | -import Data.Either (rights) |
23 | | -import Data.List |
24 | | -import qualified Data.Map.Strict as Map |
25 | | -import Data.Maybe |
26 | | -import qualified Data.Set as Set |
27 | | -import qualified Data.Text as T |
28 | | -import Development.IDE hiding (pluginHandlers) |
29 | | -import Development.IDE.Core.PositionMapping (fromCurrentRange, |
30 | | - toCurrentRange) |
31 | | -import Development.IDE.GHC.Compat as Compat hiding (locA, |
32 | | - (<+>)) |
33 | | -import Development.IDE.GHC.Compat.Util |
34 | | -import Development.IDE.Spans.AtPoint |
35 | | -import qualified GHC.Generics as Generics |
36 | | -import Ide.PluginUtils |
| 3 | +import Development.IDE (IdeState, Recorder, WithPriority) |
| 4 | +import Ide.Plugin.Class.CodeAction |
| 5 | +import Ide.Plugin.Class.CodeLens |
| 6 | +import Ide.Plugin.Class.Types |
37 | 7 | import Ide.Types |
38 | | -import Language.Haskell.GHC.ExactPrint |
39 | | -import Language.Haskell.GHC.ExactPrint.Parsers (parseDecl) |
40 | | -import Language.Haskell.GHC.ExactPrint.Types hiding (GhcPs, Parens) |
41 | | -import Language.Haskell.GHC.ExactPrint.Utils (rs) |
42 | | -import Language.LSP.Server |
43 | 8 | import Language.LSP.Types |
44 | | -import qualified Language.LSP.Types.Lens as J |
45 | | - |
46 | | -#if MIN_VERSION_ghc(9,2,0) |
47 | | -import GHC.Hs (AnnsModule (AnnsModule)) |
48 | | -import GHC.Parser.Annotation |
49 | | -#endif |
50 | | - |
51 | | -data Log |
52 | | - = LogImplementedMethods Class [T.Text] |
53 | | - |
54 | | -instance Pretty Log where |
55 | | - pretty = \case |
56 | | - LogImplementedMethods cls methods -> |
57 | | - pretty ("Detected implmented methods for class" :: String) |
58 | | - <+> pretty (show (getOccString cls) <> ":") -- 'show' is used here to add quotes around the class name |
59 | | - <+> pretty methods |
60 | 9 |
|
61 | 10 | descriptor :: Recorder (WithPriority Log) -> PluginId -> PluginDescriptor IdeState |
62 | 11 | descriptor recorder plId = (defaultPluginDescriptor plId) |
63 | | - { pluginCommands = commands |
64 | | - , pluginHandlers = mkPluginHandler STextDocumentCodeAction (codeAction recorder) |
65 | | - } |
66 | | - |
67 | | -commands :: [PluginCommand IdeState] |
68 | | -commands |
69 | | - = [ PluginCommand "addMinimalMethodPlaceholders" "add placeholders for minimal methods" addMethodPlaceholders |
| 12 | + { pluginCommands = commands plId |
| 13 | + , pluginRules = rules recorder |
| 14 | + , pluginHandlers = mkPluginHandler STextDocumentCodeAction (codeAction recorder) |
| 15 | + <> mkPluginHandler STextDocumentCodeLens codeLens |
| 16 | + , pluginConfigDescriptor = |
| 17 | + defaultConfigDescriptor { configCustomConfig = mkCustomConfig properties } |
| 18 | + } |
| 19 | + |
| 20 | +commands :: PluginId -> [PluginCommand IdeState] |
| 21 | +commands plId |
| 22 | + = [ PluginCommand codeActionCommandId |
| 23 | + "add placeholders for minimal methods" (addMethodPlaceholders plId) |
| 24 | + , PluginCommand typeLensCommandId |
| 25 | + "add type signatures for instance methods" codeLensCommandHandler |
70 | 26 | ] |
71 | | - |
72 | | --- | Parameter for the addMethods PluginCommand. |
73 | | -data AddMinimalMethodsParams = AddMinimalMethodsParams |
74 | | - { uri :: Uri |
75 | | - , range :: Range |
76 | | - , methodGroup :: List T.Text |
77 | | - } |
78 | | - deriving (Show, Eq, Generics.Generic, ToJSON, FromJSON) |
79 | | - |
80 | | -addMethodPlaceholders :: CommandFunction IdeState AddMinimalMethodsParams |
81 | | -addMethodPlaceholders state AddMinimalMethodsParams{..} = do |
82 | | - caps <- getClientCapabilities |
83 | | - medit <- liftIO $ runMaybeT $ do |
84 | | - docPath <- MaybeT . pure . uriToNormalizedFilePath $ toNormalizedUri uri |
85 | | - pm <- MaybeT . runAction "classplugin" state $ use GetParsedModule docPath |
86 | | - (hsc_dflags . hscEnv -> df) <- MaybeT . runAction "classplugin" state $ use GhcSessionDeps docPath |
87 | | - (old, new) <- makeEditText pm df |
88 | | - pure (workspaceEdit caps old new) |
89 | | - |
90 | | - forM_ medit $ \edit -> |
91 | | - sendRequest SWorkspaceApplyEdit (ApplyWorkspaceEditParams Nothing edit) (\_ -> pure ()) |
92 | | - pure (Right Null) |
93 | | - where |
94 | | - indent = 2 |
95 | | - |
96 | | - workspaceEdit caps old new |
97 | | - = diffText caps (uri, old) new IncludeDeletions |
98 | | - |
99 | | - toMethodName n |
100 | | - | Just (h, _) <- T.uncons n |
101 | | - , not (isAlpha h || h == '_') |
102 | | - = "(" <> n <> ")" |
103 | | - | otherwise |
104 | | - = n |
105 | | - |
106 | | -#if MIN_VERSION_ghc(9,2,0) |
107 | | - makeEditText pm df = do |
108 | | - List mDecls <- MaybeT . pure $ traverse (makeMethodDecl df) methodGroup |
109 | | - let ps = makeDeltaAst $ pm_parsed_source pm |
110 | | - old = T.pack $ exactPrint ps |
111 | | - (ps', _, _) = runTransform (addMethodDecls ps mDecls) |
112 | | - new = T.pack $ exactPrint ps' |
113 | | - pure (old, new) |
114 | | - |
115 | | - makeMethodDecl df mName = |
116 | | - either (const Nothing) Just . parseDecl df (T.unpack mName) . T.unpack |
117 | | - $ toMethodName mName <> " = _" |
118 | | - |
119 | | - addMethodDecls ps mDecls = do |
120 | | - allDecls <- hsDecls ps |
121 | | - let (before, ((L l inst): after)) = break (containRange range . getLoc) allDecls |
122 | | - replaceDecls ps (before ++ (L l (addWhere inst)): (map newLine mDecls ++ after)) |
123 | | - where |
124 | | - -- Add `where` keyword for `instance X where` if `where` is missing. |
125 | | - -- |
126 | | - -- The `where` in ghc-9.2 is now stored in the instance declaration |
127 | | - -- directly. More precisely, giving an `HsDecl GhcPs`, we have: |
128 | | - -- InstD --> ClsInstD --> ClsInstDecl --> XCClsInstDecl --> (EpAnn [AddEpAnn], AnnSortKey), |
129 | | - -- here `AnnEpAnn` keeps the track of Anns. |
130 | | - -- |
131 | | - -- See the link for the original definition: |
132 | | - -- https://hackage.haskell.org/package/ghc-9.2.1/docs/Language-Haskell-Syntax-Extension.html#t:XCClsInstDecl |
133 | | - addWhere (InstD xInstD (ClsInstD ext decl@ClsInstDecl{..})) = |
134 | | - let ((EpAnn entry anns comments), key) = cid_ext |
135 | | - in InstD xInstD (ClsInstD ext decl { |
136 | | - cid_ext = (EpAnn |
137 | | - entry |
138 | | - (AddEpAnn AnnWhere (EpaDelta (SameLine 1) []) : anns) |
139 | | - comments |
140 | | - , key) |
141 | | - }) |
142 | | - addWhere decl = decl |
143 | | - |
144 | | - newLine (L l e) = |
145 | | - let dp = deltaPos 1 indent |
146 | | - in L (noAnnSrcSpanDP (locA l) dp <> l) e |
147 | | - |
148 | | -#else |
149 | | - makeEditText pm df = do |
150 | | - List (unzip -> (mAnns, mDecls)) <- MaybeT . pure $ traverse (makeMethodDecl df) methodGroup |
151 | | - let ps = pm_parsed_source pm |
152 | | - anns = relativiseApiAnns ps (pm_annotations pm) |
153 | | - old = T.pack $ exactPrint ps anns |
154 | | - (ps', (anns', _), _) = runTransform (mergeAnns (mergeAnnList mAnns) anns) (addMethodDecls ps mDecls) |
155 | | - new = T.pack $ exactPrint ps' anns' |
156 | | - pure (old, new) |
157 | | - |
158 | | - makeMethodDecl df mName = |
159 | | - case parseDecl df (T.unpack mName) . T.unpack $ toMethodName mName <> " = _" of |
160 | | - Right (ann, d) -> Just (setPrecedingLines d 1 indent ann, d) |
161 | | - Left _ -> Nothing |
162 | | - |
163 | | - addMethodDecls ps mDecls = do |
164 | | - d <- findInstDecl ps |
165 | | - newSpan <- uniqueSrcSpanT |
166 | | - let |
167 | | - annKey = mkAnnKey d |
168 | | - newAnnKey = AnnKey (rs newSpan) (CN "HsValBinds") |
169 | | - addWhere mkds@(Map.lookup annKey -> Just ann) |
170 | | - = Map.insert newAnnKey ann2 mkds2 |
171 | | - where |
172 | | - ann1 = ann |
173 | | - { annsDP = annsDP ann ++ [(G AnnWhere, DP (0, 1))] |
174 | | - , annCapturedSpan = Just newAnnKey |
175 | | - , annSortKey = Just (fmap (rs . getLoc) mDecls) |
176 | | - } |
177 | | - mkds2 = Map.insert annKey ann1 mkds |
178 | | - ann2 = annNone |
179 | | - { annEntryDelta = DP (1, indent) |
180 | | - } |
181 | | - addWhere _ = panic "Ide.Plugin.Class.addMethodPlaceholder" |
182 | | - modifyAnnsT addWhere |
183 | | - modifyAnnsT (captureOrderAnnKey newAnnKey mDecls) |
184 | | - foldM (insertAfter d) ps (reverse mDecls) |
185 | | - |
186 | | - findInstDecl :: ParsedSource -> Transform (LHsDecl GhcPs) |
187 | | - findInstDecl ps = head . filter (containRange range . getLoc) <$> hsDecls ps |
188 | | -#endif |
189 | | - |
190 | | --- | |
191 | | --- This implementation is ad-hoc in a sense that the diagnostic detection mechanism is |
192 | | --- sensitive to the format of diagnostic messages from GHC. |
193 | | -codeAction :: Recorder (WithPriority Log) -> PluginMethodHandler IdeState TextDocumentCodeAction |
194 | | -codeAction recorder state plId (CodeActionParams _ _ docId _ context) = liftIO $ fmap (fromMaybe errorResult) . runMaybeT $ do |
195 | | - docPath <- MaybeT . pure . uriToNormalizedFilePath $ toNormalizedUri uri |
196 | | - actions <- join <$> mapM (mkActions docPath) methodDiags |
197 | | - pure . Right . List $ actions |
198 | | - where |
199 | | - errorResult = Right (List []) |
200 | | - uri = docId ^. J.uri |
201 | | - List diags = context ^. J.diagnostics |
202 | | - |
203 | | - ghcDiags = filter (\d -> d ^. J.source == Just "typecheck") diags |
204 | | - methodDiags = filter (\d -> isClassMethodWarning (d ^. J.message)) ghcDiags |
205 | | - |
206 | | - mkActions docPath diag = do |
207 | | - (HAR {hieAst = ast}, pmap) <- |
208 | | - MaybeT . runAction "classplugin" state $ useWithStale GetHieAst docPath |
209 | | - instancePosition <- MaybeT . pure $ |
210 | | - fromCurrentRange pmap range ^? _Just . J.start |
211 | | - & fmap (J.character -~ 1) |
212 | | - |
213 | | - ident <- findClassIdentifier ast instancePosition |
214 | | - cls <- findClassFromIdentifier docPath ident |
215 | | - implemented <- findImplementedMethods ast instancePosition |
216 | | - logWith recorder Info (LogImplementedMethods cls implemented) |
217 | | - lift . traverse (mkAction . (\\ implemented)) . minDefToMethodGroups . classMinimalDef $ cls |
218 | | - where |
219 | | - range = diag ^. J.range |
220 | | - |
221 | | - mkAction methodGroup |
222 | | - = pure $ mkCodeAction title $ mkLspCommand plId "addMinimalMethodPlaceholders" title (Just cmdParams) |
223 | | - where |
224 | | - title = mkTitle methodGroup |
225 | | - cmdParams = mkCmdParams methodGroup |
226 | | - |
227 | | - mkTitle methodGroup |
228 | | - = "Add placeholders for " |
229 | | - <> mconcat (intersperse ", " (fmap (\m -> "'" <> m <> "'") methodGroup)) |
230 | | - |
231 | | - mkCmdParams methodGroup = [toJSON (AddMinimalMethodsParams uri range (List methodGroup))] |
232 | | - |
233 | | - mkCodeAction title cmd |
234 | | - = InR |
235 | | - $ CodeAction title (Just CodeActionQuickFix) (Just (List [])) Nothing Nothing Nothing (Just cmd) Nothing |
236 | | - |
237 | | - findClassIdentifier :: HieASTs a -> Position -> MaybeT IO (Either ModuleName Name) |
238 | | - findClassIdentifier ast instancePosition = |
239 | | - pure |
240 | | - $ head . head |
241 | | - $ pointCommand ast instancePosition |
242 | | - ( (Map.keys . Map.filter isClassNodeIdentifier . Compat.getNodeIds) |
243 | | - <=< nodeChildren |
244 | | - ) |
245 | | - |
246 | | - findClassFromIdentifier docPath (Right name) = do |
247 | | - (hscEnv -> hscenv, _) <- MaybeT . runAction "classplugin" state $ useWithStale GhcSessionDeps docPath |
248 | | - (tmrTypechecked -> thisMod, _) <- MaybeT . runAction "classplugin" state $ useWithStale TypeCheck docPath |
249 | | - MaybeT . fmap snd . initTcWithGbl hscenv thisMod ghostSpan $ do |
250 | | - tcthing <- tcLookup name |
251 | | - case tcthing of |
252 | | - AGlobal (AConLike (RealDataCon con)) |
253 | | - | Just cls <- tyConClass_maybe (dataConOrigTyCon con) -> pure cls |
254 | | - _ -> panic "Ide.Plugin.Class.findClassFromIdentifier" |
255 | | - findClassFromIdentifier _ (Left _) = panic "Ide.Plugin.Class.findClassIdentifier" |
256 | | - |
257 | | - findImplementedMethods :: HieASTs a -> Position -> MaybeT IO [T.Text] |
258 | | - findImplementedMethods asts instancePosition = do |
259 | | - pure |
260 | | - $ concat |
261 | | - $ pointCommand asts instancePosition |
262 | | - $ map (T.pack . getOccString) . rights . findInstanceValBindIdentifiers |
263 | | - |
264 | | - -- | Recurses through the given AST to find identifiers which are |
265 | | - -- 'InstanceValBind's. |
266 | | - findInstanceValBindIdentifiers :: HieAST a -> [Identifier] |
267 | | - findInstanceValBindIdentifiers ast = |
268 | | - let valBindIds = Map.keys |
269 | | - . Map.filter (any isInstanceValBind . identInfo) |
270 | | - $ getNodeIds ast |
271 | | - in valBindIds <> concatMap findInstanceValBindIdentifiers (nodeChildren ast) |
272 | | - |
273 | | -ghostSpan :: RealSrcSpan |
274 | | -ghostSpan = realSrcLocSpan $ mkRealSrcLoc (fsLit "<haskell-language-sever>") 1 1 |
275 | | - |
276 | | -containRange :: Range -> SrcSpan -> Bool |
277 | | -containRange range x = isInsideSrcSpan (range ^. J.start) x || isInsideSrcSpan (range ^. J.end) x |
278 | | - |
279 | | -isClassNodeIdentifier :: IdentifierDetails a -> Bool |
280 | | -isClassNodeIdentifier ident = (isNothing . identType) ident && Use `Set.member` identInfo ident |
281 | | - |
282 | | -isClassMethodWarning :: T.Text -> Bool |
283 | | -isClassMethodWarning = T.isPrefixOf "• No explicit implementation for" |
284 | | - |
285 | | -isInstanceValBind :: ContextInfo -> Bool |
286 | | -isInstanceValBind (ValBind InstanceBind _ _) = True |
287 | | -isInstanceValBind _ = False |
288 | | - |
289 | | -minDefToMethodGroups :: BooleanFormula Name -> [[T.Text]] |
290 | | -minDefToMethodGroups = go |
291 | | - where |
292 | | - go (Var mn) = [[T.pack . occNameString . occName $ mn]] |
293 | | - go (Or ms) = concatMap (go . unLoc) ms |
294 | | - go (And ms) = foldr (liftA2 (<>)) [[]] (fmap (go . unLoc) ms) |
295 | | - go (Parens m) = go (unLoc m) |
0 commit comments