Skip to content

Commit ea32014

Browse files
committed
Add postfix completion for format-like string literals
1 parent c01cd6e commit ea32014

File tree

3 files changed

+376
-1
lines changed

3 files changed

+376
-1
lines changed

crates/ide/src/completion/complete_postfix.rs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use syntax::{
66
};
77
use text_edit::TextEdit;
88

9+
use self::format_like::add_format_like_completions;
910
use crate::{
1011
completion::{
1112
completion_config::SnippetCap,
@@ -15,6 +16,8 @@ use crate::{
1516
CompletionItem, CompletionItemKind,
1617
};
1718

19+
mod format_like;
20+
1821
pub(super) fn complete_postfix(acc: &mut Completions, ctx: &CompletionContext) {
1922
if !ctx.config.enable_postfix_completions {
2023
return;
@@ -207,6 +210,10 @@ pub(super) fn complete_postfix(acc: &mut Completions, ctx: &CompletionContext) {
207210
&format!("${{1}}({})", receiver_text),
208211
)
209212
.add_to(acc);
213+
214+
if ctx.is_string_literal {
215+
add_format_like_completions(acc, ctx, &dot_receiver, cap, &receiver_text);
216+
}
210217
}
211218

212219
fn get_receiver_text(receiver: &ast::Expr, receiver_is_ambiguous_float_literal: bool) -> String {
@@ -392,4 +399,53 @@ fn main() {
392399
check_edit("dbg", r#"fn main() { &&42.<|> }"#, r#"fn main() { dbg!(&&42) }"#);
393400
check_edit("refm", r#"fn main() { &&42.<|> }"#, r#"fn main() { &&&mut 42 }"#);
394401
}
402+
403+
#[test]
404+
fn postfix_completion_for_format_like_strings() {
405+
check_edit(
406+
"fmt",
407+
r#"fn main() { "{some_var:?}".<|> }"#,
408+
r#"fn main() { format!("{:?}", some_var) }"#,
409+
);
410+
check_edit(
411+
"panic",
412+
r#"fn main() { "Panic with {a}".<|> }"#,
413+
r#"fn main() { panic!("Panic with {}", a) }"#,
414+
);
415+
check_edit(
416+
"println",
417+
r#"fn main() { "{ 2+2 } { SomeStruct { val: 1, other: 32 } :?}".<|> }"#,
418+
r#"fn main() { println!("{} {:?}", 2+2, SomeStruct { val: 1, other: 32 }) }"#,
419+
);
420+
check_edit(
421+
"loge",
422+
r#"fn main() { "{2+2}".<|> }"#,
423+
r#"fn main() { log::error!("{}", 2+2) }"#,
424+
);
425+
check_edit(
426+
"logt",
427+
r#"fn main() { "{2+2}".<|> }"#,
428+
r#"fn main() { log::trace!("{}", 2+2) }"#,
429+
);
430+
check_edit(
431+
"logd",
432+
r#"fn main() { "{2+2}".<|> }"#,
433+
r#"fn main() { log::debug!("{}", 2+2) }"#,
434+
);
435+
check_edit(
436+
"logi",
437+
r#"fn main() { "{2+2}".<|> }"#,
438+
r#"fn main() { log::info!("{}", 2+2) }"#,
439+
);
440+
check_edit(
441+
"logw",
442+
r#"fn main() { "{2+2}".<|> }"#,
443+
r#"fn main() { log::warn!("{}", 2+2) }"#,
444+
);
445+
check_edit(
446+
"loge",
447+
r#"fn main() { "{2+2}".<|> }"#,
448+
r#"fn main() { log::error!("{}", 2+2) }"#,
449+
);
450+
}
395451
}
Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
1+
//! 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+
//! - `println` -> `println!(...)`
9+
//! - `log`:
10+
//! + `logd` -> `log::debug!(...)`
11+
//! + `logt` -> `log::trace!(...)`
12+
//! + `logi` -> `log::info!(...)`
13+
//! + `logw` -> `log::warn!(...)`
14+
//! + `loge` -> `log::error!(...)`
15+
16+
use super::postfix_snippet;
17+
use crate::completion::{
18+
completion_config::SnippetCap, completion_context::CompletionContext,
19+
completion_item::Completions,
20+
};
21+
use syntax::ast;
22+
23+
pub(super) fn add_format_like_completions(
24+
acc: &mut Completions,
25+
ctx: &CompletionContext,
26+
dot_receiver: &ast::Expr,
27+
cap: SnippetCap,
28+
receiver_text: &str,
29+
) {
30+
assert!(receiver_text.len() >= 2);
31+
let input = &receiver_text[1..receiver_text.len() - 1];
32+
33+
let mut parser = FormatStrParser::new(input);
34+
35+
if parser.parse().is_ok() {
36+
for kind in PostfixKind::all_suggestions() {
37+
let snippet = parser.into_suggestion(*kind);
38+
let (label, detail) = kind.into_description();
39+
40+
postfix_snippet(ctx, cap, &dot_receiver, label, detail, &snippet).add_to(acc);
41+
}
42+
}
43+
}
44+
45+
#[derive(Debug)]
46+
pub struct FormatStrParser {
47+
input: String,
48+
output: String,
49+
extracted_expressions: Vec<String>,
50+
state: State,
51+
parsed: bool,
52+
}
53+
54+
#[derive(Debug, Clone, Copy)]
55+
pub enum PostfixKind {
56+
Format,
57+
Panic,
58+
Println,
59+
LogDebug,
60+
LogTrace,
61+
LogInfo,
62+
LogWarn,
63+
LogError,
64+
}
65+
66+
impl PostfixKind {
67+
pub fn all_suggestions() -> &'static [PostfixKind] {
68+
&[
69+
Self::Format,
70+
Self::Panic,
71+
Self::Println,
72+
Self::LogDebug,
73+
Self::LogTrace,
74+
Self::LogInfo,
75+
Self::LogWarn,
76+
Self::LogError,
77+
]
78+
}
79+
80+
pub fn into_description(self) -> (&'static str, &'static str) {
81+
match self {
82+
Self::Format => ("fmt", "format!"),
83+
Self::Panic => ("panic", "panic!"),
84+
Self::Println => ("println", "println!"),
85+
Self::LogDebug => ("logd", "log::debug!"),
86+
Self::LogTrace => ("logt", "log::trace!"),
87+
Self::LogInfo => ("logi", "log::info!"),
88+
Self::LogWarn => ("logw", "log::warn!"),
89+
Self::LogError => ("loge", "log::error!"),
90+
}
91+
}
92+
93+
pub fn into_macro_name(self) -> &'static str {
94+
match self {
95+
Self::Format => "format!",
96+
Self::Panic => "panic!",
97+
Self::Println => "println!",
98+
Self::LogDebug => "log::debug!",
99+
Self::LogTrace => "log::trace!",
100+
Self::LogInfo => "log::info!",
101+
Self::LogWarn => "log::warn!",
102+
Self::LogError => "log::error!",
103+
}
104+
}
105+
}
106+
107+
#[derive(Debug, Clone, Copy, PartialEq)]
108+
enum State {
109+
NotExpr,
110+
MaybeExpr,
111+
Expr,
112+
MaybeIncorrect,
113+
FormatOpts,
114+
}
115+
116+
impl FormatStrParser {
117+
pub fn new(input: impl Into<String>) -> Self {
118+
Self {
119+
input: input.into(),
120+
output: String::new(),
121+
extracted_expressions: Vec::new(),
122+
state: State::NotExpr,
123+
parsed: false,
124+
}
125+
}
126+
127+
pub fn parse(&mut self) -> Result<(), ()> {
128+
let mut current_expr = String::new();
129+
130+
let mut placeholders_count = 0;
131+
132+
// Count of open braces inside of an expression.
133+
// We assume that user knows what they're doing, thus we treat it like a correct pattern, e.g.
134+
// "{MyStruct { val_a: 0, val_b: 1 }}".
135+
let mut inexpr_open_count = 0;
136+
137+
for chr in self.input.chars() {
138+
match (self.state, chr) {
139+
(State::NotExpr, '{') => {
140+
self.output.push(chr);
141+
self.state = State::MaybeExpr;
142+
}
143+
(State::NotExpr, '}') => {
144+
self.output.push(chr);
145+
self.state = State::MaybeIncorrect;
146+
}
147+
(State::NotExpr, _) => {
148+
self.output.push(chr);
149+
}
150+
(State::MaybeIncorrect, '}') => {
151+
// It's okay, we met "}}".
152+
self.output.push(chr);
153+
self.state = State::NotExpr;
154+
}
155+
(State::MaybeIncorrect, _) => {
156+
// Error in the string.
157+
return Err(());
158+
}
159+
(State::MaybeExpr, '{') => {
160+
self.output.push(chr);
161+
self.state = State::NotExpr;
162+
}
163+
(State::MaybeExpr, '}') => {
164+
// This is an empty sequence '{}'. Replace it with placeholder.
165+
self.output.push(chr);
166+
self.extracted_expressions.push(format!("${}", placeholders_count));
167+
placeholders_count += 1;
168+
self.state = State::NotExpr;
169+
}
170+
(State::MaybeExpr, _) => {
171+
current_expr.push(chr);
172+
self.state = State::Expr;
173+
}
174+
(State::Expr, '}') => {
175+
if inexpr_open_count == 0 {
176+
self.output.push(chr);
177+
self.extracted_expressions.push(current_expr.trim().into());
178+
current_expr = String::new();
179+
self.state = State::NotExpr;
180+
} else {
181+
// We're closing one brace met before inside of the expression.
182+
current_expr.push(chr);
183+
inexpr_open_count -= 1;
184+
}
185+
}
186+
(State::Expr, ':') => {
187+
if inexpr_open_count == 0 {
188+
// We're outside of braces, thus assume that it's a specifier, like "{Some(value):?}"
189+
self.output.push(chr);
190+
self.extracted_expressions.push(current_expr.trim().into());
191+
current_expr = String::new();
192+
self.state = State::FormatOpts;
193+
} else {
194+
// We're inside of braced expression, assume that it's a struct field name/value delimeter.
195+
current_expr.push(chr);
196+
}
197+
}
198+
(State::Expr, '{') => {
199+
current_expr.push(chr);
200+
inexpr_open_count += 1;
201+
}
202+
(State::Expr, _) => {
203+
current_expr.push(chr);
204+
}
205+
(State::FormatOpts, '}') => {
206+
self.output.push(chr);
207+
self.state = State::NotExpr;
208+
}
209+
(State::FormatOpts, _) => {
210+
self.output.push(chr);
211+
}
212+
}
213+
}
214+
215+
if self.state != State::NotExpr {
216+
return Err(());
217+
}
218+
219+
self.parsed = true;
220+
Ok(())
221+
}
222+
223+
pub fn into_suggestion(&self, kind: PostfixKind) -> String {
224+
assert!(self.parsed, "Attempt to get a suggestion from not parsed expression");
225+
226+
let mut output = format!(r#"{}("{}""#, kind.into_macro_name(), self.output);
227+
for expr in &self.extracted_expressions {
228+
output += ", ";
229+
output += expr;
230+
}
231+
output.push(')');
232+
233+
output
234+
}
235+
}
236+
237+
#[cfg(test)]
238+
mod tests {
239+
use super::*;
240+
241+
#[test]
242+
fn format_str_parser() {
243+
let test_vector = &[
244+
("no expressions", Some(("no expressions", vec![]))),
245+
("{expr} is {2 + 2}", Some(("{} is {}", vec!["expr", "2 + 2"]))),
246+
("{expr:?}", Some(("{:?}", vec!["expr"]))),
247+
("{malformed", None),
248+
("malformed}", None),
249+
("{{correct", Some(("{{correct", vec![]))),
250+
("correct}}", Some(("correct}}", vec![]))),
251+
("{correct}}}", Some(("{}}}", vec!["correct"]))),
252+
("{correct}}}}}", Some(("{}}}}}", vec!["correct"]))),
253+
("{incorrect}}", None),
254+
("placeholders {} {}", Some(("placeholders {} {}", vec!["$0", "$1"]))),
255+
("mixed {} {2 + 2} {}", Some(("mixed {} {} {}", vec!["$0", "2 + 2", "$1"]))),
256+
(
257+
"{SomeStruct { val_a: 0, val_b: 1 }}",
258+
Some(("{}", vec!["SomeStruct { val_a: 0, val_b: 1 }"])),
259+
),
260+
("{expr:?} is {2.32f64:.5}", Some(("{:?} is {:.5}", vec!["expr", "2.32f64"]))),
261+
(
262+
"{SomeStruct { val_a: 0, val_b: 1 }:?}",
263+
Some(("{:?}", vec!["SomeStruct { val_a: 0, val_b: 1 }"])),
264+
),
265+
("{ 2 + 2 }", Some(("{}", vec!["2 + 2"]))),
266+
];
267+
268+
for (input, output) in test_vector {
269+
let mut parser = FormatStrParser::new(*input);
270+
let outcome = parser.parse();
271+
272+
if let Some((result_str, result_args)) = output {
273+
assert!(
274+
outcome.is_ok(),
275+
"Outcome is error for input: {}, but the expected outcome is {:?}",
276+
input,
277+
output
278+
);
279+
assert_eq!(parser.output, *result_str);
280+
assert_eq!(&parser.extracted_expressions, result_args);
281+
} else {
282+
assert!(
283+
outcome.is_err(),
284+
"Outcome is OK for input: {}, but the expected outcome is error",
285+
input
286+
);
287+
}
288+
}
289+
}
290+
291+
#[test]
292+
fn test_into_suggestion() {
293+
let test_vector = &[
294+
(PostfixKind::Println, "{}", r#"println!("{}", $0)"#),
295+
(
296+
PostfixKind::LogInfo,
297+
"{} {expr} {} {2 + 2}",
298+
r#"log::info!("{} {} {} {}", $0, expr, $1, 2 + 2)"#,
299+
),
300+
(PostfixKind::Format, "{expr:?}", r#"format!("{:?}", expr)"#),
301+
];
302+
303+
for (kind, input, output) in test_vector {
304+
let mut parser = FormatStrParser::new(*input);
305+
parser.parse().expect("Parsing must succeed");
306+
307+
assert_eq!(&parser.into_suggestion(*kind), output);
308+
}
309+
}
310+
}

0 commit comments

Comments
 (0)