Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Fix postfix completions inside macros #19129

Merged
merged 1 commit into from
Feb 12, 2025
Merged
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
91 changes: 64 additions & 27 deletions crates/ide-completion/src/completions/postfix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -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);
}
}

Expand All @@ -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.
Expand All @@ -284,15 +294,15 @@ 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) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had to change this method so that the node we use will be derived from the Semantics.

let mut resulting_element = initial_element.clone();

while let Some(field_expr) = resulting_element.syntax().parent().and_then(ast::FieldExpr::cast)
{
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)
Expand All @@ -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) {
Expand All @@ -317,15 +327,15 @@ 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
// all the progress of tree climbing
resulting_element = initial_element.clone();
}

(resulting_element, new_element_opt)
(resulting_element, prefix)
}

fn build_postfix_snippet_builder<'ctx>(
Expand Down Expand Up @@ -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 }));
}
"#,
);
}
}