From c00dfa3a11409a7bc117f08cc39f6a6400067d47 Mon Sep 17 00:00:00 2001 From: A4-Tacks Date: Thu, 18 Sep 2025 13:26:16 +0800 Subject: [PATCH] Heuristic sensing parenthesis completion of fields We have conducted heuristic sensing on method parentheses, but it cannot complete fields Example --- ```rust struct Foo { far: i32 } impl Foo { fn foo(&self) {} } fn foo() -> (i32, i32) { let foo = Foo { far: 4 }; foo.f$0 (2, 3) } ``` **Before this PR**: ```text me foo() fn(&self) ... ``` **After this PR**: ```text fd far i32 me foo() fn(&self) ... ``` --- .../ide-completion/src/completions/dot.rs | 85 +++++++--- .../ide-completion/src/completions/postfix.rs | 2 +- .../crates/ide-completion/src/context.rs | 4 +- .../ide-completion/src/context/analysis.rs | 82 +++++---- .../crates/ide-completion/src/render.rs | 3 +- .../ide-completion/src/render/function.rs | 4 +- .../ide-completion/src/tests/expression.rs | 157 ++++++++++++++++++ 7 files changed, 274 insertions(+), 63 deletions(-) diff --git a/src/tools/rust-analyzer/crates/ide-completion/src/completions/dot.rs b/src/tools/rust-analyzer/crates/ide-completion/src/completions/dot.rs index 72b245ccafd9..511b59385702 100644 --- a/src/tools/rust-analyzer/crates/ide-completion/src/completions/dot.rs +++ b/src/tools/rust-analyzer/crates/ide-completion/src/completions/dot.rs @@ -25,9 +25,7 @@ pub(crate) fn complete_dot( _ => return, }; - let is_field_access = matches!(dot_access.kind, DotAccessKind::Field { .. }); - let is_method_access_with_parens = - matches!(dot_access.kind, DotAccessKind::Method { has_parens: true }); + let has_parens = matches!(dot_access.kind, DotAccessKind::Method); let traits_in_scope = ctx.traits_in_scope(); // Suggest .await syntax for types that implement Future trait @@ -48,7 +46,7 @@ pub(crate) fn complete_dot( DotAccessKind::Field { receiver_is_ambiguous_float_literal: _ } => { DotAccessKind::Field { receiver_is_ambiguous_float_literal: false } } - it @ DotAccessKind::Method { .. } => *it, + it @ DotAccessKind::Method => *it, }; let dot_access = DotAccess { receiver: dot_access.receiver.clone(), @@ -67,8 +65,7 @@ pub(crate) fn complete_dot( acc.add_field(ctx, &dot_access, Some(await_str.clone()), field, &ty) }, |acc, field, ty| acc.add_tuple_field(ctx, Some(await_str.clone()), field, &ty), - is_field_access, - is_method_access_with_parens, + has_parens, ); complete_methods(ctx, &future_output, &traits_in_scope, |func| { acc.add_method(ctx, &dot_access, func, Some(await_str.clone()), None) @@ -82,8 +79,7 @@ pub(crate) fn complete_dot( 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, - is_method_access_with_parens, + has_parens, ); complete_methods(ctx, receiver_ty, &traits_in_scope, |func| { acc.add_method(ctx, dot_access, func, None, None) @@ -112,7 +108,7 @@ pub(crate) fn complete_dot( DotAccessKind::Field { receiver_is_ambiguous_float_literal: _ } => { DotAccessKind::Field { receiver_is_ambiguous_float_literal: false } } - it @ DotAccessKind::Method { .. } => *it, + it @ DotAccessKind::Method => *it, }; let dot_access = DotAccess { receiver: dot_access.receiver.clone(), @@ -173,7 +169,6 @@ pub(crate) fn complete_undotted_self( ) }, |acc, field, ty| acc.add_tuple_field(ctx, Some(SmolStr::new_static("self")), field, &ty), - true, false, ); complete_methods(ctx, &ty, &ctx.traits_in_scope(), |func| { @@ -182,7 +177,7 @@ pub(crate) fn complete_undotted_self( &DotAccess { receiver: None, receiver_ty: None, - kind: DotAccessKind::Method { has_parens: false }, + kind: DotAccessKind::Field { receiver_is_ambiguous_float_literal: false }, ctx: DotAccessExprCtx { in_block_expr: expr_ctx.in_block_expr, in_breakable: expr_ctx.in_breakable, @@ -201,15 +196,13 @@ 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, - is_method_access_with_parens: bool, + has_parens: 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)) - && (is_field_access - || (is_method_access_with_parens && (ty.is_fn() || ty.is_closure()))) + && (!has_parens || ty.is_fn() || ty.is_closure()) { named_field(acc, field, ty); } @@ -218,8 +211,7 @@ fn complete_fields( // 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)) - && (is_field_access - || (is_method_access_with_parens && (ty.is_fn() || ty.is_closure()))) + && (!has_parens || ty.is_fn() || ty.is_closure()) { // Tuple fields are always public (tuple struct fields are handled above). tuple_index(acc, i, ty); @@ -1364,18 +1356,71 @@ fn foo() { r#" struct Foo { baz: fn() } impl Foo { - fn bar(self, t: T): T { t } + fn bar(self, t: T) -> T { t } } fn baz() { let foo = Foo{ baz: || {} }; - foo.ba$0::<>; + foo.ba$0; } "#, expect![[r#" - me bar(…) fn(self, T) + fd baz fn() + me bar(…) fn(self, T) -> T "#]], ); + + check_edit( + "baz", + r#" +struct Foo { baz: fn() } +impl Foo { + fn bar(self, t: T) -> T { t } +} + +fn baz() { + let foo = Foo{ baz: || {} }; + foo.ba$0; +} +"#, + r#" +struct Foo { baz: fn() } +impl Foo { + fn bar(self, t: T) -> T { t } +} + +fn baz() { + let foo = Foo{ baz: || {} }; + (foo.baz)(); +} +"#, + ); + + check_edit( + "bar", + r#" +struct Foo { baz: fn() } +impl Foo { + fn bar(self, t: T) -> T { t } +} + +fn baz() { + let foo = Foo{ baz: || {} }; + foo.ba$0; +} +"#, + r#" +struct Foo { baz: fn() } +impl Foo { + fn bar(self, t: T) -> T { t } +} + +fn baz() { + let foo = Foo{ baz: || {} }; + foo.bar(${1:t})$0; +} +"#, + ); } #[test] diff --git a/src/tools/rust-analyzer/crates/ide-completion/src/completions/postfix.rs b/src/tools/rust-analyzer/crates/ide-completion/src/completions/postfix.rs index 4474d6181c20..70761534cd2c 100644 --- a/src/tools/rust-analyzer/crates/ide-completion/src/completions/postfix.rs +++ b/src/tools/rust-analyzer/crates/ide-completion/src/completions/postfix.rs @@ -43,7 +43,7 @@ pub(crate) fn complete_postfix( DotAccessKind::Field { receiver_is_ambiguous_float_literal } => { receiver_is_ambiguous_float_literal } - DotAccessKind::Method { .. } => false, + DotAccessKind::Method => false, }, ), _ => return, diff --git a/src/tools/rust-analyzer/crates/ide-completion/src/context.rs b/src/tools/rust-analyzer/crates/ide-completion/src/context.rs index c95b83ef8a02..b245c0d9831c 100644 --- a/src/tools/rust-analyzer/crates/ide-completion/src/context.rs +++ b/src/tools/rust-analyzer/crates/ide-completion/src/context.rs @@ -405,9 +405,7 @@ pub(crate) enum DotAccessKind { /// like `0.$0` receiver_is_ambiguous_float_literal: bool, }, - Method { - has_parens: bool, - }, + Method, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] diff --git a/src/tools/rust-analyzer/crates/ide-completion/src/context/analysis.rs b/src/tools/rust-analyzer/crates/ide-completion/src/context/analysis.rs index f0a03dedfe88..c01b544ff6ef 100644 --- a/src/tools/rust-analyzer/crates/ide-completion/src/context/analysis.rs +++ b/src/tools/rust-analyzer/crates/ide-completion/src/context/analysis.rs @@ -891,44 +891,53 @@ fn classify_name_ref<'db>( return Some(make_res(kind)); } + let field_expr_handle = |recviver, node| { + let receiver = find_opt_node_in_file(original_file, recviver); + let receiver_is_ambiguous_float_literal = match &receiver { + Some(ast::Expr::Literal(l)) => matches! { + l.kind(), + ast::LiteralKind::FloatNumber { .. } if l.syntax().last_token().is_some_and(|it| it.text().ends_with('.')) + }, + _ => false, + }; + + let receiver_is_part_of_indivisible_expression = match &receiver { + Some(ast::Expr::IfExpr(_)) => { + let next_token_kind = + next_non_trivia_token(name_ref.syntax().clone()).map(|t| t.kind()); + next_token_kind == Some(SyntaxKind::ELSE_KW) + } + _ => false, + }; + if receiver_is_part_of_indivisible_expression { + return None; + } + + let mut receiver_ty = receiver.as_ref().and_then(|it| sema.type_of_expr(it)); + if receiver_is_ambiguous_float_literal { + // `123.|` is parsed as a float but should actually be an integer. + always!(receiver_ty.as_ref().is_none_or(|receiver_ty| receiver_ty.original.is_float())); + receiver_ty = + Some(TypeInfo { original: hir::BuiltinType::i32().ty(sema.db), adjusted: None }); + } + + let kind = NameRefKind::DotAccess(DotAccess { + receiver_ty, + kind: DotAccessKind::Field { receiver_is_ambiguous_float_literal }, + receiver, + ctx: DotAccessExprCtx { + in_block_expr: is_in_block(node), + in_breakable: is_in_breakable(node).unzip().0, + }, + }); + Some(make_res(kind)) + }; + let segment = match_ast! { match parent { ast::PathSegment(segment) => segment, ast::FieldExpr(field) => { - let receiver = find_opt_node_in_file(original_file, field.expr()); - let receiver_is_ambiguous_float_literal = match &receiver { - Some(ast::Expr::Literal(l)) => matches! { - l.kind(), - ast::LiteralKind::FloatNumber { .. } if l.syntax().last_token().is_some_and(|it| it.text().ends_with('.')) - }, - _ => false, - }; - - let receiver_is_part_of_indivisible_expression = match &receiver { - Some(ast::Expr::IfExpr(_)) => { - let next_token_kind = next_non_trivia_token(name_ref.syntax().clone()).map(|t| t.kind()); - next_token_kind == Some(SyntaxKind::ELSE_KW) - }, - _ => false - }; - if receiver_is_part_of_indivisible_expression { - return None; - } - - let mut receiver_ty = receiver.as_ref().and_then(|it| sema.type_of_expr(it)); - if receiver_is_ambiguous_float_literal { - // `123.|` is parsed as a float but should actually be an integer. - always!(receiver_ty.as_ref().is_none_or(|receiver_ty| receiver_ty.original.is_float())); - receiver_ty = Some(TypeInfo { original: hir::BuiltinType::i32().ty(sema.db), adjusted: None }); - } - - let kind = NameRefKind::DotAccess(DotAccess { - receiver_ty, - kind: DotAccessKind::Field { receiver_is_ambiguous_float_literal }, - receiver, - ctx: DotAccessExprCtx { in_block_expr: is_in_block(field.syntax()), in_breakable: is_in_breakable(field.syntax()).unzip().0 } - }); - return Some(make_res(kind)); + return field_expr_handle(field.expr(), field.syntax()); }, ast::ExternCrate(_) => { let kind = NameRefKind::ExternCrate; @@ -937,9 +946,12 @@ fn classify_name_ref<'db>( ast::MethodCallExpr(method) => { let receiver = find_opt_node_in_file(original_file, method.receiver()); let has_parens = has_parens(&method); + if !has_parens && let Some(res) = field_expr_handle(method.receiver(), method.syntax()) { + return Some(res) + } let kind = NameRefKind::DotAccess(DotAccess { receiver_ty: receiver.as_ref().and_then(|it| sema.type_of_expr(it)), - kind: DotAccessKind::Method { has_parens }, + kind: DotAccessKind::Method, receiver, ctx: DotAccessExprCtx { in_block_expr: is_in_block(method.syntax()), in_breakable: is_in_breakable(method.syntax()).unzip().0 } }); diff --git a/src/tools/rust-analyzer/crates/ide-completion/src/render.rs b/src/tools/rust-analyzer/crates/ide-completion/src/render.rs index 77a2a3a3a9a0..bc5589a64550 100644 --- a/src/tools/rust-analyzer/crates/ide-completion/src/render.rs +++ b/src/tools/rust-analyzer/crates/ide-completion/src/render.rs @@ -170,8 +170,7 @@ pub(crate) fn render_field( builder.insert(receiver.syntax().text_range().start(), "(".to_owned()); builder.insert(ctx.source_range().end(), ")".to_owned()); - let is_parens_needed = - !matches!(dot_access.kind, DotAccessKind::Method { has_parens: true }); + let is_parens_needed = !matches!(dot_access.kind, DotAccessKind::Method); if is_parens_needed { builder.insert(ctx.source_range().end(), "()".to_owned()); diff --git a/src/tools/rust-analyzer/crates/ide-completion/src/render/function.rs b/src/tools/rust-analyzer/crates/ide-completion/src/render/function.rs index c466019f991f..3235323b3a59 100644 --- a/src/tools/rust-analyzer/crates/ide-completion/src/render/function.rs +++ b/src/tools/rust-analyzer/crates/ide-completion/src/render/function.rs @@ -93,8 +93,8 @@ fn render( has_call_parens, .. }) => (false, has_call_parens, ctx.completion.config.snippet_cap), - FuncKind::Method(&DotAccess { kind: DotAccessKind::Method { has_parens }, .. }, _) => { - (true, has_parens, ctx.completion.config.snippet_cap) + FuncKind::Method(&DotAccess { kind: DotAccessKind::Method, .. }, _) => { + (true, true, ctx.completion.config.snippet_cap) } FuncKind::Method(DotAccess { kind: DotAccessKind::Field { .. }, .. }, _) => { (true, false, ctx.completion.config.snippet_cap) diff --git a/src/tools/rust-analyzer/crates/ide-completion/src/tests/expression.rs b/src/tools/rust-analyzer/crates/ide-completion/src/tests/expression.rs index f75fa7943ba6..09af635f01ca 100644 --- a/src/tools/rust-analyzer/crates/ide-completion/src/tests/expression.rs +++ b/src/tools/rust-analyzer/crates/ide-completion/src/tests/expression.rs @@ -2936,6 +2936,43 @@ fn foo() { ); } +#[test] +fn ambiguous_float_literal_in_ambiguous_method_call() { + check( + r#" +#![rustc_coherence_is_core] + +impl i32 { + pub fn int_method(self) {} +} +impl f64 { + pub fn float_method(self) {} +} + +fn foo() -> (i32, i32) { + 1.$0 + (2, 3) +} + "#, + expect![[r#" + me int_method() fn(self) + sn box Box::new(expr) + sn call function(expr) + sn const const {} + sn dbg dbg!(expr) + sn dbgr dbg!(&expr) + sn deref *expr + sn let let + sn letm let mut + sn match match expr {} + sn ref &expr + sn refm &mut expr + sn return return expr + sn unsafe unsafe {} + "#]], + ); +} + #[test] fn let_in_condition() { check_edit("let", r#"fn f() { if $0 {} }"#, r#"fn f() { if let $1 = $0 {} }"#); @@ -3113,6 +3150,126 @@ fn let_in_previous_line_of_ambiguous_expr() { ); } +#[test] +fn field_in_previous_line_of_ambiguous_expr() { + check( + r#" + struct Foo { field: i32 } + impl Foo { + fn method(&self) {} + } + fn foo() -> (i32, i32) { + let foo = Foo { field: 4 }; + foo.$0 + (2, 3) + }"#, + expect![[r#" + fd field i32 + me method() fn(&self) + sn box Box::new(expr) + sn call function(expr) + sn const const {} + sn dbg dbg!(expr) + sn dbgr dbg!(&expr) + sn deref *expr + sn let let + sn letm let mut + sn match match expr {} + sn ref &expr + sn refm &mut expr + sn return return expr + sn unsafe unsafe {} + "#]], + ); + + check( + r#" + struct Foo { field: i32 } + impl Foo { + fn method(&self) {} + } + fn foo() -> (i32, i32) { + let foo = Foo { field: 4 }; + foo.a$0 + (2, 3) + }"#, + expect![[r#" + fd field i32 + me method() fn(&self) + sn box Box::new(expr) + sn call function(expr) + sn const const {} + sn dbg dbg!(expr) + sn dbgr dbg!(&expr) + sn deref *expr + sn let let + sn letm let mut + sn match match expr {} + sn ref &expr + sn refm &mut expr + sn return return expr + sn unsafe unsafe {} + "#]], + ); +} + +#[test] +fn fn_field_in_previous_line_of_ambiguous_expr() { + check( + r#" + struct Foo { field: fn() } + impl Foo { + fn method(&self) {} + } + fn foo() -> (i32, i32) { + let foo = Foo { field: || () }; + foo.$0 + (2, 3) + }"#, + expect![[r#" + fd field fn() + me method() fn(&self) + sn box Box::new(expr) + sn call function(expr) + sn const const {} + sn dbg dbg!(expr) + sn dbgr dbg!(&expr) + sn deref *expr + sn let let + sn letm let mut + sn match match expr {} + sn ref &expr + sn refm &mut expr + sn return return expr + sn unsafe unsafe {} + "#]], + ); + + check_edit( + "field", + r#" + struct Foo { field: fn() } + impl Foo { + fn method(&self) {} + } + fn foo() -> (i32, i32) { + let foo = Foo { field: || () }; + foo.a$0 + (2, 3) + }"#, + r#" + struct Foo { field: fn() } + impl Foo { + fn method(&self) {} + } + fn foo() -> (i32, i32) { + let foo = Foo { field: || () }; + (foo.field)() + (2, 3) + }"#, + ); +} + #[test] fn private_inherent_and_public_trait() { check(