@@ -2,42 +2,156 @@ module Development.IDE.Plugin.Plugins.FillHole
22 ( suggestFillHole
33 ) where
44
5+ import Control.Lens ((^.) , (^?) )
56import Control.Monad (guard )
67import Data.Char
8+ import qualified Data.HashSet as Set
79import qualified Data.Text as T
8- import Development.IDE.Plugin.Plugins.Diagnostic
9- import Language.LSP.Protocol.Types (Diagnostic (.. ),
10- TextEdit (TextEdit ))
10+ import Development.IDE (FileDiagnostic ,
11+ fdLspDiagnosticL ,
12+ printOutputable )
13+ import Development.IDE.GHC.Compat (ParsedModule , SDoc ,
14+ defaultSDocContext ,
15+ hsmodImports ,
16+ ideclAs , ideclName ,
17+ ideclQualified ,
18+ lookupOccEnv ,
19+ moduleNameString ,
20+ pm_parsed_source ,
21+ renderWithContext ,
22+ unLoc )
23+ import Development.IDE.GHC.Compat.Error (TcRnMessageDetailed (TcRnMessageDetailed ),
24+ _TcRnMessageWithCtx ,
25+ _TcRnMessageWithInfo ,
26+ hole_occ ,
27+ msgEnvelopeErrorL )
28+ import Development.IDE.Plugin.Plugins.Diagnostic (diagReportHoleError )
29+ import Development.IDE.Types.Diagnostics (_SomeStructuredMessage ,
30+ fdStructuredMessageL )
31+ import Development.IDE.Types.Exports (ExportsMap (.. ),
32+ mkVarOrDataOcc ,
33+ moduleNameText )
34+ import GHC.Tc.Errors.Types (ErrInfo (ErrInfo ))
35+ import Ide.PluginUtils (unescape )
36+ import Language.Haskell.Syntax.ImpExp (ImportDeclQualifiedStyle (.. ))
37+ import Language.LSP.Protocol.Lens (HasRange (.. ))
38+ import Language.LSP.Protocol.Types (TextEdit (TextEdit ))
1139import Text.Regex.TDFA (MatchResult (.. ),
1240 (=~) )
1341
14- suggestFillHole :: Diagnostic -> [(T. Text , TextEdit )]
15- suggestFillHole Diagnostic {_range= _range,.. }
16- | Just holeName <- extractHoleName _message
17- , (holeFits, refFits) <- processHoleSuggestions (T. lines _message) =
18- let isInfixHole = _message =~ addBackticks holeName :: Bool in
42+ suggestFillHole :: ExportsMap -> ParsedModule -> FileDiagnostic -> [(T. Text , TextEdit )]
43+ suggestFillHole exportsMap pm diag
44+ | Just holeName <- extractHoleName diag
45+ , Just (ErrInfo ctx suppl) <- extractErrInfo diag
46+ , (holeFits, refFits) <- processHoleSuggestions $ T. lines (printErr suppl) =
47+ let isInfixHole = printErr ctx =~ addBackticks holeName :: Bool in
1948 map (proposeHoleFit holeName False isInfixHole) holeFits
2049 ++ map (proposeHoleFit holeName True isInfixHole) refFits
2150 | otherwise = []
2251 where
23- extractHoleName = fmap (headOrThrow " impossible" ) . flip matchRegexUnifySpaces " Found hole: ([^ ]*)"
52+ qualify = qualifyFit exportsMap pm
53+
54+ extractHoleName :: FileDiagnostic -> Maybe T. Text
55+ extractHoleName d = do
56+ hole <- diagReportHoleError d
57+ Just $ printOutputable (hole_occ hole)
58+
59+ extractErrInfo :: FileDiagnostic -> Maybe ErrInfo
60+ extractErrInfo d = do
61+ (_, TcRnMessageDetailed errInfo _) <-
62+ d ^? fdStructuredMessageL
63+ . _SomeStructuredMessage
64+ . msgEnvelopeErrorL
65+ . _TcRnMessageWithCtx
66+ . _TcRnMessageWithInfo
67+ Just errInfo
68+
69+ printErr :: SDoc -> T. Text
70+ printErr = unescape . T. pack . renderWithContext defaultSDocContext
71+
72+ addBackticks :: T. Text -> T. Text
2473 addBackticks text = " `" <> text <> " `"
74+
75+ addParens :: T. Text -> T. Text
2576 addParens text = " (" <> text <> " )"
77+
78+ proposeHoleFit :: T. Text -> Bool -> Bool -> T. Text -> (T. Text , TextEdit )
2679 proposeHoleFit holeName parenthise isInfixHole name =
2780 case T. uncons name of
2881 Nothing -> error " impossible: empty name provided by ghc"
2982 Just (firstChr, _) ->
30- let isInfixOperator = firstChr == ' ('
31- name' = getOperatorNotation isInfixHole isInfixOperator name in
32- ( " Replace " <> holeName <> " with " <> name
33- , TextEdit _range (if parenthise then addParens name' else name')
34- )
83+ let cleanName = qualify (stripUnique name)
84+ isInfixOperator = firstChr == ' ('
85+ name' = getOperatorNotation isInfixHole isInfixOperator cleanName
86+ replacement = if parenthise then addParens name' else name'
87+ in
88+ ( " Replace " <> holeName <> " with " <> cleanName
89+ , TextEdit (diag ^. fdLspDiagnosticL . range) replacement
90+ )
91+
92+ getOperatorNotation :: Bool -> Bool -> T. Text -> T. Text
3593 getOperatorNotation True False name = addBackticks name
3694 getOperatorNotation True True name = T. drop 1 (T. dropEnd 1 name)
3795 getOperatorNotation _isInfixHole _isInfixOperator name = name
38- headOrThrow msg = \ case
39- [] -> error msg
40- (x: _) -> x
96+
97+ stripUnique :: T. Text -> T. Text
98+ stripUnique t =
99+ case T. breakOnEnd " _" t of
100+ (prefix, suffix)
101+ | T. null prefix -> t
102+ | T. null suffix -> t
103+ | not (T. all isAlphaNum suffix) -> t
104+ | otherwise -> T. dropEnd (T. length suffix + 1 ) t
105+
106+ -- | Given the exports map, parsed module (for its imports), and a hole fit
107+ -- name like "toException", return the qualified version like "E.toException"
108+ -- if a qualifying import exists, otherwise return the name as it is.
109+ qualifyFit :: ExportsMap -> ParsedModule -> T. Text -> T. Text
110+ qualifyFit exportsMap pm fitName =
111+ case findQualifier of
112+ Nothing -> fitName
113+ Just qualifier -> qualifier <> " ." <> fitName
114+ where
115+ -- All modules that export this name
116+ exportingModules :: [T. Text ]
117+ exportingModules =
118+ let occ = mkVarOrDataOcc fitName
119+ identSet = lookupOccEnv (getExportsMap exportsMap) occ
120+ idents = maybe [] Set. toList identSet
121+ in map moduleNameText idents
122+
123+ -- All qualified imports from this file: (moduleName, qualifier)
124+ qualifiedImports :: [(T. Text , T. Text )]
125+ qualifiedImports =
126+ let imports = hsmodImports . unLoc . pm_parsed_source $ pm
127+ in [ (modName decl, qualifier decl)
128+ | i <- imports
129+ , let decl = unLoc i
130+ , isQualified decl
131+ ]
132+
133+ isQualified decl = ideclQualified decl `elem` [QualifiedPre , QualifiedPost ]
134+
135+ modName decl =
136+ T. pack . moduleNameString . unLoc . ideclName $ decl
137+
138+ qualifier decl =
139+ case ideclAs decl of
140+ Just alias -> T. pack . moduleNameString . unLoc $ alias
141+ Nothing -> modName decl
142+
143+ -- Find first qualified import whose module is in the exporting modules list
144+ findQualifier :: Maybe T. Text
145+ findQualifier =
146+ let exportingSet = exportingModules
147+ in fmap snd
148+ . safeHead
149+ . filter (\ (modN, _) -> modN `elem` exportingSet)
150+ $ qualifiedImports
151+
152+ safeHead [] = Nothing
153+ safeHead (x: _) = Just x
154+
41155
42156processHoleSuggestions :: [T. Text ] -> ([T. Text ], [T. Text ])
43157processHoleSuggestions mm = (holeSuggestions, refSuggestions)
0 commit comments