Skip to content

Commit 0d90b48

Browse files
committed
hls-class-plugin enhancement
1 parent 9b3f3bf commit 0d90b48

31 files changed

+866
-260
lines changed

exe/Plugins.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ idePlugins recorder includeExamples = pluginDescToIdePlugins allPlugins
156156
CallHierarchy.descriptor "callHierarchy":
157157
#endif
158158
#if class
159-
Class.descriptor "class" :
159+
Class.descriptor pluginRecorder "class" :
160160
#endif
161161
#if haddockComments
162162
HaddockComments.descriptor "haddockComments" :

plugins/hls-class-plugin/README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Class Plugin
2+
3+
The class plugin provides handy operations about class, includes:
4+
5+
1. Code action to add minimul class definition methods.
6+
2. Type lens about missing type signatures for instance methods.
7+
8+
## Demo
9+
10+
![Code Actions](codeactions.gif)
11+
12+
![Code Lens](codelens.gif)
876 KB
Loading

plugins/hls-class-plugin/codelens.gif

192 KB
Loading

plugins/hls-class-plugin/hls-class-plugin.cabal

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,22 @@ extra-source-files:
2222

2323
library
2424
exposed-modules: Ide.Plugin.Class
25+
other-modules: Ide.Plugin.Class.CodeAction
26+
, Ide.Plugin.Class.CodeLens
27+
, Ide.Plugin.Class.ExactPrint
28+
, Ide.Plugin.Class.Types
29+
, Ide.Plugin.Class.Utils
2530
hs-source-dirs: src
2631
build-depends:
2732
, aeson
2833
, base >=4.12 && <5
2934
, containers
35+
, deepseq
36+
, extra
3037
, ghc
3138
, ghcide ^>=1.7
39+
, ghc-boot-th
40+
, hls-graph
3241
, hls-plugin-api ^>=1.4
3342
, lens
3443
, lsp
@@ -44,8 +53,9 @@ library
4453
default-extensions:
4554
DataKinds
4655
TypeOperators
56+
OverloadedStrings
4757

48-
ghc-options: -Wno-unticked-promoted-constructors
58+
ghc-options: -Wall -Wno-unticked-promoted-constructors -Wno-name-shadowing
4959

