From ff67f6c6107421034db615fbd356e0ffd7093327 Mon Sep 17 00:00:00 2001 From: "Luke Boswell (Linux-Desktop)" Date: Fri, 21 Aug 2026 18:35:22 +1000 Subject: [PATCH] Update tree-sitter grammar for modern Roc syntax --- README.md | 2 +- extension.toml | 2 +- justfile | 14 ++- languages/roc/brackets.scm | 7 ++ languages/roc/config.toml | 1 + languages/roc/highlights.scm | 171 ++++++++++++++++++---------- languages/roc/indents.scm | 34 +++++- languages/roc/injections.scm | 50 -------- languages/roc/outline.scm | 5 + languages/roc/textobjects.scm | 9 +- scripts/check-highlight-captures.sh | 59 ++++++++++ 11 files changed, 230 insertions(+), 124 deletions(-) create mode 100644 languages/roc/brackets.scm create mode 100755 scripts/check-highlight-captures.sh diff --git a/README.md b/README.md index b386f80..d88ccfa 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,7 @@ To update: ### Useful Commands ```sh -# Sync query files from grammar to languages +# Sync the generated Zed highlight query from the grammar just sync-queries # Clean build artifacts diff --git a/extension.toml b/extension.toml index 4cd8c8c..83fc1fd 100644 --- a/extension.toml +++ b/extension.toml @@ -9,7 +9,7 @@ languages = ["languages/roc"] [grammars.roc] repository = "https://github.com/faldor20/tree-sitter-roc" -commit = "edc18052a9d7382ac9f9f5bf413db3a78d5ea12c" +commit = "fc80e44f452314518550d947bcfdde6c3001a405" [language_servers.roc] language = "Roc" diff --git a/justfile b/justfile index 4830ce0..9aaa240 100644 --- a/justfile +++ b/justfile @@ -7,13 +7,20 @@ check: cargo test cargo build --target wasm32-wasip2 -# Sync query files from the tree-sitter-roc commit in extension.toml. +# Sync the generated highlight query from the tree-sitter-roc commit in extension.toml. +# Other editor queries are maintained here because Zed's behavior differs. sync-queries: mkdir -p grammars if [ ! -d grammars/roc/.git ]; then git clone {{grammar_repository}} grammars/roc; fi cd grammars/roc && git remote set-url origin {{grammar_repository}} cd grammars/roc && git fetch origin && git checkout {{grammar_commit}} - cp -v grammars/roc/queries/*.scm languages/roc/ + cp -v grammars/roc/queries-generated/zed/queries/highlights.scm languages/roc/ + just check-highlight-captures + rm -f grammars/roc.wasm + +# Reject highlight captures that Zed themes do not support. +check-highlight-captures: + scripts/check-highlight-captures.sh # Update grammar to a specific commit and sync queries update-grammar COMMIT: @@ -21,7 +28,8 @@ update-grammar COMMIT: if [ ! -d grammars/roc/.git ]; then git clone {{grammar_repository}} grammars/roc; fi cd grammars/roc && git remote set-url origin {{grammar_repository}} cd grammars/roc && git fetch origin && git checkout {{COMMIT}} - cp -v grammars/roc/queries/*.scm languages/roc/ + cp -v grammars/roc/queries-generated/zed/queries/highlights.scm languages/roc/ + just check-highlight-captures rm -f grammars/roc.wasm @echo "Done. Click 'Rebuild' in Zed to recompile the grammar." diff --git a/languages/roc/brackets.scm b/languages/roc/brackets.scm new file mode 100644 index 0000000..82a845d --- /dev/null +++ b/languages/roc/brackets.scm @@ -0,0 +1,7 @@ +("(" @open ")" @close) +("[" @open "]" @close) +("{" @open "}" @close) + +(nominal_methods + ".{" @open + "}" @close) diff --git a/languages/roc/config.toml b/languages/roc/config.toml index 7ad4ebb..58326e9 100644 --- a/languages/roc/config.toml +++ b/languages/roc/config.toml @@ -8,6 +8,7 @@ brackets = [ { start = "(", end = ")", close = true, newline = true }, { start = "\"", end = "\"", close = true, newline = false, not_in = [ "string", + "comment", ] }, { start = "'", end = "'", close = true, newline = false, not_in = [ "string", diff --git a/languages/roc/highlights.scm b/languages/roc/highlights.scm index fb3fd50..8b43f52 100644 --- a/languages/roc/highlights.scm +++ b/languages/roc/highlights.scm @@ -8,7 +8,7 @@ -(module) @namespace +(module) @type @@ -26,16 +26,26 @@ (argument_patterns (_ (_ (_ (_ (_ (identifier_pattern (identifier) @variable.parameter))))))) (spread_pattern (identifier) @variable.parameter) (match_branch pattern: (_ (identifier_pattern (identifier) @variable.parameter))) +(tag_pattern + (_)* + (identifier_pattern (identifier) @variable.parameter)) + +; Identifier patterns introduce bindings. Plain value declarations are +; overridden back to variables below; function declarations are overridden by +; the later, higher-priority function rule. +(identifier_pattern (identifier) @variable.parameter) +(value_declaration + (decl_left (identifier_pattern (identifier) @variable))) ; N/A ; @variable.other.member.private -(field_name) @variable.other.member +(field_name) @property ; Note: This query matches the second identifier and all subsequent ones. -(field_access_expr (identifier) @variable.other.member) +(field_access_expr (identifier) @property) ; Note: This query highlights module members as records instead of free variables, ; which avoids highlighting them as out-of-scope vars. -(variable_expr (module) (identifier) @variable.other.member) +(variable_expr (module) (identifier) @property) ; N/A ; @variable.other @@ -49,25 +59,25 @@ -(inferred) @type.roc-special.inferred +(inferred) @type -(bound_variable) @type.parameter +(bound_variable) @type -(tag_type) @type.enum.variant +(tag_type) @variant ; N/A ; @type.enum ; Opinion: Type defs cross into documentation ; and should be highlighted differently from normal code. -(opaque_type_def (_ (concrete_type) @type.definition)) +(opaque_type_def (_ (concrete_type) @type)) -((concrete_type) @type.builtin - (#match? @type.builtin "^(Dec|F(32|64))")) -((concrete_type) @type.builtin - (#match? @type.builtin "^[IU](8|16|32|64|128)")) -((concrete_type) @type.builtin - (#match? @type.builtin "^(Bool|Box|Dec|Decode|Dict|Encode|Hash|Inspect|Int|List|Num|Result|Set|Str)")) +((concrete_type) @type + (#match? @type "^(Dec|F(32|64))")) +((concrete_type) @type + (#match? @type "^[IU](8|16|32|64|128)")) +((concrete_type) @type + (#match? @type "^(Bool|Box|Dec|Decode|Dict|Encode|Hash|Inspect|Int|List|Num|Result|Set|Str)")) ; Note: See the lower-priority queries below for a `@type` query. @@ -81,12 +91,12 @@ -(app_header (packages_list (platform_ref ((package_uri) @string.special.url)))) +(app_header (packages_list (platform_ref ((package_uri) @link_uri)))) -(app_header (packages_list (platform_ref ((package_uri) @string.special.url)))) +(app_header (packages_list (platform_ref ((package_uri) @link_uri)))) ; N/A ; @string.special.symbol @@ -102,31 +112,27 @@ (string) @string (multiline_string) @string +(const_pattern (string_pattern_capture)) @string ; TODO: Differentiate between values, functions, and types. -(import_expr (exposing ((ident) @special.roc-special.exposed))) +(import_expr (exposing ((ident) @label))) -(app_header (packages_list ((platform_ref) @special.roc-special.package))) +(app_header (packages_list ((platform_ref) @label))) ; TODO: Differentiate between values, functions, and types. -(app_header (provides_list ((identifier) @special.roc-special.provided))) +(app_header (provides_list ((identifier) @label))) ; N/A ; @special -[ - (interpolation_char) -] @punctuation.special - [ "," ":" (arrow) - (fat_arrow) ] @punctuation.delimiter [ @@ -149,17 +155,18 @@ "." "&" ; "|" ; TODO: This conflicts with the `"|" @punctuation.bracket` query, so improve both. - "<-" "->" ".." "!" "*" "-" "^" - (wildcard_pattern) + (fat_arrow) (operator) ] @operator +(wildcard_pattern) @variable.special + ; N/A @@ -176,7 +183,7 @@ ; TODO: Implement this for `and`, `or`, and any others. [ (suffix_operator) - ] @keyword.operator + ] @operator ; N/A ; @keyword.function @@ -185,14 +192,16 @@ ; @keyword.directive ; TODO: Also implement this for `return`. -[(suffix_operator ) "return"]@keyword.control.return +[(suffix_operator ) "return"]@keyword -; TODO: Implement this for `for` and `while`. -; @keyword.control.repeat +[ + "for" + "while" +] @keyword [ "import" -] @keyword.control.import +] @keyword ; N/A ; @keyword.control.exception @@ -202,21 +211,29 @@ "if" (match) -] @keyword.control.conditional +] @keyword [ "app" (as) "as" + (break_expr) + "crash" "expect" "exposing" + "hosted" + "in" "module" "package" + "packages" "platform" + "provides" + "requires" + "targets" (to) "var" (where) -] @keyword.control +] @keyword ; N/A ; @@ -226,10 +243,19 @@ [ "dbg" -] @function.builtin +] @function (value_declaration (decl_left (identifier_pattern (identifier) @function)) (expr_body (anon_fun_expr))) +(value_declaration (decl_left (identifier_pattern (identifier) @function)) + (expr_body + (function_call_pnc_expr + caller: (anon_fun_expr)))) +(value_declaration (decl_left (identifier_pattern (identifier) @function)) + (expr_body + (bin_op_expr + part: (function_call_pnc_expr + caller: (anon_fun_expr))))) (function_call_pnc_expr caller: (variable_expr (identifier) @function)) (function_call_pnc_expr caller: (field_access_expr (identifier) @function .)) (bin_op_expr (operator "->") (variable_expr (identifier) @function)) @@ -244,29 +270,36 @@ [ - (decimal) (float) -] @constant.numeric.float +] @number + +((number_with_suffix) @number + (#match? @number "^[+-]?[0-9][0-9_]*(\\.[0-9]|[eE])")) [ - (iint) (int) - (natural) - (uint) (xint) -] @constant.numeric.integer +] @number + +((number_with_suffix) @number + (#not-match? @number "^[+-]?[0-9][0-9_]*(\\.[0-9]|[eE])")) ; N/A ; @constant.numeric -(escape_char) @constant.character.escape +(escape_char) @string.escape -(char) @constant.character +(char) @string -(tag_expr(tag (module) @ignoreme.module "." (identifier)@constant.builtin.boolean) - (#eq? @constant.builtin.boolean "True") (#eq? @ignoreme.module "Bool")) -(tag_expr (tag(module) @module "." (identifier)@constant.builtin.boolean) - (#eq? @constant.builtin.boolean "False") (#eq? @module "Bool")) +[ + (literal_type_suffix) + (record_builder_suffix) +] @type + +(tag_expr(tag (module) @variable.special "." (identifier)@boolean) + (#eq? @boolean "True") (#eq? @variable.special "Bool")) +(tag_expr (tag(module) @type "." (identifier)@boolean) + (#eq? @boolean "False") (#eq? @type "Bool")) ; N/A ; @constant.builtin @@ -276,9 +309,9 @@ -(line_comment) @comment.line +(line_comment) @comment -(doc_comment) @comment.block.documentation +(doc_comment) @comment.doc ; N/A ; @comment.block @@ -293,24 +326,24 @@ -(record_field_type (field_name) @variable.other.member.roc-special.in-typedef) +(record_field_type (field_name) @property) -(function_type "," @punctuation.delimiter.roc-special.in-typedef) -(record_type "," @punctuation.delimiter.roc-special.in-typedef) -(tuple_type "," @punctuation.delimiter.roc-special.in-typedef) +(function_type "," @punctuation.delimiter) +(record_type "," @punctuation.delimiter) +(tuple_type "," @punctuation.delimiter) -(parenthesized_type ["(" ")"] @punctuation.bracket.roc-special.in-typedef) -(record_type ["{" "}"] @punctuation.bracket.roc-special.in-typedef) -(tags_type ["[" "]"] @punctuation.bracket.roc-special.in-typedef) -(tuple_type ["(" ")"] @punctuation.bracket.roc-special.in-typedef) +(parenthesized_type ["(" ")"] @punctuation.bracket) +(record_type ["{" "}"] @punctuation.bracket) +(tags_type ["[" "]"] @punctuation.bracket) +(tuple_type ["(" ")"] @punctuation.bracket) (static_dispatch_target -(identifier)@function.method) +(identifier)@function) -((module) @namespace.roc-special.builtin - (#match? @namespace.roc-special.builtin "^(Bool|Box|Decode|Dict|Encode|Hash|Inspect|List|Num|Result|Set|Str)")) +((module) @type + (#match? @type "^(Bool|Box|Decode|Dict|Encode|Hash|Inspect|List|Num|Result|Set|Str)")) ; TODO(bugfix): `Set` yields an ERROR in `expect Set.from_list(paths_as_str) == Set.from_list(["nested-dir/a", "nested-dir/child"])` @@ -319,6 +352,26 @@ ;; Higher-priorty queries ;; +(interpolation_char + "${" @punctuation.special + "}" @punctuation.special) +(nominal_methods + ".{" @punctuation.bracket + "}" @punctuation.bracket) + +(alias_type_def + (apply_type (concrete_type) @type) + ":" @operator) +(opaque_type_def + (apply_type (concrete_type) @type) + (double_colon) @operator) +(nominal_type_def + (apply_type (concrete_type) @type) + (colon_equals) @operator) +(string_pattern_capture + "${" @punctuation.special + "}" @punctuation.special) + ;; Highlight names (like `@comment.block.documentation`) are arbitrary. diff --git a/languages/roc/indents.scm b/languages/roc/indents.scm index ad14816..c119d28 100644 --- a/languages/roc/indents.scm +++ b/languages/roc/indents.scm @@ -1,6 +1,28 @@ -; ;;How to write indents: -; ;;think about the structures that should have indents inside and mark them with indent and extend -; ;;think about the structures that indicate the end of an indent and mark them with extend.prevent-once -; ;;find partially complete structures and create speciall rules for them. -; ;;eg: (ERROR (is)) -(expr_body)@indent \ No newline at end of file +[ + (body_expression "}" @end) + (record_expr "}" @end) + (record_type "}" @end) + (list_expr "]" @end) + (list_pattern "]" @end) + (tags_type "]" @end) + (tuple_expr ")" @end) + (tuple_type ")" @end) + (parenthesized_expr ")" @end) + (parenthesized_type ")" @end) + (nominal_methods "}" @end) +] @indent + +[ + (value_declaration) + (var_declaration) + (anon_fun_expr) + (expect) + (if_expr) + (then) + (else) + (else_if) + (match_expr) + (match_branch) + (for_expr) + (while_expr) +] @indent diff --git a/languages/roc/injections.scm b/languages/roc/injections.scm index 56cc235..559c865 100644 --- a/languages/roc/injections.scm +++ b/languages/roc/injections.scm @@ -1,53 +1,3 @@ -;injection from function calls -(function_call_pnc_expr - caller: (variable_expr (identifier) @injection.language) - (const [(multiline_string) (string)] @injection.content) - (#any-of? @injection.language - "json" - "toml" - "yaml" - "xml" - "sql" - "lua" - "js" - "html" - "css" - "http" - "jq" - "latex" - "md" - "nix" - "regex" - ) -) -;injection from piping function calls -(bin_op_expr - part: (const - [(multiline_string) (string)] @injection.content - ) - part: (operator ("->")) - part: (variable_expr - (identifier) @injection.language - ) - (#any-of? @injection.language - "json" - "toml" - "yaml" - "xml" - "sql" - "lua" - "js" - "html" - "css" - "http" - "jq" - "latex" - "md" - "nix" - "regex" - ) -) - ( [ (line_comment) diff --git a/languages/roc/outline.scm b/languages/roc/outline.scm index 336decc..f0639f6 100644 --- a/languages/roc/outline.scm +++ b/languages/roc/outline.scm @@ -13,6 +13,11 @@ (apply_type (concrete_type) @name)) @item) +(file + (nominal_type_def + (apply_type + (concrete_type) @name)) @item) + (file (annotation_type_def (annotation_pre_colon diff --git a/languages/roc/textobjects.scm b/languages/roc/textobjects.scm index 51797c4..2cfc813 100644 --- a/languages/roc/textobjects.scm +++ b/languages/roc/textobjects.scm @@ -1,6 +1,7 @@ -(anon_fun_expr - (expr_body) @function.inside -) @function.around +(value_declaration + body: (expr_body + (anon_fun_expr + (expr_body) @function.inside))) @function.around (argument_patterns ((_) @parameter.inside . ","? @parameter.around) @parameter.around @@ -25,6 +26,7 @@ (annotation_type_def ) @class.inside (alias_type_def ) @class.inside (opaque_type_def ) @class.inside + (nominal_type_def ) @class.inside ] @class.around (apply_type_arg) @parameter.inside @@ -36,4 +38,3 @@ (line_comment) @comment.around (doc_comment) @comment.around - diff --git a/scripts/check-highlight-captures.sh b/scripts/check-highlight-captures.sh new file mode 100755 index 0000000..af4267b --- /dev/null +++ b/scripts/check-highlight-captures.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash + +set -euo pipefail + +query_file="${1:-languages/roc/highlights.scm}" +supported='@attribute +@boolean +@comment +@comment.doc +@constant +@constant.builtin +@constructor +@embedded +@emphasis +@emphasis.strong +@enum +@function +@hint +@keyword +@label +@link_text +@link_uri +@number +@operator +@predictive +@primary +@property +@punctuation +@punctuation.bracket +@punctuation.delimiter +@punctuation.list_marker +@punctuation.special +@string +@string.escape +@string.regex +@string.special +@string.special.symbol +@tag +@tag.doctype +@text.literal +@title +@type +@type.builtin +@variable +@variable.parameter +@variable.special +@variant' + +invalid="$({ sed 's/;.*$//' "$query_file" \ + | rg -o '@[A-Za-z][A-Za-z0-9_.-]*' \ + | sort -u \ + | comm -23 - <(printf '%s\n' "$supported" | sort -u); } || true)" + +if [[ -n "$invalid" ]]; then + printf 'Unsupported Zed highlight captures in %s:\n%s\n' "$query_file" "$invalid" >&2 + exit 1 +fi + +printf 'All highlight captures in %s are supported by Zed.\n' "$query_file"