Skip to content

Commit 484e8b2

Browse files
Allow crate authors to declare that their trait prefers to be imported as _
For example for extension traits. Provide an attribute for that. It'll affect flyimport and the autoimport quickfix, as explained in the code.
1 parent 3c4ae11 commit 484e8b2

File tree

12 files changed

+219
-59
lines changed

12 files changed

+219
-59
lines changed

crates/hir-def/src/attrs.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,9 @@ fn match_attr_flags(attr_flags: &mut AttrFlags, attr: Meta) -> ControlFlow<Infal
234234
2 => match path.segments[0].text() {
235235
"rust_analyzer" => match path.segments[1].text() {
236236
"skip" => attr_flags.insert(AttrFlags::RUST_ANALYZER_SKIP),
237+
"prefer_underscore_import" => {
238+
attr_flags.insert(AttrFlags::PREFER_UNDERSCORE_IMPORT)
239+
}
237240
_ => {}
238241
},
239242
_ => {}
@@ -311,6 +314,8 @@ bitflags::bitflags! {
311314
const MACRO_STYLE_BRACES = 1 << 46;
312315
const MACRO_STYLE_BRACKETS = 1 << 47;
313316
const MACRO_STYLE_PARENTHESES = 1 << 48;
317+
318+
const PREFER_UNDERSCORE_IMPORT = 1 << 49;
314319
}
315320
}
316321

crates/hir/src/lib.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3256,6 +3256,19 @@ impl Trait {
32563256
pub fn complete(self, db: &dyn HirDatabase) -> Complete {
32573257
Complete::extract(true, self.attrs(db).attrs)
32583258
}
3259+
3260+
// Feature: Prefer Underscore Import Attribute
3261+
// Crate authors can declare that their trait prefers to be imported `as _`. This can be used
3262+
// for example for extension traits. To do that, a trait has to include the attribute
3263+
// `#[rust_analyzer::prefer_underscore_import]`
3264+
//
3265+
// When a trait includes this attribute, flyimport will import it `as _`, and the quickfix
3266+
// to import it will prefer to import it `as _` (but allow to import it normally as well).
3267+
//
3268+
// Malformed attributes will be ignored without warnings.
3269+
pub fn prefer_underscore_import(self, db: &dyn HirDatabase) -> bool {
3270+
AttrFlags::query(db, self.id.into()).contains(AttrFlags::PREFER_UNDERSCORE_IMPORT)
3271+
}
32593272
}
32603273

