From 41687437af09fcad7b0178462690d7e6104c02fd Mon Sep 17 00:00:00 2001 From: boattime Date: Sat, 11 Jan 2025 10:44:50 -0800 Subject: [PATCH] feat: Add dereferencing autocomplete --- crates/ide-completion/src/item.rs | 46 ++++++++---- crates/ide-completion/src/lib.rs | 5 +- crates/ide-completion/src/render.rs | 78 ++++++++++++++++---- crates/ide-completion/src/render/function.rs | 4 +- crates/ide/src/lib.rs | 2 +- crates/rust-analyzer/src/lib.rs | 12 ++- 6 files changed, 110 insertions(+), 37 deletions(-) diff --git a/crates/ide-completion/src/item.rs b/crates/ide-completion/src/item.rs index b91f915619..dc2f9a7680 100644 --- a/crates/ide-completion/src/item.rs +++ b/crates/ide-completion/src/item.rs @@ -79,7 +79,7 @@ pub struct CompletionItem { // FIXME: We shouldn't expose Mutability here (that is HIR types at all), its fine for now though // until we have more splitting completions in which case we should think about // generalizing this. See https://github.com/rust-lang/rust-analyzer/issues/12571 - pub ref_match: Option<(Mutability, TextSize)>, + pub ref_match: Option<(CompletionItemRefMode, TextSize)>, /// The import data to add to completion's edits. /// (ImportPath, LastSegment) @@ -128,8 +128,15 @@ impl fmt::Debug for CompletionItem { s.field("relevance", &self.relevance); } - if let Some((mutability, offset)) = &self.ref_match { - s.field("ref_match", &format!("&{}@{offset:?}", mutability.as_keyword_for_ref())); + if let Some((ref_mode, offset)) = self.ref_match { + let prefix = match ref_mode { + CompletionItemRefMode::Reference(mutability) => match mutability { + Mutability::Shared => "&", + Mutability::Mut => "&mut ", + }, + CompletionItemRefMode::Dereference => "*", + }; + s.field("ref_match", &format!("{}@{offset:?}", prefix)); } if self.trigger_call_info { s.field("trigger_call_info", &true); @@ -400,6 +407,12 @@ impl CompletionItemKind { } } +#[derive(Copy, Clone, Debug)] +pub enum CompletionItemRefMode { + Reference(Mutability), + Dereference, +} + impl CompletionItem { pub(crate) fn new( kind: impl Into, @@ -441,15 +454,14 @@ impl CompletionItem { let mut relevance = self.relevance; relevance.type_match = Some(CompletionRelevanceTypeMatch::Exact); - self.ref_match.map(|(mutability, offset)| { - ( - format!("&{}{}", mutability.as_keyword_for_ref(), self.label.primary), - ide_db::text_edit::Indel::insert( - offset, - format!("&{}", mutability.as_keyword_for_ref()), - ), - relevance, - ) + self.ref_match.map(|(mode, offset)| { + let prefix = match mode { + CompletionItemRefMode::Reference(Mutability::Shared) => "&", + CompletionItemRefMode::Reference(Mutability::Mut) => "&mut ", + CompletionItemRefMode::Dereference => "*", + }; + let label = format!("{prefix}{}", self.label.primary); + (label, ide_db::text_edit::Indel::insert(offset, String::from(prefix)), relevance) }) } } @@ -473,7 +485,7 @@ pub(crate) struct Builder { deprecated: bool, trigger_call_info: bool, relevance: CompletionRelevance, - ref_match: Option<(Mutability, TextSize)>, + ref_match: Option<(CompletionItemRefMode, TextSize)>, edition: Edition, } @@ -657,8 +669,12 @@ impl Builder { self.imports_to_add.push(import_to_add); self } - pub(crate) fn ref_match(&mut self, mutability: Mutability, offset: TextSize) -> &mut Builder { - self.ref_match = Some((mutability, offset)); + pub(crate) fn ref_match( + &mut self, + ref_mode: CompletionItemRefMode, + offset: TextSize, + ) -> &mut Builder { + self.ref_match = Some((ref_mode, offset)); self } } diff --git a/crates/ide-completion/src/lib.rs b/crates/ide-completion/src/lib.rs index ca6c9ad9f0..56d7eeaf8e 100644 --- a/crates/ide-completion/src/lib.rs +++ b/crates/ide-completion/src/lib.rs @@ -33,8 +33,9 @@ use crate::{ pub use crate::{ config::{AutoImportExclusionType, CallableSnippets, CompletionConfig}, item::{ - CompletionItem, CompletionItemKind, CompletionRelevance, CompletionRelevancePostfixMatch, - CompletionRelevanceReturnType, CompletionRelevanceTypeMatch, + CompletionItem, CompletionItemKind, CompletionItemRefMode, CompletionRelevance, + CompletionRelevancePostfixMatch, CompletionRelevanceReturnType, + CompletionRelevanceTypeMatch, }, snippet::{Snippet, SnippetScope}, }; diff --git a/crates/ide-completion/src/render.rs b/crates/ide-completion/src/render.rs index eb5d136b8b..0bfb890022 100644 --- a/crates/ide-completion/src/render.rs +++ b/crates/ide-completion/src/render.rs @@ -28,7 +28,8 @@ use crate::{ literal::render_variant_lit, macro_::{render_macro, render_macro_pat}, }, - CompletionContext, CompletionItem, CompletionItemKind, CompletionRelevance, + CompletionContext, CompletionItem, CompletionItemKind, CompletionItemRefMode, + CompletionRelevance, }; /// Interface for data and methods required for items rendering. #[derive(Debug, Clone)] @@ -192,8 +193,8 @@ pub(crate) fn render_field( } 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) { - item.ref_match(ref_match, original.syntax().text_range().start()); + if let Some(ref_mode) = compute_ref_match(ctx.completion, ty) { + item.ref_match(ref_mode, original.syntax().text_range().start()); } } } @@ -638,20 +639,34 @@ fn compute_exact_name_match(ctx: &CompletionContext<'_>, completion_name: &str) fn compute_ref_match( ctx: &CompletionContext<'_>, completion_ty: &hir::Type, -) -> Option { +) -> Option { let expected_type = ctx.expected_type.as_ref()?; - if completion_ty != expected_type { - let expected_type_without_ref = expected_type.remove_ref()?; - if completion_ty.autoderef(ctx.db).any(|deref_ty| deref_ty == expected_type_without_ref) { + let expected_without_ref = expected_type.remove_ref(); + let completion_without_ref = completion_ty.remove_ref(); + + if completion_ty == expected_type { + return None; + } + + if let Some(expected_without_ref) = &expected_without_ref { + if completion_ty.autoderef(ctx.db).any(|ty| ty == *expected_without_ref) { cov_mark::hit!(suggest_ref); let mutability = if expected_type.is_mutable_reference() { hir::Mutability::Mut } else { hir::Mutability::Shared }; - return Some(mutability); - }; + return Some(CompletionItemRefMode::Reference(mutability)); + } } + + if let Some(completion_without_ref) = completion_without_ref { + if completion_without_ref == *expected_type && completion_without_ref.is_copy(ctx.db) { + cov_mark::hit!(suggest_deref); + return Some(CompletionItemRefMode::Dereference); + } + } + None } @@ -664,16 +679,16 @@ fn path_ref_match( if let Some(original_path) = &path_ctx.original_path { // At least one char was typed by the user already, in that case look for the original path if let Some(original_path) = completion.sema.original_ast_node(original_path.clone()) { - if let Some(ref_match) = compute_ref_match(completion, ty) { - item.ref_match(ref_match, original_path.syntax().text_range().start()); + if let Some(ref_mode) = compute_ref_match(completion, ty) { + item.ref_match(ref_mode, original_path.syntax().text_range().start()); } } } else { // completion requested on an empty identifier, there is no path here yet. // FIXME: This might create inconsistent completions where we show a ref match in macro inputs // as long as nothing was typed yet - if let Some(ref_match) = compute_ref_match(completion, ty) { - item.ref_match(ref_match, completion.position.offset); + if let Some(ref_mode) = compute_ref_match(completion, ty) { + item.ref_match(ref_mode, completion.position.offset); } } } @@ -2065,7 +2080,42 @@ fn main() { } #[test] - fn suggest_deref() { + fn suggest_deref_copy() { + cov_mark::check!(suggest_deref); + check_relevance( + r#" +//- minicore: copy +struct Foo; + +impl Copy for Foo {} +impl Clone for Foo { + fn clone(&self) -> Self { *self } +} + +fn bar(x: Foo) {} + +fn main() { + let foo = &Foo; + bar($0); +} +"#, + expect![[r#" + st Foo Foo [type] + st Foo Foo [type] + ex Foo [type] + lc foo &Foo [local] + lc *foo [type+local] + fn bar(…) fn(Foo) [] + fn main() fn() [] + md core [] + tt Clone [] + tt Copy [] + "#]], + ); + } + + #[test] + fn suggest_deref_trait() { check_relevance( r#" //- minicore: deref diff --git a/crates/ide-completion/src/render/function.rs b/crates/ide-completion/src/render/function.rs index a859d79e24..96a58aaa81 100644 --- a/crates/ide-completion/src/render/function.rs +++ b/crates/ide-completion/src/render/function.rs @@ -143,8 +143,8 @@ fn render( } FuncKind::Method(DotAccess { receiver: Some(receiver), .. }, _) => { if let Some(original_expr) = completion.sema.original_ast_node(receiver.clone()) { - if let Some(ref_match) = compute_ref_match(completion, &ret_type) { - item.ref_match(ref_match, original_expr.syntax().text_range().start()); + if let Some(ref_mode) = compute_ref_match(completion, &ret_type) { + item.ref_match(ref_mode, original_expr.syntax().text_range().start()); } } } diff --git a/crates/ide/src/lib.rs b/crates/ide/src/lib.rs index 183962be28..043e854215 100644 --- a/crates/ide/src/lib.rs +++ b/crates/ide/src/lib.rs @@ -120,7 +120,7 @@ pub use ide_assists::{ }; pub use ide_completion::{ CallableSnippets, CompletionConfig, CompletionFieldsToResolve, CompletionItem, - CompletionItemKind, CompletionRelevance, Snippet, SnippetScope, + CompletionItemKind, CompletionItemRefMode, CompletionRelevance, Snippet, SnippetScope, }; pub use ide_db::text_edit::{Indel, TextEdit}; pub use ide_db::{ diff --git a/crates/rust-analyzer/src/lib.rs b/crates/rust-analyzer/src/lib.rs index e7f5a7f5e7..61ec576dd4 100644 --- a/crates/rust-analyzer/src/lib.rs +++ b/crates/rust-analyzer/src/lib.rs @@ -47,7 +47,8 @@ use self::lsp::ext as lsp_ext; #[cfg(test)] mod integrated_benchmarks; -use ide::{CompletionItem, CompletionRelevance}; +use hir::Mutability; +use ide::{CompletionItem, CompletionItemRefMode, CompletionRelevance}; use serde::de::DeserializeOwned; use tenthash::TentHasher; @@ -132,8 +133,13 @@ fn completion_item_hash(item: &CompletionItem, is_ref_completion: bool) -> [u8; hasher.update(detail); } hash_completion_relevance(&mut hasher, &item.relevance); - if let Some((mutability, text_size)) = &item.ref_match { - hasher.update(mutability.as_keyword_for_ref()); + if let Some((ref_mode, text_size)) = &item.ref_match { + let prefix = match ref_mode { + CompletionItemRefMode::Reference(Mutability::Shared) => "&", + CompletionItemRefMode::Reference(Mutability::Mut) => "&mut ", + CompletionItemRefMode::Dereference => "*", + }; + hasher.update(prefix); hasher.update(u32::from(*text_size).to_le_bytes()); } for (import_path, import_name) in &item.import_to_add {