diff options
| -rw-r--r-- | README.md | 1 | ||||
| -rw-r--r-- | lockfile.json | 3 | ||||
| -rw-r--r-- | lua/nvim-treesitter/parsers.lua | 8 | ||||
| -rw-r--r-- | queries/rescript/folds.scm | 12 | ||||
| -rw-r--r-- | queries/rescript/highlights.scm | 335 | ||||
| -rw-r--r-- | queries/rescript/indents.scm | 36 | ||||
| -rw-r--r-- | queries/rescript/injections.scm | 33 | ||||
| -rw-r--r-- | queries/rescript/locals.scm | 9 | ||||
| -rw-r--r-- | tests/indent/rescript/basic.res | 23 | ||||
| -rw-r--r-- | tests/indent/rescript/complex.res | 151 | ||||
| -rw-r--r-- | tests/indent/rescript/conditional.res | 104 | ||||
| -rw-r--r-- | tests/indent/rescript_spec.lua | 33 |
12 files changed, 748 insertions, 0 deletions
@@ -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) |
