Skip to content

Commit 0c84caa

Browse files
committed
Distinguish between wildcard expansion and traditional record syntax conversion
1 parent 43b7b3b commit 0c84caa

File tree

2 files changed

+90
-61
lines changed

2 files changed

+90
-61
lines changed

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

Lines changed: 36 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ import Language.LSP.Protocol.Message (Method (..),
114114
import Language.LSP.Protocol.Types (CodeAction (..),
115115
CodeActionKind (CodeActionKind_RefactorRewrite),
116116
CodeActionParams (CodeActionParams),
117-
InlayHint (..),
117+
Command, InlayHint (..),
118118
InlayHintLabelPart (InlayHintLabelPart),
119119
InlayHintParams (InlayHintParams, _range, _textDocument),
120120
TextDocumentIdentifier (TextDocumentIdentifier),
@@ -152,27 +152,45 @@ descriptor recorder plId =
152152
, pluginRules = collectRecordsRule recorder *> collectNamesRule
153153
}
154154

155+
data RecordConversionType
156+
= RecordWildcardExpansion
157+
| RecordTraditionalSyntaxConversion
158+
159+
data RecordConversion =
160+
RecordConversion
161+
Int -- ^ uid
162+
RecordConversionType
163+
164+
-- | Given a record, determine whether it is a case of wildcard expansion
165+
-- or a conversion to the traditional record syntax.
166+
getConversionType :: RecordInfo -> Maybe RecordConversionType
167+
getConversionType = \case
168+
-- Only fully saturated constructor applications can be converted to
169+
-- the record syntax through the code action
170+
RecordInfoApp _ (RecordAppExpr Unsaturated _ _) -> Nothing
171+
RecordInfoApp {} -> Just RecordTraditionalSyntaxConversion
172+
_ -> Just RecordWildcardExpansion
173+
155174
codeActionProvider :: PluginMethodHandler IdeState 'Method_TextDocumentCodeAction
156175
codeActionProvider ideState _ (CodeActionParams _ _ docId range _) = do
157176
nfp <- getNormalizedFilePathE (docId ^. L.uri)
158177
CRR {crCodeActions, crCodeActionResolve, enabledExtensions} <- runActionE "ExplicitFields.CollectRecords" ideState $ useE CollectRecords nfp
159178
-- All we need to build a code action is the list of extensions, and a int to
160179
-- allow us to resolve it later.
161-
let recordsWithUid = [ (uid, record)
180+
let recordsWithUid = [ (RecordConversion uid conversionType, record)
162181
| uid <- RangeMap.filterByRange range crCodeActions
163182
, Just record <- [IntMap.lookup uid crCodeActionResolve]
164-
-- Only fully saturated constructor applications can be
165-
-- converted to the record syntax through the code action
166-
, isConvertible record
183+
, Just conversionType <- [getConversionType record]
167184
]
168185
recordsOnly = map snd recordsWithUid
169186
sortedRecords = sortOn (recordDepth recordsOnly . snd) recordsWithUid
170187
pure $ InL $ case sortedRecords of
171188
(top : _) -> [mkCodeAction enabledExtensions (fst top)]
172189
[] -> []
173190
where
174-
mkCodeAction exts uid = InR CodeAction
175-
{ _title = mkTitle exts -- TODO: `Expand positional record` without NamedFieldPuns if RecordInfoApp
191+
mkCodeAction :: [Extension] -> RecordConversion -> Command |? CodeAction
192+
mkCodeAction exts (RecordConversion uid conversionType) = InR CodeAction
193+
{ _title = mkTitle exts conversionType
176194
, _kind = Just CodeActionKind_RefactorRewrite
177195
, _diagnostics = Nothing
178196
, _isPreferred = Nothing
@@ -182,11 +200,6 @@ codeActionProvider ideState _ (CodeActionParams _ _ docId range _) = do
182200
, _data_ = Just $ toJSON uid
183201
}
184202

185-
isConvertible :: RecordInfo -> Bool
186-
isConvertible = \case
187-
RecordInfoApp _ (RecordAppExpr Unsaturated _ _) -> False
188-
_ -> True
189-
190203
codeActionResolveProvider :: ResolveFunction IdeState Int 'Method_CodeActionResolve
191204
codeActionResolveProvider ideState pId ca uri uid = do
192205
nfp <- getNormalizedFilePathE uri
@@ -251,7 +264,7 @@ inlayHintDotdotProvider _ state pId InlayHintParams {_textDocument = TextDocumen
251264
, _label = InR label
252265
, _kind = Nothing -- neither a type nor a parameter
253266
, _textEdits = Just textEdits -- same as CodeAction
254-
, _tooltip = Just $ InL (mkTitle enabledExtensions) -- same as CodeAction
267+
, _tooltip = Just $ InL (mkTitle enabledExtensions RecordWildcardExpansion) -- same as CodeAction
255268
, _paddingLeft = Just True -- padding after dotdot
256269
, _paddingRight = Nothing
257270
, _data_ = Nothing
@@ -290,19 +303,23 @@ inlayHintPosRecProvider _ state _pId InlayHintParams {_textDocument = TextDocume
290303
, _label = InR $ pure (mkInlayHintLabelPart name fieldDefLoc)
291304
, _kind = Nothing -- neither a type nor a parameter
292305
, _textEdits = Just (maybeToList te) -- same as CodeAction
293-
, _tooltip = Just $ InL "Expand positional record" -- same as CodeAction
306+
, _tooltip = Just $ InL (mkTitle [] RecordTraditionalSyntaxConversion) -- same as CodeAction
294307
, _paddingLeft = Nothing
295308
, _paddingRight = Nothing
296309
, _data_ = Nothing
297310
}
298311

299312
mkInlayHintLabelPart name loc = InlayHintLabelPart (printFieldName (pprNameUnqualified name) <> "=") Nothing loc Nothing
300313

301-
mkTitle :: [Extension] -> Text
302-
mkTitle exts = "Expand record wildcard"
303-
<> if NamedFieldPuns `elem` exts
304-
then mempty
305-
else " (needs extension: NamedFieldPuns)"
314+
mkTitle :: [Extension] -> RecordConversionType -> Text
315+
mkTitle exts = \case
316+
RecordWildcardExpansion ->
317+
"Expand record wildcard"
318+
<> if NamedFieldPuns `elem` exts
319+
then mempty
320+
else " (needs extension: NamedFieldPuns)"
321+
RecordTraditionalSyntaxConversion ->
322+
"Convert to traditional record syntax"
306323

307324
-- Calculate the nesting depth of a record by counting how many other records
308325
-- contain it. Used to prioritize more deeply nested records in code actions.

plugins/hls-explicit-record-fields-plugin/test/Main.hs

Lines changed: 54 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -22,24 +22,24 @@ plugin = mkPluginTestDescriptor ExplicitFields.descriptor "explicit-fields"
2222
test :: TestTree
2323
test = testGroup "explicit-fields"
2424
[ testGroup "code actions"
25-
[ mkTest "WildcardOnly" "WildcardOnly" 12 10 12 20
26-
, mkTest "Unused" "Unused" 12 10 12 20
27-
, mkTest "Unused2" "Unused2" 12 10 12 20
28-
, mkTest "WithPun" "WithPun" 13 10 13 25
29-
, mkTest "WithExplicitBind" "WithExplicitBind" 12 10 12 32
30-
, mkTest "Mixed" "Mixed" 14 10 14 37
31-
, mkTest "Construction" "Construction" 16 5 16 15
32-
, mkTest "PositionalConstruction" "PositionalConstruction" 15 5 15 15
33-
, mkTest "HsExpanded1" "HsExpanded1" 17 10 17 20
34-
, mkTest "HsExpanded2" "HsExpanded2" 23 10 23 22
25+
[ mkExpansionTest "WildcardOnly" "WildcardOnly" 12 10 12 20
26+
, mkExpansionTest "Unused" "Unused" 12 10 12 20
27+
, mkExpansionTest "Unused2" "Unused2" 12 10 12 20
28+
, mkExpansionTest "WithPun" "WithPun" 13 10 13 25
29+
, mkExpansionTest "WithExplicitBind" "WithExplicitBind" 12 10 12 32
30+
, mkExpansionTest "Mixed" "Mixed" 14 10 14 37
31+
, mkExpansionTest "Construction" "Construction" 16 5 16 15
32+
, mkConversionTest "PositionalConstruction" "PositionalConstruction" 15 5 15 15
33+
, mkExpansionTest "HsExpanded1" "HsExpanded1" 17 10 17 20
34+
, mkExpansionTest "HsExpanded2" "HsExpanded2" 23 10 23 22
3535
, mkTestNoAction "ExplicitBinds" "ExplicitBinds" 11 10 11 52
3636
, mkTestNoAction "Puns" "Puns" 12 10 12 31
3737
, mkTestNoAction "Infix" "Infix" 11 11 11 31
3838
, mkTestNoAction "Prefix" "Prefix" 10 11 10 28
3939
, mkTestNoAction "PartiallyAppliedCon" "PartiallyAppliedCon" 7 8 7 12
40-
, mkTest "PolymorphicRecordConstruction" "PolymorphicRecordConstruction" 15 5 15 15
41-
, mkTest "CursorAwarePositional" "CursorPositional" 15 26 15 34
42-
, mkTest "CursorAwareRecords" "CursorRecords" 9 40 9 40
40+
, mkConversionTest "PolymorphicRecordConstruction" "PolymorphicRecordConstruction" 15 5 15 15
41+
, mkConversionTest "CursorAwarePositional" "CursorPositional" 15 26 15 34
42+
, mkExpansionTest "CursorAwareRecords" "CursorRecords" 9 40 9 40
4343
]
4444
, testGroup "inlay hints"
4545
[ mkInlayHintsTest "Construction" Nothing 16 $ \ih -> do
@@ -86,19 +86,19 @@ test = testGroup "explicit-fields"
8686
[ defInlayHint { _position = Position 15 11
8787
, _label = InR [ foo ]
8888
, _textEdits = Just [ mkLineTextEdit "MyRec { foo = a, bar = b, baz = c }" 15 5 16 ]
89-
, _tooltip = Just $ InL "Expand positional record"
89+
, _tooltip = Just $ InL "Convert to traditional record syntax"
9090
, _paddingLeft = Nothing
9191
}
9292
, defInlayHint { _position = Position 15 13
9393
, _label = InR [ bar ]
9494
, _textEdits = Just [ mkLineTextEdit "MyRec { foo = a, bar = b, baz = c }" 15 5 16 ]
95-
, _tooltip = Just $ InL "Expand positional record"
95+
, _tooltip = Just $ InL "Convert to traditional record syntax"
9696
, _paddingLeft = Nothing
9797
}
9898
, defInlayHint { _position = Position 15 15
9999
, _label = InR [ baz ]
100100
, _textEdits = Just [ mkLineTextEdit "MyRec { foo = a, bar = b, baz = c }" 15 5 16 ]
101-
, _tooltip = Just $ InL "Expand positional record"
101+
, _tooltip = Just $ InL "Convert to traditional record syntax"
102102
, _paddingLeft = Nothing
103103
}
104104
]
@@ -111,19 +111,19 @@ test = testGroup "explicit-fields"
111111
[ defInlayHint { _position = Position 15 11
112112
, _label = InR [ foo ]
113113
, _textEdits = Just [ mkLineTextEdit "MyRec { foo = a, bar = b, baz = c }" 15 5 16 ]
114-
, _tooltip = Just $ InL "Expand positional record"
114+
, _tooltip = Just $ InL "Convert to traditional record syntax"
115115
, _paddingLeft = Nothing
116116
}
117117
, defInlayHint { _position = Position 15 13
118118
, _label = InR [ bar ]
119119
, _textEdits = Just [ mkLineTextEdit "MyRec { foo = a, bar = b, baz = c }" 15 5 16 ]
120-
, _tooltip = Just $ InL "Expand positional record"
120+
, _tooltip = Just $ InL "Convert to traditional record syntax"
121121
, _paddingLeft = Nothing
122122
}
123123
, defInlayHint { _position = Position 15 15
124124
, _label = InR [ baz ]
125125
, _textEdits = Just [ mkLineTextEdit "MyRec { foo = a, bar = b, baz = c }" 15 5 16 ]
126-
, _tooltip = Just $ InL "Expand positional record"
126+
, _tooltip = Just $ InL "Convert to traditional record syntax"
127127
, _paddingLeft = Nothing
128128
}
129129
]
@@ -144,7 +144,7 @@ test = testGroup "explicit-fields"
144144
[defInlayHint { _position = Position 13 21
145145
, _label = InR [ foo ]
146146
, _textEdits = Just [ mkLineTextEdit "MyRec { foo = 5 }" 13 15 22 ]
147-
, _tooltip = Just $ InL "Expand positional record"
147+
, _tooltip = Just $ InL "Convert to traditional record syntax"
148148
, _paddingLeft = Nothing
149149
}]
150150
, mkInlayHintsTest "HsExpanded1DuplicateRecordFields" (Just " (positional)") 13 $ \ih -> do
@@ -154,7 +154,7 @@ test = testGroup "explicit-fields"
154154
[defInlayHint { _position = Position 13 21
155155
, _label = InR [ foo ]
156156
, _textEdits = Just [ mkLineTextEdit "MyRec { foo = 5 }" 13 15 22 ]
157-
, _tooltip = Just $ InL "Expand positional record"
157+
, _tooltip = Just $ InL "Convert to traditional record syntax"
158158
, _paddingLeft = Nothing
159159
}]
160160
, mkInlayHintsTest "HsExpanded2" Nothing 23 $ \ih -> do
@@ -174,7 +174,7 @@ test = testGroup "explicit-fields"
174174
[defInlayHint { _position = Position 16 21
175175
, _label = InR [ foo ]
176176
, _textEdits = Just [ mkLineTextEdit "MyRec { foo = 5 }" 16 15 22 ]
177-
, _tooltip = Just $ InL "Expand positional record"
177+
, _tooltip = Just $ InL "Convert to traditional record syntax"
178178
, _paddingLeft = Nothing
179179
}]
180180
, mkInlayHintsTest "Mixed" Nothing 14 $ \ih -> do
@@ -278,19 +278,19 @@ test = testGroup "explicit-fields"
278278
[ defInlayHint { _position = Position 15 11
279279
, _label = InR [ foo ]
280280
, _textEdits = Just [ mkLineTextEdit "MyRec { foo = a, bar = b, baz = c }" 15 5 16 ]
281-
, _tooltip = Just $ InL "Expand positional record"
281+
, _tooltip = Just $ InL "Convert to traditional record syntax"
282282
, _paddingLeft = Nothing
283283
}
284284
, defInlayHint { _position = Position 15 13
285285
, _label = InR [ bar ]
286286
, _textEdits = Just [ mkLineTextEdit "MyRec { foo = a, bar = b, baz = c }" 15 5 16 ]
287-
, _tooltip = Just $ InL "Expand positional record"
287+
, _tooltip = Just $ InL "Convert to traditional record syntax"
288288
, _paddingLeft = Nothing
289289
}
290290
, defInlayHint { _position = Position 15 15
291291
, _label = InR [ baz ]
292292
, _textEdits = Just [ mkLineTextEdit "MyRec { foo = a, bar = b, baz = c }" 15 5 16 ]
293-
, _tooltip = Just $ InL "Expand positional record"
293+
, _tooltip = Just $ InL "Convert to traditional record syntax"
294294
, _paddingLeft = Nothing
295295
}
296296
]
@@ -350,30 +350,30 @@ test = testGroup "explicit-fields"
350350
{ _position = Position 15 14
351351
, _label = InR [ middle ]
352352
, _textEdits = Just [ mkLineTextEdit "RecOuter { middle = (RecMiddle (RecInner 'c' 42)) }" 15 5 43 ]
353-
, _tooltip = Just $ InL "Expand positional record"
353+
, _tooltip = Just $ InL "Convert to traditional record syntax"
354354
, _paddingLeft = Nothing
355355
}
356356
, defInlayHint
357357
{ _position = Position 15 25
358358
, _label = InR [ inner ]
359359
, _textEdits = Just [ mkLineTextEdit "RecMiddle { inner = (RecInner 'c' 42) }" 15 15 42 ]
360-
, _tooltip = Just $ InL "Expand positional record"
360+
, _tooltip = Just $ InL "Convert to traditional record syntax"
361361
, _paddingLeft = Nothing
362362
}
363363
, defInlayHint
364364
{ _position = Position 15 35
365365
, _label = InR [ foo ]
366366
, _textEdits =
367367
Just [ mkLineTextEdit "RecInner { foo = 'c', bar = 42 }" 15 26 41 ]
368-
, _tooltip = Just $ InL "Expand positional record"
368+
, _tooltip = Just $ InL "Convert to traditional record syntax"
369369
, _paddingLeft = Nothing
370370
}
371371
, defInlayHint
372372
{ _position = Position 15 39
373373
, _label = InR [ bar ]
374374
, _textEdits =
375375
Just [ mkLineTextEdit "RecInner { foo = 'c', bar = 42 }" 15 26 41 ]
376-
, _tooltip = Just $ InL "Expand positional record"
376+
, _tooltip = Just $ InL "Convert to traditional record syntax"
377377
, _paddingLeft = Nothing
378378
}
379379
]
@@ -395,35 +395,47 @@ mkTestNoAction title fp x1 y1 x2 y2 =
395395
testCase title $
396396
runSessionWithServer def plugin (testDataDir </> "noop") $ do
397397
doc <- openDoc (fp <.> "hs") "haskell"
398-
actions <- getExplicitFieldsActions doc x1 y1 x2 y2
398+
actions <- getActions isExplicitFieldsCodeAction doc x1 y1 x2 y2
399399
liftIO $ actions @?= []
400400

401-
mkTestWithCount :: Int -> TestName -> FilePath -> UInt -> UInt -> UInt -> UInt -> TestTree
402-
mkTestWithCount cnt title fp x1 y1 x2 y2 =
401+
mkTestWithCountAndMessage :: (CodeAction -> Bool) -> Int -> TestName -> FilePath -> UInt -> UInt -> UInt -> UInt -> TestTree
402+
mkTestWithCountAndMessage msgPredicate cnt title fp x1 y1 x2 y2 =
403403
goldenWithHaskellAndCaps def codeActionResolveCaps plugin title testDataDir fp "expected" "hs" $ \doc -> do
404-
acts@(act:_) <- getExplicitFieldsActions doc x1 y1 x2 y2
404+
acts@(act:_) <- getActions msgPredicate doc x1 y1 x2 y2
405405
liftIO $ length acts @?= cnt
406406
executeCodeAction act
407407

408-
mkTest :: TestName -> FilePath -> UInt -> UInt -> UInt -> UInt -> TestTree
409-
mkTest = mkTestWithCount 1
408+
mkExpansionTest :: TestName -> FilePath -> UInt -> UInt -> UInt -> UInt -> TestTree
409+
mkExpansionTest = mkTestWithCountAndMessage isRecWildcardExpansionCodeAction 1
410410

411-
getExplicitFieldsActions
412-
:: TextDocumentIdentifier
411+
mkConversionTest :: TestName -> FilePath -> UInt -> UInt -> UInt -> UInt -> TestTree
412+
mkConversionTest = mkTestWithCountAndMessage isRecLiteralConversionCodeAction 1
413+
414+
getActions
415+
:: (CodeAction -> Bool) -> TextDocumentIdentifier
413416
-> UInt -> UInt -> UInt -> UInt
414417
-> Session [CodeAction]
415-
getExplicitFieldsActions doc x1 y1 x2 y2 =
416-
findExplicitFieldsAction <$> getAndResolveCodeActions doc range
418+
getActions p doc x1 y1 x2 y2 =
419+
findAction p <$> getAndResolveCodeActions doc range
417420
where
418421
range = Range (Position x1 y1) (Position x2 y2)
419422

420-
findExplicitFieldsAction :: [a |? CodeAction] -> [CodeAction]
421-
findExplicitFieldsAction = filter isExplicitFieldsCodeAction . rights . map toEither
423+
findAction :: (CodeAction -> Bool) -> [a |? CodeAction] -> [CodeAction]
424+
findAction p = filter p . rights . map toEither
422425

423426
isExplicitFieldsCodeAction :: CodeAction -> Bool
424-
isExplicitFieldsCodeAction CodeAction {_title} =
427+
isExplicitFieldsCodeAction act =
428+
isRecWildcardExpansionCodeAction act
429+
|| isRecLiteralConversionCodeAction act
430+
431+
isRecWildcardExpansionCodeAction :: CodeAction -> Bool
432+
isRecWildcardExpansionCodeAction CodeAction {_title} =
425433
"Expand record wildcard" `T.isPrefixOf` _title
426434

435+
isRecLiteralConversionCodeAction :: CodeAction -> Bool
436+
isRecLiteralConversionCodeAction CodeAction {_title} =
437+
"Convert to traditional record syntax" `T.isPrefixOf` _title
438+
427439
defInlayHint :: InlayHint
428440
defInlayHint =
429441
InlayHint

0 commit comments

Comments
 (0)