aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--README.md1
-rw-r--r--lockfile.json3
-rw-r--r--lua/nvim-treesitter/parsers.lua8
-rw-r--r--queries/rescript/folds.scm12
-rw-r--r--queries/rescript/highlights.scm335
-rw-r--r--queries/rescript/indents.scm36
-rw-r--r--queries/rescript/injections.scm33
-rw-r--r--queries/rescript/locals.scm9
-rw-r--r--tests/indent/rescript/basic.res23
-rw-r--r--tests/indent/rescript/complex.res151
-rw-r--r--tests/indent/rescript/conditional.res104
-rw-r--r--tests/indent/rescript_spec.lua33
12 files changed, 748 insertions, 0 deletions
diff --git a/README.md b/README.md
index 10114e826..f65e118b4 100644
--- a/README.md
+++ b/README.md
@@ -380,6 +380,7 @@ We are looking for maintainers to add more parsers and to write query files for
- [x] [regex](https://github.com/tree-sitter/tree-sitter-regex) (maintained by @theHamsta)
- [x] [rego](https://github.com/FallenAngel97/tree-sitter-rego) (maintained by @FallenAngel97)
- [x] [pip requirements](https://github.com/ObserverOfTime/tree-sitter-requirements) (maintained by @ObserverOfTime)
+- [x] [rescript](https://github.com/rescript-lang/tree-sitter-rescript) (maintained by @ribru17)
- [x] [rnoweb](https://github.com/bamonroe/tree-sitter-rnoweb) (maintained by @bamonroe)
- [x] [robot](https://github.com/Hubro/tree-sitter-robot) (maintained by @Hubro)
- [x] [robots](https://github.com/opa-oz/tree-sitter-robots-txt) (maintained by @opa-oz)
diff --git a/lockfile.json b/lockfile.json
index c99c6d812..6b1123024 100644
--- a/lockfile.json
+++ b/lockfile.json
@@ -620,6 +620,9 @@
"requirements": {
"revision": "5ad9b7581b3334f6ad492847d007f2fac6e6e5f2"
},
+ "rescript": {
+ "revision": "444c127686714b2358622427c3bdba7eb09021c6"
+ },
"rnoweb": {
"revision": "1a74dc0ed731ad07db39f063e2c5a6fe528cae7f"
},
diff --git a/lua/nvim-treesitter/parsers.lua b/lua/nvim-treesitter/parsers.lua
index 1fb398ac2..ad6068d02 100644
--- a/lua/nvim-treesitter/parsers.lua
+++ b/lua/nvim-treesitter/parsers.lua
@@ -1804,6 +1804,14 @@ list.requirements = {
readme_name = "pip requirements",
}
+list.rescript = {
+ install_info = {
+ url = "https://github.com/rescript-lang/tree-sitter-rescript",
+ files = { "src/parser.c", "src/scanner.c" },
+ },
+ maintainers = { "@ribru17" },
+}
+
list.rnoweb = {
install_info = {
url = "https://github.com/bamonroe/tree-sitter-rnoweb",
diff --git a/queries/rescript/folds.scm b/queries/rescript/folds.scm
new file mode 100644
index 000000000..4e658a57c
--- /dev/null
+++ b/queries/rescript/folds.scm
@@ -0,0 +1,12 @@
+[
+ (block)
+ (function)
+ (module_declaration)
+ (type_declaration)
+ (external_declaration)
+ (call_expression)
+ (switch_expression)
+ (parenthesized_expression)
+ (record)
+ (include_statement)+
+] @fold
diff --git a/queries/rescript/highlights.scm b/queries/rescript/highlights.scm
new file mode 100644
index 000000000..e7cba9be8
--- /dev/null
+++ b/queries/rescript/highlights.scm
@@ -0,0 +1,335 @@
+(comment) @comment @spell
+
+; Identifiers
+;------------
+; Escaped identifiers like \"+."
+((value_identifier) @constant.macro
+ (#lua-match? @constant.macro "^%.*$"))
+
+(value_identifier) @variable
+
+[
+ (type_identifier)
+ (unit_type)
+ (list)
+ (list_pattern)
+] @type
+
+((type_identifier) @type.builtin
+ (#any-of? @type.builtin "int" "char" "string" "float" "bool" "unit"))
+
+[
+ (variant_identifier)
+ (polyvar_identifier)
+] @constructor
+
+(record_type_field
+ (property_identifier) @property)
+
+(record_field
+ (property_identifier) @property)
+
+(object
+ (field
+ (property_identifier) @property))
+
+(object_type
+ (field
+ (property_identifier) @property))
+
+(module_identifier) @module
+
+(member_expression
+ (property_identifier) @variable.member)
+
+(value_identifier_path
+ (module_identifier)
+ (value_identifier) @variable)
+
+(record_pattern
+ (value_identifier_path
+ (value_identifier) @variable.member))
+
+(record_pattern
+ (value_identifier) @variable)
+
+(labeled_argument
+ label: (value_identifier) @variable.parameter)
+
+; Parameters
+;----------------
+(list_pattern
+ (value_identifier) @variable.parameter)
+
+(spread_pattern
+ (value_identifier) @variable.parameter)
+
+; String literals
+;----------------
+[
+ (string)
+ (template_string)
+] @string
+
+(character) @character
+
+(escape_sequence) @string.escape
+
+; Other literals
+;---------------
+[
+ (true)
+ (false)
+] @boolean
+
+(number) @number
+
+(polyvar) @constructor
+
+(polyvar_string) @constructor
+
+; Functions
+;----------
+; parameter(s) in parens
+(parameter
+ (value_identifier) @variable.parameter)
+
+(labeled_parameter
+ (value_identifier) @variable.parameter)
+
+; single parameter with no parens
+(function
+ parameter: (value_identifier) @variable.parameter)
+
+(parameter
+ (tuple_pattern
+ (tuple_item_pattern
+ (value_identifier) @variable.parameter)))
+
+(parameter
+ (array_pattern
+ (value_identifier) @variable.parameter))
+
+(parameter
+ (record_pattern
+ (value_identifier) @variable.parameter))
+
+; function identifier in let binding
+(let_binding
+ pattern: (value_identifier) @function
+ body: (function))
+
+; function calls
+(call_expression
+ function: (value_identifier_path
+ (value_identifier) @function.method.call .))
+
+(call_expression
+ function: (value_identifier) @function.call)
+
+; highlight the right-hand side of a pipe operator as a function call
+(pipe_expression
+ (value_identifier) @function.call .)
+
+(pipe_expression
+ (value_identifier_path
+ (value_identifier) @function.method.call .) .)
+
+; Meta
+;-----
+(decorator_identifier) @attribute
+
+(extension_identifier) @keyword
+
+"%" @keyword
+
+; Misc
+;-----
+(polyvar_type_pattern
+ "#" @constructor)
+
+[
+ "include"
+ "open"
+] @keyword.import
+
+[
+ "private"
+ "mutable"
+ "rec"
+] @keyword.modifier
+
+"type" @keyword.type
+
+[
+ "and"
+ "with"
+ "as"
+] @keyword.operator
+
+[
+ "export"
+ "external"
+ "let"
+ "module"
+ "assert"
+ "await"
+ "lazy"
+ "constraint"
+] @keyword
+
+"await" @keyword.coroutine
+
+(function
+ "async" @keyword.coroutine)
+
+(module_unpack
+ "unpack" @keyword)
+
+[
+ "if"
+ "else"
+ "switch"
+ "when"
+] @keyword.conditional
+
+[
+ "exception"
+ "try"
+ "catch"
+] @keyword.exception
+
+(call_expression
+ function: (value_identifier) @keyword.exception
+ (#eq? @keyword.exception "raise"))
+
+[
+ "for"
+ "in"
+ "to"
+ "downto"
+ "while"
+] @keyword.repeat
+
+[
+ "."
+ ","
+ "|"
+ ":"
+] @punctuation.delimiter
+
+[
+ "++"
+ "+"
+ "+."
+ "-"
+ "-."
+ "*"
+ "**"
+ "*."
+ "/."
+ "<="
+ "=="
+ "==="
+ "!"
+ "!="
+ "!=="
+ ">="
+ "&&"
+ "||"
+ "="
+ ":="
+ "->"
+ "|>"
+ ":>"
+ "+="
+ "=>"
+ (uncurry)
+] @operator
+
+; Explicitly enclose these operators with binary_expression
+; to avoid confusion with JSX tag delimiters
+(binary_expression
+ [
+ "<"
+ ">"
+ "/"
+ ] @operator)
+
+[
+ "("
+ ")"
+ "{"
+ "}"
+ "["
+ "]"
+ "<"
+ ">"
+] @punctuation.bracket
+
+(unit
+ [
+ "("
+ ")"
+ ] @constant.builtin)
+
+(template_substitution
+ "${" @punctuation.special
+ "}" @punctuation.special) @none
+
+(polyvar_type
+ [
+ "["
+ "[>"
+ "[<"
+ "]"
+ ] @punctuation.bracket)
+
+[
+ "~"
+ "?"
+ ".."
+ "..."
+] @punctuation.special
+
+(ternary_expression
+ [
+ "?"
+ ":"
+ ] @keyword.conditional.ternary)
+
+; JSX
+;----------
+(jsx_identifier) @tag
+
+(jsx_element
+ open_tag: (jsx_opening_element
+ [
+ "<"
+ ">"
+ ] @tag.delimiter))
+
+(jsx_element
+ close_tag: (jsx_closing_element
+ [
+ "<"
+ "/"
+ ">"
+ ] @tag.delimiter))
+
+(jsx_self_closing_element
+ [
+ "/"
+ ">"
+ "<"
+ ] @tag.delimiter)
+
+(jsx_fragment
+ [
+ ">"
+ "<"
+ "/"
+ ] @tag.delimiter)
+
+(jsx_attribute
+ (property_identifier) @tag.attribute)
diff --git a/queries/rescript/indents.scm b/queries/rescript/indents.scm
new file mode 100644
index 000000000..0b635dd4d
--- /dev/null
+++ b/queries/rescript/indents.scm
@@ -0,0 +1,36 @@
+[
+ (block)
+ (record_type)
+ (record)
+ (parenthesized_expression)
+ (call_expression)
+ (function_type_parameters)
+ (function)
+ (switch_match)
+ (let_declaration)
+ (jsx_element)
+ (jsx_fragment)
+ (jsx_self_closing_element)
+ (object_type)
+] @indent.begin
+
+[
+ "}"
+ ")"
+ (jsx_closing_element)
+] @indent.branch @indent.end
+
+(jsx_self_closing_element
+ "/" @indent.branch
+ ">"? @indent.end)
+
+; </> is captured as 3 different anonymous nodes
+(jsx_fragment
+ "<"
+ "<" @indent.branch)
+
+(jsx_fragment
+ ">"
+ ">" @indent.end)
+
+(comment) @indent.auto
diff --git a/queries/rescript/injections.scm b/queries/rescript/injections.scm
new file mode 100644
index 000000000..434404bef
--- /dev/null
+++ b/queries/rescript/injections.scm
@@ -0,0 +1,33 @@
+((comment) @injection.content
+ (#set! injection.language "comment"))
+
+(extension_expression
+ (extension_identifier) @_name
+ (#eq? @_name "re")
+ (expression_statement
+ (_) @injection.content
+ (#set! injection.language "regex")))
+
+(extension_expression
+ (extension_identifier) @_name
+ (#eq? @_name "raw")
+ (expression_statement
+ (_
+ (_) @injection.content
+ (#set! injection.language "javascript"))))
+
+(extension_expression
+ (extension_identifier) @_name
+ (#eq? @_name "graphql")
+ (expression_statement
+ (_
+ (_) @injection.content
+ (#set! injection.language "graphql"))))
+
+(extension_expression
+ (extension_identifier) @_name
+ (#eq? @_name "relay")
+ (expression_statement
+ (_
+ (_) @injection.content
+ (#set! injection.language "graphql"))))
diff --git a/queries/rescript/locals.scm b/queries/rescript/locals.scm
new file mode 100644
index 000000000..10a663bd7
--- /dev/null
+++ b/queries/rescript/locals.scm
@@ -0,0 +1,9 @@
+(switch_expression) @local.scope
+
+; Definitions
+;------------
+(type_declaration) @local.definition.type
+
+(let_binding) @local.definition.var
+
+(module_declaration) @local.definition.namespace
diff --git a/tests/indent/rescript/basic.res b/tests/indent/rescript/basic.res
new file mode 100644
index 000000000..c5138a7b3
--- /dev/null
+++ b/tests/indent/rescript/basic.res
@@ -0,0 +1,23 @@
+@genType
+type person = {
+ name: string,
+ age: int,
+}
+
+@genType
+type renderMe<'a> = React.component<{
+ "randomString": string,
+ "poly": 'a,
+}>
+
+@genType.import("./hookExample") @react.component
+external make: (
+ ~person: person,
+ ~children: React.element,
+ ~renderMe: renderMe<'a>,
+) => React.element = "makeRenamed"
+
+@genType.import("./hookExample")
+external foo: (~person: person) => string = "foo"
+
+let hi = 'a'
diff --git a/tests/indent/rescript/complex.res b/tests/indent/rescript/complex.res
new file mode 100644
index 000000000..1f21c3364
--- /dev/null
+++ b/tests/indent/rescript/complex.res
@@ -0,0 +1,151 @@
+let hit = ({hit, children}: DocSearch.hitComponent) => {
+ let toTitle = str =>
+ str->Js.String2.charAt(0)->Js.String2.toUpperCase ++ Js.String2.sliceToEnd(str, ~from=1)
+
+ let description = switch hit.url
+ ->Js.String2.split("/")
+ ->Js.Array2.sliceFrom(1)
+ ->Belt.List.fromArray {
+ | list{"blog" as r | "community" as r, ..._} => r->toTitle
+ | list{"docs", doc, version, ...rest} =>
+ let path = rest->Belt.List.toArray
+
+ let info =
+ path
+ ->Js.Array2.slice(~start=0, ~end_=Js.Array2.length(path) - 1)
+ ->Js.Array2.map(path =>
+ switch path {
+ | "api" => "API"
+ | other => toTitle(other)
+ }
+ )
+
+ [doc->toTitle, version->toTitle]->Js.Array2.concat(info)->Js.Array2.joinWith(" / ")
+ | _ => ""
+ }
+
+ <Next.Link href={hit.url} className="flex flex-col w-full">
+ <span className="text-gray-60 captions px-4 pt-3 pb-1 block">
+ {description->React.string}
+ </span>
+ children
+ </Next.Link>
+}
+
+let transformItems = (items: DocSearch.transformItems) => {
+ items->Belt.Array.keepMap(item => {
+ let url = try Webapi.URL.make(item.url)->Some catch {
+ | Js.Exn.Error(obj) =>
+ Js.Console.error2(`Failed to parse URL ${item.url}`, obj)
+ None
+ }
+ switch url {
+ | Some({pathname, hash}) => {...item, url: pathname ++ hash}->Some
+ | None => None
+ }
+ })
+}
+
+@react.component
+let make = () => {
+ let (state, setState) = React.useState(_ => Inactive)
+ let router = Next.Router.useRouter()
+
+ let version = switch Url.parse(router.route).version {
+ | Version(v) => v
+ | _ => "latest"
+ }
+
+ let handleCloseModal = () => {
+ let () = switch ReactDOM.querySelector(".DocSearch-Modal") {
+ | Some(modal) =>
+ switch ReactDOM.querySelector("body") {
+ | Some(body) =>
+ open Webapi
+ body->Element.classList->ClassList.remove("DocSearch--active")
+ modal->Element.addEventListener("transitionend", () => {
+ setState(_ => Inactive)
+ })
+ | None => setState(_ => Inactive)
+ }
+ | None => ()
+ }
+ }
+
+ React.useEffect(() => {
+ let isEditableTag = el =>
+ switch el->tagName {
+ | "TEXTAREA" | "SELECT" | "INPUT" => true
+ | _ => false
+ }
+
+ let focusSearch = e => {
+ switch activeElement {
+ | Some(el) if el->isEditableTag || el->isContentEditable => ()
+ | _ =>
+ setState(_ => Active)
+ e->keyboardEventPreventDefault
+ }
+ }
+
+ let handleGlobalKeyDown = e => {
+ switch e.key {
+ | "/" => focusSearch(e)
+ | "k" if e.ctrlKey || e.metaKey => focusSearch(e)
+ | "Escape" => handleCloseModal()
+ | _ => ()
+ }
+ }
+ addKeyboardEventListener("keydown", handleGlobalKeyDown)
+ Some(() => removeKeyboardEventListener("keydown", handleGlobalKeyDown))
+ }, [setState])
+
+ let onClick = _ => {
+ setState(_ => Active)
+ }
+
+ let onClose = React.useCallback(() => {
+ handleCloseModal()
+ }, [setState])
+
+ <>
+ <button onClick type_="button" className="text-gray-60 hover:text-fire-50 p-2">
+ <Icon.MagnifierGlass className="fill-current" />
+ </button>
+ {switch state {
+ | Active =>
+ switch ReactDOM.querySelector("body") {
+ | Some(element) =>
+ ReactDOM.createPortal(
+ <DocSearch
+ apiKey
+ appId
+ indexName
+ onClose
+ searchParameters={facetFilters: ["version:" ++ version]}
+ initialScrollY={window->scrollY}
+ transformItems={transformItems}
+ hitComponent=hit
+ />
+ element,
+ )
+ | None => React.null
+ }
+ | Inactive => React.null
+ }}
+ </>
+}
+
+let comparable = (type key, ~cmp) => {
+ module N = MakeComparable({
+ type t = key
+ let cmp = cmp
+ })
+ module(N: Comparable with type t = key)
+}
+
+<Next.Link href={hit.url} className="flex flex-col w-full">
+<span className="text-gray-60 captions px-4 pt-3 pb-1 block">
+ {description->React.string}
+ children
+</Next.Link>
diff --git a/tests/indent/rescript/conditional.res b/tests/indent/rescript/conditional.res
new file mode 100644
index 000000000..db8328f26
--- /dev/null
+++ b/tests/indent/rescript/conditional.res
@@ -0,0 +1,104 @@
+include UseClient
+include UseQuery
+include UseMutation
+include UseSubscription
+
+type hookResponse<'ret> = Types.Hooks.hookResponse<'ret> = {
+ operation: Types.operation,
+ fetching: bool,
+ data: option<'ret>,
+ error: option<CombinedError.t>,
+ response: Types.Hooks.response<'ret>,
+ extensions: option<Js.Json.t>,
+ stale: bool,
+}
+
+Js.Array2.slice(~start=0, ~end_=Js.Array2.length(moduleRoute) - 1)
+
+let pathModule = Path.join([dir, version, `${moduleName}.json`])
+
+let {Api.LocMsg.row: row, column, shortMsg} = locMsg
+
+let message = `${"error"->red}: failed to compile examples from ${kind} ${test.id->cyan}\n${errorMessage}`
+
+let version = (evt->ReactEvent.Form.target)["value"]
+
+let rehypePlugins =
+ [Rehype.WithOptions([Plugin(Rehype.slug), SlugOption({prefix: slugPrefix ++ "-"})])]->Some
+
+module Item = {
+ type t = {
+ name: string,
+ sellIn: int,
+ quality: int,
+ }
+
+ let make = (~name, ~sellIn, ~quality): t => {
+ name,
+ sellIn,
+ quality,
+ }
+}
+
+let updateQuality = (items: array<Item.t>) => {
+ items->Js.Array2.map(item => {
+ let newItem = ref(item)
+
+ call(
+ asdf,
+ asdf
+ )
+
+ if (
+ newItem.contents.name != "Aged Brie" && 5 > 2 &&
+ newItem.contents.name != "Backstage passes to a TAFKAL80ETC concert"
+ ) {
+ if newItem.contents.quality > 0 {
+ if newItem.contents.name != "Sulfuras, Hand of Ragnaros" {
+ newItem := {...newItem.contents, quality: newItem.contents.quality - 1}
+ }
+ }
+ } else if newItem.contents.quality < 50 {
+ newItem := {...newItem.contents, quality: newItem.contents.quality + 1}
+
+ if newItem.contents.name == "Backstage passes to a TAFKAL80ETC concert" {
+ if newItem.contents.sellIn < 11 {
+ if newItem.contents.quality < 50 {
+ newItem := {...newItem.contents, quality: newItem.contents.quality + 1}
+ }
+ }
+
+ if newItem.contents.sellIn < 6 {
+ if newItem.contents.quality < 50 {
+ newItem := {...newItem.contents, quality: newItem.contents.quality + 1}
+ }
+ }
+ }
+ }
+
+ if newItem.contents.name != "Sulfuras, Hand of Ragnaros" {
+ newItem := {...newItem.contents, sellIn: newItem.contents.sellIn - 1}
+ }
+
+ if newItem.contents.sellIn < 0 {
+ if newItem.contents.name != "Aged Brie" {
+ if newItem.contents.name != "Backstage passes to a TAFKAL80ETC concert" {
+ if newItem.contents.quality > 0 {
+ if newItem.contents.name != "Sulfuras, Hand of Ragnaros" {
+ newItem := {...newItem.contents, quality: newItem.contents.quality - 1}
+ }
+ }
+ } else {
+ newItem := {
+ ...newItem.contents,
+ quality: newItem.contents.quality - newItem.contents.quality,
+ }
+ }
+ } else if newItem.contents.quality < 50 {
+ newItem := {...newItem.contents, quality: newItem.contents.quality + 1}
+ }
+ }
+
+ newItem.contents
+ })
+}
diff --git a/tests/indent/rescript_spec.lua b/tests/indent/rescript_spec.lua
new file mode 100644
index 000000000..5b1f06abd
--- /dev/null
+++ b/tests/indent/rescript_spec.lua
@@ -0,0 +1,33 @@
+local Runner = require("tests.indent.common").Runner
+
+local run = Runner:new(it, "tests/indent/rescript", {
+ tabstop = 2,
+ shiftwidth = 2,
+ softtabstop = 0,
+ expandtab = true,
+})
+
+describe("indent ReScript:", function()
+ describe("whole file:", function()
+ run:whole_file(".", {})
+ end)
+
+ describe("new line:", function()
+ run:new_line("basic.res", { on_line = 5, text = "x", indent = 0 })
+ run:new_line("basic.res", { on_line = 9, text = '"another": here,', indent = 2 })
+ run:new_line("basic.res", { on_line = 10, text = "}", indent = 0 })
+ run:new_line("basic.res", { on_line = 14, text = "~test: test,", indent = 2 })
+ run:new_line("basic.res", { on_line = 18, text = "x", indent = 0 })
+
+ run:new_line("complex.res", { on_line = 3, text = "x", indent = 2 })
+ run:new_line("complex.res", { on_line = 5, text = "x", indent = 4 })
+ run:new_line("complex.res", { on_line = 17, text = "|", indent = 10 })
+ run:new_line("complex.res", { on_line = 25, text = "x", indent = 2 })
+ run:new_line("complex.res", { on_line = 60, text = "x", indent = 6 })
+ run:new_line("complex.res", { on_line = 120, text = "x", indent = 14 })
+ run:new_line("complex.res", { on_line = 136, text = "x", indent = 2 })
+
+ run:new_line("conditional.res", { on_line = 6, text = "test: bool,", indent = 2 })
+ run:new_line("conditional.res", { on_line = 95, text = "x", indent = 10 })
+ end)
+end)