]> git.lizzy.rs Git - rust.git/blob - src/tools/rust-analyzer/crates/ide-assists/src/handlers/move_format_string_arg.rs
Auto merge of #99099 - Stargateur:phantomdata_debug, r=joshtriplett
[rust.git] / src / tools / rust-analyzer / crates / ide-assists / src / handlers / move_format_string_arg.rs
1 use crate::{AssistContext, Assists};
2 use ide_db::{
3     assists::{AssistId, AssistKind},
4     syntax_helpers::{
5         format_string::is_format_string,
6         format_string_exprs::{parse_format_exprs, Arg},
7     },
8 };
9 use itertools::Itertools;
10 use syntax::{ast, AstNode, AstToken, NodeOrToken, SyntaxKind::COMMA, TextRange};
11
12 // Assist: move_format_string_arg
13 //
14 // Move an expression out of a format string.
15 //
16 // ```
17 // macro_rules! format_args {
18 //     ($lit:literal $(tt:tt)*) => { 0 },
19 // }
20 // macro_rules! print {
21 //     ($($arg:tt)*) => (std::io::_print(format_args!($($arg)*)));
22 // }
23 //
24 // fn main() {
25 //     print!("{x + 1}$0");
26 // }
27 // ```
28 // ->
29 // ```
30 // macro_rules! format_args {
31 //     ($lit:literal $(tt:tt)*) => { 0 },
32 // }
33 // macro_rules! print {
34 //     ($($arg:tt)*) => (std::io::_print(format_args!($($arg)*)));
35 // }
36 //
37 // fn main() {
38 //     print!("{}"$0, x + 1);
39 // }
40 // ```
41
42 pub(crate) fn move_format_string_arg(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
43     let fmt_string = ctx.find_token_at_offset::<ast::String>()?;
44     let tt = fmt_string.syntax().parent().and_then(ast::TokenTree::cast)?;
45
46     let expanded_t = ast::String::cast(
47         ctx.sema.descend_into_macros_with_kind_preference(fmt_string.syntax().clone()),
48     )?;
49     if !is_format_string(&expanded_t) {
50         return None;
51     }
52
53     let (new_fmt, extracted_args) = parse_format_exprs(fmt_string.text()).ok()?;
54     if extracted_args.is_empty() {
55         return None;
56     }
57
58     acc.add(
59         AssistId(
60             "move_format_string_arg",
61             // if there aren't any expressions, then make the assist a RefactorExtract
62             if extracted_args.iter().filter(|f| matches!(f, Arg::Expr(_))).count() == 0 {
63                 AssistKind::RefactorExtract
64             } else {
65                 AssistKind::QuickFix
66             },
67         ),
68         "Extract format args",
69         tt.syntax().text_range(),
70         |edit| {
71             let fmt_range = fmt_string.syntax().text_range();
72
73             // Replace old format string with new format string whose arguments have been extracted
74             edit.replace(fmt_range, new_fmt);
75
76             // Insert cursor at end of format string
77             edit.insert(fmt_range.end(), "$0");
78
79             // Extract existing arguments in macro
80             let tokens =
81                 tt.token_trees_and_tokens().filter_map(NodeOrToken::into_token).collect_vec();
82
83             let mut existing_args: Vec<String> = vec![];
84
85             let mut current_arg = String::new();
86             if let [_opening_bracket, format_string, _args_start_comma, tokens @ .., end_bracket] =
87                 tokens.as_slice()
88             {
89                 for t in tokens {
90                     if t.kind() == COMMA {
91                         existing_args.push(current_arg.trim().into());
92                         current_arg.clear();
93                     } else {
94                         current_arg.push_str(t.text());
95                     }
96                 }
97                 existing_args.push(current_arg.trim().into());
98
99                 // delete everything after the format string till end bracket
100                 // we're going to insert the new arguments later
101                 edit.delete(TextRange::new(
102                     format_string.text_range().end(),
103                     end_bracket.text_range().start(),
104                 ));
105             }
106
107             // Start building the new args
108             let mut existing_args = existing_args.into_iter();
109             let mut args = String::new();
110
111             let mut placeholder_idx = 1;
112
113             for extracted_args in extracted_args {
114                 // remove expr from format string
115                 args.push_str(", ");
116
117                 match extracted_args {
118                     Arg::Ident(s) | Arg::Expr(s) => {
119                         // insert arg
120                         args.push_str(&s);
121                     }
122                     Arg::Placeholder => {
123                         // try matching with existing argument
124                         match existing_args.next() {
125                             Some(ea) => {
126                                 args.push_str(&ea);
127                             }
128                             None => {
129                                 // insert placeholder
130                                 args.push_str(&format!("${placeholder_idx}"));
131                                 placeholder_idx += 1;
132                             }
133                         }
134                     }
135                 }
136             }
137
138             // Insert new args
139             edit.insert(fmt_range.end(), args);
140         },
141     );
142
143     Some(())
144 }
145
146 #[cfg(test)]
147 mod tests {
148     use super::*;
149     use crate::tests::check_assist;
150
151     const MACRO_DECL: &'static str = r#"
152 macro_rules! format_args {
153     ($lit:literal $(tt:tt)*) => { 0 },
154 }
155 macro_rules! print {
156     ($($arg:tt)*) => (std::io::_print(format_args!($($arg)*)));
157 }
158 "#;
159
160     fn add_macro_decl(s: &'static str) -> String {
161         MACRO_DECL.to_string() + s
162     }
163
164     #[test]
165     fn multiple_middle_arg() {
166         check_assist(
167             move_format_string_arg,
168             &add_macro_decl(
169                 r#"
170 fn main() {
171     print!("{} {x + 1:b} {}$0", y + 2, 2);
172 }
173 "#,
174             ),
175             &add_macro_decl(
176                 r#"
177 fn main() {
178     print!("{} {:b} {}"$0, y + 2, x + 1, 2);
179 }
180 "#,
181             ),
182         );
183     }
184
185     #[test]
186     fn single_arg() {
187         check_assist(
188             move_format_string_arg,
189             &add_macro_decl(
190                 r#"
191 fn main() {
192     print!("{obj.value:b}$0",);
193 }
194 "#,
195             ),
196             &add_macro_decl(
197                 r#"
198 fn main() {
199     print!("{:b}"$0, obj.value);
200 }
201 "#,
202             ),
203         );
204     }
205
206     #[test]
207     fn multiple_middle_placeholders_arg() {
208         check_assist(
209             move_format_string_arg,
210             &add_macro_decl(
211                 r#"
212 fn main() {
213     print!("{} {x + 1:b} {} {}$0", y + 2, 2);
214 }
215 "#,
216             ),
217             &add_macro_decl(
218                 r#"
219 fn main() {
220     print!("{} {:b} {} {}"$0, y + 2, x + 1, 2, $1);
221 }
222 "#,
223             ),
224         );
225     }
226
227     #[test]
228     fn multiple_trailing_args() {
229         check_assist(
230             move_format_string_arg,
231             &add_macro_decl(
232                 r#"
233 fn main() {
234     print!("{} {x + 1:b} {Struct(1, 2)}$0", 1);
235 }
236 "#,
237             ),
238             &add_macro_decl(
239                 r#"
240 fn main() {
241     print!("{} {:b} {}"$0, 1, x + 1, Struct(1, 2));
242 }
243 "#,
244             ),
245         );
246     }
247
248     #[test]
249     fn improper_commas() {
250         check_assist(
251             move_format_string_arg,
252             &add_macro_decl(
253                 r#"
254 fn main() {
255     print!("{} {x + 1:b} {Struct(1, 2)}$0", 1,);
256 }
257 "#,
258             ),
259             &add_macro_decl(
260                 r#"
261 fn main() {
262     print!("{} {:b} {}"$0, 1, x + 1, Struct(1, 2));
263 }
264 "#,
265             ),
266         );
267     }
268 }