From 1b0296cbde35a40cf87e3651cb2f196e30436efb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A9di-R=C3=A9mi=20Hashim?= Date: Fri, 4 Jul 2025 14:44:19 +0100 Subject: [PATCH 1/3] Show completion for inline record fields --- analysis/src/CompletionBackEnd.ml | 29 +++++- .../ConstructorCompletion__Own.res.txt | 6 +- tests/analysis_tests/tests/src/Completion.res | 17 ++++ .../tests/src/expected/Completion.res.txt | 92 ++++++++++++++++++- .../expected/CompletionExpressions.res.txt | 30 +++--- 5 files changed, 150 insertions(+), 24 deletions(-) diff --git a/analysis/src/CompletionBackEnd.ml b/analysis/src/CompletionBackEnd.ml index f678d9a6c0..75da6ae3f1 100644 --- a/analysis/src/CompletionBackEnd.ml +++ b/analysis/src/CompletionBackEnd.ml @@ -1752,7 +1752,7 @@ let rec completeTypedValue ?(typeArgContext : typeArgContext option) ~rawOpens ~env; ] else []) - | TinlineRecord {env; fields} -> ( + | TinlineRecord {env; fields} as extractedType -> ( if Debug.verbose () then print_endline "[complete_typed_value]--> TinlineRecord"; match completionContext with @@ -1761,14 +1761,35 @@ let rec completeTypedValue ?(typeArgContext : typeArgContext option) ~rawOpens |> 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) + 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:(Label "Inline record") ~env; + ~kind: + (ExtractedType + ( extractedType, + match mode with + | Pattern _ -> `Type + | Expression -> `Value )) + ~env; ] else []) | Tarray (env, typ) -> diff --git a/tests/analysis_tests/tests-incremental-typechecking/src/expected/ConstructorCompletion__Own.res.txt b/tests/analysis_tests/tests-incremental-typechecking/src/expected/ConstructorCompletion__Own.res.txt index 585f53f395..6ea8bab362 100644 --- a/tests/analysis_tests/tests-incremental-typechecking/src/expected/ConstructorCompletion__Own.res.txt +++ b/tests/analysis_tests/tests-incremental-typechecking/src/expected/ConstructorCompletion__Own.res.txt @@ -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 diff --git a/tests/analysis_tests/tests/src/Completion.res b/tests/analysis_tests/tests/src/Completion.res index 9d8c870769..ce4ca160b7 100644 --- a/tests/analysis_tests/tests/src/Completion.res +++ b/tests/analysis_tests/tests/src/Completion.res @@ -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 diff --git a/tests/analysis_tests/tests/src/expected/Completion.res.txt b/tests/analysis_tests/tests/src/expected/Completion.res.txt index f26d64ee76..18d9163214 100644 --- a/tests/analysis_tests/tests/src/expected/Completion.res.txt +++ b/tests/analysis_tests/tests/src/expected/Completion.res.txt @@ -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] @@ -2747,3 +2747,91 @@ Path FAR. "documentation": {"kind": "markdown", "value": "```rescript\nsomething: option\n```\n\n```rescript\ntype forAutoRecord = {\n forAuto: ForAuto.t,\n something: option,\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```"} + }] + diff --git a/tests/analysis_tests/tests/src/expected/CompletionExpressions.res.txt b/tests/analysis_tests/tests/src/expected/CompletionExpressions.res.txt index bfea51f5fc..663e00678c 100644 --- a/tests/analysis_tests/tests/src/expected/CompletionExpressions.res.txt +++ b/tests/analysis_tests/tests/src/expected/CompletionExpressions.res.txt @@ -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, nestedRecord: otherRecord}", + "documentation": {"kind": "markdown", "value": "```rescript\n{someBoolField: bool, otherField: option, nestedRecord: otherRecord}\n```"}, "sortText": "A", "insertText": "{$0}", "insertTextFormat": 2 @@ -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, nestedRecord: otherRecord}\n```"} }, { "label": "otherField", - "kind": 4, + "kind": 5, "tags": [], - "detail": "Inline record", - "documentation": null + "detail": "option", + "documentation": {"kind": "markdown", "value": "```rescript\notherField: option\n```\n\n```rescript\n{someBoolField: bool, otherField: option, 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, nestedRecord: otherRecord}\n```"} }] Complete src/CompletionExpressions.res 132:51 @@ -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, nestedRecord: otherRecord}\n```"} }] Complete src/CompletionExpressions.res 135:63 From 2758057dc2a2dcd20974c7d6ad57524de0551551 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A9di-R=C3=A9mi=20Hashim?= Date: Fri, 4 Jul 2025 16:18:35 +0100 Subject: [PATCH 2/3] Extract shared record completion logic into helper --- analysis/src/CompletionBackEnd.ml | 125 +++++++++++------------------- 1 file changed, 46 insertions(+), 79 deletions(-) diff --git a/analysis/src/CompletionBackEnd.ml b/analysis/src/CompletionBackEnd.ml index 75da6ae3f1..b051f6be0c 100644 --- a/analysis/src/CompletionBackEnd.ml +++ b/analysis/src/CompletionBackEnd.ml @@ -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 @@ -1710,88 +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} as extractedType -> ( + 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) -> - 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 []) + getRecordCompletions ~env ~fields ~extractedType | Tarray (env, typ) -> if Debug.verbose () then print_endline "[complete_typed_value]--> Tarray"; if prefix = "" then From 328a3eb572f6cdb1504306b3a7db1e3be9ae4fc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A9di-R=C3=A9mi=20Hashim?= Date: Fri, 4 Jul 2025 19:27:59 +0100 Subject: [PATCH 3/3] Add CHANGELOG entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 05c7f74a62..e872e6d524 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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