Skip to content

Improve completions for inline record fields #7601

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

Merged
merged 4 commits into from
Jul 4, 2025
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
- Better error message for when trying to await something that is not a promise. https://github.com/rescript-lang/rescript/pull/7561
- Better error messages for object field missing and object field type mismatches. https://github.com/rescript-lang/rescript/pull/7580
- Better error messages for when polymorphic variants does not match for various reasons. https://github.com/rescript-lang/rescript/pull/7596
- Improved completions for inline records. https://github.com/rescript-lang/rescript/pull/7601

#### :house: Internal

Expand Down
104 changes: 46 additions & 58 deletions analysis/src/CompletionBackEnd.ml
Original file line number Diff line number Diff line change
Expand Up @@ -1443,6 +1443,48 @@ let rec completeTypedValue ?(typeArgContext : typeArgContext option) ~rawOpens
let emptyCase = emptyCase ~mode in
let printConstructorArgs = printConstructorArgs ~mode in
let create = Completion.create ?typeArgContext in
let getRecordCompletions ~env ~fields ~extractedType =
(* As we're completing for a record, we'll need a hint (completionContext)
here to figure out whether we should complete for a record field, or
the record body itself. *)
match completionContext with
| Some (Completable.RecordField {seenFields}) ->
fields
|> List.filter (fun (field : field) ->
List.mem field.fname.txt seenFields = false)
|> List.map (fun (field : field) ->
match (field.optional, mode) with
| true, Pattern Destructuring ->
create ("?" ^ field.fname.txt) ?deprecated:field.deprecated
~docstring:
[
field.fname.txt
^ " is an optional field, and needs to be destructured \
using '?'.";
]
~kind:
(Field (field, TypeUtils.extractedTypeToString extractedType))
~env
| _ ->
create field.fname.txt ?deprecated:field.deprecated
~kind:
(Field (field, TypeUtils.extractedTypeToString extractedType))
~env)
|> filterItems ~prefix
| _ ->
if prefix = "" then
[
create "{}" ~includesSnippets:true ~insertText:"{$0}" ~sortText:"A"
~kind:
(ExtractedType
( extractedType,
match mode with
| Pattern _ -> `Type
| Expression -> `Value ))
~env;
]
else []
in
match t with
| TtypeT {env; path} when mode = Expression ->
if Debug.verbose () then
Expand Down Expand Up @@ -1710,67 +1752,13 @@ let rec completeTypedValue ?(typeArgContext : typeArgContext option) ~rawOpens
~insertText:(printConstructorArgs numExprs ~asSnippet:true)
~kind:(Value typ) ~env;
]
| Trecord {env; fields} as extractedType -> (
| Trecord {env; fields} as extractedType ->
if Debug.verbose () then print_endline "[complete_typed_value]--> Trecord";
(* As we're completing for a record, we'll need a hint (completionContext)
here to figure out whether we should complete for a record field, or
the record body itself. *)
match completionContext with
| Some (Completable.RecordField {seenFields}) ->
fields
|> List.filter (fun (field : field) ->
List.mem field.fname.txt seenFields = false)
|> List.map (fun (field : field) ->
match (field.optional, mode) with
| true, Pattern Destructuring ->
create ("?" ^ field.fname.txt) ?deprecated:field.deprecated
~docstring:
[
field.fname.txt
^ " is an optional field, and needs to be destructured \
using '?'.";
]
~kind:
(Field (field, TypeUtils.extractedTypeToString extractedType))
~env
| _ ->
create field.fname.txt ?deprecated:field.deprecated
~kind:
(Field (field, TypeUtils.extractedTypeToString extractedType))
~env)
|> filterItems ~prefix
| _ ->
if prefix = "" then
[
create "{}" ~includesSnippets:true ~insertText:"{$0}" ~sortText:"A"
~kind:
(ExtractedType
( extractedType,
match mode with
| Pattern _ -> `Type
| Expression -> `Value ))
~env;
]
else [])
| TinlineRecord {env; fields} -> (
getRecordCompletions ~env ~fields ~extractedType
| TinlineRecord {env; fields} as extractedType ->
if Debug.verbose () then
print_endline "[complete_typed_value]--> TinlineRecord";
match completionContext with
| Some (Completable.RecordField {seenFields}) ->
fields
|> List.filter (fun (field : field) ->
List.mem field.fname.txt seenFields = false)
|> List.map (fun (field : field) ->
create field.fname.txt ~kind:(Label "Inline record")
?deprecated:field.deprecated ~env)
|> filterItems ~prefix
| _ ->
if prefix = "" then
[
create "{}" ~includesSnippets:true ~insertText:"{$0}" ~sortText:"A"
~kind:(Label "Inline record") ~env;
]
else [])
getRecordCompletions ~env ~fields ~extractedType
| Tarray (env, typ) ->
if Debug.verbose () then print_endline "[complete_typed_value]--> Tarray";
if prefix = "" then
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ Resolved opens 1 Stdlib
ContextPath CTypeAtPos()
[{
"label": "{}",
"kind": 4,
"kind": 12,
"tags": [],
"detail": "Inline record",
"documentation": null,
"detail": "{miss: bool}",
"documentation": {"kind": "markdown", "value": "```rescript\n{miss: bool}\n```"},
"sortText": "A",
"insertText": "{$0}",
"insertTextFormat": 2
Expand Down
17 changes: 17 additions & 0 deletions tests/analysis_tests/tests/src/Completion.res
Original file line number Diff line number Diff line change
Expand Up @@ -465,3 +465,20 @@ type withUncurried = {fn: int => unit}

// let someRecord = { FAR. }
// ^com

type someRecord = {field1: string, field2: int}
type someVariantWithRecord = HasRecord(someRecord)

// let v: someVariantWithRecord = HasRecord({})
// ^com

// let v: someVariantWithRecord = HasRecord({fie})
// ^com

type someVariantWithInlineRecord = HasInlineRecord({field1: string, field2: int})

// let v: someVariantWithInlineRecord = HasInlineRecord({})
// ^com

// let v: someVariantWithInlineRecord = HasInlineRecord({fie})
// ^com
92 changes: 90 additions & 2 deletions tests/analysis_tests/tests/src/expected/Completion.res.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2342,8 +2342,8 @@ Path red
}]

