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