1 //! Postfix completions, like `Ok(10).ifl$0` => `if let Ok() = Ok(10) { $0 }`.
2
3 mod format_like;
4
5 use hir::{Documentation, HasAttrs};
6 use ide_db::{imports::insert_use::ImportScope, ty_filter::TryEnum, SnippetCap};
7 use syntax::{
8 ast::{self, make, AstNode, AstToken},
9 SyntaxKind::{BLOCK_EXPR, EXPR_STMT, FOR_EXPR, IF_EXPR, LOOP_EXPR, STMT_LIST, WHILE_EXPR},
10 TextRange, TextSize,
11 };
12 use text_edit::TextEdit;
13
14 use crate::{
15 completions::postfix::format_like::add_format_like_completions,
16 context::{CompletionContext, DotAccess, DotAccessKind},
17 item::{Builder, CompletionRelevancePostfixMatch},
18 CompletionItem, CompletionItemKind, CompletionRelevance, Completions, SnippetScope,
19 };
20
complete_postfix( acc: &mut Completions, ctx: &CompletionContext<'_>, dot_access: &DotAccess, )21 pub(crate) fn complete_postfix(
22 acc: &mut Completions,
23 ctx: &CompletionContext<'_>,
24 dot_access: &DotAccess,
25 ) {
26 if !ctx.config.enable_postfix_completions {
27 return;
28 }
29
30 let (dot_receiver, receiver_ty, receiver_is_ambiguous_float_literal) = match dot_access {
31 DotAccess { receiver_ty: Some(ty), receiver: Some(it), kind, .. } => (
32 it,
33 &ty.original,
34 match *kind {
35 DotAccessKind::Field { receiver_is_ambiguous_float_literal } => {
36 receiver_is_ambiguous_float_literal
37 }
38 DotAccessKind::Method { .. } => false,
39 },
40 ),
41 _ => return,
42 };
43
44 let receiver_text = get_receiver_text(dot_receiver, receiver_is_ambiguous_float_literal);
45
46 let cap = match ctx.config.snippet_cap {
47 Some(it) => it,
48 None => return,
49 };
50
51 let postfix_snippet = match build_postfix_snippet_builder(ctx, cap, dot_receiver) {
52 Some(it) => it,
53 None => return,
54 };
55
56 if let Some(drop_trait) = ctx.famous_defs().core_ops_Drop() {
57 if receiver_ty.impls_trait(ctx.db, drop_trait, &[]) {
58 if let &[hir::AssocItem::Function(drop_fn)] = &*drop_trait.items(ctx.db) {
59 cov_mark::hit!(postfix_drop_completion);
60 // FIXME: check that `drop` is in scope, use fully qualified path if it isn't/if shadowed
61 let mut item = postfix_snippet(
62 "drop",
63 "fn drop(&mut self)",
64 &format!("drop($0{receiver_text})"),
65 );
66 item.set_documentation(drop_fn.docs(ctx.db));
67 item.add_to(acc, ctx.db);
68 }
69 }
70 }
71
72 let try_enum = TryEnum::from_ty(&ctx.sema, &receiver_ty.strip_references());
73 if let Some(try_enum) = &try_enum {
74 match try_enum {
75 TryEnum::Result => {
76 postfix_snippet(
77 "ifl",
78 "if let Ok {}",
79 &format!("if let Ok($1) = {receiver_text} {{\n $0\n}}"),
80 )
81 .add_to(acc, ctx.db);
82
83 postfix_snippet(
84 "while",
85 "while let Ok {}",
86 &format!("while let Ok($1) = {receiver_text} {{\n $0\n}}"),
87 )
88 .add_to(acc, ctx.db);
89 }
90 TryEnum::Option => {
91 postfix_snippet(
92 "ifl",
93 "if let Some {}",
94 &format!("if let Some($1) = {receiver_text} {{\n $0\n}}"),
95 )
96 .add_to(acc, ctx.db);
97
98 postfix_snippet(
99 "while",
100 "while let Some {}",
101 &format!("while let Some($1) = {receiver_text} {{\n $0\n}}"),
102 )
103 .add_to(acc, ctx.db);
104 }
105 }
106 } else if receiver_ty.is_bool() || receiver_ty.is_unknown() {
107 postfix_snippet("if", "if expr {}", &format!("if {receiver_text} {{\n $0\n}}"))
108 .add_to(acc, ctx.db);
109 postfix_snippet("while", "while expr {}", &format!("while {receiver_text} {{\n $0\n}}"))
110 .add_to(acc, ctx.db);
111 postfix_snippet("not", "!expr", &format!("!{receiver_text}")).add_to(acc, ctx.db);
112 } else if let Some(trait_) = ctx.famous_defs().core_iter_IntoIterator() {
113 if receiver_ty.impls_trait(ctx.db, trait_, &[]) {
114 postfix_snippet(
115 "for",
116 "for ele in expr {}",
117 &format!("for ele in {receiver_text} {{\n $0\n}}"),
118 )
119 .add_to(acc, ctx.db);
120 }
121 }
122
123 postfix_snippet("ref", "&expr", &format!("&{receiver_text}")).add_to(acc, ctx.db);
124 postfix_snippet("refm", "&mut expr", &format!("&mut {receiver_text}")).add_to(acc, ctx.db);
125
126 let mut unsafe_should_be_wrapped = true;
127 if dot_receiver.syntax().kind() == BLOCK_EXPR {
128 unsafe_should_be_wrapped = false;
129 if let Some(parent) = dot_receiver.syntax().parent() {
130 if matches!(parent.kind(), IF_EXPR | WHILE_EXPR | LOOP_EXPR | FOR_EXPR) {
131 unsafe_should_be_wrapped = true;
132 }
133 }
134 };
135 let unsafe_completion_string = if unsafe_should_be_wrapped {
136 format!("unsafe {{ {receiver_text} }}")
137 } else {
138 format!("unsafe {receiver_text}")
139 };
140 postfix_snippet("unsafe", "unsafe {}", &unsafe_completion_string).add_to(acc, ctx.db);
141
142 // The rest of the postfix completions create an expression that moves an argument,
143 // so it's better to consider references now to avoid breaking the compilation
144
145 let (dot_receiver, node_to_replace_with) = include_references(dot_receiver);
146 let receiver_text =
147 get_receiver_text(&node_to_replace_with, receiver_is_ambiguous_float_literal);
148 let postfix_snippet = match build_postfix_snippet_builder(ctx, cap, &dot_receiver) {
149 Some(it) => it,
150 None => return,
151 };
152
153 if !ctx.config.snippets.is_empty() {
154 add_custom_postfix_completions(acc, ctx, &postfix_snippet, &receiver_text);
155 }
156
157 match try_enum {
158 Some(try_enum) => match try_enum {
159 TryEnum::Result => {
160 postfix_snippet(
161 "match",
162 "match expr {}",
163 &format!("match {receiver_text} {{\n Ok(${{1:_}}) => {{$2}},\n Err(${{3:_}}) => {{$0}},\n}}"),
164 )
165 .add_to(acc, ctx.db);
166 }
167 TryEnum::Option => {
168 postfix_snippet(
169 "match",
170 "match expr {}",
171 &format!(
172 "match {receiver_text} {{\n Some(${{1:_}}) => {{$2}},\n None => {{$0}},\n}}"
173 ),
174 )
175 .add_to(acc, ctx.db);
176 }
177 },
178 None => {
179 postfix_snippet(
180 "match",
181 "match expr {}",
182 &format!("match {receiver_text} {{\n ${{1:_}} => {{$0}},\n}}"),
183 )
184 .add_to(acc, ctx.db);
185 }
186 }
187
188 postfix_snippet("box", "Box::new(expr)", &format!("Box::new({receiver_text})"))
189 .add_to(acc, ctx.db);
190 postfix_snippet("dbg", "dbg!(expr)", &format!("dbg!({receiver_text})")).add_to(acc, ctx.db); // fixme
191 postfix_snippet("dbgr", "dbg!(&expr)", &format!("dbg!(&{receiver_text})")).add_to(acc, ctx.db);
192 postfix_snippet("call", "function(expr)", &format!("${{1}}({receiver_text})"))
193 .add_to(acc, ctx.db);
194
195 if let Some(parent) = dot_receiver.syntax().parent().and_then(|p| p.parent()) {
196 if matches!(parent.kind(), STMT_LIST | EXPR_STMT) {
197 postfix_snippet("let", "let", &format!("let $0 = {receiver_text};"))
198 .add_to(acc, ctx.db);
199 postfix_snippet("letm", "let mut", &format!("let mut $0 = {receiver_text};"))
200 .add_to(acc, ctx.db);
201 }
202 }
203
204 if let ast::Expr::Literal(literal) = dot_receiver.clone() {
205 if let Some(literal_text) = ast::String::cast(literal.token()) {
206 add_format_like_completions(acc, ctx, &dot_receiver, cap, &literal_text);
207 }
208 }
209 }
210
get_receiver_text(receiver: &ast::Expr, receiver_is_ambiguous_float_literal: bool) -> String211 fn get_receiver_text(receiver: &ast::Expr, receiver_is_ambiguous_float_literal: bool) -> String {
212 let text = if receiver_is_ambiguous_float_literal {
213 let text = receiver.syntax().text();
214 let without_dot = ..text.len() - TextSize::of('.');
215 text.slice(without_dot).to_string()
216 } else {
217 receiver.to_string()
218 };
219
220 // The receiver texts should be interpreted as-is, as they are expected to be
221 // normal Rust expressions. We escape '\' and '$' so they don't get treated as
222 // snippet-specific constructs.
223 //
224 // Note that we don't need to escape the other characters that can be escaped,
225 // because they wouldn't be treated as snippet-specific constructs without '$'.
226 text.replace('\\', "\\\\").replace('$', "\\$")
227 }
228
229 fn include_references(initial_element: &ast::Expr) -> (ast::Expr, ast::Expr) {
230 let mut resulting_element = initial_element.clone();
231
232 while let Some(field_expr) = resulting_element.syntax().parent().and_then(ast::FieldExpr::cast)
233 {
234 resulting_element = ast::Expr::from(field_expr);
235 }
236
237 let mut new_element_opt = initial_element.clone();
238
239 if let Some(first_ref_expr) = resulting_element.syntax().parent().and_then(ast::RefExpr::cast) {
240 if let Some(expr) = first_ref_expr.expr() {
241 resulting_element = expr;
242 }
243
244 while let Some(parent_ref_element) =
245 resulting_element.syntax().parent().and_then(ast::RefExpr::cast)
246 {
247 resulting_element = ast::Expr::from(parent_ref_element);
248
249 new_element_opt = make::expr_ref(new_element_opt, false);
250 }
251 } else {
252 // If we do not find any ref expressions, restore
253 // all the progress of tree climbing
254 resulting_element = initial_element.clone();
255 }
256
257 (resulting_element, new_element_opt)
258 }
259
260 fn build_postfix_snippet_builder<'ctx>(
261 ctx: &'ctx CompletionContext<'_>,
262 cap: SnippetCap,
263 receiver: &'ctx ast::Expr,
264 ) -> Option<impl Fn(&str, &str, &str) -> Builder + 'ctx> {
265 let receiver_range = ctx.sema.original_range_opt(receiver.syntax())?.range;
266 if ctx.source_range().end() < receiver_range.start() {
267 // This shouldn't happen, yet it does. I assume this might be due to an incorrect token mapping.
268 return None;
269 }
270 let delete_range = TextRange::new(receiver_range.start(), ctx.source_range().end());
271
272 // Wrapping impl Fn in an option ruins lifetime inference for the parameters in a way that
273 // can't be annotated for the closure, hence fix it by constructing it without the Option first
274 fn build<'ctx>(
275 ctx: &'ctx CompletionContext<'_>,
276 cap: SnippetCap,
277 delete_range: TextRange,
278 ) -> impl Fn(&str, &str, &str) -> Builder + 'ctx {
279 move |label, detail, snippet| {
280 let edit = TextEdit::replace(delete_range, snippet.to_string());
281 let mut item =
282 CompletionItem::new(CompletionItemKind::Snippet, ctx.source_range(), label);
283 item.detail(detail).snippet_edit(cap, edit);
284 let postfix_match = if ctx.original_token.text() == label {
285 cov_mark::hit!(postfix_exact_match_is_high_priority);
286 Some(CompletionRelevancePostfixMatch::Exact)
287 } else {
288 cov_mark::hit!(postfix_inexact_match_is_low_priority);
289 Some(CompletionRelevancePostfixMatch::NonExact)
290 };
291 let relevance = CompletionRelevance { postfix_match, ..Default::default() };
292 item.set_relevance(relevance);
293 item
294 }
295 }
296 Some(build(ctx, cap, delete_range))
297 }
298
299 fn add_custom_postfix_completions(
300 acc: &mut Completions,
301 ctx: &CompletionContext<'_>,
302 postfix_snippet: impl Fn(&str, &str, &str) -> Builder,
303 receiver_text: &str,
304 ) -> Option<()> {
305 if ImportScope::find_insert_use_container(&ctx.token.parent()?, &ctx.sema).is_none() {
306 return None;
307 }
308 ctx.config.postfix_snippets().filter(|(_, snip)| snip.scope == SnippetScope::Expr).for_each(
309 |(trigger, snippet)| {
310 let imports = match snippet.imports(ctx) {
311 Some(imports) => imports,
312 None => return,
313 };
314 let body = snippet.postfix_snippet(receiver_text);
315 let mut builder =
316 postfix_snippet(trigger, snippet.description.as_deref().unwrap_or_default(), &body);
317 builder.documentation(Documentation::new(format!("```rust\n{body}\n```")));
318 for import in imports.into_iter() {
319 builder.add_import(import);
320 }
321 builder.add_to(acc, ctx.db);
322 },
323 );
324 None
325 }
326
327 #[cfg(test)]
328 mod tests {
329 use expect_test::{expect, Expect};
330
331 use crate::{
332 tests::{check_edit, check_edit_with_config, completion_list, TEST_CONFIG},
333 CompletionConfig, Snippet,
334 };
335
336 fn check(ra_fixture: &str, expect: Expect) {
337 let actual = completion_list(ra_fixture);
338 expect.assert_eq(&actual)
339 }
340
341 #[test]
342 fn postfix_completion_works_for_trivial_path_expression() {
343 check(
344 r#"
main()345 fn main() {
346 let bar = true;
347 bar.$0
348 }
349 "#,
350 expect![[r#"
351 sn box Box::new(expr)
352 sn call function(expr)
353 sn dbg dbg!(expr)
354 sn dbgr dbg!(&expr)
355 sn if if expr {}
356 sn let let
357 sn letm let mut
358 sn match match expr {}
359 sn not !expr
360 sn ref &expr
361 sn refm &mut expr
362 sn unsafe unsafe {}
363 sn while while expr {}
364 "#]],
365 );
366 }
367
368 #[test]
369 fn postfix_completion_works_for_function_calln() {
370 check(
371 r#"
foo(elt: bool) -> bool372 fn foo(elt: bool) -> bool {
373 !elt
374 }
375
376 fn main() {
377 let bar = true;
378 foo(bar.$0)
379 }
380 "#,
381 expect![[r#"
382 sn box Box::new(expr)
383 sn call function(expr)
384 sn dbg dbg!(expr)
385 sn dbgr dbg!(&expr)
386 sn if if expr {}
387 sn match match expr {}
388 sn not !expr
389 sn ref &expr
390 sn refm &mut expr
391 sn unsafe unsafe {}
392 sn while while expr {}
393 "#]],
394 );
395 }
396
397 #[test]
398 fn postfix_type_filtering() {
399 check(
400 r#"
main()401 fn main() {
402 let bar: u8 = 12;
403 bar.$0
404 }
405 "#,
406 expect![[r#"
407 sn box Box::new(expr)
408 sn call function(expr)
409 sn dbg dbg!(expr)
410 sn dbgr dbg!(&expr)
411 sn let let
412 sn letm let mut
413 sn match match expr {}
414 sn ref &expr
415 sn refm &mut expr
416 sn unsafe unsafe {}
417 "#]],
418 )
419 }
420
421 #[test]
422 fn let_middle_block() {
423 check(
424 r#"
425 fn main() {
426 baz.l$0
427 res
428 }
429 "#,
430 expect![[r#"
431 sn box Box::new(expr)
432 sn call function(expr)
433 sn dbg dbg!(expr)
434 sn dbgr dbg!(&expr)
435 sn if if expr {}
436 sn let let
437 sn letm let mut
438 sn match match expr {}
439 sn not !expr
440 sn ref &expr
441 sn refm &mut expr
442 sn unsafe unsafe {}
443 sn while while expr {}
444 "#]],
445 );
446 }
447
448 #[test]
449 fn option_iflet() {
450 check_edit(
451 "ifl",
452 r#"
453 //- minicore: option
main()454 fn main() {
455 let bar = Some(true);
456 bar.$0
457 }
458 "#,
459 r#"
460 fn main() {
461 let bar = Some(true);
462 if let Some($1) = bar {
463 $0
464 }
465 }
466 "#,
467 );
468 }
469
470 #[test]
471 fn result_match() {
472 check_edit(
473 "match",
474 r#"
475 //- minicore: result
main()476 fn main() {
477 let bar = Ok(true);
478 bar.$0
479 }
480 "#,
481 r#"
482 fn main() {
483 let bar = Ok(true);
484 match bar {
485 Ok(${1:_}) => {$2},
486 Err(${3:_}) => {$0},
487 }
488 }
489 "#,
490 );
491 }
492
493 #[test]
494 fn postfix_completion_works_for_ambiguous_float_literal() {
main()495 check_edit("refm", r#"fn main() { 42.$0 }"#, r#"fn main() { &mut 42 }"#)
496 }
497
498 #[test]
499 fn works_in_simple_macro() {
500 check_edit(
501 "dbg",
502 r#"
503 macro_rules! m { ($e:expr) => { $e } }
504 fn main() {
505 let bar: u8 = 12;
506 m!(bar.d$0)
507 }
508 "#,
509 r#"
510 macro_rules! m { ($e:expr) => { $e } }
511 fn main() {
512 let bar: u8 = 12;
513 m!(dbg!(bar))
514 }
515 "#,
516 );
517 }
518
519 #[test]
520 fn postfix_completion_for_references() {
main()521 check_edit("dbg", r#"fn main() { &&42.$0 }"#, r#"fn main() { dbg!(&&42) }"#);
main()522 check_edit("refm", r#"fn main() { &&42.$0 }"#, r#"fn main() { &&&mut 42 }"#);
523 check_edit(
524 "ifl",
525 r#"
526 //- minicore: option
main()527 fn main() {
528 let bar = &Some(true);
529 bar.$0
530 }
531 "#,
532 r#"
533 fn main() {
534 let bar = &Some(true);
535 if let Some($1) = bar {
536 $0
537 }
538 }
539 "#,
540 )
541 }
542
543 #[test]
544 fn postfix_completion_for_unsafe() {
545 check_edit("unsafe", r#"fn main() { foo.$0 }"#, r#"fn main() { unsafe { foo } }"#);
546 check_edit("unsafe", r#"fn main() { { foo }.$0 }"#, r#"fn main() { unsafe { foo } }"#);
547 check_edit(
548 "unsafe",
549 r#"fn main() { if x { foo }.$0 }"#,
550 r#"fn main() { unsafe { if x { foo } } }"#,
551 );
552 check_edit(
553 "unsafe",
554 r#"fn main() { loop { foo }.$0 }"#,
555 r#"fn main() { unsafe { loop { foo } } }"#,
556 );
557 check_edit(
558 "unsafe",
559 r#"fn main() { if true {}.$0 }"#,
560 r#"fn main() { unsafe { if true {} } }"#,
561 );
562 check_edit(
563 "unsafe",
564 r#"fn main() { while true {}.$0 }"#,
565 r#"fn main() { unsafe { while true {} } }"#,
566 );
567 check_edit(
568 "unsafe",
569 r#"fn main() { for i in 0..10 {}.$0 }"#,
570 r#"fn main() { unsafe { for i in 0..10 {} } }"#,
571 );
572 check_edit(
573 "unsafe",
574 r#"fn main() { let x = if true {1} else {2}.$0 }"#,
575 r#"fn main() { let x = unsafe { if true {1} else {2} } }"#,
576 );
577
578 // completion will not be triggered
579 check_edit(
580 "unsafe",
581 r#"fn main() { let x = true else {panic!()}.$0}"#,
582 r#"fn main() { let x = true else {panic!()}.unsafe}"#,
583 );
584 }
585
586 #[test]
587 fn custom_postfix_completion() {
588 let config = CompletionConfig {
589 snippets: vec![Snippet::new(
590 &[],
591 &["break".into()],
592 &["ControlFlow::Break(${receiver})".into()],
593 "",
594 &["core::ops::ControlFlow".into()],
595 crate::SnippetScope::Expr,
596 )
597 .unwrap()],
598 ..TEST_CONFIG
599 };
600
601 check_edit_with_config(
602 config.clone(),
603 "break",
604 r#"
605 //- minicore: try
main()606 fn main() { 42.$0 }
607 "#,
608 r#"
609 use core::ops::ControlFlow;
610
611 fn main() { ControlFlow::Break(42) }
612 "#,
613 );
614
615 // The receiver texts should be escaped, see comments in `get_receiver_text()`
616 // for detail.
617 //
618 // Note that the last argument is what *lsp clients would see* rather than
619 // what users would see. Unescaping happens thereafter.
620 check_edit_with_config(
621 config.clone(),
622 "break",
623 r#"
624 //- minicore: try
main()625 fn main() { '\\'.$0 }
626 "#,
627 r#"
628 use core::ops::ControlFlow;
629
630 fn main() { ControlFlow::Break('\\\\') }
631 "#,
632 );
633
634 check_edit_with_config(
635 config,
636 "break",
637 r#"
638 //- minicore: try
main()639 fn main() {
640 match true {
641 true => "${1:placeholder}",
642 false => "\$",
643 }.$0
644 }
645 "#,
646 r#"
647 use core::ops::ControlFlow;
648
649 fn main() {
650 ControlFlow::Break(match true {
651 true => "\${1:placeholder}",
652 false => "\\\$",
653 })
654 }
655 "#,
656 );
657 }
658
659 #[test]
660 fn postfix_completion_for_format_like_strings() {
661 check_edit(
662 "format",
main()663 r#"fn main() { "{some_var:?}".$0 }"#,
664 r#"fn main() { format!("{some_var:?}") }"#,
665 );
666 check_edit(
667 "panic",
main()668 r#"fn main() { "Panic with {a}".$0 }"#,
669 r#"fn main() { panic!("Panic with {a}") }"#,
670 );
671 check_edit(
672 "println",
main()673 r#"fn main() { "{ 2+2 } { SomeStruct { val: 1, other: 32 } :?}".$0 }"#,
674 r#"fn main() { println!("{} {:?}", 2+2, SomeStruct { val: 1, other: 32 }) }"#,
675 );
676 check_edit(
677 "loge",
main()678 r#"fn main() { "{2+2}".$0 }"#,
679 r#"fn main() { log::error!("{}", 2+2) }"#,
680 );
681 check_edit(
682 "logt",
main()683 r#"fn main() { "{2+2}".$0 }"#,
684 r#"fn main() { log::trace!("{}", 2+2) }"#,
685 );
686 check_edit(
687 "logd",
main()688 r#"fn main() { "{2+2}".$0 }"#,
689 r#"fn main() { log::debug!("{}", 2+2) }"#,
690 );
main()691 check_edit("logi", r#"fn main() { "{2+2}".$0 }"#, r#"fn main() { log::info!("{}", 2+2) }"#);
main()692 check_edit("logw", r#"fn main() { "{2+2}".$0 }"#, r#"fn main() { log::warn!("{}", 2+2) }"#);
693 check_edit(
694 "loge",
main()695 r#"fn main() { "{2+2}".$0 }"#,
696 r#"fn main() { log::error!("{}", 2+2) }"#,
697 );
698 }
699
700 #[test]
701 fn postfix_custom_snippets_completion_for_references() {
702 // https://github.com/rust-lang/rust-analyzer/issues/7929
703
704 let snippet = Snippet::new(
705 &[],
706 &["ok".into()],
707 &["Ok(${receiver})".into()],
708 "",
709 &[],
710 crate::SnippetScope::Expr,
711 )
712 .unwrap();
713
714 check_edit_with_config(
715 CompletionConfig { snippets: vec![snippet.clone()], ..TEST_CONFIG },
716 "ok",
main()717 r#"fn main() { &&42.o$0 }"#,
718 r#"fn main() { Ok(&&42) }"#,
719 );
720
721 check_edit_with_config(
722 CompletionConfig { snippets: vec![snippet.clone()], ..TEST_CONFIG },
723 "ok",
main()724 r#"fn main() { &&42.$0 }"#,
725 r#"fn main() { Ok(&&42) }"#,
726 );
727
728 check_edit_with_config(
729 CompletionConfig { snippets: vec![snippet], ..TEST_CONFIG },
730 "ok",
731 r#"
732 struct A {
733 a: i32,
734 }
735
main()736 fn main() {
737 let a = A {a :1};
738 &a.a.$0
739 }
740 "#,
741 r#"
742 struct A {
743 a: i32,
744 }
745
746 fn main() {
747 let a = A {a :1};
748 Ok(&a.a)
749 }
750 "#,
751 );
752 }
753
754 #[test]
755 fn no_postfix_completions_in_if_block_that_has_an_else() {
756 check(
757 r#"
test()758 fn test() {
759 if true {}.$0 else {}
760 }
761 "#,
762 expect![[r#""#]],
763 );
764 }
765 }
766