]>
Commit | Line | Data |
---|---|---|
064997fb FG |
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::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 | ||
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. | |
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 { | |
98 | 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 | ||
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 | // We need to escape '\' and '$'. See the comments on `get_receiver_text()` for detail. | |
119 | let mut chars = self.input.chars().peekable(); | |
120 | while let Some(chr) = chars.next() { | |
121 | match (self.state, chr) { | |
122 | (State::NotExpr, '{') => { | |
123 | self.output.push(chr); | |
124 | self.state = State::MaybeExpr; | |
125 | } | |
126 | (State::NotExpr, '}') => { | |
127 | self.output.push(chr); | |
128 | self.state = State::MaybeIncorrect; | |
129 | } | |
130 | (State::NotExpr, _) => { | |
131 | if matches!(chr, '\\' | '$') { | |
132 | self.output.push('\\'); | |
133 | } | |
134 | self.output.push(chr); | |
135 | } | |
136 | (State::MaybeIncorrect, '}') => { | |
137 | // It's okay, we met "}}". | |
138 | self.output.push(chr); | |
139 | self.state = State::NotExpr; | |
140 | } | |
141 | (State::MaybeIncorrect, _) => { | |
142 | // Error in the string. | |
143 | return Err(()); | |
144 | } | |
145 | (State::MaybeExpr, '{') => { | |
146 | self.output.push(chr); | |
147 | self.state = State::NotExpr; | |
148 | } | |
149 | (State::MaybeExpr, '}') => { | |
150 | // This is an empty sequence '{}'. Replace it with placeholder. | |
151 | self.output.push(chr); | |
152 | self.extracted_expressions.push(format!("${}", placeholder_id)); | |
153 | placeholder_id += 1; | |
154 | self.state = State::NotExpr; | |
155 | } | |
156 | (State::MaybeExpr, _) => { | |
157 | if matches!(chr, '\\' | '$') { | |
158 | current_expr.push('\\'); | |
159 | } | |
160 | current_expr.push(chr); | |
161 | self.state = State::Expr; | |
162 | } | |
163 | (State::Expr, '}') => { | |
164 | if inexpr_open_count == 0 { | |
165 | self.output.push(chr); | |
166 | self.extracted_expressions.push(current_expr.trim().into()); | |
167 | current_expr = String::new(); | |
168 | self.state = State::NotExpr; | |
169 | } else { | |
170 | // We're closing one brace met before inside of the expression. | |
171 | current_expr.push(chr); | |
172 | inexpr_open_count -= 1; | |
173 | } | |
174 | } | |
175 | (State::Expr, ':') if chars.peek().copied() == Some(':') => { | |
f2b60f7d | 176 | // path separator |
064997fb FG |
177 | current_expr.push_str("::"); |
178 | chars.next(); | |
179 | } | |
180 | (State::Expr, ':') => { | |
181 | if inexpr_open_count == 0 { | |
182 | // We're outside of braces, thus assume that it's a specifier, like "{Some(value):?}" | |
183 | self.output.push(chr); | |
184 | self.extracted_expressions.push(current_expr.trim().into()); | |
185 | current_expr = String::new(); | |
186 | self.state = State::FormatOpts; | |
187 | } else { | |
f2b60f7d | 188 | // We're inside of braced expression, assume that it's a struct field name/value delimiter. |
064997fb FG |
189 | current_expr.push(chr); |
190 | } | |
191 | } | |
192 | (State::Expr, '{') => { | |
193 | current_expr.push(chr); | |
194 | inexpr_open_count += 1; | |
195 | } | |
196 | (State::Expr, _) => { | |
197 | if matches!(chr, '\\' | '$') { | |
198 | current_expr.push('\\'); | |
199 | } | |
200 | current_expr.push(chr); | |
201 | } | |
202 | (State::FormatOpts, '}') => { | |
203 | self.output.push(chr); | |
204 | self.state = State::NotExpr; | |
205 | } | |
206 | (State::FormatOpts, _) => { | |
207 | if matches!(chr, '\\' | '$') { | |
208 | self.output.push('\\'); | |
209 | } | |
210 | self.output.push(chr); | |
211 | } | |
212 | } | |
213 | } | |
214 | ||
215 | if self.state != State::NotExpr { | |
216 | return Err(()); | |
217 | } | |
218 | ||
219 | self.parsed = true; | |
220 | Ok(()) | |
221 | } | |
222 | ||
223 | pub(crate) fn to_suggestion(&self, macro_name: &str) -> String { | |
224 | assert!(self.parsed, "Attempt to get a suggestion from not parsed expression"); | |
225 | ||
226 | let expressions_as_string = self.extracted_expressions.join(", "); | |
227 | format!(r#"{}("{}", {})"#, macro_name, self.output, expressions_as_string) | |
228 | } | |
229 | } | |
230 | ||
231 | #[cfg(test)] | |
232 | mod tests { | |
233 | use super::*; | |
234 | use expect_test::{expect, Expect}; | |
235 | ||
236 | fn check(input: &str, expect: &Expect) { | |
237 | let mut parser = FormatStrParser::new((*input).to_owned()); | |
238 | let outcome_repr = if parser.parse().is_ok() { | |
239 | // Parsing should be OK, expected repr is "string; expr_1, expr_2". | |
240 | if parser.extracted_expressions.is_empty() { | |
241 | parser.output | |
242 | } else { | |
243 | format!("{}; {}", parser.output, parser.extracted_expressions.join(", ")) | |
244 | } | |
245 | } else { | |
246 | // Parsing should fail, expected repr is "-". | |
247 | "-".to_owned() | |
248 | }; | |
249 | ||
250 | expect.assert_eq(&outcome_repr); | |
251 | } | |
252 | ||
253 | #[test] | |
254 | fn format_str_parser() { | |
255 | let test_vector = &[ | |
256 | ("no expressions", expect![["no expressions"]]), | |
257 | (r"no expressions with \$0$1", expect![r"no expressions with \\\$0\$1"]), | |
258 | ("{expr} is {2 + 2}", expect![["{} is {}; expr, 2 + 2"]]), | |
259 | ("{expr:?}", expect![["{:?}; expr"]]), | |
260 | ("{expr:1$}", expect![[r"{:1\$}; expr"]]), | |
261 | ("{$0}", expect![[r"{}; \$0"]]), | |
262 | ("{malformed", expect![["-"]]), | |
263 | ("malformed}", expect![["-"]]), | |
264 | ("{{correct", expect![["{{correct"]]), | |
265 | ("correct}}", expect![["correct}}"]]), | |
266 | ("{correct}}}", expect![["{}}}; correct"]]), | |
267 | ("{correct}}}}}", expect![["{}}}}}; correct"]]), | |
268 | ("{incorrect}}", expect![["-"]]), | |
269 | ("placeholders {} {}", expect![["placeholders {} {}; $1, $2"]]), | |
270 | ("mixed {} {2 + 2} {}", expect![["mixed {} {} {}; $1, 2 + 2, $2"]]), | |
271 | ( | |
272 | "{SomeStruct { val_a: 0, val_b: 1 }}", | |
273 | expect![["{}; SomeStruct { val_a: 0, val_b: 1 }"]], | |
274 | ), | |
275 | ("{expr:?} is {2.32f64:.5}", expect![["{:?} is {:.5}; expr, 2.32f64"]]), | |
276 | ( | |
277 | "{SomeStruct { val_a: 0, val_b: 1 }:?}", | |
278 | expect![["{:?}; SomeStruct { val_a: 0, val_b: 1 }"]], | |
279 | ), | |
280 | ("{ 2 + 2 }", expect![["{}; 2 + 2"]]), | |
281 | ("{strsim::jaro_winkle(a)}", expect![["{}; strsim::jaro_winkle(a)"]]), | |
282 | ("{foo::bar::baz()}", expect![["{}; foo::bar::baz()"]]), | |
283 | ("{foo::bar():?}", expect![["{:?}; foo::bar()"]]), | |
284 | ]; | |
285 | ||
286 | for (input, output) in test_vector { | |
287 | check(input, output) | |
288 | } | |
289 | } | |
290 | ||
291 | #[test] | |
292 | fn test_into_suggestion() { | |
293 | let test_vector = &[ | |
294 | ("println!", "{}", r#"println!("{}", $1)"#), | |
295 | ("eprintln!", "{}", r#"eprintln!("{}", $1)"#), | |
296 | ( | |
297 | "log::info!", | |
298 | "{} {expr} {} {2 + 2}", | |
299 | r#"log::info!("{} {} {} {}", $1, expr, $2, 2 + 2)"#, | |
300 | ), | |
301 | ("format!", "{expr:?}", r#"format!("{:?}", expr)"#), | |
302 | ]; | |
303 | ||
304 | for (kind, input, output) in test_vector { | |
305 | let mut parser = FormatStrParser::new((*input).to_owned()); | |
306 | parser.parse().expect("Parsing must succeed"); | |
307 | ||
308 | assert_eq!(&parser.to_suggestion(*kind), output); | |
309 | } | |
310 | } | |
311 | } |