Skip to content

Commit d6f3ff1

Browse files
Fix postfix completions inside macros
Previously the receiver text was taken directly from the AST, which in macros is missing trivia, leading to corruption (or just unintended replacement of user code). Now we upmap the range, and extract the original file text in it.
1 parent f01f900 commit d6f3ff1

File tree

1 file changed

+64
-27
lines changed

1 file changed

+64
-27
lines changed

crates/ide-completion/src/completions/postfix.rs

+64-27
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,18 @@
22
33
mod format_like;
44

5-
use hir::ItemInNs;
6-
use ide_db::text_edit::TextEdit;
5+
use base_db::SourceDatabase;
6+
use hir::{ItemInNs, Semantics};
77
use ide_db::{
88
documentation::{Documentation, HasDocs},
99
imports::insert_use::ImportScope,
10+
text_edit::TextEdit,
1011
ty_filter::TryEnum,
11-
SnippetCap,
12+
RootDatabase, SnippetCap,
1213
};
1314
use stdx::never;
1415
use syntax::{
15-
ast::{self, make, AstNode, AstToken},
16+
ast::{self, AstNode, AstToken},
1617
SyntaxKind::{BLOCK_EXPR, EXPR_STMT, FOR_EXPR, IF_EXPR, LOOP_EXPR, STMT_LIST, WHILE_EXPR},
1718
TextRange, TextSize,
1819
};
@@ -48,7 +49,8 @@ pub(crate) fn complete_postfix(
4849
};
4950
let expr_ctx = &dot_access.ctx;
5051

51-
let receiver_text = get_receiver_text(dot_receiver, receiver_is_ambiguous_float_literal);
52+
let receiver_text =
53+
get_receiver_text(&ctx.sema, dot_receiver, receiver_is_ambiguous_float_literal);
5254