5060
test-suite tests
5161
type: exitcode-stdio-1.0
@@ -54,9 +64,11 @@ test-suite tests
5464
main-is: Main.hs
5565
ghc-options: -threaded -rtsopts -with-rtsopts=-N
5666
build-depends:
67+
, aeson
5768
, base
5869
, filepath
5970
, hls-class-plugin
71+
, hls-plugin-api
6072
, hls-test-utils ^>=1.3
6173
, lens
6274
, lsp-types
Lines changed: 21 additions & 250 deletions
Original file line numberDiff line numberDiff line change
@@ -1,255 +1,26 @@
1-
{-# LANGUAGE CPP #-}
2-
{-# LANGUAGE DeriveAnyClass #-}
3-
{-# LANGUAGE DeriveGeneric #-}
4-
{-# LANGUAGE OverloadedStrings #-}
5-
{-# LANGUAGE RecordWildCards #-}
6-
{-# LANGUAGE TypeFamilies #-}
7-
{-# LANGUAGE ViewPatterns #-}
8-
module Ide.Plugin.Class
9-
( descriptor
10-
) where
1+
module Ide.Plugin.Class (descriptor) where
112

12-
import Control.Applicative
13-
import Control.Lens hiding (List, use)
14-
import Control.Monad
15-
import Control.Monad.IO.Class
16-
import Control.Monad.Trans.Class
17-
import Control.Monad.Trans.Maybe
18-
import Data.Aeson
19-
import Data.Char
20-
import Data.List
21-
import qualified Data.Map.Strict as Map
22-
import Data.Maybe
23-
import qualified Data.Text as T
24-
import qualified Data.Set as Set
25-
import Development.IDE hiding (pluginHandlers)
26-
import Development.IDE.Core.PositionMapping (fromCurrentRange,
27-
toCurrentRange)
28-
import Development.IDE.GHC.Compat as Compat hiding (locA)
29-
import Development.IDE.GHC.Compat.Util
30-
import Development.IDE.Spans.AtPoint
31-
import qualified GHC.Generics as Generics
32-
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
337
import Ide.Types
34-
import Language.Haskell.GHC.ExactPrint
35-
import Language.Haskell.GHC.ExactPrint.Parsers (parseDecl)
36-
import Language.Haskell.GHC.ExactPrint.Types hiding (GhcPs, Parens)
37-
import Language.Haskell.GHC.ExactPrint.Utils (rs)
38-
import Language.LSP.Server
398
import Language.LSP.Types
40-
import qualified Language.LSP.Types.Lens as J
419

42-
#if MIN_VERSION_ghc(9,2,0)
43-
import GHC.Hs (AnnsModule(AnnsModule))
44-
import GHC.Parser.Annotation
45-
#endif
46-
47-
descriptor :: PluginId -> PluginDescriptor IdeState
48-
descriptor plId = (defaultPluginDescriptor plId)
49-
{ pluginCommands = commands
50-
, pluginHandlers = mkPluginHandler STextDocumentCodeAction codeAction
51-
}
52-
53-
commands :: [PluginCommand IdeState]
54-
commands
55-
= [ PluginCommand "addMinimalMethodPlaceholders" "add placeholders for minimal methods" addMethodPlaceholders
10+
descriptor :: Recorder (WithPriority Log) -> PluginId -> PluginDescriptor IdeState
11+
descriptor recorder plId = (defaultPluginDescriptor plId)
12+
{ pluginCommands = commands plId
13+
, pluginRules = rules recorder
14+
, pluginHandlers = mkPluginHandler STextDocumentCodeAction codeAction
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
5626
]
57-
58-
-- | Parameter for the addMethods PluginCommand.
59-
data AddMinimalMethodsParams = AddMinimalMethodsParams
60-
{ uri :: Uri
61-
, range :: Range
62-
, methodGroup :: List T.Text
63-
}
64-
deriving (Show, Eq, Generics.Generic, ToJSON, FromJSON)
65-
66-
addMethodPlaceholders :: CommandFunction IdeState AddMinimalMethodsParams
67-
addMethodPlaceholders state AddMinimalMethodsParams{..} = do
68-
caps <- getClientCapabilities
69-
medit <- liftIO $ runMaybeT $ do
70-
docPath <- MaybeT . pure . uriToNormalizedFilePath $ toNormalizedUri uri
71-
pm <- MaybeT . runAction "classplugin" state $ use GetParsedModule docPath
72-
(hsc_dflags . hscEnv -> df) <- MaybeT . runAction "classplugin" state $ use GhcSessionDeps docPath
73-
(old, new) <- makeEditText pm df
74-
pure (workspaceEdit caps old new)
75-
76-
forM_ medit $ \edit ->
77-
sendRequest SWorkspaceApplyEdit (ApplyWorkspaceEditParams Nothing edit) (\_ -> pure ())
78-
pure (Right Null)
79-
where
80-
indent = 2
81-
82-
workspaceEdit caps old new
83-
= diffText caps (uri, old) new IncludeDeletions
84-
85-
toMethodName n
86-
| Just (h, _) <- T.uncons n
87-
, not (isAlpha h || h == '_')
88-
= "(" <> n <> ")"
89-
| otherwise
90-
= n
91-
92-
#if MIN_VERSION_ghc(9,2,0)
93-
makeEditText pm df = do
94-
List mDecls <- MaybeT . pure $ traverse (makeMethodDecl df) methodGroup
95-
let ps = makeDeltaAst $ pm_parsed_source pm
96-
old = T.pack $ exactPrint ps
97-
(ps', _, _) = runTransform (addMethodDecls ps mDecls)
98-
new = T.pack $ exactPrint ps'
99-
pure (old, new)
100-
101-
makeMethodDecl df mName =
102-
either (const Nothing) Just . parseDecl df (T.unpack mName) . T.unpack
103-
$ toMethodName mName <> " = _"
104-
105-
addMethodDecls ps mDecls = do
106-
allDecls <- hsDecls ps
107-
let (before, ((L l inst): after)) = break (containRange range . getLoc) allDecls
108-
replaceDecls ps (before ++ (L l (addWhere inst)): (map newLine mDecls ++ after))
109-
where
110-
-- Add `where` keyword for `instance X where` if `where` is missing.
111-
--
112-
-- The `where` in ghc-9.2 is now stored in the instance declaration
113-
-- directly. More precisely, giving an `HsDecl GhcPs`, we have:
114-
-- InstD --> ClsInstD --> ClsInstDecl --> XCClsInstDecl --> (EpAnn [AddEpAnn], AnnSortKey),
115-
-- here `AnnEpAnn` keeps the track of Anns.
116-
--
117-
-- See the link for the original definition:
118-
-- https://hackage.haskell.org/package/ghc-9.2.1/docs/Language-Haskell-Syntax-Extension.html#t:XCClsInstDecl
119-
addWhere (InstD xInstD (ClsInstD ext decl@ClsInstDecl{..})) =
120-
let ((EpAnn entry anns comments), key) = cid_ext
121-
in InstD xInstD (ClsInstD ext decl {
122-
cid_ext = (EpAnn
123-
entry
124-
(AddEpAnn AnnWhere (EpaDelta (SameLine 1) []) : anns)
125-
comments
126-
, key)
127-
})
128-
addWhere decl = decl
129-
130-
newLine (L l e) =
131-
let dp = deltaPos 1 indent
132-
in L (noAnnSrcSpanDP (locA l) dp <> l) e
133-
134-
#else
135-
makeEditText pm df = do
136-
List (unzip -> (mAnns, mDecls)) <- MaybeT . pure $ traverse (makeMethodDecl df) methodGroup
137-
let ps = pm_parsed_source pm
138-
anns = relativiseApiAnns ps (pm_annotations pm)
139-
old = T.pack $ exactPrint ps anns
140-
(ps', (anns', _), _) = runTransform (mergeAnns (mergeAnnList mAnns) anns) (addMethodDecls ps mDecls)
141-
new = T.pack $ exactPrint ps' anns'
142-
pure (old, new)
143-
144-
makeMethodDecl df mName =
145-
case parseDecl df (T.unpack mName) . T.unpack $ toMethodName mName <> " = _" of
146-
Right (ann, d) -> Just (setPrecedingLines d 1 indent ann, d)
147-
Left _ -> Nothing
148-
149-
addMethodDecls ps mDecls = do
150-
d <- findInstDecl ps
151-
newSpan <- uniqueSrcSpanT
152-
let
153-
annKey = mkAnnKey d
154-
newAnnKey = AnnKey (rs newSpan) (CN "HsValBinds")
155-
addWhere mkds@(Map.lookup annKey -> Just ann)
156-
= Map.insert newAnnKey ann2 mkds2
157-
where
158-
ann1 = ann
159-
{ annsDP = annsDP ann ++ [(G AnnWhere, DP (0, 1))]
160-
, annCapturedSpan = Just newAnnKey
161-
, annSortKey = Just (fmap (rs . getLoc) mDecls)
162-
}
163-
mkds2 = Map.insert annKey ann1 mkds
164-
ann2 = annNone
165-
{ annEntryDelta = DP (1, indent)
166-
}
167-
addWhere _ = panic "Ide.Plugin.Class.addMethodPlaceholder"
168-
modifyAnnsT addWhere
169-
modifyAnnsT (captureOrderAnnKey newAnnKey mDecls)
170-
foldM (insertAfter d) ps (reverse mDecls)
171-
172-
findInstDecl :: ParsedSource -> Transform (LHsDecl GhcPs)
173-
findInstDecl ps = head . filter (containRange range . getLoc) <$> hsDecls ps
174-
#endif
175-
176-
-- |
177-
-- This implementation is ad-hoc in a sense that the diagnostic detection mechanism is
178-
-- sensitive to the format of diagnostic messages from GHC.
179-
codeAction :: PluginMethodHandler IdeState TextDocumentCodeAction
180-
codeAction state plId (CodeActionParams _ _ docId _ context) = liftIO $ fmap (fromMaybe errorResult) . runMaybeT $ do
181-
docPath <- MaybeT . pure . uriToNormalizedFilePath $ toNormalizedUri uri
182-
actions <- join <$> mapM (mkActions docPath) methodDiags
183-
pure . Right . List $ actions
184-
where
185-
errorResult = Right (List [])
186-
uri = docId ^. J.uri
187-
List diags = context ^. J.diagnostics
188-
189-
ghcDiags = filter (\d -> d ^. J.source == Just "typecheck") diags
190-
methodDiags = filter (\d -> isClassMethodWarning (d ^. J.message)) ghcDiags
191-
192-
mkActions docPath diag = do
193-
ident <- findClassIdentifier docPath range
194-
cls <- findClassFromIdentifier docPath ident
195-
lift . traverse mkAction . minDefToMethodGroups . classMinimalDef $ cls
196-
where
197-
range = diag ^. J.range
198-
199-
mkAction methodGroup
200-
= pure $ mkCodeAction title $ mkLspCommand plId "addMinimalMethodPlaceholders" title (Just cmdParams)
201-
where
202-
title = mkTitle methodGroup
203-
cmdParams = mkCmdParams methodGroup
204-
205-
mkTitle methodGroup
206-
= "Add placeholders for "
207-
<> mconcat (intersperse ", " (fmap (\m -> "'" <> m <> "'") methodGroup))
208-
209-
mkCmdParams methodGroup = [toJSON (AddMinimalMethodsParams uri range (List methodGroup))]
210-
211-
mkCodeAction title cmd
212-
= InR
213-
$ CodeAction title (Just CodeActionQuickFix) (Just (List [])) Nothing Nothing Nothing (Just cmd) Nothing
214-
215-
findClassIdentifier docPath range = do
216-
(hieAstResult, pmap) <- MaybeT . runAction "classplugin" state $ useWithStale GetHieAst docPath
217-
case hieAstResult of
218-
HAR {hieAst = hf} ->
219-
pure
220-
$ head . head
221-
$ pointCommand hf (fromJust (fromCurrentRange pmap range) ^. J.start & J.character -~ 1)
222-
( (Map.keys . Map.filter isClassNodeIdentifier . Compat.getNodeIds)
223-
<=< nodeChildren
224-
)
225-
226-
findClassFromIdentifier docPath (Right name) = do
227-
(hscEnv -> hscenv, _) <- MaybeT . runAction "classplugin" state $ useWithStale GhcSessionDeps docPath
228-
(tmrTypechecked -> thisMod, _) <- MaybeT . runAction "classplugin" state $ useWithStale TypeCheck docPath
229-
MaybeT . fmap snd . initTcWithGbl hscenv thisMod ghostSpan $ do
230-
tcthing <- tcLookup name
231-
case tcthing of
232-
AGlobal (AConLike (RealDataCon con))
233-
| Just cls <- tyConClass_maybe (dataConOrigTyCon con) -> pure cls
234-
_ -> panic "Ide.Plugin.Class.findClassFromIdentifier"
235-
findClassFromIdentifier _ (Left _) = panic "Ide.Plugin.Class.findClassIdentifier"
236-
237-
ghostSpan :: RealSrcSpan
238-
ghostSpan = realSrcLocSpan $ mkRealSrcLoc (fsLit "<haskell-language-sever>") 1 1
239-
240-
containRange :: Range -> SrcSpan -> Bool
241-
containRange range x = isInsideSrcSpan (range ^. J.start) x || isInsideSrcSpan (range ^. J.end) x
242-
243-
isClassNodeIdentifier :: IdentifierDetails a -> Bool
244-
isClassNodeIdentifier ident = (isNothing . identType) ident && Use `Set.member` (identInfo ident)
245-
246-
isClassMethodWarning :: T.Text -> Bool
247-
isClassMethodWarning = T.isPrefixOf "• No explicit implementation for"
248-
249-
minDefToMethodGroups :: BooleanFormula Name -> [[T.Text]]
250-
minDefToMethodGroups = go
251-
where
252-
go (Var mn) = [[T.pack . occNameString . occName $ mn]]
253-
go (Or ms) = concatMap (go . unLoc) ms
254-
go (And ms) = foldr (liftA2 (<>)) [[]] (fmap (go . unLoc) ms)
255-
go (Parens m) = go (unLoc m)

0 commit comments

Comments
 (0)