Skip to content
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

complete JSX prop values #667

Merged
merged 3 commits into from
Dec 29, 2022
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 @@ -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

Expand Down
172 changes: 93 additions & 79 deletions analysis/src/CompletionBackEnd.ml
Original file line number Diff line number Diff line change
Expand Up @@ -1572,6 +1572,81 @@ let completeTypedValue ~env ~envWhereCompletionStarted ~full ~prefix
in
completeTypedValueInner ~env ~full ~prefix ~expandOption

let getJsxLabels ~componentPath ~findTypeOfValue ~package =
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fn is mostly just extracted for reuse, and adapted so it also returns the relevant env for the labels produced.

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
Expand All @@ -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
Expand All @@ -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}
Expand Down
49 changes: 33 additions & 16 deletions analysis/src/CompletionFrontEnd.ml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@ 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 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

type prop = {
name: string;
posStart: int * int;
Expand Down Expand Up @@ -44,11 +56,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 =
Expand Down Expand Up @@ -106,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 =
Expand Down
8 changes: 8 additions & 0 deletions analysis/src/SharedTypes.ml
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,11 @@ module Completable = struct
prefix: string;
}
(** e.g. someFunction(~someBoolArg=<com>), 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 =
Expand Down Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions analysis/tests/src/CompletionJsxProps.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// let _ = <CompletionSupport.TestComponent on=
// ^com

// let _ = <CompletionSupport.TestComponent on=t
// ^com

// let _ = <CompletionSupport.TestComponent test=T
// ^com

11 changes: 11 additions & 0 deletions analysis/tests/src/CompletionSupport.res
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,14 @@ module Test = {
let addSelf = (ax: t) => {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
}
}
4 changes: 1 addition & 3 deletions analysis/tests/src/expected/Completion.res.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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 <O.Comp:[59:15->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,
Expand Down
48 changes: 48 additions & 0 deletions analysis/tests/src/expected/CompletionJsxProps.res.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
Complete src/CompletionJsxProps.res 0:47
posCursor:[0:47] posNoWhite:[0:46] Found expr:[0:12->0:47]
JSX <CompletionSupport.TestComponent:[0:12->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 <CompletionSupport.TestComponent:[3:12->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 <CompletionSupport.TestComponent:[6:12->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
}]

Loading