aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIgor Aleksanov <[email protected]>2020-09-12 15:14:17 +0100
committerIgor Aleksanov <[email protected]>2020-10-02 10:42:39 +0100
commitea320141c6f87383880878b91182355c9ad7dc7b (patch)
tree50e1ee50690c1ecea60caa01d6c6f7fbee983bfc
parentc01cd6e3ed0763f8e773c34dc76db0e39396133d (diff)
Add postfix completion for format-like string literals
-rw-r--r--crates/ide/src/completion/complete_postfix.rs56
-rw-r--r--crates/ide/src/completion/complete_postfix/format_like.rs310
-rw-r--r--crates/ide/src/completion/completion_context.rs11
3 files changed, 376 insertions, 1 deletions
diff --git a/crates/ide/src/completion/complete_postfix.rs b/crates/ide/src/completion/complete_postfix.rs
index 26a5af5b9..73a5f1439 100644
--- a/crates/ide/src/completion/complete_postfix.rs
+++ b/crates/ide/src/completion/complete_postfix.rs
@@ -6,6 +6,7 @@ use syntax::{
6}; 6};
7use text_edit::TextEdit; 7use text_edit::TextEdit;
8 8
9use self::format_like::add_format_like_completions;
9use crate::{ 10use crate::{
10 completion::{ 11 completion::{
11 completion_config::SnippetCap, 12 completion_config::SnippetCap,
@@ -15,6 +16,8 @@ use crate::{
15 CompletionItem, CompletionItemKind, 16 CompletionItem, CompletionItemKind,
16}; 17};
17 18
19mod format_like;
20
18pub(super) fn complete_postfix(acc: &mut Completions, ctx: &CompletionContext) { 21pub(super) fn complete_postfix(acc: &mut Completions, ctx: &CompletionContext) {
19 if !ctx.config.enable_postfix_completions { 22 if !ctx.config.enable_postfix_completions {
20 return; 23 return;
@@ -207,6 +210,10 @@ pub(super) fn complete_postfix(acc: &mut Completions, ctx: &CompletionContext) {
207 &format!("${{1}}({})", receiver_text), 210 &format!("${{1}}({})", receiver_text),
208 ) 211 )
209 .add_to(acc); 212 .add_to(acc);
213
214 if ctx.is_string_literal {
215 add_format_like_completions(acc, ctx, &dot_receiver, cap, &receiver_text);
216 }
210} 217}
211 218
212fn get_receiver_text(receiver: &ast::Expr, receiver_is_ambiguous_float_literal: bool) -> String { 219fn get_receiver_text(receiver: &ast::Expr, receiver_is_ambiguous_float_literal: bool) -> String {
@@ -392,4 +399,53 @@ fn main() {
392 check_edit("dbg", r#"fn main() { &&42.<|> }"#, r#"fn main() { dbg!(&&42) }"#); 399 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 }"#); 400 check_edit("refm", r#"fn main() { &&42.<|> }"#, r#"fn main() { &&&mut 42 }"#);
394 } 401 }
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 }
395} 451}
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..93211a35f
--- /dev/null
+++ b/crates/ide/src/completion/complete_postfix/format_like.rs
@@ -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
16use super::postfix_snippet;
17use crate::completion::{
18 completion_config::SnippetCap, completion_context::CompletionContext,
19 completion_item::Completions,
20};
21use syntax::ast;
22
23pub(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)]
46pub 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)]
55pub enum PostfixKind {
56 Format,
57 Panic,
58 Println,
59 LogDebug,
60 LogTrace,
61 LogInfo,
62 LogWarn,
63 LogError,
64}
65
66impl 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)]
108enum State {
109 NotExpr,
110 MaybeExpr,
111 Expr,
112 MaybeIncorrect,
113 FormatOpts,
114}
115
116impl 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)]
238mod 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}
diff --git a/crates/ide/src/completion/completion_context.rs b/crates/ide/src/completion/completion_context.rs
index 671b13328..842d1987c 100644
--- a/crates/ide/src/completion/completion_context.rs
+++ b/crates/ide/src/completion/completion_context.rs
@@ -74,6 +74,8 @@ pub(crate) struct CompletionContext<'a> {
74 pub(super) is_pattern_call: bool, 74 pub(super) is_pattern_call: bool,
75 /// If this is a macro call, i.e. the () are already there. 75 /// If this is a macro call, i.e. the () are already there.
76 pub(super) is_macro_call: bool, 76 pub(super) is_macro_call: bool,
77 /// If this is a string literal, like "lorem ipsum".
78 pub(super) is_string_literal: bool,
77 pub(super) is_path_type: bool, 79 pub(super) is_path_type: bool,
78 pub(super) has_type_args: bool, 80 pub(super) has_type_args: bool,
79 pub(super) attribute_under_caret: Option<ast::Attr>, 81 pub(super) attribute_under_caret: Option<ast::Attr>,
@@ -156,6 +158,7 @@ impl<'a> CompletionContext<'a> {
156 is_call: false, 158 is_call: false,
157 is_pattern_call: false, 159 is_pattern_call: false,
158 is_macro_call: false, 160 is_macro_call: false,
161 is_string_literal: false,
159 is_path_type: false, 162 is_path_type: false,
160 has_type_args: false, 163 has_type_args: false,
161 dot_receiver_is_ambiguous_float_literal: false, 164 dot_receiver_is_ambiguous_float_literal: false,
@@ -469,7 +472,13 @@ impl<'a> CompletionContext<'a> {
469 } 472 }
470 } else { 473 } else {
471 false 474 false
472 } 475 };
476
477 self.is_string_literal = if let Some(ast::Expr::Literal(l)) = &self.dot_receiver {
478 matches!(l.kind(), ast::LiteralKind::String { .. })
479 } else {
480 false
481 };
473 } 482 }
474 if let Some(method_call_expr) = ast::MethodCallExpr::cast(parent) { 483 if let Some(method_call_expr) = ast::MethodCallExpr::cast(parent) {
475 // As above 484 // As above