Skip to content

Generic JSX transform completion #919

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

Merged
merged 7 commits into from
Feb 11, 2024
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
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ examples/*/lib

analysis/tests/lib
analysis/tests/.bsb.lock
analysis/tests/.merlin

analysis/tests-generic-jsx-transform/lib
analysis/tests-generic-jsx-transform/.bsb.lock

tools/node_modules
tools/lib
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
- 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
- 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
- Latest parser for newest syntax features. https://github.com/rescript-lang/rescript-vscode/pull/917
- 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

## 1.38.0

Expand Down
6 changes: 5 additions & 1 deletion analysis/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,20 @@ SHELL = /bin/bash
build-tests:
make -C tests build

build-tests-generic-jsx-transform:
make -C tests-generic-jsx-transform build

build-reanalyze:
make -C reanalyze build

build: build-reanalyze build-tests
build: build-reanalyze build-tests build-tests-generic-jsx-transform

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

test-analysis-binary:
make -C tests test
make -C tests-generic-jsx-transform test

test-reanalyze:
make -C reanalyze test
Expand Down
36 changes: 26 additions & 10 deletions analysis/src/CompletionBackEnd.ml
Original file line number Diff line number Diff line change
Expand Up @@ -1010,14 +1010,23 @@ and getCompletionsForContextPath ~debug ~full ~opens ~rawOpens ~pos ~env ~exact
| Some (builtinNameToComplete, typ)
when Utils.checkName builtinNameToComplete ~prefix:funNamePrefix
~exact:false ->
let name =
match package.genericJsxModule with
| None -> "React." ^ builtinNameToComplete
| Some g ->
g ^ "." ^ builtinNameToComplete
|> String.split_on_char '.'
|> TypeUtils.removeOpensFromCompletionPath ~rawOpens
~package:full.package
|> String.concat "."
in
[
Completion.createWithSnippet
~name:("React." ^ builtinNameToComplete)
~kind:(Value typ) ~env ~sortText:"A"
Completion.createWithSnippet ~name ~kind:(Value typ) ~env
~sortText:"A"
~docstring:
[
"Turns `" ^ builtinNameToComplete
^ "` into `React.element` so it can be used inside of JSX.";
^ "` into a JSX element so it can be used inside of JSX.";
]
();
]
Expand Down Expand Up @@ -1078,7 +1087,7 @@ and getCompletionsForContextPath ~debug ~full ~opens ~rawOpens ~pos ~env ~exact
| Some f -> Some (f.fname.txt, f.typ, env))
| _ -> None
in
["ReactDOM"; "domProps"] |> digToTypeForCompletion
TypeUtils.pathToElementProps package |> digToTypeForCompletion
else
CompletionJsx.getJsxLabels ~componentPath:pathToComponent
~findTypeOfValue ~package
Expand Down Expand Up @@ -1692,9 +1701,14 @@ let rec processCompletable ~debug ~full ~scope ~env ~pos ~forHover completable =
(* We always try to look up completion from the actual domProps type first.
This works in JSXv4. For JSXv3, we have a backup hardcoded list of dom
labels we can use for completion. *)
let fromDomProps =
let pathToElementProps = TypeUtils.pathToElementProps package in
if Debug.verbose () then
Printf.printf
"[completing-lowercase-jsx] Attempting to complete from type at %s\n"
(pathToElementProps |> String.concat ".");
let fromElementProps =
match
["ReactDOM"; "domProps"]
pathToElementProps
|> digToRecordFieldsForCompletion ~debug ~package ~opens ~full ~pos ~env
~scope
with
Expand All @@ -1713,11 +1727,13 @@ let rec processCompletable ~debug ~full ~scope ~env ~pos ~forHover completable =
else None)
|> List.map mkLabel)
in
match fromDomProps with
| Some domProps -> domProps
match fromElementProps with
| Some elementProps -> elementProps
| None ->
if debug then
Printf.printf "Could not find ReactDOM.domProps to complete from.\n";
Printf.printf
"[completing-lowercase-jsx] could not find element props to complete \
from.\n";
(CompletionJsx.domLabels
|> List.filter (fun (name, _t) ->
Utils.startsWith name prefix
Expand Down
9 changes: 8 additions & 1 deletion analysis/src/CompletionDecorators.ml
Original file line number Diff line number Diff line change
Expand Up @@ -164,10 +164,17 @@ Example `@raises(Exn)` or `@raises([E1, E2, E3])` for multiple exceptions.

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

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

[Read more and see examples in the documentation](https://rescript-lang.org/syntax-lookup#react-component-decorator).|};
] );
( "jsx.component",
None,
[
{|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).

You will need this decorator whenever you want to use a JSX component in ReScript JSX expressions.|};
] );
( "return",
Some "return(${1:nullable})",
[
Expand Down
2 changes: 1 addition & 1 deletion analysis/src/CompletionFrontEnd.ml
Original file line number Diff line number Diff line change
Expand Up @@ -659,7 +659,7 @@ let completionWithParser1 ~currentFile ~debug ~offset ~path ~posCursor
let value_binding (iterator : Ast_iterator.iterator)
(value_binding : Parsetree.value_binding) =
let oldInJsxContext = !inJsxContext in
if Utils.isReactComponent value_binding then inJsxContext := true;
if Utils.isJsxComponent value_binding then inJsxContext := true;
(match value_binding with
| {pvb_pat = {ppat_desc = Ppat_constraint (_pat, coreType)}; pvb_expr}
when locHasCursor pvb_expr.pexp_loc -> (
Expand Down
11 changes: 11 additions & 0 deletions analysis/src/Packages.ml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,16 @@ let newBsPackage ~rootPath =
| (major, _), None when major >= 11 -> Some true
| _, ns -> Option.bind ns Json.bool
in
let genericJsxModule =
let jsxConfig = config |> Json.get "jsx" in
match jsxConfig with
| Some jsxConfig -> (
match jsxConfig |> Json.get "module" with
| Some (String m) when String.lowercase_ascii m <> "react" ->
Some m
| _ -> None)
| None -> None
in
let uncurried = uncurried = Some true in
let sourceDirectories =
FindFiles.getSourceDirectories ~includeDev:true ~baseDir:rootPath
Expand Down Expand Up @@ -121,6 +131,7 @@ let newBsPackage ~rootPath =
("Opens from ReScript config file: "
^ (opens |> List.map pathToString |> String.concat " "));
{
genericJsxModule;
suffix;
rescriptVersion;
rootPath;
Expand Down
4 changes: 3 additions & 1 deletion analysis/src/SharedTypes.ml
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,7 @@ type builtInCompletionModules = {
}

type package = {
genericJsxModule: string option;
suffix: string;
rootPath: filePath;
projectFiles: FileSet.t;
Expand Down Expand Up @@ -718,7 +719,8 @@ module Completable = struct
| Cpath cp -> "Cpath " ^ contextPathToString cp
| Cdecorator s -> "Cdecorator(" ^ str s ^ ")"
| CdecoratorPayload (Module s) -> "CdecoratorPayload(module=" ^ s ^ ")"
| CdecoratorPayload (ModuleWithImportAttributes _) -> "CdecoratorPayload(moduleWithImportAttributes)"
| CdecoratorPayload (ModuleWithImportAttributes _) ->
"CdecoratorPayload(moduleWithImportAttributes)"
| CdecoratorPayload (JsxConfig _) -> "JsxConfig"
| CnamedArg (cp, s, sl2) ->
"CnamedArg("
Expand Down
5 changes: 5 additions & 0 deletions analysis/src/TypeUtils.ml
Original file line number Diff line number Diff line change
Expand Up @@ -1079,3 +1079,8 @@ let removeOpensFromCompletionPath ~rawOpens ~package completionPath =
|> removeRawOpens rawOpens
in
completionPathMinusOpens

let pathToElementProps package =
match package.genericJsxModule with
| None -> ["ReactDOM"; "domProps"]
| Some g -> (g |> String.split_on_char '.') @ ["Elements"; "props"]
4 changes: 2 additions & 2 deletions analysis/src/Utils.ml
Original file line number Diff line number Diff line change
Expand Up @@ -156,10 +156,10 @@ let rec unwrapIfOption (t : Types.type_expr) =
| Tconstr (Path.Pident {name = "option"}, [unwrappedType], _) -> unwrappedType
| _ -> t

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

let checkName name ~prefix ~exact =
Expand Down
6 changes: 3 additions & 3 deletions analysis/src/Xform.ml
Original file line number Diff line number Diff line change
Expand Up @@ -215,9 +215,9 @@ module AddTypeAnnotation = struct
match si.pstr_desc with
| Pstr_value (_recFlag, bindings) ->
let processBinding (vb : Parsetree.value_binding) =
(* Can't add a type annotation to a react component, or the compiler crashes *)
let isReactComponent = Utils.isReactComponent vb in
if not isReactComponent then processPattern vb.pvb_pat;
(* Can't add a type annotation to a jsx component, or the compiler crashes *)
let isJsxComponent = Utils.isJsxComponent vb in
if not isJsxComponent then processPattern vb.pvb_pat;
processFunction vb.pvb_expr
in
bindings |> List.iter (processBinding ~argNum:1);
Expand Down
17 changes: 17 additions & 0 deletions analysis/tests-generic-jsx-transform/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
SHELL = /bin/bash

node_modules/.bin/rescript:
npm install

build: node_modules/.bin/rescript
node_modules/.bin/rescript

test: build
./test.sh

clean:
rm -r node_modules lib

.DEFAULT_GOAL := test

.PHONY: clean test
33 changes: 33 additions & 0 deletions analysis/tests-generic-jsx-transform/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions analysis/tests-generic-jsx-transform/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"scripts": {
"build": "rescript",
"clean": "rescript clean -with-deps"
},
"private": true,
"dependencies": {
"rescript": "11.1.0-rc.2"
}
}
11 changes: 11 additions & 0 deletions analysis/tests-generic-jsx-transform/rescript.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"name": "test-generic-jsx-transform",
"sources": [
{
"dir": "src",
"subdirs": true
}
],
"bsc-flags": ["-w -33-44-8"],
"jsx": { "module": "GenericJsx" }
}
63 changes: 63 additions & 0 deletions analysis/tests-generic-jsx-transform/src/GenericJsx.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/* Below is a number of aliases to the common `Jsx` module */
type element = Jsx.element