32613274
impl HasVisibility for Trait {

crates/ide-assists/src/handlers/auto_import.rs

Lines changed: 108 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use ide_db::{
66
active_parameter::ActiveParameter,
77
helpers::mod_path_to_ast,
88
imports::{
9-
import_assets::{ImportAssets, ImportCandidate, LocatedImport},
9+
import_assets::{ImportAssets, ImportCandidate, LocatedImport, TraitImportCandidate},
1010
insert_use::{ImportScope, insert_use, insert_use_as_alias},
1111
},
1212
};
@@ -123,44 +123,48 @@ pub(crate) fn auto_import(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<
123123

124124
let (assist_id, import_name) =
125125
(AssistId::quick_fix("auto_import"), import_path.display(ctx.db(), edition));
126-
acc.add_group(
127-
&group_label,
128-
assist_id,
129-
format!("Import `{import_name}`"),
130-
range,
131-
|builder| {
126+
let add_normal_import = |acc: &mut Assists, label| {
127+
acc.add_group(&group_label, assist_id, label, range, |builder| {
132128
let scope = builder.make_import_scope_mut(scope.clone());
133129
insert_use(&scope, mod_path_to_ast(&import_path, edition), &ctx.config.insert_use);
134-
},
135-
);
136-
137-
match import_assets.import_candidate() {
138-
ImportCandidate::TraitAssocItem(name) | ImportCandidate::TraitMethod(name) => {
139-
let is_method =
140-
matches!(import_assets.import_candidate(), ImportCandidate::TraitMethod(_));
141-
let type_ = if is_method { "method" } else { "item" };
142-
let group_label = GroupLabel(format!(
143-
"Import a trait for {} {} by alias",
144-
type_,
145-
name.assoc_item_name.text()
146-
));
147-
acc.add_group(
148-
&group_label,
149-
assist_id,
150-
format!("Import `{import_name} as _`"),
151-
range,
152-
|builder| {
153-
let scope = builder.make_import_scope_mut(scope.clone());
154-
insert_use_as_alias(
155-
&scope,
156-
mod_path_to_ast(&import_path, edition),
157-
&ctx.config.insert_use,
158-
edition,
159-
);
160-
},
130+
})
131+
};
132+
let add_underscore_import = |acc: &mut Assists, name: &TraitImportCandidate<'_>, label| {
133+
let is_method =
134+
matches!(import_assets.import_candidate(), ImportCandidate::TraitMethod(_));
135+
let type_ = if is_method { "method" } else { "item" };
136+
let group_label = GroupLabel(format!(
137+
"Import a trait for {} {} by alias",
138+
type_,
139+
name.assoc_item_name.text()
140+
));
141+
acc.add_group(&group_label, assist_id, label, range, |builder| {
142+
let scope = builder.make_import_scope_mut(scope.clone());
143+
insert_use_as_alias(
144+
&scope,
145+
mod_path_to_ast(&import_path, edition),
146+
&ctx.config.insert_use,
147+
edition,
161148
);
162-
}
163-
_ => {}
149+
});
150+
};
151+
152+
if let ImportCandidate::TraitAssocItem(name) | ImportCandidate::TraitMethod(name) =
153+
import_assets.import_candidate()
154+
{
155+
if let hir::ItemInNs::Types(hir::ModuleDef::Trait(trait_to_import)) =
156+
import.item_to_import
157+
&& trait_to_import.prefer_underscore_import(ctx.db())
158+
{
159+
// Flip the order of the suggestions and show a preference for `as _` in the name.
160+
add_underscore_import(acc, name, format!("Import `{import_name}`"));
161+
add_normal_import(acc, format!("Import `{import_name}` without `as _`"));
162+
} else {
163+
add_normal_import(acc, format!("Import `{import_name}`"));
164+
add_underscore_import(acc, name, format!("Import `{import_name} as _`"));
165+
}
166+
} else {
167+
add_normal_import(acc, format!("Import `{import_name}`"));
164168
}
165169
}
166170
Some(())
@@ -1928,4 +1932,72 @@ fn f() {
19281932
"#;
19291933
check_auto_import_order(before, &["Import `foo::wanted`", "Import `quux::wanted`"]);
19301934
}
1935+
1936+
#[test]
1937+
fn prefer_underscore_import() {
1938+
check_assist_by_label(
1939+
auto_import,
1940+
r#"
1941+
mod foo {
1942+
#[rust_analyzer::prefer_underscore_import]
1943+
pub trait Ext {
1944+
fn bar(&self) {}
1945+
}
1946+
impl<T> Ext for T {}
1947+
}
1948+
1949+
fn baz() {
1950+
1.b$0ar();
1951+
}
1952+
"#,
1953+
r#"
1954+
use foo::Ext as _;
1955+
1956+
mod foo {
1957+
#[rust_analyzer::prefer_underscore_import]
1958+
pub trait Ext {
1959+
fn bar(&self) {}
1960+
}
1961+
impl<T> Ext for T {}
1962+
}
1963+
1964+
fn baz() {
1965+
1.bar();
1966+
}
1967+
"#,
1968+
"Import `foo::Ext`",
1969+
);
1970+
check_assist_by_label(
1971+
auto_import,
1972+
r#"
1973+
mod foo {
1974+
#[rust_analyzer::prefer_underscore_import]
1975+
pub trait Ext {
1976+
fn bar(&self) {}
1977+
}
1978+
impl<T> Ext for T {}
1979+
}
1980+
1981+
fn baz() {
1982+
1.b$0ar();
1983+
}
1984+
"#,
1985+
r#"
1986+
use foo::Ext;
1987+
1988+
mod foo {
1989+
#[rust_analyzer::prefer_underscore_import]
1990+
pub trait Ext {
1991+
fn bar(&self) {}
1992+
}
1993+
impl<T> Ext for T {}
1994+
}
1995+
1996+
fn baz() {
1997+
1.bar();
1998+
}
1999+
"#,
2000+
"Import `foo::Ext` without `as _`",
2001+
);
2002+
}
19312003
}

crates/ide-completion/src/item.rs

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,15 @@ pub struct CompletionItem {
8484
pub ref_match: Option<(CompletionItemRefMode, TextSize)>,
8585

8686
/// The import data to add to completion's edits.
87-
pub import_to_add: SmallVec<[String; 1]>,
87+
pub import_to_add: SmallVec<[CompletionItemImport; 1]>,
88+
}
89+
90+
#[derive(Clone, UpmapFromRaFixture)]
91+
pub struct CompletionItemImport {
92+
/// The path to import.
93+
pub path: String,
94+
/// Whether to import `as _`.
95+
pub as_underscore: bool,
8896
}
8997

9098
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
@@ -574,7 +582,18 @@ impl Builder {
574582
let import_to_add = self
575583
.imports_to_add
576584
.into_iter()
577-
.map(|import| import.import_path.display(db, self.edition).to_string())
585+
.map(|import| {
586+
let path = import.import_path.display(db, self.edition).to_string();
587+
let as_underscore =
588+
if let hir::ItemInNs::Types(hir::ModuleDef::Trait(trait_to_import)) =
589+
import.item_to_import
590+
{
591+
trait_to_import.prefer_underscore_import(db)
592+
} else {
593+
false
594+
};
595+
CompletionItemImport { path, as_underscore }
596+
})
578597
.collect();
579598

580599
CompletionItem {

crates/ide-completion/src/lib.rs

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ use crate::{
3636
pub use crate::{
3737
config::{AutoImportExclusionType, CallableSnippets, CompletionConfig},
3838
item::{
39-
CompletionItem, CompletionItemKind, CompletionItemRefMode, CompletionRelevance,
40-
CompletionRelevancePostfixMatch, CompletionRelevanceReturnType,
39+
CompletionItem, CompletionItemImport, CompletionItemKind, CompletionItemRefMode,
40+
CompletionRelevance, CompletionRelevancePostfixMatch, CompletionRelevanceReturnType,
4141
CompletionRelevanceTypeMatch,
4242
},
4343
snippet::{Snippet, SnippetScope},
@@ -279,7 +279,7 @@ pub fn resolve_completion_edits(
279279
db: &RootDatabase,
280280
config: &CompletionConfig<'_>,
281281
FilePosition { file_id, offset }: FilePosition,
282-
imports: impl IntoIterator<Item = String>,
282+
imports: impl IntoIterator<Item = CompletionItemImport>,
283283
) -> Option<Vec<TextEdit>> {
284284
let _p = tracing::info_span!("resolve_completion_edits").entered();
285285
let sema = hir::Semantics::new(db);
@@ -298,12 +298,18 @@ pub fn resolve_completion_edits(
298298
let new_ast = scope.clone_for_update();
299299
let mut import_insert = TextEdit::builder();
300300

301-
imports.into_iter().for_each(|full_import_path| {
302-
insert_use::insert_use(
303-
&new_ast,
304-
make::path_from_text_with_edition(&full_import_path, current_edition),
305-
&config.insert_use,
306-
);
301+
imports.into_iter().for_each(|import| {
302+
let full_path = make::path_from_text_with_edition(&import.path, current_edition);
303+
if import.as_underscore {
304+
insert_use::insert_use_as_alias(
305+
&new_ast,
306+
full_path,
307+
&config.insert_use,
308+
current_edition,
309+
);
310+
} else {
311+
insert_use::insert_use(&new_ast, full_path, &config.insert_use);
312+
}
307313
});
308314

309315
diff(scope.as_syntax_node(), new_ast.as_syntax_node()).into_text_edit(&mut import_insert);

crates/ide-completion/src/tests/flyimport.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2024,3 +2024,38 @@ fn main() {
20242024
"#,
20252025
);
20262026
}
2027+
2028+
#[test]
2029+
fn prefer_underscore_import() {
2030+
check_edit(
2031+
"bar",
2032+
r#"
2033+
mod foo {
2034+
#[rust_analyzer::prefer_underscore_import]
2035+
pub trait Ext {
2036+
fn bar(&self) {}
2037+
}
2038+
impl<T> Ext for T {}
2039+
}
2040+
2041+
fn baz() {
2042+
1.bar$0
2043+
}
2044+
"#,
2045+
r#"
2046+
use foo::Ext as _;
2047+
2048+
mod foo {
2049+
#[rust_analyzer::prefer_underscore_import]
2050+
pub trait Ext {
2051+
fn bar(&self) {}
2052+
}
2053+
impl<T> Ext for T {}
2054+
}
2055+
2056+
fn baz() {
2057+
1.bar();$0
2058+
}
2059+
"#,
2060+
);
2061+
}

crates/ide/src/lib.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,8 @@ pub use ide_assists::{
125125
};
126126
pub use ide_completion::{
127127
CallableSnippets, CompletionConfig, CompletionFieldsToResolve, CompletionItem,
128-
CompletionItemKind, CompletionItemRefMode, CompletionRelevance, Snippet, SnippetScope,
128+
CompletionItemImport, CompletionItemKind, CompletionItemRefMode, CompletionRelevance, Snippet,
129+
SnippetScope,
129130
};
130131
pub use ide_db::{
131132
FileId, FilePosition, FileRange, RootDatabase, Severity, SymbolKind,
@@ -766,7 +767,7 @@ impl Analysis {
766767
&self,
767768
config: &CompletionConfig<'_>,
768769
position: FilePosition,
769-
imports: impl IntoIterator<Item = String> + std::panic::UnwindSafe,
770+
imports: impl IntoIterator<Item = CompletionItemImport> + std::panic::UnwindSafe,
770771
) -> Cancellable<Vec<TextEdit>> {
771772
Ok(self
772773
.with_db(|db| ide_completion::resolve_completion_edits(db, config, position, imports))?

crates/rust-analyzer/src/handlers/request.rs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ use anyhow::Context;
77

88
use base64::{Engine, prelude::BASE64_STANDARD};
99
use ide::{
10-
AssistKind, AssistResolveStrategy, Cancellable, CompletionFieldsToResolve, FilePosition,
11-
FileRange, FileStructureConfig, FindAllRefsConfig, HoverAction, HoverGotoTypeData,
12-
InlayFieldsToResolve, Query, RangeInfo, ReferenceCategory, Runnable, RunnableKind,
13-
SingleResolve, SourceChange, TextEdit,
10+
AssistKind, AssistResolveStrategy, Cancellable, CompletionFieldsToResolve,
11+
CompletionItemImport, FilePosition, FileRange, FileStructureConfig, FindAllRefsConfig,
12+
HoverAction, HoverGotoTypeData, InlayFieldsToResolve, Query, RangeInfo, ReferenceCategory,
13+
Runnable, RunnableKind, SingleResolve, SourceChange, TextEdit,
1414
};
1515
use ide_db::{FxHashMap, SymbolKind};
1616
use itertools::Itertools;
@@ -1233,7 +1233,10 @@ pub(crate) fn handle_completion_resolve(
12331233
.resolve_completion_edits(
12341234
&forced_resolve_completions_config,
12351235
position,
1236-
resolve_data.imports.into_iter().map(|import| import.full_import_path),
1236+
resolve_data.imports.into_iter().map(|import| CompletionItemImport {
1237+
path: import.full_import_path,
1238+
as_underscore: import.as_underscore,
1239+
}),
12371240
)?
12381241
.into_iter()
12391242
.flat_map(|edit| edit.into_iter().map(|indel| to_proto::text_edit(&line_index, indel)))

crates/rust-analyzer/src/lsp.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
use core::fmt;
44

55
use hir::Mutability;
6-
use ide::{CompletionItem, CompletionItemRefMode, CompletionRelevance};
6+
use ide::{CompletionItem, CompletionItemImport, CompletionItemRefMode, CompletionRelevance};
77
use tenthash::TentHash;
88

99
pub mod ext;
@@ -136,8 +136,10 @@ pub(crate) fn completion_item_hash(item: &CompletionItem, is_ref_completion: boo
136136

137137
hasher.update(item.import_to_add.len().to_ne_bytes());
138138
for import_path in &item.import_to_add {
139-
hasher.update(import_path.len().to_ne_bytes());
140-
hasher.update(import_path);
139+
let CompletionItemImport { path, as_underscore } = import_path;
140+
hasher.update(path.len().to_ne_bytes());
141+
hasher.update(path);
142+
hasher.update([u8::from(*as_underscore)]);
141143
}
142144

143145
hasher.finalize()

0 commit comments

Comments
 (0)