|
| 1 | +use std::cmp::Reverse; |
| 2 | + |
| 3 | +use hir::{db::HirDatabase, Module}; |
1 | 4 | use ide_db::{
|
2 | 5 | helpers::mod_path_to_ast,
|
3 | 6 | imports::{
|
4 |
| - import_assets::{ImportAssets, ImportCandidate}, |
| 7 | + import_assets::{ImportAssets, ImportCandidate, LocatedImport}, |
5 | 8 | insert_use::{insert_use, ImportScope},
|
6 | 9 | },
|
7 | 10 | };
|
@@ -107,6 +110,19 @@ pub(crate) fn auto_import(acc: &mut Assists, ctx: &AssistContext) -> Option<()>
|
107 | 110 |
|
108 | 111 | // we aren't interested in different namespaces
|
109 | 112 | 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 | + |
110 | 126 | for import in proposed_imports {
|
111 | 127 | acc.add_group(
|
112 | 128 | &group_label,
|
@@ -158,11 +174,135 @@ fn group_label(import_candidate: &ImportCandidate) -> GroupLabel {
|
158 | 174 | GroupLabel(name)
|
159 | 175 | }
|
160 | 176 |
|
| 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, ¤t_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 | + |
161 | 235 | #[cfg(test)]
|
162 | 236 | mod tests {
|
163 | 237 | use super::*;
|
164 | 238 |
|
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 | + } |
166 | 306 |
|
167 | 307 | #[test]
|
168 | 308 | fn not_applicable_if_scope_inside_macro() {
|
|
0 commit comments