about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--crates/ide-assists/src/handlers/move_format_string_arg.rs209
-rw-r--r--crates/ide-assists/src/lib.rs1
-rw-r--r--crates/ide-assists/src/tests/generated.rs17
-rw-r--r--crates/ide-completion/src/completions/postfix/format_like.rs242
-rw-r--r--crates/ide-db/src/lib.rs1
-rw-r--r--crates/ide-db/src/syntax_helpers/format_string_exprs.rs227
6 files changed, 463 insertions, 234 deletions
diff --git a/crates/ide-assists/src/handlers/move_format_string_arg.rs b/crates/ide-assists/src/handlers/move_format_string_arg.rs
new file mode 100644
index 00000000000..696fd50b5cb
--- /dev/null
+++ b/crates/ide-assists/src/handlers/move_format_string_arg.rs
@@ -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));
+}
+"#),
+        );
+    }
+
+}
diff --git a/crates/ide-assists/src/lib.rs b/crates/ide-assists/src/lib.rs
index e52544db5f5..f881be9cf6d 100644
--- a/crates/ide-assists/src/lib.rs
+++ b/crates/ide-assists/src/lib.rs
@@ -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;
diff --git a/crates/ide-assists/src/tests/generated.rs b/crates/ide-assists/src/tests/generated.rs
index 227e2300f92..fa88abd6c55 100644
--- a/crates/ide-assists/src/tests/generated.rs
+++ b/crates/ide-assists/src/tests/generated.rs
@@ -1592,6 +1592,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(
         "move_from_mod_rs",
diff --git a/crates/ide-completion/src/completions/postfix/format_like.rs b/crates/ide-completion/src/completions/postfix/format_like.rs
index b273a4cb53b..89bfdac74d2 100644
--- a/crates/ide-completion/src/completions/postfix/format_like.rs
+++ b/crates/ide-completion/src/completions/postfix/format_like.rs
@@ -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);
         }
     }
 }
diff --git a/crates/ide-db/src/lib.rs b/crates/ide-db/src/lib.rs
index 1ec62a8425a..e0bc0f89f0a 100644
--- a/crates/ide-db/src/lib.rs
+++ b/crates/ide-db/src/lib.rs
@@ -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;
 }
diff --git a/crates/ide-db/src/syntax_helpers/format_string_exprs.rs b/crates/ide-db/src/syntax_helpers/format_string_exprs.rs
new file mode 100644
index 00000000000..b6b2eb268d3
--- /dev/null
+++ b/crates/ide-db/src/syntax_helpers/format_string_exprs.rs
@@ -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)
+        }
+    }
+}