Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 60 additions & 1 deletion crates/ide/src/completion/complete_postfix.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 changes: 277 additions & 0 deletions crates/ide/src/completion/complete_postfix/format_like.rs
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
2 changes: 1 addition & 1 deletion crates/ide/src/completion/completion_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -469,7 +469,7 @@ impl<'a> CompletionContext<'a> {
}
} else {
false
}
};
}
if let Some(method_call_expr) = ast::MethodCallExpr::cast(parent) {
// As above
Expand Down