From ea320141c6f87383880878b91182355c9ad7dc7b Mon Sep 17 00:00:00 2001 From: Igor Aleksanov Date: Sat, 12 Sep 2020 17:14:17 +0300 Subject: Add postfix completion for format-like string literals --- crates/ide/src/completion/complete_postfix.rs | 56 ++++ .../src/completion/complete_postfix/format_like.rs | 310 +++++++++++++++++++++ crates/ide/src/completion/completion_context.rs | 11 +- 3 files changed, 376 insertions(+), 1 deletion(-) create mode 100644 crates/ide/src/completion/complete_postfix/format_like.rs (limited to 'crates/ide') 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::{ }; use text_edit::TextEdit; +use self::format_like::add_format_like_completions; use crate::{ completion::{ completion_config::SnippetCap, @@ -15,6 +16,8 @@ use crate::{ CompletionItem, CompletionItemKind, }; +mod format_like; + pub(super) fn complete_postfix(acc: &mut Completions, ctx: &CompletionContext) { if !ctx.config.enable_postfix_completions { return; @@ -207,6 +210,10 @@ pub(super) fn complete_postfix(acc: &mut Completions, ctx: &CompletionContext) { &format!("${{1}}({})", receiver_text), ) .add_to(acc); + + if ctx.is_string_literal { + add_format_like_completions(acc, ctx, &dot_receiver, cap, &receiver_text); + } } fn get_receiver_text(receiver: &ast::Expr, receiver_is_ambiguous_float_literal: bool) -> String { @@ -392,4 +399,53 @@ fn main() { check_edit("dbg", r#"fn main() { &&42.<|> }"#, r#"fn main() { dbg!(&&42) }"#); check_edit("refm", r#"fn main() { &&42.<|> }"#, r#"fn main() { &&&mut 42 }"#); } + + #[test] + fn postfix_completion_for_format_like_strings() { + check_edit( + "fmt", + r#"fn main() { "{some_var:?}".<|> }"#, + r#"fn main() { format!("{:?}", some_var) }"#, + ); + check_edit( + "panic", + r#"fn main() { "Panic with {a}".<|> }"#, + r#"fn main() { panic!("Panic with {}", a) }"#, + ); + check_edit( + "println", + r#"fn main() { "{ 2+2 } { SomeStruct { val: 1, other: 32 } :?}".<|> }"#, + r#"fn main() { println!("{} {:?}", 2+2, SomeStruct { val: 1, other: 32 }) }"#, + ); + check_edit( + "loge", + r#"fn main() { "{2+2}".<|> }"#, + r#"fn main() { log::error!("{}", 2+2) }"#, + ); + check_edit( + "logt", + r#"fn main() { "{2+2}".<|> }"#, + r#"fn main() { log::trace!("{}", 2+2) }"#, + ); + check_edit( + "logd", + r#"fn main() { "{2+2}".<|> }"#, + r#"fn main() { log::debug!("{}", 2+2) }"#, + ); + check_edit( + "logi", + r#"fn main() { "{2+2}".<|> }"#, + r#"fn main() { log::info!("{}", 2+2) }"#, + ); + check_edit( + "logw", + r#"fn main() { "{2+2}".<|> }"#, + r#"fn main() { log::warn!("{}", 2+2) }"#, + ); + check_edit( + "loge", + r#"fn main() { "{2+2}".<|> }"#, + r#"fn main() { log::error!("{}", 2+2) }"#, + ); + } } 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 @@ +//! Postfix completion for `format`-like strings. +//! +//! `"Result {result} is {2 + 2}"` is expanded to the `"Result {} is {}", result, 2 + 2`. +//! +//! The following postfix snippets are available: +//! +//! - `format` -> `format!(...)` +//! - `println` -> `println!(...)` +//! - `log`: +//! + `logd` -> `log::debug!(...)` +//! + `logt` -> `log::trace!(...)` +//! + `logi` -> `log::info!(...)` +//! + `logw` -> `log::warn!(...)` +//! + `loge` -> `log::error!(...)` + +use super::postfix_snippet; +use crate::completion::{ + completion_config::SnippetCap, completion_context::CompletionContext, + completion_item::Completions, +}; +use syntax::ast; + +pub(super) fn add_format_like_completions( + acc: &mut Completions, + ctx: &CompletionContext, + dot_receiver: &ast::Expr, + cap: SnippetCap, + receiver_text: &str, +) { + assert!(receiver_text.len() >= 2); + let input = &receiver_text[1..receiver_text.len() - 1]; + + let mut parser = FormatStrParser::new(input); + + if parser.parse().is_ok() { + for kind in PostfixKind::all_suggestions() { + let snippet = parser.into_suggestion(*kind); + let (label, detail) = kind.into_description(); + + postfix_snippet(ctx, cap, &dot_receiver, label, detail, &snippet).add_to(acc); + } + } +} + +#[derive(Debug)] +pub struct FormatStrParser { + input: String, + output: String, + extracted_expressions: Vec, + state: State, + parsed: bool, +} + +#[derive(Debug, Clone, Copy)] +pub enum PostfixKind { + Format, + Panic, + Println, + LogDebug, + LogTrace, + LogInfo, + LogWarn, + LogError, +} + +impl PostfixKind { + pub fn all_suggestions() -> &'static [PostfixKind] { + &[ + Self::Format, + Self::Panic, + Self::Println, + Self::LogDebug, + Self::LogTrace, + Self::LogInfo, + Self::LogWarn, + Self::LogError, + ] + } + + pub fn into_description(self) -> (&'static str, &'static str) { + match self { + Self::Format => ("fmt", "format!"), + Self::Panic => ("panic", "panic!"), + Self::Println => ("println", "println!"), + Self::LogDebug => ("logd", "log::debug!"), + Self::LogTrace => ("logt", "log::trace!"), + Self::LogInfo => ("logi", "log::info!"), + Self::LogWarn => ("logw", "log::warn!"), + Self::LogError => ("loge", "log::error!"), + } + } + + pub fn into_macro_name(self) -> &'static str { + match self { + Self::Format => "format!", + Self::Panic => "panic!", + Self::Println => "println!", + Self::LogDebug => "log::debug!", + Self::LogTrace => "log::trace!", + Self::LogInfo => "log::info!", + Self::LogWarn => "log::warn!", + Self::LogError => "log::error!", + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq)] +enum State { + NotExpr, + MaybeExpr, + Expr, + MaybeIncorrect, + FormatOpts, +} + +impl FormatStrParser { + pub fn new(input: impl Into) -> Self { + Self { + input: input.into(), + output: String::new(), + extracted_expressions: Vec::new(), + state: State::NotExpr, + parsed: false, + } + } + + pub fn parse(&mut self) -> Result<(), ()> { + let mut current_expr = String::new(); + + let mut placeholders_count = 0; + + // Count of open braces inside of an expression. + // We assume that user knows what they're doing, thus we treat it like a correct pattern, e.g. + // "{MyStruct { val_a: 0, val_b: 1 }}". + let mut inexpr_open_count = 0; + + for chr in self.input.chars() { + match (self.state, chr) { + (State::NotExpr, '{') => { + self.output.push(chr); + self.state = State::MaybeExpr; + } + (State::NotExpr, '}') => { + self.output.push(chr); + self.state = State::MaybeIncorrect; + } + (State::NotExpr, _) => { + self.output.push(chr); + } + (State::MaybeIncorrect, '}') => { + // It's okay, we met "}}". + self.output.push(chr); + self.state = State::NotExpr; + } + (State::MaybeIncorrect, _) => { + // Error in the string. + return Err(()); + } + (State::MaybeExpr, '{') => { + self.output.push(chr); + self.state = State::NotExpr; + } + (State::MaybeExpr, '}') => { + // This is an empty sequence '{}'. Replace it with placeholder. + self.output.push(chr); + self.extracted_expressions.push(format!("${}", placeholders_count)); + placeholders_count += 1; + self.state = State::NotExpr; + } + (State::MaybeExpr, _) => { + current_expr.push(chr); + self.state = State::Expr; + } + (State::Expr, '}') => { + if inexpr_open_count == 0 { + self.output.push(chr); + self.extracted_expressions.push(current_expr.trim().into()); + current_expr = String::new(); + self.state = State::NotExpr; + } else { + // We're closing one brace met before inside of the expression. + current_expr.push(chr); + inexpr_open_count -= 1; + } + } + (State::Expr, ':') => { + if inexpr_open_count == 0 { + // We're outside of braces, thus assume that it's a specifier, like "{Some(value):?}" + self.output.push(chr); + self.extracted_expressions.push(current_expr.trim().into()); + current_expr = String::new(); + self.state = State::FormatOpts; + } else { + // We're inside of braced expression, assume that it's a struct field name/value delimeter. + current_expr.push(chr); + } + } + (State::Expr, '{') => { + current_expr.push(chr); + inexpr_open_count += 1; + } + (State::Expr, _) => { + current_expr.push(chr); + } + (State::FormatOpts, '}') => { + self.output.push(chr); + self.state = State::NotExpr; + } + (State::FormatOpts, _) => { + self.output.push(chr); + } + } + } + + if self.state != State::NotExpr { + return Err(()); + } + + self.parsed = true; + Ok(()) + } + + pub fn into_suggestion(&self, kind: PostfixKind) -> String { + assert!(self.parsed, "Attempt to get a suggestion from not parsed expression"); + + let mut output = format!(r#"{}("{}""#, kind.into_macro_name(), self.output); + for expr in &self.extracted_expressions { + output += ", "; + output += expr; + } + output.push(')'); + + output + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn format_str_parser() { + let test_vector = &[ + ("no expressions", Some(("no expressions", vec![]))), + ("{expr} is {2 + 2}", Some(("{} is {}", vec!["expr", "2 + 2"]))), + ("{expr:?}", Some(("{:?}", vec!["expr"]))), + ("{malformed", None), + ("malformed}", None), + ("{{correct", Some(("{{correct", vec![]))), + ("correct}}", Some(("correct}}", vec![]))), + ("{correct}}}", Some(("{}}}", vec!["correct"]))), + ("{correct}}}}}", Some(("{}}}}}", vec!["correct"]))), + ("{incorrect}}", None), + ("placeholders {} {}", Some(("placeholders {} {}", vec!["$0", "$1"]))), + ("mixed {} {2 + 2} {}", Some(("mixed {} {} {}", vec!["$0", "2 + 2", "$1"]))), + ( + "{SomeStruct { val_a: 0, val_b: 1 }}", + Some(("{}", vec!["SomeStruct { val_a: 0, val_b: 1 }"])), + ), + ("{expr:?} is {2.32f64:.5}", Some(("{:?} is {:.5}", vec!["expr", "2.32f64"]))), + ( + "{SomeStruct { val_a: 0, val_b: 1 }:?}", + Some(("{:?}", vec!["SomeStruct { val_a: 0, val_b: 1 }"])), + ), + ("{ 2 + 2 }", Some(("{}", vec!["2 + 2"]))), + ]; + + for (input, output) in test_vector { + let mut parser = FormatStrParser::new(*input); + let outcome = parser.parse(); + + if let Some((result_str, result_args)) = output { + assert!( + outcome.is_ok(), + "Outcome is error for input: {}, but the expected outcome is {:?}", + input, + output + ); + assert_eq!(parser.output, *result_str); + assert_eq!(&parser.extracted_expressions, result_args); + } else { + assert!( + outcome.is_err(), + "Outcome is OK for input: {}, but the expected outcome is error", + input + ); + } + } + } + + #[test] + fn test_into_suggestion() { + let test_vector = &[ + (PostfixKind::Println, "{}", r#"println!("{}", $0)"#), + ( + PostfixKind::LogInfo, + "{} {expr} {} {2 + 2}", + r#"log::info!("{} {} {} {}", $0, expr, $1, 2 + 2)"#, + ), + (PostfixKind::Format, "{expr:?}", r#"format!("{:?}", expr)"#), + ]; + + for (kind, input, output) in test_vector { + let mut parser = FormatStrParser::new(*input); + parser.parse().expect("Parsing must succeed"); + + assert_eq!(&parser.into_suggestion(*kind), output); + } + } +} 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> { pub(super) is_pattern_call: bool, /// If this is a macro call, i.e. the () are already there. pub(super) is_macro_call: bool, + /// If this is a string literal, like "lorem ipsum". + pub(super) is_string_literal: bool, pub(super) is_path_type: bool, pub(super) has_type_args: bool, pub(super) attribute_under_caret: Option, @@ -156,6 +158,7 @@ impl<'a> CompletionContext<'a> { is_call: false, is_pattern_call: false, is_macro_call: false, + is_string_literal: false, is_path_type: false, has_type_args: false, dot_receiver_is_ambiguous_float_literal: false, @@ -469,7 +472,13 @@ impl<'a> CompletionContext<'a> { } } else { false - } + }; + + self.is_string_literal = if let Some(ast::Expr::Literal(l)) = &self.dot_receiver { + matches!(l.kind(), ast::LiteralKind::String { .. }) + } else { + false + }; } if let Some(method_call_expr) = ast::MethodCallExpr::cast(parent) { // As above -- cgit v1.2.3 From e447b3a4a2bf089e5e3a190a532c17a4572ea013 Mon Sep 17 00:00:00 2001 From: Igor Aleksanov Date: Sat, 12 Sep 2020 17:32:19 +0300 Subject: Improve checks for postfix suggestions --- crates/ide/src/completion/complete_postfix.rs | 4 +--- .../src/completion/complete_postfix/format_like.rs | 26 ++++++++++++++++++---- crates/ide/src/completion/completion_context.rs | 9 -------- 3 files changed, 23 insertions(+), 16 deletions(-) (limited to 'crates/ide') diff --git a/crates/ide/src/completion/complete_postfix.rs b/crates/ide/src/completion/complete_postfix.rs index 73a5f1439..599074254 100644 --- a/crates/ide/src/completion/complete_postfix.rs +++ b/crates/ide/src/completion/complete_postfix.rs @@ -211,9 +211,7 @@ pub(super) fn complete_postfix(acc: &mut Completions, ctx: &CompletionContext) { ) .add_to(acc); - if ctx.is_string_literal { - add_format_like_completions(acc, ctx, &dot_receiver, cap, &receiver_text); - } + add_format_like_completions(acc, ctx, &dot_receiver, cap, &receiver_text); } fn get_receiver_text(receiver: &ast::Expr, receiver_is_ambiguous_float_literal: bool) -> String { diff --git a/crates/ide/src/completion/complete_postfix/format_like.rs b/crates/ide/src/completion/complete_postfix/format_like.rs index 93211a35f..6be3c2c92 100644 --- a/crates/ide/src/completion/complete_postfix/format_like.rs +++ b/crates/ide/src/completion/complete_postfix/format_like.rs @@ -27,7 +27,11 @@ pub(super) fn add_format_like_completions( cap: SnippetCap, receiver_text: &str, ) { - assert!(receiver_text.len() >= 2); + if !is_string_literal(receiver_text) { + // It's not a string literal, do not parse input. + return; + } + let input = &receiver_text[1..receiver_text.len() - 1]; let mut parser = FormatStrParser::new(input); @@ -42,6 +46,20 @@ pub(super) fn add_format_like_completions( } } +/// Checks whether provided item is a string literal. +fn is_string_literal(item: &str) -> bool { + if item.len() < 2 { + return false; + } + if item.chars().nth(0) != Some('"') || item.chars().nth(item.len() - 1) != Some('"') { + return false; + } + + true +} + +/// Parser for a format-like string. It is more allowing in terms of string contents, +/// as we expect variable placeholders to be filled with expressions. #[derive(Debug)] pub struct FormatStrParser { input: String, @@ -127,7 +145,7 @@ impl FormatStrParser { pub fn parse(&mut self) -> Result<(), ()> { let mut current_expr = String::new(); - let mut placeholders_count = 0; + let mut placeholder_id = 1; // Count of open braces inside of an expression. // We assume that user knows what they're doing, thus we treat it like a correct pattern, e.g. @@ -163,8 +181,8 @@ impl FormatStrParser { (State::MaybeExpr, '}') => { // This is an empty sequence '{}'. Replace it with placeholder. self.output.push(chr); - self.extracted_expressions.push(format!("${}", placeholders_count)); - placeholders_count += 1; + self.extracted_expressions.push(format!("${}", placeholder_id)); + placeholder_id += 1; self.state = State::NotExpr; } (State::MaybeExpr, _) => { diff --git a/crates/ide/src/completion/completion_context.rs b/crates/ide/src/completion/completion_context.rs index 842d1987c..101be8eb5 100644 --- a/crates/ide/src/completion/completion_context.rs +++ b/crates/ide/src/completion/completion_context.rs @@ -74,8 +74,6 @@ pub(crate) struct CompletionContext<'a> { pub(super) is_pattern_call: bool, /// If this is a macro call, i.e. the () are already there. pub(super) is_macro_call: bool, - /// If this is a string literal, like "lorem ipsum". - pub(super) is_string_literal: bool, pub(super) is_path_type: bool, pub(super) has_type_args: bool, pub(super) attribute_under_caret: Option, @@ -158,7 +156,6 @@ impl<'a> CompletionContext<'a> { is_call: false, is_pattern_call: false, is_macro_call: false, - is_string_literal: false, is_path_type: false, has_type_args: false, dot_receiver_is_ambiguous_float_literal: false, @@ -473,12 +470,6 @@ impl<'a> CompletionContext<'a> { } else { false }; - - self.is_string_literal = if let Some(ast::Expr::Literal(l)) = &self.dot_receiver { - matches!(l.kind(), ast::LiteralKind::String { .. }) - } else { - false - }; } if let Some(method_call_expr) = ast::MethodCallExpr::cast(parent) { // As above -- cgit v1.2.3 From cd3d654f60dad121fb44dc7f2874003ee6f73a55 Mon Sep 17 00:00:00 2001 From: Igor Aleksanov Date: Sat, 12 Sep 2020 18:09:00 +0300 Subject: Simplify is_string_literal function --- crates/ide/src/completion/complete_postfix/format_like.rs | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) (limited to 'crates/ide') diff --git a/crates/ide/src/completion/complete_postfix/format_like.rs b/crates/ide/src/completion/complete_postfix/format_like.rs index 6be3c2c92..8a16147ed 100644 --- a/crates/ide/src/completion/complete_postfix/format_like.rs +++ b/crates/ide/src/completion/complete_postfix/format_like.rs @@ -51,11 +51,7 @@ fn is_string_literal(item: &str) -> bool { if item.len() < 2 { return false; } - if item.chars().nth(0) != Some('"') || item.chars().nth(item.len() - 1) != Some('"') { - return false; - } - - true + item.starts_with("\"") && item.ends_with("\"") } /// Parser for a format-like string. It is more allowing in terms of string contents, @@ -269,8 +265,8 @@ mod tests { ("{correct}}}", Some(("{}}}", vec!["correct"]))), ("{correct}}}}}", Some(("{}}}}}", vec!["correct"]))), ("{incorrect}}", None), - ("placeholders {} {}", Some(("placeholders {} {}", vec!["$0", "$1"]))), - ("mixed {} {2 + 2} {}", Some(("mixed {} {} {}", vec!["$0", "2 + 2", "$1"]))), + ("placeholders {} {}", Some(("placeholders {} {}", vec!["$1", "$2"]))), + ("mixed {} {2 + 2} {}", Some(("mixed {} {} {}", vec!["$1", "2 + 2", "$2"]))), ( "{SomeStruct { val_a: 0, val_b: 1 }}", Some(("{}", vec!["SomeStruct { val_a: 0, val_b: 1 }"])), @@ -309,11 +305,11 @@ mod tests { #[test] fn test_into_suggestion() { let test_vector = &[ - (PostfixKind::Println, "{}", r#"println!("{}", $0)"#), + (PostfixKind::Println, "{}", r#"println!("{}", $1)"#), ( PostfixKind::LogInfo, "{} {expr} {} {2 + 2}", - r#"log::info!("{} {} {} {}", $0, expr, $1, 2 + 2)"#, + r#"log::info!("{} {} {} {}", $1, expr, $2, 2 + 2)"#, ), (PostfixKind::Format, "{expr:?}", r#"format!("{:?}", expr)"#), ]; -- cgit v1.2.3 From 777ccb58f06d10e0bbcdaa5512e73a0f3554ddcc Mon Sep 17 00:00:00 2001 From: Igor Aleksanov Date: Sat, 12 Sep 2020 18:15:11 +0300 Subject: Add missing entry to doc-comment --- crates/ide/src/completion/complete_postfix/format_like.rs | 1 + 1 file changed, 1 insertion(+) (limited to 'crates/ide') diff --git a/crates/ide/src/completion/complete_postfix/format_like.rs b/crates/ide/src/completion/complete_postfix/format_like.rs index 8a16147ed..0772dbf29 100644 --- a/crates/ide/src/completion/complete_postfix/format_like.rs +++ b/crates/ide/src/completion/complete_postfix/format_like.rs @@ -5,6 +5,7 @@ //! The following postfix snippets are available: //! //! - `format` -> `format!(...)` +//! - `panic` -> `panic!(...)` //! - `println` -> `println!(...)` //! - `log`: //! + `logd` -> `log::debug!(...)` -- cgit v1.2.3 From 2557cb8518a70b0d3b8689be6cb3c8d33342cd0d Mon Sep 17 00:00:00 2001 From: Igor Aleksanov Date: Fri, 2 Oct 2020 12:49:24 +0300 Subject: Improve format-like completions code appearance --- crates/ide/src/completion/complete_postfix.rs | 5 +- .../src/completion/complete_postfix/format_like.rs | 53 ++++++++++------------ 2 files changed, 26 insertions(+), 32 deletions(-) (limited to 'crates/ide') diff --git a/crates/ide/src/completion/complete_postfix.rs b/crates/ide/src/completion/complete_postfix.rs index 599074254..e549e0517 100644 --- a/crates/ide/src/completion/complete_postfix.rs +++ b/crates/ide/src/completion/complete_postfix.rs @@ -1,4 +1,7 @@ //! FIXME: write short doc here + +mod format_like; + use assists::utils::TryEnum; use syntax::{ ast::{self, AstNode}, @@ -16,8 +19,6 @@ use crate::{ CompletionItem, CompletionItemKind, }; -mod format_like; - pub(super) fn complete_postfix(acc: &mut Completions, ctx: &CompletionContext) { if !ctx.config.enable_postfix_completions { return; diff --git a/crates/ide/src/completion/complete_postfix/format_like.rs b/crates/ide/src/completion/complete_postfix/format_like.rs index 0772dbf29..f0ef017d1 100644 --- a/crates/ide/src/completion/complete_postfix/format_like.rs +++ b/crates/ide/src/completion/complete_postfix/format_like.rs @@ -1,23 +1,22 @@ -//! Postfix completion for `format`-like strings. -//! -//! `"Result {result} is {2 + 2}"` is expanded to the `"Result {} is {}", result, 2 + 2`. -//! -//! The following postfix snippets are available: -//! -//! - `format` -> `format!(...)` -//! - `panic` -> `panic!(...)` -//! - `println` -> `println!(...)` -//! - `log`: -//! + `logd` -> `log::debug!(...)` -//! + `logt` -> `log::trace!(...)` -//! + `logi` -> `log::info!(...)` -//! + `logw` -> `log::warn!(...)` -//! + `loge` -> `log::error!(...)` +// Feature: Postfix completion for `format`-like strings. +// +// `"Result {result} is {2 + 2}"` is expanded to the `"Result {} is {}", result, 2 + 2`. +// +// The following postfix snippets are available: +// +// - `format` -> `format!(...)` +// - `panic` -> `panic!(...)` +// - `println` -> `println!(...)` +// - `log`: +// + `logd` -> `log::debug!(...)` +// + `logt` -> `log::trace!(...)` +// + `logi` -> `log::info!(...)` +// + `logw` -> `log::warn!(...)` +// + `loge` -> `log::error!(...)` -use super::postfix_snippet; use crate::completion::{ - completion_config::SnippetCap, completion_context::CompletionContext, - completion_item::Completions, + complete_postfix::postfix_snippet, completion_config::SnippetCap, + completion_context::CompletionContext, completion_item::Completions, }; use syntax::ast; @@ -35,7 +34,7 @@ pub(super) fn add_format_like_completions( let input = &receiver_text[1..receiver_text.len() - 1]; - let mut parser = FormatStrParser::new(input); + let mut parser = FormatStrParser::new(input.to_owned()); if parser.parse().is_ok() { for kind in PostfixKind::all_suggestions() { @@ -129,7 +128,7 @@ enum State { } impl FormatStrParser { - pub fn new(input: impl Into) -> Self { + pub fn new(input: String) -> Self { Self { input: input.into(), output: String::new(), @@ -238,14 +237,8 @@ impl FormatStrParser { pub fn into_suggestion(&self, kind: PostfixKind) -> String { assert!(self.parsed, "Attempt to get a suggestion from not parsed expression"); - let mut output = format!(r#"{}("{}""#, kind.into_macro_name(), self.output); - for expr in &self.extracted_expressions { - output += ", "; - output += expr; - } - output.push(')'); - - output + let expressions_as_string = self.extracted_expressions.join(", "); + format!(r#"{}("{}", {})"#, kind.into_macro_name(), self.output, expressions_as_string) } } @@ -281,7 +274,7 @@ mod tests { ]; for (input, output) in test_vector { - let mut parser = FormatStrParser::new(*input); + let mut parser = FormatStrParser::new((*input).to_owned()); let outcome = parser.parse(); if let Some((result_str, result_args)) = output { @@ -316,7 +309,7 @@ mod tests { ]; for (kind, input, output) in test_vector { - let mut parser = FormatStrParser::new(*input); + let mut parser = FormatStrParser::new((*input).to_owned()); parser.parse().expect("Parsing must succeed"); assert_eq!(&parser.into_suggestion(*kind), output); -- cgit v1.2.3 From b7ac540f150e74ec7577df08511f977a67cd40e1 Mon Sep 17 00:00:00 2001 From: Igor Aleksanov Date: Fri, 2 Oct 2020 13:23:49 +0300 Subject: Use ast::String for extracting string literal contents --- crates/ide/src/completion/complete_postfix.rs | 8 +++++-- .../src/completion/complete_postfix/format_like.rs | 25 +++++++++++----------- 2 files changed, 19 insertions(+), 14 deletions(-) (limited to 'crates/ide') diff --git a/crates/ide/src/completion/complete_postfix.rs b/crates/ide/src/completion/complete_postfix.rs index e549e0517..db5319618 100644 --- a/crates/ide/src/completion/complete_postfix.rs +++ b/crates/ide/src/completion/complete_postfix.rs @@ -4,7 +4,7 @@ mod format_like; use assists::utils::TryEnum; use syntax::{ - ast::{self, AstNode}, + ast::{self, AstNode, AstToken}, TextRange, TextSize, }; use text_edit::TextEdit; @@ -212,7 +212,11 @@ pub(super) fn complete_postfix(acc: &mut Completions, ctx: &CompletionContext) { ) .add_to(acc); - add_format_like_completions(acc, ctx, &dot_receiver, cap, &receiver_text); + if let ast::Expr::Literal(literal) = dot_receiver.clone() { + if let Some(literal_text) = ast::String::cast(literal.token()) { + add_format_like_completions(acc, ctx, &dot_receiver, cap, &literal_text); + } + } } fn get_receiver_text(receiver: &ast::Expr, receiver_is_ambiguous_float_literal: bool) -> String { diff --git a/crates/ide/src/completion/complete_postfix/format_like.rs b/crates/ide/src/completion/complete_postfix/format_like.rs index f0ef017d1..53cb3af39 100644 --- a/crates/ide/src/completion/complete_postfix/format_like.rs +++ b/crates/ide/src/completion/complete_postfix/format_like.rs @@ -18,23 +18,22 @@ use crate::completion::{ complete_postfix::postfix_snippet, completion_config::SnippetCap, completion_context::CompletionContext, completion_item::Completions, }; -use syntax::ast; +use syntax::ast::{self, AstToken}; pub(super) fn add_format_like_completions( acc: &mut Completions, ctx: &CompletionContext, dot_receiver: &ast::Expr, cap: SnippetCap, - receiver_text: &str, + receiver_text: &ast::String, ) { - if !is_string_literal(receiver_text) { + let input = match string_literal_contents(receiver_text) { // It's not a string literal, do not parse input. - return; - } - - let input = &receiver_text[1..receiver_text.len() - 1]; + Some(input) => input, + None => return, + }; - let mut parser = FormatStrParser::new(input.to_owned()); + let mut parser = FormatStrParser::new(input); if parser.parse().is_ok() { for kind in PostfixKind::all_suggestions() { @@ -47,11 +46,13 @@ pub(super) fn add_format_like_completions( } /// Checks whether provided item is a string literal. -fn is_string_literal(item: &str) -> bool { - if item.len() < 2 { - return false; +fn string_literal_contents(item: &ast::String) -> Option { + let item = item.text(); + if item.len() >= 2 && item.starts_with("\"") && item.ends_with("\"") { + return Some(item[1..item.len() - 1].to_owned()); } - item.starts_with("\"") && item.ends_with("\"") + + None } /// Parser for a format-like string. It is more allowing in terms of string contents, -- cgit v1.2.3 From 76d0546ac7789ce94d15d305b07269d8b5e10038 Mon Sep 17 00:00:00 2001 From: Igor Aleksanov Date: Fri, 2 Oct 2020 13:33:27 +0300 Subject: Use lookup table instead of enum for postfix completion kinds --- .../src/completion/complete_postfix/format_like.rs | 82 ++++++---------------- 1 file changed, 20 insertions(+), 62 deletions(-) (limited to 'crates/ide') diff --git a/crates/ide/src/completion/complete_postfix/format_like.rs b/crates/ide/src/completion/complete_postfix/format_like.rs index 53cb3af39..b663ba171 100644 --- a/crates/ide/src/completion/complete_postfix/format_like.rs +++ b/crates/ide/src/completion/complete_postfix/format_like.rs @@ -20,6 +20,18 @@ use crate::completion::{ }; use syntax::ast::{self, AstToken}; +/// Mapping ("postfix completion item" => "macro to use") +static KINDS: &[(&str, &str)] = &[ + ("fmt", "format!"), + ("panic", "panic!"), + ("println", "println!"), + ("logd", "log::debug!"), + ("logt", "log::trace!"), + ("logi", "log::info!"), + ("logw", "log::warn!"), + ("loge", "log::error!"), +]; + pub(super) fn add_format_like_completions( acc: &mut Completions, ctx: &CompletionContext, @@ -36,11 +48,10 @@ pub(super) fn add_format_like_completions( let mut parser = FormatStrParser::new(input); if parser.parse().is_ok() { - for kind in PostfixKind::all_suggestions() { - let snippet = parser.into_suggestion(*kind); - let (label, detail) = kind.into_description(); + for (label, macro_name) in KINDS { + let snippet = parser.into_suggestion(macro_name); - postfix_snippet(ctx, cap, &dot_receiver, label, detail, &snippet).add_to(acc); + postfix_snippet(ctx, cap, &dot_receiver, label, macro_name, &snippet).add_to(acc); } } } @@ -66,59 +77,6 @@ pub struct FormatStrParser { parsed: bool, } -#[derive(Debug, Clone, Copy)] -pub enum PostfixKind { - Format, - Panic, - Println, - LogDebug, - LogTrace, - LogInfo, - LogWarn, - LogError, -} - -impl PostfixKind { - pub fn all_suggestions() -> &'static [PostfixKind] { - &[ - Self::Format, - Self::Panic, - Self::Println, - Self::LogDebug, - Self::LogTrace, - Self::LogInfo, - Self::LogWarn, - Self::LogError, - ] - } - - pub fn into_description(self) -> (&'static str, &'static str) { - match self { - Self::Format => ("fmt", "format!"), - Self::Panic => ("panic", "panic!"), - Self::Println => ("println", "println!"), - Self::LogDebug => ("logd", "log::debug!"), - Self::LogTrace => ("logt", "log::trace!"), - Self::LogInfo => ("logi", "log::info!"), - Self::LogWarn => ("logw", "log::warn!"), - Self::LogError => ("loge", "log::error!"), - } - } - - pub fn into_macro_name(self) -> &'static str { - match self { - Self::Format => "format!", - Self::Panic => "panic!", - Self::Println => "println!", - Self::LogDebug => "log::debug!", - Self::LogTrace => "log::trace!", - Self::LogInfo => "log::info!", - Self::LogWarn => "log::warn!", - Self::LogError => "log::error!", - } - } -} - #[derive(Debug, Clone, Copy, PartialEq)] enum State { NotExpr, @@ -235,11 +193,11 @@ impl FormatStrParser { Ok(()) } - pub fn into_suggestion(&self, kind: PostfixKind) -> String { + pub fn into_suggestion(&self, macro_name: &str) -> String { assert!(self.parsed, "Attempt to get a suggestion from not parsed expression"); let expressions_as_string = self.extracted_expressions.join(", "); - format!(r#"{}("{}", {})"#, kind.into_macro_name(), self.output, expressions_as_string) + format!(r#"{}("{}", {})"#, macro_name, self.output, expressions_as_string) } } @@ -300,13 +258,13 @@ mod tests { #[test] fn test_into_suggestion() { let test_vector = &[ - (PostfixKind::Println, "{}", r#"println!("{}", $1)"#), + ("println!", "{}", r#"println!("{}", $1)"#), ( - PostfixKind::LogInfo, + "log::info!", "{} {expr} {} {2 + 2}", r#"log::info!("{} {} {} {}", $1, expr, $2, 2 + 2)"#, ), - (PostfixKind::Format, "{expr:?}", r#"format!("{:?}", expr)"#), + ("format!", "{expr:?}", r#"format!("{:?}", expr)"#), ]; for (kind, input, output) in test_vector { -- cgit v1.2.3 From 97f2905dec269891eb81a75cf0d639408a3f7268 Mon Sep 17 00:00:00 2001 From: Igor Aleksanov Date: Fri, 2 Oct 2020 14:51:20 +0300 Subject: Use expect_test to make format_str_parser test more data-driven --- .../src/completion/complete_postfix/format_like.rs | 70 +++++++++++----------- 1 file changed, 35 insertions(+), 35 deletions(-) (limited to 'crates/ide') diff --git a/crates/ide/src/completion/complete_postfix/format_like.rs b/crates/ide/src/completion/complete_postfix/format_like.rs index b663ba171..0287fc803 100644 --- a/crates/ide/src/completion/complete_postfix/format_like.rs +++ b/crates/ide/src/completion/complete_postfix/format_like.rs @@ -204,54 +204,54 @@ impl FormatStrParser { #[cfg(test)] mod tests { use super::*; + use expect_test::{expect, Expect}; + + fn check(input: &str, expect: &Expect) { + let mut parser = FormatStrParser::new((*input).to_owned()); + let outcome_repr = if parser.parse().is_ok() { + // Parsing should be OK, expected repr is "string; expr_1, expr_2". + if parser.extracted_expressions.is_empty() { + parser.output + } else { + format!("{}; {}", parser.output, parser.extracted_expressions.join(", ")) + } + } else { + // Parsing should fail, expected repr is "-". + "-".to_owned() + }; + + expect.assert_eq(&outcome_repr); + } #[test] fn format_str_parser() { let test_vector = &[ - ("no expressions", Some(("no expressions", vec![]))), - ("{expr} is {2 + 2}", Some(("{} is {}", vec!["expr", "2 + 2"]))), - ("{expr:?}", Some(("{:?}", vec!["expr"]))), - ("{malformed", None), - ("malformed}", None), - ("{{correct", Some(("{{correct", vec![]))), - ("correct}}", Some(("correct}}", vec![]))), - ("{correct}}}", Some(("{}}}", vec!["correct"]))), - ("{correct}}}}}", Some(("{}}}}}", vec!["correct"]))), - ("{incorrect}}", None), - ("placeholders {} {}", Some(("placeholders {} {}", vec!["$1", "$2"]))), - ("mixed {} {2 + 2} {}", Some(("mixed {} {} {}", vec!["$1", "2 + 2", "$2"]))), + ("no expressions", expect![["no expressions"]]), + ("{expr} is {2 + 2}", expect![["{} is {}; expr, 2 + 2"]]), + ("{expr:?}", expect![["{:?}; expr"]]), + ("{malformed", expect![["-"]]), + ("malformed}", expect![["-"]]), + ("{{correct", expect![["{{correct"]]), + ("correct}}", expect![["correct}}"]]), + ("{correct}}}", expect![["{}}}; correct"]]), + ("{correct}}}}}", expect![["{}}}}}; correct"]]), + ("{incorrect}}", expect![["-"]]), + ("placeholders {} {}", expect![["placeholders {} {}; $1, $2"]]), + ("mixed {} {2 + 2} {}", expect![["mixed {} {} {}; $1, 2 + 2, $2"]]), ( "{SomeStruct { val_a: 0, val_b: 1 }}", - Some(("{}", vec!["SomeStruct { val_a: 0, val_b: 1 }"])), + expect![["{}; SomeStruct { val_a: 0, val_b: 1 }"]], ), - ("{expr:?} is {2.32f64:.5}", Some(("{:?} is {:.5}", vec!["expr", "2.32f64"]))), + ("{expr:?} is {2.32f64:.5}", expect![["{:?} is {:.5}; expr, 2.32f64"]]), ( "{SomeStruct { val_a: 0, val_b: 1 }:?}", - Some(("{:?}", vec!["SomeStruct { val_a: 0, val_b: 1 }"])), + expect![["{:?}; SomeStruct { val_a: 0, val_b: 1 }"]], ), - ("{ 2 + 2 }", Some(("{}", vec!["2 + 2"]))), + ("{ 2 + 2 }", expect![["{}; 2 + 2"]]), ]; for (input, output) in test_vector { - let mut parser = FormatStrParser::new((*input).to_owned()); - let outcome = parser.parse(); - - if let Some((result_str, result_args)) = output { - assert!( - outcome.is_ok(), - "Outcome is error for input: {}, but the expected outcome is {:?}", - input, - output - ); - assert_eq!(parser.output, *result_str); - assert_eq!(&parser.extracted_expressions, result_args); - } else { - assert!( - outcome.is_err(), - "Outcome is OK for input: {}, but the expected outcome is error", - input - ); - } + check(input, output) } } -- cgit v1.2.3