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