Merge pull request #19129 from ChayimFriedman2/snippet-macro

fix: Fix postfix completions inside macros
This commit is contained in:
Lukas Wirth 2025-02-12 11:55:46 +00:00 committed by GitHub
commit 5235caf402
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -2,17 +2,18 @@
mod format_like;
use hir::ItemInNs;
use ide_db::text_edit::TextEdit;
use base_db::SourceDatabase;
use hir::{ItemInNs, Semantics};
use ide_db::{
documentation::{Documentation, HasDocs},
imports::insert_use::ImportScope,
text_edit::TextEdit,
ty_filter::TryEnum,
SnippetCap,
RootDatabase, SnippetCap,
};
use stdx::never;
use syntax::{
ast::{self, make, AstNode, AstToken},
ast::{self, AstNode, AstToken},
SyntaxKind::{BLOCK_EXPR, EXPR_STMT, FOR_EXPR, IF_EXPR, LOOP_EXPR, STMT_LIST, WHILE_EXPR},
TextRange, TextSize,
};
@ -48,7 +49,8 @@ pub(crate) fn complete_postfix(
};
let expr_ctx = &dot_access.ctx;
let receiver_text = get_receiver_text(dot_receiver, receiver_is_ambiguous_float_literal);
let receiver_text =
get_receiver_text(&ctx.sema, dot_receiver, receiver_is_ambiguous_float_literal);
let cap = match ctx.config.snippet_cap {
Some(it) => it,
@ -172,13 +174,15 @@ pub(crate) fn complete_postfix(
// The rest of the postfix completions create an expression that moves an argument,
// so it's better to consider references now to avoid breaking the compilation
let (dot_receiver, node_to_replace_with) = include_references(dot_receiver);
let receiver_text =
get_receiver_text(&node_to_replace_with, receiver_is_ambiguous_float_literal);
let postfix_snippet = match build_postfix_snippet_builder(ctx, cap, &dot_receiver) {
Some(it) => it,
None => return,
};
let (dot_receiver_including_refs, prefix) = include_references(dot_receiver);
let mut receiver_text =
get_receiver_text(&ctx.sema, dot_receiver, receiver_is_ambiguous_float_literal);
receiver_text.insert_str(0, &prefix);
let postfix_snippet =
match build_postfix_snippet_builder(ctx, cap, &dot_receiver_including_refs) {
Some(it) => it,
None => return,
};
if !ctx.config.snippets.is_empty() {
add_custom_postfix_completions(acc, ctx, &postfix_snippet, &receiver_text);
@ -222,7 +226,7 @@ pub(crate) fn complete_postfix(
postfix_snippet("call", "function(expr)", &format!("${{1}}({receiver_text})"))
.add_to(acc, ctx.db);
if let Some(parent) = dot_receiver.syntax().parent().and_then(|p| p.parent()) {
if let Some(parent) = dot_receiver_including_refs.syntax().parent().and_then(|p| p.parent()) {
if matches!(parent.kind(), STMT_LIST | EXPR_STMT) {
postfix_snippet("let", "let", &format!("let $0 = {receiver_text};"))
.add_to(acc, ctx.db);
@ -231,9 +235,9 @@ pub(crate) fn complete_postfix(
}
}
if let ast::Expr::Literal(literal) = dot_receiver.clone() {
if let ast::Expr::Literal(literal) = dot_receiver_including_refs.clone() {
if let Some(literal_text) = ast::String::cast(literal.token()) {
add_format_like_completions(acc, ctx, &dot_receiver, cap, &literal_text);
add_format_like_completions(acc, ctx, &dot_receiver_including_refs, cap, &literal_text);
}
}
@ -260,14 +264,20 @@ pub(crate) fn complete_postfix(
}
}
fn get_receiver_text(receiver: &ast::Expr, receiver_is_ambiguous_float_literal: bool) -> String {
let mut text = if receiver_is_ambiguous_float_literal {
let text = receiver.syntax().text();
let without_dot = ..text.len() - TextSize::of('.');
text.slice(without_dot).to_string()
} else {
receiver.to_string()
fn get_receiver_text(
sema: &Semantics<'_, RootDatabase>,
receiver: &ast::Expr,
receiver_is_ambiguous_float_literal: bool,
) -> String {
// Do not just call `receiver.to_string()`, as that will mess up whitespaces inside macros.
let Some(mut range) = sema.original_range_opt(receiver.syntax()) else {
return receiver.to_string();
};
if receiver_is_ambiguous_float_literal {
range.range = TextRange::at(range.range.start(), range.range.len() - TextSize::of('.'))
}
let file_text = sema.db.file_text(range.file_id.file_id());
let mut text = file_text[range.range].to_owned();
// The receiver texts should be interpreted as-is, as they are expected to be
// normal Rust expressions.
@ -284,7 +294,7 @@ fn escape_snippet_bits(text: &mut String) {
stdx::replace(text, '$', "\\$");
}
fn include_references(initial_element: &ast::Expr) -> (ast::Expr, ast::Expr) {
fn include_references(initial_element: &ast::Expr) -> (ast::Expr, String) {
let mut resulting_element = initial_element.clone();
while let Some(field_expr) = resulting_element.syntax().parent().and_then(ast::FieldExpr::cast)
@ -292,7 +302,7 @@ fn include_references(initial_element: &ast::Expr) -> (ast::Expr, ast::Expr) {
resulting_element = ast::Expr::from(field_expr);
}
let mut new_element_opt = initial_element.clone();
let mut prefix = String::new();
while let Some(parent_deref_element) =
resulting_element.syntax().parent().and_then(ast::PrefixExpr::cast)
@ -303,7 +313,7 @@ fn include_references(initial_element: &ast::Expr) -> (ast::Expr, ast::Expr) {
resulting_element = ast::Expr::from(parent_deref_element);
new_element_opt = make::expr_prefix(syntax::T![*], new_element_opt).into();
prefix.insert(0, '*');
}
if let Some(first_ref_expr) = resulting_element.syntax().parent().and_then(ast::RefExpr::cast) {
@ -317,7 +327,7 @@ fn include_references(initial_element: &ast::Expr) -> (ast::Expr, ast::Expr) {
let exclusive = parent_ref_element.mut_token().is_some();
resulting_element = ast::Expr::from(parent_ref_element);
new_element_opt = make::expr_ref(new_element_opt, exclusive);
prefix.insert_str(0, if exclusive { "&mut " } else { "&" });
}
} else {
// If we do not find any ref expressions, restore
@ -325,7 +335,7 @@ fn include_references(initial_element: &ast::Expr) -> (ast::Expr, ast::Expr) {
resulting_element = initial_element.clone();
}
(resulting_element, new_element_opt)
(resulting_element, prefix)
}
fn build_postfix_snippet_builder<'ctx>(
@ -901,4 +911,31 @@ fn main() {
"#,
);
}
#[test]
fn inside_macro() {
check_edit(
"box",
r#"
macro_rules! assert {
( $it:expr $(,)? ) => { $it };
}
fn foo() {
let a = true;
assert!(if a == false { true } else { false }.$0);
}
"#,
r#"
macro_rules! assert {
( $it:expr $(,)? ) => { $it };
}
fn foo() {
let a = true;
assert!(Box::new(if a == false { true } else { false }));
}
"#,
);
}
}