Skip to content

Commit 3270e7a

Browse files
committed
Implements michaelpj code review suggestions
1 parent 239737b commit 3270e7a

21 files changed

+213
-144
lines changed

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ jobs:
247247

248248
- if: matrix.test && matrix.ghc != '8.10.7' && matrix.ghc != '9.0.2'
249249
name: Test hls-overloaded-record-dot-plugin test suite
250-
run: cabal test hls-overloaded-record-dot-plugin --test-options="$TEST_OPTS" || LSP_TEST_LOG_COLOR=0 LSP_TEST_LOG_MESSAGES=true LSP_TEST_LOG_STDERR=true cabal test hls-retrie-plugin --test-options="$TEST_OPTS"
250+
run: cabal test hls-overloaded-record-dot-plugin --test-options="$TEST_OPTS" || LSP_TEST_LOG_COLOR=0 LSP_TEST_LOG_MESSAGES=true LSP_TEST_LOG_STDERR=true cabal test hls-overloaded-record-dot-plugin --test-options="$TEST_OPTS"
251251

252252

253253

CODEOWNERS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
/plugins/hls-tactics-plugin @isovector
3434
/plugins/hls-stan-plugin @uhbif19
3535
/plugins/hls-explicit-record-fields-plugin @ozkutuk
36+
/plugins/hls-overloaded-record-dot-plugin @joyfulmantis
3637

3738
# Benchmarking
3839
/shake-bench @pepeiborra

