Skip to content

Commit f64c956

Browse files
committed
Auto merge of #13216 - DesmondWillowbrook:move_format_string_arg, r=DesmondWillowbrook
New assist: move_format_string_arg The name might need some improving. ```rust fn main() { print!("{x + 1}"); } ``` to ```rust fn main() { print!("{}"$0, x + 1); } ``` fixes #13180 ref to #5988 for similar work * extracted `format_like`'s parser to it's own module in `ide-db` * reworked the parser's API to be more direct * added assist to extract expressions in format args
2 parents b1a4ba3 + 54e9324 commit f64c956

File tree

6 files changed

+581
-235
lines changed

6 files changed

+581
-235
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
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+
}

crates/ide-assists/src/lib.rs

+2
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ mod handlers {
136136
mod flip_binexpr;
137137
mod flip_comma;
138138
mod flip_trait_bound;
139+
mod move_format_string_arg;
139140
mod generate_constant;
140141
mod generate_default_from_enum_variant;
141142
mod generate_default_from_new;
@@ -254,6 +255,7 @@ mod handlers {
254255
merge_imports::merge_imports,
255256
merge_match_arms::merge_match_arms,
256257
move_bounds::move_bounds_to_where_clause,
258+
move_format_string_arg::move_format_string_arg,
257259
move_guard::move_arm_cond_to_match_guard,
258260
move_guard::move_guard_to_arm_body,
259261
move_module_to_file::move_module_to_file,

crates/ide-assists/src/tests/generated.rs

+31
Original file line numberDiff line numberDiff line change
@@ -1591,6 +1591,37 @@ fn apply<T, U, F>(f: F, x: T) -> U where F: FnOnce(T) -> U {
15911591
)
15921592
}
15931593

1594+
#[test]
1595+
fn doctest_move_format_string_arg() {
1596+
check_doc_test(
1597+
"move_format_string_arg",
1598+
r#####"
1599+
macro_rules! format_args {
1600+
($lit:literal $(tt:tt)*) => { 0 },
1601+
}
1602+
macro_rules! print {
1603+
($($arg:tt)*) => (std::io::_print(format_args!($($arg)*)));
1604+
}
1605+
1606+
fn main() {
1607+
print!("{x + 1}$0");
1608+
}
1609+
"#####,
1610+
r#####"
1611+
macro_rules! format_args {
1612+
($lit:literal $(tt:tt)*) => { 0 },
1613+
}
1614+
macro_rules! print {
1615+
($($arg:tt)*) => (std::io::_print(format_args!($($arg)*)));
1616+
}
1617+
1618+
fn main() {
1619+
print!("{}"$0, x + 1);
1620+
}
1621+
"#####,
1622+
)
1623+
}
1624+
15941625
#[test]
15951626
fn doctest_move_from_mod_rs() {
15961627
check_doc_test(

0 commit comments

Comments
 (0)