]> git.lizzy.rs Git - rust.git/blob - crates/ide_db/src/helpers/format_string.rs
Merge #11294
[rust.git] / crates / ide_db / src / helpers / format_string.rs
1 //! Tools to work with format string literals for the `format_args!` family of macros.
2 use syntax::{
3     ast::{self, IsString},
4     AstNode, AstToken, TextRange,
5 };
6
7 pub fn is_format_string(string: &ast::String) -> bool {
8     // Check if `string` is a format string argument of a macro invocation.
9     // `string` is a string literal, mapped down into the innermost macro expansion.
10     // Since `format_args!` etc. remove the format string when expanding, but place all arguments
11     // in the expanded output, we know that the string token is (part of) the format string if it
12     // appears in `format_args!` (otherwise it would have been mapped down further).
13     //
14     // This setup lets us correctly highlight the components of `concat!("{}", "bla")` format
15     // strings. It still fails for `concat!("{", "}")`, but that is rare.
16     format!("{string} {bar}", bar = string);
17     (|| {
18         let macro_call = string.syntax().ancestors().find_map(ast::MacroCall::cast)?;
19         let name = macro_call.path()?.segment()?.name_ref()?;
20
21         if !matches!(
22             name.text().as_str(),
23             "format_args" | "format_args_nl" | "const_format_args" | "panic_2015" | "panic_2021"
24         ) {
25             return None;
26         }
27
28         // NB: we match against `panic_2015`/`panic_2021` here because they have a special-cased arm for
29         // `"{}"`, which otherwise wouldn't get highlighted.
30
31         Some(())
32     })()
33     .is_some()
34 }
35
36 #[derive(Debug)]
37 pub enum FormatSpecifier {
38     Open,
39     Close,
40     Integer,
41     Identifier,
42     Colon,
43     Fill,
44     Align,
45     Sign,
46     NumberSign,
47     Zero,
48     DollarSign,
49     Dot,
50     Asterisk,
51     QuestionMark,
52 }
53
54 pub fn lex_format_specifiers(
55     string: &ast::String,
56     mut callback: &mut dyn FnMut(TextRange, FormatSpecifier),
57 ) {
58     let mut char_ranges = Vec::new();
59     string.escaped_char_ranges(&mut |range, res| char_ranges.push((range, res)));
60     let mut chars = char_ranges
61         .iter()
62         .filter_map(|(range, res)| Some((*range, *res.as_ref().ok()?)))
63         .peekable();
64
65     while let Some((range, first_char)) = chars.next() {
66         if let '{' = first_char {
67             // Format specifier, see syntax at https://doc.rust-lang.org/std/fmt/index.html#syntax
68             if let Some((_, '{')) = chars.peek() {
69                 // Escaped format specifier, `{{`
70                 chars.next();
71                 continue;
72             }
73
74             callback(range, FormatSpecifier::Open);
75
76             // check for integer/identifier
77             let (_, int_char) = chars.peek().copied().unwrap_or_default();
78             match int_char {
79                 // integer
80                 '0'..='9' => read_integer(&mut chars, &mut callback),
81                 // identifier
82                 c if c == '_' || c.is_alphabetic() => read_identifier(&mut chars, &mut callback),
83                 _ => {}
84             }
85
86             if let Some((_, ':')) = chars.peek() {
87                 skip_char_and_emit(&mut chars, FormatSpecifier::Colon, &mut callback);
88
89                 // check for fill/align
90                 let mut cloned = chars.clone().take(2);
91                 let (_, first) = cloned.next().unwrap_or_default();
92                 let (_, second) = cloned.next().unwrap_or_default();
93                 match second {
94                     '<' | '^' | '>' => {
95                         // alignment specifier, first char specifies fillment
96                         skip_char_and_emit(&mut chars, FormatSpecifier::Fill, &mut callback);
97                         skip_char_and_emit(&mut chars, FormatSpecifier::Align, &mut callback);
98                     }
99                     _ => {
100                         if let '<' | '^' | '>' = first {
101                             skip_char_and_emit(&mut chars, FormatSpecifier::Align, &mut callback);
102                         }
103                     }
104                 }
105
106                 // check for sign
107                 match chars.peek().copied().unwrap_or_default().1 {
108                     '+' | '-' => {
109                         skip_char_and_emit(&mut chars, FormatSpecifier::Sign, &mut callback);
110                     }
111                     _ => {}
112                 }
113
114                 // check for `#`
115                 if let Some((_, '#')) = chars.peek() {
116                     skip_char_and_emit(&mut chars, FormatSpecifier::NumberSign, &mut callback);
117                 }
118
119                 // check for `0`
120                 let mut cloned = chars.clone().take(2);
121                 let first = cloned.next().map(|next| next.1);
122                 let second = cloned.next().map(|next| next.1);
123
124                 if first == Some('0') && second != Some('$') {
125                     skip_char_and_emit(&mut chars, FormatSpecifier::Zero, &mut callback);
126                 }
127
128                 // width
129                 match chars.peek().copied().unwrap_or_default().1 {
130                     '0'..='9' => {
131                         read_integer(&mut chars, &mut callback);
132                         if let Some((_, '$')) = chars.peek() {
133                             skip_char_and_emit(
134                                 &mut chars,
135                                 FormatSpecifier::DollarSign,
136                                 &mut callback,
137                             );
138                         }
139                     }
140                     c if c == '_' || c.is_alphabetic() => {
141                         read_identifier(&mut chars, &mut callback);
142
143                         if chars.peek().map(|&(_, c)| c) == Some('?') {
144                             skip_char_and_emit(
145                                 &mut chars,
146                                 FormatSpecifier::QuestionMark,
147                                 &mut callback,
148                             );
149                         }
150
151                         // can be either width (indicated by dollar sign, or type in which case
152                         // the next sign has to be `}`)
153                         let next = chars.peek().map(|&(_, c)| c);
154
155                         match next {
156                             Some('$') => skip_char_and_emit(
157                                 &mut chars,
158                                 FormatSpecifier::DollarSign,
159                                 &mut callback,
160                             ),
161                             Some('}') => {
162                                 skip_char_and_emit(
163                                     &mut chars,
164                                     FormatSpecifier::Close,
165                                     &mut callback,
166                                 );
167                                 continue;
168                             }
169                             _ => continue,
170                         };
171                     }
172                     _ => {}
173                 }
174
175                 // precision
176                 if let Some((_, '.')) = chars.peek() {
177                     skip_char_and_emit(&mut chars, FormatSpecifier::Dot, &mut callback);
178
179                     match chars.peek().copied().unwrap_or_default().1 {
180                         '*' => {
181                             skip_char_and_emit(
182                                 &mut chars,
183                                 FormatSpecifier::Asterisk,
184                                 &mut callback,
185                             );
186                         }
187                         '0'..='9' => {
188                             read_integer(&mut chars, &mut callback);
189                             if let Some((_, '$')) = chars.peek() {
190                                 skip_char_and_emit(
191                                     &mut chars,
192                                     FormatSpecifier::DollarSign,
193                                     &mut callback,
194                                 );
195                             }
196                         }
197                         c if c == '_' || c.is_alphabetic() => {
198                             read_identifier(&mut chars, &mut callback);
199                             if chars.peek().map(|&(_, c)| c) != Some('$') {
200                                 continue;
201                             }
202                             skip_char_and_emit(
203                                 &mut chars,
204                                 FormatSpecifier::DollarSign,
205                                 &mut callback,
206                             );
207                         }
208                         _ => {
209                             continue;
210                         }
211                     }
212                 }
213
214                 // type
215                 match chars.peek().copied().unwrap_or_default().1 {
216                     '?' => {
217                         skip_char_and_emit(
218                             &mut chars,
219                             FormatSpecifier::QuestionMark,
220                             &mut callback,
221                         );
222                     }
223                     c if c == '_' || c.is_alphabetic() => {
224                         read_identifier(&mut chars, &mut callback);
225
226                         if chars.peek().map(|&(_, c)| c) == Some('?') {
227                             skip_char_and_emit(
228                                 &mut chars,
229                                 FormatSpecifier::QuestionMark,
230                                 &mut callback,
231                             );
232                         }
233                     }
234                     _ => {}
235                 }
236             }
237
238             if let Some((_, '}')) = chars.peek() {
239                 skip_char_and_emit(&mut chars, FormatSpecifier::Close, &mut callback);
240             }
241             continue;
242         }
243     }
244
245     fn skip_char_and_emit<I, F>(
246         chars: &mut std::iter::Peekable<I>,
247         emit: FormatSpecifier,
248         callback: &mut F,
249     ) where
250         I: Iterator<Item = (TextRange, char)>,
251         F: FnMut(TextRange, FormatSpecifier),
252     {
253         let (range, _) = chars.next().unwrap();
254         callback(range, emit);
255     }
256
257     fn read_integer<I, F>(chars: &mut std::iter::Peekable<I>, callback: &mut F)
258     where
259         I: Iterator<Item = (TextRange, char)>,
260         F: FnMut(TextRange, FormatSpecifier),
261     {
262         let (mut range, c) = chars.next().unwrap();
263         assert!(c.is_ascii_digit());
264         while let Some(&(r, next_char)) = chars.peek() {
265             if next_char.is_ascii_digit() {
266                 chars.next();
267                 range = range.cover(r);
268             } else {
269                 break;
270             }
271         }
272         callback(range, FormatSpecifier::Integer);
273     }
274
275     fn read_identifier<I, F>(chars: &mut std::iter::Peekable<I>, callback: &mut F)
276     where
277         I: Iterator<Item = (TextRange, char)>,
278         F: FnMut(TextRange, FormatSpecifier),
279     {
280         let (mut range, c) = chars.next().unwrap();
281         assert!(c.is_alphabetic() || c == '_');
282         while let Some(&(r, next_char)) = chars.peek() {
283             if next_char == '_' || next_char.is_ascii_digit() || next_char.is_alphabetic() {
284                 chars.next();
285                 range = range.cover(r);
286             } else {
287                 break;
288             }
289         }
290         callback(range, FormatSpecifier::Identifier);
291     }
292 }