ghcide/src/Development/IDE/GHC/Orphans.hs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,3 +236,9 @@ instance NFData NodeKey where
236236
instance NFData HomeModLinkable where
237237
rnf = rwhnf
238238
#endif
239+
240+
instance NFData (HsExpr (GhcPass 'Renamed)) where
241+
rnf = rwhnf
242+
243+
instance NFData Extension where
244+
rnf = rwhnf

plugins/hls-explicit-record-fields-plugin/src/Ide/Plugin/ExplicitFields.hs

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ import Data.Functor ((<&>))
2222
import Data.Generics (GenericQ, everything, extQ,
2323
mkQ)
2424
import qualified Data.HashMap.Strict as HashMap
25-
import Data.Maybe (isJust, listToMaybe,
26-
maybeToList, fromMaybe)
25+
import Data.Maybe (fromMaybe, isJust,
26+
listToMaybe, maybeToList)
2727
import Data.Text (Text)
2828
import Development.IDE (IdeState, NormalizedFilePath,
2929
Pretty (..), Recorder (..),
@@ -36,8 +36,8 @@ import Development.IDE.Core.Shake (define, use)
3636
import qualified Development.IDE.Core.Shake as Shake
3737
import Development.IDE.GHC.Compat (HsConDetails (RecCon),
3838
HsRecFields (..), LPat,
39-
Outputable, getLoc, unLoc,
40-
recDotDot)
39+
Outputable, getLoc, recDotDot,
40+
unLoc)
4141
import Development.IDE.GHC.Compat.Core (Extension (NamedFieldPuns),
4242
GhcPass,
4343
HsExpr (RecordCon, rcon_flds),
@@ -103,7 +103,7 @@ codeActionProvider :: PluginMethodHandler IdeState 'TextDocumentCodeAction
103103
codeActionProvider ideState pId (CodeActionParams _ _ docId range _) = pluginResponse $ do
104104
nfp <- getNormalizedFilePath (docId ^. L.uri)
105105
pragma <- getFirstPragma pId ideState nfp
106-
CRR recMap (map unExt -> exts) <- collectRecords' ideState nfp
106+
CRR recMap exts <- collectRecords' ideState nfp
107107
let actions = map (mkCodeAction nfp exts pragma) (RangeMap.filterByRange range recMap)
108108
pure $ List actions
109109

@@ -160,8 +160,8 @@ collectRecordsRule recorder = define (cmapWithPrio LogShake recorder) $ \Collect
160160
pure ([], CRR <$> recMap <*> Just exts)
161161

162162
where
163-
getEnabledExtensions :: TcModuleResult -> [GhcExtension]
164-
getEnabledExtensions = map GhcExtension . getExtensions . tmrParsed
163+
getEnabledExtensions :: TcModuleResult -> [Extension]
164+
getEnabledExtensions = getExtensions . tmrParsed
165165

166166
getRecords :: TcModuleResult -> [RecordInfo]
167167
getRecords (tmrRenamed -> (hs_valds -> valBinds,_,_,_)) =
@@ -186,7 +186,7 @@ instance NFData CollectRecords
186186

187187
data CollectRecordsResult = CRR
188188
{ recordInfos :: RangeMap RenderedRecordInfo
189-
, enabledExtensions :: [GhcExtension]
189+
, enabledExtensions :: [Extension]
190190
}
191191
deriving (Generic)
192192

@@ -213,15 +213,8 @@ instance Show CollectNamesResult where
213213

214214
type instance RuleResult CollectNames = CollectNamesResult
215215

216-
-- `Extension` is wrapped so that we can provide an `NFData` instance
217-
-- (without resorting to creating an orphan instance).
218-
newtype GhcExtension = GhcExtension { unExt :: Extension }
219-
220-
instance NFData GhcExtension where
221-
rnf x = x `seq` ()
222-
223216
-- As with `GhcExtension`, this newtype exists mostly to attach
224-
-- an `NFData` instance to `UniqFM`.
217+
-- an `NFData` instance to `UniqFM`.(without resorting to creating an orphan instance).
225218
newtype NameMap = NameMap (UniqFM Name [Name])
226219

227220
instance NFData NameMap where

plugins/hls-overloaded-record-dot-plugin/hls-overloaded-record-dot-plugin.cabal

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ library
4141
, ghc-boot-th
4242
, unordered-containers
4343
, containers
44+
, deepseq
4445
hs-source-dirs: src
4546
default-language: GHC2021
4647

plugins/hls-overloaded-record-dot-plugin/src/Ide/Plugin/OverloadedRecordDot.hs

Lines changed: 82 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ module Ide.Plugin.OverloadedRecordDot
1616
import Control.Lens ((^.))
1717
import Control.Monad.IO.Class (MonadIO, liftIO)
1818
import Control.Monad.Trans.Except (ExceptT)
19-
import Data.Generics (GenericQ, everything, mkQ)
19+
import Data.Generics (GenericQ, everything,
20+
everythingBut, mkQ)
2021
import qualified Data.HashMap.Strict as HashMap
2122
import Data.Maybe (listToMaybe, maybeToList)
2223
import Data.Text (Text)
@@ -37,11 +38,17 @@ import Development.IDE.GHC.Compat (HsExpr (HsRecSel))
3738
import Development.IDE.GHC.Compat (HsExpr (HsRecFld))
3839
#endif
3940

41+
import Control.DeepSeq (rwhnf)
42+
import Data.Bifunctor (Bifunctor (first))
4043
import Development.IDE.GHC.Compat (Extension (OverloadedRecordDot),
4144
GhcPass,
42-
HsExpr (HsApp, HsPar, HsVar, OpApp),
45+
HsExpansion (HsExpanded),
46+
HsExpr (HsApp, HsPar, HsVar, OpApp, XExpr),
4347
LHsExpr, Outputable, Pass (..),
44-
RealSrcSpan, getLoc, hs_valds,
48+
RealSrcSpan, appPrec,
49+
dollarName, getLoc, hs_valds,
50+
parenthesizeHsContext,
51+
parenthesizeHsExpr,
4552
pattern RealSrcSpan, unLoc)
4653
import Development.IDE.GHC.Util (getExtensions, printOutputable)
4754
import Development.IDE.Graph (RuleResult)
@@ -71,168 +78,138 @@ import Language.LSP.Types (CodeAction (..),
7178
normalizedFilePathToUri,
7279
type (|?) (InR))
7380
import qualified Language.LSP.Types.Lens as L
74-
7581
data Log
7682
= LogShake Shake.Log
77-
| LogCollectedRecordSelectors [RecordSelectors]
78-
| LogRenderedRecordSelectors [ConvertedRecordSelector]
83+
| LogCollectedRecordSelectors [RecordSelectorExpr]
84+
| LogTextEdits [TextEdit]
7985

8086
instance Pretty Log where
8187
pretty = \case
8288
LogShake shakeLog -> pretty shakeLog
8389
LogCollectedRecordSelectors recs -> "Collected record selectors:" <+> pretty recs
84-
LogRenderedRecordSelectors recs -> "Rendered record selectors:" <+> pretty recs
8590

8691
data CollectRecordSelectors = CollectRecordSelectors
8792
deriving (Eq, Show, Generic)
8893

8994
instance Hashable CollectRecordSelectors
9095
instance NFData CollectRecordSelectors
9196

92-
data CollectConvertedRecordSelectorsResult = CCRSR
93-
{ recordInfos :: RangeMap ConvertedRecordSelector
94-
, enabledExtensions :: [GhcExtension]
97+
data CollectRecordSelectorsResult = CRSR
98+
{ recordInfos :: RangeMap RecordSelectorExpr
99+
, enabledExtensions :: [Extension]
95100
}
96101
deriving (Generic)
97102

98-
instance NFData CollectConvertedRecordSelectorsResult
103+
instance NFData CollectRecordSelectorsResult
99104

100-
instance Show CollectConvertedRecordSelectorsResult where
105+
instance Show CollectRecordSelectorsResult where
101106
show _ = "<CollectRecordsResult>"
102107

103-
type instance RuleResult CollectRecordSelectors = CollectConvertedRecordSelectorsResult
104-
105-
-- `Extension` is wrapped so that we can provide an `NFData` instance
106-
-- (without resorting to creating an orphan instance).
107-
newtype GhcExtension = GhcExtension { unExt :: Extension }
108-
109-
instance NFData GhcExtension where
110-
rnf x = x `seq` ()
108+
type instance RuleResult CollectRecordSelectors = CollectRecordSelectorsResult
111109

112-
data RecordSelectors
113-
= RecordSelectors RealSrcSpan (HsExpr (GhcPass 'Renamed))
114110

115-
instance Pretty RecordSelectors where
116-
pretty (RecordSelectors ss e) = pretty (printOutputable ss) <> ":" <+> pretty (printOutputable e)
117-
118-
data ConvertedRecordSelector = ConvertedRecordSelector
119-
{ range :: Range
120-
, convertedDotRecord :: Text
121-
}
122-
deriving (Generic)
111+
data RecordSelectorExpr = RecordSelectorExpr { location :: Range,
112+
selectorExpr :: LHsExpr (GhcPass 'Renamed),
113+
recordExpr :: LHsExpr (GhcPass 'Renamed) }
123114

124-
instance Pretty ConvertedRecordSelector where
125-
pretty (ConvertedRecordSelector r cdr) = pretty (show r) <> ":" <+> pretty cdr
115+
instance Pretty RecordSelectorExpr where
116+
pretty (RecordSelectorExpr l rs se) = pretty (printOutputable rs) <> ":" <+> pretty (printOutputable se)
126117

127-
instance NFData ConvertedRecordSelector
118+
instance NFData RecordSelectorExpr where
119+
rnf = rwhnf
128120

129121
descriptor :: Recorder (WithPriority Log) -> PluginId -> PluginDescriptor IdeState
130122
descriptor recorder plId = (defaultPluginDescriptor plId)
131123
{ pluginHandlers = mkPluginHandler STextDocumentCodeAction codeActionProvider
132-
, pluginRules = collectConvRecSelsRule recorder
124+
, pluginRules = collectRecSelsRule recorder
133125
}
134126

135127
codeActionProvider :: PluginMethodHandler IdeState 'TextDocumentCodeAction
136128
codeActionProvider ideState pId (CodeActionParams _ _ caDocId caRange _) = pluginResponse $ do
137129
nfp <- getNormalizedFilePath (caDocId ^. L.uri)
138130
pragma <- getFirstPragma pId ideState nfp
139-
CCRSR crsMap (coerce -> exts) <- collectConvRecSels' ideState nfp
140-
let actions = map (mkCodeAction nfp exts pragma) (RangeMap.filterByRange caRange crsMap)
131+
CRSR crsMap exts <- collectRecSelResult ideState nfp
132+
let
133+
pragmaEdit = if OverloadedRecordDot `elem` exts
134+
then Nothing
135+
else Just $ insertNewPragma pragma OverloadedRecordDot
136+
edits crs = maybeToList (convertRecordSelectors crs) <> maybeToList pragmaEdit
137+
changes crs = Just $ HashMap.singleton (fromNormalizedUri (normalizedFilePathToUri nfp)) (List (edits crs))
138+
mkCodeAction crs = InR CodeAction
139+
{ _title = mkCodeActionTitle exts crs
140+
, _kind = Just CodeActionRefactorRewrite
141+
, _diagnostics = Nothing
142+
, _isPreferred = Nothing
143+
, _disabled = Nothing
144+
, _edit = Just $ WorkspaceEdit (changes crs) Nothing Nothing
145+
, _command = Nothing
146+
, _xdata = Nothing
147+
}
148+
actions = map mkCodeAction (RangeMap.filterByRange caRange crsMap)
141149
pure $ List actions
142150
where
143-
mkCodeAction :: NormalizedFilePath -> [Extension] -> NextPragmaInfo -> ConvertedRecordSelector -> Command |? CodeAction
144-
mkCodeAction nfp exts pragma crs = InR CodeAction
145-
{ _title = mkCodeActionTitle exts
146-
, _kind = Just CodeActionRefactorRewrite
147-
, _diagnostics = Nothing
148-
, _isPreferred = Nothing
149-
, _disabled = Nothing
150-
, _edit = Just $ mkWorkspaceEdit nfp edits
151-
, _command = Nothing
152-
, _xdata = Nothing
153-
}
154-
where
155-
edits = mkTextEdit crs : maybeToList pragmaEdit
156-
157-
mkTextEdit :: ConvertedRecordSelector -> TextEdit
158-
mkTextEdit (ConvertedRecordSelector r cdr) = TextEdit r cdr
159-
160-
pragmaEdit :: Maybe TextEdit
161-
pragmaEdit = if OverloadedRecordDot `elem` exts
162-
then Nothing
163-
else Just $ insertNewPragma pragma OverloadedRecordDot
164-
165-
mkWorkspaceEdit :: NormalizedFilePath -> [TextEdit] -> WorkspaceEdit
166-
mkWorkspaceEdit nfp edits = WorkspaceEdit changes Nothing Nothing
167-
where
168-
changes = Just $ HashMap.singleton (fromNormalizedUri (normalizedFilePathToUri nfp)) (List edits)
169-
170-
mkCodeActionTitle :: [Extension] -> Text
171-
mkCodeActionTitle exts =
151+
mkCodeActionTitle :: [Extension] -> RecordSelectorExpr-> Text
152+
mkCodeActionTitle exts (RecordSelectorExpr _ se _) =
172153
if OverloadedRecordDot `elem` exts
173154
then title
174155
else title <> " (needs extension: OverloadedRecordDot)"
175156
where
176-
title = "Convert to record dot syntax"
157+
title = "Convert `" <> name <> "` to record dot syntax"
158+
name = printOutputable se
177159

178-
collectConvRecSelsRule :: Recorder (WithPriority Log) -> Rules ()
179-
collectConvRecSelsRule recorder = define (cmapWithPrio LogShake recorder) $ \CollectRecordSelectors nfp ->
160+
collectRecSelsRule :: Recorder (WithPriority Log) -> Rules ()
161+
collectRecSelsRule recorder = define (cmapWithPrio LogShake recorder) $ \CollectRecordSelectors nfp ->
180162
use TypeCheck nfp >>= \case
181163
Nothing -> pure ([], Nothing)
182164
Just tmr -> do
183165
let exts = getEnabledExtensions tmr
184166
recSels = getRecordSelectors tmr
185167
logWith recorder Debug (LogCollectedRecordSelectors recSels)
186-
let convertedRecordSelectors = traverse convertRecordSelectors recSels
187-
crsMap = RangeMap.fromList range <$> convertedRecordSelectors
188-
logWith recorder Debug (LogRenderedRecordSelectors (concat convertedRecordSelectors))
189-
pure ([], CCRSR <$> crsMap <*> Just exts)
168+
let crsMap :: RangeMap RecordSelectorExpr
169+
crsMap = RangeMap.fromList location recSels
170+
pure ([], CRSR <$> Just crsMap <*> Just exts)
190171
where
191-
getEnabledExtensions :: TcModuleResult -> [GhcExtension]
192-
getEnabledExtensions = map GhcExtension . getExtensions . tmrParsed
172+
getEnabledExtensions :: TcModuleResult -> [Extension]
173+
getEnabledExtensions = getExtensions . tmrParsed
193174

194-
getRecordSelectors :: TcModuleResult -> [RecordSelectors]
175+
getRecordSelectors :: TcModuleResult -> [RecordSelectorExpr]
195176
getRecordSelectors (tmrRenamed -> (hs_valds -> valBinds,_,_,_)) =
196177
collectRecordSelectors valBinds
197178

198-
convertRecordSelectors :: RecordSelectors -> Maybe ConvertedRecordSelector
199-
convertRecordSelectors (RecordSelectors ss expr) = ConvertedRecordSelector (realSrcSpanToRange ss) <$> convertRecSel expr
179+
convertRecordSelectors :: RecordSelectorExpr -> Maybe TextEdit
180+
convertRecordSelectors (RecordSelectorExpr l se re) = TextEdit l <$> convertRecSel se re
200181

201-
convertRecSel :: Outputable (HsExpr (GhcPass c)) => HsExpr (GhcPass c) -> Maybe Text
202-
convertRecSel (HsApp _ s r) =
203-
Just $ printOutputable r <> "." <> printOutputable s
204-
convertRecSel ( OpApp _ s _ r) =
205-
Just $ "(" <> printOutputable r <> ")." <> printOutputable s
206-
convertRecSel _ = Nothing
182+
convertRecSel :: Outputable (LHsExpr (GhcPass 'Renamed)) => LHsExpr (GhcPass 'Renamed) -> LHsExpr (GhcPass 'Renamed) -> Maybe Text
183+
convertRecSel se re =
184+
Just $ printOutputable (parenthesizeHsExpr appPrec re) <> "." <> printOutputable se
207185

208-
collectRecordSelectors :: GenericQ [RecordSelectors]
209-
collectRecordSelectors = everything (<>) (maybeToList . (Nothing `mkQ` getRecSels))
186+
-- It's important that we use everthingBut here, because if we used everything we would
187+
-- get duplicates for every case that occures inside a HsExpanded expression.
188+
collectRecordSelectors :: GenericQ [RecordSelectorExpr]
189+
collectRecordSelectors = everythingBut (<>) (first maybeToList. ((Nothing, False) `mkQ` getRecSels))
210190

191+
getRecSels :: LHsExpr (GhcPass 'Renamed) -> (Maybe RecordSelectorExpr, Bool)
192+
-- When we stumble upon an occurance of HsExpanded, we only want to follow one branch
193+
-- we do this here, by explicitly returning an occurance from traversing the original branch,
194+
-- and returning True, which keeps syb from implicitly continuing to traverse.
195+
getRecSels (unLoc -> XExpr (HsExpanded a _)) = (listToMaybe $ collectRecordSelectors a, True)
211196
#if __GLASGOW_HASKELL__ >= 903
212-
getRecSels :: LHsExpr (GhcPass 'Renamed) -> Maybe RecordSelectors
213-
-- standard record selection: "field record"
214-
getRecSels e@(unLoc -> HsApp _ (unLoc -> HsRecSel _ _) (unLoc -> HsVar _ _)) =
215-
listToMaybe [ RecordSelectors realSpan' (unLoc e) | RealSrcSpan realSpan' _ <- [ getLoc e ]]
216-
-- Record selection where the field is being applied to a parenthesised expression: "field (record)"
217-
getRecSels e@(unLoc -> HsApp _ (unLoc -> HsRecSel _ _) (unLoc -> HsPar _ _ _ _)) =
218-
listToMaybe [ RecordSelectors realSpan' (unLoc e) | RealSrcSpan realSpan' _ <- [ getLoc e ]]
197+
-- applied record selection: "field record" or "field (record)" or "field field.record"
198+
getRecSels e@(unLoc -> HsApp _ se@(unLoc -> HsRecSel _ _) re) =
199+
(listToMaybe [ RecordSelectorExpr (realSrcSpanToRange realSpan') se re | RealSrcSpan realSpan' _ <- [ getLoc e ]], False)
219200
-- Record selection where the field is being applied with the "$" operator: "field $ record"
220-
getRecSels e@(unLoc -> OpApp _ (unLoc -> HsRecSel _ _) (unLoc -> HsVar _ (unLoc -> d)) _)
221-
| printOutputable d == "$" = listToMaybe [ RecordSelectors realSpan' (unLoc e) | RealSrcSpan realSpan' _ <- [ getLoc e ]]
222-
getRecSels _ = Nothing
201+
getRecSels e@(unLoc -> OpApp _ se@(unLoc -> HsRecSel _ _) (unLoc -> HsVar _ (unLoc -> d)) re)
202+
| d == dollarName = (listToMaybe [ RecordSelectorExpr (realSrcSpanToRange realSpan') se re | RealSrcSpan realSpan' _ <- [ getLoc e ]], False)
223203
#else
224-
getRecSels :: LHsExpr (GhcPass 'Renamed) -> Maybe RecordSelectors
225-
getRecSels e@(unLoc -> HsApp _ (unLoc -> HsRecFld _ _) (unLoc -> HsVar _ _)) =
226-
listToMaybe [ RecordSelectors realSpan' (unLoc e) | RealSrcSpan realSpan' _ <- [ getLoc e ]]
227-
getRecSels e@(unLoc -> HsApp _ (unLoc -> HsRecFld _ _) (unLoc -> HsPar _ _)) =
228-
listToMaybe [ RecordSelectors realSpan' (unLoc e) | RealSrcSpan realSpan' _ <- [ getLoc e ]]
229-
getRecSels e@(unLoc -> OpApp _ (unLoc -> HsRecFld _ _) (unLoc -> HsVar _ (unLoc -> d)) _)
230-
| printOutputable d == "$" = listToMaybe [ RecordSelectors realSpan' (unLoc e) | RealSrcSpan realSpan' _ <- [ getLoc e ]]
231-
getRecSels _ = Nothing
204+
getRecSels e@(unLoc -> HsApp _ se@(unLoc -> HsRecFld _ _) re) =
205+
(listToMaybe [ RecordSelectorExpr (realSrcSpanToRange realSpan') se re | RealSrcSpan realSpan' _ <- [ getLoc e ]], False)
206+
getRecSels e@(unLoc -> OpApp _ se@(unLoc -> HsRecFld _ _) (unLoc -> HsVar _ (unLoc -> d)) re)
207+
| d == dollarName = (listToMaybe [ RecordSelectorExpr (realSrcSpanToRange realSpan') se re | RealSrcSpan realSpan' _ <- [ getLoc e ]], False)
232208
#endif
209+
getRecSels _ = (Nothing, False)
233210

234-
collectConvRecSels' :: MonadIO m => IdeState -> NormalizedFilePath -> ExceptT String m CollectConvertedRecordSelectorsResult
235-
collectConvRecSels' ideState =
211+
collectRecSelResult :: MonadIO m => IdeState -> NormalizedFilePath -> ExceptT String m CollectRecordSelectorsResult
212+
collectRecSelResult ideState =
236213
handleMaybeM "Unable to TypeCheck"
237214
. liftIO
238215
. runAction "overloadedRecordDot.collectRecordSelectors" ideState

0 commit comments

Comments
 (0)