Skip to content

Commit 2d9ed4f

Browse files
committed
Auto merge of #13641 - DesmondWillowbrook:fix-move-format-string, r=Veykril
fix: format expression parsing edge-cases - Handle positional arguments with formatting options (i.e. `{:b}`). Previously copied `:b` as an argument, producing broken code. - Handle indexed positional arguments (`{0}`) ([reference](https://doc.rust-lang.org/std/fmt/#positional-parameters)). Previously copied over `0` as an argument. Note: the assist also breaks when named arguments are used (`"{name}$0", name = 2 + 2` is converted to `"{}"$0, name`. I'm working on fix for that as well.
2 parents 8050fdb + 6d4b2b4 commit 2d9ed4f

File tree

2 files changed

+36
-30
lines changed

2 files changed

+36
-30
lines changed

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ pub(crate) fn move_format_string_arg(acc: &mut Assists, ctx: &AssistContext<'_>)
9292
NodeOrToken::Node(n) => {
9393
format_to!(current_arg, "{n}");
9494
},
95-
NodeOrToken::Token(t) if t.kind() == COMMA=> {
95+
NodeOrToken::Token(t) if t.kind() == COMMA => {
9696
existing_args.push(current_arg.trim().into());
9797
current_arg.clear();
9898
},
@@ -238,14 +238,14 @@ fn main() {
238238
&add_macro_decl(
239239
r#"
240240
fn main() {
241-
print!("{} {x + 1:b} {Struct(1, 2)}$0", 1);
241+
print!("{:b} {x + 1:b} {Struct(1, 2)}$0", 1);
242242
}
243243
"#,
244244
),
245245
&add_macro_decl(
246246
r#"
247247
fn main() {
248-
print!("{} {:b} {}"$0, 1, x + 1, Struct(1, 2));
248+
print!("{:b} {:b} {}"$0, 1, x + 1, Struct(1, 2));
249249
}
250250
"#,
251251
),

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

Lines changed: 33 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,11 @@ pub fn parse_format_exprs(input: &str) -> Result<(String, Vec<Arg>), ()> {
104104
extracted_expressions.push(Arg::Placeholder);
105105
state = State::NotArg;
106106
}
107+
(State::MaybeArg, ':') => {
108+
output.push(chr);
109+
extracted_expressions.push(Arg::Placeholder);
110+
state = State::FormatOpts;
111+
}
107112
(State::MaybeArg, _) => {
108113
if matches!(chr, '\\' | '$') {
109114
current_expr.push('\\');
@@ -118,44 +123,41 @@ pub fn parse_format_exprs(input: &str) -> Result<(String, Vec<Arg>), ()> {
118123
state = State::Expr;
119124
}
120125
}
121-
(State::Ident | State::Expr, '}') => {
122-
if inexpr_open_count == 0 {
123-
output.push(chr);
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-
131-
current_expr = String::new();
132-
state = State::NotArg;
133-
} else {
134-
// We're closing one brace met before inside of the expression.
135-
current_expr.push(chr);
136-
inexpr_open_count -= 1;
137-
}
138-
}
139126
(State::Ident | State::Expr, ':') if matches!(chars.peek(), Some(':')) => {
140127
// path separator
141128
state = State::Expr;
142129
current_expr.push_str("::");
143130
chars.next();
144131
}
145-
(State::Ident | State::Expr, ':') => {
132+
(State::Ident | State::Expr, ':' | '}') => {
146133
if inexpr_open_count == 0 {
147-
// We're outside of braces, thus assume that it's a specifier, like "{Some(value):?}"
148-
output.push(chr);
134+
let trimmed = current_expr.trim();
149135

150-
if matches!(state, State::Expr) {
151-
extracted_expressions.push(Arg::Expr(current_expr.trim().into()));
136+
// if the expression consists of a single number, like "0" or "12", it can refer to
137+
// format args in the order they are specified.
138+
// see: https://doc.rust-lang.org/std/fmt/#positional-parameters
139+
if trimmed.chars().fold(true, |only_num, c| c.is_ascii_digit() && only_num) {
140+
output.push_str(trimmed);
141+
} else if matches!(state, State::Expr) {
142+
extracted_expressions.push(Arg::Expr(trimmed.into()));
152143
} else {
153-
extracted_expressions.push(Arg::Ident(current_expr.trim().into()));
144+
extracted_expressions.push(Arg::Ident(trimmed.into()));
154145
}
155146

156-
current_expr = String::new();
157-
state = State::FormatOpts;
158-
} else {
147+
output.push(chr);
148+
current_expr.clear();
149+
state = if chr == ':' {
150+
State::FormatOpts
151+
} else if chr == '}' {
152+
State::NotArg
153+
} else {
154+
unreachable!()
155+
};
156+
} else if chr == '}' {
157+
// We're closing one brace met before inside of the expression.
158+
current_expr.push(chr);
159+
inexpr_open_count -= 1;
160+
} else if chr == ':' {
159161
// We're inside of braced expression, assume that it's a struct field name/value delimiter.
160162
current_expr.push(chr);
161163
}
@@ -219,6 +221,10 @@ mod tests {
219221
("{expr} is {2 + 2}", expect![["{} is {}; expr, 2 + 2"]]),
220222
("{expr:?}", expect![["{:?}; expr"]]),
221223
("{expr:1$}", expect![[r"{:1\$}; expr"]]),
224+
("{:1$}", expect![[r"{:1\$}; $1"]]),
225+
("{:>padding$}", expect![[r"{:>padding\$}; $1"]]),
226+
("{}, {}, {0}", expect![[r"{}, {}, {0}; $1, $2"]]),
227+
("{}, {}, {0:b}", expect![[r"{}, {}, {0:b}; $1, $2"]]),
222228
("{$0}", expect![[r"{}; \$0"]]),
223229
("{malformed", expect![["-"]]),
224230
("malformed}", expect![["-"]]),

0 commit comments

Comments
 (0)