diff --git a/CHANGELOG.md b/CHANGELOG.md index 502c42f1d..9d0efa108 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ - Handle optional record fields in expression/pattern completion. https://github.com/rescript-lang/rescript-vscode/pull/691 - Expand options in completion to make working with options a bit more ergonomic. https://github.com/rescript-lang/rescript-vscode/pull/690 - Let `_` trigger completion in patterns. https://github.com/rescript-lang/rescript-vscode/pull/692 +- Support inline records in completion. https://github.com/rescript-lang/rescript-vscode/pull/695 #### :nail_care: Polish diff --git a/analysis/src/CompletionBackEnd.ml b/analysis/src/CompletionBackEnd.ml index 34ee9afed..d6af1d4fe 100644 --- a/analysis/src/CompletionBackEnd.ml +++ b/analysis/src/CompletionBackEnd.ml @@ -490,8 +490,8 @@ let domLabels = let showConstructor {Constructor.cname = {txt}; args; res} = txt ^ (match args with - | [] -> "" - | _ -> + | Args [] | InlineRecord _ -> "" + | Args args -> "(" ^ (args |> List.map (fun (typ, _) -> typ |> Shared.typeToString) @@ -1793,29 +1793,35 @@ let printConstructorArgs argsLen ~asSnippet = if List.length !args > 0 then "(" ^ (!args |> String.concat ", ") ^ ")" else "" -let rec completeTypedValue (t : Types.type_expr) ~env ~full ~prefix +let rec completeTypedValue (t : SharedTypes.completionType) ~env ~full ~prefix ~completionContext = - match t |> extractType ~env ~package:full.package with + let extractedType = + match t with + | TypeExpr t -> t |> extractType ~env ~package:full.package + | InlineRecord fields -> Some (TinlineRecord {env; fields}) + in + match extractedType with | Some (Tbool env) -> [ - Completion.create "true" ~kind:(Label (t |> Shared.typeToString)) ~env; - Completion.create "false" ~kind:(Label (t |> Shared.typeToString)) ~env; + Completion.create "true" ~kind:(Label "bool") ~env; + Completion.create "false" ~kind:(Label "bool") ~env; ] |> filterItems ~prefix | Some (Tvariant {env; constructors; variantDecl; variantName}) -> constructors |> List.map (fun (constructor : Constructor.t) -> + let numArgs = + match constructor.args with + | InlineRecord _ -> 1 + | Args args -> List.length args + in Completion.createWithSnippet ~name: (constructor.cname.txt - ^ printConstructorArgs - (List.length constructor.args) - ~asSnippet:false) + ^ printConstructorArgs numArgs ~asSnippet:false) ~insertText: (constructor.cname.txt - ^ printConstructorArgs - (List.length constructor.args) - ~asSnippet:true) + ^ printConstructorArgs numArgs ~asSnippet:true) ~kind: (Constructor (constructor, variantDecl |> Shared.declToString variantName)) @@ -1844,7 +1850,7 @@ let rec completeTypedValue (t : Types.type_expr) ~env ~full ~prefix | Some (Toption (env, t)) -> let innerType = Utils.unwrapIfOption t in let expandedCompletions = - innerType + TypeExpr innerType |> completeTypedValue ~env ~full ~prefix ~completionContext |> List.map (fun (c : Completion.t) -> { @@ -1895,6 +1901,24 @@ let rec completeTypedValue (t : Types.type_expr) ~env ~full ~prefix ~sortText:"A" ~kind:(Value typeExpr) ~env (); ] else []) + | Some (TinlineRecord {env; fields}) -> ( + 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) -> + Completion.create field.fname.txt ~kind:(Label "Inline record") + ~env) + |> filterItems ~prefix + | None -> + if prefix = "" then + [ + Completion.createWithSnippet ~name:"{}" + ~insertText:(if !Cfg.supportsSnippets then "{$0}" else "{}") + ~sortText:"A" ~kind:(Label "Inline record") ~env (); + ] + else []) | Some (Tarray (env, typeExpr)) -> if prefix = "" then [ @@ -1917,16 +1941,22 @@ let rec completeTypedValue (t : Types.type_expr) ~env ~full ~prefix | _ -> [] (** This moves through a nested path via a set of instructions, trying to resolve the type at the end of the path. *) -let rec resolveNested typ ~env ~package ~nested = +let rec resolveNested (typ : completionType) ~env ~package ~nested = match nested with | [] -> Some (typ, env, None) | patternPath :: nested -> ( - match (patternPath, typ |> extractType ~env ~package) with + let extractedType = + match typ with + | TypeExpr typ -> typ |> extractType ~env ~package + | InlineRecord fields -> Some (TinlineRecord {env; fields}) + in + match (patternPath, extractedType) with | Completable.NTupleItem {itemNum}, Some (Tuple (env, tupleItems, _)) -> ( match List.nth_opt tupleItems itemNum with | None -> None - | Some typ -> typ |> resolveNested ~env ~package ~nested) - | NFollowRecordField {fieldName}, Some (Trecord {env; fields}) -> ( + | Some typ -> TypeExpr typ |> resolveNested ~env ~package ~nested) + | ( NFollowRecordField {fieldName}, + Some (TinlineRecord {env; fields} | Trecord {env; fields}) ) -> ( match fields |> List.find_opt (fun (field : field) -> field.fname.txt = fieldName) @@ -1934,12 +1964,15 @@ let rec resolveNested typ ~env ~package ~nested = | None -> None | Some {typ; optional} -> let typ = if optional then Utils.unwrapIfOption typ else typ in - typ |> resolveNested ~env ~package ~nested) + TypeExpr typ |> resolveNested ~env ~package ~nested) | NRecordBody {seenFields}, Some (Trecord {env; typeExpr}) -> - Some (typeExpr, env, Some (Completable.RecordField {seenFields})) + Some (TypeExpr typeExpr, env, Some (Completable.RecordField {seenFields})) + | NRecordBody {seenFields}, Some (TinlineRecord {env; fields}) -> + Some + (InlineRecord fields, env, Some (Completable.RecordField {seenFields})) | ( NVariantPayload {constructorName = "Some"; itemNum = 0}, Some (Toption (env, typ)) ) -> - typ |> resolveNested ~env ~package ~nested + TypeExpr typ |> resolveNested ~env ~package ~nested | ( NVariantPayload {constructorName; itemNum}, Some (Tvariant {env; constructors}) ) -> ( match @@ -1947,11 +1980,13 @@ let rec resolveNested typ ~env ~package ~nested = |> List.find_opt (fun (c : Constructor.t) -> c.cname.txt = constructorName) with - | None -> None - | Some constructor -> ( - match List.nth_opt constructor.args itemNum with + | Some {args = Args args} -> ( + match List.nth_opt args itemNum with | None -> None - | Some (typ, _) -> typ |> resolveNested ~env ~package ~nested)) + | Some (typ, _) -> TypeExpr typ |> resolveNested ~env ~package ~nested) + | Some {args = InlineRecord fields} when itemNum = 0 -> + InlineRecord fields |> resolveNested ~env ~package ~nested + | _ -> None) | ( NPolyvariantPayload {constructorName; itemNum}, Some (Tpolyvariant {env; constructors}) ) -> ( match @@ -1963,9 +1998,9 @@ let rec resolveNested typ ~env ~package ~nested = | Some constructor -> ( match List.nth_opt constructor.args itemNum with | None -> None - | Some typ -> typ |> resolveNested ~env ~package ~nested)) + | Some typ -> TypeExpr typ |> resolveNested ~env ~package ~nested)) | NArray, Some (Tarray (env, typ)) -> - typ |> resolveNested ~env ~package ~nested + TypeExpr typ |> resolveNested ~env ~package ~nested | _ -> None) let rec processCompletable ~debug ~full ~scope ~env ~pos ~forHover @@ -2301,7 +2336,9 @@ Note: The `@react.component` decorator requires the react-jsx config to be set i |> completionsGetTypeEnv with | Some (typ, env) -> ( - match typ |> resolveNested ~env ~package:full.package ~nested with + match + TypeExpr typ |> resolveNested ~env ~package:full.package ~nested + with | None -> fallbackOrEmpty () | Some (typ, env, completionContext) -> let items = @@ -2318,7 +2355,9 @@ Note: The `@react.component` decorator requires the react-jsx config to be set i with | None -> [] | Some (typ, env) -> ( - match typ |> resolveNested ~env ~package:full.package ~nested with + match + TypeExpr typ |> resolveNested ~env ~package:full.package ~nested + with | None -> [] | Some (typ, env, completionContext) -> ( let items = diff --git a/analysis/src/Hover.ml b/analysis/src/Hover.ml index deef54ebf..cf5bbc529 100644 --- a/analysis/src/Hover.ml +++ b/analysis/src/Hover.ml @@ -245,8 +245,8 @@ let newHover ~full:{file; package} ~supportsMarkdownLinks locItem = let typeString, docstring = t |> fromType ~docstring in let argsString = match args with - | [] -> "" - | _ -> + | InlineRecord _ | Args [] -> "" + | Args args -> args |> List.map (fun (t, _) -> Shared.typeToString t) |> String.concat ", " |> Printf.sprintf "(%s)" diff --git a/analysis/src/ProcessCmt.ml b/analysis/src/ProcessCmt.ml index 96f3b15e9..2865dc02d 100644 --- a/analysis/src/ProcessCmt.ml +++ b/analysis/src/ProcessCmt.ml @@ -15,6 +15,20 @@ let attrsToDocstring attrs = | None -> [] | Some docstring -> [docstring] +let mapRecordField {Types.ld_id; ld_type; ld_attributes} = + let astamp = Ident.binding_time ld_id in + let name = Ident.name ld_id in + { + stamp = astamp; + fname = Location.mknoloc name; + typ = ld_type; + optional = Res_parsetree_viewer.hasOptionalAttribute ld_attributes; + docstring = + (match ProcessAttributes.findDocAttribute ld_attributes with + | None -> [] + | Some docstring -> [docstring]); + } + let rec forTypeSignatureItem ~(env : SharedTypes.Env.t) ~(exported : Exported.t) (item : Types.signature_item) = match item with @@ -76,9 +90,11 @@ let rec forTypeSignatureItem ~(env : SharedTypes.Env.t) ~(exported : Exported.t) args = (match cd_args with | Cstr_tuple args -> - args |> List.map (fun t -> (t, Location.none)) - (* TODO(406): constructor record args support *) - | Cstr_record _ -> []); + Args + (args + |> List.map (fun t -> (t, Location.none))) + | Cstr_record fields -> + InlineRecord (fields |> List.map mapRecordField)); res = cd_res; typeDecl = (name, decl); docstring = attrsToDocstring cd_attributes; @@ -93,20 +109,7 @@ let rec forTypeSignatureItem ~(env : SharedTypes.Env.t) ~(exported : Exported.t) Stamps.addConstructor env.stamps stamp declared; item)) | Type_record (fields, _) -> - Record - (fields - |> List.map (fun {Types.ld_id; ld_type; ld_attributes} -> - let astamp = Ident.binding_time ld_id in - let name = Ident.name ld_id in - { - stamp = astamp; - fname = Location.mknoloc name; - typ = ld_type; - optional = - Res_parsetree_viewer.hasOptionalAttribute - ld_attributes; - docstring = attrsToDocstring ld_attributes; - }))); + Record (fields |> List.map mapRecordField)); } ~name ~stamp:(Ident.binding_time ident) ~env type_attributes (Exported.add exported Exported.Type) @@ -198,11 +201,35 @@ let forTypeDeclaration ~env ~(exported : Exported.t) args = (match cd_args with | Cstr_tuple args -> - args - |> List.map (fun t -> - (t.Typedtree.ctyp_type, t.ctyp_loc)) - (* TODO(406) *) - | Cstr_record _ -> []); + Args + (args + |> List.map (fun t -> + (t.Typedtree.ctyp_type, t.ctyp_loc))) + | Cstr_record fields -> + InlineRecord + (fields + |> List.map + (fun (f : Typedtree.label_declaration) -> + let astamp = + Ident.binding_time f.ld_id + in + let name = Ident.name f.ld_id in + { + stamp = astamp; + fname = Location.mknoloc name; + typ = f.ld_type.ctyp_type; + optional = + Res_parsetree_viewer + .hasOptionalAttribute + f.ld_attributes; + docstring = + (match + ProcessAttributes + .findDocAttribute f.ld_attributes + with + | None -> [] + | Some docstring -> [docstring]); + }))); res = (match cd_res with | None -> None diff --git a/analysis/src/SharedTypes.ml b/analysis/src/SharedTypes.ml index 7cc7196c5..9448da7a8 100644 --- a/analysis/src/SharedTypes.ml +++ b/analysis/src/SharedTypes.ml @@ -32,11 +32,17 @@ type field = { docstring: string list; } +type completionType = TypeExpr of Types.type_expr | InlineRecord of field list + +type constructorArgs = + | InlineRecord of field list + | Args of (Types.type_expr * Location.t) list + module Constructor = struct type t = { stamp: int; cname: string Location.loc; - args: (Types.type_expr * Location.t) list; + args: constructorArgs; res: Types.type_expr option; typeDecl: string * Types.type_declaration; docstring: string list; @@ -631,6 +637,7 @@ module Completable = struct fields: field list; typeExpr: Types.type_expr; } + | TinlineRecord of {env: QueryEnv.t; fields: field list} let toString = let completionContextToString = function diff --git a/analysis/tests/src/CompletionExpressions.res b/analysis/tests/src/CompletionExpressions.res index 4e806fbe7..62df14860 100644 --- a/analysis/tests/src/CompletionExpressions.res +++ b/analysis/tests/src/CompletionExpressions.res @@ -116,3 +116,25 @@ let fnTakingRecordWithOptVariant = (r: recordWithOptVariant) => { // let _ = fnTakingRecordWithOptVariant({someVariant: }) // ^com + +type variantWithInlineRecord = + WithInlineRecord({someBoolField: bool, otherField: option, nestedRecord: otherRecord}) + +let fnTakingInlineRecord = (r: variantWithInlineRecord) => { + ignore(r) +} + +// let _ = fnTakingInlineRecord(WithInlineRecord()) +// ^com + +// let _ = fnTakingInlineRecord(WithInlineRecord({})) +// ^com + +// let _ = fnTakingInlineRecord(WithInlineRecord({s})) +// ^com + +// let _ = fnTakingInlineRecord(WithInlineRecord({nestedRecord: })) +// ^com + +// let _ = fnTakingInlineRecord(WithInlineRecord({nestedRecord: {} })) +// ^com diff --git a/analysis/tests/src/expected/CompletionExpressions.res.txt b/analysis/tests/src/expected/CompletionExpressions.res.txt index 6531231ae..d9d67abf4 100644 --- a/analysis/tests/src/expected/CompletionExpressions.res.txt +++ b/analysis/tests/src/expected/CompletionExpressions.res.txt @@ -533,3 +533,87 @@ Completable: Cexpression CArgument Value[fnTakingRecordWithOptVariant]($0)->reco "insertTextFormat": 2 }] +Complete src/CompletionExpressions.res 126:49 +posCursor:[126:49] posNoWhite:[126:48] Found expr:[126:11->126:51] +Pexp_apply ...[126:11->126:31] (...[126:32->126:50]) +Completable: Cexpression CArgument Value[fnTakingInlineRecord]($0)->variantPayload::WithInlineRecord($0) +[{ + "label": "{}", + "kind": 4, + "tags": [], + "detail": "Inline record", + "documentation": null, + "sortText": "A", + "insertText": "{$0}", + "insertTextFormat": 2 + }] + +Complete src/CompletionExpressions.res 129:50 +posCursor:[129:50] posNoWhite:[129:49] Found expr:[129:11->129:53] +Pexp_apply ...[129:11->129:31] (...[129:32->129:52]) +Completable: Cexpression CArgument Value[fnTakingInlineRecord]($0)->variantPayload::WithInlineRecord($0), recordBody +[{ + "label": "someBoolField", + "kind": 4, + "tags": [], + "detail": "Inline record", + "documentation": null + }, { + "label": "otherField", + "kind": 4, + "tags": [], + "detail": "Inline record", + "documentation": null + }, { + "label": "nestedRecord", + "kind": 4, + "tags": [], + "detail": "Inline record", + "documentation": null + }] + +Complete src/CompletionExpressions.res 132:51 +posCursor:[132:51] posNoWhite:[132:50] Found expr:[132:11->132:54] +Pexp_apply ...[132:11->132:31] (...[132:32->132:53]) +Completable: Cexpression CArgument Value[fnTakingInlineRecord]($0)=s->variantPayload::WithInlineRecord($0), recordBody +[{ + "label": "someBoolField", + "kind": 4, + "tags": [], + "detail": "Inline record", + "documentation": null + }] + +Complete src/CompletionExpressions.res 135:63 +posCursor:[135:63] posNoWhite:[135:62] Found expr:[135:11->135:67] +Pexp_apply ...[135:11->135:31] (...[135:32->135:66]) +Completable: Cexpression CArgument Value[fnTakingInlineRecord]($0)->variantPayload::WithInlineRecord($0), recordField(nestedRecord) +[{ + "label": "{}", + "kind": 12, + "tags": [], + "detail": "otherRecord", + "documentation": null, + "sortText": "A", + "insertText": "{$0}", + "insertTextFormat": 2 + }] + +Complete src/CompletionExpressions.res 138:65 +posCursor:[138:65] posNoWhite:[138:64] Found expr:[138:11->138:70] +Pexp_apply ...[138:11->138:31] (...[138:32->138:69]) +Completable: Cexpression CArgument Value[fnTakingInlineRecord]($0)->variantPayload::WithInlineRecord($0), recordField(nestedRecord), recordBody +[{ + "label": "someField", + "kind": 5, + "tags": [], + "detail": "someField: int\n\notherRecord", + "documentation": null + }, { + "label": "otherField", + "kind": 5, + "tags": [], + "detail": "otherField: string\n\notherRecord", + "documentation": null + }] +