Skip to content

Commit 583b3d0

Browse files
authored
Filter completions for record fields in patterns (#15903)
1 parent 60d2dbd commit 583b3d0

File tree

8 files changed

+215
-152
lines changed

8 files changed

+215
-152
lines changed

src/Compiler/Service/FSharpCheckerResults.fs

Lines changed: 133 additions & 146 deletions
Original file line numberDiff line numberDiff line change
@@ -800,6 +800,28 @@ type internal TypeCheckInfo
800800
| Item.Types (_, ty :: _) when isInterfaceTy g ty -> true
801801
| _ -> false
802802

803+
/// Is the item suitable for completion in a pattern
804+
let IsPatternCandidate (item: CompletionItem) =
805+
match item.Item with
806+
| Item.Value v -> v.LiteralValue.IsSome
807+
| Item.ILField field -> field.LiteralValue.IsSome
808+
| Item.ActivePatternCase _
809+
| Item.ExnCase _
810+
| Item.ModuleOrNamespaces _
811+
| Item.Types _
812+
| Item.UnionCase _ -> true
813+
| _ -> false
814+
815+
/// Is the item suitable for completion in a type application or type annotation
816+
let IsTypeCandidate (item: CompletionItem) =
817+
match item.Item with
818+
| Item.ModuleOrNamespaces _
819+
| Item.Types _
820+
| Item.TypeVar _
821+
| Item.UnqualifiedType _
822+
| Item.ExnCase _ -> true
823+
| _ -> false
824+
803825
/// Return only items with the specified name, modulo "Attribute" for type completions
804826
let FilterDeclItemsByResidue (getItem: 'a -> Item) residue (items: 'a list) =
805827
let attributedResidue = residue + "Attribute"
@@ -939,6 +961,10 @@ type internal TypeCheckInfo
939961
Unresolved = None
940962
}
941963

964+
let getItem (x: ItemWithInst) = x.Item
965+
966+
let getItem2 (x: CompletionItem) = x.Item
967+
942968
/// Checks whether the suggested name is unused.
943969
/// In the future we could use an increasing numeric suffix for conflict resolution
944970
let CreateCompletionItemForSuggestedPatternName (pos: pos) name =
@@ -1044,7 +1070,7 @@ type internal TypeCheckInfo
10441070
|> List.map (fun (name, overloads) ->
10451071
Item.MethodGroup(name, overloads, None)
10461072
|> ItemWithNoInst
1047-
|> CompletionItem ValueNone ValueNone)
1073+
|> DefaultCompletionItem)
10481074

10491075
Some(overridableMethods, nenv.DisplayEnv, m)
10501076
| _ -> None)
@@ -1063,12 +1089,77 @@ type internal TypeCheckInfo
10631089
else
10641090
Item.UnionCaseField(uci, index)
10651091
|> ItemWithNoInst
1066-
|> CompletionItem ValueNone ValueNone
1092+
|> DefaultCompletionItem
10671093
|> Some)
10681094
|> Some
10691095
| _ -> None)
10701096

1071-
let getItem (x: ItemWithInst) = x.Item
1097+
let GetCompletionsForUnionCaseField pos indexOrName caseIdRange isTheOnlyField declaredItems =
1098+
let declaredItems =
1099+
declaredItems
1100+
|> Option.bind (FilterRelevantItemsBy getItem2 None IsPatternCandidate)
1101+
1102+
// When the user types `fun (Case (x| )) ->`, we do not yet know whether the intention is to use positional or named arguments,
1103+
// so let's show options for both.
1104+
let fields indexOrName isTheOnlyField (uci: UnionCaseInfo) =
1105+
match indexOrName, isTheOnlyField with
1106+
| Choice1Of2 (Some 0), true ->
1107+
uci.UnionCase.RecdFields
1108+
|> List.mapi (fun index _ -> Item.UnionCaseField(uci, index) |> ItemWithNoInst |> DefaultCompletionItem)
1109+
| _ -> []
1110+
1111+
sResolutions.CapturedNameResolutions
1112+
|> ResizeArray.tryPick (fun r ->
1113+
match r.Item with
1114+
| Item.UnionCase (uci, _) when equals r.Range caseIdRange ->
1115+
let list =
1116+
declaredItems
1117+
|> Option.map p13
1118+
|> Option.defaultValue []
1119+
|> List.append (fields indexOrName isTheOnlyField uci)
1120+
1121+
Some(SuggestNameForUnionCaseFieldPattern g caseIdRange.End pos uci indexOrName list, r.DisplayEnv, r.Range)
1122+
| _ -> None)
1123+
|> Option.orElse declaredItems
1124+
1125+
let GetCompletionsForRecordField pos referencedFields declaredItems =
1126+
declaredItems
1127+
|> Option.map (fun (items: CompletionItem list, denv, range) ->
1128+
let fields =
1129+
// Try to find a name resolution for any of the referenced fields, and through it access all available fields of the record
1130+
referencedFields
1131+
|> List.tryPick (fun (_, fieldRange) ->
1132+
sResolutions.CapturedNameResolutions
1133+
|> ResizeArray.tryPick (fun cnr ->
1134+
match cnr.Item with
1135+
| Item.RecdField info when equals cnr.Range fieldRange ->
1136+
info.TyconRef.AllFieldAsRefList
1137+
|> List.choose (fun field ->
1138+
if
1139+
referencedFields
1140+
|> List.exists (fun (fieldName, _) -> fieldName = field.DisplayName)
1141+
then
1142+
None
1143+
else
1144+
FreshenRecdFieldRef ncenv field.Range field |> Item.RecdField |> Some)
1145+
|> Some
1146+
| _ -> None))
1147+
|> Option.defaultWith (fun () ->
1148+
// Fall back to showing all record field names in scope
1149+
let (nenv, _), _ = GetBestEnvForPos pos
1150+
getRecordFieldsInScope nenv)
1151+
|> List.map (ItemWithNoInst >> DefaultCompletionItem)
1152+
1153+
let items =
1154+
items
1155+
|> List.filter (fun item ->
1156+
match item.Item with
1157+
| Item.ModuleOrNamespaces _ -> true
1158+
| Item.Types (_, ty :: _) -> isRecdTy g ty
1159+
| _ -> false)
1160+
|> List.append fields
1161+
1162+
items, denv, range)
10721163

