diff --git a/docs/release-notes/.FSharp.Compiler.Service/10.0.100.md b/docs/release-notes/.FSharp.Compiler.Service/10.0.100.md index fe1ddaf787..89afb46101 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/10.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/10.0.100.md @@ -36,6 +36,7 @@ * Caches: type subsumption cache key perf regression ([Issue #18925](https://github.com/dotnet/fsharp/issues/18925) [PR #18926](https://github.com/dotnet/fsharp/pull/18926)) * Ensure that line directives are applied to source identifiers (issue [#18908](https://github.com/dotnet/fsharp/issues/18908), PR [#18918](https://github.com/dotnet/fsharp/pull/18918)) * Fix expected and actual types in ErrorFromAddingTypeEquation message and extended diagnostic data. ([PR #18915](https://github.com/dotnet/fsharp/pull/18915)) +* Editor: Fix Record fields completion in update record with partial field name. ([PR #18946](https://github.com/dotnet/fsharp/pull/18946)) ### Changed * Use `errorR` instead of `error` in `CheckDeclarations.fs` when possible. ([PR #18645](https://github.com/dotnet/fsharp/pull/18645)) diff --git a/src/Compiler/Service/ServiceParseTreeWalk.fs b/src/Compiler/Service/ServiceParseTreeWalk.fs index 133df625a8..f280df1b23 100644 --- a/src/Compiler/Service/ServiceParseTreeWalk.fs +++ b/src/Compiler/Service/ServiceParseTreeWalk.fs @@ -528,7 +528,17 @@ module SyntaxTraversal = for SynExprRecordField(fieldName = (field, _); expr = e; blockSeparator = sepOpt) in fields do yield dive (path, copyOpt, Some field) field.Range (fun r -> - if rangeContainsPos field.Range pos then + // Treat the caret placed right after the field name (before '=' or a value) as "inside" the field, + // but only if the field does not yet have a value. + // + // Examples (the '$' marks the caret): + // { r with Field1$ } + // { r with + // Field1$ + // } + let isCaretAfterFieldNameWithoutValue = (e.IsNone && posEq pos field.Range.End) + + if rangeContainsPos field.Range pos || isCaretAfterFieldNameWithoutValue then visitor.VisitRecordField r else None) diff --git a/tests/FSharp.Compiler.Service.Tests/EditorTests.fs b/tests/FSharp.Compiler.Service.Tests/EditorTests.fs index 6df9a1dc4a..6ce135326f 100644 --- a/tests/FSharp.Compiler.Service.Tests/EditorTests.fs +++ b/tests/FSharp.Compiler.Service.Tests/EditorTests.fs @@ -1937,6 +1937,15 @@ let hasRecordType (recordTypeName: string) (symbolUses: FSharpSymbolUse list) = | _ -> false ) |> fun exists -> Assert.True(exists, $"Record type {recordTypeName} not found.") + +let private assertItemsWithNames contains names (completionInfo: DeclarationListInfo) = + let itemNames = completionInfo.Items |> Array.map _.NameInCode |> set + + for name in names do + Assert.True(Set.contains name itemNames = contains) + +let assertHasItemWithNames names (completionInfo: DeclarationListInfo) = + assertItemsWithNames true names completionInfo [] let ``Record fields are completed via type name usage`` () = @@ -2113,10 +2122,9 @@ let rUpdate = { r1 with } hasRecordField "Field1" declarations hasRecordField "Field2" declarations -[] +[] let ``Record fields are completed in update record with partial field name`` () = - let parseResults, checkResults = - getParseAndCheckResults """ + let info = Checker.getCompletionInfo """ module Module type R1 = @@ -2124,23 +2132,53 @@ type R1 = let r1 = { Field1 = 1; Field2 = 2 } -let rUpdate = { r1 with Fi } +let rUpdate = { r1 with Fi{caret} } """ - let declarations = - checkResults.GetDeclarationListSymbols( - Some parseResults, - 9, - "let rUpdate = { r1 with Fi }", - { - EndColumn = 26 - LastDotPos = None - PartialIdent = "Fi" - QualifyingIdents = [] - }, - fun _ -> List.empty - ) - |> List.concat + assertHasItemWithNames ["Field1"; "Field2"] info - hasRecordField "Field1" declarations - hasRecordField "Field2" declarations +[] +let ``Multiline record fields are completed in update record with partial field name`` () = + let info = Checker.getCompletionInfo """ +module Module + +type R1 = + { Field1: int; Field2: int } + +let r1 = { Field1 = 1; Field2 = 2 } + +let rUpdate = + { r1 with + Fi{caret} + } +""" + + assertHasItemWithNames ["Field1"; "Field2"] info + +[] +let ``No record field completion after '=' with missing value in first binding (after =;)`` () = + let info = Checker.getCompletionInfo """ +module M + +type X = + val field1: int + val field2: string + +let x = new X() +let _ = { field1 =; {caret} } +""" + assertItemsWithNames false ["field1"; "field2"] info + +[] +let ``No record field completion after '=' with missing value in first binding (after =; f)`` () = + let info = Checker.getCompletionInfo """ +module M + +type X = + val field1: int + val field2: string + +let x = new X() +let _ = { field1 =; f{caret} } +""" + assertItemsWithNames false ["field1"; "field2"] info \ No newline at end of file