From 0456716ece0b2c57b3fa68f3eb1ac59319155450 Mon Sep 17 00:00:00 2001 From: Asuka Minato Date: Sat, 20 Dec 2025 14:04:37 +0900 Subject: [PATCH 1/2] fix fix name --- crates/ide-completion/src/render/function.rs | 133 ++++++++++++++++-- crates/ide-completion/src/tests/expression.rs | 35 +++++ 2 files changed, 158 insertions(+), 10 deletions(-) diff --git a/crates/ide-completion/src/render/function.rs b/crates/ide-completion/src/render/function.rs index 4713b1f1afa7..70e22a246d31 100644 --- a/crates/ide-completion/src/render/function.rs +++ b/crates/ide-completion/src/render/function.rs @@ -1,15 +1,17 @@ //! Renderer for function calls. -use hir::{AsAssocItem, HirDisplay, db::HirDatabase}; -use ide_db::{SnippetCap, SymbolKind}; +use hir::{AsAssocItem, AssocItemContainer, HirDisplay, PathResolution, db::HirDatabase}; +use ide_db::{SnippetCap, SymbolKind, text_edit::TextEdit}; use itertools::Itertools; +use std::borrow::Cow; use stdx::{format_to, to_lower_snake_case}; -use syntax::{AstNode, SmolStr, ToSmolStr, format_smolstr}; +use syntax::{AstNode, SmolStr, TextRange, ToSmolStr, format_smolstr}; use crate::{ CallableSnippets, context::{ - CompleteSemicolon, CompletionContext, DotAccess, DotAccessKind, PathCompletionCtx, PathKind, + CompleteSemicolon, CompletionContext, DotAccess, DotAccessKind, PathCompletionCtx, + PathKind, Qualified, }, item::{ Builder, CompletionItem, CompletionItemKind, CompletionRelevance, CompletionRelevanceFn, @@ -26,6 +28,17 @@ enum FuncKind<'ctx> { Method(&'ctx DotAccess<'ctx>, Option), } +struct UfcsData { + prefix: String, + replacement_range: TextRange, +} + +#[derive(Clone, Copy)] +pub(super) struct CallSnippetRewrite<'a> { + prefix: &'a str, + replace_range: TextRange, +} + pub(crate) fn render_fn( ctx: RenderContext<'_>, path_ctx: &PathCompletionCtx<'_>, @@ -87,6 +100,22 @@ fn render( } }); + let trait_container = assoc_item.and_then(|assoc_item| match assoc_item.container(db) { + AssocItemContainer::Trait(trait_) => Some(trait_), + _ => None, + }); + + let ufcs_data = if ctx.import_to_add.is_none() { + match (&func_kind, trait_container) { + (FuncKind::Function(path_ctx), Some(trait_)) => { + trait_method_ufcs_data(ctx.completion, path_ctx, trait_) + } + _ => None, + } + } else { + None + }; + let (has_dot_receiver, has_call_parens, cap) = match func_kind { FuncKind::Function(&PathCompletionCtx { kind: PathKind::Expr { .. }, @@ -151,7 +180,16 @@ fn render( .detail(detail) .lookup_by(name.as_str().to_smolstr()); + if let Some(data) = &ufcs_data { + let insert_text = format!("{}{}", data.prefix, escaped_call); + item.text_edit(TextEdit::replace(data.replacement_range, insert_text)); + } + if let Some((cap, (self_param, params))) = complete_call_parens { + let rewrite = ufcs_data.as_ref().map(|data| CallSnippetRewrite { + replace_range: data.replacement_range, + prefix: &data.prefix, + }); add_call_parens( &mut item, completion, @@ -161,6 +199,7 @@ fn render( self_param, params, &ret_type, + rewrite, ); } @@ -217,11 +256,19 @@ pub(super) fn add_call_parens<'b>( self_param: Option, params: Vec>, ret_type: &hir::Type<'_>, + mut rewrite: Option>, ) -> &'b mut Builder { cov_mark::hit!(inserts_parens_for_function_calls); + let call_head = if let Some(rewrite_data) = rewrite.as_ref() { + Cow::Owned(format!("{}{}", rewrite_data.prefix, escaped_name)) + } else { + Cow::Borrowed(escaped_name.as_str()) + }; + let call_head = call_head.as_ref(); + let (mut snippet, label_suffix) = if self_param.is_none() && params.is_empty() { - (format!("{escaped_name}()$0"), "()") + (format!("{call_head}()$0"), "()") } else { builder.trigger_call_info(); let snippet = if let Some(CallableSnippets::FillArguments) = ctx.config.callable { @@ -247,20 +294,19 @@ pub(super) fn add_call_parens<'b>( match self_param { Some(self_param) => { format!( - "{}(${{1:{}}}{}{})$0", - escaped_name, + "{call_head}(${{1:{}}}{}{})$0", self_param.display(ctx.db, ctx.display_target), if params.is_empty() { "" } else { ", " }, function_params_snippet ) } None => { - format!("{escaped_name}({function_params_snippet})$0") + format!("{call_head}({function_params_snippet})$0") } } } else { cov_mark::hit!(suppress_arg_snippets); - format!("{escaped_name}($0)") + format!("{call_head}($0)") }; (snippet, "(…)") @@ -283,7 +329,12 @@ pub(super) fn add_call_parens<'b>( } } } - builder.label(SmolStr::from_iter([&name, label_suffix])).insert_snippet(cap, snippet) + let builder = builder.label(SmolStr::from_iter([&name, label_suffix])); + if let Some(rewrite_data) = rewrite.take() { + builder.snippet_edit(cap, TextEdit::replace(rewrite_data.replace_range, snippet)) + } else { + builder.insert_snippet(cap, snippet) + } } fn ref_of_param(ctx: &CompletionContext<'_>, arg: &str, ty: &hir::Type<'_>) -> &'static str { @@ -393,6 +444,68 @@ fn params<'db>( Some((self_param, func.params_without_self(ctx.db))) } +fn trait_method_ufcs_data( + completion: &CompletionContext<'_>, + path_ctx: &PathCompletionCtx<'_>, + trait_: hir::Trait, +) -> Option { + let needs_ufcs = match &path_ctx.qualified { + Qualified::With { resolution: Some(resolution), .. } => resolution_targets_type(resolution), + Qualified::TypeAnchor { ty, trait_: None } => ty.is_some(), + _ => false, + }; + + if !needs_ufcs { + return None; + } + + let (qualifier_text, replacement_range) = qualifier_text_and_range(completion, path_ctx)?; + let trait_path = trait_path_string(completion, trait_); + let prefix = format!("<{qualifier_text} as {trait_path}>::"); + Some(UfcsData { prefix, replacement_range }) +} + +fn qualifier_text_and_range( + completion: &CompletionContext<'_>, + path_ctx: &PathCompletionCtx<'_>, +) -> Option<(String, TextRange)> { + let qualifier = path_ctx + .original_path + .as_ref() + .and_then(|path| path.qualifier()) + .or_else(|| path_ctx.path.qualifier())?; + let qualifier_syntax = qualifier.syntax().clone(); + let text = qualifier_syntax.text().to_string(); + if text.is_empty() { + return None; + } + let start = qualifier_syntax.text_range().start(); + let end = completion.position.offset; + if start >= end { + return None; + } + Some((text, TextRange::new(start, end))) +} + +fn trait_path_string(ctx: &CompletionContext<'_>, trait_: hir::Trait) -> String { + let cfg = ctx.config.find_path_config(ctx.is_nightly); + ctx.module + .find_path(ctx.db, hir::ModuleDef::Trait(trait_), cfg) + .map(|path| path.display(ctx.db, ctx.edition).to_string()) + .unwrap_or_else(|| trait_.name(ctx.db).display_no_db(ctx.edition).to_string()) +} + +fn resolution_targets_type(resolution: &PathResolution) -> bool { + match resolution { + PathResolution::Def(def) => matches!( + def, + hir::ModuleDef::Adt(_) | hir::ModuleDef::TypeAlias(_) | hir::ModuleDef::BuiltinType(_) + ), + PathResolution::TypeParam(_) | PathResolution::SelfType(_) => true, + _ => false, + } +} + #[cfg(test)] mod tests { use crate::{ diff --git a/crates/ide-completion/src/tests/expression.rs b/crates/ide-completion/src/tests/expression.rs index 78f003dd210b..be36ba4ea857 100644 --- a/crates/ide-completion/src/tests/expression.rs +++ b/crates/ide-completion/src/tests/expression.rs @@ -2911,6 +2911,41 @@ fn foo() { ); } +#[test] +fn trait_method_completion_uses_ufcs_syntax() { + check_edit( + "baby_name", + r#" +trait Animal { + fn baby_name() -> String; +} + +struct Dog; +impl Animal for Dog { + fn baby_name() -> String { String::from("Puppy") } +} + +fn make() { + Dog::$0 +} +"#, + r#" +trait Animal { + fn baby_name() -> String; +} + +struct Dog; +impl Animal for Dog { + fn baby_name() -> String { String::from("Puppy") } +} + +fn make() { + ::baby_name()$0 +} +"#, + ); +} + #[test] fn hide_ragennew_synthetic_identifiers() { check( From a4d805a5fdebc236fd8b60338ad7d78b59d68cca Mon Sep 17 00:00:00 2001 From: Asuka Minato Date: Sat, 20 Dec 2025 19:56:23 +0900 Subject: [PATCH 2/2] Update crates/ide-completion/src/render/function.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Update crates/ide-completion/src/render/function.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> clippy --- crates/ide-completion/src/render/function.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/ide-completion/src/render/function.rs b/crates/ide-completion/src/render/function.rs index 70e22a246d31..f72a085aad9d 100644 --- a/crates/ide-completion/src/render/function.rs +++ b/crates/ide-completion/src/render/function.rs @@ -180,7 +180,9 @@ fn render( .detail(detail) .lookup_by(name.as_str().to_smolstr()); - if let Some(data) = &ufcs_data { + if complete_call_parens.is_none() + && let Some(data) = &ufcs_data + { let insert_text = format!("{}{}", data.prefix, escaped_call); item.text_edit(TextEdit::replace(data.replacement_range, insert_text)); } @@ -474,7 +476,7 @@ fn qualifier_text_and_range( .as_ref() .and_then(|path| path.qualifier()) .or_else(|| path_ctx.path.qualifier())?; - let qualifier_syntax = qualifier.syntax().clone(); + let qualifier_syntax = qualifier.syntax(); let text = qualifier_syntax.text().to_string(); if text.is_empty() { return None;