1 // Feature: Format String Completion
3 // `"Result {result} is {2 + 2}"` is expanded to the `"Result {} is {}", result, 2 + 2`.
5 // The following postfix snippets are available:
7 // * `format` -> `format!(...)`
8 // * `panic` -> `panic!(...)`
9 // * `println` -> `println!(...)`
11 // ** `logd` -> `log::debug!(...)`
12 // ** `logt` -> `log::trace!(...)`
13 // ** `logi` -> `log::info!(...)`
14 // ** `logw` -> `log::warn!(...)`
15 // ** `loge` -> `log::error!(...)`
17 // image::https://user-images.githubusercontent.com/48062697/113020656-b560f500-917a-11eb-87de-02991f61beb8.gif[]
19 use ide_db
::SnippetCap
;
20 use syntax
::ast
::{self, AstToken}
;
23 completions
::postfix
::build_postfix_snippet_builder
, context
::CompletionContext
, Completions
,
26 /// Mapping ("postfix completion item" => "macro to use")
27 static KINDS
: &[(&str, &str)] = &[
28 ("format", "format!"),
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!"),
39 pub(crate) fn add_format_like_completions(
40 acc
: &mut Completions
,
41 ctx
: &CompletionContext
<'_
>,
42 dot_receiver
: &ast
::Expr
,
44 receiver_text
: &ast
::String
,
46 let input
= match string_literal_contents(receiver_text
) {
47 // It's not a string literal, do not parse input.
52 let postfix_snippet
= match build_postfix_snippet_builder(ctx
, cap
, dot_receiver
) {
56 let mut parser
= FormatStrParser
::new(input
);
58 if parser
.parse().is_ok() {
59 for (label
, macro_name
) in KINDS
{
60 let snippet
= parser
.to_suggestion(macro_name
);
62 postfix_snippet(label
, macro_name
, &snippet
).add_to(acc
);
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());
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.
80 pub(crate) struct FormatStrParser
{
83 extracted_expressions
: Vec
<String
>,
88 #[derive(Debug, Clone, Copy, PartialEq)]
97 impl FormatStrParser
{
98 pub(crate) fn new(input
: String
) -> Self {
101 output
: String
::new(),
102 extracted_expressions
: Vec
::new(),
103 state
: State
::NotExpr
,
108 pub(crate) fn parse(&mut self) -> Result
<(), ()> {
109 let mut current_expr
= String
::new();
111 let mut placeholder_id
= 1;
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;
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
;
126 (State
::NotExpr
, '
}'
) => {
127 self.output
.push(chr
);
128 self.state
= State
::MaybeIncorrect
;
130 (State
::NotExpr
, _
) => {
131 if matches
!(chr
, '
\\'
| '$'
) {
132 self.output
.push('
\\'
);
134 self.output
.push(chr
);
136 (State
::MaybeIncorrect
, '
}'
) => {
137 // It's okay, we met "}}".
138 self.output
.push(chr
);
139 self.state
= State
::NotExpr
;
141 (State
::MaybeIncorrect
, _
) => {
142 // Error in the string.
145 (State
::MaybeExpr
, '
{'
) => {
146 self.output
.push(chr
);
147 self.state
= State
::NotExpr
;
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
));
154 self.state
= State
::NotExpr
;
156 (State
::MaybeExpr
, _
) => {
157 if matches
!(chr
, '
\\'
| '$'
) {
158 current_expr
.push('
\\'
);
160 current_expr
.push(chr
);
161 self.state
= State
::Expr
;
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
;
170 // We're closing one brace met before inside of the expression.
171 current_expr
.push(chr
);
172 inexpr_open_count
-= 1;
175 (State
::Expr
, '
:'
) if chars
.peek().copied() == Some('
:'
) => {
177 current_expr
.push_str("::");
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
;
188 // We're inside of braced expression, assume that it's a struct field name/value delimiter.
189 current_expr
.push(chr
);
192 (State
::Expr
, '
{'
) => {
193 current_expr
.push(chr
);
194 inexpr_open_count
+= 1;
196 (State
::Expr
, _
) => {
197 if matches
!(chr
, '
\\'
| '$'
) {
198 current_expr
.push('
\\'
);
200 current_expr
.push(chr
);
202 (State
::FormatOpts
, '
}'
) => {
203 self.output
.push(chr
);
204 self.state
= State
::NotExpr
;
206 (State
::FormatOpts
, _
) => {
207 if matches
!(chr
, '
\\'
| '$'
) {
208 self.output
.push('
\\'
);
210 self.output
.push(chr
);
215 if self.state
!= State
::NotExpr
{
223 pub(crate) fn to_suggestion(&self, macro_name
: &str) -> String
{
224 assert
!(self.parsed
, "Attempt to get a suggestion from not parsed expression");
226 let expressions_as_string
= self.extracted_expressions
.join(", ");
227 format
!(r
#"{}("{}", {})"#, macro_name, self.output, expressions_as_string)
234 use expect_test
::{expect, Expect}
;
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() {
243 format
!("{}; {}", parser
.output
, parser
.extracted_expressions
.join(", "))
246 // Parsing should fail, expected repr is "-".
250 expect
.assert_eq(&outcome_repr
);
254 fn format_str_parser() {
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"]]),
272 "{SomeStruct { val_a: 0, val_b: 1 }}",
273 expect
![["{}; SomeStruct { val_a: 0, val_b: 1 }"]],
275 ("{expr:?} is {2.32f64:.5}", expect
![["{:?} is {:.5}; expr, 2.32f64"]]),
277 "{SomeStruct { val_a: 0, val_b: 1 }:?}",
278 expect
![["{:?}; SomeStruct { val_a: 0, val_b: 1 }"]],
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()"]]),
286 for (input
, output
) in test_vector
{
292 fn test_into_suggestion() {
294 ("println!", "{}", r
#"println!("{}", $1)"#),
295 ("eprintln!", "{}", r
#"eprintln!("{}", $1)"#),
298 "{} {expr} {} {2 + 2}",
299 r
#"log::info!("{} {} {} {}", $1, expr, $2, 2 + 2)"#,
301 ("format!", "{expr:?}", r
#"format!("{:?}", expr)"#),
304 for (kind
, input
, output
) in test_vector
{
305 let mut parser
= FormatStrParser
::new((*input
).to_owned());
306 parser
.parse().expect("Parsing must succeed");
308 assert_eq
!(&parser
.to_suggestion(*kind
), output
);