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 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