From 19c10672023ead0c1d64486154b6c4145b649568 Mon Sep 17 00:00:00 2001 From: Igor Aleksanov Date: Sun, 25 Oct 2020 10:59:15 +0300 Subject: Reorganize completions structure --- .../src/completions/postfix/format_like.rs | 279 +++++++++++++++++++++ 1 file changed, 279 insertions(+) create mode 100644 crates/completion/src/completions/postfix/format_like.rs (limited to 'crates/completion/src/completions/postfix/format_like.rs') diff --git a/crates/completion/src/completions/postfix/format_like.rs b/crates/completion/src/completions/postfix/format_like.rs new file mode 100644 index 000000000..8a91665b6 --- /dev/null +++ b/crates/completion/src/completions/postfix/format_like.rs @@ -0,0 +1,279 @@ +// Feature: Format String Completion. +// +// `"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 crate::{ + completions::postfix::postfix_snippet, config::SnippetCap, context::CompletionContext, + item::Completions, +}; +use syntax::ast::{self, AstToken}; + +/// Mapping ("postfix completion item" => "macro to use") +static KINDS: &[(&str, &str)] = &[ + ("fmt", "format!"), + ("panic", "panic!"), + ("println", "println!"), + ("eprintln", "eprintln!"), + ("logd", "log::debug!"), + ("logt", "log::trace!"), + ("logi", "log::info!"), + ("logw", "log::warn!"), + ("loge", "log::error!"), +]; + +pub(crate) fn add_format_like_completions( + acc: &mut Completions, + ctx: &CompletionContext, + dot_receiver: &ast::Expr, + cap: SnippetCap, + receiver_text: &ast::String, +) { + let input = match string_literal_contents(receiver_text) { + // It's not a string literal, do not parse input. + Some(input) => input, + None => return, + }; + + let mut parser = FormatStrParser::new(input); + + if parser.parse().is_ok() { + for (label, macro_name) in KINDS { + let snippet = parser.into_suggestion(macro_name); + + postfix_snippet(ctx, cap, &dot_receiver, label, macro_name, &snippet).add_to(acc); + } + } +} + +/// Checks whether provided item is a string literal. +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()); + } + + None +} + +/// 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, + output: String, + extracted_expressions: Vec, + state: State, + parsed: bool, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +enum State { + NotExpr, + MaybeExpr, + Expr, + MaybeIncorrect, + FormatOpts, +} + +impl FormatStrParser { + pub fn new(input: String) -> 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 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. + // "{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!("${}", placeholder_id)); + placeholder_id += 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, 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#"{}("{}", {})"#, macro_name, self.output, expressions_as_string) + } +} + +#[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", 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 }}", + expect![["{}; SomeStruct { val_a: 0, val_b: 1 }"]], + ), + ("{expr:?} is {2.32f64:.5}", expect![["{:?} is {:.5}; expr, 2.32f64"]]), + ( + "{SomeStruct { val_a: 0, val_b: 1 }:?}", + expect![["{:?}; SomeStruct { val_a: 0, val_b: 1 }"]], + ), + ("{ 2 + 2 }", expect![["{}; 2 + 2"]]), + ]; + + for (input, output) in test_vector { + check(input, output) + } + } + + #[test] + fn test_into_suggestion() { + let test_vector = &[ + ("println!", "{}", r#"println!("{}", $1)"#), + ("eprintln!", "{}", r#"eprintln!("{}", $1)"#), + ( + "log::info!", + "{} {expr} {} {2 + 2}", + r#"log::info!("{} {} {} {}", $1, expr, $2, 2 + 2)"#, + ), + ("format!", "{expr:?}", r#"format!("{:?}", expr)"#), + ]; + + for (kind, input, output) in test_vector { + 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