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 // image::https://user-images.githubusercontent.com/48062697/113020656-b560f500-917a-11eb-87de-02991f61beb8.gif[]
18 
19 use ide_db::helpers::SnippetCap;
20 use syntax::ast::{self, AstToken};
21 
22 use crate::{
23     completions::postfix::build_postfix_snippet_builder, context::CompletionContext, Completions,
24 };
25 
26 /// Mapping ("postfix completion item" => "macro to use")
27 static KINDS: &[(&str, &str)] = &[
28     ("format", "format!"),
29     ("panic", "panic!"),
30     ("println", "println!"),
31     ("eprintln", "eprintln!"),
32     ("logd", "log::debug!"),
33     ("logt", "log::trace!"),
34     ("logi", "log::info!"),
35     ("logw", "log::warn!"),
36     ("loge", "log::error!"),
37 ];
38 
add_format_like_completions( acc: &mut Completions, ctx: &CompletionContext, dot_receiver: &ast::Expr, cap: SnippetCap, receiver_text: &ast::String, )39 pub(crate) fn add_format_like_completions(
40     acc: &mut Completions,
41     ctx: &CompletionContext,
42     dot_receiver: &ast::Expr,
43     cap: SnippetCap,
44     receiver_text: &ast::String,
45 ) {
46     let input = match string_literal_contents(receiver_text) {
47         // It's not a string literal, do not parse input.
48         Some(input) => input,
49         None => return,
50     };
51 
52     let postfix_snippet = match build_postfix_snippet_builder(ctx, cap, dot_receiver) {
53         Some(it) => it,
54         None => return,
55     };
56     let mut parser = FormatStrParser::new(input);
57 
58     if parser.parse().is_ok() {
59         for (label, macro_name) in KINDS {
60             let snippet = parser.to_suggestion(macro_name);
61 
62             postfix_snippet(label, macro_name, &snippet).add_to(acc);
63         }
64     }
65 }
66 
67 /// Checks whether provided item is a string literal.
string_literal_contents(item: &ast::String) -> Option<String>68 fn string_literal_contents(item: &ast::String) -> Option<String> {
69     let item = item.text();
70     if item.len() >= 2 && item.starts_with('\"') && item.ends_with('\"') {
71         return Some(item[1..item.len() - 1].to_owned());
72     }
73 
74     None
75 }
76 
77 /// Parser for a format-like string. It is more allowing in terms of string contents,
78 /// as we expect variable placeholders to be filled with expressions.
79 #[derive(Debug)]
80 pub(crate) struct FormatStrParser {
81     input: String,
82     output: String,
83     extracted_expressions: Vec<String>,
84     state: State,
85     parsed: bool,
86 }
87 
88 #[derive(Debug, Clone, Copy, PartialEq)]
89 enum State {
90     NotExpr,
91     MaybeExpr,
92     Expr,
93     MaybeIncorrect,
94     FormatOpts,
95 }
96 
97 impl FormatStrParser {
new(input: String) -> Self98     pub(crate) fn new(input: String) -> Self {
99         Self {
100             input,
101             output: String::new(),
102             extracted_expressions: Vec::new(),
103             state: State::NotExpr,
104             parsed: false,
105         }
106     }
107 
parse(&mut self) -> Result<(), ()>108     pub(crate) fn parse(&mut self) -> Result<(), ()> {
109         let mut current_expr = String::new();
110 
111         let mut placeholder_id = 1;
112 
113         // Count of open braces inside of an expression.
114         // We assume that user knows what they're doing, thus we treat it like a correct pattern, e.g.
115         // "{MyStruct { val_a: 0, val_b: 1 }}".
116         let mut inexpr_open_count = 0;
117 
118         let mut chars = self.input.chars().peekable();
119         while let Some(chr) = chars.next() {
120             match (self.state, chr) {
121                 (State::NotExpr, '{') => {
122                     self.output.push(chr);
123                     self.state = State::MaybeExpr;
124                 }
125                 (State::NotExpr, '}') => {
126                     self.output.push(chr);
127                     self.state = State::MaybeIncorrect;
128                 }
129                 (State::NotExpr, _) => {
130                     self.output.push(chr);
131                 }
132                 (State::MaybeIncorrect, '}') => {
133                     // It's okay, we met "}}".
134                     self.output.push(chr);
135                     self.state = State::NotExpr;
136                 }
137                 (State::MaybeIncorrect, _) => {
138                     // Error in the string.
139                     return Err(());
140                 }
141                 (State::MaybeExpr, '{') => {
142                     self.output.push(chr);
143                     self.state = State::NotExpr;
144                 }
145                 (State::MaybeExpr, '}') => {
146                     // This is an empty sequence '{}'. Replace it with placeholder.
147                     self.output.push(chr);
148                     self.extracted_expressions.push(format!("${}", placeholder_id));
149                     placeholder_id += 1;
150                     self.state = State::NotExpr;
151                 }
152                 (State::MaybeExpr, _) => {
153                     current_expr.push(chr);
154                     self.state = State::Expr;
155                 }
156                 (State::Expr, '}') => {
157                     if inexpr_open_count == 0 {
158                         self.output.push(chr);
159                         self.extracted_expressions.push(current_expr.trim().into());
160                         current_expr = String::new();
161                         self.state = State::NotExpr;
162                     } else {
163                         // We're closing one brace met before inside of the expression.
164                         current_expr.push(chr);
165                         inexpr_open_count -= 1;
166                     }
167                 }
168                 (State::Expr, ':') if chars.peek().copied() == Some(':') => {
169                     // path seperator
170                     current_expr.push_str("::");
171                     chars.next();
172                 }
173                 (State::Expr, ':') => {
174                     if inexpr_open_count == 0 {
175                         // We're outside of braces, thus assume that it's a specifier, like "{Some(value):?}"
176                         self.output.push(chr);
177                         self.extracted_expressions.push(current_expr.trim().into());
178                         current_expr = String::new();
179                         self.state = State::FormatOpts;
180                     } else {
181                         // We're inside of braced expression, assume that it's a struct field name/value delimeter.
182                         current_expr.push(chr);
183                     }
184                 }
185                 (State::Expr, '{') => {
186                     current_expr.push(chr);
187                     inexpr_open_count += 1;
188                 }
189                 (State::Expr, _) => {
190                     current_expr.push(chr);
191                 }
192                 (State::FormatOpts, '}') => {
193                     self.output.push(chr);
194                     self.state = State::NotExpr;
195                 }
196                 (State::FormatOpts, _) => {
197                     self.output.push(chr);
198                 }
199             }
200         }
201 
202         if self.state != State::NotExpr {
203             return Err(());
204         }
205 
206         self.parsed = true;
207         Ok(())
208     }
209 
to_suggestion(&self, macro_name: &str) -> String210     pub(crate) fn to_suggestion(&self, macro_name: &str) -> String {
211         assert!(self.parsed, "Attempt to get a suggestion from not parsed expression");
212 
213         let expressions_as_string = self.extracted_expressions.join(", ");
214         format!(r#"{}("{}", {})"#, macro_name, self.output, expressions_as_string)
215     }
216 }
217 
218 #[cfg(test)]
219 mod tests {
220     use super::*;
221     use expect_test::{expect, Expect};
222 
check(input: &str, expect: &Expect)223     fn check(input: &str, expect: &Expect) {
224         let mut parser = FormatStrParser::new((*input).to_owned());
225         let outcome_repr = if parser.parse().is_ok() {
226             // Parsing should be OK, expected repr is "string; expr_1, expr_2".
227             if parser.extracted_expressions.is_empty() {
228                 parser.output
229             } else {
230                 format!("{}; {}", parser.output, parser.extracted_expressions.join(", "))
231             }
232         } else {
233             // Parsing should fail, expected repr is "-".
234             "-".to_owned()
235         };
236 
237         expect.assert_eq(&outcome_repr);
238     }
239 
240     #[test]
format_str_parser()241     fn format_str_parser() {
242         let test_vector = &[
243             ("no expressions", expect![["no expressions"]]),
244             ("{expr} is {2 + 2}", expect![["{} is {}; expr, 2 + 2"]]),
245             ("{expr:?}", expect![["{:?}; expr"]]),
246             ("{malformed", expect![["-"]]),
247             ("malformed}", expect![["-"]]),
248             ("{{correct", expect![["{{correct"]]),
249             ("correct}}", expect![["correct}}"]]),
250             ("{correct}}}", expect![["{}}}; correct"]]),
251             ("{correct}}}}}", expect![["{}}}}}; correct"]]),
252             ("{incorrect}}", expect![["-"]]),
253             ("placeholders {} {}", expect![["placeholders {} {}; $1, $2"]]),
254             ("mixed {} {2 + 2} {}", expect![["mixed {} {} {}; $1, 2 + 2, $2"]]),
255             (
256                 "{SomeStruct { val_a: 0, val_b: 1 }}",
257                 expect![["{}; SomeStruct { val_a: 0, val_b: 1 }"]],
258             ),
259             ("{expr:?} is {2.32f64:.5}", expect![["{:?} is {:.5}; expr, 2.32f64"]]),
260             (
261                 "{SomeStruct { val_a: 0, val_b: 1 }:?}",
262                 expect![["{:?}; SomeStruct { val_a: 0, val_b: 1 }"]],
263             ),
264             ("{     2 + 2        }", expect![["{}; 2 + 2"]]),
265             ("{strsim::jaro_winkle(a)}", expect![["{}; strsim::jaro_winkle(a)"]]),
266             ("{foo::bar::baz()}", expect![["{}; foo::bar::baz()"]]),
267             ("{foo::bar():?}", expect![["{:?}; foo::bar()"]]),
268         ];
269 
270         for (input, output) in test_vector {
271             check(input, output)
272         }
273     }
274 
275     #[test]
test_into_suggestion()276     fn test_into_suggestion() {
277         let test_vector = &[
278             ("println!", "{}", r#"println!("{}", $1)"#),
279             ("eprintln!", "{}", r#"eprintln!("{}", $1)"#),
280             (
281                 "log::info!",
282                 "{} {expr} {} {2 + 2}",
283                 r#"log::info!("{} {} {} {}", $1, expr, $2, 2 + 2)"#,
284             ),
285             ("format!", "{expr:?}", r#"format!("{:?}", expr)"#),
286         ];
287 
288         for (kind, input, output) in test_vector {
289             let mut parser = FormatStrParser::new((*input).to_owned());
290             parser.parse().expect("Parsing must succeed");
291 
292             assert_eq!(&parser.to_suggestion(*kind), output);
293         }
294     }
295 }
296