Skip to content

Commit 6574a6f

Browse files
bors[bot]popzxc
andauthored
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 <popzxc@yandex.ru>
2 parents d8e5265 + 97f2905 commit 6574a6f

File tree

3 files changed

+338
-2
lines changed

3 files changed

+338
-2
lines changed

crates/ide/src/completion/complete_postfix.rs

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
//! FIXME: write short doc here
2+
3+
mod format_like;
4+
25
use assists::utils::TryEnum;
36
use syntax::{
4-
ast::{self, AstNode},
7+
ast::{self, AstNode, AstToken},
58
TextRange, TextSize,
69
};
710
use text_edit::TextEdit;
811

12+
use self::format_like::add_format_like_completions;
913
use crate::{
1014
completion::{
1115
completion_config::SnippetCap,
@@ -207,6 +211,12 @@ pub(super) fn complete_postfix(acc: &mut Completions, ctx: &CompletionContext) {
207211
&format!("${{1}}({})", receiver_text),
208212
)
209213
.add_to(acc);
214+
215+
if let ast::Expr::Literal(literal) = dot_receiver.clone() {
216+
if let Some(literal_text) = ast::String::cast(literal.token()) {
217+
add_format_like_completions(acc, ctx, &dot_receiver, cap, &literal_text);
218+
}
219+
}
210220
}
211221

212222
fn get_receiver_text(receiver: &ast::Expr, receiver_is_ambiguous_float_literal: bool) -> String {
@@ -392,4 +402,53 @@ fn main() {
392402
check_edit("dbg", r#"fn main() { &&42.<|> }"#, r#"fn main() { dbg!(&&42) }"#);
393403
check_edit("refm", r#"fn main() { &&42.<|> }"#, r#"fn main() { &&&mut 42 }"#);
394404
}
405+
406+
#[test]
407+
fn postfix_completion_for_format_like_strings() {
408+
check_edit(
409+
"fmt",
410+
r#"fn main() { "{some_var:?}".<|> }"#,
411+
r#"fn main() { format!("{:?}", some_var) }"#,
412+
);
413+
check_edit(
414+
"panic",
415+
r#"fn main() { "Panic with {a}".<|> }"#,
416+
r#"fn main() { panic!("Panic with {}", a) }"#,
417+
);
418+
check_edit(
419+
"println",
420+
r#"fn main() { "{ 2+2 } { SomeStruct { val: 1, other: 32 } :?}".<|> }"#,
421+
r#"fn main() { println!("{} {:?}", 2+2, SomeStruct { val: 1, other: 32 }) }"#,
422+
);
423+
check_edit(
424+
"loge",
425+
r#"fn main() { "{2+2}".<|> }"#,
426+
r#"fn main() { log::error!("{}", 2+2) }"#,
427+
);
428+
check_edit(
429+
"logt",
430+
r#"fn main() { "{2+2}".<|> }"#,
431+
r#"fn main() { log::trace!("{}", 2+2) }"#,
432+
);
433+
check_edit(
434+
"logd",
435+
r#"fn main() { "{2+2}".<|> }"#,
436+
r#"fn main() { log::debug!("{}", 2+2) }"#,
437+
);
438+
check_edit(
439+
"logi",
440+
r#"fn main() { "{2+2}".<|> }"#,
441+
r#"fn main() { log::info!("{}", 2+2) }"#,
442+
);
443+
check_edit(
444+
"logw",
445+
r#"fn main() { "{2+2}".<|> }"#,
446+
r#"fn main() { log::warn!("{}", 2+2) }"#,
447+
);
448+
check_edit(
449+
"loge",
450+
r#"fn main() { "{2+2}".<|> }"#,
451+
r#"fn main() { log::error!("{}", 2+2) }"#,
452+
);
453+
}
395454
}
Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
17+
use crate::completion::{
18+
complete_postfix::postfix_snippet, completion_config::SnippetCap,
19+
completion_context::CompletionContext, completion_item::Completions,
20+
};
21+
use syntax::ast::{self, AstToken};
22+
23+
/// Mapping ("postfix completion item" => "macro to use")
24+
static 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+
35+
pub(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.
60+
fn 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)]
72+
pub 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)]
81+
enum State {
82+
NotExpr,
83+
MaybeExpr,
84+
Expr,
85+
MaybeIncorrect,
86+
FormatOpts,
87+
}
88+
89+
impl 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)]
205+
mod 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+
}

crates/ide/src/completion/completion_context.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -469,7 +469,7 @@ impl<'a> CompletionContext<'a> {
469469
}
470470
} else {
471471
false
472-
}
472+
};
473473
}
474474
if let Some(method_call_expr) = ast::MethodCallExpr::cast(parent) {
475475
// As above

0 commit comments

Comments
 (0)