5355
let cap = match ctx.config.snippet_cap {
5456
Some(it) => it,
@@ -172,13 +174,15 @@ pub(crate) fn complete_postfix(
172174
// The rest of the postfix completions create an expression that moves an argument,
173175
// so it's better to consider references now to avoid breaking the compilation
174176

175-
let (dot_receiver, node_to_replace_with) = include_references(dot_receiver);
176-
let receiver_text =
177-
get_receiver_text(&node_to_replace_with, receiver_is_ambiguous_float_literal);
178-
let postfix_snippet = match build_postfix_snippet_builder(ctx, cap, &dot_receiver) {
179-
Some(it) => it,
180-
None => return,
181-
};
177+
let (dot_receiver_including_refs, prefix) = include_references(dot_receiver);
178+
let mut receiver_text =
179+
get_receiver_text(&ctx.sema, dot_receiver, receiver_is_ambiguous_float_literal);
180+
receiver_text.insert_str(0, &prefix);
181+
let postfix_snippet =
182+
match build_postfix_snippet_builder(ctx, cap, &dot_receiver_including_refs) {
183+
Some(it) => it,
184+
None => return,
185+
};
182186

183187
if !ctx.config.snippets.is_empty() {
184188
add_custom_postfix_completions(acc, ctx, &postfix_snippet, &receiver_text);
@@ -222,7 +226,7 @@ pub(crate) fn complete_postfix(
222226
postfix_snippet("call", "function(expr)", &format!("${{1}}({receiver_text})"))
223227
.add_to(acc, ctx.db);
224228

225-
if let Some(parent) = dot_receiver.syntax().parent().and_then(|p| p.parent()) {
229+
if let Some(parent) = dot_receiver_including_refs.syntax().parent().and_then(|p| p.parent()) {
226230
if matches!(parent.kind(), STMT_LIST | EXPR_STMT) {
227231
postfix_snippet("let", "let", &format!("let $0 = {receiver_text};"))
228232
.add_to(acc, ctx.db);
@@ -231,9 +235,9 @@ pub(crate) fn complete_postfix(
231235
}
232236
}
233237

234-
if let ast::Expr::Literal(literal) = dot_receiver.clone() {
238+
if let ast::Expr::Literal(literal) = dot_receiver_including_refs.clone() {
235239
if let Some(literal_text) = ast::String::cast(literal.token()) {
236-
add_format_like_completions(acc, ctx, &dot_receiver, cap, &literal_text);
240+
add_format_like_completions(acc, ctx, &dot_receiver_including_refs, cap, &literal_text);
237241
}
238242
}
239243

@@ -260,14 +264,20 @@ pub(crate) fn complete_postfix(
260264
}
261265
}
262266

263-
fn get_receiver_text(receiver: &ast::Expr, receiver_is_ambiguous_float_literal: bool) -> String {
264-
let mut text = if receiver_is_ambiguous_float_literal {
265-
let text = receiver.syntax().text();
266-
let without_dot = ..text.len() - TextSize::of('.');
267-
text.slice(without_dot).to_string()
268-
} else {
269-
receiver.to_string()
267+
fn get_receiver_text(
268+
sema: &Semantics<'_, RootDatabase>,
269+
receiver: &ast::Expr,
270+
receiver_is_ambiguous_float_literal: bool,
271+
) -> String {
272+
// Do not just call `receiver.to_string()`, as that will mess up whitespaces inside macros.
273+
let Some(mut range) = sema.original_range_opt(receiver.syntax()) else {
274+
return receiver.to_string();
270275
};
276+
if receiver_is_ambiguous_float_literal {
277+
range.range = TextRange::at(range.range.start(), range.range.len() - TextSize::of('.'))
278+
}
279+
let file_text = sema.db.file_text(range.file_id.file_id());
280+
let mut text = file_text[range.range].to_owned();
271281

272282
// The receiver texts should be interpreted as-is, as they are expected to be
273283
// normal Rust expressions.
@@ -284,15 +294,15 @@ fn escape_snippet_bits(text: &mut String) {
284294
stdx::replace(text, '$', "\\$");
285295
}
286296

287-
fn include_references(initial_element: &ast::Expr) -> (ast::Expr, ast::Expr) {
297+
fn include_references(initial_element: &ast::Expr) -> (ast::Expr, String) {
288298
let mut resulting_element = initial_element.clone();
289299

290300
while let Some(field_expr) = resulting_element.syntax().parent().and_then(ast::FieldExpr::cast)
291301
{
292302
resulting_element = ast::Expr::from(field_expr);
293303
}
294304

295-
let mut new_element_opt = initial_element.clone();
305+
let mut prefix = String::new();
296306

297307
while let Some(parent_deref_element) =
298308
resulting_element.syntax().parent().and_then(ast::PrefixExpr::cast)
@@ -303,7 +313,7 @@ fn include_references(initial_element: &ast::Expr) -> (ast::Expr, ast::Expr) {
303313

304314
resulting_element = ast::Expr::from(parent_deref_element);
305315

306-
new_element_opt = make::expr_prefix(syntax::T![*], new_element_opt).into();
316+
prefix.insert(0, '*');
307317
}
308318

309319
if let Some(first_ref_expr) = resulting_element.syntax().parent().and_then(ast::RefExpr::cast) {
@@ -317,15 +327,15 @@ fn include_references(initial_element: &ast::Expr) -> (ast::Expr, ast::Expr) {
317327
let exclusive = parent_ref_element.mut_token().is_some();
318328
resulting_element = ast::Expr::from(parent_ref_element);
319329

320-
new_element_opt = make::expr_ref(new_element_opt, exclusive);
330+
prefix.insert_str(0, if exclusive { "&mut " } else { "&" });
321331
}
322332
} else {
323333
// If we do not find any ref expressions, restore
324334
// all the progress of tree climbing
325335
resulting_element = initial_element.clone();
326336
}
327337

328-
(resulting_element, new_element_opt)
338+
(resulting_element, prefix)
329339
}
330340

331341
fn build_postfix_snippet_builder<'ctx>(
@@ -901,4 +911,31 @@ fn main() {
901911
"#,
902912
);
903913
}
914+
915+
#[test]
916+
fn inside_macro() {
917+
check_edit(
918+
"box",
919+
r#"
920+
macro_rules! assert {
921+
( $it:expr $(,)? ) => { $it };
922+
}
923+
924+
fn foo() {
925+
let a = true;
926+
assert!(if a == false { true } else { false }.$0);
927+
}
928+
"#,
929+
r#"
930+
macro_rules! assert {
931+
( $it:expr $(,)? ) => { $it };
932+
}
933+
934+
fn foo() {
935+
let a = true;
936+
assert!(Box::new(if a == false { true } else { false }));
937+
}
938+
"#,
939+
);
940+
}
904941
}

0 commit comments

Comments
 (0)