aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--crates/ide/src/completion/complete_postfix.rs61
-rw-r--r--crates/ide/src/completion/complete_postfix/format_like.rs277
-rw-r--r--crates/ide/src/completion/completion_context.rs2
3 files changed, 338 insertions, 2 deletions
diff --git a/crates/ide/src/completion/complete_postfix.rs b/crates/ide/src/completion/complete_postfix.rs
index 26a5af5b9..db5319618 100644
--- a/crates/ide/src/completion/complete_postfix.rs
+++ b/crates/ide/src/completion/complete_postfix.rs
@@ -1,11 +1,15 @@
1//! FIXME: write short doc here 1//! FIXME: write short doc here
2
3mod format_like;
4
2use assists::utils::TryEnum; 5use assists::utils::TryEnum;
3use syntax::{ 6use syntax::{
4 ast::{self, AstNode}, 7 ast::{self, AstNode, AstToken},
5 TextRange, TextSize, 8 TextRange, TextSize,
6}; 9};
7use text_edit::TextEdit; 10use text_edit::TextEdit;
8 11
12use self::format_like::add_format_like_completions;
9use crate::{ 13use crate::{
10 completion::{ 14 completion::{
11 completion_config::SnippetCap, 15 completion_config::SnippetCap,
@@ -207,6 +211,12 @@ pub(super) fn complete_postfix(acc: &mut Completions, ctx: &CompletionContext) {
207 &format!("${{1}}({})", receiver_text), 211 &format!("${{1}}({})", receiver_text),
208 ) 212 )
209 .add_to(acc); 213 .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 }
210} 220}
211 221
212fn get_receiver_text(receiver: &ast::Expr, receiver_is_ambiguous_float_literal: bool) -> String { 222fn get_receiver_text(receiver: &ast::Expr, receiver_is_ambiguous_float_literal: bool) -> String {
@@ -392,4 +402,53 @@ fn main() {
392 check_edit("dbg", r#"fn main() { &&42.<|> }"#, r#"fn main() { dbg!(&&42) }"#); 402 check_edit("dbg", r#"fn main() { &&42.<|> }"#, r#"fn main() { dbg!(&&42) }"#);
393 check_edit("refm", r#"fn main() { &&42.<|> }"#, r#"fn main() { &&&mut 42 }"#); 403 check_edit("refm", r#"fn main() { &&42.<|> }"#, r#"fn main() { &&&mut 42 }"#);
394 } 404 }
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 }
395} 454}
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}
diff --git a/crates/ide/src/completion/completion_context.rs b/crates/ide/src/completion/completion_context.rs
index 671b13328..101be8eb5 100644
--- a/crates/ide/src/completion/completion_context.rs
+++ b/crates/ide/src/completion/completion_context.rs
@@ -469,7 +469,7 @@ impl<'a> CompletionContext<'a> {
469 } 469 }
470 } else { 470 } else {
471 false 471 false
472 } 472 };
473 } 473 }
474 if let Some(method_call_expr) = ast::MethodCallExpr::cast(parent) { 474 if let Some(method_call_expr) = ast::MethodCallExpr::cast(parent) {
475 // As above 475 // As above