Skip to content

Commit fb5ae99

Browse files
committed
suggest ExtractRefactor if no expressions found
Added `Ident` variant to arg enum.
1 parent a5cbee4 commit fb5ae99

File tree

2 files changed

+86
-32
lines changed

2 files changed

+86
-32
lines changed

crates/ide-assists/src/handlers/move_format_string_arg.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,15 @@ pub(crate) fn move_format_string_arg(acc: &mut Assists, ctx: &AssistContext<'_>)
5656
}
5757

5858
acc.add(
59-
AssistId("move_format_string_arg", AssistKind::QuickFix),
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+
),
6068
"Extract format args",
6169
tt.syntax().text_range(),
6270
|edit| {
@@ -107,7 +115,7 @@ pub(crate) fn move_format_string_arg(acc: &mut Assists, ctx: &AssistContext<'_>)
107115
args.push_str(", ");
108116

109117
match extracted_args {
110-
Arg::Expr(s) => {
118+
Arg::Ident(s) | Arg::Expr(s) => {
111119
// insert arg
112120
args.push_str(&s);
113121
}

crates/ide-db/src/syntax_helpers/format_string_exprs.rs

Lines changed: 76 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,27 @@
44
/// Enum for represenging extraced format string args.
55
/// Can either be extracted expressions (which includes identifiers),
66
/// or placeholders `{}`.
7-
#[derive(Debug)]
7+
#[derive(Debug, PartialEq, Eq)]
88
pub enum Arg {
99
Placeholder,
10+
Ident(String),
1011
Expr(String),
1112
}
1213

1314
/**
14-
Add placeholders like `$1` and `$2` in place of [`Arg::Placeholder`].
15+
Add placeholders like `$1` and `$2` in place of [`Arg::Placeholder`],
16+
and unwraps the [`Arg::Ident`] and [`Arg::Expr`] enums.
1517
```rust
16-
assert_eq!(vec![Arg::Expr("expr"), Arg::Placeholder, Arg::Expr("expr")], vec!["expr", "$1", "expr"])
18+
# use ide_db::syntax_helpers::format_string_exprs::*;
19+
assert_eq!(with_placeholders(vec![Arg::Ident("ident".to_owned()), Arg::Placeholder, Arg::Expr("expr + 2".to_owned())]), vec!["ident".to_owned(), "$1".to_owned(), "expr + 2".to_owned()])
1720
```
1821
*/
1922

2023
pub fn with_placeholders(args: Vec<Arg>) -> Vec<String> {
2124
let mut placeholder_id = 1;
2225
args.into_iter()
2326
.map(move |a| match a {
24-
Arg::Expr(s) => s,
27+
Arg::Expr(s) | Arg::Ident(s) => s,
2528
Arg::Placeholder => {
2629
let s = format!("${placeholder_id}");
2730
placeholder_id += 1;
@@ -40,21 +43,22 @@ pub fn with_placeholders(args: Vec<Arg>) -> Vec<String> {
4043
Splits a format string that may contain expressions
4144
like
4245
```rust
43-
assert_eq!(parse("{expr} {} {expr} ").unwrap(), ("{} {} {}", vec![Arg::Expr("expr"), Arg::Placeholder, Arg::Expr("expr")]));
46+
assert_eq!(parse("{ident} {} {expr + 42} ").unwrap(), ("{} {} {}", vec![Arg::Ident("ident"), Arg::Placeholder, Arg::Expr("expr + 42")]));
4447
```
4548
*/
4649
pub fn parse_format_exprs(input: &str) -> Result<(String, Vec<Arg>), ()> {
4750
#[derive(Debug, Clone, Copy, PartialEq)]
4851
enum State {
49-
NotExpr,
50-
MaybeExpr,
52+
NotArg,
53+
MaybeArg,
5154
Expr,
55+
Ident,
5256
MaybeIncorrect,
5357
FormatOpts,
5458
}
5559

60+
let mut state = State::NotArg;
5661
let mut current_expr = String::new();
57-
let mut state = State::NotExpr;
5862
let mut extracted_expressions = Vec::new();
5963
let mut output = String::new();
6064

@@ -66,15 +70,15 @@ pub fn parse_format_exprs(input: &str) -> Result<(String, Vec<Arg>), ()> {
6670
let mut chars = input.chars().peekable();
6771
while let Some(chr) = chars.next() {
6872
match (state, chr) {
69-
(State::NotExpr, '{') => {
73+
(State::NotArg, '{') => {
7074
output.push(chr);
71-
state = State::MaybeExpr;
75+
state = State::MaybeArg;
7276
}
73-
(State::NotExpr, '}') => {
77+
(State::NotArg, '}') => {
7478
output.push(chr);
7579
state = State::MaybeIncorrect;
7680
}
77-
(State::NotExpr, _) => {
81+
(State::NotArg, _) => {
7882
if matches!(chr, '\\' | '$') {
7983
output.push('\\');
8084
}
@@ -83,71 +87,97 @@ pub fn parse_format_exprs(input: &str) -> Result<(String, Vec<Arg>), ()> {
8387
(State::MaybeIncorrect, '}') => {
8488
// It's okay, we met "}}".
8589
output.push(chr);
86-
state = State::NotExpr;
90+
state = State::NotArg;
8791
}
8892
(State::MaybeIncorrect, _) => {
8993
// Error in the string.
9094
return Err(());
9195
}
92-
(State::MaybeExpr, '{') => {
96+
// Escaped braces `{{`
97+
(State::MaybeArg, '{') => {
9398
output.push(chr);
94-
state = State::NotExpr;
99+
state = State::NotArg;
95100
}
96-
(State::MaybeExpr, '}') => {
97-
// This is an empty sequence '{}'. Replace it with placeholder.
101+
(State::MaybeArg, '}') => {
102+
// This is an empty sequence '{}'.
98103
output.push(chr);
99104
extracted_expressions.push(Arg::Placeholder);
100-
state = State::NotExpr;
105+
state = State::NotArg;
101106
}
102-
(State::MaybeExpr, _) => {
107+
(State::MaybeArg, _) => {
103108
if matches!(chr, '\\' | '$') {
104109
current_expr.push('\\');
105110
}
106111
current_expr.push(chr);
107-
state = State::Expr;
112+
113+
// While Rust uses the unicode sets of XID_start and XID_continue for Identifiers
114+
// this is probably the best we can do to avoid a false positive
115+
if chr.is_alphabetic() || chr == '_' {
116+
state = State::Ident;
117+
} else {
118+
state = State::Expr;
119+
}
108120
}
109-
(State::Expr, '}') => {
121+
(State::Ident | State::Expr, '}') => {
110122
if inexpr_open_count == 0 {
111123
output.push(chr);
112-
extracted_expressions.push(Arg::Expr(current_expr.trim().into()));
124+
125+
if matches!(state, State::Expr) {
126+
extracted_expressions.push(Arg::Expr(current_expr.trim().into()));
127+
} else {
128+
extracted_expressions.push(Arg::Ident(current_expr.trim().into()));
129+
}
130+
113131
current_expr = String::new();
114-
state = State::NotExpr;
132+
state = State::NotArg;
115133
} else {
116134
// We're closing one brace met before inside of the expression.
117135
current_expr.push(chr);
118136
inexpr_open_count -= 1;
119137
}
120138
}
121-
(State::Expr, ':') if matches!(chars.peek(), Some(':')) => {
139+
(State::Ident | State::Expr, ':') if matches!(chars.peek(), Some(':')) => {
122140
// path separator
141+
state = State::Expr;
123142
current_expr.push_str("::");
124143
chars.next();
125144
}
126-
(State::Expr, ':') => {
145+
(State::Ident | State::Expr, ':') => {
127146
if inexpr_open_count == 0 {
128147
// We're outside of braces, thus assume that it's a specifier, like "{Some(value):?}"
129148
output.push(chr);
130-
extracted_expressions.push(Arg::Expr(current_expr.trim().into()));
149+
150+
if matches!(state, State::Expr) {
151+
extracted_expressions.push(Arg::Expr(current_expr.trim().into()));
152+
} else {
153+
extracted_expressions.push(Arg::Ident(current_expr.trim().into()));
154+
}
155+
131156
current_expr = String::new();
132157
state = State::FormatOpts;
133158
} else {
134159
// We're inside of braced expression, assume that it's a struct field name/value delimiter.
135160
current_expr.push(chr);
136161
}
137162
}
138-
(State::Expr, '{') => {
163+
(State::Ident | State::Expr, '{') => {
164+
state = State::Expr;
139165
current_expr.push(chr);
140166
inexpr_open_count += 1;
141167
}
142-
(State::Expr, _) => {
168+
(State::Ident | State::Expr, _) => {
169+
if !(chr.is_alphanumeric() || chr == '_' || chr == '#') {
170+
state = State::Expr;
171+
}
172+
143173
if matches!(chr, '\\' | '$') {
144174
current_expr.push('\\');
145175
}
146176
current_expr.push(chr);
147177
}
148178
(State::FormatOpts, '}') => {
149179
output.push(chr);
150-
state = State::NotExpr;
180+
state = State::NotArg;
151181
}
152182
(State::FormatOpts, _) => {
153183
if matches!(chr, '\\' | '$') {
@@ -158,7 +188,7 @@ pub fn parse_format_exprs(input: &str) -> Result<(String, Vec<Arg>), ()> {
158188
}
159189
}
160190

161-
if state != State::NotExpr {
191+
if state != State::NotArg {
162192
return Err(());
163193
}
164194

@@ -218,4 +248,20 @@ mod tests {
218248
check(input, output)
219249
}
220250
}
251+
252+
#[test]
253+
fn arg_type() {
254+
assert_eq!(
255+
parse_format_exprs("{_ident} {r#raw_ident} {expr.obj} {name {thing: 42} } {}")
256+
.unwrap()
257+
.1,
258+
vec![
259+
Arg::Ident("_ident".to_owned()),
260+
Arg::Ident("r#raw_ident".to_owned()),
261+
Arg::Expr("expr.obj".to_owned()),
262+
Arg::Expr("name {thing: 42}".to_owned()),
263+
Arg::Placeholder
264+
]
265+
);
266+
}
221267
}

0 commit comments

Comments
 (0)