Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Completion: empty record fixes #12872

Merged
merged 4 commits into from
Apr 29, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 15 additions & 11 deletions src/fsharp/NameResolution.fs
Original file line number Diff line number Diff line change
Expand Up @@ -4484,22 +4484,31 @@ let rec ResolvePartialLongIdentInModuleOrNamespaceForRecordFields (ncenv: NameRe
| _ -> []
)

let getRecordFieldsInScope nenv =
nenv.eFieldLabels
|> Seq.collect (fun (KeyValue(_, v)) -> v)
|> Seq.map (fun fref ->
let typeInsts = fref.TyconRef.TyparsNoRange |> List.map mkTyparTy
Item.RecdField(RecdFieldInfo(typeInsts, fref)))
|> List.ofSeq

/// allowObsolete - specifies whether we should return obsolete types & modules
/// as (no other obsolete items are returned)
let rec ResolvePartialLongIdentToClassOrRecdFields (ncenv: NameResolver) (nenv: NameResolutionEnv) m ad plid (allowObsolete: bool) =
ResolvePartialLongIdentToClassOrRecdFieldsImpl ncenv nenv OpenQualified m ad plid allowObsolete
let rec ResolvePartialLongIdentToClassOrRecdFields (ncenv: NameResolver) (nenv: NameResolutionEnv) m ad plid (allowObsolete: bool) (fieldsOnly: bool) =
ResolvePartialLongIdentToClassOrRecdFieldsImpl ncenv nenv OpenQualified m ad plid allowObsolete fieldsOnly

and ResolvePartialLongIdentToClassOrRecdFieldsImpl (ncenv: NameResolver) (nenv: NameResolutionEnv) fullyQualified m ad plid allowObsolete =
and ResolvePartialLongIdentToClassOrRecdFieldsImpl (ncenv: NameResolver) (nenv: NameResolutionEnv) fullyQualified m ad plid allowObsolete fieldsOnly =
let g = ncenv.g

match plid with
| id :: plid when id = "global" -> // this is deliberately not the mangled name
// dive deeper
ResolvePartialLongIdentToClassOrRecdFieldsImpl ncenv nenv FullyQualified m ad plid allowObsolete
ResolvePartialLongIdentToClassOrRecdFieldsImpl ncenv nenv FullyQualified m ad plid allowObsolete fieldsOnly
| [] ->

// empty plid - return namespaces\modules\record types\accessible fields

if fieldsOnly then getRecordFieldsInScope nenv else

