Add postfix completion for format-like string literals
This commit is contained in:
parent
c01cd6e3ed
commit
ea320141c6
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) }"#,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
310
crates/ide/src/completion/complete_postfix/format_like.rs
Normal file
310
crates/ide/src/completion/complete_postfix/format_like.rs
Normal file
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user