Skip to content
56 changes: 50 additions & 6 deletions src/Compiler/Service/FSharpCheckerResults.fs
Original file line number Diff line number Diff line change
Expand Up @@ -886,6 +886,7 @@ type internal TypeCheckInfo
| minfo :: _ -> CompletionItemKind.Method minfo.IsExtensionMember
| Item.AnonRecdField _
| Item.RecdField _
| Item.UnionCaseField _
| Item.Property _ -> CompletionItemKind.Property
| Item.Event _ -> CompletionItemKind.Event
| Item.ILField _
Expand All @@ -900,7 +901,6 @@ type internal TypeCheckInfo
| Item.TypeVar _
| Item.Types _
| Item.UnionCase _
| Item.UnionCaseField _
| Item.UnqualifiedType _
| Item.NewDef _
| Item.SetterArg _
Expand Down Expand Up @@ -1049,6 +1049,25 @@ type internal TypeCheckInfo
Some(overridableMethods, nenv.DisplayEnv, m)
| _ -> None)

/// Gets all field identifiers of a union case that can be referred to in a pattern.
let GetUnionCaseFields caseIdRange alreadyReferencedFields =
sResolutions.CapturedNameResolutions
|> ResizeArray.tryPick (fun r ->
match r.Item with
| Item.UnionCase (uci, _) when equals r.Range caseIdRange ->
uci.UnionCase.RecdFields
|> List.indexed
|> List.choose (fun (index, field) ->
if List.contains field.LogicalName alreadyReferencedFields then
None
else
Item.UnionCaseField(uci, index)
|> ItemWithNoInst
|> CompletionItem ValueNone ValueNone
|> Some)
|> Some
| _ -> None)

let getItem (x: ItemWithInst) = x.Item

let GetDeclaredItems
Expand Down Expand Up @@ -1549,6 +1568,12 @@ type internal TypeCheckInfo
denv,
m)

| Some (CompletionContext.Pattern (PatternContext.UnionCaseFieldIdentifier (referencedFields, caseIdRange))) ->
GetUnionCaseFields caseIdRange referencedFields
|> Option.map (fun completions ->
let (nenv, _ad), m = GetBestEnvForPos pos
completions, nenv.DisplayEnv, m)

