diff --git a/README.md b/README.md index cd707298..b8854b69 100644 --- a/README.md +++ b/README.md @@ -125,8 +125,8 @@ target/release/rcl fmt examples/tags.rcl RCL is usable and useful, well-tested, and well-documented. It is still pre-1.0, though backwards-incompatible changes have been rare in the past years. Syntax -highlighting is available for major editors like Vim, Emacs, Helix, and Zed. -RCL is a community project without commercial support. +highlighting is available for major editors like Vim, Emacs, Helix, VSCode, and +Zed. RCL is a community project without commercial support. ## Support RCL diff --git a/build.rcl b/build.rcl index f54b34c7..bb765760 100644 --- a/build.rcl +++ b/build.rcl @@ -17,12 +17,28 @@ let target_toml = contents => { contents = contents, }; +let target_json = contents => { + format = "json", + contents = contents, +}; + { "Cargo.toml": target_toml(import "//Cargo.rcl"), "fuzz/Cargo.toml": target_toml(import "//fuzz/Cargo.rcl"), "grammar/tree-sitter-rcl/Cargo.toml": target_toml( import "//grammar/tree-sitter-rcl/Cargo.rcl", ), + "grammar/vscode/language-configuration.json": target_json( + import "//grammar/vscode/language-configuration.rcl", + ), + // TODO: Maybe instead of checking these generated json files + // in to the repository, we can just generate them at package build time. + "grammar/vscode/package.json": target_json( + import "//grammar/vscode/package.rcl", + ), + "grammar/vscode/rcl.tmLanguage.json": target_json( + import "//grammar/vscode/rcl.tmLanguage.rcl", + ), "grammar/zed/extension.toml": target_toml( import "//grammar/zed/extension.rcl", ), diff --git a/docs/grammars.md b/docs/grammars.md index bd8ada32..5f3cc3e5 100644 --- a/docs/grammars.md +++ b/docs/grammars.md @@ -42,6 +42,26 @@ to use for highlighting, similar to scopes for Tree Sitter. [vim-groups]: https://vimhelp.org/syntax.txt.html#group-name +## Visual Studio Code + +The `vscode` directory contains the Visual Studio Code plugin including TextMate +grammar. The json version of the grammar is generated with RCL. The +TextMate documentation has [a section on scope naming conventions][tm-scopes]. +Some helpful links: + + * [TextMate language grammar documentation][tm-docs] + * [Helpful _lessons learned_ blog post][tm-lessons] + * [VSCode syntax highlighting docs][vscode-highlight] + +[tm-docs]: https://macromates.com/manual/en/language_grammars +[tm-scopes]: https://macromates.com/manual/en/language_grammars#naming_conventions +[tm-lessons]: https://www.apeth.com/nonblog/stories/textmatebundle.html +[vscode-highlight]: https://code.visualstudio.com/api/language-extensions/syntax-highlight-guide + +The Nix flake includes a VSIX extension package. To build it: + + nix build .#vscode-extension + ## Zed The `zed` directory contains the Zed plugin. This directory is the source of diff --git a/docs/index.md b/docs/index.md index 876facb4..f4fc3d9a 100644 --- a/docs/index.md +++ b/docs/index.md @@ -53,8 +53,8 @@ For an interactive demo in your browser, see . RCL is usable and useful, well-tested, and well-documented. It is still pre-1.0, though backwards-incompatible changes have been rare in the past years. Syntax -highlighting is available for major editors like Vim, Emacs, Helix, and Zed. -RCL is a community project without commercial support. +highlighting is available for major editors like Vim, Emacs, Helix, VSCode, and +Zed. RCL is a community project without commercial support. ## License diff --git a/docs/syntax_highlighting.md b/docs/syntax_highlighting.md index 15e14dbc..138a6877 100644 --- a/docs/syntax_highlighting.md +++ b/docs/syntax_highlighting.md @@ -91,6 +91,19 @@ The directory `grammar/rcl.vim` contains support for highlighting in Vim. You can symlink the contents into your `~/.vim`, or use a plugin manager like Pathogen and symlink the directory into `~/.vim/bundle`. +## Visual Studio Code + +The [Visual Studio Code extension][vscode-ext] is available from the Visual +Studio marketplace. + +If you want to install a development version of the extension, the extension +is developed in the main repository. See [the section in the grammars +chapter][grammars-vscode] for how to build it, then install the extension using +_Extensions: Install from VSIX…_ from the command panel. + +[vscode-ext]: https://marketplace.visualstudio.com/items?itemName=rcl-lang.rcl +[grammars-vscode]: grammars.md#visual-studio-code + ## Zed The [Zed extension](https://github.com/rcl-lang/zed-rcl) is available from the diff --git a/flake.nix b/flake.nix index e0bf1d9c..00046c6c 100644 --- a/flake.nix +++ b/flake.nix @@ -158,7 +158,7 @@ pythonSources = pkgs.lib.sourceFilesBySuffices ./. [ ".py" ".pyi" ]; - rclTomlSources = pkgs.lib.sourceFilesBySuffices ./. [ ".rcl" ".toml" ]; + rclGeneratedSources = pkgs.lib.sourceFilesBySuffices ./. [ ".rcl" ".toml" ".json" ]; goldenSources = ./golden; @@ -412,6 +412,29 @@ RUSTFLAGS = "-C instrument-coverage -C link-dead-code -C debug-assertions"; }); + vscode-extension = pkgs.stdenv.mkDerivation { + pname = "rcl-vscode"; + inherit version; + src = ./grammar/vscode; + nativeBuildInputs = [ pkgs.vsce ]; + doCheck = false; + buildPhase = + '' + # We want only the json files, not the RCL sources. Also, the + # `vsce` tool complains if there is no LICENSE file, so copy it + # in. + rm *.rcl + cp ${./LICENSE} LICENSE + + # TODO: The VSIX file is just a zip file of the directory, with + # two additional XML files in it. One of them may be kind of a + # pain to generate, but on the other hand, we could skip nodejs + # if we build the zip file ourselves. + mkdir -p $out + vsce package --no-dependencies --out $out/rcl-${version}.vsix + ''; + }; + in rec { devShells.default = pkgs.mkShell { @@ -503,14 +526,14 @@ "check-fmt-rcl" { buildInputs = [ debugBuild ]; } '' - rcl format --check ${rclTomlSources}/**.rcl | tee $out + rcl format --check ${rclGeneratedSources}/**.rcl | tee $out ''; buildRcl = pkgs.runCommand "check-rcl-build" { buildInputs = [ debugBuild ]; } '' - rcl build --check --directory ${rclTomlSources} | tee $out + rcl build --check --directory ${rclGeneratedSources} | tee $out ''; # Build documentation with warnings denied, so the check fails if @@ -545,7 +568,7 @@ }; packages = { - inherit fuzzers-coverage rcl pyrcl pyrcl-wheel treeSitterRcl website; + inherit fuzzers-coverage rcl pyrcl pyrcl-wheel treeSitterRcl vscode-extension website; default = rcl; binaries = rcl-binaries; @@ -561,7 +584,7 @@ RCL_BIN=${coverageBuild}/bin/rcl python3 ${goldenSources}/run.py # Also run `rcl build` to make sure we cover that part of the application. - ${coverageBuild}/bin/rcl build --check --directory ${rclTomlSources} + ${coverageBuild}/bin/rcl build --check --directory ${rclGeneratedSources} # Copy in the .profraw files from the tests. cp ${coverageBuild}/prof/*.profraw . diff --git a/grammar/vscode/CHANGELOG.md b/grammar/vscode/CHANGELOG.md new file mode 100644 index 00000000..d7c108bb --- /dev/null +++ b/grammar/vscode/CHANGELOG.md @@ -0,0 +1,2 @@ +Changes to the VSCode extension are listed as part of +[the regular RCL changelog](https://docs.ruuda.nl/rcl/changelog/). diff --git a/grammar/vscode/README.md b/grammar/vscode/README.md new file mode 100644 index 00000000..c4abfe01 --- /dev/null +++ b/grammar/vscode/README.md @@ -0,0 +1,26 @@ +# The RCL Configuration Language + +This extension provides support for the [RCL configuration language][rcl-lang]. +The RCL configuration language is a domain-specific language that extends json +into a simple, gradually typed, functional language that resembles Python and +Nix. It reduces configuration boilerplate by enabling abstraction and reuse. + + * [Website and online playground][rcl-lang] + * [General documentation][docs] + +[rcl-lang]: https://rcl-lang.org/ +[docs]: https://docs.ruuda.nl/rcl/ + +# About + +This extension is official. It is developed upstream as part of the main +repository, in the `grammar/vscode` directory. It is available from the +following mirrors: + + * + * + +For hacking on the extension itself, see the [Grammars][docs-grammars] chapter +of the documentation. + +[docs-grammars]: https://docs.ruuda.nl/rcl/grammars/#visual-studio-code diff --git a/grammar/vscode/language-configuration.json b/grammar/vscode/language-configuration.json new file mode 100644 index 00000000..aba4669e --- /dev/null +++ b/grammar/vscode/language-configuration.json @@ -0,0 +1,12 @@ +{ + "autoCloseBefore": ":.,", + "autoClosingPairs": [ + {"close": "}", "open": "{"}, + {"close": "]", "open": "["}, + {"close": ")", "open": "("}, + {"close": "\"", "notIn": ["string"], "open": "\""} + ], + "brackets": [["{", "}"], ["[", "]"], ["(", ")"]], + "comments": {"lineComment": "//"}, + "surroundingPairs": [["\"", "\""], ["(", ")"], ["[", "]"], ["{", "}"]] +} diff --git a/grammar/vscode/language-configuration.rcl b/grammar/vscode/language-configuration.rcl new file mode 100644 index 00000000..4b6e66eb --- /dev/null +++ b/grammar/vscode/language-configuration.rcl @@ -0,0 +1,29 @@ +// RCL -- A reasonable configuration language. +// Copyright 2025 Ruud van Asseldonk + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// A copy of the License has been included in the root of the repository. + +let brackets = [ + ["{", "}"], + ["[", "]"], + ["(", ")"], +]; + +{ + comments = { lineComment = "//" }, + brackets = brackets, + autoClosingPairs = [ + for b in brackets: { open = b[0], close = b[1] }, + { open = "\"", close = "\"", notIn = ["string"] }, + ], + autoCloseBefore = ":.,", + surroundingPairs = { + for b in brackets: b, + ["\"", "\""], + }, + // TODO: Add `folding`? + // TODO: Add `wordPattern`? + // TODO: Add `indentationRules`? I can't make sense of the example regex. +} diff --git a/grammar/vscode/package.json b/grammar/vscode/package.json new file mode 100644 index 00000000..b56cdcda --- /dev/null +++ b/grammar/vscode/package.json @@ -0,0 +1,34 @@ +{ + "author": {"name": "Ruud van Asseldonk"}, + "categories": ["Programming Languages"], + "contributes": { + "configurationDefaults": { + "[rcl]": {"editor.insertSpaces": true, "editor.tabSize": 2} + }, + "grammars": [ + { + "language": "rcl", + "path": "./rcl.tmLanguage.json", + "scopeName": "source.rcl" + } + ], + "languages": [ + { + "aliases": ["RCL"], + "configuration": "./language-configuration.json", + "extensions": [".rcl"], + "id": "rcl" + } + ] + }, + "description": "Support for the RCL configuration language.", + "displayName": "RCL", + "engines": {"vscode": "^1.100.0"}, + "homepage": "https://rcl-lang.org", + "license": "Apache-2.0", + "name": "rcl", + "preview": true, + "publisher": "rcl-lang", + "repository": {"type": "git", "url": "https://github.com/ruuda/rcl.git"}, + "version": "0.13.0" +} diff --git a/grammar/vscode/package.rcl b/grammar/vscode/package.rcl new file mode 100644 index 00000000..5d4e5960 --- /dev/null +++ b/grammar/vscode/package.rcl @@ -0,0 +1,43 @@ +let root = import "//Cargo.rcl"; + +{ + name = "rcl", + displayName = "RCL", + description = "Support for the RCL configuration language.", + // TODO: Remove the preview designation once the extension is ready. + preview = true, + version = root.package.version, + license = root.package.license, + author = { name = "Ruud van Asseldonk" }, + publisher = "rcl-lang", + + engines = { vscode = "^1.100.0" }, + categories = ["Programming Languages"], + + repository = { type = "git", url = "https://github.com/ruuda/rcl.git" }, + homepage = "https://rcl-lang.org", + + contributes = { + languages = [ + { + id = "rcl", + aliases = ["RCL"], + extensions = [".rcl"], + configuration = "./language-configuration.json", + }, + ], + grammars = [ + { + language = "rcl", + scopeName = "source.rcl", + path = "./rcl.tmLanguage.json", + }, + ], + configurationDefaults = { + "[rcl]": { + "editor.tabSize": 2, + "editor.insertSpaces": true, + }, + }, + }, +} diff --git a/grammar/vscode/rcl.tmLanguage.json b/grammar/vscode/rcl.tmLanguage.json new file mode 100644 index 00000000..b147b416 --- /dev/null +++ b/grammar/vscode/rcl.tmLanguage.json @@ -0,0 +1,254 @@ +{ + "patterns": [{"include": "#expr"}], + "repository": { + "comment": {"match": "//.*$", "name": "comment.line.double-slash.rcl"}, + "expr": { + "patterns": [ + {"include": "#stmt"}, + {"include": "#expr_if"}, + {"include": "#expr_op"}, + {"include": "#comment"} + ] + }, + "expr_binop": { + "match": "\\b(and|or)\\b", + "name": "keyword.operator.binop.rcl" + }, + "expr_if": { + "begin": "\\bif\\b", + "beginCaptures": {"0": {"name": "keyword.control.if.rcl"}}, + "end": "\\belse\\b", + "endCaptures": {"0": {"name": "keyword.control.else.rcl"}}, + "name": "meta.structure.if.rcl", + "patterns": [{"include": "#expr"}] + }, + "expr_import": {"match": "\\bimport\\b", "name": "keyword.other.rcl"}, + "expr_not_op": {"patterns": [{"include": "#expr_term"}]}, + "expr_op": { + "patterns": [ + {"include": "#expr_import"}, + {"include": "#expr_unop"}, + {"include": "#expr_binop"}, + {"include": "#expr_not_op"} + ] + }, + "expr_term": { + "patterns": [ + {"include": "#term_brace"}, + {"include": "#term_bracket"}, + {"include": "#term_parens"}, + {"include": "#term_string"}, + {"include": "#term_number"}, + {"include": "#term_constant"}, + {"match": "[a-zA-Z_][a-zA-Z0-9_-]*", "name": "meta.structure.ident.rcl"} + ] + }, + "expr_unop": {"match": "\\bnot\\b", "name": "keyword.operator.unop.rcl"}, + "seq": { + "name": "meta.structure.seq.rcl", + "patterns": [ + {"include": "#stmt"}, + {"include": "#seq_for"}, + {"include": "#seq_if"}, + {"include": "#seq_kv"}, + {"include": "#expr_op"}, + {"include": "#comment"} + ] + }, + "seq_for": { + "begin": "\\bfor\\b", + "beginCaptures": {"0": {"name": "keyword.control.for.rcl"}}, + "end": ":", + "patterns": [ + { + "begin": "(?<=\\bfor\\b)", + "end": "\\bin\\b", + "endCaptures": {"0": {"name": "keyword.control.in.rcl"}}, + "patterns": [ + {"match": "[a-zA-Z_][a-zA-Z0-9_-]*", "name": "meta.declaration.rcl"} + ] + }, + { + "begin": "(?<=\\bin\\b)", + "end": "(?=:)", + "patterns": [{"include": "#expr"}] + } + ] + }, + "seq_if": { + "begin": "\\bif\\b", + "beginCaptures": {"0": {"name": "keyword.control.if.rcl"}}, + "end": ":", + "patterns": [{"include": "#expr"}] + }, + "seq_kv": { + "captures": {"1": {"name": "support.type.property-name.json.rcl"}}, + "match": "\\b([a-zA-Z_][a-zA-Z0-9_-]*)\\s+=" + }, + "stmt": { + "patterns": [ + { + "begin": "\\blet\\b", + "beginCaptures": {"0": {"name": "keyword.other.let.rcl"}}, + "end": ";", + "name": "meta.structure.let.rcl", + "patterns": [ + { + "begin": "(?<=\\blet\\b)", + "end": "[:=]", + "patterns": [ + { + "match": "[a-zA-Z_][a-zA-Z0-9_-]*", + "name": "meta.declaration.rcl" + } + ] + }, + { + "begin": "(?<=:)", + "end": "=", + "name": "meta.structure.annotation.rcl", + "patterns": [ + { + "match": "[a-zA-Z_][a-zA-Z0-9_-]*", + "name": "entity.name.type.rcl" + } + ] + }, + { + "begin": "(?<=[=])", + "end": "(?=[;])", + "patterns": [{"include": "#expr"}] + } + ] + }, + { + "begin": "\\bassert\\b", + "beginCaptures": {"0": {"name": "keyword.control.assert.rcl"}}, + "end": ";", + "name": "meta.structure.assert.rcl", + "patterns": [{"include": "#expr"}] + }, + { + "begin": "\\btrace\\b", + "beginCaptures": {"0": {"name": "keyword.control.trace.rcl"}}, + "end": ";", + "name": "meta.structure.trace.rcl", + "patterns": [{"include": "#expr"}] + } + ] + }, + "string_hole": { + "begin": "\\{", + "end": "\\}", + "name": "string.interpolated.rcl", + "patterns": [{"include": "#expr"}] + }, + "term_brace": { + "begin": "\\{", + "end": "\\}", + "name": "meta.structure.brace.rcl", + "patterns": [{"include": "#seq"}] + }, + "term_bracket": { + "begin": "\\[", + "end": "\\]", + "name": "meta.structure.bracket.rcl", + "patterns": [{"include": "#seq"}] + }, + "term_constant": { + "match": "\\b(?:true|false|null)\\b", + "name": "constant.language.rcl" + }, + "term_number": { + "patterns": [ + { + "match": "\\b0x[0-9a-fA-F_]*\\b", + "name": "constant.numeric.hexadecimal.rcl" + }, + {"match": "\\b0b[01_]*\\b", "name": "constant.numeric.binary.rcl"}, + { + "match": "\\b[0-9][0-9_]*(?:\\.[0-9_]*)?(?:[eE][+-]?[0-9_]*)?", + "name": "constant.numeric.decimal.rcl" + } + ] + }, + "term_parens": { + "begin": "\\(", + "end": "\\)", + "name": "meta.structure.parens.rcl", + "patterns": [{"include": "#expr"}] + }, + "term_string": { + "name": "string.quoted.rcl", + "patterns": [ + { + "begin": "f\"\"\"", + "end": "\"\"\"", + "name": "string.quoted.triple.rcl", + "patterns": [ + { + "match": "\\\\u\\{[0-9a-fA-F_]+\\}", + "name": "constant.character.escape.unibrace.rcl" + }, + { + "match": "\\\\u[0-9a-fA-F_]{4}", + "name": "constant.character.escape.rcl" + }, + {"match": "\\\\.", "name": "constant.character.escape.single.rcl"}, + {"include": "#string_hole"} + ] + }, + { + "begin": "f\"", + "end": "\"", + "name": "string.quoted.double.rcl", + "patterns": [ + { + "match": "\\\\u\\{[0-9a-fA-F_]+\\}", + "name": "constant.character.escape.unibrace.rcl" + }, + { + "match": "\\\\u[0-9a-fA-F_]{4}", + "name": "constant.character.escape.rcl" + }, + {"match": "\\\\.", "name": "constant.character.escape.single.rcl"}, + {"include": "#string_hole"} + ] + }, + { + "begin": "\"\"\"", + "end": "\"\"\"", + "name": "string.quoted.triple.rcl", + "patterns": [ + { + "match": "\\\\u\\{[0-9a-fA-F_]+\\}", + "name": "constant.character.escape.unibrace.rcl" + }, + { + "match": "\\\\u[0-9a-fA-F_]{4}", + "name": "constant.character.escape.rcl" + }, + {"match": "\\\\.", "name": "constant.character.escape.single.rcl"} + ] + }, + { + "begin": "\"", + "end": "\"", + "name": "string.quoted.double.rcl", + "patterns": [ + { + "match": "\\\\u\\{[0-9a-fA-F_]+\\}", + "name": "constant.character.escape.unibrace.rcl" + }, + { + "match": "\\\\u[0-9a-fA-F_]{4}", + "name": "constant.character.escape.rcl" + }, + {"match": "\\\\.", "name": "constant.character.escape.single.rcl"} + ] + } + ] + } + }, + "scopeName": "source.rcl" +} diff --git a/grammar/vscode/rcl.tmLanguage.rcl b/grammar/vscode/rcl.tmLanguage.rcl new file mode 100644 index 00000000..e6f165f0 --- /dev/null +++ b/grammar/vscode/rcl.tmLanguage.rcl @@ -0,0 +1,331 @@ +// RCL -- A reasonable configuration language. +// Copyright 2025 Ruud van Asseldonk + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// A copy of the License has been included in the root of the repository. + +// This TextMate grammar is structured to reflect the Bison grammar in +// grammar.y, with the rules in the repository here matching the rules in the +// Bison grammar by name and structure where possible. Note that TextMate rules +// are not production rules though! They indicate nesting, but not sequencing, +// e.g. the expr_op and expr_binop rules here have no need to be distinct rules, +// they match the keywords anywhere, but we still keep them distinct in order to +// match the Bison grammar as closely as possible, to make it more obvious that +// they are in sync. +// +// There are links to helpful documentation in the grammars directory's readme. + +{ + scopeName = "source.rcl", + patterns = [ + { include = "#expr" }, + ], + + let identifier = "[a-zA-Z_][a-zA-Z0-9_-]*"; + + // In some places, like after `let` and `for`, we can match identifiers, but + // we don't assign them the `variable` scope. Some themes highlight it, but in + // my opinion it makes the document too cluttered when everything lights up. + let inner_ident = { match = f"{identifier}", name = "meta.declaration.rcl" }; + + let make_term_string = (name, begin, end, patterns) => { + name = name, + begin = begin, + end = end, + patterns = [ + { + name = "constant.character.escape.unibrace.rcl", + match = "\\\\u\\{[0-9a-fA-F_]+\\}", + }, + { + name = "constant.character.escape.rcl", + match = "\\\\u[0-9a-fA-F_]{4}", + }, + { + name = "constant.character.escape.single.rcl", + match = "\\\\.", + }, + for include_pattern in patterns: { include = include_pattern }, + ], + }; + + repository = { + term_bracket = { + name = "meta.structure.bracket.rcl", + begin = "\\[", + end = "\\]", + patterns = [{ include = "#seq" }], + }, + + term_brace = { + name = "meta.structure.brace.rcl", + begin = "\\{", + end = "\\}", + patterns = [{ include = "#seq" }], + }, + + term_parens = { + name = "meta.structure.parens.rcl", + begin = "\\(", + end = "\\)", + patterns = [{ include = "#expr" }], + }, + + string_hole = { + begin = "\\{", + end = "\\}", + name = "string.interpolated.rcl", + patterns = [{ include = "#expr" }], + }, + + term_string = { + name = "string.quoted.rcl", + patterns = [ + // The order of the patterns matters, we need to put the largest + // possible match first: f-strings go before normal strings, triple + // before double. + make_term_string( + "string.quoted.triple.rcl", + "f\"\"\"", + "\"\"\"", + ["#string_hole"], + ), + make_term_string( + "string.quoted.double.rcl", + "f\"", + "\"", + ["#string_hole"], + ), + make_term_string("string.quoted.triple.rcl", "\"\"\"", "\"\"\"", []), + make_term_string("string.quoted.double.rcl", "\"", "\"", []), + ], + }, + + term_constant = { + // ?: means a non-captured group. + match = "\\b(?:true|false|null)\\b", + name = "constant.language.rcl", + }, + + term_number = { + // The regex for number is more lax than what the RCL lexer/parser allow, + // such that writing something invalid does not break highlighting of the + // entire document. We do split out int with possible prefixes, such that + // e.g. `a` on its own is not considered a hex number. + patterns = [ + { + name = "constant.numeric.hexadecimal.rcl", + match = "\\b0x[0-9a-fA-F_]*\\b", + }, + { + name = "constant.numeric.binary.rcl", + match = "\\b0b[01_]*\\b", + }, + { + name = "constant.numeric.decimal.rcl", + // The structure of this regex is chosen such that a prefix of a + // number still matches, even when it's not valid for the RCL lexer. + // For example, `42.` and `4.2e` match. This ensures that while you + // are typing `42.0` or `4.2e1`, the number does not become + // un-highlighted as soon as you type the `.` or the `e`. There is no + // word boundary anchor at the end because it eats the `.` if there is + // one. + match = "\\b[0-9][0-9_]*(?:\\.[0-9_]*)?(?:[eE][+-]?[0-9_]*)?", + }, + ], + }, + + expr = { + patterns = [ + { include = "#stmt" }, + { include = "#expr_if" }, + { include = "#expr_op" }, + { include = "#comment" }, + ], + }, + + expr_if = { + begin = "\\bif\\b", + end = "\\belse\\b", + beginCaptures = { "0": { name = "keyword.control.if.rcl" } }, + endCaptures = { "0": { name = "keyword.control.else.rcl" } }, + name = "meta.structure.if.rcl", + patterns = [{ include = "#expr" }], + }, + + expr_import = { + match = "\\bimport\\b", + name = "keyword.other.rcl", + }, + + expr_op = { + patterns = [ + { include = "#expr_import" }, + { include = "#expr_unop" }, + { include = "#expr_binop" }, + { include = "#expr_not_op" }, + // In the grammar we also have function expressions, but we don't need + // to define anything explicitly here; we could match on the fat arrow + // but leaving the entire thing an `expr` scope works fine. + ], + }, + + expr_unop = { + match = "\\bnot\\b", + name = "keyword.operator.unop.rcl", + }, + + expr_binop = { + match = "\\b(and|or)\\b", + name = "keyword.operator.binop.rcl", + }, + + expr_not_op = { + patterns = [ + { include = "#expr_term" }, + // In the grammar we also have function calls, array indexing, and field + // access, but we don't need to explicitly define them here, we just + // leave the punctuation tokens unmatched. + ], + }, + + expr_term = { + patterns = [ + { include = "#term_brace" }, + { include = "#term_bracket" }, + { include = "#term_parens" }, + { include = "#term_string" }, + { include = "#term_number" }, + { include = "#term_constant" }, + { + // We don't need to highlight identifiers, so we don't really need a + // rule for it, but we include it for completeness and to allow other + // extensions to potentially do something with it. + name = "meta.structure.ident.rcl", + match = f"{identifier}", + }, + ], + }, + + stmt = { + patterns = [ + { + // The rule for a let statement is more complex than this, with the + // identifier and `=`, but we don't make them part of the `begin` + // match so that `let` can be highlighted immediately after you type + // it. Initially we had the rule run until the closing `;`, but that + // makes it difficult match the inner structure before the `=`; it's + // more productive to match only until `=`, and let the regular `expr` + // rule handle everything after it on both sides of the `;`. + begin = "\\blet\\b", + end = ";", + beginCaptures = { "0": { name = "keyword.other.let.rcl" } }, + name = "meta.structure.let.rcl", + patterns = [ + // The identifier part, the start of which we match with a + // lookbehind because it's already consumed by the outer rule. + { + begin = "(?<=\\blet\\b)", + end = "[:=]", + patterns = [inner_ident], + }, + // Optionally, a type annotation part, which we again match using + // a lookbehind. + { + name = "meta.structure.annotation.rcl", + begin = "(?<=:)", + end = "=", + patterns = [ + // We don't use `storage.type` because it's not really a type + // like that like it is in C. The Rust grammar uses this scope + // so we'll use it as well. + { match = f"{identifier}", name = "entity.name.type.rcl" }, + ], + }, + // Finally, the expression part, the end of which we need to match + // using a lookahead because it's again consumed by the outer rule. + { + begin = "(?<=[=])", + end = "(?=[;])", + patterns = [{ include = "#expr" }], + }, + ], + }, + { + // The structure is more complex than this, but we don't capture that + // in the grammar for highlighting. + begin = "\\bassert\\b", + end = ";", + beginCaptures = { "0": { name = "keyword.control.assert.rcl" } }, + name = "meta.structure.assert.rcl", + patterns = [{ include = "#expr" }], + }, + { + begin = "\\btrace\\b", + end = ";", + beginCaptures = { "0": { name = "keyword.control.trace.rcl" } }, + name = "meta.structure.trace.rcl", + patterns = [{ include = "#expr" }], + }, + ], + }, + + seq = { + name = "meta.structure.seq.rcl", + patterns = [ + { include = "#stmt" }, + { include = "#seq_for" }, + { include = "#seq_if" }, + { include = "#seq_kv" }, + { include = "#expr_op" }, + { include = "#comment" }, + ], + }, + + seq_for = { + begin = "\\bfor\\b", + end = ":", + beginCaptures = { "0": { name = "keyword.control.for.rcl" } }, + // For the two sections inside, we use lookbehind to match the start of + // the group, so we don't consume the match, because we can consume every + // match only once. + patterns = [ + { + begin = "(?<=\\bfor\\b)", + end = "\\bin\\b", + endCaptures = { "0": { name = "keyword.control.in.rcl" } }, + patterns = [inner_ident], + }, + { + begin = "(?<=\\bin\\b)", + end = "(?=:)", + patterns = [{ include = "#expr" }], + }, + ], + }, + + seq_if = { + begin = "\\bif\\b", + end = ":", + beginCaptures = { "0": { name = "keyword.control.if.rcl" } }, + patterns = [{ include = "#expr" }], + }, + + seq_kv = { + match = f"\\b({identifier})\\s+=", + captures = { + // This scope is the same one as used by the json grammar for keys. + // Some themes special-case json so let's include this to make it behave + // similarly. + "1": { name = "support.type.property-name.json.rcl" }, + }, + }, + + // Comment is a magic rule in TextMate grammars, it is allowed anywhere. + comment = { + name = "comment.line.double-slash.rcl", + match = "//.*$", + }, + }, +} diff --git a/website/index.html b/website/index.html index 5c0063bc..0b65e1d6 100644 --- a/website/index.html +++ b/website/index.html @@ -617,7 +617,7 @@

Status

RCL is usable and useful, well-tested, and well-documented. It is still pre-1.0, though backwards-incompatible changes have been rare in the past years. Syntax highlighting is available for major editors like Vim, Emacs, - Helix, and Zed. RCL is a community project without commercial support. + Helix, VSCode, and Zed. RCL is a community project without commercial support.