From 10301a8a427d1fe97830675147c7d7f7333d74ab Mon Sep 17 00:00:00 2001 From: Nicolas Guichard Date: Sat, 6 Jun 2026 20:46:31 +0200 Subject: [PATCH 1/8] Make Analysis Clone When multi-threading StaticIndex::compute, each thread will get its own Analysis clone. --- crates/ide/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ide/src/lib.rs b/crates/ide/src/lib.rs index bd09cb11ec05..135f0981ac23 100644 --- a/crates/ide/src/lib.rs +++ b/crates/ide/src/lib.rs @@ -239,7 +239,7 @@ impl Default for AnalysisHost { /// entry point for asking semantic information about the world. When the world /// state is advanced using `AnalysisHost::apply_change` method, all existing /// `Analysis` are canceled (most method return `Err(Canceled)`). -#[derive(Debug)] +#[derive(Clone, Debug)] pub struct Analysis { db: RootDatabase, } From d65894228f58bea0999a2806ca383b7b46000c26 Mon Sep 17 00:00:00 2001 From: Nicolas Guichard Date: Fri, 5 Jun 2026 13:19:20 +0200 Subject: [PATCH 2/8] In StaticIndex::compute, build the list of files to index upfront When multi-threading StaticIndex::compute, we want the list of files to be computed only once and the files to index to be split between threads. --- crates/ide/src/static_index.rs | 55 ++++++++++++++++++++-------------- 1 file changed, 32 insertions(+), 23 deletions(-) diff --git a/crates/ide/src/static_index.rs b/crates/ide/src/static_index.rs index 9e8d772cb3d1..313d127b4a4d 100644 --- a/crates/ide/src/static_index.rs +++ b/crates/ide/src/static_index.rs @@ -5,7 +5,7 @@ use arrayvec::ArrayVec; use either::Either; use hir::{Crate, Module, Semantics, db::HirDatabase}; use ide_db::{ - FileId, FileRange, FxHashMap, FxHashSet, RootDatabase, + FileId, FileRange, FxHashMap, RootDatabase, base_db::{SourceDatabase, VfsPath}, defs::{Definition, IdentClass}, documentation::Documentation, @@ -270,21 +270,37 @@ impl<'a> StaticIndex<'a> { vendored_libs_config: VendoredLibrariesConfig<'_>, ) -> StaticIndex<'a> { let db = &analysis.db; - hir::attach_db(db, || { - let work = all_modules(db).into_iter().filter(|module| { - let file_id = module.definition_source_file_id(db).original_file(db); - let source_root = - db.file_source_root(file_id.file_id(&analysis.db)).source_root_id(db); - let source_root = db.source_root(source_root).source_root(db); - let is_vendored = match vendored_libs_config { - VendoredLibrariesConfig::Included { workspace_root } => source_root - .path_for_file(&file_id.file_id(&analysis.db)) - .is_some_and(|module_path| module_path.starts_with(workspace_root)), - VendoredLibrariesConfig::Excluded => false, - }; - - !source_root.is_library || is_vendored + + let files_to_index = { + let mut files_to_index: Vec<_> = hir::attach_db(db, || { + let modules = all_modules(db).into_iter(); + + let modules_to_index = modules.filter(|module| { + let file_id = module.definition_source_file_id(db).original_file(db); + let source_root = db.file_source_root(file_id.file_id(db)).source_root_id(db); + let source_root = db.source_root(source_root).source_root(db); + let is_vendored = match vendored_libs_config { + VendoredLibrariesConfig::Included { workspace_root } => source_root + .path_for_file(&file_id.file_id(db)) + .is_some_and(|module_path| module_path.starts_with(workspace_root)), + VendoredLibrariesConfig::Excluded => false, + }; + + !source_root.is_library || is_vendored + }); + + let files_to_index = modules_to_index.map(|module| { + module.definition_source_file_id(db).original_file(db).file_id(db) + }); + + files_to_index.collect() }); + files_to_index.sort(); + files_to_index.dedup(); + files_to_index + }; + + hir::attach_db(db, || { let mut this = StaticIndex { files: vec![], tokens: Default::default(), @@ -292,15 +308,8 @@ impl<'a> StaticIndex<'a> { db, def_map: Default::default(), }; - let mut visited_files = FxHashSet::default(); - for module in work { - let file_id = - module.definition_source_file_id(db).original_file(db).file_id(&analysis.db); - if visited_files.contains(&file_id) { - continue; - } + for file_id in files_to_index { this.add_file(file_id); - visited_files.insert(file_id); } this }) From 59250a15c1b83a5a435b86a14845a724a905ab38 Mon Sep 17 00:00:00 2001 From: Nicolas Guichard Date: Thu, 4 Jun 2026 23:10:54 +0200 Subject: [PATCH 3/8] Replace StaticIndex::add_file(&mut self) with a free function MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The analysis, db and def_map members of StaticIndex were only used for building the index and are of no use once the index is built. Instead of have an &mut self method which mutates those fields, we can extract the FileId→StaticIndexedFile indexing into a free function and keep the token_store and def_map as locals in StaticIndex::compute. This will make multi-threading StaticIndex::compute easier. --- crates/ide/src/static_index.rs | 218 ++++++++++++++++----------------- 1 file changed, 105 insertions(+), 113 deletions(-) diff --git a/crates/ide/src/static_index.rs b/crates/ide/src/static_index.rs index 313d127b4a4d..f79e43daf1bf 100644 --- a/crates/ide/src/static_index.rs +++ b/crates/ide/src/static_index.rs @@ -26,12 +26,9 @@ use crate::{ /// /// The intended use-case is powering read-only code browsers and emitting LSIF/SCIP. #[derive(Debug)] -pub struct StaticIndex<'a> { +pub struct StaticIndex { pub files: Vec, pub tokens: TokenStore, - analysis: &'a Analysis, - db: &'a RootDatabase, - def_map: FxHashMap, TokenId>, } #[derive(Debug)] @@ -164,111 +161,110 @@ pub enum VendoredLibrariesConfig<'a> { Excluded, } -impl<'a> StaticIndex<'a> { - fn add_file(&mut self, file_id: FileId) { - let current_crate = crates_for(self.db, file_id).pop().map(Into::into); - let folds = self.analysis.folding_ranges(file_id, true).unwrap(); - // hovers - let sema = hir::Semantics::new(self.db); - let root = sema.parse_guess_edition(file_id).syntax().clone(); - let edition = sema.attach_first_edition(file_id).edition(sema.db); - let display_target = match sema.first_crate(file_id) { - Some(krate) => krate.to_display_target(sema.db), - None => return, - }; - let tokens = root.descendants_with_tokens().filter_map(|it| match it { - syntax::NodeOrToken::Node(_) => None, - syntax::NodeOrToken::Token(it) => Some(it), - }); - let hover_config = HoverConfig { - links_in_hover: true, - memory_layout: None, - documentation: true, - keywords: true, - format: crate::HoverDocFormat::Markdown, - max_trait_assoc_items_count: None, - max_fields_count: Some(5), - max_enum_variants_count: Some(5), - max_subst_ty_len: SubstTyLen::Unlimited, - show_drop_glue: true, - ra_fixture: RaFixtureConfig::default(), - }; - let mut result = StaticIndexedFile { file_id, folds, tokens: vec![] }; - - let mut add_token = |def: Definition<'a>, range: TextRange, scope_node: &SyntaxNode| { - let id = if let Some(it) = self.def_map.get(&def) { - *it - } else { - let nav = def.try_to_nav(&sema).map(UpmappingResult::call_site); - let it = self.tokens.insert(TokenStaticData { - documentation: documentation_for_definition(&sema, def, scope_node), - hover: Some(hover_for_definition( - &sema, - file_id, - def, - None, - scope_node, - None, - false, - &hover_config, - edition, - display_target, - )), - definition: nav.as_ref().map(|it| FileRange { - file_id: it.file_id, - range: it.focus_or_full_range(), - }), - definition_body: nav.as_ref().map(|it| FileRange { - file_id: it.file_id, - range: definition_range_excluding_trivia(&sema, it.file_id, it.full_range), - }), - references: vec![], - moniker: current_crate.and_then(|cc| def_to_moniker(self.db, def, cc)), - display_name: def - .name(self.db) - .map(|name| name.display(self.db, edition).to_string()), - signature: Some(def.label(self.db, display_target)), - kind: def_to_kind(self.db, def), - }); - self.def_map.insert(def, it); - it - }; - let token = self.tokens.get_mut(id).unwrap(); - token.references.push(ReferenceData { - range: FileRange { range, file_id }, - is_definition: match def.try_to_nav(&sema).map(UpmappingResult::call_site) { - Some(it) => it.file_id == file_id && it.focus_or_full_range() == range, - None => false, - }, +fn index_file<'a>( + analysis: &'a Analysis, + token_store: &mut TokenStore, + def_map: &mut FxHashMap, TokenId>, + file_id: FileId, +) -> Option { + let db = &analysis.db; + + let current_crate = crates_for(db, file_id).pop().map(Into::into); + let folds = analysis.folding_ranges(file_id, true).unwrap(); + // hovers + let sema = hir::Semantics::new(db); + let root = sema.parse_guess_edition(file_id).syntax().clone(); + let edition = sema.attach_first_edition(file_id).edition(sema.db); + let display_target = sema.first_crate(file_id)?.to_display_target(sema.db); + let tokens = root.descendants_with_tokens().filter_map(|it| match it { + syntax::NodeOrToken::Node(_) => None, + syntax::NodeOrToken::Token(it) => Some(it), + }); + let hover_config = HoverConfig { + links_in_hover: true, + memory_layout: None, + documentation: true, + keywords: true, + format: crate::HoverDocFormat::Markdown, + max_trait_assoc_items_count: None, + max_fields_count: Some(5), + max_enum_variants_count: Some(5), + max_subst_ty_len: SubstTyLen::Unlimited, + show_drop_glue: true, + ra_fixture: RaFixtureConfig::default(), + }; + let mut result = StaticIndexedFile { file_id, folds, tokens: vec![] }; + + let mut add_token = |def: Definition<'a>, range: TextRange, scope_node: &SyntaxNode| { + let id = if let Some(it) = def_map.get(&def) { + *it + } else { + let nav = def.try_to_nav(&sema).map(UpmappingResult::call_site); + let it = token_store.insert(TokenStaticData { + documentation: documentation_for_definition(&sema, def, scope_node), + hover: Some(hover_for_definition( + &sema, + file_id, + def, + None, + scope_node, + None, + false, + &hover_config, + edition, + display_target, + )), + definition: nav + .as_ref() + .map(|it| FileRange { file_id: it.file_id, range: it.focus_or_full_range() }), + definition_body: nav.as_ref().map(|it| FileRange { + file_id: it.file_id, + range: definition_range_excluding_trivia(&sema, it.file_id, it.full_range), + }), + references: vec![], + moniker: current_crate.and_then(|cc| def_to_moniker(db, def, cc)), + display_name: def.name(db).map(|name| name.display(db, edition).to_string()), + signature: Some(def.label(db, display_target)), + kind: def_to_kind(db, def), }); - result.tokens.push((range, id)); + def_map.insert(def, it); + it }; + let token = token_store.get_mut(id).unwrap(); + token.references.push(ReferenceData { + range: FileRange { range, file_id }, + is_definition: match def.try_to_nav(&sema).map(UpmappingResult::call_site) { + Some(it) => it.file_id == file_id && it.focus_or_full_range() == range, + None => false, + }, + }); + result.tokens.push((range, id)); + }; - if let Some(module) = sema.file_to_module_def(file_id) { - let def = Definition::Module(module); - let range = root.text_range(); - add_token(def, range, &root); - } + if let Some(module) = sema.file_to_module_def(file_id) { + let def = Definition::Module(module); + let range = root.text_range(); + add_token(def, range, &root); + } - for token in tokens { - let range = token.text_range(); - let node = token.parent().unwrap(); - match hir::attach_db(self.db, || get_definitions(&sema, token.clone())) { - Some(defs) => { - for (def, _) in defs { - add_token(def, range, &node); - } + for token in tokens { + let range = token.text_range(); + let node = token.parent().unwrap(); + match hir::attach_db(db, || get_definitions(&sema, token.clone())) { + Some(defs) => { + for (def, _) in defs { + add_token(def, range, &node); } - None => continue, - }; - } - self.files.push(result); + } + None => continue, + }; } - pub fn compute( - analysis: &'a Analysis, - vendored_libs_config: VendoredLibrariesConfig<'_>, - ) -> StaticIndex<'a> { + Some(result) +} + +impl StaticIndex { + pub fn compute(analysis: &Analysis, vendored_libs_config: VendoredLibrariesConfig<'_>) -> Self { let db = &analysis.db; let files_to_index = { @@ -300,18 +296,14 @@ impl<'a> StaticIndex<'a> { files_to_index }; + let mut tokens = Default::default(); + let mut def_map = Default::default(); hir::attach_db(db, || { - let mut this = StaticIndex { - files: vec![], - tokens: Default::default(), - analysis, - db, - def_map: Default::default(), - }; - for file_id in files_to_index { - this.add_file(file_id); - } - this + let files = files_to_index + .into_iter() + .flat_map(|file_id| index_file(analysis, &mut tokens, &mut def_map, file_id)) + .collect(); + StaticIndex { files, tokens } }) } } From 0fecf8727eee93b18e5778e509c391196a21c20b Mon Sep 17 00:00:00 2001 From: Nicolas Guichard Date: Wed, 22 Jul 2026 11:21:44 +0200 Subject: [PATCH 4/8] Use the entry API to lookup-or-insert in def_map --- crates/ide/src/static_index.rs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/crates/ide/src/static_index.rs b/crates/ide/src/static_index.rs index f79e43daf1bf..d8ae5f7a8597 100644 --- a/crates/ide/src/static_index.rs +++ b/crates/ide/src/static_index.rs @@ -196,11 +196,9 @@ fn index_file<'a>( let mut result = StaticIndexedFile { file_id, folds, tokens: vec![] }; let mut add_token = |def: Definition<'a>, range: TextRange, scope_node: &SyntaxNode| { - let id = if let Some(it) = def_map.get(&def) { - *it - } else { + let id = *def_map.entry(def).or_insert_with(|| { let nav = def.try_to_nav(&sema).map(UpmappingResult::call_site); - let it = token_store.insert(TokenStaticData { + token_store.insert(TokenStaticData { documentation: documentation_for_definition(&sema, def, scope_node), hover: Some(hover_for_definition( &sema, @@ -226,10 +224,8 @@ fn index_file<'a>( display_name: def.name(db).map(|name| name.display(db, edition).to_string()), signature: Some(def.label(db, display_target)), kind: def_to_kind(db, def), - }); - def_map.insert(def, it); - it - }; + }) + }); let token = token_store.get_mut(id).unwrap(); token.references.push(ReferenceData { range: FileRange { range, file_id }, From a158bca5910e8ea2a90b75b0be350972cea148b4 Mon Sep 17 00:00:00 2001 From: Nicolas Guichard Date: Tue, 21 Jul 2026 11:31:57 +0200 Subject: [PATCH 5/8] Run StaticIndex::compute over multiple threads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thanks to the previous commits, everything is in place to start multiple threads in StaticIndex::compute. Each thread takes care of the files whose index in the precomputed list is congruent to the thread index modulo the thread count and generates its StaticIndexedFile. The Definition→TokenId and TokenId→TokenStaticData maps are shared across all threads and are now DashMaps. TokenStore now uses an AtomicUsize to generate new TokenIds. --- Cargo.lock | 1 + crates/ide/Cargo.toml | 1 + crates/ide/src/static_index.rs | 90 ++++++++++++++++++++-------- crates/rust-analyzer/src/cli/lsif.rs | 19 +++++- crates/rust-analyzer/src/cli/scip.rs | 24 +++++--- 5 files changed, 101 insertions(+), 34 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 19bdd0c7635a..27441b53641b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1043,6 +1043,7 @@ dependencies = [ "arrayvec", "cfg", "cov-mark", + "dashmap", "dot", "either", "expect-test", diff --git a/crates/ide/Cargo.toml b/crates/ide/Cargo.toml index 08ffd391c02d..6a2f5fd2c2f6 100644 --- a/crates/ide/Cargo.toml +++ b/crates/ide/Cargo.toml @@ -15,6 +15,7 @@ doctest = false [dependencies] cov-mark = "2.0.0" arrayvec.workspace = true +dashmap.workspace = true either.workspace = true itertools.workspace = true tracing.workspace = true diff --git a/crates/ide/src/static_index.rs b/crates/ide/src/static_index.rs index d8ae5f7a8597..41bae8ee0b3f 100644 --- a/crates/ide/src/static_index.rs +++ b/crates/ide/src/static_index.rs @@ -1,11 +1,17 @@ //! This module provides `StaticIndex` which is used for powering //! read-only code browsers and emitting LSIF +use std::sync::atomic::{AtomicUsize, Ordering}; + use arrayvec::ArrayVec; +use dashmap::{ + DashMap, + mapref::one::{Ref, RefMut}, +}; use either::Either; use hir::{Crate, Module, Semantics, db::HirDatabase}; use ide_db::{ - FileId, FileRange, FxHashMap, RootDatabase, + FileId, FileRange, RootDatabase, base_db::{SourceDatabase, VfsPath}, defs::{Definition, IdentClass}, documentation::Documentation, @@ -84,25 +90,28 @@ impl TokenId { } #[derive(Default, Debug)] -pub struct TokenStore(Vec); +pub struct TokenStore { + data: DashMap, + last_index: AtomicUsize, +} impl TokenStore { - pub fn insert(&mut self, data: TokenStaticData) -> TokenId { - let id = TokenId(self.0.len()); - self.0.push(data); + pub fn insert(&self, data: TokenStaticData) -> TokenId { + let id = TokenId(self.last_index.fetch_add(1, Ordering::Relaxed)); + self.data.insert(id, data); id } - pub fn get_mut(&mut self, id: TokenId) -> Option<&mut TokenStaticData> { - self.0.get_mut(id.0) + pub fn get_mut(&self, id: TokenId) -> Option> { + self.data.get_mut(&id) } - pub fn get(&self, id: TokenId) -> Option<&TokenStaticData> { - self.0.get(id.0) + pub fn get(&self, id: TokenId) -> Option> { + self.data.get(&id) } pub fn iter(self) -> impl Iterator { - self.0.into_iter().enumerate().map(|(id, data)| (TokenId(id), data)) + self.data.into_iter() } } @@ -163,8 +172,8 @@ pub enum VendoredLibrariesConfig<'a> { fn index_file<'a>( analysis: &'a Analysis, - token_store: &mut TokenStore, - def_map: &mut FxHashMap, TokenId>, + token_store: &TokenStore, + def_map: &DashMap, TokenId>, file_id: FileId, ) -> Option { let db = &analysis.db; @@ -226,7 +235,7 @@ fn index_file<'a>( kind: def_to_kind(db, def), }) }); - let token = token_store.get_mut(id).unwrap(); + let mut token = token_store.get_mut(id).unwrap(); token.references.push(ReferenceData { range: FileRange { range, file_id }, is_definition: match def.try_to_nav(&sema).map(UpmappingResult::call_site) { @@ -260,7 +269,11 @@ fn index_file<'a>( } impl StaticIndex { - pub fn compute(analysis: &Analysis, vendored_libs_config: VendoredLibrariesConfig<'_>) -> Self { + pub fn compute( + analysis: &Analysis, + vendored_libs_config: VendoredLibrariesConfig<'_>, + num_threads: usize, + ) -> Self { let db = &analysis.db; let files_to_index = { @@ -292,15 +305,42 @@ impl StaticIndex { files_to_index }; - let mut tokens = Default::default(); - let mut def_map = Default::default(); - hir::attach_db(db, || { - let files = files_to_index - .into_iter() - .flat_map(|file_id| index_file(analysis, &mut tokens, &mut def_map, file_id)) + // Because Analysis is not Sync, each thread needs to get exclusive access to its own Analysis clone. + // The Definitions which are used as keys in def_map inherit the lifetime of these Analysis clones, so analyses should live longer than def_map. + // NOTE this assumes that Definitions yielded from multiple untouched Analysis clones still compare equal. + let mut analyses: Vec<_> = (0..num_threads).map(|_| analysis.clone()).collect(); + + let def_map = Default::default(); + let tokens = Default::default(); + let files = std::thread::scope(|s| { + let threads: Vec<_> = analyses + .iter_mut() + .enumerate() + .map(|(thread_index, analysis)| { + let files_to_index = files_to_index + .iter() + .copied() + .enumerate() + .filter(move |(index, _)| index % num_threads == thread_index) + .map(|(_, file_id)| file_id); + + s.spawn(|| { + let analysis = analysis; + hir::attach_db(&analysis.db, || { + files_to_index + .into_iter() + .flat_map(|file_id| { + index_file(analysis, &tokens, &def_map, file_id) + }) + .collect::>() + }) + }) + }) .collect(); - StaticIndex { files, tokens } - }) + threads.into_iter().flat_map(|join_handle| join_handle.join().unwrap()).collect() + }); + + StaticIndex { files, tokens } } } @@ -370,7 +410,7 @@ mod tests { vendored_libs_config: VendoredLibrariesConfig<'_>, ) { let (analysis, ranges) = fixture::annotations_without_marker(ra_fixture); - let s = StaticIndex::compute(&analysis, vendored_libs_config); + let s = StaticIndex::compute(&analysis, vendored_libs_config, 1); let mut range_set: FxHashSet<_> = ranges.iter().map(|it| it.0).collect(); for f in s.files { for (range, _) in f.tokens { @@ -396,7 +436,7 @@ mod tests { vendored_libs_config: VendoredLibrariesConfig<'_>, ) { let (analysis, ranges) = fixture::annotations_without_marker(ra_fixture); - let s = StaticIndex::compute(&analysis, vendored_libs_config); + let s = StaticIndex::compute(&analysis, vendored_libs_config, 1); let mut range_set: FxHashSet<_> = ranges.iter().map(|it| it.0).collect(); for (_, t) in s.tokens.iter() { if let Some(t) = t.definition { @@ -421,7 +461,7 @@ mod tests { vendored_libs_config: VendoredLibrariesConfig<'_>, ) { let (analysis, ranges) = fixture::annotations_without_marker(ra_fixture); - let s = StaticIndex::compute(&analysis, vendored_libs_config); + let s = StaticIndex::compute(&analysis, vendored_libs_config, 1); let mut range_set: FxHashMap<_, i32> = ranges.iter().map(|it| (it.0, 0)).collect(); // Make sure that all references have at least one range. We use a HashMap instead of a diff --git a/crates/rust-analyzer/src/cli/lsif.rs b/crates/rust-analyzer/src/cli/lsif.rs index 8a1d5f336dd7..5f0ca94bf481 100644 --- a/crates/rust-analyzer/src/cli/lsif.rs +++ b/crates/rust-analyzer/src/cli/lsif.rs @@ -317,7 +317,7 @@ impl flags::Lsif { VendoredLibrariesConfig::Included { workspace_root: &path.clone().into() } }; - let si = StaticIndex::compute(&analysis, vendored_libs_config); + let si = StaticIndex::compute(&analysis, vendored_libs_config, 1); let mut lsif = LsifManager::new(&analysis, db, &vfs, out); lsif.add_vertex(lsif::Vertex::MetaData(lsif::MetaData { @@ -333,7 +333,22 @@ impl flags::Lsif { for file in si.files { lsif.add_file(file); } - for (id, token) in si.tokens.iter() { + + // The output order depends on the iteration order, make that order somewhat stable to avoid making lsif_contains_generated_constant flaky + let tokens = { + let mut tokens: Vec<_> = si.tokens.iter().collect(); + tokens.sort_by_key(|(_, token)| { + ( + token.references[0].range.file_id, + token.references[0].range.range.start(), + token.references[0].range.range.end(), + token.kind, + ) + }); + tokens + }; + + for (id, token) in tokens { lsif.add_token(id, token); } eprintln!("Generating LSIF finished in {:?}", now.elapsed()); diff --git a/crates/rust-analyzer/src/cli/scip.rs b/crates/rust-analyzer/src/cli/scip.rs index 3d904e7148e2..bbd543b2ab77 100644 --- a/crates/rust-analyzer/src/cli/scip.rs +++ b/crates/rust-analyzer/src/cli/scip.rs @@ -22,7 +22,8 @@ use crate::{ impl flags::Scip { pub fn run(self) -> anyhow::Result<()> { - eprintln!("Generating SCIP start..."); + let num_threads = self.num_threads.unwrap_or_else(num_cpus::get_physical); + eprintln!("Generating SCIP start with {num_threads} threads..."); let now = Instant::now(); let no_progress = &|s| eprintln!("rust-analyzer: Loading {s}"); @@ -52,7 +53,7 @@ impl flags::Scip { load_out_dirs_from_check: true, with_proc_macro_server: ProcMacroServerChoice::Sysroot, prefill_caches: true, - num_worker_threads: self.num_threads.unwrap_or_else(num_cpus::get_physical), + num_worker_threads: num_threads, proc_macro_processes: config.proc_macro_num_processes(), }; let cargo_config = config.cargo(None); @@ -72,7 +73,7 @@ impl flags::Scip { VendoredLibrariesConfig::Included { workspace_root: &root.clone().into() } }; - let si = StaticIndex::compute(&analysis, vendored_libs_config); + let si = StaticIndex::compute(&analysis, vendored_libs_config, num_threads); let metadata = scip_types::Metadata { version: scip_types::ProtocolVersion::UnspecifiedProtocolVersion.into(), @@ -145,7 +146,7 @@ impl flags::Scip { let token = si.tokens.get(id).unwrap(); let Some(TokenSymbols { symbol, enclosing_symbol, is_inherent_impl }) = - symbol_generator.token_symbols(id, token) + symbol_generator.token_symbols(id, token.value()) else { // token did not have a moniker, so there is no reasonable occurrence to emit // see ide::moniker::def_to_moniker @@ -171,7 +172,7 @@ impl flags::Scip { symbols.push(compute_symbol_info( symbol.clone(), enclosing_symbol, - token, + token.value(), )); } } @@ -253,7 +254,7 @@ impl flags::Scip { } let TokenSymbols { symbol, enclosing_symbol, .. } = symbol_generator - .token_symbols(id, token) + .token_symbols(id, token.value()) .expect("To have been referenced, the symbol must be in the cache."); record_error_if_symbol_already_used( @@ -263,7 +264,11 @@ impl flags::Scip { &line_index, text_range, ); - external_symbols.push(compute_symbol_info(symbol.clone(), enclosing_symbol, token)); + external_symbols.push(compute_symbol_info( + symbol.clone(), + enclosing_symbol, + token.value(), + )); } let index = scip_types::Index { @@ -544,6 +549,7 @@ mod test { VendoredLibrariesConfig::Included { workspace_root: &VfsPath::new_virtual_path("/workspace".to_owned()), }, + 1, ); let FilePosition { file_id, offset } = position; @@ -912,6 +918,7 @@ pub mod example_mod { VendoredLibrariesConfig::Included { workspace_root: &VfsPath::new_virtual_path("/workspace".to_owned()), }, + 1, ); let file = si.files.first().unwrap(); @@ -935,6 +942,7 @@ pub mod example_mod { VendoredLibrariesConfig::Included { workspace_root: &VfsPath::new_virtual_path("/workspace".to_owned()), }, + 1, ); let file = si.files.first().unwrap(); @@ -963,6 +971,7 @@ pub mod example_mod { VendoredLibrariesConfig::Included { workspace_root: &VfsPath::new_virtual_path("/workspace".to_owned()), }, + 1, ); let file = si.files.first().unwrap(); @@ -995,6 +1004,7 @@ pub mod example_mod { VendoredLibrariesConfig::Included { workspace_root: &VfsPath::new_virtual_path("/workspace".to_owned()), }, + 1, ); let file = si.files.first().unwrap(); From 74387df2de0803aaa29c49f908817fb0e99940bb Mon Sep 17 00:00:00 2001 From: Nicolas Guichard Date: Thu, 23 Jul 2026 16:29:22 +0200 Subject: [PATCH 6/8] scip: don't compute folds and hover information These fields are only used by the LSIF indexer, and are quite costly to compute when SCIP just throws them away. --- crates/ide/src/static_index.rs | 30 +++++++++++++++++++--------- crates/rust-analyzer/src/cli/lsif.rs | 2 +- crates/rust-analyzer/src/cli/scip.rs | 12 ++++++++++- 3 files changed, 33 insertions(+), 11 deletions(-) diff --git a/crates/ide/src/static_index.rs b/crates/ide/src/static_index.rs index 41bae8ee0b3f..867ffd5cc83d 100644 --- a/crates/ide/src/static_index.rs +++ b/crates/ide/src/static_index.rs @@ -175,11 +175,13 @@ fn index_file<'a>( token_store: &TokenStore, def_map: &DashMap, TokenId>, file_id: FileId, + with_folds: bool, + with_hover: bool, ) -> Option { let db = &analysis.db; let current_crate = crates_for(db, file_id).pop().map(Into::into); - let folds = analysis.folding_ranges(file_id, true).unwrap(); + let folds = if with_folds { analysis.folding_ranges(file_id, true).unwrap() } else { vec![] }; // hovers let sema = hir::Semantics::new(db); let root = sema.parse_guess_edition(file_id).syntax().clone(); @@ -207,9 +209,8 @@ fn index_file<'a>( let mut add_token = |def: Definition<'a>, range: TextRange, scope_node: &SyntaxNode| { let id = *def_map.entry(def).or_insert_with(|| { let nav = def.try_to_nav(&sema).map(UpmappingResult::call_site); - token_store.insert(TokenStaticData { - documentation: documentation_for_definition(&sema, def, scope_node), - hover: Some(hover_for_definition( + let hover = if with_hover { + Some(hover_for_definition( &sema, file_id, def, @@ -220,7 +221,13 @@ fn index_file<'a>( &hover_config, edition, display_target, - )), + )) + } else { + None + }; + token_store.insert(TokenStaticData { + documentation: documentation_for_definition(&sema, def, scope_node), + hover, definition: nav .as_ref() .map(|it| FileRange { file_id: it.file_id, range: it.focus_or_full_range() }), @@ -273,6 +280,8 @@ impl StaticIndex { analysis: &Analysis, vendored_libs_config: VendoredLibrariesConfig<'_>, num_threads: usize, + with_folds: bool, + with_hover: bool, ) -> Self { let db = &analysis.db; @@ -330,7 +339,10 @@ impl StaticIndex { files_to_index .into_iter() .flat_map(|file_id| { - index_file(analysis, &tokens, &def_map, file_id) + index_file( + analysis, &tokens, &def_map, file_id, with_folds, + with_hover, + ) }) .collect::>() }) @@ -410,7 +422,7 @@ mod tests { vendored_libs_config: VendoredLibrariesConfig<'_>, ) { let (analysis, ranges) = fixture::annotations_without_marker(ra_fixture); - let s = StaticIndex::compute(&analysis, vendored_libs_config, 1); + let s = StaticIndex::compute(&analysis, vendored_libs_config, 1, false, false); let mut range_set: FxHashSet<_> = ranges.iter().map(|it| it.0).collect(); for f in s.files { for (range, _) in f.tokens { @@ -436,7 +448,7 @@ mod tests { vendored_libs_config: VendoredLibrariesConfig<'_>, ) { let (analysis, ranges) = fixture::annotations_without_marker(ra_fixture); - let s = StaticIndex::compute(&analysis, vendored_libs_config, 1); + let s = StaticIndex::compute(&analysis, vendored_libs_config, 1, false, false); let mut range_set: FxHashSet<_> = ranges.iter().map(|it| it.0).collect(); for (_, t) in s.tokens.iter() { if let Some(t) = t.definition { @@ -461,7 +473,7 @@ mod tests { vendored_libs_config: VendoredLibrariesConfig<'_>, ) { let (analysis, ranges) = fixture::annotations_without_marker(ra_fixture); - let s = StaticIndex::compute(&analysis, vendored_libs_config, 1); + let s = StaticIndex::compute(&analysis, vendored_libs_config, 1, false, false); let mut range_set: FxHashMap<_, i32> = ranges.iter().map(|it| (it.0, 0)).collect(); // Make sure that all references have at least one range. We use a HashMap instead of a diff --git a/crates/rust-analyzer/src/cli/lsif.rs b/crates/rust-analyzer/src/cli/lsif.rs index 5f0ca94bf481..bde1fc376a94 100644 --- a/crates/rust-analyzer/src/cli/lsif.rs +++ b/crates/rust-analyzer/src/cli/lsif.rs @@ -317,7 +317,7 @@ impl flags::Lsif { VendoredLibrariesConfig::Included { workspace_root: &path.clone().into() } }; - let si = StaticIndex::compute(&analysis, vendored_libs_config, 1); + let si = StaticIndex::compute(&analysis, vendored_libs_config, 1, true, true); let mut lsif = LsifManager::new(&analysis, db, &vfs, out); lsif.add_vertex(lsif::Vertex::MetaData(lsif::MetaData { diff --git a/crates/rust-analyzer/src/cli/scip.rs b/crates/rust-analyzer/src/cli/scip.rs index bbd543b2ab77..d87becad615c 100644 --- a/crates/rust-analyzer/src/cli/scip.rs +++ b/crates/rust-analyzer/src/cli/scip.rs @@ -73,7 +73,7 @@ impl flags::Scip { VendoredLibrariesConfig::Included { workspace_root: &root.clone().into() } }; - let si = StaticIndex::compute(&analysis, vendored_libs_config, num_threads); + let si = StaticIndex::compute(&analysis, vendored_libs_config, num_threads, false, false); let metadata = scip_types::Metadata { version: scip_types::ProtocolVersion::UnspecifiedProtocolVersion.into(), @@ -550,6 +550,8 @@ mod test { workspace_root: &VfsPath::new_virtual_path("/workspace".to_owned()), }, 1, + false, + false, ); let FilePosition { file_id, offset } = position; @@ -919,6 +921,8 @@ pub mod example_mod { workspace_root: &VfsPath::new_virtual_path("/workspace".to_owned()), }, 1, + false, + false, ); let file = si.files.first().unwrap(); @@ -943,6 +947,8 @@ pub mod example_mod { workspace_root: &VfsPath::new_virtual_path("/workspace".to_owned()), }, 1, + false, + false, ); let file = si.files.first().unwrap(); @@ -972,6 +978,8 @@ pub mod example_mod { workspace_root: &VfsPath::new_virtual_path("/workspace".to_owned()), }, 1, + false, + false, ); let file = si.files.first().unwrap(); @@ -1005,6 +1013,8 @@ pub mod example_mod { workspace_root: &VfsPath::new_virtual_path("/workspace".to_owned()), }, 1, + false, + false, ); let file = si.files.first().unwrap(); From 9636652eca5fd5c2c002d9014ccf9b0f77d0df94 Mon Sep 17 00:00:00 2001 From: Nicolas Guichard Date: Thu, 23 Jul 2026 21:21:38 +0200 Subject: [PATCH 7/8] scip test: support multiple symbols being located at the same position In the next commit, we add support for include! macros, which can lead to multiple symbols being defined or used at the same position. --- crates/rust-analyzer/src/cli/scip.rs | 135 +++++++++++++-------------- 1 file changed, 63 insertions(+), 72 deletions(-) diff --git a/crates/rust-analyzer/src/cli/scip.rs b/crates/rust-analyzer/src/cli/scip.rs index d87becad615c..97db5aebf4d1 100644 --- a/crates/rust-analyzer/src/cli/scip.rs +++ b/crates/rust-analyzer/src/cli/scip.rs @@ -540,7 +540,7 @@ mod test { /// If expected == "", then assert that there are no symbols (this is basically local symbol) #[track_caller] - fn check_symbol(#[rust_analyzer::rust_fixture] ra_fixture: &str, expected: &str) { + fn check_symbols(#[rust_analyzer::rust_fixture] ra_fixture: &str, expected: &[&str]) { let (host, position) = position(ra_fixture); let analysis = host.analysis(); @@ -556,48 +556,39 @@ mod test { let FilePosition { file_id, offset } = position; - let mut found_symbol = None; - for file in &si.files { - if file.file_id != file_id { - continue; - } - for &(range, id) in &file.tokens { - // check if cursor is within token, ignoring token for the module defined by the file (whose range is the whole file) - if range.start() != TextSize::from(0) && range.contains(offset - TextSize::from(1)) - { - let token = si.tokens.get(id).unwrap(); - found_symbol = match token.moniker.as_ref() { - None => None, - Some(MonikerResult::Moniker(moniker)) => { - Some(scip::symbol::format_symbol(moniker_to_symbol(moniker))) - } - Some(MonikerResult::Local { enclosing_moniker: Some(moniker) }) => { - Some(format!( - "local enclosed by {}", - scip::symbol::format_symbol(moniker_to_symbol(moniker)) - )) - } - Some(MonikerResult::Local { enclosing_moniker: None }) => { - Some("unenclosed local".to_owned()) - } - }; - break; - } - } - } + let tokens_at_position = si + .files + .into_iter() + .filter(|file| file.file_id == file_id) + .flat_map(|file| file.tokens) + .filter(|(range, _token_id)| { + range.start() != TextSize::from(0) && range.contains(offset - TextSize::from(1)) + }) + .map(|(_range, token_id)| si.tokens.get(token_id).unwrap()); - if expected.is_empty() { - assert!(found_symbol.is_none(), "must have no symbols {found_symbol:?}"); - return; - } + let mut symbols_at_position: Vec<_> = tokens_at_position + .filter_map(|token| match token.moniker.as_ref() { + None => None, + Some(MonikerResult::Moniker(moniker)) => { + Some(scip::symbol::format_symbol(moniker_to_symbol(moniker))) + } + Some(MonikerResult::Local { enclosing_moniker: Some(moniker) }) => Some(format!( + "local enclosed by {}", + scip::symbol::format_symbol(moniker_to_symbol(moniker)) + )), + Some(MonikerResult::Local { enclosing_moniker: None }) => { + Some("unenclosed local".to_owned()) + } + }) + .collect(); - assert!(found_symbol.is_some(), "must have one symbol {found_symbol:?}"); - assert_eq!(found_symbol.unwrap(), expected); + symbols_at_position.sort_unstable(); + assert_eq!(symbols_at_position, expected); } #[test] fn basic() { - check_symbol( + check_symbols( r#" //- /workspace/lib.rs crate:main deps:foo use foo::example_mod::func; @@ -609,13 +600,13 @@ pub mod example_mod { pub fn func() {} } "#, - "rust-analyzer cargo foo 0.1.0 example_mod/func().", + &["rust-analyzer cargo foo 0.1.0 example_mod/func()."], ); } #[test] fn operator_overload() { - check_symbol( + check_symbols( r#" //- minicore: add //- /workspace/lib.rs crate:main @@ -632,13 +623,13 @@ fn main() { s +=$0 S; } "#, - "rust-analyzer cargo main . impl#[S][`AddAssign`]add_assign().", + &["rust-analyzer cargo main . impl#[S][`AddAssign`]add_assign()."], ); } #[test] fn symbol_for_trait() { - check_symbol( + check_symbols( r#" //- /foo/lib.rs crate:foo@0.1.0,https://a.b/foo.git library pub mod module { @@ -647,13 +638,13 @@ pub mod module { } } "#, - "rust-analyzer cargo foo 0.1.0 module/MyTrait#func().", + &["rust-analyzer cargo foo 0.1.0 module/MyTrait#func()."], ); } #[test] fn symbol_for_trait_alias() { - check_symbol( + check_symbols( r#" //- /foo/lib.rs crate:foo@0.1.0,https://a.b/foo.git library #![feature(trait_alias)] @@ -662,13 +653,13 @@ pub mod module { pub trait MyTraitAlias$0 = MyTrait; } "#, - "rust-analyzer cargo foo 0.1.0 module/MyTraitAlias#", + &["rust-analyzer cargo foo 0.1.0 module/MyTraitAlias#"], ); } #[test] fn symbol_for_trait_constant() { - check_symbol( + check_symbols( r#" //- /foo/lib.rs crate:foo@0.1.0,https://a.b/foo.git library pub mod module { @@ -677,13 +668,13 @@ pub mod module { } } "#, - "rust-analyzer cargo foo 0.1.0 module/MyTrait#MY_CONST.", + &["rust-analyzer cargo foo 0.1.0 module/MyTrait#MY_CONST."], ); } #[test] fn symbol_for_trait_type() { - check_symbol( + check_symbols( r#" //- /foo/lib.rs crate:foo@0.1.0,https://a.b/foo.git library pub mod module { @@ -692,13 +683,13 @@ pub mod module { } } "#, - "rust-analyzer cargo foo 0.1.0 module/MyTrait#MyType#", + &["rust-analyzer cargo foo 0.1.0 module/MyTrait#MyType#"], ); } #[test] fn symbol_for_trait_impl_function() { - check_symbol( + check_symbols( r#" //- /foo/lib.rs crate:foo@0.1.0,https://a.b/foo.git library pub mod module { @@ -713,13 +704,13 @@ pub mod module { } } "#, - "rust-analyzer cargo foo 0.1.0 module/impl#[MyStruct][MyTrait]func().", + &["rust-analyzer cargo foo 0.1.0 module/impl#[MyStruct][MyTrait]func()."], ); } #[test] fn symbol_for_field() { - check_symbol( + check_symbols( r#" //- /workspace/lib.rs crate:main deps:foo use foo::St; @@ -731,13 +722,13 @@ pub mod module { pub a: i32, } "#, - "rust-analyzer cargo foo 0.1.0 St#a.", + &["rust-analyzer cargo foo 0.1.0 St#a."], ); } #[test] fn symbol_for_param() { - check_symbol( + check_symbols( r#" //- /workspace/lib.rs crate:main deps:foo use foo::example_mod::func; @@ -749,13 +740,13 @@ pub mod example_mod { pub fn func(x$0: usize) {} } "#, - "local enclosed by rust-analyzer cargo foo 0.1.0 example_mod/func().", + &["local enclosed by rust-analyzer cargo foo 0.1.0 example_mod/func()."], ); } #[test] fn symbol_for_closure_param() { - check_symbol( + check_symbols( r#" //- /workspace/lib.rs crate:main deps:foo use foo::example_mod::func; @@ -769,13 +760,13 @@ pub mod example_mod { } } "#, - "local enclosed by rust-analyzer cargo foo 0.1.0 example_mod/func().", + &["local enclosed by rust-analyzer cargo foo 0.1.0 example_mod/func()."], ); } #[test] fn local_symbol_for_local() { - check_symbol( + check_symbols( r#" //- /workspace/lib.rs crate:main deps:foo use foo::module::func; @@ -789,13 +780,13 @@ pub mod example_mod { } } "#, - "local enclosed by rust-analyzer cargo foo 0.1.0 module/func().", + &["local enclosed by rust-analyzer cargo foo 0.1.0 module/func()."], ); } #[test] fn global_symbol_for_pub_struct() { - check_symbol( + check_symbols( r#" //- /workspace/lib.rs crate:main mod foo; @@ -808,13 +799,13 @@ pub mod example_mod { pub i: i32, } "#, - "rust-analyzer cargo main . foo/Bar#", + &["rust-analyzer cargo main . foo/Bar#"], ); } #[test] fn global_symbol_for_pub_struct_reference() { - check_symbol( + check_symbols( r#" //- /workspace/lib.rs crate:main mod foo; @@ -827,32 +818,32 @@ pub mod example_mod { pub i: i32, } "#, - "rust-analyzer cargo main . foo/Bar#", + &["rust-analyzer cargo main . foo/Bar#"], ); } #[test] fn symbol_for_type_alias() { - check_symbol( + check_symbols( r#" //- /workspace/lib.rs crate:main pub type MyTypeAlias$0 = u8; "#, - "rust-analyzer cargo main . MyTypeAlias#", + &["rust-analyzer cargo main . MyTypeAlias#"], ); } // FIXME: This test represents current misbehavior. #[test] fn symbol_for_nested_function() { - check_symbol( + check_symbols( r#" //- /workspace/lib.rs crate:main pub fn func() { pub fn inner_func$0() {} } "#, - "rust-analyzer cargo main . inner_func().", + &["rust-analyzer cargo main . inner_func()."], // FIXME: This should be a local: // "local enclosed by rust-analyzer cargo main . func().", ); @@ -861,14 +852,14 @@ pub mod example_mod { // FIXME: This test represents current misbehavior. #[test] fn symbol_for_struct_in_function() { - check_symbol( + check_symbols( r#" //- /workspace/lib.rs crate:main pub fn func() { struct SomeStruct$0 {} } "#, - "rust-analyzer cargo main . SomeStruct#", + &["rust-analyzer cargo main . SomeStruct#"], // FIXME: This should be a local: // "local enclosed by rust-analyzer cargo main . func().", ); @@ -877,14 +868,14 @@ pub mod example_mod { // FIXME: This test represents current misbehavior. #[test] fn symbol_for_const_in_function() { - check_symbol( + check_symbols( r#" //- /workspace/lib.rs crate:main pub fn func() { const SOME_CONST$0: u32 = 1; } "#, - "rust-analyzer cargo main . SOME_CONST.", + &["rust-analyzer cargo main . SOME_CONST."], // FIXME: This should be a local: // "local enclosed by rust-analyzer cargo main . func().", ); @@ -893,14 +884,14 @@ pub mod example_mod { // FIXME: This test represents current misbehavior. #[test] fn symbol_for_static_in_function() { - check_symbol( + check_symbols( r#" //- /workspace/lib.rs crate:main pub fn func() { static SOME_STATIC$0: u32 = 1; } "#, - "rust-analyzer cargo main . SOME_STATIC.", + &["rust-analyzer cargo main . SOME_STATIC."], // FIXME: This should be a local: // "local enclosed by rust-analyzer cargo main . func().", ); From 91b497111c5dda35b5129f41247c3e36fdb4becf Mon Sep 17 00:00:00 2001 From: Nicolas Guichard Date: Thu, 23 Jul 2026 12:34:11 +0200 Subject: [PATCH 8/8] StaticIndex: process tokens from include! expansions This updates index_file to traverse macro expansions, but only actually records tokens from include! expansions. The reason for not recording tokens from all expansions is twofold: - I didn't find a way to reliably get the tokens' spelling location, original_node_file_range_rooted returns the macro call location. - That would register a huge amount of tokens at macro call sites. --- crates/ide/src/static_index.rs | 204 ++++++++++++++++++--------- crates/rust-analyzer/src/cli/scip.rs | 61 ++++++++ 2 files changed, 196 insertions(+), 69 deletions(-) diff --git a/crates/ide/src/static_index.rs b/crates/ide/src/static_index.rs index 867ffd5cc83d..fada0e18b5f3 100644 --- a/crates/ide/src/static_index.rs +++ b/crates/ide/src/static_index.rs @@ -1,7 +1,10 @@ //! This module provides `StaticIndex` which is used for powering //! read-only code browsers and emitting LSIF -use std::sync::atomic::{AtomicUsize, Ordering}; +use std::{ + collections::VecDeque, + sync::atomic::{AtomicUsize, Ordering}, +}; use arrayvec::ArrayVec; use dashmap::{ @@ -9,9 +12,9 @@ use dashmap::{ mapref::one::{Ref, RefMut}, }; use either::Either; -use hir::{Crate, Module, Semantics, db::HirDatabase}; +use hir::{Crate, InFile, Module, Semantics, db::HirDatabase}; use ide_db::{ - FileId, FileRange, RootDatabase, + FileId, FileRange, FxHashMap, RootDatabase, base_db::{SourceDatabase, VfsPath}, defs::{Definition, IdentClass}, documentation::Documentation, @@ -170,6 +173,8 @@ pub enum VendoredLibrariesConfig<'a> { Excluded, } +/// Computes the index for a single file identified by file_id. +/// Note this doesn't return a single StaticIndexedFile but multiple because this also indexes include! expansions. fn index_file<'a>( analysis: &'a Analysis, token_store: &TokenStore, @@ -177,7 +182,7 @@ fn index_file<'a>( file_id: FileId, with_folds: bool, with_hover: bool, -) -> Option { +) -> FxHashMap { let db = &analysis.db; let current_crate = crates_for(db, file_id).pop().map(Into::into); @@ -186,11 +191,10 @@ fn index_file<'a>( let sema = hir::Semantics::new(db); let root = sema.parse_guess_edition(file_id).syntax().clone(); let edition = sema.attach_first_edition(file_id).edition(sema.db); - let display_target = sema.first_crate(file_id)?.to_display_target(sema.db); - let tokens = root.descendants_with_tokens().filter_map(|it| match it { - syntax::NodeOrToken::Node(_) => None, - syntax::NodeOrToken::Token(it) => Some(it), - }); + let Some(krate) = sema.first_crate(file_id) else { + return Default::default(); + }; + let display_target = krate.to_display_target(sema.db); let hover_config = HoverConfig { links_in_hover: true, memory_layout: None, @@ -204,75 +208,123 @@ fn index_file<'a>( show_drop_glue: true, ra_fixture: RaFixtureConfig::default(), }; - let mut result = StaticIndexedFile { file_id, folds, tokens: vec![] }; - - let mut add_token = |def: Definition<'a>, range: TextRange, scope_node: &SyntaxNode| { - let id = *def_map.entry(def).or_insert_with(|| { - let nav = def.try_to_nav(&sema).map(UpmappingResult::call_site); - let hover = if with_hover { - Some(hover_for_definition( - &sema, - file_id, - def, - None, - scope_node, - None, - false, - &hover_config, - edition, - display_target, - )) - } else { - None - }; - token_store.insert(TokenStaticData { - documentation: documentation_for_definition(&sema, def, scope_node), - hover, - definition: nav - .as_ref() - .map(|it| FileRange { file_id: it.file_id, range: it.focus_or_full_range() }), - definition_body: nav.as_ref().map(|it| FileRange { - file_id: it.file_id, - range: definition_range_excluding_trivia(&sema, it.file_id, it.full_range), - }), - references: vec![], - moniker: current_crate.and_then(|cc| def_to_moniker(db, def, cc)), - display_name: def.name(db).map(|name| name.display(db, edition).to_string()), - signature: Some(def.label(db, display_target)), - kind: def_to_kind(db, def), - }) - }); - let mut token = token_store.get_mut(id).unwrap(); - token.references.push(ReferenceData { - range: FileRange { range, file_id }, - is_definition: match def.try_to_nav(&sema).map(UpmappingResult::call_site) { - Some(it) => it.file_id == file_id && it.focus_or_full_range() == range, - None => false, - }, - }); - result.tokens.push((range, id)); - }; + let mut result: FxHashMap = Default::default(); + result.insert(file_id, StaticIndexedFile { file_id, folds, tokens: vec![] }); + + let mut add_token = + |def: Definition<'a>, file_id: FileId, range: TextRange, scope_node: &SyntaxNode| { + let id = *def_map.entry(def).or_insert_with(|| { + let nav = def.try_to_nav(&sema).map(UpmappingResult::call_site); + let hover = if with_hover { + Some(hover_for_definition( + &sema, + file_id, + def, + None, + scope_node, + None, + false, + &hover_config, + edition, + display_target, + )) + } else { + None + }; + token_store.insert(TokenStaticData { + documentation: documentation_for_definition(&sema, def, scope_node), + hover, + definition: nav.as_ref().map(|it| FileRange { + file_id: it.file_id, + range: it.focus_or_full_range(), + }), + definition_body: nav.as_ref().map(|it| FileRange { + file_id: it.file_id, + range: definition_range_excluding_trivia(&sema, it.file_id, it.full_range), + }), + references: vec![], + moniker: current_crate.and_then(|cc| def_to_moniker(db, def, cc)), + display_name: def.name(db).map(|name| name.display(db, edition).to_string()), + signature: Some(def.label(db, display_target)), + kind: def_to_kind(db, def), + }) + }); + let mut token = token_store.get_mut(id).unwrap(); + token.references.push(ReferenceData { + range: FileRange { range, file_id }, + is_definition: match def.try_to_nav(&sema).map(UpmappingResult::call_site) { + Some(it) => it.file_id == file_id && it.focus_or_full_range() == range, + None => false, + }, + }); + result + .entry(file_id) + .or_insert_with(|| StaticIndexedFile { file_id, folds: vec![], tokens: vec![] }) + .tokens + .push((range, id)); + }; if let Some(module) = sema.file_to_module_def(file_id) { let def = Definition::Module(module); let range = root.text_range(); - add_token(def, range, &root); + add_token(def, file_id, range, &root); } - for token in tokens { - let range = token.text_range(); - let node = token.parent().unwrap(); - match hir::attach_db(db, || get_definitions(&sema, token.clone())) { - Some(defs) => { - for (def, _) in defs { - add_token(def, range, &node); + // The loop below will traverse all macro expansions, but only record tokens from the current file and include! calls. + #[derive(PartialEq, Eq, Clone, Copy)] + enum RootKind { + OnlyTraverse, + RecordTokens, + } + + let mut roots = VecDeque::from([(root, RootKind::RecordTokens)]); + while let Some((root, root_kind)) = roots.pop_front() { + for node_or_token in root.descendants_with_tokens() { + match node_or_token { + NodeOrToken::Token(token) => { + if root_kind != RootKind::RecordTokens { + continue; + } + + let Some(node) = token.parent() else { + continue; + }; + let Some(defs) = hir::attach_db(db, || get_definitions(&sema, token.clone())) + else { + continue; + }; + + let file_range = InFile::new(sema.hir_file_for(&node), token.text_range()) + .original_node_file_range_rooted(db); + + for (def, _) in defs { + let range = file_range.range; + let file_id = file_range.file_id.file_id(db); + add_token(def, file_id, range, &node); + } + } + + NodeOrToken::Node(node) => { + let Some(macro_call) = ast::MacroCall::cast(node) else { + continue; + }; + let Some(macro_call) = sema.to_def(¯o_call) else { + continue; + }; + + let expansion = sema.parse_or_expand(macro_call.into()); + let root_kind = if macro_call.is_include_macro(db) { + RootKind::RecordTokens + } else { + RootKind::OnlyTraverse + }; + roots.push_front((expansion, root_kind)); } } - None => continue, - }; + } } - Some(result) + result } impl StaticIndex { @@ -349,7 +401,21 @@ impl StaticIndex { }) }) .collect(); - threads.into_iter().flat_map(|join_handle| join_handle.join().unwrap()).collect() + + let files_with_duplicates = + threads.into_iter().flat_map(|join_handle| join_handle.join().unwrap()); + + let mut result = FxHashMap::default(); + for (file_id, mut file) in files_with_duplicates { + result + .entry(file_id) + .and_modify(|existing_file: &mut StaticIndexedFile| { + existing_file.tokens.extend(std::mem::take(&mut file.tokens)) + }) + .or_insert(file); + } + + result.into_values().collect() }); StaticIndex { files, tokens } diff --git a/crates/rust-analyzer/src/cli/scip.rs b/crates/rust-analyzer/src/cli/scip.rs index 97db5aebf4d1..a96ef404d241 100644 --- a/crates/rust-analyzer/src/cli/scip.rs +++ b/crates/rust-analyzer/src/cli/scip.rs @@ -897,6 +897,67 @@ pub mod example_mod { ); } + #[test] + fn include_macro() { + check_symbols( + r#" + //- minicore: include + //- /workspace/lib.rs crate:main + use core::include; + + include!("included.rs"); + + //- /workspace/included.rs + struct Included$0; + "#, + &["rust-analyzer cargo main . Included#"], + ); + } + + #[test] + fn include_macro_indirect() { + check_symbols( + r#" + //- minicore: include + //- /workspace/lib.rs crate:main + macro_rules! includer { + () => { + include!("include_intermediate.rs"); + }; + } + + includer!(); + + //- /workspace/include_intermediate.rs + include!("included.rs"); + + //- /workspace/included.rs + struct Included$0; + "#, + &["rust-analyzer cargo main . Included#"], + ); + } + + #[test] + fn include_macro_multiple() { + check_symbols( + r#" + //- minicore: include + //- /workspace/lib.rs crate:main + mod a { + include!("included.rs"); + } + mod b { + include!("included.rs"); + } + + //- /workspace/included.rs + struct Included$0; + "#, + &["rust-analyzer cargo main . a/Included#", "rust-analyzer cargo main . b/Included#"], + ); + } + #[test] fn documentation_matches_doc_comment() { let s = "/// foo\nfn bar() {}";