Skip to content
This repository was archived by the owner on May 28, 2025. It is now read-only.

Commit aef1630

Browse files
committed
Order auto-imports by relevance
This first attempt prefers items that are closer to the module they are imported in.
1 parent 2a978e1 commit aef1630

File tree

1 file changed

+140
-2
lines changed

1 file changed

+140
-2
lines changed

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

Lines changed: 140 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
use std::cmp::Reverse;
2+
3+
use hir::{db::HirDatabase, Module};
14
use ide_db::{
25
helpers::mod_path_to_ast,
36
imports::{
4-
import_assets::{ImportAssets, ImportCandidate},
7+
import_assets::{ImportAssets, ImportCandidate, LocatedImport},
58
insert_use::{insert_use, ImportScope},
69
},
710
};
@@ -107,6 +110,10 @@ pub(crate) fn auto_import(acc: &mut Assists, ctx: &AssistContext) -> Option<()>
107110

108111
// we aren't interested in different namespaces
109112
proposed_imports.dedup_by(|a, b| a.import_path == b.import_path);
113+
114+
// prioritize more relevant imports
115+
proposed_imports.sort_by_key(|import| Reverse(relevance_score(ctx, import)));
116+
110117
for import in proposed_imports {
111118
acc.add_group(
112119
&group_label,
@@ -158,11 +165,142 @@ fn group_label(import_candidate: &ImportCandidate) -> GroupLabel {
158165
GroupLabel(name)
159166
}
160167

168+
/// Determine how relevant a given import is in the current context. Higher scores are more
169+
/// relevant.
170+
fn relevance_score(ctx: &AssistContext, import: &LocatedImport) -> i32 {
171+
let mut score = 0;
172+
173+
let db = ctx.db();
174+
175+
let item_module = match import.item_to_import {
176+
hir::ItemInNs::Types(item) | hir::ItemInNs::Values(item) => item.module(db),
177+
hir::ItemInNs::Macros(makro) => Some(makro.module(db)),
178+
};
179+
180+
let current_node = match ctx.covering_element() {
181+
NodeOrToken::Node(node) => Some(node),
182+
NodeOrToken::Token(token) => token.parent(),
183+
};
184+
185+
if let Some(module) = item_module.as_ref() {
186+
if module.krate().is_builtin(db) {
187+
// prefer items builtin to the language
188+
score += 5;
189+
}
190+
}
191+
192+
match item_module.zip(current_node) {
193+
// get the distance between the modules (prefer items that are more local)
194+
Some((item_module, current_node)) => {
195+
let current_module = ctx.sema.scope(&current_node).unwrap().module();
196+
score -= module_distance_hueristic(&current_module, &item_module, db) as i32;
197+
}
198+
199+
// could not find relevant modules, so just use the length of the path as an estimate
200+
None => return -(2 * import.import_path.len() as i32),
201+
}
202+
203+
score
204+
}
205+
206+
/// A heuristic that gives a higher score to modules that are more separated.
207+
fn module_distance_hueristic(current: &Module, item: &Module, db: &dyn HirDatabase) -> usize {
208+
let mut current_path = current.path_to_root(db);
209+
let mut item_path = item.path_to_root(db);
210+
211+
current_path.reverse();
212+
item_path.reverse();
213+
214+
let mut i = 0;
215+
while i < current_path.len() && i < item_path.len() {
216+
if current_path[i] == item_path[i] {
217+
i += 1
218+
} else {
219+
break;
220+
}
221+
}
222+
223+
let distinct_distance = current_path.len() + item_path.len();
224+
let common_prefix = 2 * i;
225+
226+
// prefer builtin crates and items within the same crate
227+
let crate_boundary_cost =
228+
if item.krate().is_builtin(db) || current.krate() == item.krate() { 0 } else { 4 };
229+
230+
distinct_distance - common_prefix + crate_boundary_cost
231+
}
232+
161233
#[cfg(test)]
162234
mod tests {
163235
use super::*;
164236

165-
use crate::tests::{check_assist, check_assist_not_applicable, check_assist_target};
237+
use hir::Semantics;
238+
use ide_db::{
239+
assists::AssistResolveStrategy,
240+
base_db::{fixture::WithFixture, FileRange},
241+
RootDatabase,
242+
};
243+
244+
use crate::tests::{
245+
check_assist, check_assist_not_applicable, check_assist_target, TEST_CONFIG,
246+
};
247+
248+
fn check_auto_import_order(before: &str, order: &[&str]) {
249+
let (db, file_id, range_or_offset) = RootDatabase::with_range_or_offset(before);
250+
let frange = FileRange { file_id, range: range_or_offset.into() };
251+
252+
let sema = Semantics::new(&db);
253+
let config = TEST_CONFIG;
254+
let ctx = AssistContext::new(sema, &config, frange);
255+
let mut acc = Assists::new(&ctx, AssistResolveStrategy::All);
256+
auto_import(&mut acc, &ctx);
257+
let assists = acc.finish();
258+
259+
let labels = assists.iter().map(|assist| assist.label.to_string()).collect::<Vec<_>>();
260+
261+
assert_eq!(labels, order);
262+
}
263+
264+
#[test]
265+
fn prefer_shorter_paths() {
266+
let before = r"
267+
//- /main.rs crate:main deps:foo,bar
268+
HashMap$0::new();
269+
270+
//- /lib.rs crate:foo
271+
pub mod collections { pub struct HashMap; }
272+
273+
//- /lib.rs crate:bar
274+
pub mod collections { pub mod hash_map { pub struct HashMap; } }
275+
";
276+
277+
check_auto_import_order(
278+
before,
279+
&["Import `foo::collections::HashMap`", "Import `bar::collections::hash_map::HashMap`"],
280+
)
281+
}
282+
283+
#[test]
284+
fn prefer_same_crate() {
285+
let before = r"
286+
//- /main.rs crate:main deps:foo
287+
HashMap$0::new();
288+
289+
mod collections {
290+
pub mod hash_map {
291+
pub struct HashMap;
292+
}
293+
}
294+
295+
//- /lib.rs crate:foo
296+
pub struct HashMap;
297+
";
298+
299+
check_auto_import_order(
300+
before,
301+
&["Import `collections::hash_map::HashMap`", "Import `foo::HashMap`"],
302+
)
303+
}
166304

167305
#[test]
168306
fn not_applicable_if_scope_inside_macro() {

0 commit comments

Comments
 (0)