]> git.proxmox.com Git - rustc.git/blame - src/tools/rust-analyzer/crates/ide-completion/src/completions/postfix/format_like.rs
New upstream version 1.65.0+dfsg1
[rustc.git] / src / tools / rust-analyzer / crates / ide-completion / src / completions / postfix / format_like.rs
CommitLineData
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
19use ide_db::SnippetCap;
20use syntax::ast::{self, AstToken};
21
22use crate::{
23 completions::postfix::build_postfix_snippet_builder, context::CompletionContext, Completions,
24};
25
26/// Mapping ("postfix completion item" => "macro to use")
27static 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
39pub(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.
68fn 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)]
80pub(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)]
89enum State {
90 NotExpr,
91 MaybeExpr,
92 Expr,
93 MaybeIncorrect,
94 FormatOpts,
95}
96
97impl 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)]
232mod 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}