Skip to content

Commit 1023e8f

Browse files
authored
feat: highlight rust string interpolation macros that use format_args! (#13533)
Co-authored-by: Nik Revenco <[email protected]>
1 parent 223ceec commit 1023e8f

File tree

6 files changed

+171
-6
lines changed

6 files changed

+171
-6
lines changed

book/src/generated/lang-support.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@
199199
| rst || | | |
200200
| ruby |||| `ruby-lsp`, `solargraph` |
201201
| rust |||| `rust-analyzer` |
202+
| rust-format-args || | | |
202203
| sage ||| | |
203204
| scala |||| `metals` |
204205
| scheme || || |

helix-term/tests/test/commands.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -734,7 +734,7 @@ async fn surround_replace_ts() -> anyhow::Result<()> {
734734
const INPUT: &str = r#"\
735735
fn foo() {
736736
if let Some(_) = None {
737-
todo!("f#[|o]#o)");
737+
testing!("f#[|o]#o)");
738738
}
739739
}
740740
"#;
@@ -744,7 +744,7 @@ fn foo() {
744744
r#"\
745745
fn foo() {
746746
if let Some(_) = None {
747-
todo!('f#[|o]#o)');
747+
testing!('f#[|o]#o)');
748748
}
749749
}
750750
"#,
@@ -757,7 +757,7 @@ fn foo() {
757757
r#"\
758758
fn foo() {
759759
if let Some(_) = None [
760-
todo!("f#[|o]#o)");
760+
testing!("f#[|o]#o)");
761761
]
762762
}
763763
"#,
@@ -770,7 +770,7 @@ fn foo() {
770770
r#"\
771771
fn foo() {
772772
if let Some(_) = None {
773-
todo!{"f#[|o]#o)"};
773+
testing!{"f#[|o]#o)"};
774774
}
775775
}
776776
"#,

helix-term/tests/test/movement.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -379,9 +379,9 @@ async fn match_around_closest_ts() -> anyhow::Result<()> {
379379
test_with_config(
380380
AppBuilder::new().with_file("foo.rs", None),
381381
(
382-
r#"fn main() {todo!{"f#[|oo]#)"};}"#,
382+
r#"fn main() {testing!{"f#[|oo]#)"};}"#,
383383
"mam",
384-
r#"fn main() {todo!{#[|"foo)"]#};}"#,
384+
r#"fn main() {testing!{#[|"foo)"]#};}"#,
385385
),
386386
)
387387
.await?;

languages.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4360,3 +4360,13 @@ file-types = [ { glob = "dunst/dunstrc" } ]
43604360
[[grammar]]
43614361
name = "dunstrc"
43624362
source = { git = "https://github.com/rotmh/tree-sitter-dunstrc", rev = "9cb9d5cc51cf5e2a47bb2a0e2f2e519ff11c1431" }
4363+
4364+
[[language]]
4365+
name = "rust-format-args"
4366+
scope = "source.rust-format-args"
4367+
file-types = []
4368+
injection-regex = "rust-format-args"
4369+
4370+
[[grammar]]
4371+
name = "rust-format-args"
4372+
source = { git = "https://github.com/nik-rev/tree-sitter-rustfmt", rev = "2ca0bdd763d0c9dbb1d0bd14aea7544cbe81309c" }
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
; regular escapes like `\n` are detected using another grammar
2+
; Here, we only detect `{{` and `}}` as escapes for `{` and `}`
3+
(escaped) @constant.character.escape
4+
5+
[
6+
"#"
7+
(type)
8+
] @special
9+
10+
[
11+
(sign)
12+
(fill)
13+
(align)
14+
(width)
15+
] @operator
16+
17+
(number) @constant.numeric
18+
19+
(colon) @punctuation
20+
21+
(identifier) @variable
22+
23+
; SCREAMING_CASE is assumed to be constant
24+
((identifier) @constant
25+
(#match? @constant "^[A-Z_]+$"))
26+
27+
[
28+
"{"
29+
"}"
30+
] @punctuation.special

runtime/queries/rust/injections.scm

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,3 +97,127 @@
9797
]
9898
)
9999
(#set! injection.language "sql"))
100+
101+
; Special language `tree-sitter-rust-format-args` for Rust macros,
102+
; which use `format_args!` under the hood and therefore have
103+
; the `format_args!` syntax.
104+
;
105+
; This language is injected into a hard-coded set of macros.
106+
107+
; 1st argument is `format_args!`
108+
(
109+
(macro_invocation
110+
macro:
111+
[
112+
(scoped_identifier
113+
name: (_) @_macro_name)
114+
(identifier) @_macro_name
115+
]
116+
(token_tree
117+
. (string_literal
118+
(string_content) @injection.content
119+
)
120+
)
121+
)
122+
(#any-of? @_macro_name
123+
; std
124+
"print" "println" "eprint" "eprintln"
125+
"format" "format_args" "todo" "panic"
126+
"unreachable" "unimplemented" "compile_error"
127+
; log
128+
"crit" "trace" "debug" "info" "warn" "error"
129+
; anyhow
130+
"anyhow" "bail"
131+
; syn
132+
"format_ident"
133+
; indoc
134+
"formatdoc" "printdoc" "eprintdoc" "writedoc"
135+
; iced
136+
"text"
137+
; ratatui
138+
"span"
139+
; eyre
140+
"eyre"
141+
; miette
142+
"miette"
143+
)
144+
(#set! injection.language "rust-format-args")
145+
(#set! injection.include-children)
146+
)
147+
148+
; 2nd argument is `format_args!`
149+
(
150+
(macro_invocation
151+
macro:
152+
[
153+
(scoped_identifier
154+
name: (_) @_macro_name)
155+
(identifier) @_macro_name
156+
]
157+
(token_tree
158+
. (_)
159+
. (string_literal
160+
(string_content) @injection.content
161+
)
162+
)
163+
)
164+
(#any-of? @_macro_name
165+
; std
166+
"write" "writeln" "assert" "debug_assert"
167+
; defmt
168+
"expect" "unwrap"
169+
; ratatui
170+
"span"
171+
)
172+
(#set! injection.language "rust-format-args")
173+
(#set! injection.include-children)
174+
)
175+
176+
; 3rd argument is `format_args!`
177+
(
178+
(macro_invocation
179+
macro:
180+
[
181+
(scoped_identifier
182+
name: (_) @_macro_name)
183+
(identifier) @_macro_name
184+
]
185+
(token_tree
186+
. (_)
187+
. (_)
188+
. (string_literal
189+
(string_content) @injection.content
190+
)
191+
)
192+
)
193+
(#any-of? @_macro_name
194+
; std
195+
"assert_eq" "debug_assert_eq" "assert_ne" "debug_assert_ne"
196+
)
197+
(#set! injection.language "rust-format-args")
198+
(#set! injection.include-children)
199+
)
200+
201+
; Dioxus' "rsx!" macro relies heavily on string interpolation as well. The strings can be nested very deeply
202+
(
203+
(macro_invocation
204+
macro: [
205+
(scoped_identifier
206+
name: (_) @_macro_name)
207+
(identifier) @_macro_name
208+
]
209+
; TODO: This only captures 1 level of string literals. But in dioxus you can have
210+
; nested string literals. For instance:
211+
;
212+
; rsx! { "{hello} world" }:
213+
; -> (token_tree (string_literal))
214+
; rsx! { div { "{hello} world" } }
215+
; -> (token_tree (token_tree (string_literal)))
216+
; rsx! { div { div { "{hello} world" } } }
217+
; -> (token_tree (token_tree (token_tree (string_literal))))
218+
(token_tree (string_literal) @injection.content)
219+
)
220+
(#eq? @_macro_name "rsx")
221+
(#set! injection.language "rust-format-args")
222+
(#set! injection.include-children)
223+
)

0 commit comments

Comments
 (0)