mirror of
https://github.com/rust-lang/rust.git
synced 2024-12-05 05:04:24 +00:00
Merge #5988
5988: Postfix completions for fmt-like string literals r=matklad a=popzxc This pull request adds a bunch of new postfix completions for `format`-like string literls. For example, `"{32} {some_var:?}".println` will expand to `println!("{} {:?}", 32, some_var)`. Postfix completions were added for most common format-like macros: - `println` -> `println!(...)` - `fmt` -> `format!(...)` - `panic` -> `panic!(...)` - `log` macros: + `logi` -> `log::info!(...)` + `logw` -> `log::warn!(...)` + `loge` -> `log::error!(...)` + `logt` -> `log::trace!(...)` + `logd` -> `log::debug!(...)` ![fmt_postfix](https://user-images.githubusercontent.com/12111581/92998650-a048af80-f523-11ea-8fd8-410146de8caa.gif) Co-authored-by: Igor Aleksanov <popzxc@yandex.ru>
This commit is contained in:
commit
6574a6f448
@ -1,11 +1,15 @@
|
||||
//! FIXME: write short doc here
|
||||
|
||||
mod format_like;
|
||||
|
||||
use assists::utils::TryEnum;
|
||||
use syntax::{
|
||||
ast::{self, AstNode},
|
||||
ast::{self, AstNode, AstToken},
|
||||
TextRange, TextSize,
|
||||
};
|
||||
use text_edit::TextEdit;
|
||||
|
||||
use self::format_like::add_format_like_completions;
|
||||
use crate::{
|
||||
completion::{
|
||||
completion_config::SnippetCap,
|
||||
@ -207,6 +211,12 @@ pub(super) fn complete_postfix(acc: &mut Completions, ctx: &CompletionContext) {
|
||||
&format!("${{1}}({})", receiver_text),
|
||||
)
|
||||
.add_to(acc);
|
||||
|
||||
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 {
|
||||
@ -392,4 +402,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) }"#,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
277
crates/ide/src/completion/complete_postfix/format_like.rs
Normal file
277
crates/ide/src/completion/complete_postfix/format_like.rs
Normal file
@ -0,0 +1,277 @@
|
||||
// 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 crate::completion::{
|
||||
complete_postfix::postfix_snippet, completion_config::SnippetCap,
|
||||
completion_context::CompletionContext, completion_item::Completions,
|
||||
};
|
||||
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,
|
||||
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<String> {
|
||||
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<String>,
|
||||
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)"#),
|
||||
(
|
||||
"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);
|
||||
}
|
||||
}
|
||||
}
|
@ -469,7 +469,7 @@ impl<'a> CompletionContext<'a> {
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
};
|
||||
}
|
||||
if let Some(method_call_expr) = ast::MethodCallExpr::cast(parent) {
|
||||
// As above
|
||||
|
Loading…
Reference in New Issue
Block a user