diff --git a/crates/ide-completion/src/completions/dot.rs b/crates/ide-completion/src/completions/dot.rs index 5bcc867fe181..57e06461099e 100644 --- a/crates/ide-completion/src/completions/dot.rs +++ b/crates/ide-completion/src/completions/dot.rs @@ -26,17 +26,17 @@ pub(crate) fn complete_dot( item.add_to(acc, ctx.db); } - if let DotAccessKind::Method { .. } = dot_access.kind { - cov_mark::hit!(test_no_struct_field_completion_for_method_call); - } else { - complete_fields( - acc, - ctx, - receiver_ty, - |acc, field, ty| acc.add_field(ctx, dot_access, None, field, &ty), - |acc, field, ty| acc.add_tuple_field(ctx, None, field, &ty), - ); - } + let is_field_access = matches!(dot_access.kind, DotAccessKind::Field { .. }); + + complete_fields( + acc, + ctx, + receiver_ty, + |acc, field, ty| acc.add_field(ctx, dot_access, None, field, &ty), + |acc, field, ty| acc.add_tuple_field(ctx, None, field, &ty), + is_field_access, + ); + complete_methods(ctx, receiver_ty, |func| acc.add_method(ctx, dot_access, func, None, None)); } @@ -82,6 +82,7 @@ pub(crate) fn complete_undotted_self( ) }, |acc, field, ty| acc.add_tuple_field(ctx, Some(hir::known::SELF_PARAM), field, &ty), + true, ); complete_methods(ctx, &ty, |func| { acc.add_method( @@ -104,18 +105,23 @@ fn complete_fields( receiver: &hir::Type, mut named_field: impl FnMut(&mut Completions, hir::Field, hir::Type), mut tuple_index: impl FnMut(&mut Completions, usize, hir::Type), + is_field_access: bool, ) { let mut seen_names = FxHashSet::default(); for receiver in receiver.autoderef(ctx.db) { for (field, ty) in receiver.fields(ctx.db) { - if seen_names.insert(field.name(ctx.db)) { + if seen_names.insert(field.name(ctx.db)) + && (is_field_access || ty.is_fn() || ty.is_closure()) + { named_field(acc, field, ty); } } for (i, ty) in receiver.tuple_fields(ctx.db).into_iter().enumerate() { // Tuples are always the last type in a deref chain, so just check if the name is // already seen without inserting into the hashset. - if !seen_names.contains(&hir::Name::new_tuple_field(i)) { + if !seen_names.contains(&hir::Name::new_tuple_field(i)) + && (is_field_access || ty.is_fn() || ty.is_closure()) + { // Tuple fields are always public (tuple struct fields are handled above). tuple_index(acc, i, ty); } @@ -250,7 +256,6 @@ impl A { #[test] fn test_no_struct_field_completion_for_method_call() { - cov_mark::check!(test_no_struct_field_completion_for_method_call); check( r#" struct A { the_field: u32 } @@ -1172,4 +1177,63 @@ impl> Foo { "#]], ); } + + #[test] + fn test_struct_function_field_completion() { + check( + r#" +struct S { va_field: u32, fn_field: fn() } +fn foo() { S { va_field: 0, fn_field: || {} }.fi$0() } +"#, + expect![[r#" + fd fn_field fn() + "#]], + ); + + check_edit( + "fn_field", + r#" +struct S { va_field: u32, fn_field: fn() } +fn foo() { S { va_field: 0, fn_field: || {} }.fi$0() } +"#, + r#" +struct S { va_field: u32, fn_field: fn() } +fn foo() { (S { va_field: 0, fn_field: || {} }.fn_field)() } +"#, + ); + } + + #[test] + fn test_tuple_function_field_completion() { + check( + r#" +struct B(u32, fn()) +fn foo() { + let b = B(0, || {}); + b.$0() +} +"#, + expect![[r#" + fd 1 fn() + "#]], + ); + + check_edit( + "1", + r#" +struct B(u32, fn()) +fn foo() { + let b = B(0, || {}); + b.$0() +} +"#, + r#" +struct B(u32, fn()) +fn foo() { + let b = B(0, || {}); + (b.1)() +} +"#, + ) + } } diff --git a/crates/ide-completion/src/render.rs b/crates/ide-completion/src/render.rs index 00a9081985b8..048730c078d7 100644 --- a/crates/ide-completion/src/render.rs +++ b/crates/ide-completion/src/render.rs @@ -18,9 +18,10 @@ use ide_db::{ RootDatabase, SnippetCap, SymbolKind, }; use syntax::{AstNode, SmolStr, SyntaxKind, TextRange}; +use text_edit::TextEdit; use crate::{ - context::{DotAccess, PathCompletionCtx, PathKind, PatternContext}, + context::{DotAccess, DotAccessKind, PathCompletionCtx, PathKind, PatternContext}, item::{Builder, CompletionRelevanceTypeMatch}, render::{ function::render_fn, @@ -147,7 +148,42 @@ pub(crate) fn render_field( .set_documentation(field.docs(db)) .set_deprecated(is_deprecated) .lookup_by(name); - item.insert_text(field_with_receiver(db, receiver.as_ref(), &escaped_name)); + + let is_field_access = matches!(dot_access.kind, DotAccessKind::Field { .. }); + if !is_field_access || ty.is_fn() || ty.is_closure() { + let mut builder = TextEdit::builder(); + // Using TextEdit, insert '(' before the struct name and ')' before the + // dot access, then comes the field name and optionally insert function + // call parens. + + builder.replace( + ctx.source_range(), + field_with_receiver(db, receiver.as_ref(), &escaped_name).into(), + ); + + let expected_fn_type = + ctx.completion.expected_type.as_ref().is_some_and(|ty| ty.is_fn() || ty.is_closure()); + + if !expected_fn_type { + if let Some(receiver) = &dot_access.receiver { + if let Some(receiver) = ctx.completion.sema.original_ast_node(receiver.clone()) { + builder.insert(receiver.syntax().text_range().start(), "(".to_string()); + builder.insert(ctx.source_range().end(), ")".to_string()); + } + } + + let is_parens_needed = + !matches!(dot_access.kind, DotAccessKind::Method { has_parens: true }); + + if is_parens_needed { + builder.insert(ctx.source_range().end(), "()".to_string()); + } + } + + item.text_edit(builder.finish()); + } else { + item.insert_text(field_with_receiver(db, receiver.as_ref(), &escaped_name)); + } if let Some(receiver) = &dot_access.receiver { if let Some(original) = ctx.completion.sema.original_ast_node(receiver.clone()) { if let Some(ref_match) = compute_ref_match(ctx.completion, ty) { @@ -1600,7 +1636,7 @@ fn main() { fn struct_field_method_ref() { check_kinds( r#" -struct Foo { bar: u32 } +struct Foo { bar: u32, qux: fn() } impl Foo { fn baz(&self) -> u32 { 0 } } fn foo(f: Foo) { let _: &u32 = f.b$0 } @@ -1610,30 +1646,92 @@ fn foo(f: Foo) { let _: &u32 = f.b$0 } [ CompletionItem { label: "baz()", - source_range: 98..99, - delete: 98..99, + source_range: 109..110, + delete: 109..110, insert: "baz()$0", kind: Method, lookup: "baz", detail: "fn(&self) -> u32", - ref_match: "&@96", + ref_match: "&@107", }, CompletionItem { label: "bar", - source_range: 98..99, - delete: 98..99, + source_range: 109..110, + delete: 109..110, insert: "bar", kind: SymbolKind( Field, ), detail: "u32", - ref_match: "&@96", + ref_match: "&@107", + }, + CompletionItem { + label: "qux", + source_range: 109..110, + text_edit: TextEdit { + indels: [ + Indel { + insert: "(", + delete: 107..107, + }, + Indel { + insert: "qux)()", + delete: 109..110, + }, + ], + }, + kind: SymbolKind( + Field, + ), + detail: "fn()", }, ] "#]], ); } + #[test] + fn expected_fn_type_ref() { + check_kinds( + r#" +struct S { field: fn() } + +fn foo() { + let foo: fn() = S { fields: || {}}.fi$0; +} +"#, + &[CompletionItemKind::SymbolKind(SymbolKind::Field)], + expect![[r#" + [ + CompletionItem { + label: "field", + source_range: 76..78, + delete: 76..78, + insert: "field", + kind: SymbolKind( + Field, + ), + detail: "fn()", + relevance: CompletionRelevance { + exact_name_match: false, + type_match: Some( + Exact, + ), + is_local: false, + is_item_from_trait: false, + is_name_already_imported: false, + requires_import: false, + is_op_method: false, + is_private_editable: false, + postfix_match: None, + is_definite: false, + }, + }, + ] + "#]], + ) + } + #[test] fn qualified_path_ref() { check_kinds(