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