let mods =
let moduleOrNamespaceRefs =
Expand Down Expand Up @@ -4528,12 +4537,7 @@ and ResolvePartialLongIdentToClassOrRecdFieldsImpl (ncenv: NameResolver) (nenv:
|> Seq.toList

let recdFields =
nenv.eFieldLabels
|> Seq.collect (fun (KeyValue(_, v)) -> v)
|> Seq.map (fun fref ->
let typeInsts = fref.TyconRef.TyparsNoRange |> List.map mkTyparTy
Item.RecdField(RecdFieldInfo(typeInsts, fref)))
|> List.ofSeq
getRecordFieldsInScope nenv

mods @ recdTyCons @ recdFields

Expand All @@ -4549,7 +4553,7 @@ and ResolvePartialLongIdentToClassOrRecdFieldsImpl (ncenv: NameResolver) (nenv:

let qualifiedFields =
match rest with
| [] ->
| [] when not fieldsOnly ->
// get record types accessible in given nenv
let tycons = LookupTypeNameInEnvNoArity OpenQualified id nenv
tycons
Expand Down
4 changes: 3 additions & 1 deletion src/fsharp/NameResolution.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -557,8 +557,10 @@ val internal ResolveField : TcResultsSink -> NameResolver -
/// Resolve a long identifier occurring in an expression position
val internal ResolveExprLongIdent : TcResultsSink -> NameResolver -> range -> AccessorDomain -> NameResolutionEnv -> TypeNameResolutionInfo -> Ident list -> ResultOrException<EnclosingTypeInst * Item * Ident list>

val internal getRecordFieldsInScope: NameResolutionEnv -> Item list

/// Resolve a (possibly incomplete) long identifier to a loist of possible class or record fields
val internal ResolvePartialLongIdentToClassOrRecdFields: NameResolver -> NameResolutionEnv -> range -> AccessorDomain -> string list -> bool -> Item list
val internal ResolvePartialLongIdentToClassOrRecdFields: NameResolver -> NameResolutionEnv -> range -> AccessorDomain -> string list -> bool -> bool -> Item list

/// Return the fields for the given class or record
val internal ResolveRecordOrClassFieldsOfType : NameResolver -> range -> AccessorDomain -> TType -> bool -> Item list
Expand Down
48 changes: 37 additions & 11 deletions src/fsharp/service/FSharpCheckerResults.fs
Original file line number Diff line number Diff line change
Expand Up @@ -638,9 +638,9 @@ type internal TypeCheckInfo
GetEnvironmentLookupResolutions(nenv, ad, m, plid, filterCtors, showObsolete)

/// Find record fields in the best naming environment.
let GetClassOrRecordFieldsEnvironmentLookupResolutions(cursorPos, plid) =
let GetClassOrRecordFieldsEnvironmentLookupResolutions(cursorPos, plid, fieldsOnly) =
let (nenv, ad),m = GetBestEnvForPos cursorPos
let items = ResolvePartialLongIdentToClassOrRecdFields ncenv nenv m ad plid false
let items = ResolvePartialLongIdentToClassOrRecdFields ncenv nenv m ad plid false fieldsOnly
let items = items |> List.map ItemWithNoInst
let items = items |> RemoveDuplicateItems g
let items = items |> RemoveExplicitlySuppressed g
Expand Down Expand Up @@ -913,6 +913,21 @@ type internal TypeCheckInfo
let toCompletionItems (items: ItemWithInst list, denv: DisplayEnv, m: range ) =
items |> List.map DefaultCompletionItem, denv, m

/// Find record fields in the best naming environment.
let GetEnvironmentLookupResolutionsIncludingRecordFieldsAtPosition cursorPos plid envItems =
// An empty record expression may be completed into something like these:
// { XXX = ... }
// { xxx with XXX ... }
// Provide both expression items in scope and available record fields.
let (nenv, _), m = GetBestEnvForPos cursorPos

let fieldItems, _, _ = GetClassOrRecordFieldsEnvironmentLookupResolutions(cursorPos, plid, true)
let fieldCompletionItems, _, _ as fieldsResult = (fieldItems, nenv.DisplayEnv, m) |> toCompletionItems

match envItems with
| Some(items, denv, m) -> Some(fieldCompletionItems @ items, denv, m)
| _ -> Some(fieldsResult)

/// Get the auto-complete items at a particular location.
let GetDeclItemsForNamesAtPosition(parseResultsOpt: FSharpParseFileResults option, origLongIdentOpt: string list option,
residueOpt:string option, lastDotPos: int option, line:int, lineStr:string, colAtEndOfNamesAndResidue, filterCtors, resolveOverloads,
Expand Down Expand Up @@ -963,27 +978,38 @@ type internal TypeCheckInfo
|> Option.map toCompletionItems

// Completion at ' { XXX = ... } "
| Some(CompletionContext.RecordField(RecordContext.New(plid, _))) ->
// { x. } can be either record construction or computation expression. Try to get all visible record fields first
match GetClassOrRecordFieldsEnvironmentLookupResolutions(mkPos line loc, plid) |> toCompletionItems with
| [],_,_ ->
// no record fields found, return completion list as if we were outside any computation expression
GetDeclaredItems (parseResultsOpt, lineStr, origLongIdentOpt, colAtEndOfNamesAndResidue, residueOpt, lastDotPos, line, loc, filterCtors,resolveOverloads, false, fun() -> [])
| result -> Some(result)
| Some(CompletionContext.RecordField(RecordContext.New((plid, _), isFirstField))) ->
if isFirstField then
let cursorPos = mkPos line loc
let envItems = GetDeclaredItems (parseResultsOpt, lineStr, origLongIdentOpt, colAtEndOfNamesAndResidue, residueOpt, lastDotPos, line, loc, filterCtors,resolveOverloads, false, fun () -> [])
GetEnvironmentLookupResolutionsIncludingRecordFieldsAtPosition cursorPos plid envItems
else
// { x. } can be either record construction or computation expression. Try to get all visible record fields first
match GetClassOrRecordFieldsEnvironmentLookupResolutions(mkPos line loc, plid, false) |> toCompletionItems with
| [],_,_ ->
// no record fields found, return completion list as if we were outside any computation expression
GetDeclaredItems (parseResultsOpt, lineStr, origLongIdentOpt, colAtEndOfNamesAndResidue, residueOpt, lastDotPos, line, loc, filterCtors,resolveOverloads, false, fun() -> [])
| result -> Some(result)

// Completion at '{ ... }'
| Some(CompletionContext.RecordField RecordContext.Empty) ->
let cursorPos = mkPos line loc
let envItems = GetDeclaredItems (parseResultsOpt, lineStr, origLongIdentOpt, colAtEndOfNamesAndResidue, residueOpt, lastDotPos, line, loc, filterCtors,resolveOverloads, false, fun () -> [])
GetEnvironmentLookupResolutionsIncludingRecordFieldsAtPosition cursorPos [] envItems

// Completion at ' { XXX = ... with ... } "
| Some(CompletionContext.RecordField(RecordContext.CopyOnUpdate(r, (plid, _)))) ->
match GetRecdFieldsForExpr(r) with
| None ->
Some (GetClassOrRecordFieldsEnvironmentLookupResolutions(mkPos line loc, plid))
Some (GetClassOrRecordFieldsEnvironmentLookupResolutions(mkPos line loc, plid, false))
|> Option.map toCompletionItems
| Some (items, denv, m) ->
Some (List.map ItemWithNoInst items, denv, m)
|> Option.map toCompletionItems

// Completion at ' { XXX = ... with ... } "
| Some(CompletionContext.RecordField(RecordContext.Constructor(typeName))) ->
Some(GetClassOrRecordFieldsEnvironmentLookupResolutions(mkPos line loc, [typeName]))
Some(GetClassOrRecordFieldsEnvironmentLookupResolutions(mkPos line loc, [typeName], false))
|> Option.map toCompletionItems

// No completion at '...: string'
Expand Down
23 changes: 20 additions & 3 deletions src/fsharp/service/ServiceParsedInputOps.fs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ type InheritanceContext =
type RecordContext =
| CopyOnUpdate of range: range * path: CompletionPath
| Constructor of typeName: string
| New of path: CompletionPath
| Empty
| New of path: CompletionPath * isFirstField: bool
| Declaration of isInIdentifier: bool

[<RequireQualifiedAccess>]
Expand Down Expand Up @@ -956,7 +957,8 @@ module ParsedInput =
Some (CompletionContext.ParameterList args)
| _ ->
defaultTraverse expr

| SynExpr.Record(None, None, [], _) ->
Some(CompletionContext.RecordField RecordContext.Empty)
// Unchecked.defaultof<str$>
| SynExpr.TypeApp (typeArgsRange = range) when rangeContainsPos range pos ->
Some CompletionContext.PatternType
Expand All @@ -968,7 +970,22 @@ module ParsedInput =
match path with
| SyntaxNode.SynExpr _ :: SyntaxNode.SynBinding _ :: SyntaxNode.SynMemberDefn _ :: SyntaxNode.SynTypeDefn(SynTypeDefn(typeInfo=SynComponentInfo(longId=[id]))) :: _ ->
RecordContext.Constructor(id.idText)
| _ -> RecordContext.New completionPath

| SyntaxNode.SynExpr(SynExpr.Record(None, _, fields, _)) :: _ ->
let isFirstField =
match field, fields with
| Some contextLid, SynExprRecordField(fieldName = lid, _) :: _ -> contextLid.Range = lid.Range
| _ -> false

RecordContext.New(completionPath, isFirstField)

// Unfinished `{ xxx }` expression considered a record field by the tree walker.
| SyntaxNode.SynExpr(SynExpr.ComputationExpr _) :: _ ->
RecordContext.New(completionPath, true)

| _ ->
RecordContext.New(completionPath, false)

match field with
| Some field ->
match parseLid field with
Expand Down
3 changes: 2 additions & 1 deletion src/fsharp/service/ServiceParsedInputOps.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ type public InheritanceContext =
type public RecordContext =
| CopyOnUpdate of range: range * path: CompletionPath
| Constructor of typeName: string
| New of path: CompletionPath
| Empty
| New of path: CompletionPath * isFirstField: bool
| Declaration of isInIdentifier: bool

[<RequireQualifiedAccess>]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3511,27 +3511,34 @@ FSharp.Compiler.EditorServices.RecordContext+CopyOnUpdate: System.Tuple`2[Micros
FSharp.Compiler.EditorServices.RecordContext+CopyOnUpdate: System.Tuple`2[Microsoft.FSharp.Collections.FSharpList`1[System.String],Microsoft.FSharp.Core.FSharpOption`1[System.String]] path
FSharp.Compiler.EditorServices.RecordContext+Declaration: Boolean get_isInIdentifier()
FSharp.Compiler.EditorServices.RecordContext+Declaration: Boolean isInIdentifier
FSharp.Compiler.EditorServices.RecordContext+New: Boolean get_isFirstField()
FSharp.Compiler.EditorServices.RecordContext+New: Boolean isFirstField
FSharp.Compiler.EditorServices.RecordContext+New: System.Tuple`2[Microsoft.FSharp.Collections.FSharpList`1[System.String],Microsoft.FSharp.Core.FSharpOption`1[System.String]] get_path()
FSharp.Compiler.EditorServices.RecordContext+New: System.Tuple`2[Microsoft.FSharp.Collections.FSharpList`1[System.String],Microsoft.FSharp.Core.FSharpOption`1[System.String]] path
FSharp.Compiler.EditorServices.RecordContext+Tags: Int32 Constructor
FSharp.Compiler.EditorServices.RecordContext+Tags: Int32 CopyOnUpdate
FSharp.Compiler.EditorServices.RecordContext+Tags: Int32 Declaration
FSharp.Compiler.EditorServices.RecordContext+Tags: Int32 Empty
FSharp.Compiler.EditorServices.RecordContext+Tags: Int32 New
FSharp.Compiler.EditorServices.RecordContext: Boolean Equals(FSharp.Compiler.EditorServices.RecordContext)
FSharp.Compiler.EditorServices.RecordContext: Boolean Equals(System.Object)
FSharp.Compiler.EditorServices.RecordContext: Boolean Equals(System.Object, System.Collections.IEqualityComparer)
FSharp.Compiler.EditorServices.RecordContext: Boolean IsConstructor
FSharp.Compiler.EditorServices.RecordContext: Boolean IsCopyOnUpdate
FSharp.Compiler.EditorServices.RecordContext: Boolean IsDeclaration
FSharp.Compiler.EditorServices.RecordContext: Boolean IsEmpty
FSharp.Compiler.EditorServices.RecordContext: Boolean IsNew
FSharp.Compiler.EditorServices.RecordContext: Boolean get_IsConstructor()
FSharp.Compiler.EditorServices.RecordContext: Boolean get_IsCopyOnUpdate()
FSharp.Compiler.EditorServices.RecordContext: Boolean get_IsDeclaration()
FSharp.Compiler.EditorServices.RecordContext: Boolean get_IsEmpty()
FSharp.Compiler.EditorServices.RecordContext: Boolean get_IsNew()
FSharp.Compiler.EditorServices.RecordContext: FSharp.Compiler.EditorServices.RecordContext Empty
FSharp.Compiler.EditorServices.RecordContext: FSharp.Compiler.EditorServices.RecordContext NewConstructor(System.String)
FSharp.Compiler.EditorServices.RecordContext: FSharp.Compiler.EditorServices.RecordContext NewCopyOnUpdate(FSharp.Compiler.Text.Range, System.Tuple`2[Microsoft.FSharp.Collections.FSharpList`1[System.String],Microsoft.FSharp.Core.FSharpOption`1[System.String]])
FSharp.Compiler.EditorServices.RecordContext: FSharp.Compiler.EditorServices.RecordContext NewDeclaration(Boolean)
FSharp.Compiler.EditorServices.RecordContext: FSharp.Compiler.EditorServices.RecordContext NewNew(System.Tuple`2[Microsoft.FSharp.Collections.FSharpList`1[System.String],Microsoft.FSharp.Core.FSharpOption`1[System.String]])
FSharp.Compiler.EditorServices.RecordContext: FSharp.Compiler.EditorServices.RecordContext NewNew(System.Tuple`2[Microsoft.FSharp.Collections.FSharpList`1[System.String],Microsoft.FSharp.Core.FSharpOption`1[System.String]], Boolean)
FSharp.Compiler.EditorServices.RecordContext: FSharp.Compiler.EditorServices.RecordContext get_Empty()
FSharp.Compiler.EditorServices.RecordContext: FSharp.Compiler.EditorServices.RecordContext+Constructor
FSharp.Compiler.EditorServices.RecordContext: FSharp.Compiler.EditorServices.RecordContext+CopyOnUpdate
FSharp.Compiler.EditorServices.RecordContext: FSharp.Compiler.EditorServices.RecordContext+Declaration
Expand Down
55 changes: 48 additions & 7 deletions tests/service/CompletionTests.fs
Original file line number Diff line number Diff line change
@@ -1,18 +1,59 @@
module FSharp.Compiler.Service.Tests.CompletionTests

open FSharp.Compiler.EditorServices
open FsUnit
open NUnit.Framework

let getCompletionInfo lineText (line, column) source =
let parseResults, checkResults = getParseAndCheckResults source
let plid = QuickParse.GetPartialLongNameEx(lineText, column)
checkResults.GetDeclarationListInfo(Some parseResults, line, lineText, plid)

let getCompletionItemNames (completionInfo: DeclarationListInfo) =
completionInfo.Items |> Array.map (fun item -> item.Name)

let assertHasItemWithNames names (completionInfo: DeclarationListInfo) =
let itemNames = getCompletionItemNames completionInfo |> set

for name in names do
Assert.That(Set.contains name itemNames, name)


[<Test>]
let ``Expr - record - field 01 - anon module`` () =
let parseResults, checkResults = getParseAndCheckResults """
type Record = { Field: int}
let info = getCompletionInfo "{ Fi }" (4, 3) """
type Record = { Field: int }

{ Fi }
"""
let lineText = "{ Fi }"
let plid = QuickParse.GetPartialLongNameEx(lineText, 3)
let info = checkResults.GetDeclarationListInfo(Some parseResults, 4, lineText, plid)
assertHasItemWithNames ["Field"] info

info.Items |> Array.exists (fun item -> item.Name = "Field") |> shouldEqual true
[<Test>]
let ``Expr - record - field 02 - anon module`` () =
let info = getCompletionInfo "{ Fi }" (6, 3) """
type Record = { Field: int }

let record = { Field = 1 }

{ Fi }
"""
assertHasItemWithNames ["Field"] info

[<Test>]
let ``Expr - record - empty 01`` () =
let info = getCompletionInfo "{ }" (4, 2) """
type Record = { Field: int }

{ }
"""
assertHasItemWithNames ["Field"] info

[<Test>]
let ``Expr - record - empty 02`` () =
let info = getCompletionInfo "{ }" (6, 2) """
type Record = { Field: int }

let record = { Field = 1 }

{ }
"""
assertHasItemWithNames ["Field"; "record"] info
Original file line number Diff line number Diff line change
Expand Up @@ -786,15 +786,15 @@ for i in 0..a."]
]
let useCases =
[
"let _ = (* MARKER*){X", "(* MARKER*){X", [], ["XX"]
"let _ = (* MARKER*){X }", "(* MARKER*){X", [], ["XX"]
"let _ = {(* MARKER*)Mod. = 1; O", "(* MARKER*)Mod.", ["XX"; "YY"], ["System"]
"let _ = {(* MARKER*)Mod.Rec. ", "(* MARKER*)Mod.Rec.", ["XX"; "YY"], ["System"]
"let _ = (* MARKER*){Mod.XX = 1; }", "(* MARKER*){Mod.XX = 1; ", ["Mod"], ["XX"; "abs"]
]

for (code, marker, should, shouldnot) in useCases do
let code = prologue @ [code]
let shouldnot = shouldnot @ ["abs"]
AssertCtrlSpaceCompleteContains code marker should ["abs"]
AssertCtrlSpaceCompleteContains code marker should shouldnot

[<Test>]
[<Category("Records")>]
Expand Down