Complete src/Completion.res 407:22
posCursor:[407:22] posNoWhite:[407:21] Found expr:[407:11->468:0]
Pexp_apply ...__ghost__[0:-1->0:-1] (...[407:11->425:17], ...[430:0->468:0])
posCursor:[407:22] posNoWhite:[407:21] Found expr:[407:11->485:0]
Pexp_apply ...__ghost__[0:-1->0:-1] (...[407:11->425:17], ...[430:0->485:0])
posCursor:[407:22] posNoWhite:[407:21] Found expr:[407:11->425:17]
Pexp_apply ...__ghost__[0:-1->0:-1] (...[407:11->407:19], ...[407:21->425:17])
posCursor:[407:22] posNoWhite:[407:21] Found expr:[407:21->425:17]
Expand Down Expand Up @@ -2747,3 +2747,91 @@ Path FAR.
"documentation": {"kind": "markdown", "value": "```rescript\nsomething: option<int>\n```\n\n```rescript\ntype forAutoRecord = {\n forAuto: ForAuto.t,\n something: option<int>,\n}\n```"}
}]

Complete src/Completion.res 471:45
XXX Not found!
Completable: Cexpression Type[someVariantWithRecord]->variantPayload::HasRecord($0), recordBody
Raw opens: 2 Shadow.B.place holder ... Shadow.A.place holder
Package opens Stdlib.place holder Pervasives.JsxModules.place holder
Resolved opens 3 Stdlib Completion Completion
ContextPath Type[someVariantWithRecord]
Path someVariantWithRecord
[{
"label": "field1",
"kind": 5,
"tags": [],
"detail": "string",
"documentation": {"kind": "markdown", "value": "```rescript\nfield1: string\n```\n\n```rescript\ntype someRecord = {field1: string, field2: int}\n```"}
}, {
"label": "field2",
"kind": 5,
"tags": [],
"detail": "int",
"documentation": {"kind": "markdown", "value": "```rescript\nfield2: int\n```\n\n```rescript\ntype someRecord = {field1: string, field2: int}\n```"}
}]

Complete src/Completion.res 474:48
XXX Not found!
Completable: Cexpression Type[someVariantWithRecord]=fie->variantPayload::HasRecord($0), recordBody
Raw opens: 2 Shadow.B.place holder ... Shadow.A.place holder
Package opens Stdlib.place holder Pervasives.JsxModules.place holder
Resolved opens 3 Stdlib Completion Completion
ContextPath Type[someVariantWithRecord]
Path someVariantWithRecord
[{
"label": "field1",
"kind": 5,
"tags": [],
"detail": "string",
"documentation": {"kind": "markdown", "value": "```rescript\nfield1: string\n```\n\n```rescript\ntype someRecord = {field1: string, field2: int}\n```"}
}, {
"label": "field2",
"kind": 5,
"tags": [],
"detail": "int",
"documentation": {"kind": "markdown", "value": "```rescript\nfield2: int\n```\n\n```rescript\ntype someRecord = {field1: string, field2: int}\n```"}
}]

