aboutsummaryrefslogtreecommitdiff
path: root/crates/ide/src/completion/complete_postfix/format_like.rs
diff options
context:
space:
mode:
authorbors[bot] <26634292+bors[bot]@users.noreply.github.com>2020-10-02 13:13:53 +0100
committerGitHub <[email protected]>2020-10-02 13:13:53 +0100
commit6574a6f448e3315343c8db497b5e68033ed4d603 (patch)
treed4e01a4cefa3d15fb8e393abf3082b2418867fcb /crates/ide/src/completion/complete_postfix/format_like.rs
parentd8e5265309cf92857c996d4f55372e3b431468bf (diff)
parent97f2905dec269891eb81a75cf0d639408a3f7268 (diff)
Merge #5988
5988: Postfix completions for fmt-like string literals r=matklad a=popzxc This pull request adds a bunch of new postfix completions for `format`-like string literls. For example, `"{32} {some_var:?}".println` will expand to `println!("{} {:?}", 32, some_var)`. Postfix completions were added for most common format-like macros: - `println` -> `println!(...)` - `fmt` -> `format!(...)` - `panic` -> `panic!(...)` - `log` macros: + `logi` -> `log::info!(...)` + `logw` -> `log::warn!(...)` + `loge` -> `log::error!(...)` + `logt` -> `log::trace!(...)` + `logd` -> `log::debug!(...)` ![fmt_postfix](https://user-images.githubusercontent.com/12111581/92998650-a048af80-f523-11ea-8fd8-410146de8caa.gif) Co-authored-by: Igor Aleksanov <[email protected]>
Diffstat (limited to 'crates/ide/src/completion/complete_postfix/format_like.rs')
-rw-r--r--crates/ide/src/completion/complete_postfix/format_like.rs277
1 files changed, 277 insertions, 0 deletions
diff --git a/crates/ide/src/completion/complete_postfix/format_like.rs b/crates/ide/src/completion/complete_postfix/format_like.rs
new file mode 100644
index 000000000..0287fc803
--- /dev/null
+++ b/crates/ide/src/completion/complete_postfix/format_like.rs
@@ -0,0 +1,277 @@
1// Feature: Postfix completion for `format`-like strings.
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::completion::{
18 complete_postfix::postfix_snippet, completion_config::SnippetCap,
19 completion_context::CompletionContext, completion_item::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 ("logd", "log::debug!"),
29 ("logt", "log::trace!"),
30 ("logi", "log::info!"),
31 ("logw", "log::warn!"),
32 ("loge", "log::error!"),
33];
34
35pub(super) 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 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 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 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 for chr in self.input.chars() {
111 match (self.state, chr) {
112 (State::NotExpr, '{') => {
113 self.output.push(chr);
114 self.state = State::MaybeExpr;
115 }
116 (State::NotExpr, '}') => {
117 self.output.push(chr);
118 self.state = State::MaybeIncorrect;
119 }
120 (State::NotExpr, _) => {
121 self.output.push(chr);
122 }
123 (State::MaybeIncorrect, '}') => {
124 // It's okay, we met "}}".
125 self.output.push(chr);
126 self.state = State::NotExpr;
127 }
128 (State::MaybeIncorrect, _) => {
129 // Error in the string.
130 return Err(());
131 }
132 (State::MaybeExpr, '{') => {
133 self.output.push(chr);
134 self.state = State::NotExpr;
135 }
136 (State::MaybeExpr, '}') => {
137 // This is an empty sequence '{}'. Replace it with placeholder.
138 self.output.push(chr);
139 self.extracted_expressions.push(format!("${}", placeholder_id));
140 placeholder_id += 1;
141 self.state = State::NotExpr;
142 }
143 (State::MaybeExpr, _) => {
144 current_expr.push(chr);
145 self.state = State::Expr;
146 }
147 (State::Expr, '}') => {
148 if inexpr_open_count == 0 {
149 self.output.push(chr);
150 self.extracted_expressions.push(current_expr.trim().into());
151 current_expr = String::new();
152 self.state = State::NotExpr;
153 } else {
154 // We're closing one brace met before inside of the expression.
155 current_expr.push(chr);
156 inexpr_open_count -= 1;
157 }
158 }
159 (State::Expr, ':') => {
160 if inexpr_open_count == 0 {
161 // We're outside of braces, thus assume that it's a specifier, like "{Some(value):?}"
162 self.output.push(chr);
163 self.extracted_expressions.push(current_expr.trim().into());
164 current_expr = String::new();
165 self.state = State::FormatOpts;
166 } else {
167 // We're inside of braced expression, assume that it's a struct field name/value delimeter.
168 current_expr.push(chr);
169 }
170 }
171 (State::Expr, '{') => {
172 current_expr.push(chr);
173 inexpr_open_count += 1;
174 }
175 (State::Expr, _) => {
176 current_expr.push(chr);
177 }
178 (State::FormatOpts, '}') => {
179 self.output.push(chr);
180 self.state = State::NotExpr;
181 }
182 (State::FormatOpts, _) => {
183 self.output.push(chr);
184 }
185 }
186 }
187
188 if self.state != State::NotExpr {
189 return Err(());
190 }
191
192 self.parsed = true;
193 Ok(())
194 }
195
196 pub fn into_suggestion(&self, macro_name: &str) -> String {
197 assert!(self.parsed, "Attempt to get a suggestion from not parsed expression");
198
199 let expressions_as_string = self.extracted_expressions.join(", ");
200 format!(r#"{}("{}", {})"#, macro_name, self.output, expressions_as_string)
201 }
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207 use expect_test::{expect, Expect};
208
209 fn check(input: &str, expect: &Expect) {
210 let mut parser = FormatStrParser::new((*input).to_owned());
211 let outcome_repr = if parser.parse().is_ok() {
212 // Parsing should be OK, expected repr is "string; expr_1, expr_2".
213 if parser.extracted_expressions.is_empty() {
214 parser.output
215 } else {
216 format!("{}; {}", parser.output, parser.extracted_expressions.join(", "))
217 }
218 } else {
219 // Parsing should fail, expected repr is "-".
220 "-".to_owned()
221 };
222
223 expect.assert_eq(&outcome_repr);
224 }
225
226 #[test]
227 fn format_str_parser() {
228 let test_vector = &[
229 ("no expressions", expect![["no expressions"]]),
230 ("{expr} is {2 + 2}", expect![["{} is {}; expr, 2 + 2"]]),
231 ("{expr:?}", expect![["{:?}; expr"]]),
232 ("{malformed", expect![["-"]]),
233 ("malformed}", expect![["-"]]),
234 ("{{correct", expect![["{{correct"]]),
235 ("correct}}", expect![["correct}}"]]),
236 ("{correct}}}", expect![["{}}}; correct"]]),
237 ("{correct}}}}}", expect![["{}}}}}; correct"]]),
238 ("{incorrect}}", expect![["-"]]),
239 ("placeholders {} {}", expect![["placeholders {} {}; $1, $2"]]),
240 ("mixed {} {2 + 2} {}", expect![["mixed {} {} {}; $1, 2 + 2, $2"]]),
241 (
242 "{SomeStruct { val_a: 0, val_b: 1 }}",
243 expect![["{}; SomeStruct { val_a: 0, val_b: 1 }"]],
244 ),
245 ("{expr:?} is {2.32f64:.5}", expect![["{:?} is {:.5}; expr, 2.32f64"]]),
246 (
247 "{SomeStruct { val_a: 0, val_b: 1 }:?}",
248 expect![["{:?}; SomeStruct { val_a: 0, val_b: 1 }"]],
249 ),
250 ("{ 2 + 2 }", expect![["{}; 2 + 2"]]),
251 ];
252
253 for (input, output) in test_vector {
254 check(input, output)
255 }
256 }
257
258 #[test]
259 fn test_into_suggestion() {
260 let test_vector = &[
261 ("println!", "{}", r#"println!("{}", $1)"#),
262 (
263 "log::info!",
264 "{} {expr} {} {2 + 2}",
265 r#"log::info!("{} {} {} {}", $1, expr, $2, 2 + 2)"#,
266 ),
267 ("format!", "{expr:?}", r#"format!("{:?}", expr)"#),
268 ];
269
270 for (kind, input, output) in test_vector {
271 let mut parser = FormatStrParser::new((*input).to_owned());
272 parser.parse().expect("Parsing must succeed");
273
274 assert_eq!(&parser.into_suggestion(*kind), output);
275 }
276 }
277}