Skip to content

Commit 58b6d46

Browse files
committed
Auto merge of rust-lang#12333 - nolanderc:order-import-assist, r=Veykril
Order auto-imports by relevance Fixes rust-lang#10337. Basically we sort the imports according to how "far away" the imported item is from where we want to import it to. This change makes it so that imports from the current crate are sorted before any third-party crates. Additionally, we make an exception for builtin crates (`std`, `core`, etc.) so that they are sorted before any third-party crates. There are probably other heuristics that should be added to improve the experience (such as preferring imports that are common elsewhere in the same crate, and ranking crates depending on the dependency graph). However, I think this is a first good step. PS. This is my first time contributing here, so please be gentle if I have missed something obvious :-)
2 parents ea15e10 + 8e5b318 commit 58b6d46

File tree

1 file changed

+142
-2
lines changed

1 file changed

+142
-2
lines changed

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

+142-2
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,19 @@ 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+
let current_node = match ctx.covering_element() {
115+
NodeOrToken::Node(node) => Some(node),
116+
NodeOrToken::Token(token) => token.parent(),
117+
};
118+
119+
let current_module =
120+
current_node.as_ref().and_then(|node| ctx.sema.scope(node)).map(|scope| scope.module());
121+
122+
// prioritize more relevant imports
123+
proposed_imports
124+
.sort_by_key(|import| Reverse(relevance_score(ctx, import, current_module.as_ref())));
125+
110126
for import in proposed_imports {
111127
acc.add_group(
112128
&group_label,
@@ -158,11 +174,135 @@ fn group_label(import_candidate: &ImportCandidate) -> GroupLabel {
158174
GroupLabel(name)
159175
}
160176

177+
/// Determine how relevant a given import is in the current context. Higher scores are more
178+
/// relevant.
179+
fn relevance_score(
180+
ctx: &AssistContext,
181+
import: &LocatedImport,
182+
current_module: Option<&Module>,
183+
) -> i32 {
184+
let mut score = 0;
185+
186+
let db = ctx.db();
187+
188+
let item_module = match import.item_to_import {
189+
hir::ItemInNs::Types(item) | hir::ItemInNs::Values(item) => item.module(db),
190+
hir::ItemInNs::Macros(makro) => Some(makro.module(db)),
191+
};
192+
193+
match item_module.zip(current_module) {
194+
// get the distance between the imported path and the current module
195+
// (prefer items that are more local)
196+
Some((item_module, current_module)) => {
197+
score -= module_distance_hueristic(db, &current_module, &item_module) as i32;
198+
}
199+
200+
// could not find relevant modules, so just use the length of the path as an estimate
201+
None => return -(2 * import.import_path.len() as i32),
202+
}
203+
204+
score
205+
}
206+
207+
/// A heuristic that gives a higher score to modules that are more separated.
208+
fn module_distance_hueristic(db: &dyn HirDatabase, current: &Module, item: &Module) -> usize {
209+
// get the path starting from the item to the respective crate roots
210+
let mut current_path = current.path_to_root(db);
211+
let mut item_path = item.path_to_root(db);
212+
213+
// we want paths going from the root to the item
214+
current_path.reverse();
215+
item_path.reverse();
216+
217+
// length of the common prefix of the two paths
218+
let prefix_length = current_path.iter().zip(&item_path).take_while(|(a, b)| a == b).count();
219+
220+
// how many modules differ between the two paths (all modules, removing any duplicates)
221+
let distinct_length = current_path.len() + item_path.len() - 2 * prefix_length;
222+
223+
// cost of importing from another crate
224+
let crate_boundary_cost = if current.krate() == item.krate() {
225+
0
226+
} else if item.krate().is_builtin(db) {
227+
2
228+
} else {
229+
4
230+
};
231+
232+
distinct_length + crate_boundary_cost
233+
}
234+
161235
#[cfg(test)]
162236
mod tests {
163237
use super::*;
164238

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

167307
#[test]
168308
fn not_applicable_if_scope_inside_macro() {

0 commit comments

Comments
 (0)