Complete src/Completion.res 479:57
XXX Not found!
Completable: Cexpression Type[someVariantWithInlineRecord]->variantPayload::HasInlineRecord($0), recordBody
Raw opens: 2 Shadow.B.place holder ... Shadow.A.place holder
Package opens Stdlib.place holder Pervasives.JsxModules.place holder
Resolved opens 3 Stdlib Completion Completion
ContextPath Type[someVariantWithInlineRecord]
Path someVariantWithInlineRecord
[{
"label": "field1",
"kind": 5,
"tags": [],
"detail": "string",
"documentation": {"kind": "markdown", "value": "```rescript\nfield1: string\n```\n\n```rescript\n{field1: string, field2: int}\n```"}
}, {
"label": "field2",
"kind": 5,
"tags": [],
"detail": "int",
"documentation": {"kind": "markdown", "value": "```rescript\nfield2: int\n```\n\n```rescript\n{field1: string, field2: int}\n```"}
}]

Complete src/Completion.res 482:60
XXX Not found!
Completable: Cexpression Type[someVariantWithInlineRecord]=fie->variantPayload::HasInlineRecord($0), recordBody
Raw opens: 2 Shadow.B.place holder ... Shadow.A.place holder
Package opens Stdlib.place holder Pervasives.JsxModules.place holder
Resolved opens 3 Stdlib Completion Completion
ContextPath Type[someVariantWithInlineRecord]
Path someVariantWithInlineRecord
[{
"label": "field1",
"kind": 5,
"tags": [],
"detail": "string",
"documentation": {"kind": "markdown", "value": "```rescript\nfield1: string\n```\n\n```rescript\n{field1: string, field2: int}\n```"}
}, {
"label": "field2",
"kind": 5,
"tags": [],
"detail": "int",
"documentation": {"kind": "markdown", "value": "```rescript\nfield2: int\n```\n\n```rescript\n{field1: string, field2: int}\n```"}
}]

Original file line number Diff line number Diff line change
Expand Up @@ -700,10 +700,10 @@ ContextPath Value[fnTakingInlineRecord]
Path fnTakingInlineRecord
[{
"label": "{}",
"kind": 4,
"kind": 12,
"tags": [],
"detail": "Inline record",
"documentation": null,
"detail": "{someBoolField: bool, otherField: option<bool>, nestedRecord: otherRecord}",
"documentation": {"kind": "markdown", "value": "```rescript\n{someBoolField: bool, otherField: option<bool>, nestedRecord: otherRecord}\n```"},
"sortText": "A",
"insertText": "{$0}",
"insertTextFormat": 2
Expand All @@ -720,22 +720,22 @@ ContextPath Value[fnTakingInlineRecord]
Path fnTakingInlineRecord
[{
"label": "someBoolField",
"kind": 4,
"kind": 5,
"tags": [],
"detail": "Inline record",
"documentation": null
"detail": "bool",
"documentation": {"kind": "markdown", "value": "```rescript\nsomeBoolField: bool\n```\n\n```rescript\n{someBoolField: bool, otherField: option<bool>, nestedRecord: otherRecord}\n```"}
}, {
"label": "otherField",
"kind": 4,
"kind": 5,
"tags": [],
"detail": "Inline record",
"documentation": null
"detail": "option<bool>",
"documentation": {"kind": "markdown", "value": "```rescript\notherField: option<bool>\n```\n\n```rescript\n{someBoolField: bool, otherField: option<bool>, nestedRecord: otherRecord}\n```"}
}, {
"label": "nestedRecord",
"kind": 4,
"kind": 5,
"tags": [],
"detail": "Inline record",
"documentation": null
"detail": "otherRecord",
"documentation": {"kind": "markdown", "value": "```rescript\nnestedRecord: otherRecord\n```\n\n```rescript\n{someBoolField: bool, otherField: option<bool>, nestedRecord: otherRecord}\n```"}
}]

Complete src/CompletionExpressions.res 132:51
Expand All @@ -749,10 +749,10 @@ ContextPath Value[fnTakingInlineRecord]
Path fnTakingInlineRecord
[{
"label": "someBoolField",
"kind": 4,
"kind": 5,
"tags": [],
"detail": "Inline record",
"documentation": null
"detail": "bool",
"documentation": {"kind": "markdown", "value": "```rescript\nsomeBoolField: bool\n```\n\n```rescript\n{someBoolField: bool, otherField: option<bool>, nestedRecord: otherRecord}\n```"}
}]

Complete src/CompletionExpressions.res 135:63
Expand Down