type component<'props> = Jsx.component<'props>

type componentLike<'props, 'return> = Jsx.componentLike<'props, 'return>

@module("preact")
external jsx: (component<'props>, 'props) => element = "jsx"

@module("preact")
external jsxKeyed: (component<'props>, 'props, ~key: string=?, @ignore unit) => element = "jsx"

@module("preact")
external jsxs: (component<'props>, 'props) => element = "jsxs"

@module("preact")
external jsxsKeyed: (component<'props>, 'props, ~key: string=?, @ignore unit) => element = "jsxs"

/* These identity functions and static values below are optional, but lets
you move things easily to the `element` type. The only required thing to
define though is `array`, which the JSX transform will output. */
external array: array<element> => element = "%identity"
@val external null: element = "null"

external float: float => element = "%identity"
external int: int => element = "%identity"
external string: string => element = "%identity"

/* These are needed for Fragment (<> </>) support */
type fragmentProps = {children?: element}

@module("preact") external jsxFragment: component<fragmentProps> = "Fragment"

/* The Elements module is the equivalent to the ReactDOM module in React. This holds things relevant to _lowercase_ JSX elements. */
module Elements = {
/* Here you can control what props lowercase JSX elements should have.
A base that the React JSX transform uses is provided via JsxDOM.domProps,
but you can make this anything. The editor tooling will support
autocompletion etc for your specific type. */
type props = {
testing?: bool,
test2?: string,
children?: element
}

@module("preact")
external jsx: (string, props) => Jsx.element = "jsx"

@module("preact")
external div: (string, props) => Jsx.element = "jsx"

@module("preact")
external jsxKeyed: (string, props, ~key: string=?, @ignore unit) => Jsx.element = "jsx"

@module("preact")
external jsxs: (string, props) => Jsx.element = "jsxs"

@module("preact")
external jsxsKeyed: (string, props, ~key: string=?, @ignore unit) => Jsx.element = "jsxs"

external someElement: element => option<element> = "%identity"
}
25 changes: 25 additions & 0 deletions analysis/tests-generic-jsx-transform/src/GenericJsxCompletion.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// <div
// ^com

// <div testing={}
// ^com

module SomeComponent = {
@jsx.component
let make = (~someProp) => {
let someString = ""
let someInt = 12
let someArr = [GenericJsx.null]
ignore(someInt)
ignore(someArr)
// someString->st
// ^com
open GenericJsx
<div>
{GenericJsx.string(someProp)}
<div> {GenericJsx.null} </div>
// {someString->st}
// ^com
</div>
}
}
Empty file.
Loading