use crate::{AssistContext, Assists, utils}; use ide_db::{ assists::{AssistId, AssistKind}, syntax_helpers::format_string_exprs::{Arg, parse_format_exprs}, }; use itertools::Itertools; use syntax::{ AstNode, AstToken, NodeOrToken, SyntaxKind::WHITESPACE, SyntaxToken, T, ast::{self, TokenTree, make, syntax_factory::SyntaxFactory}, }; // Assist: extract_expressions_from_format_string // // Move an expression out of a format string. // // ``` // # //- minicore: fmt // fn main() { // print!("{var} {x + 1}$0"); // } // ``` // -> // ``` // fn main() { // print!("{var} {}"$0, x + 1); // } // ``` pub(crate) fn extract_expressions_from_format_string( acc: &mut Assists, ctx: &AssistContext<'_>, ) -> Option<()> { let fmt_string = ctx.find_token_at_offset::()?; let tt = fmt_string.syntax().parent().and_then(ast::TokenTree::cast)?; let tt_delimiter = tt.left_delimiter_token()?.kind(); let _ = ctx.sema.as_format_args_parts(&fmt_string)?; let (new_fmt, extracted_args) = parse_format_exprs(fmt_string.text()).ok()?; if extracted_args.is_empty() { return None; } acc.add( AssistId( "extract_expressions_from_format_string", // 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 }, None, ), "Extract format expressions", tt.syntax().text_range(), |edit| { // Extract existing arguments in macro let mut raw_tokens = tt.token_trees_and_tokens().skip(1).collect_vec(); let format_string_index = format_str_index(&raw_tokens, &fmt_string); let tokens = raw_tokens.split_off(format_string_index); let existing_args = if let [ NodeOrToken::Token(_format_string), _args_start_comma, tokens @ .., NodeOrToken::Token(_end_bracket), ] = tokens.as_slice() { let args = tokens .split(|it| matches!(it, NodeOrToken::Token(t) if t.kind() == T![,])) .map(|arg| { // Strip off leading and trailing whitespace tokens let arg = match arg.split_first() { Some((NodeOrToken::Token(t), rest)) if t.kind() == WHITESPACE => rest, _ => arg, }; match arg.split_last() { Some((NodeOrToken::Token(t), rest)) if t.kind() == WHITESPACE => rest, _ => arg, } }); args.collect() } else { vec![] }; // Start building the new args let mut existing_args = existing_args.into_iter(); let mut new_tt_bits = raw_tokens; let mut placeholder_indexes = vec![]; new_tt_bits.push(NodeOrToken::Token(make::tokens::literal(&new_fmt))); for arg in extracted_args { if matches!(arg, Arg::Expr(_) | Arg::Placeholder) { // insert ", " before each arg new_tt_bits.extend_from_slice(&[ NodeOrToken::Token(make::token(T![,])), NodeOrToken::Token(make::tokens::single_space()), ]); } match arg { Arg::Expr(s) => { // insert arg let expr = ast::Expr::parse(&s, ctx.edition()).syntax_node(); let mut expr_tt = utils::tt_from_syntax(expr); new_tt_bits.append(&mut expr_tt); } Arg::Placeholder => { // try matching with existing argument match existing_args.next() { Some(arg) => { new_tt_bits.extend_from_slice(arg); } None => { placeholder_indexes.push(new_tt_bits.len()); new_tt_bits.push(NodeOrToken::Token(make::token(T![_]))); } } } Arg::Ident(_s) => (), } } // Insert new args let make = SyntaxFactory::with_mappings(); let new_tt = make.token_tree(tt_delimiter, new_tt_bits); let mut editor = edit.make_editor(tt.syntax()); editor.replace(tt.syntax(), new_tt.syntax()); if let Some(cap) = ctx.config.snippet_cap { // Add placeholder snippets over placeholder args for pos in placeholder_indexes { // Skip the opening delimiter let Some(NodeOrToken::Token(placeholder)) = new_tt.token_trees_and_tokens().skip(1).nth(pos) else { continue; }; if stdx::always!(placeholder.kind() == T![_]) { let annotation = edit.make_placeholder_snippet(cap); editor.add_annotation(placeholder, annotation); } } // Add the final tabstop after the format literal if let Some(NodeOrToken::Token(literal)) = new_tt.token_trees_and_tokens().nth(1 + format_string_index) { let annotation = edit.make_tabstop_after(cap); editor.add_annotation(literal, annotation); } } editor.add_mappings(make.finish_with_mappings()); edit.add_file_edits(ctx.vfs_file_id(), editor); }, ); Some(()) } fn format_str_index( raw_tokens: &[NodeOrToken], fmt_string: &ast::String, ) -> usize { let fmt_string = fmt_string.syntax(); raw_tokens .iter() .position(|tt| tt.as_token().is_some_and(|tt| tt == fmt_string)) .unwrap_or_default() } #[cfg(test)] mod tests { use super::*; use crate::tests::{check_assist, check_assist_no_snippet_cap}; #[test] fn multiple_middle_arg() { check_assist( extract_expressions_from_format_string, r#" //- minicore: fmt fn main() { print!("{} {x + 1:b} {}$0", y + 2, 2); } "#, r#" fn main() { print!("{} {:b} {}"$0, y + 2, x + 1, 2); } "#, ); } #[test] fn multiple_middle_arg_on_write() { check_assist( extract_expressions_from_format_string, r#" //- minicore: write fn main() { write!(writer(), "{} {x + 1:b} {}$0", y + 2, 2); } "#, r#" fn main() { write!(writer(), "{} {:b} {}"$0, y + 2, x + 1, 2); } "#, ); } #[test] fn single_arg() { check_assist( extract_expressions_from_format_string, r#" //- minicore: fmt fn main() { print!("{obj.value:b}$0",); } "#, r#" fn main() { print!("{:b}"$0, obj.value); } "#, ); } #[test] fn multiple_middle_placeholders_arg() { check_assist( extract_expressions_from_format_string, r#" //- minicore: fmt fn main() { print!("{} {x + 1:b} {} {}$0", y + 2, 2); } "#, r#" fn main() { print!("{} {:b} {} {}"$0, y + 2, x + 1, 2, ${1:_}); } "#, ); } #[test] fn multiple_trailing_args() { check_assist( extract_expressions_from_format_string, r#" //- minicore: fmt fn main() { print!("{:b} {x + 1:b} {Struct(1, 2)}$0", 1); } "#, r#" fn main() { print!("{:b} {:b} {}"$0, 1, x + 1, Struct(1, 2)); } "#, ); } #[test] fn improper_commas() { check_assist( extract_expressions_from_format_string, r#" //- minicore: fmt fn main() { print!("{} {x + 1:b} {Struct(1, 2)}$0", 1,); } "#, r#" fn main() { print!("{} {:b} {}"$0, 1, x + 1, Struct(1, 2)); } "#, ); } #[test] fn nested_tt() { check_assist( extract_expressions_from_format_string, r#" //- minicore: fmt fn main() { print!("My name is {} {x$0 + x}", stringify!(Paperino)) } "#, r#" fn main() { print!("My name is {} {}"$0, stringify!(Paperino), x + x) } "#, ); } #[test] fn extract_only_expressions() { check_assist( extract_expressions_from_format_string, r#" //- minicore: fmt fn main() { let var = 1 + 1; print!("foobar {var} {var:?} {x$0 + x}") } "#, r#" fn main() { let var = 1 + 1; print!("foobar {var} {var:?} {}"$0, x + x) } "#, ); } #[test] fn escaped_literals() { check_assist( extract_expressions_from_format_string, r#" //- minicore: fmt fn main() { print!("\n$ {x + 1}$0"); } "#, r#" fn main() { print!("\n$ {}"$0, x + 1); } "#, ); } #[test] fn without_snippets() { check_assist_no_snippet_cap( extract_expressions_from_format_string, r#" //- minicore: fmt fn main() { print!("{} {x + 1:b} {} {}$0", y + 2, 2); } "#, r#" fn main() { print!("{} {:b} {} {}", y + 2, x + 1, 2, _); } "#, ); } }