diff --git a/book/src/generated/lang-support.md b/book/src/generated/lang-support.md index 1447b66da1bf..448b7a240c74 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 2af1a054fccc..29f76cfb83ef 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 77098a33615b..062d379672f9 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 7cc3730034e4..c070caeed878 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 000000000000..7baea85a3876 --- /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 42ca12b5b8ab..506ca66e8517 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) +)