Allow crate authors to control completion of their things

Via the new `#[rust_analyzer::completions(...)]` attribute.

Also fix a bug with existing settings for that where the paths wouldn't resolve correctly.
This commit is contained in:
Chayim Refael Friedman 2025-03-16 13:24:11 +02:00
parent 62e7d9f0fc
commit 7b584ef9bf
30 changed files with 770 additions and 293 deletions

View file

@ -2,7 +2,7 @@
use std::ops::ControlFlow;
use hir::{HasContainer, ItemContainer, MethodCandidateCallback, Name};
use hir::{Complete, HasContainer, ItemContainer, MethodCandidateCallback, Name};
use ide_db::FxHashSet;
use syntax::SmolStr;
@ -259,7 +259,9 @@ fn complete_methods(
// This needs to come before the `seen_methods` test, so that if we see the same method twice,
// once as inherent and once not, we will include it.
if let ItemContainer::Trait(trait_) = func.container(self.ctx.db) {
if self.ctx.exclude_traits.contains(&trait_) {
if self.ctx.exclude_traits.contains(&trait_)
|| trait_.complete(self.ctx.db) == Complete::IgnoreMethods
{
return ControlFlow::Continue(());
}
}

View file

@ -2,7 +2,7 @@
use std::ops::ControlFlow;
use hir::{Name, PathCandidateCallback, ScopeDef, sym};
use hir::{Complete, Name, PathCandidateCallback, ScopeDef, sym};
use ide_db::FxHashSet;
use syntax::ast;
@ -33,10 +33,10 @@ where
fn on_trait_item(&mut self, item: hir::AssocItem) -> ControlFlow<()> {
// The excluded check needs to come before the `seen` test, so that if we see the same method twice,
// once as inherent and once not, we will include it.
if item
.container_trait(self.ctx.db)
.is_none_or(|trait_| !self.ctx.exclude_traits.contains(&trait_))
&& self.seen.insert(item)
if item.container_trait(self.ctx.db).is_none_or(|trait_| {
!self.ctx.exclude_traits.contains(&trait_)
&& trait_.complete(self.ctx.db) != Complete::IgnoreMethods
}) && self.seen.insert(item)
{
(self.add_assoc_item)(self.acc, item);
}
@ -104,7 +104,9 @@ pub(crate) fn complete_expr_path(
.iter()
.copied()
.map(hir::Trait::from)
.filter(|it| !ctx.exclude_traits.contains(it))
.filter(|it| {
!ctx.exclude_traits.contains(it) && it.complete(ctx.db) != Complete::IgnoreMethods
})
.flat_map(|it| it.items(ctx.sema.db))
.for_each(|item| add_assoc_item(acc, item)),
Qualified::TypeAnchor { trait_: Some(trait_), .. } => {

View file

@ -268,19 +268,7 @@ fn import_on_the_fly(
&& !ctx.is_item_hidden(original_item)
&& ctx.check_stability(original_item.attrs(ctx.db).as_deref())
})
.filter(|import| {
let def = import.item_to_import.into_module_def();
if let Some(&kind) = ctx.exclude_flyimport.get(&def) {
if kind == AutoImportExclusionType::Always {
return false;
}
let method_imported = import.item_to_import != import.original_item;
if method_imported {
return false;
}
}
true
})
.filter(|import| filter_excluded_flyimport(ctx, import))
.sorted_by(|a, b| {
let key = |import_path| {
(
@ -366,24 +354,7 @@ fn import_on_the_fly_method(
!ctx.is_item_hidden(&import.item_to_import)
&& !ctx.is_item_hidden(&import.original_item)
})
.filter(|import| {
let def = import.item_to_import.into_module_def();
if let Some(&kind) = ctx.exclude_flyimport.get(&def) {
if kind == AutoImportExclusionType::Always {
return false;
}
let method_imported = import.item_to_import != import.original_item;
if method_imported {
return false;
}
}
if let ModuleDef::Trait(_) = import.item_to_import.into_module_def() {
!ctx.exclude_flyimport.contains_key(&def)
} else {
true
}
})
.filter(|import| filter_excluded_flyimport(ctx, import))
.sorted_by(|a, b| {
let key = |import_path| {
(
@ -401,6 +372,28 @@ fn import_on_the_fly_method(
Some(())
}
fn filter_excluded_flyimport(ctx: &CompletionContext<'_>, import: &LocatedImport) -> bool {
let def = import.item_to_import.into_module_def();
let is_exclude_flyimport = ctx.exclude_flyimport.get(&def).copied();
if matches!(is_exclude_flyimport, Some(AutoImportExclusionType::Always))
|| !import.complete_in_flyimport.0
{
return false;
}
let method_imported = import.item_to_import != import.original_item;
if method_imported
&& (is_exclude_flyimport.is_some()
|| ctx.exclude_flyimport.contains_key(&import.original_item.into_module_def()))
{
// If this is a method, exclude it either if it was excluded itself (which may not be caught above,
// because `item_to_import` is the trait), or if its trait was excluded. We don't need to check
// the attributes here, since they pass from trait to methods on import map construction.
return false;
}
true
}
fn import_name(ctx: &CompletionContext<'_>) -> String {
let token_kind = ctx.token.kind();

View file

@ -8,8 +8,8 @@ use std::{iter, ops::ControlFlow};
use base_db::{RootQueryDb as _, salsa::AsDynDatabase};
use hir::{
DisplayTarget, HasAttrs, Local, ModPath, ModuleDef, ModuleSource, Name, PathResolution,
ScopeDef, Semantics, SemanticsScope, Symbol, Type, TypeInfo,
DisplayTarget, HasAttrs, Local, ModuleDef, ModuleSource, Name, PathResolution, ScopeDef,
Semantics, SemanticsScope, Symbol, Type, TypeInfo,
};
use ide_db::{
FilePosition, FxHashMap, FxHashSet, RootDatabase, famous_defs::FamousDefs,
@ -796,15 +796,12 @@ impl<'a> CompletionContext<'a> {
.exclude_traits
.iter()
.filter_map(|path| {
scope
.resolve_mod_path(&ModPath::from_segments(
hir::PathKind::Plain,
path.split("::").map(Symbol::intern).map(Name::new_symbol_root),
))
.find_map(|it| match it {
hir::resolve_absolute_path(db, path.split("::").map(Symbol::intern)).find_map(
|it| match it {
hir::ItemInNs::Types(ModuleDef::Trait(t)) => Some(t),
_ => None,
})
},
)
})
.collect();
@ -812,11 +809,7 @@ impl<'a> CompletionContext<'a> {
.exclude_flyimport
.iter()
.flat_map(|(path, kind)| {
scope
.resolve_mod_path(&ModPath::from_segments(
hir::PathKind::Plain,
path.split("::").map(Symbol::intern).map(Name::new_symbol_root),
))
hir::resolve_absolute_path(db, path.split("::").map(Symbol::intern))
.map(|it| (it.into_module_def(), *kind))
})
.collect();

View file

@ -334,7 +334,7 @@ pub(crate) fn render_expr(
continue;
};
item.add_import(LocatedImport::new(path, trait_item, trait_item));
item.add_import(LocatedImport::new_no_completion(path, trait_item, trait_item));
}
Some(item)

View file

@ -174,7 +174,7 @@ fn import_edits(ctx: &CompletionContext<'_>, requires: &[ModPath]) -> Option<Vec
ctx.config.insert_use.prefix_kind,
import_cfg,
)?;
Some((path.len() > 1).then(|| LocatedImport::new(path.clone(), item, item)))
Some((path.len() > 1).then(|| LocatedImport::new_no_completion(path.clone(), item, item)))
};
let mut res = Vec::with_capacity(requires.len());
for import in requires {

View file

@ -1555,7 +1555,10 @@ fn main() {
#[test]
fn excluded_trait_method_is_excluded() {
check_with_config(
CompletionConfig { exclude_traits: &["test::ExcludedTrait".to_owned()], ..TEST_CONFIG },
CompletionConfig {
exclude_traits: &["ra_test_fixture::ExcludedTrait".to_owned()],
..TEST_CONFIG
},
r#"
trait ExcludedTrait {
fn foo(&self) {}
@ -1575,23 +1578,20 @@ fn foo() {
}
"#,
expect![[r#"
me bar() (as ExcludedTrait) fn(&self)
me baz() (as ExcludedTrait) fn(&self)
me foo() (as ExcludedTrait) fn(&self)
me inherent() 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 {}
me inherent() 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 {}
"#]],
);
}
@ -1599,7 +1599,10 @@ fn foo() {
#[test]
fn excluded_trait_not_excluded_when_inherent() {
check_with_config(
CompletionConfig { exclude_traits: &["test::ExcludedTrait".to_owned()], ..TEST_CONFIG },
CompletionConfig {
exclude_traits: &["ra_test_fixture::ExcludedTrait".to_owned()],
..TEST_CONFIG
},
r#"
trait ExcludedTrait {
fn foo(&self) {}
@ -1633,7 +1636,10 @@ fn foo(v: &dyn ExcludedTrait) {
"#]],
);
check_with_config(
CompletionConfig { exclude_traits: &["test::ExcludedTrait".to_owned()], ..TEST_CONFIG },
CompletionConfig {
exclude_traits: &["ra_test_fixture::ExcludedTrait".to_owned()],
..TEST_CONFIG
},
r#"
trait ExcludedTrait {
fn foo(&self) {}
@ -1667,7 +1673,10 @@ fn foo(v: impl ExcludedTrait) {
"#]],
);
check_with_config(
CompletionConfig { exclude_traits: &["test::ExcludedTrait".to_owned()], ..TEST_CONFIG },
CompletionConfig {
exclude_traits: &["ra_test_fixture::ExcludedTrait".to_owned()],
..TEST_CONFIG
},
r#"
trait ExcludedTrait {
fn foo(&self) {}
@ -1706,7 +1715,7 @@ fn foo<T: ExcludedTrait>(v: T) {
fn excluded_trait_method_is_excluded_from_flyimport() {
check_with_config(
CompletionConfig {
exclude_traits: &["test::module2::ExcludedTrait".to_owned()],
exclude_traits: &["ra_test_fixture::module2::ExcludedTrait".to_owned()],
..TEST_CONFIG
},
r#"
@ -1730,23 +1739,20 @@ fn foo() {
}
"#,
expect![[r#"
me bar() (use module2::ExcludedTrait) fn(&self)
me baz() (use module2::ExcludedTrait) fn(&self)
me foo() (use module2::ExcludedTrait) fn(&self)
me inherent() 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 {}
me inherent() 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 {}
"#]],
);
}
@ -1756,7 +1762,7 @@ fn flyimport_excluded_trait_method_is_excluded_from_flyimport() {
check_with_config(
CompletionConfig {
exclude_flyimport: vec![(
"test::module2::ExcludedTrait".to_owned(),
"ra_test_fixture::module2::ExcludedTrait".to_owned(),
AutoImportExclusionType::Methods,
)],
..TEST_CONFIG
@ -1782,23 +1788,20 @@ fn foo() {
}
"#,
expect![[r#"
me bar() (use module2::ExcludedTrait) fn(&self)
me baz() (use module2::ExcludedTrait) fn(&self)
me foo() (use module2::ExcludedTrait) fn(&self)
me inherent() 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 {}
me inherent() 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 {}
"#]],
);
}
@ -1806,7 +1809,10 @@ fn foo() {
#[test]
fn excluded_trait_method_is_excluded_from_path_completion() {
check_with_config(
CompletionConfig { exclude_traits: &["test::ExcludedTrait".to_owned()], ..TEST_CONFIG },
CompletionConfig {
exclude_traits: &["ra_test_fixture::ExcludedTrait".to_owned()],
..TEST_CONFIG
},
r#"
pub trait ExcludedTrait {
fn foo(&self) {}
@ -1826,10 +1832,7 @@ fn foo() {
}
"#,
expect![[r#"
me bar() (as ExcludedTrait) fn(&self)
me baz() (as ExcludedTrait) fn(&self)
me foo() (as ExcludedTrait) fn(&self)
me inherent() fn(&self)
me inherent() fn(&self)
"#]],
);
}
@ -1837,7 +1840,10 @@ fn foo() {
#[test]
fn excluded_trait_method_is_not_excluded_when_trait_is_specified() {
check_with_config(
CompletionConfig { exclude_traits: &["test::ExcludedTrait".to_owned()], ..TEST_CONFIG },
CompletionConfig {
exclude_traits: &["ra_test_fixture::ExcludedTrait".to_owned()],
..TEST_CONFIG
},
r#"
pub trait ExcludedTrait {
fn foo(&self) {}
@ -1863,7 +1869,10 @@ fn foo() {
"#]],
);
check_with_config(
CompletionConfig { exclude_traits: &["test::ExcludedTrait".to_owned()], ..TEST_CONFIG },
CompletionConfig {
exclude_traits: &["ra_test_fixture::ExcludedTrait".to_owned()],
..TEST_CONFIG
},
r#"
pub trait ExcludedTrait {
fn foo(&self) {}
@ -1893,7 +1902,10 @@ fn foo() {
#[test]
fn excluded_trait_not_excluded_when_inherent_path() {
check_with_config(
CompletionConfig { exclude_traits: &["test::ExcludedTrait".to_owned()], ..TEST_CONFIG },
CompletionConfig {
exclude_traits: &["ra_test_fixture::ExcludedTrait".to_owned()],
..TEST_CONFIG
},
r#"
trait ExcludedTrait {
fn foo(&self) {}
@ -1914,7 +1926,10 @@ fn foo() {
"#]],
);
check_with_config(
CompletionConfig { exclude_traits: &["test::ExcludedTrait".to_owned()], ..TEST_CONFIG },
CompletionConfig {
exclude_traits: &["ra_test_fixture::ExcludedTrait".to_owned()],
..TEST_CONFIG
},
r#"
trait ExcludedTrait {
fn foo(&self) {}

View file

@ -1810,9 +1810,10 @@ fn function() {
#[test]
fn excluded_trait_item_included_when_exact_match() {
// FIXME: This does not work, we need to change the code.
check_with_config(
CompletionConfig {
exclude_traits: &["test::module2::ExcludedTrait".to_owned()],
exclude_traits: &["ra_test_fixture::module2::ExcludedTrait".to_owned()],
..TEST_CONFIG
},
r#"
@ -1830,8 +1831,120 @@ fn foo() {
true.foo$0
}
"#,
expect![[r#"
me foo() (use module2::ExcludedTrait) fn(&self)
"#]],
expect![""],
);
}
#[test]
fn excluded_via_attr() {
check(
r#"
mod module2 {
#[rust_analyzer::completions(ignore_flyimport)]
pub trait ExcludedTrait {
fn foo(&self) {}
fn bar(&self) {}
fn baz(&self) {}
}
impl<T> ExcludedTrait for T {}
}
fn foo() {
true.$0
}
"#,
expect![""],
);
check(
r#"
mod module2 {
#[rust_analyzer::completions(ignore_flyimport_methods)]
pub trait ExcludedTrait {
fn foo(&self) {}
fn bar(&self) {}
fn baz(&self) {}
}
impl<T> ExcludedTrait for T {}
}
fn foo() {
true.$0
}
"#,
expect![""],
);
check(
r#"
mod module2 {
#[rust_analyzer::completions(ignore_methods)]
pub trait ExcludedTrait {
fn foo(&self) {}
fn bar(&self) {}
fn baz(&self) {}
}
impl<T> ExcludedTrait for T {}
}
fn foo() {
true.$0
}
"#,
expect![""],
);
check(
r#"
mod module2 {
#[rust_analyzer::completions(ignore_flyimport)]
pub trait ExcludedTrait {
fn foo(&self) {}
fn bar(&self) {}
fn baz(&self) {}
}
impl<T> ExcludedTrait for T {}
}
fn foo() {
ExcludedTrait$0
}
"#,
expect![""],
);
check(
r#"
mod module2 {
#[rust_analyzer::completions(ignore_methods)]
pub trait ExcludedTrait {
fn foo(&self) {}
fn bar(&self) {}
fn baz(&self) {}
}
impl<T> ExcludedTrait for T {}
}
fn foo() {
ExcludedTrait$0
}
"#,
expect![[r#"
tt ExcludedTrait (use module2::ExcludedTrait)
"#]],
);
check(
r#"
mod module2 {
#[rust_analyzer::completions(ignore_flyimport)]
pub struct Foo {}
}
fn foo() {
Foo$0
}
"#,
expect![""],
);
}