From 56d5594af7dca915a49a5c905ff6d1c7722b69b3 Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Thu, 29 Dec 2022 11:31:33 +0100 Subject: [PATCH 1/3] complete JSX prop values --- analysis/src/CompletionBackEnd.ml | 172 ++++++++++-------- analysis/src/CompletionFrontEnd.ml | 36 +++- analysis/src/SharedTypes.ml | 8 + analysis/tests/src/CompletionJsxProps.res | 9 + analysis/tests/src/CompletionSupport.res | 11 ++ .../tests/src/expected/Completion.res.txt | 4 +- .../src/expected/CompletionJsxProps.res.txt | 48 +++++ analysis/tests/src/expected/Jsx2.res.txt | 4 +- 8 files changed, 203 insertions(+), 89 deletions(-) create mode 100644 analysis/tests/src/CompletionJsxProps.res create mode 100644 analysis/tests/src/expected/CompletionJsxProps.res.txt diff --git a/analysis/src/CompletionBackEnd.ml b/analysis/src/CompletionBackEnd.ml index 22b0e25a0..641d0f453 100644 --- a/analysis/src/CompletionBackEnd.ml +++ b/analysis/src/CompletionBackEnd.ml @@ -1572,6 +1572,81 @@ let completeTypedValue ~env ~envWhereCompletionStarted ~full ~prefix in completeTypedValueInner ~env ~full ~prefix ~expandOption +let getJsxLabels ~componentPath ~findTypeOfValue ~package = + match componentPath @ ["make"] |> findTypeOfValue with + | Some (typ, make_env) -> + let rec getFieldsV3 (texp : Types.type_expr) = + match texp.desc with + | Tfield (name, _, t1, t2) -> + let fields = t2 |> getFieldsV3 in + if name = "children" then fields else (name, t1, make_env) :: fields + | Tlink te | Tsubst te | Tpoly (te, []) -> te |> getFieldsV3 + | Tvar None -> [] + | _ -> [] + in + let getFieldsV4 ~path ~typeArgs = + match References.digConstructor ~env:make_env ~package path with + | Some + ( env, + { + item = + { + decl = + { + type_kind = Type_record (labelDecls, _repr); + type_params = typeParams; + }; + }; + } ) -> + labelDecls + |> List.map (fun (ld : Types.label_declaration) -> + let name = Ident.name ld.ld_id in + let t = ld.ld_type |> instantiateType ~typeParams ~typeArgs in + (name, t, env)) + | _ -> [] + in + let rec getLabels (t : Types.type_expr) = + match t.desc with + | Tlink t1 | Tsubst t1 | Tpoly (t1, []) -> getLabels t1 + | Tarrow + ( Nolabel, + { + desc = + ( Tconstr (* Js.t *) (_, [{desc = Tobject (tObj, _)}], _) + | Tobject (tObj, _) ); + }, + _, + _ ) -> + (* JSX V3 *) + getFieldsV3 tObj + | Tarrow (Nolabel, {desc = Tconstr (path, typeArgs, _)}, _, _) + when Path.last path = "props" -> + (* JSX V4 *) + getFieldsV4 ~path ~typeArgs + | Tconstr + ( clPath, + [ + { + desc = + ( Tconstr (* Js.t *) (_, [{desc = Tobject (tObj, _)}], _) + | Tobject (tObj, _) ); + }; + _; + ], + _ ) + when Path.name clPath = "React.componentLike" -> + (* JSX V3 external or interface *) + getFieldsV3 tObj + | Tconstr (clPath, [{desc = Tconstr (path, typeArgs, _)}; _], _) + when Path.name clPath = "React.componentLike" + && Path.last path = "props" -> + (* JSX V4 external or interface *) + getFieldsV4 ~path ~typeArgs + | _ -> [] + in + typ |> getLabels + | None -> [] + let processCompletable ~debug ~full ~scope ~env ~pos ~forHover (completable : Completable.t) = let package = full.package in @@ -1591,6 +1666,7 @@ let processCompletable ~debug ~full ~scope ~env ~pos ~forHover |> getCompletionsForContextPath ~full ~opens ~rawOpens ~allFiles ~pos ~env ~exact:forHover ~scope | Cjsx ([id], prefix, identsSeen) when String.uncapitalize_ascii id = id -> + (* Lowercase JSX tag means builtin *) let mkLabel (name, typString) = Completion.create ~name ~kind:(Label typString) ~env in @@ -1604,99 +1680,37 @@ let processCompletable ~debug ~full ~scope ~env ~pos ~forHover |> List.map mkLabel) @ keyLabels | Cjsx (componentPath, prefix, identsSeen) -> - let labels = - match componentPath @ ["make"] |> findTypeOfValue with - | Some (typ, make_env) -> - let rec getFieldsV3 (texp : Types.type_expr) = - match texp.desc with - | Tfield (name, _, t1, t2) -> - let fields = t2 |> getFieldsV3 in - if name = "children" then fields else (name, t1) :: fields - | Tlink te | Tsubst te | Tpoly (te, []) -> te |> getFieldsV3 - | Tvar None -> [] - | _ -> [] - in - let getFieldsV4 ~path ~typeArgs = - match References.digConstructor ~env:make_env ~package path with - | Some - ( _env, - { - item = - { - decl = - { - type_kind = Type_record (labelDecls, _repr); - type_params = typeParams; - }; - }; - } ) -> - labelDecls - |> List.map (fun (ld : Types.label_declaration) -> - let name = Ident.name ld.ld_id in - let t = - ld.ld_type |> instantiateType ~typeParams ~typeArgs - in - (name, t)) - | _ -> [] - in - let rec getLabels (t : Types.type_expr) = - match t.desc with - | Tlink t1 | Tsubst t1 | Tpoly (t1, []) -> getLabels t1 - | Tarrow - ( Nolabel, - { - desc = - ( Tconstr (* Js.t *) (_, [{desc = Tobject (tObj, _)}], _) - | Tobject (tObj, _) ); - }, - _, - _ ) -> - (* JSX V3 *) - getFieldsV3 tObj - | Tarrow (Nolabel, {desc = Tconstr (path, typeArgs, _)}, _, _) - when Path.last path = "props" -> - (* JSX V4 *) - getFieldsV4 ~path ~typeArgs - | Tconstr - ( clPath, - [ - { - desc = - ( Tconstr (* Js.t *) (_, [{desc = Tobject (tObj, _)}], _) - | Tobject (tObj, _) ); - }; - _; - ], - _ ) - when Path.name clPath = "React.componentLike" -> - (* JSX V3 external or interface *) - getFieldsV3 tObj - | Tconstr (clPath, [{desc = Tconstr (path, typeArgs, _)}; _], _) - when Path.name clPath = "React.componentLike" - && Path.last path = "props" -> - (* JSX V4 external or interface *) - getFieldsV4 ~path ~typeArgs - | _ -> [] - in - typ |> getLabels - | None -> [] - in + let labels = getJsxLabels ~componentPath ~findTypeOfValue ~package in let mkLabel_ name typString = Completion.create ~name ~kind:(Label typString) ~env in - let mkLabel (name, typ) = mkLabel_ name (typ |> Shared.typeToString) in + let mkLabel (name, typ, _env) = + mkLabel_ name (typ |> Shared.typeToString) + in let keyLabels = if Utils.startsWith "key" prefix then [mkLabel_ "key" "string"] else [] in if labels = [] then [] else (labels - |> List.filter (fun (name, _t) -> + |> List.filter (fun (name, _t, _env) -> Utils.startsWith name prefix && name <> "key" && (forHover || not (List.mem name identsSeen))) |> List.map mkLabel) @ keyLabels + | CjsxPropValue {pathToComponent; prefix; propName} -> ( + let targetLabel = + getJsxLabels ~componentPath:pathToComponent ~findTypeOfValue ~package + |> List.find_opt (fun (label, _, _) -> label = propName) + in + let envWhereCompletionStarted = env in + match targetLabel with + | None -> [] + | Some (_, typ, env) -> + typ + |> completeTypedValue ~env ~envWhereCompletionStarted ~full ~prefix + ~expandOption:true) | Cdecorator prefix -> let mkDecorator (name, docstring) = {(Completion.create ~name ~kind:(Label "") ~env) with docstring} diff --git a/analysis/src/CompletionFrontEnd.ml b/analysis/src/CompletionFrontEnd.ml index 22b039103..d6cf2594b 100644 --- a/analysis/src/CompletionFrontEnd.ml +++ b/analysis/src/CompletionFrontEnd.ml @@ -7,6 +7,17 @@ let rec skipWhite text i = | ' ' | '\n' | '\r' | '\t' -> skipWhite text (i - 1) | _ -> i +let extractCompletableArgValueInfo exp = + match exp.Parsetree.pexp_desc with + | Pexp_ident {txt = Lident prefix} -> Some prefix + | Pexp_construct ({txt = Lident prefix}, _) -> Some prefix + | _ -> None + +let isExprHole exp = + match exp.Parsetree.pexp_desc with + | Pexp_extension ({txt = "rescript.exprhole"}, _) -> true + | _ -> false + type prop = { name: string; posStart: int * int; @@ -44,11 +55,28 @@ let findJsxPropsCompletable ~jsxProps ~endPos ~posBeforeCursor ~posAfterCompName None else if prop.exp.pexp_loc |> Loc.hasPos ~pos:posBeforeCursor then (* Cursor on expr assigned *) - None + match extractCompletableArgValueInfo prop.exp with + | Some prefix -> + Some + (CjsxPropValue + { + pathToComponent = + Utils.flattenLongIdent ~jsx:true jsxProps.compName.txt; + prefix; + propName = prop.name; + }) + | _ -> None else if prop.exp.pexp_loc |> Loc.end_ = (Location.none |> Loc.end_) then - (* Expr assigned presumably is "rescript.exprhole" after parser recovery. - To be on the safe side, don't do label completion. *) - None + if isExprHole prop.exp then + Some + (CjsxPropValue + { + pathToComponent = + Utils.flattenLongIdent ~jsx:true jsxProps.compName.txt; + prefix = ""; + propName = prop.name; + }) + else None else loop rest | [] -> let beforeChildrenStart = diff --git a/analysis/src/SharedTypes.ml b/analysis/src/SharedTypes.ml index c90a93c4c..888b5adfc 100644 --- a/analysis/src/SharedTypes.ml +++ b/analysis/src/SharedTypes.ml @@ -537,6 +537,11 @@ module Completable = struct prefix: string; } (** e.g. someFunction(~someBoolArg=), complete for the value of `someBoolArg` (true or false). *) + | CjsxPropValue of { + pathToComponent: string list; + propName: string; + prefix: string; + } (** An extracted type from a type expr *) type extractedType = @@ -597,6 +602,9 @@ module Completable = struct | Optional name -> "~" ^ name ^ "=?") ^ (if prefix <> "" then "=" ^ prefix else "") ^ ")" + | CjsxPropValue {prefix; pathToComponent; propName} -> + "CjsxPropValue " ^ (pathToComponent |> list) ^ " " ^ propName ^ "=" + ^ prefix end module CursorPosition = struct diff --git a/analysis/tests/src/CompletionJsxProps.res b/analysis/tests/src/CompletionJsxProps.res new file mode 100644 index 000000000..65f5fa7e1 --- /dev/null +++ b/analysis/tests/src/CompletionJsxProps.res @@ -0,0 +1,9 @@ +// let _ = {name: ax.name + 1} let make = (name: int): t => {name: name} } + +type testVariant = One | Two | Three(int) + +module TestComponent = { + @react.component + let make = (~on: bool, ~test: testVariant) => { + ignore(on) + ignore(test) + React.null + } +} diff --git a/analysis/tests/src/expected/Completion.res.txt b/analysis/tests/src/expected/Completion.res.txt index 9ee0144ed..56d4cd5a6 100644 --- a/analysis/tests/src/expected/Completion.res.txt +++ b/analysis/tests/src/expected/Completion.res.txt @@ -463,9 +463,7 @@ Completable: Cpath Value[Js, Dict, u] Complete src/Completion.res 59:30 posCursor:[59:30] posNoWhite:[59:29] Found expr:[59:15->59:30] JSX 59:21] second[59:22->59:28]=...[59:29->59:30]> _children:None -posCursor:[59:30] posNoWhite:[59:29] Found expr:[59:29->59:30] -Pexp_ident z:[59:29->59:30] -Completable: Cpath Value[z] +Completable: CjsxPropValue [O, Comp] second=z [{ "label": "zzz", "kind": 12, diff --git a/analysis/tests/src/expected/CompletionJsxProps.res.txt b/analysis/tests/src/expected/CompletionJsxProps.res.txt new file mode 100644 index 000000000..c0c4f5133 --- /dev/null +++ b/analysis/tests/src/expected/CompletionJsxProps.res.txt @@ -0,0 +1,48 @@ +Complete src/CompletionJsxProps.res 0:47 +posCursor:[0:47] posNoWhite:[0:46] Found expr:[0:12->0:47] +JSX 0:43] on[0:44->0:46]=...__ghost__[0:-1->0:-1]> _children:None +Completable: CjsxPropValue [CompletionSupport, TestComponent] on= +[{ + "label": "true", + "kind": 4, + "tags": [], + "detail": "bool", + "documentation": null + }, { + "label": "false", + "kind": 4, + "tags": [], + "detail": "bool", + "documentation": null + }] + +Complete src/CompletionJsxProps.res 3:48 +posCursor:[3:48] posNoWhite:[3:47] Found expr:[3:12->3:48] +JSX 3:43] on[3:44->3:46]=...[3:47->3:48]> _children:None +Completable: CjsxPropValue [CompletionSupport, TestComponent] on=t +[{ + "label": "true", + "kind": 4, + "tags": [], + "detail": "bool", + "documentation": null + }] + +Complete src/CompletionJsxProps.res 6:50 +posCursor:[6:50] posNoWhite:[6:49] Found expr:[6:12->6:50] +JSX 6:43] test[6:44->6:48]=...[6:49->6:50]> _children:None +Completable: CjsxPropValue [CompletionSupport, TestComponent] test=T +[{ + "label": "Two", + "kind": 4, + "tags": [], + "detail": "Two\n\ntype testVariant = One | Two | Three(int)", + "documentation": null + }, { + "label": "Three(_)", + "kind": 4, + "tags": [], + "detail": "Three(int)\n\ntype testVariant = One | Two | Three(int)", + "documentation": null + }] + diff --git a/analysis/tests/src/expected/Jsx2.res.txt b/analysis/tests/src/expected/Jsx2.res.txt index 8dfba3444..00d97ddb4 100644 --- a/analysis/tests/src/expected/Jsx2.res.txt +++ b/analysis/tests/src/expected/Jsx2.res.txt @@ -6,9 +6,7 @@ the type is not great but jump to definition works Complete src/Jsx2.res 8:15 posCursor:[8:15] posNoWhite:[8:14] Found expr:[8:4->8:15] JSX 8:5] second[8:6->8:12]=...[8:13->8:15]> _children:None -posCursor:[8:15] posNoWhite:[8:14] Found expr:[8:13->8:15] -Pexp_ident fi:[8:13->8:15] -Completable: Cpath Value[fi] +Completable: CjsxPropValue [M] second=fi [] Complete src/Jsx2.res 11:20 From 8726850ae172728962c66585d719f063507b41e5 Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Thu, 29 Dec 2022 11:37:02 +0100 Subject: [PATCH 2/3] cleanup --- analysis/src/CompletionFrontEnd.ml | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/analysis/src/CompletionFrontEnd.ml b/analysis/src/CompletionFrontEnd.ml index d6cf2594b..5203c1572 100644 --- a/analysis/src/CompletionFrontEnd.ml +++ b/analysis/src/CompletionFrontEnd.ml @@ -9,8 +9,9 @@ let rec skipWhite text i = let extractCompletableArgValueInfo exp = match exp.Parsetree.pexp_desc with - | Pexp_ident {txt = Lident prefix} -> Some prefix - | Pexp_construct ({txt = Lident prefix}, _) -> Some prefix + | Pexp_ident {txt = Lident txt} -> Some txt + | Pexp_construct ({txt = Lident "()"}, _) -> Some "" + | Pexp_construct ({txt = Lident txt}, _) -> Some txt | _ -> None let isExprHole exp = @@ -134,18 +135,6 @@ let extractJsxProps ~(compName : Longident.t Location.loc) ~args = in args |> processProps ~acc:[] -let extractCompletableArgValueInfo exp = - match exp.Parsetree.pexp_desc with - | Pexp_ident {txt = Lident txt} -> Some txt - | Pexp_construct ({txt = Lident "()"}, _) -> Some "" - | Pexp_construct ({txt = Lident txt}, _) -> Some txt - | _ -> None - -let isExprHole exp = - match exp.Parsetree.pexp_desc with - | Pexp_extension ({txt = "rescript.exprhole"}, _) -> true - | _ -> false - let findArgCompletables ~(args : arg list) ~endPos ~posBeforeCursor ~(contextPath : Completable.contextPath) ~posAfterFunExpr ~charBeforeCursor ~isPipedExpr = From 0c0be5b518dac932cde3e03cef5e5e7c486f528c Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Thu, 29 Dec 2022 18:38:29 +0100 Subject: [PATCH 3/3] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ba76946c..49b4d835a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ #### :rocket: New Feature - Add autocomplete for function argument values (booleans, variants and options. More values coming), both labelled and unlabelled. https://github.com/rescript-lang/rescript-vscode/pull/665 +- Add autocomplete for JSX prop values. https://github.com/rescript-lang/rescript-vscode/pull/667 #### :nail_care: Polish