mirror of
https://github.com/rust-lang/rust-analyzer.git
synced 2025-09-30 22:01:37 +00:00
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
This commit is contained in:
commit
f64c95600c
6 changed files with 581 additions and 235 deletions
268
crates/ide-assists/src/handlers/move_format_string_arg.rs
Normal file
268
crates/ide-assists/src/handlers/move_format_string_arg.rs
Normal file
|
@ -0,0 +1,268 @@
|
|||
use crate::{AssistContext, Assists};
|
||||
use ide_db::{
|
||||
assists::{AssistId, AssistKind},
|
||||
syntax_helpers::{
|
||||
format_string::is_format_string,
|
||||
format_string_exprs::{parse_format_exprs, Arg},
|
||||
},
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use syntax::{ast, AstNode, AstToken, NodeOrToken, SyntaxKind::COMMA, TextRange};
|
||||
|
||||
// Assist: move_format_string_arg
|
||||
//
|
||||
// Move an expression out of a format string.
|
||||
//
|
||||
// ```
|
||||
// macro_rules! format_args {
|
||||
// ($lit:literal $(tt:tt)*) => { 0 },
|
||||
// }
|
||||
// macro_rules! print {
|
||||
// ($($arg:tt)*) => (std::io::_print(format_args!($($arg)*)));
|
||||
// }
|
||||
//
|
||||
// fn main() {
|
||||
// print!("{x + 1}$0");
|
||||
// }
|
||||
// ```
|
||||
// ->
|
||||
// ```
|
||||
// macro_rules! format_args {
|
||||
// ($lit:literal $(tt:tt)*) => { 0 },
|
||||
// }
|
||||
// macro_rules! print {
|
||||
// ($($arg:tt)*) => (std::io::_print(format_args!($($arg)*)));
|
||||
// }
|
||||
//
|
||||
// fn main() {
|
||||
// print!("{}"$0, x + 1);
|
||||
// }
|
||||
// ```
|
||||
|
||||
pub(crate) fn move_format_string_arg(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
|
||||
let fmt_string = ctx.find_token_at_offset::<ast::String>()?;
|
||||
let tt = fmt_string.syntax().parent().and_then(ast::TokenTree::cast)?;
|
||||
|
||||
let expanded_t = ast::String::cast(
|
||||
ctx.sema.descend_into_macros_with_kind_preference(fmt_string.syntax().clone()),
|
||||
)?;
|
||||
if !is_format_string(&expanded_t) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let (new_fmt, extracted_args) = parse_format_exprs(fmt_string.text()).ok()?;
|
||||
if extracted_args.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
acc.add(
|
||||
AssistId(
|
||||
"move_format_string_arg",
|
||||
// if there aren't any expressions, then make the assist a RefactorExtract
|
||||
if extracted_args.iter().filter(|f| matches!(f, Arg::Expr(_))).count() == 0 {
|
||||
AssistKind::RefactorExtract
|
||||
} else {
|
||||
AssistKind::QuickFix
|
||||
},
|
||||
),
|
||||
"Extract format args",
|
||||
tt.syntax().text_range(),
|
||||
|edit| {
|
||||
let fmt_range = fmt_string.syntax().text_range();
|
||||
|
||||
// Replace old format string with new format string whose arguments have been extracted
|
||||
edit.replace(fmt_range, new_fmt);
|
||||
|
||||
// Insert cursor at end of format string
|
||||
edit.insert(fmt_range.end(), "$0");
|
||||
|
||||
// Extract existing arguments in macro
|
||||
let tokens =
|
||||
tt.token_trees_and_tokens().filter_map(NodeOrToken::into_token).collect_vec();
|
||||
|
||||
let mut existing_args: Vec<String> = vec![];
|
||||
|
||||
let mut current_arg = String::new();
|
||||
if let [_opening_bracket, format_string, _args_start_comma, tokens @ .., end_bracket] =
|
||||
tokens.as_slice()
|
||||
{
|
||||
for t in tokens {
|
||||
if t.kind() == COMMA {
|
||||
existing_args.push(current_arg.trim().into());
|
||||
current_arg.clear();
|
||||
} else {
|
||||
current_arg.push_str(t.text());
|
||||
}
|
||||
}
|
||||
existing_args.push(current_arg.trim().into());
|
||||
|
||||
// delete everything after the format string till end bracket
|
||||
// we're going to insert the new arguments later
|
||||
edit.delete(TextRange::new(
|
||||
format_string.text_range().end(),
|
||||
end_bracket.text_range().start(),
|
||||
));
|
||||
}
|
||||
|
||||
// Start building the new args
|
||||
let mut existing_args = existing_args.into_iter();
|
||||
let mut args = String::new();
|
||||
|
||||
let mut placeholder_idx = 1;
|
||||
|
||||
for extracted_args in extracted_args {
|
||||
// remove expr from format string
|
||||
args.push_str(", ");
|
||||
|
||||
match extracted_args {
|
||||
Arg::Ident(s) | Arg::Expr(s) => {
|
||||
// insert arg
|
||||
args.push_str(&s);
|
||||
}
|
||||
Arg::Placeholder => {
|
||||
// try matching with existing argument
|
||||
match existing_args.next() {
|
||||
Some(ea) => {
|
||||
args.push_str(&ea);
|
||||
}
|
||||
None => {
|
||||
// insert placeholder
|
||||
args.push_str(&format!("${placeholder_idx}"));
|
||||
placeholder_idx += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Insert new args
|
||||
edit.insert(fmt_range.end(), args);
|
||||
},
|
||||
);
|
||||
|
||||
Some(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::tests::check_assist;
|
||||
|
||||
const MACRO_DECL: &'static str = r#"
|
||||
macro_rules! format_args {
|
||||
($lit:literal $(tt:tt)*) => { 0 },
|
||||
}
|
||||
macro_rules! print {
|
||||
($($arg:tt)*) => (std::io::_print(format_args!($($arg)*)));
|
||||
}
|
||||
"#;
|
||||
|
||||
fn add_macro_decl(s: &'static str) -> String {
|
||||
MACRO_DECL.to_string() + s
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_middle_arg() {
|
||||
check_assist(
|
||||
move_format_string_arg,
|
||||
&add_macro_decl(
|
||||
r#"
|
||||
fn main() {
|
||||
print!("{} {x + 1:b} {}$0", y + 2, 2);
|
||||
}
|
||||
"#,
|
||||
),
|
||||
&add_macro_decl(
|
||||
r#"
|
||||
fn main() {
|
||||
print!("{} {:b} {}"$0, y + 2, x + 1, 2);
|
||||
}
|
||||
"#,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_arg() {
|
||||
check_assist(
|
||||
move_format_string_arg,
|
||||
&add_macro_decl(
|
||||
r#"
|
||||
fn main() {
|
||||
print!("{obj.value:b}$0",);
|
||||
}
|
||||
"#,
|
||||
),
|
||||
&add_macro_decl(
|
||||
r#"
|
||||
fn main() {
|
||||
print!("{:b}"$0, obj.value);
|
||||
}
|
||||
"#,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_middle_placeholders_arg() {
|
||||
check_assist(
|
||||
move_format_string_arg,
|
||||
&add_macro_decl(
|
||||
r#"
|
||||
fn main() {
|
||||
print!("{} {x + 1:b} {} {}$0", y + 2, 2);
|
||||
}
|
||||
"#,
|
||||
),
|
||||
&add_macro_decl(
|
||||
r#"
|
||||
fn main() {
|
||||
print!("{} {:b} {} {}"$0, y + 2, x + 1, 2, $1);
|
||||
}
|
||||
"#,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_trailing_args() {
|
||||
check_assist(
|
||||
move_format_string_arg,
|
||||
&add_macro_decl(
|
||||
r#"
|
||||
fn main() {
|
||||
print!("{} {x + 1:b} {Struct(1, 2)}$0", 1);
|
||||
}
|
||||
"#,
|
||||
),
|
||||
&add_macro_decl(
|
||||
r#"
|
||||
fn main() {
|
||||
print!("{} {:b} {}"$0, 1, x + 1, Struct(1, 2));
|
||||
}
|
||||
"#,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn improper_commas() {
|
||||
check_assist(
|
||||
move_format_string_arg,
|
||||
&add_macro_decl(
|
||||
r#"
|
||||
fn main() {
|
||||
print!("{} {x + 1:b} {Struct(1, 2)}$0", 1,);
|
||||
}
|
||||
"#,
|
||||
),
|
||||
&add_macro_decl(
|
||||
r#"
|
||||
fn main() {
|
||||
print!("{} {:b} {}"$0, 1, x + 1, Struct(1, 2));
|
||||
}
|
||||
"#,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -136,6 +136,7 @@ mod handlers {
|
|||
mod flip_binexpr;
|
||||
mod flip_comma;
|
||||
mod flip_trait_bound;
|
||||
mod move_format_string_arg;
|
||||
mod generate_constant;
|
||||
mod generate_default_from_enum_variant;
|
||||
mod generate_default_from_new;
|
||||
|
@ -254,6 +255,7 @@ mod handlers {
|
|||
merge_imports::merge_imports,
|
||||
merge_match_arms::merge_match_arms,
|
||||
move_bounds::move_bounds_to_where_clause,
|
||||
move_format_string_arg::move_format_string_arg,
|
||||
move_guard::move_arm_cond_to_match_guard,
|
||||
move_guard::move_guard_to_arm_body,
|
||||
move_module_to_file::move_module_to_file,
|
||||
|
|
|
@ -1591,6 +1591,37 @@ fn apply<T, U, F>(f: F, x: T) -> U where F: FnOnce(T) -> U {
|
|||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn doctest_move_format_string_arg() {
|
||||
check_doc_test(
|
||||
"move_format_string_arg",
|
||||
r#####"
|
||||
macro_rules! format_args {
|
||||
($lit:literal $(tt:tt)*) => { 0 },
|
||||
}
|
||||
macro_rules! print {
|
||||
($($arg:tt)*) => (std::io::_print(format_args!($($arg)*)));
|
||||
}
|
||||
|
||||
fn main() {
|
||||
print!("{x + 1}$0");
|
||||
}
|
||||
"#####,
|
||||
r#####"
|
||||
macro_rules! format_args {
|
||||
($lit:literal $(tt:tt)*) => { 0 },
|
||||
}
|
||||
macro_rules! print {
|
||||
($($arg:tt)*) => (std::io::_print(format_args!($($arg)*)));
|
||||
}
|
||||
|
||||
fn main() {
|
||||
print!("{}"$0, x + 1);
|
||||
}
|
||||
"#####,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn doctest_move_from_mod_rs() {
|
||||
check_doc_test(
|
||||
|
|
|
@ -16,8 +16,11 @@
|
|||
//
|
||||
// image::https://user-images.githubusercontent.com/48062697/113020656-b560f500-917a-11eb-87de-02991f61beb8.gif[]
|
||||
|
||||
use ide_db::SnippetCap;
|
||||
use syntax::ast::{self, AstToken};
|
||||
use ide_db::{
|
||||
syntax_helpers::format_string_exprs::{parse_format_exprs, with_placeholders},
|
||||
SnippetCap,
|
||||
};
|
||||
use syntax::{ast, AstToken};
|
||||
|
||||
use crate::{
|
||||
completions::postfix::build_postfix_snippet_builder, context::CompletionContext, Completions,
|
||||
|
@ -43,250 +46,24 @@ pub(crate) fn add_format_like_completions(
|
|||
cap: SnippetCap,
|
||||
receiver_text: &ast::String,
|
||||
) {
|
||||
let input = match string_literal_contents(receiver_text) {
|
||||
// It's not a string literal, do not parse input.
|
||||
Some(input) => input,
|
||||
None => return,
|
||||
};
|
||||
|
||||
let postfix_snippet = match build_postfix_snippet_builder(ctx, cap, dot_receiver) {
|
||||
Some(it) => it,
|
||||
None => return,
|
||||
};
|
||||
let mut parser = FormatStrParser::new(input);
|
||||
|
||||
if parser.parse().is_ok() {
|
||||
if let Ok((out, exprs)) = parse_format_exprs(receiver_text.text()) {
|
||||
let exprs = with_placeholders(exprs);
|
||||
for (label, macro_name) in KINDS {
|
||||
let snippet = parser.to_suggestion(macro_name);
|
||||
let snippet = format!(r#"{}({}, {})"#, macro_name, out, exprs.join(", "));
|
||||
|
||||
postfix_snippet(label, macro_name, &snippet).add_to(acc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks whether provided item is a string literal.
|
||||
fn string_literal_contents(item: &ast::String) -> Option<String> {
|
||||
let item = item.text();
|
||||
if item.len() >= 2 && item.starts_with('\"') && item.ends_with('\"') {
|
||||
return Some(item[1..item.len() - 1].to_owned());
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Parser for a format-like string. It is more allowing in terms of string contents,
|
||||
/// as we expect variable placeholders to be filled with expressions.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct FormatStrParser {
|
||||
input: String,
|
||||
output: String,
|
||||
extracted_expressions: Vec<String>,
|
||||
state: State,
|
||||
parsed: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum State {
|
||||
NotExpr,
|
||||
MaybeExpr,
|
||||
Expr,
|
||||
MaybeIncorrect,
|
||||
FormatOpts,
|
||||
}
|
||||
|
||||
impl FormatStrParser {
|
||||
pub(crate) fn new(input: String) -> Self {
|
||||
Self {
|
||||
input,
|
||||
output: String::new(),
|
||||
extracted_expressions: Vec::new(),
|
||||
state: State::NotExpr,
|
||||
parsed: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn parse(&mut self) -> Result<(), ()> {
|
||||
let mut current_expr = String::new();
|
||||
|
||||
let mut placeholder_id = 1;
|
||||
|
||||
// Count of open braces inside of an expression.
|
||||
// We assume that user knows what they're doing, thus we treat it like a correct pattern, e.g.
|
||||
// "{MyStruct { val_a: 0, val_b: 1 }}".
|
||||
let mut inexpr_open_count = 0;
|
||||
|
||||
// We need to escape '\' and '$'. See the comments on `get_receiver_text()` for detail.
|
||||
let mut chars = self.input.chars().peekable();
|
||||
while let Some(chr) = chars.next() {
|
||||
match (self.state, chr) {
|
||||
(State::NotExpr, '{') => {
|
||||
self.output.push(chr);
|
||||
self.state = State::MaybeExpr;
|
||||
}
|
||||
(State::NotExpr, '}') => {
|
||||
self.output.push(chr);
|
||||
self.state = State::MaybeIncorrect;
|
||||
}
|
||||
(State::NotExpr, _) => {
|
||||
if matches!(chr, '\\' | '$') {
|
||||
self.output.push('\\');
|
||||
}
|
||||
self.output.push(chr);
|
||||
}
|
||||
(State::MaybeIncorrect, '}') => {
|
||||
// It's okay, we met "}}".
|
||||
self.output.push(chr);
|
||||
self.state = State::NotExpr;
|
||||
}
|
||||
(State::MaybeIncorrect, _) => {
|
||||
// Error in the string.
|
||||
return Err(());
|
||||
}
|
||||
(State::MaybeExpr, '{') => {
|
||||
self.output.push(chr);
|
||||
self.state = State::NotExpr;
|
||||
}
|
||||
(State::MaybeExpr, '}') => {
|
||||
// This is an empty sequence '{}'. Replace it with placeholder.
|
||||
self.output.push(chr);
|
||||
self.extracted_expressions.push(format!("${}", placeholder_id));
|
||||
placeholder_id += 1;
|
||||
self.state = State::NotExpr;
|
||||
}
|
||||
(State::MaybeExpr, _) => {
|
||||
if matches!(chr, '\\' | '$') {
|
||||
current_expr.push('\\');
|
||||
}
|
||||
current_expr.push(chr);
|
||||
self.state = State::Expr;
|
||||
}
|
||||
(State::Expr, '}') => {
|
||||
if inexpr_open_count == 0 {
|
||||
self.output.push(chr);
|
||||
self.extracted_expressions.push(current_expr.trim().into());
|
||||
current_expr = String::new();
|
||||
self.state = State::NotExpr;
|
||||
} else {
|
||||
// We're closing one brace met before inside of the expression.
|
||||
current_expr.push(chr);
|
||||
inexpr_open_count -= 1;
|
||||
}
|
||||
}
|
||||
(State::Expr, ':') if chars.peek().copied() == Some(':') => {
|
||||
// path separator
|
||||
current_expr.push_str("::");
|
||||
chars.next();
|
||||
}
|
||||
(State::Expr, ':') => {
|
||||
if inexpr_open_count == 0 {
|
||||
// We're outside of braces, thus assume that it's a specifier, like "{Some(value):?}"
|
||||
self.output.push(chr);
|
||||
self.extracted_expressions.push(current_expr.trim().into());
|
||||
current_expr = String::new();
|
||||
self.state = State::FormatOpts;
|
||||
} else {
|
||||
// We're inside of braced expression, assume that it's a struct field name/value delimiter.
|
||||
current_expr.push(chr);
|
||||
}
|
||||
}
|
||||
(State::Expr, '{') => {
|
||||
current_expr.push(chr);
|
||||
inexpr_open_count += 1;
|
||||
}
|
||||
(State::Expr, _) => {
|
||||
if matches!(chr, '\\' | '$') {
|
||||
current_expr.push('\\');
|
||||
}
|
||||
current_expr.push(chr);
|
||||
}
|
||||
(State::FormatOpts, '}') => {
|
||||
self.output.push(chr);
|
||||
self.state = State::NotExpr;
|
||||
}
|
||||
(State::FormatOpts, _) => {
|
||||
if matches!(chr, '\\' | '$') {
|
||||
self.output.push('\\');
|
||||
}
|
||||
self.output.push(chr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if self.state != State::NotExpr {
|
||||
return Err(());
|
||||
}
|
||||
|
||||
self.parsed = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn to_suggestion(&self, macro_name: &str) -> String {
|
||||
assert!(self.parsed, "Attempt to get a suggestion from not parsed expression");
|
||||
|
||||
let expressions_as_string = self.extracted_expressions.join(", ");
|
||||
format!(r#"{}("{}", {})"#, macro_name, self.output, expressions_as_string)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use expect_test::{expect, Expect};
|
||||
|
||||
fn check(input: &str, expect: &Expect) {
|
||||
let mut parser = FormatStrParser::new((*input).to_owned());
|
||||
let outcome_repr = if parser.parse().is_ok() {
|
||||
// Parsing should be OK, expected repr is "string; expr_1, expr_2".
|
||||
if parser.extracted_expressions.is_empty() {
|
||||
parser.output
|
||||
} else {
|
||||
format!("{}; {}", parser.output, parser.extracted_expressions.join(", "))
|
||||
}
|
||||
} else {
|
||||
// Parsing should fail, expected repr is "-".
|
||||
"-".to_owned()
|
||||
};
|
||||
|
||||
expect.assert_eq(&outcome_repr);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_str_parser() {
|
||||
let test_vector = &[
|
||||
("no expressions", expect![["no expressions"]]),
|
||||
(r"no expressions with \$0$1", expect![r"no expressions with \\\$0\$1"]),
|
||||
("{expr} is {2 + 2}", expect![["{} is {}; expr, 2 + 2"]]),
|
||||
("{expr:?}", expect![["{:?}; expr"]]),
|
||||
("{expr:1$}", expect![[r"{:1\$}; expr"]]),
|
||||
("{$0}", expect![[r"{}; \$0"]]),
|
||||
("{malformed", expect![["-"]]),
|
||||
("malformed}", expect![["-"]]),
|
||||
("{{correct", expect![["{{correct"]]),
|
||||
("correct}}", expect![["correct}}"]]),
|
||||
("{correct}}}", expect![["{}}}; correct"]]),
|
||||
("{correct}}}}}", expect![["{}}}}}; correct"]]),
|
||||
("{incorrect}}", expect![["-"]]),
|
||||
("placeholders {} {}", expect![["placeholders {} {}; $1, $2"]]),
|
||||
("mixed {} {2 + 2} {}", expect![["mixed {} {} {}; $1, 2 + 2, $2"]]),
|
||||
(
|
||||
"{SomeStruct { val_a: 0, val_b: 1 }}",
|
||||
expect![["{}; SomeStruct { val_a: 0, val_b: 1 }"]],
|
||||
),
|
||||
("{expr:?} is {2.32f64:.5}", expect![["{:?} is {:.5}; expr, 2.32f64"]]),
|
||||
(
|
||||
"{SomeStruct { val_a: 0, val_b: 1 }:?}",
|
||||
expect![["{:?}; SomeStruct { val_a: 0, val_b: 1 }"]],
|
||||
),
|
||||
("{ 2 + 2 }", expect![["{}; 2 + 2"]]),
|
||||
("{strsim::jaro_winkle(a)}", expect![["{}; strsim::jaro_winkle(a)"]]),
|
||||
("{foo::bar::baz()}", expect![["{}; foo::bar::baz()"]]),
|
||||
("{foo::bar():?}", expect![["{:?}; foo::bar()"]]),
|
||||
];
|
||||
|
||||
for (input, output) in test_vector {
|
||||
check(input, output)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_into_suggestion() {
|
||||
|
@ -302,10 +79,10 @@ mod tests {
|
|||
];
|
||||
|
||||
for (kind, input, output) in test_vector {
|
||||
let mut parser = FormatStrParser::new((*input).to_owned());
|
||||
parser.parse().expect("Parsing must succeed");
|
||||
|
||||
assert_eq!(&parser.to_suggestion(*kind), output);
|
||||
let (parsed_string, exprs) = parse_format_exprs(input).unwrap();
|
||||
let exprs = with_placeholders(exprs);
|
||||
let snippet = format!(r#"{}("{}", {})"#, kind, parsed_string, exprs.join(", "));
|
||||
assert_eq!(&snippet, output);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,6 +38,7 @@ pub mod syntax_helpers {
|
|||
pub mod node_ext;
|
||||
pub mod insert_whitespace_into_node;
|
||||
pub mod format_string;
|
||||
pub mod format_string_exprs;
|
||||
|
||||
pub use parser::LexedStr;
|
||||
}
|
||||
|
|
267
crates/ide-db/src/syntax_helpers/format_string_exprs.rs
Normal file
267
crates/ide-db/src/syntax_helpers/format_string_exprs.rs
Normal file
|
@ -0,0 +1,267 @@
|
|||
//! Tools to work with expressions present in format string literals for the `format_args!` family of macros.
|
||||
//! Primarily meant for assists and completions.
|
||||
|
||||
/// Enum for represenging extraced format string args.
|
||||
/// Can either be extracted expressions (which includes identifiers),
|
||||
/// or placeholders `{}`.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum Arg {
|
||||
Placeholder,
|
||||
Ident(String),
|
||||
Expr(String),
|
||||
}
|
||||
|
||||
/**
|
||||
Add placeholders like `$1` and `$2` in place of [`Arg::Placeholder`],
|
||||
and unwraps the [`Arg::Ident`] and [`Arg::Expr`] enums.
|
||||
```rust
|
||||
# use ide_db::syntax_helpers::format_string_exprs::*;
|
||||
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()])
|
||||
```
|
||||
*/
|
||||
|
||||
pub fn with_placeholders(args: Vec<Arg>) -> Vec<String> {
|
||||
let mut placeholder_id = 1;
|
||||
args.into_iter()
|
||||
.map(move |a| match a {
|
||||
Arg::Expr(s) | Arg::Ident(s) => s,
|
||||
Arg::Placeholder => {
|
||||
let s = format!("${placeholder_id}");
|
||||
placeholder_id += 1;
|
||||
s
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/**
|
||||
Parser for a format-like string. It is more allowing in terms of string contents,
|
||||
as we expect variable placeholders to be filled with expressions.
|
||||
|
||||
Built for completions and assists, and escapes `\` and `$` in output.
|
||||
(See the comments on `get_receiver_text()` for detail.)
|
||||
Splits a format string that may contain expressions
|
||||
like
|
||||
```rust
|
||||
assert_eq!(parse("{ident} {} {expr + 42} ").unwrap(), ("{} {} {}", vec![Arg::Ident("ident"), Arg::Placeholder, Arg::Expr("expr + 42")]));
|
||||
```
|
||||
*/
|
||||
pub fn parse_format_exprs(input: &str) -> Result<(String, Vec<Arg>), ()> {
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum State {
|
||||
NotArg,
|
||||
MaybeArg,
|
||||
Expr,
|
||||
Ident,
|
||||
MaybeIncorrect,
|
||||
FormatOpts,
|
||||
}
|
||||
|
||||
let mut state = State::NotArg;
|
||||
let mut current_expr = String::new();
|
||||
let mut extracted_expressions = Vec::new();
|
||||
let mut output = String::new();
|
||||
|
||||
// Count of open braces inside of an expression.
|
||||
// We assume that user knows what they're doing, thus we treat it like a correct pattern, e.g.
|
||||
// "{MyStruct { val_a: 0, val_b: 1 }}".
|
||||
let mut inexpr_open_count = 0;
|
||||
|
||||
let mut chars = input.chars().peekable();
|
||||
while let Some(chr) = chars.next() {
|
||||
match (state, chr) {
|
||||
(State::NotArg, '{') => {
|
||||
output.push(chr);
|
||||
state = State::MaybeArg;
|
||||
}
|
||||
(State::NotArg, '}') => {
|
||||
output.push(chr);
|
||||
state = State::MaybeIncorrect;
|
||||
}
|
||||
(State::NotArg, _) => {
|
||||
if matches!(chr, '\\' | '$') {
|
||||
output.push('\\');
|
||||
}
|
||||
output.push(chr);
|
||||
}
|
||||
(State::MaybeIncorrect, '}') => {
|
||||
// It's okay, we met "}}".
|
||||
output.push(chr);
|
||||
state = State::NotArg;
|
||||
}
|
||||
(State::MaybeIncorrect, _) => {
|
||||
// Error in the string.
|
||||
return Err(());
|
||||
}
|
||||
// Escaped braces `{{`
|
||||
(State::MaybeArg, '{') => {
|
||||
output.push(chr);
|
||||
state = State::NotArg;
|
||||
}
|
||||
(State::MaybeArg, '}') => {
|
||||
// This is an empty sequence '{}'.
|
||||
output.push(chr);
|
||||
extracted_expressions.push(Arg::Placeholder);
|
||||
state = State::NotArg;
|
||||
}
|
||||
(State::MaybeArg, _) => {
|
||||
if matches!(chr, '\\' | '$') {
|
||||
current_expr.push('\\');
|
||||
}
|
||||
current_expr.push(chr);
|
||||
|
||||
// While Rust uses the unicode sets of XID_start and XID_continue for Identifiers
|
||||
// this is probably the best we can do to avoid a false positive
|
||||
if chr.is_alphabetic() || chr == '_' {
|
||||
state = State::Ident;
|
||||
} else {
|
||||
state = State::Expr;
|
||||
}
|
||||
}
|
||||
(State::Ident | State::Expr, '}') => {
|
||||
if inexpr_open_count == 0 {
|
||||
output.push(chr);
|
||||
|
||||
if matches!(state, State::Expr) {
|
||||
extracted_expressions.push(Arg::Expr(current_expr.trim().into()));
|
||||
} else {
|
||||
extracted_expressions.push(Arg::Ident(current_expr.trim().into()));
|
||||
}
|
||||
|
||||
current_expr = String::new();
|
||||
state = State::NotArg;
|
||||
} else {
|
||||
// We're closing one brace met before inside of the expression.
|
||||
current_expr.push(chr);
|
||||
inexpr_open_count -= 1;
|
||||
}
|
||||
}
|
||||
(State::Ident | State::Expr, ':') if matches!(chars.peek(), Some(':')) => {
|
||||
// path separator
|
||||
state = State::Expr;
|
||||
current_expr.push_str("::");
|
||||
chars.next();
|
||||
}
|
||||
(State::Ident | State::Expr, ':') => {
|
||||
if inexpr_open_count == 0 {
|
||||
// We're outside of braces, thus assume that it's a specifier, like "{Some(value):?}"
|
||||
output.push(chr);
|
||||
|
||||
if matches!(state, State::Expr) {
|
||||
extracted_expressions.push(Arg::Expr(current_expr.trim().into()));
|
||||
} else {
|
||||
extracted_expressions.push(Arg::Ident(current_expr.trim().into()));
|
||||
}
|
||||
|
||||
current_expr = String::new();
|
||||
state = State::FormatOpts;
|
||||
} else {
|
||||
// We're inside of braced expression, assume that it's a struct field name/value delimiter.
|
||||
current_expr.push(chr);
|
||||
}
|
||||
}
|
||||
(State::Ident | State::Expr, '{') => {
|
||||
state = State::Expr;
|
||||
current_expr.push(chr);
|
||||
inexpr_open_count += 1;
|
||||
}
|
||||
(State::Ident | State::Expr, _) => {
|
||||
if !(chr.is_alphanumeric() || chr == '_' || chr == '#') {
|
||||
state = State::Expr;
|
||||
}
|
||||
|
||||
if matches!(chr, '\\' | '$') {
|
||||
current_expr.push('\\');
|
||||
}
|
||||
current_expr.push(chr);
|
||||
}
|
||||
(State::FormatOpts, '}') => {
|
||||
output.push(chr);
|
||||
state = State::NotArg;
|
||||
}
|
||||
(State::FormatOpts, _) => {
|
||||
if matches!(chr, '\\' | '$') {
|
||||
output.push('\\');
|
||||
}
|
||||
output.push(chr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if state != State::NotArg {
|
||||
return Err(());
|
||||
}
|
||||
|
||||
Ok((output, extracted_expressions))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use expect_test::{expect, Expect};
|
||||
|
||||
fn check(input: &str, expect: &Expect) {
|
||||
let (output, exprs) = parse_format_exprs(input).unwrap_or(("-".to_string(), vec![]));
|
||||
let outcome_repr = if !exprs.is_empty() {
|
||||
format!("{}; {}", output, with_placeholders(exprs).join(", "))
|
||||
} else {
|
||||
output
|
||||
};
|
||||
|
||||
expect.assert_eq(&outcome_repr);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_str_parser() {
|
||||
let test_vector = &[
|
||||
("no expressions", expect![["no expressions"]]),
|
||||
(r"no expressions with \$0$1", expect![r"no expressions with \\\$0\$1"]),
|
||||
("{expr} is {2 + 2}", expect![["{} is {}; expr, 2 + 2"]]),
|
||||
("{expr:?}", expect![["{:?}; expr"]]),
|
||||
("{expr:1$}", expect![[r"{:1\$}; expr"]]),
|
||||
("{$0}", expect![[r"{}; \$0"]]),
|
||||
("{malformed", expect![["-"]]),
|
||||
("malformed}", expect![["-"]]),
|
||||
("{{correct", expect![["{{correct"]]),
|
||||
("correct}}", expect![["correct}}"]]),
|
||||
("{correct}}}", expect![["{}}}; correct"]]),
|
||||
("{correct}}}}}", expect![["{}}}}}; correct"]]),
|
||||
("{incorrect}}", expect![["-"]]),
|
||||
("placeholders {} {}", expect![["placeholders {} {}; $1, $2"]]),
|
||||
("mixed {} {2 + 2} {}", expect![["mixed {} {} {}; $1, 2 + 2, $2"]]),
|
||||
(
|
||||
"{SomeStruct { val_a: 0, val_b: 1 }}",
|
||||
expect![["{}; SomeStruct { val_a: 0, val_b: 1 }"]],
|
||||
),
|
||||
("{expr:?} is {2.32f64:.5}", expect![["{:?} is {:.5}; expr, 2.32f64"]]),
|
||||
(
|
||||
"{SomeStruct { val_a: 0, val_b: 1 }:?}",
|
||||
expect![["{:?}; SomeStruct { val_a: 0, val_b: 1 }"]],
|
||||
),
|
||||
("{ 2 + 2 }", expect![["{}; 2 + 2"]]),
|
||||
("{strsim::jaro_winkle(a)}", expect![["{}; strsim::jaro_winkle(a)"]]),
|
||||
("{foo::bar::baz()}", expect![["{}; foo::bar::baz()"]]),
|
||||
("{foo::bar():?}", expect![["{:?}; foo::bar()"]]),
|
||||
];
|
||||
|
||||
for (input, output) in test_vector {
|
||||
check(input, output)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arg_type() {
|
||||
assert_eq!(
|
||||
parse_format_exprs("{_ident} {r#raw_ident} {expr.obj} {name {thing: 42} } {}")
|
||||
.unwrap()
|
||||
.1,
|
||||
vec![
|
||||
Arg::Ident("_ident".to_owned()),
|
||||
Arg::Ident("r#raw_ident".to_owned()),
|
||||
Arg::Expr("expr.obj".to_owned()),
|
||||
Arg::Expr("name {thing: 42}".to_owned()),
|
||||
Arg::Placeholder
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue