Add postfix completion for format-like string literals

This commit is contained in:
Igor Aleksanov 2020-09-12 17:14:17 +03:00
parent c01cd6e3ed
commit ea320141c6
3 changed files with 376 additions and 1 deletions
crates/ide/src/completion

@ -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) }"#,
);
}
}

@ -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<String>,
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<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 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);
}
}
}

@ -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<ast::Attr>,
@ -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