ide_completion/completions/postfix/
format_like.rs

1// Feature: Format String Completion
2//
3// `"Result {result} is {2 + 2}"` is expanded to the `"Result {} is {}", result, 2 + 2`.
4//
5// The following postfix snippets are available:
6//
7// * `format` -> `format!(...)`
8// * `panic` -> `panic!(...)`
9// * `println` -> `println!(...)`
10// * `log`:
11// ** `logd` -> `log::debug!(...)`
12// ** `logt` -> `log::trace!(...)`
13// ** `logi` -> `log::info!(...)`
14// ** `logw` -> `log::warn!(...)`
15// ** `loge` -> `log::error!(...)`
16//
17// ![Format String Completion](https://user-images.githubusercontent.com/48062697/113020656-b560f500-917a-11eb-87de-02991f61beb8.gif)
18
19use ide_db::{
20    SnippetCap,
21    syntax_helpers::format_string_exprs::{Arg, parse_format_exprs, with_placeholders},
22};
23use syntax::{AstToken, ast};
24
25use crate::{
26    Completions,
27    completions::postfix::{build_postfix_snippet_builder, escape_snippet_bits},
28    context::CompletionContext,
29};
30
31/// Mapping ("postfix completion item" => "macro to use")
32static KINDS: &[(&str, &str)] = &[
33    ("format", "format!"),
34    ("panic", "panic!"),
35    ("println", "println!"),
36    ("eprintln", "eprintln!"),
37    ("logd", "log::debug!"),
38    ("logt", "log::trace!"),
39    ("logi", "log::info!"),
40    ("logw", "log::warn!"),
41    ("loge", "log::error!"),
42];
43static SNIPPET_RETURNS_NON_UNIT: &[&str] = &["format"];
44
45pub(crate) fn add_format_like_completions(
46    acc: &mut Completions,
47    ctx: &CompletionContext<'_>,
48    dot_receiver: &ast::Expr,
49    cap: SnippetCap,
50    receiver_text: &ast::String,
51    semi: &str,
52) {
53    let postfix_snippet = match build_postfix_snippet_builder(ctx, cap, dot_receiver) {
54        Some(it) => it,
55        None => return,
56    };
57
58    if let Ok((mut out, mut exprs)) = parse_format_exprs(receiver_text.text()) {
59        // Escape any snippet bits in the out text and any of the exprs.
60        escape_snippet_bits(&mut out);
61        for arg in &mut exprs {
62            if let Arg::Ident(text) | Arg::Expr(text) = arg {
63                escape_snippet_bits(text)
64            }
65        }
66
67        let exprs = with_placeholders(exprs);
68        for (label, macro_name) in KINDS {
69            let semi = if SNIPPET_RETURNS_NON_UNIT.contains(label) { "" } else { semi };
70            let snippet = if exprs.is_empty() {
71                format!(r#"{macro_name}({out}){semi}"#)
72            } else {
73                format!(r#"{}({}, {}){semi}"#, macro_name, out, exprs.join(", "))
74            };
75
76            postfix_snippet(label, macro_name, &snippet).add_to(acc, ctx.db);
77        }
78    }
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84
85    #[test]
86    fn test_into_suggestion() {
87        let test_vector = &[
88            ("println!", "{}", r#"println!("{}", $1)"#),
89            ("eprintln!", "{}", r#"eprintln!("{}", $1)"#),
90            (
91                "log::info!",
92                "{} {ident} {} {2 + 2}",
93                r#"log::info!("{} {ident} {} {}", $1, $2, 2 + 2)"#,
94            ),
95        ];
96
97        for (kind, input, output) in test_vector {
98            let (parsed_string, exprs) = parse_format_exprs(input).unwrap();
99            let exprs = with_placeholders(exprs);
100            let snippet = format!(r#"{kind}("{parsed_string}", {})"#, exprs.join(", "));
101            assert_eq!(&snippet, output);
102        }
103    }
104
105    #[test]
106    fn test_into_suggestion_no_epxrs() {
107        let test_vector = &[
108            ("println!", "{ident}", r#"println!("{ident}")"#),
109            ("format!", "{ident:?}", r#"format!("{ident:?}")"#),
110        ];
111
112        for (kind, input, output) in test_vector {
113            let (parsed_string, _exprs) = parse_format_exprs(input).unwrap();
114            let snippet = format!(r#"{kind}("{parsed_string}")"#);
115            assert_eq!(&snippet, output);
116        }
117    }
118}