This commit is contained in:
Kartavya Vashishtha 2022-09-10 19:06:50 +05:30
parent b7e8b9a6b8
commit 2584d48508
No known key found for this signature in database
GPG key ID: A50012C2324E5DF0
6 changed files with 463 additions and 234 deletions

View file

@ -0,0 +1,209 @@
use ide_db::{syntax_helpers::{format_string::is_format_string, format_string_exprs::{parse_format_exprs, Arg}}, assists::{AssistId, AssistKind}};
use itertools::Itertools;
use syntax::{ast, AstToken, AstNode, NodeOrToken, SyntaxKind::COMMA, TextRange};
// Assist: move_format_string_arg
//
// Move an expression out of a format string.
//
// ```
// fn main() {
// println!("{x + 1}$0");
// }
// ```
// ->
// ```
// fn main() {
// println!("{a}", a$0 = x + 1);
// }
// ```
use crate::{AssistContext, /* AssistId, AssistKind, */ Assists};
pub(crate) fn move_format_string_arg (acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
let t = ctx.find_token_at_offset::<ast::String>()?;
let tt = t.syntax().parent_ancestors().find_map(ast::TokenTree::cast)?;
let expanded_t = ast::String::cast(ctx.sema.descend_into_macros_with_kind_preference(t.syntax().clone()))?;
if !is_format_string(&expanded_t) {
return None;
}
let target = tt.syntax().text_range();
let extracted_args = parse_format_exprs(&t).ok()?;
let str_range = t.syntax().text_range();
let tokens =
tt.token_trees_and_tokens()
.filter_map(NodeOrToken::into_token)
.collect_vec();
acc.add(AssistId("move_format_string_arg", AssistKind::QuickFix), "Extract format args", target, |edit| {
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 to the 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()));
}
let mut existing_args = existing_args.into_iter();
// insert cursor at end of format string
edit.insert(str_range.end(), "$0");
let mut placeholder_idx = 1;
let mut args = String::new();
for (text, extracted_args) in extracted_args {
// remove expr from format string
edit.delete(text);
args.push_str(", ");
match extracted_args {
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;
}
}
}
}
}
edit.insert(str_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));
}
"#),
);
}
}

View file

@ -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;

View file

@ -1591,6 +1591,23 @@ 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#####"
fn main() {
println!("{x + 1}$0");
}
"#####,
r#####"
fn main() {
println!("{a}", a$0 = x + 1);
}
"#####,
)
}
#[test]
fn doctest_move_from_mod_rs() {
check_doc_test(

View file

@ -16,7 +16,7 @@
//
// image::https://user-images.githubusercontent.com/48062697/113020656-b560f500-917a-11eb-87de-02991f61beb8.gif[]
use ide_db::SnippetCap;
use ide_db::{syntax_helpers::format_string_exprs::{parse_format_exprs, add_placeholders}, SnippetCap};
use syntax::ast::{self, AstToken};
use crate::{
@ -43,250 +43,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) {
let exprs = add_placeholders(exprs.map(|e| e.1)).collect_vec();
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 +76,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 = add_placeholders(exprs.map(|e| e.1)).collect_vec();;
let snippet = format!(r#"{}("{}", {})"#, kind, parsed_string, exprs.join(", "));
assert_eq!(&snippet, output);
}
}
}

View file

@ -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;
}

View file

@ -0,0 +1,227 @@
use syntax::{ast, TextRange, AstToken};
#[derive(Debug)]
pub enum Arg {
Placeholder,
Expr(String)
}
/**
Add placeholders like `$1` and `$2` in place of [`Arg::Placeholder`].
```rust
assert_eq!(vec![Arg::Expr("expr"), Arg::Placeholder, Arg::Expr("expr")], vec!["expr", "$1", "expr"])
```
*/
pub fn add_placeholders (args: impl Iterator<Item = Arg>) -> impl Iterator<Item = String> {
let mut placeholder_id = 1;
args.map(move |a|
match a {
Arg::Expr(s) => s,
Arg::Placeholder => {
let s = format!("${placeholder_id}");
placeholder_id += 1;
s
}
}
)
}
/**
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("{expr} {} {expr} ").unwrap(), ("{} {} {}", vec![Arg::Expr("expr"), Arg::Placeholder, Arg::Expr("expr")]));
```
*/
pub fn parse_format_exprs(input: &ast::String) -> Result<Vec<(TextRange, Arg)>, ()> {
#[derive(Debug, Clone, Copy, PartialEq)]
enum State {
NotExpr,
MaybeExpr,
Expr,
MaybeIncorrect,
FormatOpts,
}
let start = input.syntax().text_range().start();
let mut expr_start = start;
let mut current_expr = String::new();
let mut state = State::NotExpr;
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.text().chars().zip(0u32..).peekable();
while let Some((chr, idx )) = chars.next() {
match (state, chr) {
(State::NotExpr, '{') => {
output.push(chr);
state = State::MaybeExpr;
}
(State::NotExpr, '}') => {
output.push(chr);
state = State::MaybeIncorrect;
}
(State::NotExpr, _) => {
if matches!(chr, '\\' | '$') {
output.push('\\');
}
output.push(chr);
}
(State::MaybeIncorrect, '}') => {
// It's okay, we met "}}".
output.push(chr);
state = State::NotExpr;
}
(State::MaybeIncorrect, _) => {
// Error in the string.
return Err(());
}
(State::MaybeExpr, '{') => {
output.push(chr);
state = State::NotExpr;
}
(State::MaybeExpr, '}') => {
// This is an empty sequence '{}'. Replace it with placeholder.
output.push(chr);
extracted_expressions.push((TextRange::empty(expr_start), Arg::Placeholder));
state = State::NotExpr;
}
(State::MaybeExpr, _) => {
if matches!(chr, '\\' | '$') {
current_expr.push('\\');
}
current_expr.push(chr);
expr_start = start.checked_add(idx.into()).ok_or(())?;
state = State::Expr;
}
(State::Expr, '}') => {
if inexpr_open_count == 0 {
output.push(chr);
extracted_expressions.push((TextRange::new(expr_start, start.checked_add(idx.into()).ok_or(())?), Arg::Expr(current_expr.trim().into())));
current_expr = String::new();
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 matches!(chars.peek(), 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):?}"
output.push(chr);
extracted_expressions.push((TextRange::new(expr_start, start.checked_add(idx.into()).ok_or(())?), Arg::Expr(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::Expr, '{') => {
current_expr.push(chr);
inexpr_open_count += 1;
}
(State::Expr, _) => {
if matches!(chr, '\\' | '$') {
current_expr.push('\\');
}
current_expr.push(chr);
}
(State::FormatOpts, '}') => {
output.push(chr);
state = State::NotExpr;
}
(State::FormatOpts, _) => {
if matches!(chr, '\\' | '$') {
output.push('\\');
}
output.push(chr);
}
}
}
if state != State::NotExpr {
return Err(());
}
Ok(extracted_expressions)
}
#[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)
}
}
}