10731164
let GetDeclaredItems
10741165
(
@@ -1316,6 +1407,22 @@ type internal TypeCheckInfo
13161407
| atStart when atStart = 0 -> 0
13171408
| otherwise -> otherwise - 1
13181409

1410+
let getDeclaredItemsNotInRangeOpWithAllSymbols () =
1411+
GetDeclaredItems(
1412+
parseResultsOpt,
1413+
lineStr,
1414+
origLongIdentOpt,
1415+
colAtEndOfNamesAndResidue,
1416+
residueOpt,
1417+
lastDotPos,
1418+
line,
1419+
loc,
1420+
filterCtors,
1421+
resolveOverloads,
1422+
false,
1423+
getAllSymbols
1424+
)
1425+
13191426
let pos = mkPos line colAtEndOfNamesAndResidue
13201427

13211428
// Look for a "special" completion context
@@ -1445,21 +1552,7 @@ type internal TypeCheckInfo
14451552
| Some (CompletionContext.ParameterList (endPos, fields)) ->
14461553
let results = GetNamedParametersAndSettableFields endPos
14471554

1448-
let declaredItems =
1449-
GetDeclaredItems(
1450-
parseResultsOpt,
1451-
lineStr,
1452-
origLongIdentOpt,
1453-
colAtEndOfNamesAndResidue,
1454-
residueOpt,
1455-
lastDotPos,
1456-
line,
1457-
loc,
1458-
filterCtors,
1459-
resolveOverloads,
1460-
false,
1461-
getAllSymbols
1462-
)
1555+
let declaredItems = getDeclaredItemsNotInRangeOpWithAllSymbols ()
14631556

14641557
match results with
14651558
| NameResResult.Members (items, denv, m) ->
@@ -1484,20 +1577,7 @@ type internal TypeCheckInfo
14841577
| _ -> declaredItems
14851578

14861579
| Some (CompletionContext.AttributeApplication) ->
1487-
GetDeclaredItems(
1488-
parseResultsOpt,
1489-
lineStr,
1490-
origLongIdentOpt,
1491-
colAtEndOfNamesAndResidue,
1492-
residueOpt,
1493-
lastDotPos,
1494-
line,
1495-
loc,
1496-
filterCtors,
1497-
resolveOverloads,
1498-
false,
1499-
getAllSymbols
1500-
)
1580+
getDeclaredItemsNotInRangeOpWithAllSymbols ()
15011581
|> Option.map (fun (items, denv, m) ->
15021582
items
15031583
|> List.filter (fun cItem ->
@@ -1509,20 +1589,7 @@ type internal TypeCheckInfo
15091589
m)
15101590

15111591
| Some (CompletionContext.OpenDeclaration isOpenType) ->
1512-
GetDeclaredItems(
1513-
parseResultsOpt,
1514-
lineStr,
1515-
origLongIdentOpt,
1516-
colAtEndOfNamesAndResidue,
1517-
residueOpt,
1518-
lastDotPos,
1519-
line,
1520-
loc,
1521-
filterCtors,
1522-
resolveOverloads,
1523-
false,
1524-
getAllSymbols
1525-
)
1592+
getDeclaredItemsNotInRangeOpWithAllSymbols ()
15261593
|> Option.map (fun (items, denv, m) ->
15271594
items
15281595
|> List.filter (fun x ->
@@ -1541,108 +1608,28 @@ type internal TypeCheckInfo
15411608
| Some CompletionContext.TypeAbbreviationOrSingleCaseUnion
15421609
// Completion at 'Field1: ...'
15431610
| Some (CompletionContext.RecordField (RecordContext.Declaration false)) ->
1544-
GetDeclaredItems(
1545-
parseResultsOpt,
1546-
lineStr,
1547-
origLongIdentOpt,
1548-
colAtEndOfNamesAndResidue,
1549-
residueOpt,
1550-
lastDotPos,
1551-
line,
1552-
loc,
1553-
filterCtors,
1554-
resolveOverloads,
1555-
false,
1556-
getAllSymbols
1557-
)
1558-
|> Option.map (fun (items, denv, m) ->
1559-
items
1560-
|> List.filter (fun cItem ->
1561-
match cItem.Item with
1562-
| Item.ModuleOrNamespaces _
1563-
| Item.Types _
1564-
| Item.TypeVar _
1565-
| Item.UnqualifiedType _
1566-
| Item.ExnCase _ -> true
1567-
| _ -> false),
1568-
denv,
1569-
m)
1570-
1571-
| Some (CompletionContext.Pattern (PatternContext.UnionCaseFieldIdentifier (referencedFields, caseIdRange))) ->
1572-
GetUnionCaseFields caseIdRange referencedFields
1573-
|> Option.map (fun completions ->
1574-
let (nenv, _ad), m = GetBestEnvForPos pos
1575-
completions, nenv.DisplayEnv, m)
1611+
getDeclaredItemsNotInRangeOpWithAllSymbols ()
1612+
|> Option.bind (FilterRelevantItemsBy getItem2 None IsTypeCandidate)
15761613

15771614
| Some (CompletionContext.Pattern patternContext) ->
1578-
let declaredItems =
1579-
GetDeclaredItems(
1580-
parseResultsOpt,
1581-
lineStr,
1582-
origLongIdentOpt,
1583-
colAtEndOfNamesAndResidue,
1584-
residueOpt,
1585-
lastDotPos,
1586-
line,
1587-
loc,
1588-
filterCtors,
1589-
resolveOverloads,
1590-
false,
1591-
getAllSymbols
1592-
)
1593-
|> Option.map (fun (items, denv, range) ->
1594-
let filtered =
1595-
items
1596-
|> List.filter (fun item ->
1597-
match item.Item with
1598-
| Item.Value v -> v.LiteralValue.IsSome
1599-
| Item.ILField field -> field.LiteralValue.IsSome
1600-
| Item.ActivePatternCase _
1601-
| Item.ExnCase _
1602-
| Item.ModuleOrNamespaces _
1603-
| Item.NewDef _
1604-
| Item.Types _
1605-
| Item.UnionCase _ -> true
1606-
| _ -> false)
1607-
1608-
filtered, denv, range)
1609-
1610-
let indexOrName, caseIdRange =
1611-
match patternContext with
1612-
| PatternContext.PositionalUnionCaseField (index, _, m) -> Choice1Of2 index, m
1613-
| PatternContext.NamedUnionCaseField (name, m) -> Choice2Of2 name, m
1614-
| PatternContext.UnionCaseFieldIdentifier _
1615-
| PatternContext.Other -> Choice1Of2 None, range0
1616-
1617-
// No special handling other than filtering out items that may not appear in a pattern
1618-
if equals caseIdRange range0 then
1619-
declaredItems
1620-
else
1621-
// When the user types `fun (Case (x| )) ->`, we do not yet know whether the intention is to use positional or named arguments,
1622-
// so let's show options for both.
1623-
let fields patternContext (uci: UnionCaseInfo) =
1624-
match patternContext with
1625-
| PatternContext.PositionalUnionCaseField (Some 0, true, _) ->
1626-
uci.UnionCase.RecdFields
1627-
|> List.mapi (fun index _ ->
1628-
Item.UnionCaseField(uci, index)
1629-
|> ItemWithNoInst
1630-
|> CompletionItem ValueNone ValueNone)
1631-
| _ -> []
1632-
1633-
sResolutions.CapturedNameResolutions
1634-
|> ResizeArray.tryPick (fun r ->
1635-
match r.Item with
1636-
| Item.UnionCase (uci, _) when equals r.Range caseIdRange ->
1637-
let list =
1638-
declaredItems
1639-
|> Option.map p13
1640-
|> Option.defaultValue []
1641-
|> List.append (fields patternContext uci)
1642-
1643-
Some(SuggestNameForUnionCaseFieldPattern g caseIdRange.End pos uci indexOrName list, r.DisplayEnv, r.Range)
1644-
| _ -> None)
1645-
|> Option.orElse declaredItems
1615+
match patternContext with
1616+
| PatternContext.UnionCaseFieldIdentifier (referencedFields, caseIdRange) ->
1617+
GetUnionCaseFields caseIdRange referencedFields
1618+
|> Option.map (fun completions ->
1619+
let (nenv, _ad), m = GetBestEnvForPos pos
1620+
completions, nenv.DisplayEnv, m)
1621+
| PatternContext.PositionalUnionCaseField (fieldIndex, isTheOnlyField, caseIdRange) ->
1622+
getDeclaredItemsNotInRangeOpWithAllSymbols ()
1623+
|> GetCompletionsForUnionCaseField pos (Choice1Of2 fieldIndex) caseIdRange isTheOnlyField
1624+
| PatternContext.NamedUnionCaseField (fieldName, caseIdRange) ->
1625+
getDeclaredItemsNotInRangeOpWithAllSymbols ()
1626+
|> GetCompletionsForUnionCaseField pos (Choice2Of2 fieldName) caseIdRange false
1627+
| PatternContext.RecordFieldIdentifier referencedFields ->
1628+
getDeclaredItemsNotInRangeOpWithAllSymbols ()
1629+
|> GetCompletionsForRecordField pos referencedFields
1630+
| PatternContext.Other ->
1631+
getDeclaredItemsNotInRangeOpWithAllSymbols ()
1632+
|> Option.bind (FilterRelevantItemsBy getItem2 None IsPatternCandidate)
16461633

16471634
| Some (CompletionContext.MethodOverride enclosingTypeNameRange) -> GetOverridableMethods pos enclosingTypeNameRange
16481635

src/Compiler/Service/ServiceParsedInputOps.fs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ type PatternContext =
6464
/// Completing union case field identifier in a pattern (e.g. fun (Case (field1 = a; fie| )) -> )
6565
| UnionCaseFieldIdentifier of referencedFields: string list * caseIdRange: range
6666

67+
/// Completing a record field identifier in a pattern (e.g. fun { Field1 = a; Fie| } -> )
68+
| RecordFieldIdentifier of referencedFields: (string * range) list
69+
6770
/// Any other position in a pattern that does not need special handling
6871
| Other
6972

@@ -1310,10 +1313,28 @@ module ParsedInput =
13101313
| _ ->
13111314
pats
13121315
|> List.tryPick (fun pat -> TryGetCompletionContextInPattern false pat None pos)
1316+
| SynPat.Record (fieldPats = pats) ->
1317+
pats
1318+
|> List.tryPick (fun ((_, fieldId), _, pat) ->
1319+
if rangeContainsPos fieldId.idRange pos then
1320+
let referencedFields = pats |> List.map (fun ((_, x), _, _) -> x.idText, x.idRange)
1321+
Some(CompletionContext.Pattern(PatternContext.RecordFieldIdentifier referencedFields))
1322+
elif rangeContainsPos pat.Range pos then
1323+
TryGetCompletionContextInPattern false pat None pos
1324+
else
1325+
None)
1326+
|> Option.orElseWith (fun () ->
1327+
// Last resort - check for fun { Field1 = a; F| } ->
1328+
// That is, pos is after the last field and still within braces
1329+
if pats |> List.forall (fun (_, m, _) -> rangeBeforePos m pos) then
1330+
let referencedFields = pats |> List.map (fun ((_, x), _, _) -> x.idText, x.idRange)
1331+
Some(CompletionContext.Pattern(PatternContext.RecordFieldIdentifier referencedFields))
1332+
else
1333+
None)
13131334
| SynPat.Ands (pats = pats)
13141335
| SynPat.ArrayOrList (elementPats = pats) ->
13151336
pats
1316-
|> List.tryPick (fun pat -> TryGetCompletionContextInPattern suppressIdentifierCompletions pat None pos)
1337+
|> List.tryPick (fun pat -> TryGetCompletionContextInPattern false pat None pos)
13171338
| SynPat.Tuple (elementPats = pats; commaRanges = commas; range = m) ->
13181339
pats
13191340
|> List.indexed

src/Compiler/Service/ServiceParsedInputOps.fsi

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ type public PatternContext =
3636
/// Completing union case field identifier in a pattern (e.g. fun (Case (field1 = a; fie| )) -> )
3737
| UnionCaseFieldIdentifier of referencedFields: string list * caseIdRange: range
3838

39+
/// Completing a record field identifier in a pattern (e.g. fun { Field1 = a; Fie| } -> )
40+
| RecordFieldIdentifier of referencedFields: (string * range) list
41+
3942
/// Any other position in a pattern that does not need special handling
4043
| Other
4144

0 commit comments

Comments
 (0)