| Some (CompletionContext.Pattern patternContext) ->
let declaredItems =
GetDeclaredItems(
Expand All @@ -1573,6 +1598,7 @@ type internal TypeCheckInfo
| Item.Value v -> v.LiteralValue.IsSome
| Item.ILField field -> field.LiteralValue.IsSome
| Item.ActivePatternCase _
| Item.ExnCase _
| Item.ModuleOrNamespaces _
| Item.NewDef _
| Item.Types _
Expand All @@ -1583,19 +1609,37 @@ type internal TypeCheckInfo

let indexOrName, caseIdRange =
match patternContext with
| PatternContext.PositionalUnionCaseField (index, m) -> Choice1Of2 index, m
| PatternContext.PositionalUnionCaseField (index, _, m) -> Choice1Of2 index, m
| PatternContext.NamedUnionCaseField (name, m) -> Choice2Of2 name, m
| PatternContext.UnionCaseFieldIdentifier _
| PatternContext.Other -> Choice1Of2 None, range0

// No special handling for PatternContext.Other other than filtering out non-literal values
// No special handling other than filtering out items that may not appear in a pattern
if equals caseIdRange range0 then
declaredItems
else
GetCapturedNameResolutions caseIdRange.End ResolveOverloads.Yes
// When the user types `fun (Case (x| )) ->`, we do not yet know whether the intention is to use positional or named arguments,
// so let's show options for both.
let fields patternContext (uci: UnionCaseInfo) =
match patternContext with
| PatternContext.PositionalUnionCaseField (Some 0, true, _) ->
uci.UnionCase.RecdFields
|> List.mapi (fun index _ ->
Item.UnionCaseField(uci, index)
|> ItemWithNoInst
|> CompletionItem ValueNone ValueNone)
| _ -> []

sResolutions.CapturedNameResolutions
|> ResizeArray.tryPick (fun r ->
match r.Item with
| Item.UnionCase (uci, _) ->
let list = declaredItems |> Option.map p13 |> Option.defaultValue []
| Item.UnionCase (uci, _) when equals r.Range caseIdRange ->
let list =
declaredItems
|> Option.map p13
|> Option.defaultValue []
|> List.append (fields patternContext uci)

Some(SuggestNameForUnionCaseFieldPattern g caseIdRange.End pos uci indexOrName list, r.DisplayEnv, r.Range)
| _ -> None)
|> Option.orElse declaredItems
Expand Down
65 changes: 46 additions & 19 deletions src/Compiler/Service/ServiceParsedInputOps.fs
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,18 @@ type RecordContext =

[<RequireQualifiedAccess>]
type PatternContext =
/// Completing union case field in a pattern (e.g. fun (Some v|) -> )
/// fieldIndex None signifies that the case identifier is followed by a single field, outside of parentheses
| PositionalUnionCaseField of fieldIndex: int option * caseIdRange: range
/// <summary>Completing union case field pattern (e.g. fun (Some v| ) -> ) or fun (Some (v| )) -> ). In theory, this could also be parameterized active pattern usage.</summary>
/// <param name="fieldIndex">Position in the tuple. <see cref="None">None</see> if there is no tuple, with only one field outside of parentheses - `Some v|`</param>
/// <param name="isTheOnlyField">True when completing the first field in the tuple and no other field is bound - `Case (a|)` but not `Case (a|, b)`</param>
/// <param name="caseIdRange">Range of the case identifier</param>
| PositionalUnionCaseField of fieldIndex: int option * isTheOnlyField: bool * caseIdRange: range

/// Completing union case field in a pattern (e.g. fun (Some (Value = v|) -> )
/// Completing union case field pattern (e.g. fun (Some (Value = v| )) -> )
| NamedUnionCaseField of fieldName: string * caseIdRange: range

/// Completing union case field identifier in a pattern (e.g. fun (Case (field1 = a; fie| )) -> )
| UnionCaseFieldIdentifier of referencedFields: string list * caseIdRange: range

/// Any other position in a pattern that does not need special handling
| Other

Expand Down Expand Up @@ -1261,28 +1266,46 @@ module ParsedInput =
let rec TryGetCompletionContextInPattern suppressIdentifierCompletions (pat: SynPat) previousContext pos =
match pat with
| SynPat.LongIdent (longDotId = id) when rangeContainsPos id.Range pos -> Some(CompletionContext.Pattern PatternContext.Other)
| SynPat.LongIdent (argPats = SynArgPats.NamePatPairs (pats = pats); longDotId = id) ->
| SynPat.LongIdent (argPats = SynArgPats.NamePatPairs (pats = pats; range = mPairs); longDotId = caseId; range = m) when
rangeContainsPos m pos
->
pats
|> List.tryPick (fun (patId, _, pat) ->
if rangeContainsPos patId.idRange pos then
Some CompletionContext.Invalid
|> List.tryPick (fun (fieldId, _, pat) ->
if rangeContainsPos fieldId.idRange pos then
let referencedFields = pats |> List.map (fun (id, _, _) -> id.idText)
Some(CompletionContext.Pattern(PatternContext.UnionCaseFieldIdentifier(referencedFields, caseId.Range)))
else
let context = Some(PatternContext.NamedUnionCaseField(patId.idText, id.Range))
let context = Some(PatternContext.NamedUnionCaseField(fieldId.idText, caseId.Range))
TryGetCompletionContextInPattern suppressIdentifierCompletions pat context pos)
|> Option.orElseWith (fun () ->
// Last resort - check for fun (Case (item1 = a; | )) ->
// That is, pos is after the last pair and still within parentheses
if rangeBeforePos mPairs pos then
let referencedFields = pats |> List.map (fun (id, _, _) -> id.idText)
Some(CompletionContext.Pattern(PatternContext.UnionCaseFieldIdentifier(referencedFields, caseId.Range)))
else
None)
| SynPat.LongIdent (argPats = SynArgPats.Pats pats; longDotId = id; range = m) when rangeContainsPos m pos ->
match pats with

// fun (Some v| ) ->
| [ SynPat.Named _ ] -> Some(CompletionContext.Pattern(PatternContext.PositionalUnionCaseField(None, id.Range)))
| [ SynPat.Named _ ] -> Some(CompletionContext.Pattern(PatternContext.PositionalUnionCaseField(None, true, id.Range)))

// fun (Case (| )) ->
| [ SynPat.Paren (SynPat.Const (SynConst.Unit, _), m) ] when rangeContainsPos m pos ->
Some(CompletionContext.Pattern(PatternContext.PositionalUnionCaseField(Some 0, id.Range)))
Some(CompletionContext.Pattern(PatternContext.PositionalUnionCaseField(Some 0, true, id.Range)))

// fun (Case (a| )) ->
// This could either be the first positional field pattern or the user might want to use named pairs
| [ SynPat.Paren (SynPat.Named _, _) ] ->
Some(CompletionContext.Pattern(PatternContext.PositionalUnionCaseField(Some 0, true, id.Range)))

// fun (Case (a| , b)) ->
| [ SynPat.Paren (SynPat.Tuple _ | SynPat.Named _ as pat, _) ] ->
TryGetCompletionContextInPattern false pat (Some(PatternContext.PositionalUnionCaseField(Some 0, id.Range))) pos
|> Option.orElseWith (fun () -> Some CompletionContext.Invalid)
| [ SynPat.Paren (SynPat.Tuple (elementPats = pats) as pat, _) ] ->
let context =
Some(PatternContext.PositionalUnionCaseField(Some 0, pats.Length = 1, id.Range))

TryGetCompletionContextInPattern false pat context pos

| _ ->
pats
Expand All @@ -1297,21 +1320,25 @@ module ParsedInput =
|> List.tryPick (fun (i, pat) ->
let context =
match previousContext with
| Some (PatternContext.PositionalUnionCaseField (_, caseIdRange)) ->
Some(PatternContext.PositionalUnionCaseField(Some i, caseIdRange))
| Some (PatternContext.PositionalUnionCaseField (_, isTheOnlyField, caseIdRange)) ->
Some(PatternContext.PositionalUnionCaseField(Some i, isTheOnlyField, caseIdRange))
| _ ->
// No preceding LongIdent => this is a tuple deconstruction
None

TryGetCompletionContextInPattern suppressIdentifierCompletions pat context pos)
|> Option.orElseWith (fun () ->
// Last resort - check for fun (Case (a, | )) ->
// Last resort - check for fun (Case (item1 = a, | )) ->
// That is, pos is after the last comma and before the end of the tuple
match previousContext, List.tryLast commas with
| Some (PatternContext.PositionalUnionCaseField (_, caseIdRange)), Some mComma when
| Some (PatternContext.PositionalUnionCaseField (_, isTheOnlyField, caseIdRange)), Some mComma when
rangeBeforePos mComma pos && rangeContainsPos m pos
->
Some(CompletionContext.Pattern(PatternContext.PositionalUnionCaseField(Some(pats.Length - 1), caseIdRange)))
Some(
CompletionContext.Pattern(
PatternContext.PositionalUnionCaseField(Some(pats.Length - 1), isTheOnlyField, caseIdRange)
)
)
| _ -> None)
| SynPat.Named (range = m) when rangeContainsPos m pos ->
if suppressIdentifierCompletions then
Expand Down
13 changes: 9 additions & 4 deletions src/Compiler/Service/ServiceParsedInputOps.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,18 @@ type public RecordContext =

[<RequireQualifiedAccess>]
type public PatternContext =
/// Completing union case field in a pattern (e.g. fun (Some v|) -> )
/// fieldIndex None signifies that the case identifier is followed by a single field, outside of parentheses
| PositionalUnionCaseField of fieldIndex: int option * caseIdRange: range
/// <summary>Completing union case field pattern (e.g. fun (Some v| ) -> ) or fun (Some (v| )) -> ). In theory, this could also be parameterized active pattern usage.</summary>
/// <param name="fieldIndex">Position in the tuple. <see cref="None">None</see> if there is no tuple, with only one field outside of parentheses - `Some v|`</param>
/// <param name="isTheOnlyField">True when completing the first field in the tuple and no other field is bound - `Case (a|)` but not `Case (a|, b)`</param>
/// <param name="caseIdRange">Range of the case identifier</param>
| PositionalUnionCaseField of fieldIndex: int option * isTheOnlyField: bool * caseIdRange: range

/// Completing union case field in a pattern (e.g. fun (Some (Value = v|) -> )
/// Completing union case field pattern (e.g. fun (Some (Value = v| )) -> )
| NamedUnionCaseField of fieldName: string * caseIdRange: range

/// Completing union case field identifier in a pattern (e.g. fun (Case (field1 = a; fie| )) -> )
| UnionCaseFieldIdentifier of referencedFields: string list * caseIdRange: range

/// Any other position in a pattern that does not need special handling
| Other

Expand Down
13 changes: 5 additions & 8 deletions src/Compiler/pars.fsy
Original file line number Diff line number Diff line change
Expand Up @@ -3480,16 +3480,14 @@ conjPatternElements:

namePatPairs:
| namePatPair opt_seps
{ [$1], lhs parseState }
{ [$1] }

| namePatPair seps namePatPairs
{ let rs, _ = $3
($1 :: rs), lhs parseState }
{ $1 :: $3 }

| namePatPair seps seps namePatPairs
{ let rs, _ = $4
reportParseErrorAt (rhs parseState 3) (FSComp.SR.parsExpectingPattern ())
($1 :: rs), lhs parseState }
{ reportParseErrorAt (rhs parseState 3) (FSComp.SR.parsExpectingPattern ())
($1 :: $4) }

namePatPair:
| ident EQUALS parenPattern
Expand Down Expand Up @@ -3553,9 +3551,8 @@ constrPattern:
atomicPatsOrNamePatPairs:
| LPAREN namePatPairs rparen
{ let mParen = rhs2 parseState 1 3
let pats, m = $2
let trivia = { ParenRange = mParen }
SynArgPats.NamePatPairs(pats, m, trivia), snd $2 }
SynArgPats.NamePatPairs($2, rhs parseState 2, trivia), mParen }

| atomicPatterns
{ let mParsed = rhs parseState 1
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3577,29 +3577,40 @@ FSharp.Compiler.EditorServices.PatternContext+NamedUnionCaseField: FSharp.Compil
FSharp.Compiler.EditorServices.PatternContext+NamedUnionCaseField: FSharp.Compiler.Text.Range get_caseIdRange()
FSharp.Compiler.EditorServices.PatternContext+NamedUnionCaseField: System.String fieldName
FSharp.Compiler.EditorServices.PatternContext+NamedUnionCaseField: System.String get_fieldName()
FSharp.Compiler.EditorServices.PatternContext+PositionalUnionCaseField: Boolean get_isTheOnlyField()
FSharp.Compiler.EditorServices.PatternContext+PositionalUnionCaseField: Boolean isTheOnlyField
FSharp.Compiler.EditorServices.PatternContext+PositionalUnionCaseField: FSharp.Compiler.Text.Range caseIdRange
FSharp.Compiler.EditorServices.PatternContext+PositionalUnionCaseField: FSharp.Compiler.Text.Range get_caseIdRange()
FSharp.Compiler.EditorServices.PatternContext+PositionalUnionCaseField: Microsoft.FSharp.Core.FSharpOption`1[System.Int32] fieldIndex
FSharp.Compiler.EditorServices.PatternContext+PositionalUnionCaseField: Microsoft.FSharp.Core.FSharpOption`1[System.Int32] get_fieldIndex()
FSharp.Compiler.EditorServices.PatternContext+Tags: Int32 NamedUnionCaseField
FSharp.Compiler.EditorServices.PatternContext+Tags: Int32 Other
FSharp.Compiler.EditorServices.PatternContext+Tags: Int32 PositionalUnionCaseField
FSharp.Compiler.EditorServices.PatternContext+Tags: Int32 UnionCaseFieldIdentifier
FSharp.Compiler.EditorServices.PatternContext+UnionCaseFieldIdentifier: FSharp.Compiler.Text.Range caseIdRange
FSharp.Compiler.EditorServices.PatternContext+UnionCaseFieldIdentifier: FSharp.Compiler.Text.Range get_caseIdRange()
FSharp.Compiler.EditorServices.PatternContext+UnionCaseFieldIdentifier: Microsoft.FSharp.Collections.FSharpList`1[System.String] get_referencedFields()
FSharp.Compiler.EditorServices.PatternContext+UnionCaseFieldIdentifier: Microsoft.FSharp.Collections.FSharpList`1[System.String] referencedFields
FSharp.Compiler.EditorServices.PatternContext: Boolean Equals(FSharp.Compiler.EditorServices.PatternContext)
FSharp.Compiler.EditorServices.PatternContext: Boolean Equals(System.Object)
FSharp.Compiler.EditorServices.PatternContext: Boolean Equals(System.Object, System.Collections.IEqualityComparer)
FSharp.Compiler.EditorServices.PatternContext: Boolean IsNamedUnionCaseField
FSharp.Compiler.EditorServices.PatternContext: Boolean IsOther
FSharp.Compiler.EditorServices.PatternContext: Boolean IsPositionalUnionCaseField
FSharp.Compiler.EditorServices.PatternContext: Boolean IsUnionCaseFieldIdentifier
FSharp.Compiler.EditorServices.PatternContext: Boolean get_IsNamedUnionCaseField()
FSharp.Compiler.EditorServices.PatternContext: Boolean get_IsOther()
FSharp.Compiler.EditorServices.PatternContext: Boolean get_IsPositionalUnionCaseField()
FSharp.Compiler.EditorServices.PatternContext: Boolean get_IsUnionCaseFieldIdentifier()
FSharp.Compiler.EditorServices.PatternContext: FSharp.Compiler.EditorServices.PatternContext NewNamedUnionCaseField(System.String, FSharp.Compiler.Text.Range)
FSharp.Compiler.EditorServices.PatternContext: FSharp.Compiler.EditorServices.PatternContext NewPositionalUnionCaseField(Microsoft.FSharp.Core.FSharpOption`1[System.Int32], FSharp.Compiler.Text.Range)
FSharp.Compiler.EditorServices.PatternContext: FSharp.Compiler.EditorServices.PatternContext NewPositionalUnionCaseField(Microsoft.FSharp.Core.FSharpOption`1[System.Int32], Boolean, FSharp.Compiler.Text.Range)
FSharp.Compiler.EditorServices.PatternContext: FSharp.Compiler.EditorServices.PatternContext NewUnionCaseFieldIdentifier(Microsoft.FSharp.Collections.FSharpList`1[System.String], FSharp.Compiler.Text.Range)
FSharp.Compiler.EditorServices.PatternContext: FSharp.Compiler.EditorServices.PatternContext Other
FSharp.Compiler.EditorServices.PatternContext: FSharp.Compiler.EditorServices.PatternContext get_Other()
FSharp.Compiler.EditorServices.PatternContext: FSharp.Compiler.EditorServices.PatternContext+NamedUnionCaseField
FSharp.Compiler.EditorServices.PatternContext: FSharp.Compiler.EditorServices.PatternContext+PositionalUnionCaseField
FSharp.Compiler.EditorServices.PatternContext: FSharp.Compiler.EditorServices.PatternContext+Tags
FSharp.Compiler.EditorServices.PatternContext: FSharp.Compiler.EditorServices.PatternContext+UnionCaseFieldIdentifier
FSharp.Compiler.EditorServices.PatternContext: Int32 GetHashCode()
FSharp.Compiler.EditorServices.PatternContext: Int32 GetHashCode(System.Collections.IEqualityComparer)
FSharp.Compiler.EditorServices.PatternContext: Int32 Tag
Expand Down
Loading