Skip to content

Commit 5b39b09

Browse files
authored
Generic JSX transform completion (#919)
* add test project for generic JSX transform * adapt completion to look up the correct types when using a generic JSX transform * changelog * adapt the primitive completions inside of jsx to the generic JSX moe * remove uneccessary ignore * update * update
1 parent a4d9c0a commit 5b39b09

24 files changed

+406
-37
lines changed

.gitignore

+3-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ examples/*/lib
55

66
analysis/tests/lib
77
analysis/tests/.bsb.lock
8-
analysis/tests/.merlin
8+
9+
analysis/tests-generic-jsx-transform/lib
10+
analysis/tests-generic-jsx-transform/.bsb.lock
911

1012
tools/node_modules
1113
tools/lib

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
- Relax filter for what local files that come up in from and regular string completion in `@module`. https://github.com/rescript-lang/rescript-vscode/pull/918
2020
- Make from completion trigger for expr hole so we get a nice experience when completing {from: <com>} in `@module`. https://github.com/rescript-lang/rescript-vscode/pull/918
2121
- Latest parser for newest syntax features. https://github.com/rescript-lang/rescript-vscode/pull/917
22+
- Handle completion for DOM/element attributes and attribute values properly when using a generic JSX transform. https://github.com/rescript-lang/rescript-vscode/pull/919
2223

2324
## 1.38.0
2425

analysis/Makefile

+5-1
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,20 @@ SHELL = /bin/bash
33
build-tests:
44
make -C tests build
55

6+
build-tests-generic-jsx-transform:
7+
make -C tests-generic-jsx-transform build
8+
69
build-reanalyze:
710
make -C reanalyze build
811

9-
build: build-reanalyze build-tests
12+
build: build-reanalyze build-tests build-tests-generic-jsx-transform
1013

1114
dce: build-analysis-binary
1215
opam exec reanalyze.exe -- -dce-cmt _build -suppress vendor
1316

1417
test-analysis-binary:
1518
make -C tests test
19+
make -C tests-generic-jsx-transform test
1620

1721
test-reanalyze:
1822
make -C reanalyze test

analysis/src/CompletionBackEnd.ml

+26-10
Original file line numberDiff line numberDiff line change
@@ -1010,14 +1010,23 @@ and getCompletionsForContextPath ~debug ~full ~opens ~rawOpens ~pos ~env ~exact
10101010
| Some (builtinNameToComplete, typ)
10111011
when Utils.checkName builtinNameToComplete ~prefix:funNamePrefix
10121012
~exact:false ->
1013+
let name =
1014+
match package.genericJsxModule with
1015+
| None -> "React." ^ builtinNameToComplete
1016+
| Some g ->
1017+
g ^ "." ^ builtinNameToComplete
1018+
|> String.split_on_char '.'
1019+
|> TypeUtils.removeOpensFromCompletionPath ~rawOpens
1020+
~package:full.package
1021+
|> String.concat "."
1022+
in
10131023
[
1014-
Completion.createWithSnippet
1015-
~name:("React." ^ builtinNameToComplete)
1016-
~kind:(Value typ) ~env ~sortText:"A"
1024+
Completion.createWithSnippet ~name ~kind:(Value typ) ~env
1025+
~sortText:"A"
10171026
~docstring:
10181027
[
10191028
"Turns `" ^ builtinNameToComplete
1020-
^ "` into `React.element` so it can be used inside of JSX.";
1029+
^ "` into a JSX element so it can be used inside of JSX.";
10211030
]
10221031
();
10231032
]
@@ -1078,7 +1087,7 @@ and getCompletionsForContextPath ~debug ~full ~opens ~rawOpens ~pos ~env ~exact
10781087
| Some f -> Some (f.fname.txt, f.typ, env))
10791088
| _ -> None
10801089
in
1081-
["ReactDOM"; "domProps"] |> digToTypeForCompletion
1090+
TypeUtils.pathToElementProps package |> digToTypeForCompletion
10821091
else
10831092
CompletionJsx.getJsxLabels ~componentPath:pathToComponent
10841093
~findTypeOfValue ~package
@@ -1692,9 +1701,14 @@ let rec processCompletable ~debug ~full ~scope ~env ~pos ~forHover completable =
16921701
(* We always try to look up completion from the actual domProps type first.
16931702
This works in JSXv4. For JSXv3, we have a backup hardcoded list of dom
16941703
labels we can use for completion. *)
1695-
let fromDomProps =
1704+
let pathToElementProps = TypeUtils.pathToElementProps package in
1705+
if Debug.verbose () then
1706+
Printf.printf
1707+
"[completing-lowercase-jsx] Attempting to complete from type at %s\n"
1708+
(pathToElementProps |> String.concat ".");
1709+
let fromElementProps =
16961710
match
1697-
["ReactDOM"; "domProps"]
1711+
pathToElementProps
16981712
|> digToRecordFieldsForCompletion ~debug ~package ~opens ~full ~pos ~env
16991713
~scope
17001714
with
@@ -1713,11 +1727,13 @@ let rec processCompletable ~debug ~full ~scope ~env ~pos ~forHover completable =
17131727
else None)
17141728
|> List.map mkLabel)
17151729
in
1716-
match fromDomProps with
1717-
| Some domProps -> domProps
1730+
match fromElementProps with
1731+
| Some elementProps -> elementProps
17181732
| None ->
17191733
if debug then
1720-
Printf.printf "Could not find ReactDOM.domProps to complete from.\n";
1734+
Printf.printf
1735+
"[completing-lowercase-jsx] could not find element props to complete \
1736+
from.\n";
17211737
(CompletionJsx.domLabels
17221738
|> List.filter (fun (name, _t) ->
17231739
Utils.startsWith name prefix

analysis/src/CompletionDecorators.ml

+8-1
Original file line numberDiff line numberDiff line change
@@ -164,10 +164,17 @@ Example `@raises(Exn)` or `@raises([E1, E2, E3])` for multiple exceptions.
164164

165165
You will need this decorator whenever you want to use a ReScript / React component in ReScript JSX expressions.
166166

167-
Note: The `@react.component` decorator requires the react-jsx config to be set in your `bsconfig.json` to enable the required React transformations.
167+
Note: The `@react.component` decorator requires the `jsx` config to be set in your `rescript.json`/`bsconfig.json` to enable the required React transformations.
168168

169169
[Read more and see examples in the documentation](https://rescript-lang.org/syntax-lookup#react-component-decorator).|};
170170
] );
171+
( "jsx.component",
172+
None,
173+
[
174+
{|The `@jsx.component` decorator is used to annotate functions that are JSX components used with ReScript's [generic JSX transform](https://rescript-lang.org/docs/manual/latest/jsx#generic-jsx-transform-jsx-beyond-react-experimental).
175+
176+
You will need this decorator whenever you want to use a JSX component in ReScript JSX expressions.|};
177+
] );
171178
( "return",
172179
Some "return(${1:nullable})",
173180
[

analysis/src/CompletionFrontEnd.ml

+1-1
Original file line numberDiff line numberDiff line change
@@ -659,7 +659,7 @@ let completionWithParser1 ~currentFile ~debug ~offset ~path ~posCursor
659659
let value_binding (iterator : Ast_iterator.iterator)
660660
(value_binding : Parsetree.value_binding) =
661661
let oldInJsxContext = !inJsxContext in
662-
if Utils.isReactComponent value_binding then inJsxContext := true;
662+
if Utils.isJsxComponent value_binding then inJsxContext := true;
663663
(match value_binding with
664664
| {pvb_pat = {ppat_desc = Ppat_constraint (_pat, coreType)}; pvb_expr}
665665
when locHasCursor pvb_expr.pexp_loc -> (

analysis/src/Packages.ml

+11
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,16 @@ let newBsPackage ~rootPath =
5959
| (major, _), None when major >= 11 -> Some true
6060
| _, ns -> Option.bind ns Json.bool
6161
in
62+
let genericJsxModule =
63+
let jsxConfig = config |> Json.get "jsx" in
64+
match jsxConfig with
65+
| Some jsxConfig -> (
66+
match jsxConfig |> Json.get "module" with
67+
| Some (String m) when String.lowercase_ascii m <> "react" ->
68+
Some m
69+
| _ -> None)
70+
| None -> None
71+
in
6272
let uncurried = uncurried = Some true in
6373
let sourceDirectories =
6474
FindFiles.getSourceDirectories ~includeDev:true ~baseDir:rootPath
@@ -121,6 +131,7 @@ let newBsPackage ~rootPath =
121131
("Opens from ReScript config file: "
122132
^ (opens |> List.map pathToString |> String.concat " "));
123133
{
134+
genericJsxModule;
124135
suffix;
125136
rescriptVersion;
126137
rootPath;

analysis/src/SharedTypes.ml

+3-1
Original file line numberDiff line numberDiff line change
@@ -497,6 +497,7 @@ type builtInCompletionModules = {
497497
}
498498

499499
type package = {
500+
genericJsxModule: string option;
500501
suffix: string;
501502
rootPath: filePath;
502503
projectFiles: FileSet.t;
@@ -718,7 +719,8 @@ module Completable = struct
718719
| Cpath cp -> "Cpath " ^ contextPathToString cp
719720
| Cdecorator s -> "Cdecorator(" ^ str s ^ ")"
720721
| CdecoratorPayload (Module s) -> "CdecoratorPayload(module=" ^ s ^ ")"
721-
| CdecoratorPayload (ModuleWithImportAttributes _) -> "CdecoratorPayload(moduleWithImportAttributes)"
722+
| CdecoratorPayload (ModuleWithImportAttributes _) ->
723+
"CdecoratorPayload(moduleWithImportAttributes)"
722724
| CdecoratorPayload (JsxConfig _) -> "JsxConfig"
723725
| CnamedArg (cp, s, sl2) ->
724726
"CnamedArg("

analysis/src/TypeUtils.ml

+5
Original file line numberDiff line numberDiff line change
@@ -1079,3 +1079,8 @@ let removeOpensFromCompletionPath ~rawOpens ~package completionPath =
10791079
|> removeRawOpens rawOpens
10801080
in
10811081
completionPathMinusOpens
1082+
1083+
let pathToElementProps package =
1084+
match package.genericJsxModule with
1085+
| None -> ["ReactDOM"; "domProps"]
1086+
| Some g -> (g |> String.split_on_char '.') @ ["Elements"; "props"]

analysis/src/Utils.ml

+2-2
Original file line numberDiff line numberDiff line change
@@ -156,10 +156,10 @@ let rec unwrapIfOption (t : Types.type_expr) =
156156
| Tconstr (Path.Pident {name = "option"}, [unwrappedType], _) -> unwrappedType
157157
| _ -> t
158158

159-
let isReactComponent (vb : Parsetree.value_binding) =
159+
let isJsxComponent (vb : Parsetree.value_binding) =
160160
vb.pvb_attributes
161161
|> List.exists (function
162-
| {Location.txt = "react.component"}, _payload -> true
162+
| {Location.txt = "react.component" | "jsx.component"}, _payload -> true
163163
| _ -> false)
164164

165165
let checkName name ~prefix ~exact =

analysis/src/Xform.ml

+3-3
Original file line numberDiff line numberDiff line change
@@ -215,9 +215,9 @@ module AddTypeAnnotation = struct
215215
match si.pstr_desc with
216216
| Pstr_value (_recFlag, bindings) ->
217217
let processBinding (vb : Parsetree.value_binding) =
218-
(* Can't add a type annotation to a react component, or the compiler crashes *)
219-
let isReactComponent = Utils.isReactComponent vb in
220-
if not isReactComponent then processPattern vb.pvb_pat;
218+
(* Can't add a type annotation to a jsx component, or the compiler crashes *)
219+
let isJsxComponent = Utils.isJsxComponent vb in
220+
if not isJsxComponent then processPattern vb.pvb_pat;
221221
processFunction vb.pvb_expr
222222
in
223223
bindings |> List.iter (processBinding ~argNum:1);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
SHELL = /bin/bash
2+
3+
node_modules/.bin/rescript:
4+
npm install
5+
6+
build: node_modules/.bin/rescript
7+
node_modules/.bin/rescript
8+
9+
test: build
10+
./test.sh
11+
12+
clean:
13+
rm -r node_modules lib
14+
15+
.DEFAULT_GOAL := test
16+
17+
.PHONY: clean test

analysis/tests-generic-jsx-transform/package-lock.json

+33
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"scripts": {
3+
"build": "rescript",
4+
"clean": "rescript clean -with-deps"
5+
},
6+
"private": true,
7+
"dependencies": {
8+
"rescript": "11.1.0-rc.2"
9+
}
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"name": "test-generic-jsx-transform",
3+
"sources": [
4+
{
5+
"dir": "src",
6+
"subdirs": true
7+
}
8+
],
9+
"bsc-flags": ["-w -33-44-8"],
10+
"jsx": { "module": "GenericJsx" }
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/* Below is a number of aliases to the common `Jsx` module */
2+
type element = Jsx.element
3+
4+
type component<'props> = Jsx.component<'props>
5+
6+
type componentLike<'props, 'return> = Jsx.componentLike<'props, 'return>
7+
8+
@module("preact")
9+
external jsx: (component<'props>, 'props) => element = "jsx"
10+
11+
@module("preact")
12+
external jsxKeyed: (component<'props>, 'props, ~key: string=?, @ignore unit) => element = "jsx"
13+
14+
@module("preact")
15+
external jsxs: (component<'props>, 'props) => element = "jsxs"
16+
17+
@module("preact")
18+
external jsxsKeyed: (component<'props>, 'props, ~key: string=?, @ignore unit) => element = "jsxs"
19+
20+
/* These identity functions and static values below are optional, but lets
21+
you move things easily to the `element` type. The only required thing to
22+
define though is `array`, which the JSX transform will output. */
23+
external array: array<element> => element = "%identity"
24+
@val external null: element = "null"
25+
26+
external float: float => element = "%identity"
27+
external int: int => element = "%identity"
28+
external string: string => element = "%identity"
29+
30+
/* These are needed for Fragment (<> </>) support */
31+
type fragmentProps = {children?: element}
32+
33+
@module("preact") external jsxFragment: component<fragmentProps> = "Fragment"
34+
35+
/* The Elements module is the equivalent to the ReactDOM module in React. This holds things relevant to _lowercase_ JSX elements. */
36+
module Elements = {
37+
/* Here you can control what props lowercase JSX elements should have.
38+
A base that the React JSX transform uses is provided via JsxDOM.domProps,
39+
but you can make this anything. The editor tooling will support
40+
autocompletion etc for your specific type. */
41+
type props = {
42+
testing?: bool,
43+
test2?: string,
44+
children?: element
45+
}
46+
47+
@module("preact")
48+
external jsx: (string, props) => Jsx.element = "jsx"
49+
50+
@module("preact")
51+
external div: (string, props) => Jsx.element = "jsx"
52+
53+
@module("preact")
54+
external jsxKeyed: (string, props, ~key: string=?, @ignore unit) => Jsx.element = "jsx"
55+
56+
@module("preact")
57+
external jsxs: (string, props) => Jsx.element = "jsxs"
58+
59+
@module("preact")
60+
external jsxsKeyed: (string, props, ~key: string=?, @ignore unit) => Jsx.element = "jsxs"
61+
62+
external someElement: element => option<element> = "%identity"
63+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// <div
2+
// ^com
3+
4+
// <div testing={}
5+
// ^com
6+
7+
module SomeComponent = {
8+
@jsx.component
9+
let make = (~someProp) => {
10+
let someString = ""
11+
let someInt = 12
12+
let someArr = [GenericJsx.null]
13+
ignore(someInt)
14+
ignore(someArr)
15+
// someString->st
16+
// ^com
17+
open GenericJsx
18+
<div>
19+
{GenericJsx.string(someProp)}
20+
<div> {GenericJsx.null} </div>
21+
// {someString->st}
22+
// ^com
23+
</div>
24+
}
25+
}

analysis/tests-generic-jsx-transform/src/expected/GenericJsx.res.txt

Whitespace-only changes.

0 commit comments

Comments
 (0)