Unnamed repository; edit this file 'description' to name the repository.
feat: highlight rust string interpolation macros that use `format_args!` (#13533)
Co-authored-by: Nik Revenco <[email protected]>
Nik Revenco 9 months ago
parent 223ceec · commit 1023e8f
-rw-r--r--book/src/generated/lang-support.md1
-rw-r--r--helix-term/tests/test/commands.rs8
-rw-r--r--helix-term/tests/test/movement.rs4
-rw-r--r--languages.toml10
-rw-r--r--runtime/queries/rust-format-args/highlights.scm30
-rw-r--r--runtime/queries/rust/injections.scm124
6 files changed, 171 insertions, 6 deletions
diff --git a/book/src/generated/lang-support.md b/book/src/generated/lang-support.md
index 1447b66d..448b7a24 100644
--- a/book/src/generated/lang-support.md
+++ b/book/src/generated/lang-support.md
@@ -199,6 +199,7 @@
| rst | ✓ | | | |
| ruby | ✓ | ✓ | ✓ | `ruby-lsp`, `solargraph` |
| rust | ✓ | ✓ | ✓ | `rust-analyzer` |
+| rust-format-args | ✓ | | | |
| sage | ✓ | ✓ | | |
| scala | ✓ | ✓ | ✓ | `metals` |
| scheme | ✓ | | ✓ | |
diff --git a/helix-term/tests/test/commands.rs b/helix-term/tests/test/commands.rs
index 2af1a054..29f76cfb 100644
--- a/helix-term/tests/test/commands.rs
+++ b/helix-term/tests/test/commands.rs
@@ -734,7 +734,7 @@ async fn surround_replace_ts() -> anyhow::Result<()> {
const INPUT: &str = r#"\
fn foo() {
if let Some(_) = None {
- todo!("f#[|o]#o)");
+ testing!("f#[|o]#o)");
}
}
"#;
@@ -744,7 +744,7 @@ fn foo() {
r#"\
fn foo() {
if let Some(_) = None {
- todo!('f#[|o]#o)');
+ testing!('f#[|o]#o)');
}
}
"#,
@@ -757,7 +757,7 @@ fn foo() {
r#"\
fn foo() {
if let Some(_) = None [
- todo!("f#[|o]#o)");
+ testing!("f#[|o]#o)");
]
}
"#,
@@ -770,7 +770,7 @@ fn foo() {
r#"\
fn foo() {
if let Some(_) = None {
- todo!{"f#[|o]#o)"};
+ testing!{"f#[|o]#o)"};
}
}
"#,
diff --git a/helix-term/tests/test/movement.rs b/helix-term/tests/test/movement.rs
index 77098a33..062d3796 100644
--- a/helix-term/tests/test/movement.rs
+++ b/helix-term/tests/test/movement.rs
@@ -379,9 +379,9 @@ async fn match_around_closest_ts() -> anyhow::Result<()> {
test_with_config(
AppBuilder::new().with_file("foo.rs", None),
(
- r#"fn main() {todo!{"f#[|oo]#)"};}"#,
+ r#"fn main() {testing!{"f#[|oo]#)"};}"#,
"mam",
- r#"fn main() {todo!{#[|"foo)"]#};}"#,
+ r#"fn main() {testing!{#[|"foo)"]#};}"#,
),
)
.await?;
diff --git a/languages.toml b/languages.toml
index 1f32ad30..b6ee6e9e 100644
--- a/languages.toml
+++ b/languages.toml
@@ -4360,3 +4360,13 @@ file-types = [ { glob = "dunst/dunstrc" } ]
[[grammar]]
name = "dunstrc"
source = { git = "https://github.com/rotmh/tree-sitter-dunstrc", rev = "9cb9d5cc51cf5e2a47bb2a0e2f2e519ff11c1431" }
+
+[[language]]
+name = "rust-format-args"
+scope = "source.rust-format-args"
+file-types = []
+injection-regex = "rust-format-args"
+
+[[grammar]]
+name = "rust-format-args"
+source = { git = "https://github.com/nik-rev/tree-sitter-rustfmt", rev = "2ca0bdd763d0c9dbb1d0bd14aea7544cbe81309c" }
diff --git a/runtime/queries/rust-format-args/highlights.scm b/runtime/queries/rust-format-args/highlights.scm
new file mode 100644
index 00000000..7baea85a
--- /dev/null
+++ b/runtime/queries/rust-format-args/highlights.scm
@@ -0,0 +1,30 @@
+; regular escapes like `\n` are detected using another grammar
+; Here, we only detect `{{` and `}}` as escapes for `{` and `}`
+(escaped) @constant.character.escape
+
+[
+ "#"
+ (type)
+] @special
+
+[
+ (sign)
+ (fill)
+ (align)
+ (width)
+] @operator
+
+(number) @constant.numeric
+
+(colon) @punctuation
+
+(identifier) @variable
+
+; SCREAMING_CASE is assumed to be constant
+((identifier) @constant
+ (#match? @constant "^[A-Z_]+$"))
+
+[
+ "{"
+ "}"
+] @punctuation.special
diff --git a/runtime/queries/rust/injections.scm b/runtime/queries/rust/injections.scm
index 1f83c6e7..c895b4ef 100644
--- a/runtime/queries/rust/injections.scm
+++ b/runtime/queries/rust/injections.scm
@@ -97,3 +97,127 @@
]
)
(#set! injection.language "sql"))
+
+; Special language `tree-sitter-rust-format-args` for Rust macros,
+; which use `format_args!` under the hood and therefore have
+; the `format_args!` syntax.
+;
+; This language is injected into a hard-coded set of macros.
+
+; 1st argument is `format_args!`
+(
+ (macro_invocation
+ macro:
+ [
+ (scoped_identifier
+ name: (_) @_macro_name)
+ (identifier) @_macro_name
+ ]
+ (token_tree
+ . (string_literal
+ (string_content) @injection.content
+ )
+ )
+ )
+ (#any-of? @_macro_name
+ ; std
+ "print" "println" "eprint" "eprintln"
+ "format" "format_args" "todo" "panic"
+ "unreachable" "unimplemented" "compile_error"
+ ; log
+ "crit" "trace" "debug" "info" "warn" "error"
+ ; anyhow
+ "anyhow" "bail"
+ ; syn
+ "format_ident"
+ ; indoc
+ "formatdoc" "printdoc" "eprintdoc" "writedoc"
+ ; iced
+ "text"
+ ; ratatui
+ "span"
+ ; eyre
+ "eyre"
+ ; miette
+ "miette"
+ )
+ (#set! injection.language "rust-format-args")
+ (#set! injection.include-children)
+)
+
+; 2nd argument is `format_args!`
+(
+ (macro_invocation
+ macro:
+ [
+ (scoped_identifier
+ name: (_) @_macro_name)
+ (identifier) @_macro_name
+ ]
+ (token_tree
+ . (_)
+ . (string_literal
+ (string_content) @injection.content
+ )
+ )
+ )
+ (#any-of? @_macro_name
+ ; std
+ "write" "writeln" "assert" "debug_assert"
+ ; defmt
+ "expect" "unwrap"
+ ; ratatui
+ "span"
+ )
+ (#set! injection.language "rust-format-args")
+ (#set! injection.include-children)
+)
+
+; 3rd argument is `format_args!`
+(
+ (macro_invocation
+ macro:
+ [
+ (scoped_identifier
+ name: (_) @_macro_name)
+ (identifier) @_macro_name
+ ]
+ (token_tree
+ . (_)
+ . (_)
+ . (string_literal
+ (string_content) @injection.content
+ )
+ )
+ )
+ (#any-of? @_macro_name
+ ; std
+ "assert_eq" "debug_assert_eq" "assert_ne" "debug_assert_ne"
+ )
+ (#set! injection.language "rust-format-args")
+ (#set! injection.include-children)
+)
+
+; Dioxus' "rsx!" macro relies heavily on string interpolation as well. The strings can be nested very deeply
+(
+ (macro_invocation
+ macro: [
+ (scoped_identifier
+ name: (_) @_macro_name)
+ (identifier) @_macro_name
+ ]
+ ; TODO: This only captures 1 level of string literals. But in dioxus you can have
+ ; nested string literals. For instance:
+ ;
+ ; rsx! { "{hello} world" }:
+ ; -> (token_tree (string_literal))
+ ; rsx! { div { "{hello} world" } }
+ ; -> (token_tree (token_tree (string_literal)))
+ ; rsx! { div { div { "{hello} world" } } }
+ ; -> (token_tree (token_tree (token_tree (string_literal))))
+ (token_tree (string_literal) @injection.content)
+ )
+ (#eq? @_macro_name "rsx")
+ (#set! injection.language "rust-format-args")
+ (#set! injection.include-children)
+)