Skip to content

Commit ceeed40

Browse files
committed
Add ide-assist: convert_to_format_string
Example --- ```rust fn foo() { let n = 2; let s = "n: {n$0}"; } ``` -> ```rust fn foo() { let n = 2; let s = format!("n: {n}"); } ```
1 parent c937fcc commit ceeed40

File tree

3 files changed

+384
-0
lines changed

3 files changed

+384
-0
lines changed
Lines changed: 363 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,363 @@
1+
use std::iter::once;
2+
3+
use ide_db::syntax_helpers::format_string::is_format_string;
4+
use syntax::{
5+
AstNode, AstToken, NodeOrToken, T,
6+
ast::{self, make::tokens, syntax_factory::SyntaxFactory},
7+
};
8+
9+
use crate::{
10+
AssistId,
11+
assist_context::{AssistContext, Assists},
12+
};
13+
14+
// Assist: convert_to_format_string
15+
//
16+
// Convert string literal to `format!()`.
17+
//
18+
// ```
19+
// fn foo() {
20+
// let n = 2;
21+
// let s = "n: {n$0}";
22+
// }
23+
// ```
24+
// ->
25+
// ```
26+
// fn foo() {
27+
// let n = 2;
28+
// let s = format!("n: {n}");
29+
// }
30+
// ```
31+
pub(crate) fn convert_to_format_string(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
32+
let str_token = ctx.find_token_at_offset::<ast::String>()?;
33+
let parent = str_token.syntax().parent()?;
34+
let text = str_token.syntax().text();
35+
36+
if ctx
37+
.sema
38+
.descend_into_macros(str_token.syntax().clone())
39+
.into_iter()
40+
.filter_map(ast::String::cast)
41+
.any(|string| is_format_string(&string))
42+
{
43+
return None;
44+
}
45+
46+
let offset_in_text = ctx.offset().checked_sub(str_token.syntax().text_range().start())?;
47+
let (left, variable, right) = split_curly(text, offset_in_text.into())?;
48+
let scope = ctx.sema.scope(&parent)?;
49+
50+
if !variable.is_empty() && !exist_variable(variable, scope) {
51+
return None;
52+
}
53+
54+
acc.add(
55+
AssistId::refactor_rewrite("convert_to_format_string"),
56+
"Convert to `format!()`",
57+
str_token.syntax().text_range(),
58+
|builder| {
59+
let left = escape_format_string(left);
60+
let right = escape_format_string(right);
61+
62+
let mut edit = builder.make_editor(&parent);
63+
let make = SyntaxFactory::with_mappings();
64+
65+
let new_str = format!("{left}{{{variable}}}{right}");
66+
let new_str = tokens::literal(&new_str);
67+
let args = once(NodeOrToken::Token(new_str)).chain(
68+
variable
69+
.is_empty()
70+
.then(|| {
71+
[
72+
NodeOrToken::Token(make.token(T![,])),
73+
NodeOrToken::Token(make.whitespace(" ")),
74+
]
75+
})
76+
.into_iter()
77+
.flatten(),
78+
);
79+
let tt = make.token_tree(T!['('], args);
80+
let expr_macro = make.expr_macro(make.ident_path("format"), tt);
81+
edit.replace(str_token.syntax(), expr_macro.syntax());
82+
83+
if variable.is_empty()
84+
&& let Some(cap) = ctx.config.snippet_cap
85+
&& let Some(macro_call) = expr_macro.macro_call()
86+
&& let Some(token_tree) = macro_call.token_tree()
87+
&& let Some(NodeOrToken::Token(last)) = token_tree.token_trees_and_tokens().last()
88+
{
89+
let annotation = builder.make_tabstop_before(cap);
90+
edit.add_annotation(last, annotation);
91+
}
92+
93+
edit.add_mappings(make.finish_with_mappings());
94+
builder.add_file_edits(ctx.vfs_file_id(), edit);
95+
},
96+
)
97+
}
98+
99+
fn escape_format_string(s: &str) -> String {
100+
let mut replaced = s.replace('{', "{{");
101+
stdx::replace(&mut replaced, '}', "}}");
102+
replaced
103+
}
104+
105+
fn exist_variable(variable: &str, scope: hir::SemanticsScope<'_>) -> bool {
106+
let mut exist = false;
107+
scope.process_all_names(&mut |name, def| {
108+
if !matches!(def, hir::ScopeDef::Local(_)) || exist {
109+
return;
110+
}
111+
exist = name.as_str() == variable;
112+
});
113+
exist
114+
}
115+
116+
fn split_curly(text: &str, offset: usize) -> Option<(&str, &str, &str)> {
117+
let offset = improve_side_offset(text, offset).unwrap_or(offset);
118+
let (left, right) = text.split_at_checked(offset)?;
119+
let l_curly = left.rfind('{')?;
120+
let r_curly = right.find('}')? + left.len();
121+
122+
Some((&text[..l_curly], &text[l_curly + 1..r_curly], &text[r_curly + 1..]))
123+
}
124+
125+
fn improve_side_offset(text: &str, offset_in_text: usize) -> Option<usize> {
126+
text.get(offset_in_text..)
127+
.and_then(|s| (s.chars().next()? == '{').then_some(offset_in_text + 1))
128+
.or_else(|| {
129+
(text.get(..offset_in_text)?.chars().next_back()? == '}').then_some(offset_in_text - 1)
130+
})
131+
}
132+
133+
#[cfg(test)]
134+
mod tests {
135+
use crate::tests::{check_assist, check_assist_not_applicable};
136+
137+
use super::*;
138+
139+
#[test]
140+
fn empty_format() {
141+
check_assist(
142+
convert_to_format_string,
143+
r#"
144+
fn foo() {
145+
let s = "{$0}";
146+
}
147+
"#,
148+
r#"
149+
fn foo() {
150+
let s = format!("{}", $0);
151+
}
152+
"#,
153+
);
154+
155+
check_assist(
156+
convert_to_format_string,
157+
r#"
158+
fn foo() {
159+
let s = "left{$0}right";
160+
}
161+
"#,
162+
r#"
163+
fn foo() {
164+
let s = format!("left{}right", $0);
165+
}
166+
"#,
167+
);
168+
}
169+
170+
#[test]
171+
fn curly_offsets() {
172+
check_assist(
173+
convert_to_format_string,
174+
r#"
175+
fn foo() {
176+
let s = "left{}$0right";
177+
}
178+
"#,
179+
r#"
180+
fn foo() {
181+
let s = format!("left{}right", $0);
182+
}
183+
"#,
184+
);
185+
186+
check_assist(
187+
convert_to_format_string,
188+
r#"
189+
fn foo() {
190+
let s = "left$0{}right";
191+
}
192+
"#,
193+
r#"
194+
fn foo() {
195+
let s = format!("left{}right", $0);
196+
}
197+
"#,
198+
);
199+
}
200+
201+
#[test]
202+
fn biased_curlys() {
203+
check_assist(
204+
convert_to_format_string,
205+
r#"
206+
fn foo() {
207+
let s = "left{}$0{}right";
208+
}
209+
"#,
210+
r#"
211+
fn foo() {
212+
let s = format!("left{{}}{}right", $0);
213+
}
214+
"#,
215+
);
216+
}
217+
218+
#[test]
219+
fn not_format_other_curlys() {
220+
check_assist(
221+
convert_to_format_string,
222+
r#"
223+
fn foo() {
224+
let s = "{left{$0}right}";
225+
}
226+
"#,
227+
r#"
228+
fn foo() {
229+
let s = format!("{{left{}right}}", $0);
230+
}
231+
"#,
232+
);
233+
234+
check_assist(
235+
convert_to_format_string,
236+
r#"
237+
fn foo() {
238+
let s = "{{}left{{$0}}right{}}";
239+
}
240+
"#,
241+
r#"
242+
fn foo() {
243+
let s = format!("{{{{}}left{{{}}}right{{}}}}", $0);
244+
}
245+
"#,
246+
);
247+
}
248+
249+
#[test]
250+
fn format_variable() {
251+
check_assist(
252+
convert_to_format_string,
253+
r#"
254+
fn foo() {
255+
let var = 2;
256+
let s = "{left{var$0}right}";
257+
}
258+
"#,
259+
r#"
260+
fn foo() {
261+
let var = 2;
262+
let s = format!("{{left{var}right}}");
263+
}
264+
"#,
265+
);
266+
267+
check_assist(
268+
convert_to_format_string,
269+
r#"
270+
fn foo() {
271+
let s = "{{}left{{$0}}right{}}";
272+
}
273+
"#,
274+
r#"
275+
fn foo() {
276+
let s = format!("{{{{}}left{{{}}}right{{}}}}", $0);
277+
}
278+
"#,
279+
);
280+
}
281+
282+
#[test]
283+
fn applicable_in_macro() {
284+
check_assist(
285+
convert_to_format_string,
286+
r#"
287+
fn foo() {
288+
let var = 2;
289+
let s = some_macro!("{left{var$0}right}");
290+
}
291+
"#,
292+
r#"
293+
fn foo() {
294+
let var = 2;
295+
let s = some_macro!(format!("{{left{var}right}}"));
296+
}
297+
"#,
298+
);
299+
300+
check_assist(
301+
convert_to_format_string,
302+
r#"
303+
//- minicore: fmt
304+
fn foo() {
305+
let var = 2;
306+
let s = print!("{}", "{left{var$0}right}");
307+
}
308+
"#,
309+
r#"
310+
fn foo() {
311+
let var = 2;
312+
let s = print!("{}", format!("{{left{var}right}}"));
313+
}
314+
"#,
315+
);
316+
}
317+
318+
#[test]
319+
fn not_applicable_outside_curly() {
320+
check_assist_not_applicable(
321+
convert_to_format_string,
322+
r#"
323+
fn foo() {
324+
let s = "left{}r$0ight";
325+
}
326+
"#,
327+
);
328+
329+
check_assist_not_applicable(
330+
convert_to_format_string,
331+
r#"
332+
fn foo() {
333+
let s = "l$0eft{}right";
334+
}
335+
"#,
336+
);
337+
}
338+
339+
#[test]
340+
fn not_applicable_unknown_variable() {
341+
check_assist_not_applicable(
342+
convert_to_format_string,
343+
r#"
344+
fn foo() {
345+
let s = "left{var$0}right";
346+
}
347+
"#,
348+
);
349+
}
350+
351+
#[test]
352+
fn not_applicable_is_format_string() {
353+
check_assist_not_applicable(
354+
convert_to_format_string,
355+
r#"
356+
//- minicore: fmt
357+
fn foo() {
358+
let s = print!("left{$0}right");
359+
}
360+
"#,
361+
);
362+
}
363+
}

crates/ide-assists/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ mod handlers {
132132
mod convert_named_struct_to_tuple_struct;
133133
mod convert_nested_function_to_closure;
134134
mod convert_range_for_to_while;
135+
mod convert_to_format_string;
135136
mod convert_to_guarded_return;
136137
mod convert_tuple_return_type_to_struct;
137138
mod convert_tuple_struct_to_named_struct;
@@ -270,6 +271,7 @@ mod handlers {
270271
convert_named_struct_to_tuple_struct::convert_named_struct_to_tuple_struct,
271272
convert_nested_function_to_closure::convert_nested_function_to_closure,
272273
convert_range_for_to_while::convert_range_for_to_while,
274+
convert_to_format_string::convert_to_format_string,
273275
convert_to_guarded_return::convert_to_guarded_return,
274276
convert_tuple_return_type_to_struct::convert_tuple_return_type_to_struct,
275277
convert_tuple_struct_to_named_struct::convert_tuple_struct_to_named_struct,

0 commit comments

Comments
 (0)