diff --git a/crates/ide/src/analysis.rs b/crates/ide/src/analysis.rs index 9495312d..8cdfde81 100644 --- a/crates/ide/src/analysis.rs +++ b/crates/ide/src/analysis.rs @@ -39,6 +39,7 @@ use crate::{ references::{self, References, ReferencesConfig}, rename::{self, RenameConfig, RenameResult}, selection_ranges, + semantic_index::{self, ModuleCallEdge}, semantic_tokens::{self, SemaToken, SemaTokenConfig}, signature_help::{self, SignatureHelp, SignatureHelpConfig}, source_change::SourceChange, @@ -184,6 +185,22 @@ impl Analysis { self.with_db(|db| references::references(db, position, config)) } + pub fn module_incoming_calls( + &self, + file_id: FileId, + name_range: TextRange, + ) -> Cancellable> { + self.with_db(|db| semantic_index::incoming_module_edges(db, file_id, name_range)) + } + + pub fn module_outgoing_calls( + &self, + file_id: FileId, + name_range: TextRange, + ) -> Cancellable> { + self.with_db(|db| semantic_index::outgoing_module_edges(db, file_id, name_range)) + } + pub fn prepare_rename( &self, position: FilePosition, diff --git a/crates/ide/src/db/workspace_symbol_index_db.rs b/crates/ide/src/db/workspace_symbol_index_db.rs index 7602b4ed..9bc7cc61 100644 --- a/crates/ide/src/db/workspace_symbol_index_db.rs +++ b/crates/ide/src/db/workspace_symbol_index_db.rs @@ -7,6 +7,7 @@ use vfs::FileId; use crate::{ db::root_db::RootDb, + semantic_index::{ModuleIndex, SemanticIndex}, workspace_symbols::{SymbolIndex, WorkspaceSymbol}, }; @@ -14,6 +15,7 @@ use crate::{ pub trait WorkspaceSymbolIndexDb: SourceRootDb + HirDb { fn file_workspace_symbols(&self, file_id: FileId) -> Arc<[WorkspaceSymbol]>; fn source_root_symbol_index(&self, source_root_id: SourceRootId) -> Arc; + fn source_root_module_index(&self, source_root_id: SourceRootId) -> Arc; } fn file_workspace_symbols( @@ -30,9 +32,30 @@ fn source_root_symbol_index( Arc::new(SymbolIndex::for_source_root(db, source_root_id)) } +fn source_root_module_index( + db: &dyn WorkspaceSymbolIndexDb, + source_root_id: SourceRootId, +) -> Arc { + Arc::new(ModuleIndex::for_source_root(db, source_root_id)) +} + pub(crate) fn source_root_symbol_index_for_root( db: &RootDb, source_root_id: SourceRootId, ) -> Arc { WorkspaceSymbolIndexDb::source_root_symbol_index(db, source_root_id) } + +pub(crate) fn source_root_module_index_for_root( + db: &RootDb, + source_root_id: SourceRootId, +) -> Arc { + WorkspaceSymbolIndexDb::source_root_module_index(db, source_root_id) +} + +pub(crate) fn source_root_semantic_index_for_root( + db: &RootDb, + source_root_id: SourceRootId, +) -> Arc { + Arc::new(SemanticIndex::for_source_root(db, source_root_id)) +} diff --git a/crates/ide/src/lib.rs b/crates/ide/src/lib.rs index a47c820a..5cba0852 100644 --- a/crates/ide/src/lib.rs +++ b/crates/ide/src/lib.rs @@ -40,6 +40,7 @@ pub mod range; pub mod references; pub mod rename; pub mod selection_ranges; +pub mod semantic_index; pub(crate) mod semantic_target; pub mod semantic_tokens; pub mod signature_help; diff --git a/crates/ide/src/module_resolution.rs b/crates/ide/src/module_resolution.rs index 5a3d7324..0e0b700d 100644 --- a/crates/ide/src/module_resolution.rs +++ b/crates/ide/src/module_resolution.rs @@ -1,7 +1,10 @@ use std::cmp::Ordering; use hir::{ - base_db::{source_db::SourceRootDb, source_root::SourceRootRole}, + base_db::{ + source_db::{SourceDb, SourceRootDb}, + source_root::SourceRootRole, + }, container::InModule, db::HirDb, hir_def::{ @@ -11,7 +14,7 @@ use hir::{ lower_ident_opt, module::{ModuleId, instantiation::Instantiation}, }, - scope::{ModuleEntry, ScopeResolution}, + scope::ModuleEntry, semantics::pathres::PathResolution, }; use syntax::{ @@ -21,7 +24,7 @@ use syntax::{ use utils::get::GetRef; use vfs::{FileId, VfsPath}; -use crate::db::root_db::RootDb; +use crate::db::{root_db::RootDb, workspace_symbol_index_db::source_root_module_index_for_root}; #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) enum ModuleResolution { @@ -154,13 +157,34 @@ fn resolve_module_name_with_policy( name: &Ident, policy: ModuleResolutionPolicy, ) -> ModuleResolution { - match db.unit_scope().resolve_module(name) { - ScopeResolution::Unique(module_id) => ModuleResolution::Unique(module_id), - ScopeResolution::Unresolved => ModuleResolution::Unresolved, - ScopeResolution::Ambiguous(candidates) => { - policy.resolve_ambiguous(db, candidates.into_vec()) - } + let candidates = module_candidates(db, name); + match candidates.as_slice() { + [module_id] => ModuleResolution::Unique(*module_id), + [] => ModuleResolution::Unresolved, + _ => policy.resolve_ambiguous(db, candidates), + } +} + +fn module_candidates(db: &RootDb, name: &Ident) -> Vec { + let mut source_root_ids = + db.files().iter().map(|&file_id| db.source_root_id(file_id)).collect::>(); + source_root_ids.sort_unstable(); + source_root_ids.dedup(); + + let mut candidates = Vec::new(); + for source_root_id in source_root_ids { + let module_index = source_root_module_index_for_root(db, source_root_id); + candidates.extend( + module_index + .module_definitions(name) + .iter() + .map(|module| (module.file_id, module.name_range.start(), module.module_id)), + ); } + + candidates.sort_by_key(|(file_id, name_start, _)| (file_id.0, *name_start)); + candidates.dedup_by_key(|(_, _, module_id)| *module_id); + candidates.into_iter().map(|(_, _, module_id)| module_id).collect() } #[derive(Debug, Clone, Copy, PartialEq, Eq)] diff --git a/crates/ide/src/references/search.rs b/crates/ide/src/references/search.rs index ad482c7a..d104f227 100644 --- a/crates/ide/src/references/search.rs +++ b/crates/ide/src/references/search.rs @@ -1,33 +1,26 @@ -use std::cell::LazyCell; - use hir::{ - base_db::{intern::Lookup, salsa::Database, source_db::SourceDb}, + base_db::{ + intern::Lookup, + salsa::Database, + source_db::{SourceDb, SourceRootDb}, + source_root::SourceRootId, + }, container::{ContainerId, InFile}, semantics::Semantics, source_map::IsSrc, }; -use itertools::Itertools; -use memchr::memmem::Finder; use nohash_hasher::IntMap; use rustc_hash::FxHashMap; -use smallvec::SmallVec; -use syntax::{ - SyntaxNode, SyntaxTokenWithParent, has_text_range::HasTextRange, ptr::SyntaxTokenPtr, - token::TokenKindExt, -}; -use triomphe::Arc; -use utils::{ - get::Get, - line_index::{TextRange, TextSize}, -}; +use syntax::{SyntaxTokenWithParent, ptr::SyntaxTokenPtr}; +use utils::{get::Get, line_index::TextRange}; use vfs::FileId; use super::{ReferenceCategory, ReferencesConfig}; use crate::{ ScopeVisibility, - db::root_db::RootDb, - definitions::{Definition, DefinitionClass}, - source_targets::SourceTargetRequestCache, + db::{root_db::RootDb, workspace_symbol_index_db::source_root_semantic_index_for_root}, + definitions::Definition, + semantic_index::SemanticReference, }; /// A search scope is a set of files and ranges within those files that should @@ -136,6 +129,20 @@ impl SearchScope { pub(crate) fn range_for_file(&self, file_id: FileId) -> Option> { self.0.get(&file_id).copied() } + + pub(crate) fn contains(&self, file_id: FileId, range: TextRange) -> bool { + self.range_for_file(file_id).is_some_and(|file_range| { + file_range.is_none_or(|file_range| file_range.intersect(range).is_some()) + }) + } + + fn source_root_ids(&self, db: &RootDb) -> Vec { + let mut root_ids = + self.0.keys().map(|file_id| db.source_root_id(*file_id)).collect::>(); + root_ids.sort_unstable(); + root_ids.dedup(); + root_ids + } } pub(crate) struct ReferencesCtx<'a, 'b> { @@ -147,19 +154,17 @@ pub(crate) struct ReferencesCtx<'a, 'b> { #[derive(Debug, Clone, Copy)] pub(crate) struct ReferenceToken { ptr: SyntaxTokenPtr, + range: TextRange, category: ReferenceCategory, } impl ReferenceToken { - pub fn new(token: SyntaxTokenWithParent) -> Self { - Self { - ptr: SyntaxTokenPtr::from_token(token), - category: ReferenceCategory::from_tok(token), - } + pub(crate) fn from_semantic_reference(reference: &SemanticReference) -> Self { + Self { ptr: reference.ptr, range: reference.range, category: reference.category } } pub fn range(&self) -> TextRange { - self.ptr.range() + self.range } pub fn category(&self) -> ReferenceCategory { @@ -184,142 +189,26 @@ impl<'a, 'b> ReferencesCtx<'a, 'b> { } pub(crate) fn search(&self) -> IntMap> { - let sema = self.sema; - let db = sema.db; + let db = self.sema.db; let mut res: IntMap<_, Vec<_>> = IntMap::default(); - let Some(name) = self.def.origins().into_iter().find_map(|def| def.name(db)) else { - return res; - }; - debug_assert! {{ - let names = self - .def - .origins() - .into_iter() - .filter_map(|def| def.name(sema.db)) - .collect_vec(); - !names.is_empty() && names.iter().all(|namei| namei == &name) - }}; - - let def_ranges: SmallVec<[_; 6]> = - self.def.origins().into_iter().filter_map(|def| def.name_range(db)).collect(); - - let finder = &Finder::new(&name); - let mut source_target_cache = SourceTargetRequestCache::default(); - for (text, file_id, range) in self.scope_files() { + for source_root_id in self.scope.source_root_ids(db) { self.sema.db.unwind_if_cancelled(); + let index = source_root_semantic_index_for_root(db, source_root_id); + let Some(group) = index.references_for_definition(self.def) else { + continue; + }; - let parsed_file = LazyCell::new(|| sema.parse_file(file_id)); - Self::match_text(&text, finder, range) - .flat_map(|offset| { - let Some(root) = (*parsed_file).root() else { - return Vec::new(); - }; - Self::filter_tokens( - sema.db, - root, - file_id, - &def_ranges, - offset, - &mut source_target_cache, - ) - }) - .filter(|tp| self.classify_and_filter(sema, file_id.into(), tp)) - .for_each(|token| { - res.entry(file_id) - .or_insert_with(|| Vec::with_capacity(Self::FILE_REF_CAPACITY)) - .push(ReferenceToken::new(token)) - }); + for reference in group.references.iter() { + if !self.scope.contains(reference.file_id, reference.range) { + continue; + } + res.entry(reference.file_id) + .or_insert_with(|| Vec::with_capacity(Self::FILE_REF_CAPACITY)) + .push(ReferenceToken::from_semantic_reference(reference)); + } } res } - - fn scope_files(&self) -> impl Iterator, FileId, TextRange)> + '_ { - self.scope.0.iter().map(|(file_id, range)| { - let text = self.sema.db.file_text(*file_id); - let range = range.unwrap_or_else(|| TextRange::up_to(TextSize::of(&*text))); - (text, *file_id, range) - }) - } - - fn match_text<'c>( - text: &'c str, - finder: &'c Finder, - search_range: TextRange, - ) -> impl Iterator + 'c { - finder.find_iter(text.as_bytes()).filter_map(move |idx| { - let offset = TextSize::from(idx as u32); - if !search_range.contains_inclusive(offset) { - return None; - } - - // If this is not a word boundary, that means this is only part of an ident. - if text[..idx].chars().next_back().is_some_and(|ch| ch.is_alphabetic() || ch == '_') - || text[idx + finder.needle().len()..] - .chars() - .next() - .is_some_and(|ch| ch.is_alphanumeric() || ch == '_') - { - return None; - } - - Some(offset) - }) - } - - fn filter_tokens<'tree>( - db: &RootDb, - node: SyntaxNode<'tree>, - file_id: FileId, - names: &[InFile], - offset: TextSize, - source_target_cache: &mut SourceTargetRequestCache, - ) -> Vec> { - let Some(target) = crate::source_targets::source_target_at_offset_with_cache( - db, - file_id, - node, - offset, - super::token_precedence, - source_target_cache, - ) - .and_then(|resolution| resolution.resolved()) else { - return Vec::new(); - }; - - target - .into_tokens() - .into_iter() - .filter(|tok| tok.kind().name_like()) - .filter(|tok| { - let Some(tok_range) = tok.text_range() else { - return false; - }; - - !names.iter().any(|InFile { value: range, file_id: name_file_id }| { - tok_range == *range && *name_file_id == file_id.into() - }) - }) - .collect() - } - - fn classify_and_filter<'tree>( - &self, - sema: &Semantics<'_, RootDb>, - file_id: hir::file::HirFileId, - tp: &SyntaxTokenWithParent<'tree>, - ) -> bool { - let Some(def) = DefinitionClass::resolve(sema, file_id, *tp) else { - return false; - }; - - match def { - DefinitionClass::Definition(def) => def == *self.def, - DefinitionClass::PortConnShorthand { local, port } => { - local == *self.def || port == *self.def - } - DefinitionClass::Ambiguous(_) => false, - } - } } diff --git a/crates/ide/src/semantic_index.rs b/crates/ide/src/semantic_index.rs new file mode 100644 index 00000000..dc2ca6fd --- /dev/null +++ b/crates/ide/src/semantic_index.rs @@ -0,0 +1,511 @@ +use hir::{ + base_db::{ + source_db::{SourceDb, SourceRootDb}, + source_root::SourceRootId, + }, + container::InFile, + db::HirDb, + file::HirFileId, + hir_def::{Ident, module::ModuleId}, + scope::UnitEntry, + semantics::Semantics, + source_map::IsSrc, +}; +use itertools::Itertools; +use rustc_hash::FxHashMap; +use syntax::{ + SyntaxElement, SyntaxNodeExt, SyntaxTokenWithParent, TokenKind, WalkEvent, + has_text_range::HasTextRange, ptr::SyntaxTokenPtr, token::TokenKindExt, +}; +use utils::{get::Get, line_index::TextRange}; +use vfs::FileId; + +use crate::{ + db::{ + root_db::RootDb, + workspace_symbol_index_db::{ + WorkspaceSymbolIndexDb, source_root_module_index_for_root, + source_root_semantic_index_for_root, + }, + }, + definitions::{Definition, DefinitionClass, DefinitionOrigin}, + module_resolution::resolve_hir_instantiation_target, + references::ReferenceCategory, + semantic_target::{SemanticTarget, TargetIntent, resolve_semantic_target}, +}; + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub(crate) struct DefinitionKey(Box<[DefinitionOrigin]>); + +impl DefinitionKey { + pub(crate) fn from_definition(definition: &Definition) -> Option { + let origins = definition.origins(); + (!origins.is_empty()).then(|| Self(origins.into_iter().collect())) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub(crate) struct SemanticDefinitionRange { + pub file_id: FileId, + pub range: TextRange, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct SemanticReference { + pub file_id: FileId, + pub range: TextRange, + pub category: ReferenceCategory, + pub ptr: SyntaxTokenPtr, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct SemanticReferenceGroup { + pub name: String, + pub definition_ranges: Box<[SemanticDefinitionRange]>, + pub references: Box<[SemanticReference]>, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct SemanticModuleDefinition { + pub module_id: ModuleId, + pub file_id: FileId, + pub name: Ident, + pub name_range: TextRange, + pub full_range: TextRange, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ModuleCallItem { + pub file_id: FileId, + pub name: String, + pub full_range: TextRange, + pub name_range: TextRange, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ModuleCallEdge { + pub caller: ModuleCallItem, + pub callee: ModuleCallItem, + pub call_range: TextRange, +} + +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct ModuleIndex { + modules_by_name: FxHashMap>, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct SemanticIndex { + references_by_definition: FxHashMap, + incoming_module_edges: FxHashMap>, + outgoing_module_edges: FxHashMap>, +} + +#[derive(Debug, Default)] +struct SemanticIndexBuilder { + references_by_definition: FxHashMap, + incoming_module_edges: FxHashMap>, + outgoing_module_edges: FxHashMap>, +} + +#[derive(Debug)] +struct SemanticReferenceGroupBuilder { + name: String, + definition_ranges: Vec, + references: Vec, +} + +impl ModuleIndex { + pub(crate) fn for_source_root( + db: &dyn WorkspaceSymbolIndexDb, + source_root_id: SourceRootId, + ) -> Self { + let source_root = db.source_root(source_root_id); + let mut modules_by_name: FxHashMap> = + FxHashMap::default(); + + for file_id in source_root.iter() { + let hir_file_id = HirFileId::from(file_id); + for (_, entry) in db.file_scope(hir_file_id).iter() { + let UnitEntry::ModuleId(module_id) = entry else { + continue; + }; + let Some(module) = SemanticModuleDefinition::new(db, module_id) else { + continue; + }; + modules_by_name.entry(module.name.clone()).or_default().push(module); + } + } + + Self { + modules_by_name: modules_by_name + .into_iter() + .map(|(name, mut modules)| { + modules.sort_by_key(|module| (module.file_id.0, module.name_range.start())); + modules.dedup_by(|lhs, rhs| { + lhs.module_id == rhs.module_id + || (lhs.file_id == rhs.file_id && lhs.name_range == rhs.name_range) + }); + (name, modules.into_boxed_slice()) + }) + .collect(), + } + } + + pub(crate) fn module_definitions(&self, name: &Ident) -> &[SemanticModuleDefinition] { + self.modules_by_name.get(name).map_or(&[], |modules| modules.as_ref()) + } + + fn module_definition_at( + &self, + file_id: FileId, + name_range: TextRange, + ) -> Option<&SemanticModuleDefinition> { + self.all_module_definitions() + .find(|module| module.file_id == file_id && module.name_range == name_range) + } + + fn all_module_definitions(&self) -> impl Iterator { + self.modules_by_name.values().flat_map(|modules| modules.iter()) + } +} + +impl SemanticModuleDefinition { + fn new(db: &dyn HirDb, module_id: ModuleId) -> Option { + let origin = DefinitionOrigin::ModuleId(module_id); + let name = origin.name(db)?; + let InFile { file_id, value: name_range } = origin.name_range(db)?; + let InFile { value: full_range, .. } = origin.range(db)?; + + Some(Self { module_id, file_id: file_id.file_id(), name, name_range, full_range }) + } + + fn call_item(&self) -> ModuleCallItem { + ModuleCallItem { + file_id: self.file_id, + name: self.name.to_string(), + full_range: self.full_range, + name_range: self.name_range, + } + } +} + +impl SemanticIndex { + pub(crate) fn for_source_root(db: &RootDb, source_root_id: SourceRootId) -> Self { + let source_root = db.source_root(source_root_id); + let module_index = source_root_module_index_for_root(db, source_root_id); + let mut builder = SemanticIndexBuilder::default(); + + builder.collect_module_edges(db, &module_index); + for file_id in source_root.iter() { + builder.collect_file(db, file_id); + } + + builder.finish() + } + + pub(crate) fn references_for_definition( + &self, + definition: &Definition, + ) -> Option<&SemanticReferenceGroup> { + let key = DefinitionKey::from_definition(definition)?; + self.references_by_definition.get(&key) + } + + pub(crate) fn incoming_module_edges(&self, module_id: ModuleId) -> &[ModuleCallEdge] { + self.incoming_module_edges.get(&module_id).map_or(&[], |edges| edges.as_ref()) + } + + pub(crate) fn outgoing_module_edges(&self, module_id: ModuleId) -> &[ModuleCallEdge] { + self.outgoing_module_edges.get(&module_id).map_or(&[], |edges| edges.as_ref()) + } + + #[cfg(test)] + pub(crate) fn reference_groups_named(&self, name: &str) -> Vec<&SemanticReferenceGroup> { + self.references_by_definition.values().filter(|group| group.name == name).collect() + } +} + +impl SemanticIndexBuilder { + fn collect_file(&mut self, db: &RootDb, file_id: FileId) { + let sema = Semantics::new(db); + let parsed_file = sema.parse_file(file_id); + let Some(root) = parsed_file.root() else { + return; + }; + let hir_file_id = HirFileId::from(file_id); + + for event in root.elem_preorder() { + let WalkEvent::Enter(SyntaxElement::Token(token)) = event else { + continue; + }; + if !token.kind().name_like() { + continue; + } + let Some(range) = token.text_range() else { + continue; + }; + let Some(SemanticTarget::Source(target)) = + resolve_semantic_target(db, file_id, range.start(), Some(root), token_precedence) + .unique_for_intent(TargetIntent::FindReferences) + else { + continue; + }; + + for token in target.into_tokens().into_iter().filter(|token| token.kind().name_like()) { + self.collect_token(db, &sema, hir_file_id, token); + } + } + } + + fn collect_token( + &mut self, + db: &RootDb, + sema: &Semantics<'_, RootDb>, + file_id: HirFileId, + token: SyntaxTokenWithParent<'_>, + ) { + let Some(range) = token.text_range() else { + return; + }; + let Some(class) = DefinitionClass::resolve(sema, file_id, token) else { + return; + }; + + match class { + DefinitionClass::Definition(definition) => { + self.collect_definition_token(db, &definition, file_id.file_id(), range, token) + } + DefinitionClass::PortConnShorthand { port, local } => { + self.collect_definition_token(db, &port, file_id.file_id(), range, token); + self.collect_definition_token(db, &local, file_id.file_id(), range, token); + } + DefinitionClass::Ambiguous(_) => {} + } + } + + fn collect_definition_token( + &mut self, + db: &RootDb, + definition: &Definition, + file_id: FileId, + range: TextRange, + token: SyntaxTokenWithParent<'_>, + ) { + let Some(key) = DefinitionKey::from_definition(definition) else { + return; + }; + let Some(name) = definition.origins().into_iter().find_map(|origin| origin.name(db)) else { + return; + }; + let definition_ranges = definition + .origins() + .into_iter() + .filter_map(|origin| origin.name_range(db)) + .map(|InFile { file_id, value }| SemanticDefinitionRange { + file_id: file_id.file_id(), + range: value, + }) + .unique() + .collect_vec(); + let is_definition_site = definition_ranges.iter().any(|definition_range| { + definition_range.file_id == file_id && definition_range.range == range + }); + + let group = self.references_by_definition.entry(key).or_insert_with(|| { + SemanticReferenceGroupBuilder { + name: name.to_string(), + definition_ranges, + references: Vec::new(), + } + }); + + if is_definition_site { + return; + } + + let reference = SemanticReference { + file_id, + range, + category: ReferenceCategory::from_tok(token), + ptr: SyntaxTokenPtr::from_token(token), + }; + if !group.references.iter().any(|existing| { + existing.file_id == reference.file_id && existing.range == reference.range + }) { + group.references.push(reference); + } + } + + fn collect_module_edges(&mut self, db: &RootDb, module_index: &ModuleIndex) { + for caller in module_index.all_module_definitions() { + let (module, source_map) = db.module_with_source_map(caller.module_id); + for (instantiation_id, instantiation) in module.instantiations.iter() { + let Some(callee_module_id) = + resolve_hir_instantiation_target(db, caller.file_id, instantiation) + else { + continue; + }; + let Some(callee) = SemanticModuleDefinition::new(db, callee_module_id) else { + continue; + }; + let Some(src) = source_map.get(instantiation_id) else { + continue; + }; + let Some(call_range) = instantiation_name_range(db, caller.file_id, src) else { + continue; + }; + + self.collect_module_edge( + caller.module_id, + callee.module_id, + ModuleCallEdge { + caller: caller.call_item(), + callee: callee.call_item(), + call_range, + }, + ); + } + } + } + + fn collect_module_edge( + &mut self, + caller_module_id: ModuleId, + callee_module_id: ModuleId, + edge: ModuleCallEdge, + ) { + push_unique_edge( + self.outgoing_module_edges.entry(caller_module_id).or_default(), + edge.clone(), + ); + push_unique_edge(self.incoming_module_edges.entry(callee_module_id).or_default(), edge); + } + + fn finish(self) -> SemanticIndex { + SemanticIndex { + references_by_definition: self + .references_by_definition + .into_iter() + .map(|(key, group)| (key, group.finish())) + .collect(), + incoming_module_edges: finish_edge_map(self.incoming_module_edges), + outgoing_module_edges: finish_edge_map(self.outgoing_module_edges), + } + } +} + +impl SemanticReferenceGroupBuilder { + fn finish(self) -> SemanticReferenceGroup { + SemanticReferenceGroup { + name: self.name, + definition_ranges: self.definition_ranges.into_boxed_slice(), + references: self.references.into_boxed_slice(), + } + } +} + +pub(crate) fn incoming_module_edges( + db: &RootDb, + file_id: FileId, + name_range: TextRange, +) -> Vec { + module_edges(db, file_id, name_range, |index, module_id| index.incoming_module_edges(module_id)) +} + +pub(crate) fn outgoing_module_edges( + db: &RootDb, + file_id: FileId, + name_range: TextRange, +) -> Vec { + module_edges(db, file_id, name_range, |index, module_id| index.outgoing_module_edges(module_id)) +} + +fn module_edges( + db: &RootDb, + file_id: FileId, + name_range: TextRange, + edges_for_index: impl Fn(&SemanticIndex, ModuleId) -> &[ModuleCallEdge], +) -> Vec { + let Some(module_id) = module_id_at_range(db, file_id, name_range) else { + return Vec::new(); + }; + + let mut source_root_ids = + db.files().iter().map(|&file_id| db.source_root_id(file_id)).collect::>(); + source_root_ids.sort_unstable(); + source_root_ids.dedup(); + + let mut edges = Vec::new(); + for source_root_id in source_root_ids { + let index = source_root_semantic_index_for_root(db, source_root_id); + edges.extend(edges_for_index(&index, module_id).iter().cloned()); + } + sort_and_dedup_edges(&mut edges); + edges +} + +fn module_id_at_range(db: &RootDb, file_id: FileId, name_range: TextRange) -> Option { + let module_index = source_root_module_index_for_root(db, db.source_root_id(file_id)); + module_index.module_definition_at(file_id, name_range).map(|module| module.module_id) +} + +fn instantiation_name_range( + db: &RootDb, + file_id: FileId, + src: hir::hir_def::module::instantiation::InstantiationSrc, +) -> Option { + let tree = db.parse_src(file_id); + let root = tree.root()?; + let instantiation_range = src.range(); + let mut offset = instantiation_range.start(); + + while offset < instantiation_range.end() { + let token = root.token_after_or_at_offset(offset)?; + let range = token.text_range()?; + if range.start() >= instantiation_range.end() { + return None; + } + if token.kind().name_like() { + return Some(range); + } + offset = range.end(); + } + + None +} + +fn push_unique_edge(edges: &mut Vec, edge: ModuleCallEdge) { + if !edges.iter().any(|existing| existing == &edge) { + edges.push(edge); + } +} + +fn finish_edge_map( + edges_by_module: FxHashMap>, +) -> FxHashMap> { + edges_by_module + .into_iter() + .map(|(key, mut edges)| { + sort_and_dedup_edges(&mut edges); + (key, edges.into_boxed_slice()) + }) + .collect() +} + +fn sort_and_dedup_edges(edges: &mut Vec) { + edges.sort_by_key(|edge| { + ( + edge.caller.file_id.0, + edge.caller.name_range.start(), + edge.callee.file_id.0, + edge.callee.name_range.start(), + edge.call_range.start(), + ) + }); + edges.dedup(); +} + +fn token_precedence(kind: TokenKind) -> usize { + usize::from(kind.name_like()) +} diff --git a/crates/ide/src/verilog_2005.rs b/crates/ide/src/verilog_2005.rs index 8d8ba746..b51503d3 100644 --- a/crates/ide/src/verilog_2005.rs +++ b/crates/ide/src/verilog_2005.rs @@ -188,6 +188,32 @@ fn setup_marked_with_path( (host, file_id, text, markers) } +type MarkedFile = (FileId, String, HashMap); + +fn setup_marked_files(files: &[(&str, &str)]) -> (AnalysisHost, Vec) { + let mut file_set = FileSet::default(); + let mut change = Change::new(); + let mut marked_files = Vec::new(); + + for (idx, (path, text)) in files.iter().enumerate() { + let file_id = FileId(idx as u32); + let text = normalize_fixture_text(text); + let (text, markers) = strip_markers(text); + file_set.insert(file_id, VfsPath::new_virtual_path((*path).to_owned())); + change.add_changed_file(ChangedFile { + file_id, + change_kind: ChangeKind::Create(Arc::from(text.as_str()), LineEnding::Unix), + }); + marked_files.push((file_id, text, markers)); + } + + change.set_roots(vec![SourceRoot::new_local(file_set)]); + + let mut host = AnalysisHost::default(); + host.apply_change(change); + (host, marked_files) +} + fn setup_marked_with_predefines( text: &str, predefines: Vec, @@ -2801,6 +2827,151 @@ endmodule ); } +#[test] +fn semantic_index_groups_modules_and_references_by_definition() { + let (host, files) = setup_marked_files(&[ + ( + "/a.sv", + r#" +module /*marker:a_module_def*/mod_a; + wire /*marker:a_shared_def*/shared; + assign y = /*marker:a_shared_ref*/shared; +endmodule +"#, + ), + ( + "/b.sv", + r#" +module /*marker:b_module_def*/mod_b; + wire /*marker:b_shared_def*/shared; + assign y = /*marker:b_shared_ref*/shared; +endmodule +"#, + ), + ]); + let [(file_a, _text_a, markers_a), (file_b, _text_b, markers_b)] = files.as_slice() else { + panic!("expected two fixture files"); + }; + + let module_index = crate::db::workspace_symbol_index_db::source_root_module_index_for_root( + host.raw_db(), + SourceRootId(0), + ); + let index = crate::db::workspace_symbol_index_db::source_root_semantic_index_for_root( + host.raw_db(), + SourceRootId(0), + ); + + let modules = module_index.module_definitions(&"mod_a".into()); + assert_eq!(modules.len(), 1, "module index should contain mod_a exactly once"); + assert_eq!(modules[0].file_id, *file_a); + assert_eq!(modules[0].name_range, marked_range(markers_a, "a_module_def", 5)); + + let groups = index.reference_groups_named("shared"); + assert_eq!(groups.len(), 2, "same-name definitions should be separate reference groups"); + + let a_def = marked_range(markers_a, "a_shared_def", 6); + let a_ref = marked_range(markers_a, "a_shared_ref", 6); + let group_a = groups + .iter() + .find(|group| { + group + .definition_ranges + .iter() + .any(|range| range.file_id == *file_a && range.range == a_def) + }) + .expect("shared definition in a.sv should have a reference group"); + let refs_a = group_a + .references + .iter() + .map(|reference| (reference.file_id, reference.range)) + .collect::>(); + assert_eq!(refs_a, vec![(*file_a, a_ref)]); + + let b_def = marked_range(markers_b, "b_shared_def", 6); + let b_ref = marked_range(markers_b, "b_shared_ref", 6); + let group_b = groups + .iter() + .find(|group| { + group + .definition_ranges + .iter() + .any(|range| range.file_id == *file_b && range.range == b_def) + }) + .expect("shared definition in b.sv should have a reference group"); + let refs_b = group_b + .references + .iter() + .map(|reference| (reference.file_id, reference.range)) + .collect::>(); + assert_eq!(refs_b, vec![(*file_b, b_ref)]); +} + +#[test] +fn semantic_index_records_module_instantiation_edges() { + let (host, files) = setup_marked_files(&[ + ( + "/top.sv", + r#" +module /*marker:top_def*/top; + /*marker:child_call*/child u_child(); +endmodule +"#, + ), + ( + "/child.sv", + r#" +module /*marker:child_def*/child; + /*marker:leaf_call*/leaf u_leaf(); +endmodule +"#, + ), + ( + "/leaf.sv", + r#" +module /*marker:leaf_def*/leaf; +endmodule +"#, + ), + ]); + let [ + (top_file, _top_text, top_markers), + (child_file, _child_text, child_markers), + (leaf_file, _leaf_text, leaf_markers), + ] = files.as_slice() + else { + panic!("expected three fixture files"); + }; + + let top_def = marked_range(top_markers, "top_def", 3); + let child_def = marked_range(child_markers, "child_def", 5); + let leaf_def = marked_range(leaf_markers, "leaf_def", 4); + let child_call = marked_range(top_markers, "child_call", 5); + let leaf_call = marked_range(child_markers, "leaf_call", 4); + + let top_outgoing = + crate::semantic_index::outgoing_module_edges(host.raw_db(), *top_file, top_def); + assert_eq!(top_outgoing.len(), 1); + assert_eq!(top_outgoing[0].caller.file_id, *top_file); + assert_eq!(top_outgoing[0].caller.name_range, top_def); + assert_eq!(top_outgoing[0].callee.file_id, *child_file); + assert_eq!(top_outgoing[0].callee.name_range, child_def); + assert_eq!(top_outgoing[0].call_range, child_call); + + let child_outgoing = + crate::semantic_index::outgoing_module_edges(host.raw_db(), *child_file, child_def); + assert_eq!(child_outgoing.len(), 1); + assert_eq!(child_outgoing[0].callee.file_id, *leaf_file); + assert_eq!(child_outgoing[0].callee.name_range, leaf_def); + assert_eq!(child_outgoing[0].call_range, leaf_call); + + let child_incoming = + crate::semantic_index::incoming_module_edges(host.raw_db(), *child_file, child_def); + assert_eq!(child_incoming.len(), 1); + assert_eq!(child_incoming[0].caller.file_id, *top_file); + assert_eq!(child_incoming[0].call_range, child_call); +} + #[test] fn verilog_2005_hover_covers_all_definition_kinds() { let (host, file_id, _clean_text, markers) = setup_marked(VERILOG_2005_NAV_TEXT); diff --git a/src/global_state/handlers/request/navigation.rs b/src/global_state/handlers/request/navigation.rs index 11f3e1df..4417e2a4 100644 --- a/src/global_state/handlers/request/navigation.rs +++ b/src/global_state/handlers/request/navigation.rs @@ -1,9 +1,8 @@ use ide::{ - FilePosition, FileRange, SymbolKind, document_symbols::DocumentSymbol, - navigation_target::NavTarget, references::References, + FileRange, SymbolKind, navigation_target::NavTarget, references::References, + semantic_index::ModuleCallItem, }; use itertools::Itertools; -use utils::text_edit::{TextRange, TextSize}; use crate::{ global_state::snapshot::GlobalStateSnapshot, @@ -75,16 +74,10 @@ pub(crate) fn handle_call_hierarchy_incoming( let target_selection_range = from_proto::text_range(&target_line_info, target.selection_range)?; let mut groups = Vec::<(lsp_types::CallHierarchyItem, Vec)>::new(); - for reference in reference_ranges_for_call_item(&snap, &target)? { - if reference.file_id == target_file_id && reference.range == target_selection_range { - continue; - } - - let Some(caller) = enclosing_module_item(&snap, reference)? else { - continue; - }; - let line_info = snap.line_info(reference.file_id)?; - push_call_range(&mut groups, caller, to_proto::range(&line_info, reference.range)); + for edge in snap.analysis.module_incoming_calls(target_file_id, target_selection_range)? { + let caller = call_hierarchy_item_for_module(&snap, &edge.caller)?; + let line_info = snap.line_info(edge.caller.file_id)?; + push_call_range(&mut groups, caller, to_proto::range(&line_info, edge.call_range)); } let calls = groups @@ -101,25 +94,16 @@ pub(crate) fn handle_call_hierarchy_outgoing( let caller = params.item; let caller_file_id = from_proto::file_id(&snap, &caller.uri)?; let caller_line_info = snap.line_info(caller_file_id)?; - let caller_range = from_proto::text_range(&caller_line_info, caller.range)?; + let caller_selection_range = from_proto::text_range(&caller_line_info, caller.selection_range)?; let mut groups = Vec::<(lsp_types::CallHierarchyItem, Vec)>::new(); - for callee in workspace_module_items(&snap)? { + for edge in snap.analysis.module_outgoing_calls(caller_file_id, caller_selection_range)? { + let callee = call_hierarchy_item_for_module(&snap, &edge.callee)?; if same_call_hierarchy_item(&caller, &callee) { continue; } - for reference in reference_ranges_for_call_item(&snap, &callee)? { - if reference.file_id == caller_file_id - && range_contains_range(caller_range, reference.range) - { - push_call_range( - &mut groups, - callee.clone(), - to_proto::range(&caller_line_info, reference.range), - ); - } - } + push_call_range(&mut groups, callee, to_proto::range(&caller_line_info, edge.call_range)); } let calls = groups @@ -184,30 +168,6 @@ pub(crate) fn handle_references( Ok(Some(locations)) } -fn reference_ranges_for_call_item( - snap: &GlobalStateSnapshot, - item: &lsp_types::CallHierarchyItem, -) -> anyhow::Result> { - let file_id = from_proto::file_id(snap, &item.uri)?; - let line_info = snap.line_info(file_id)?; - let offset = from_proto::offset(&line_info, item.selection_range.start)?; - let position = FilePosition { file_id, offset }; - let config = snap.config.references(); - let Some(references) = snap.analysis.references(position, config)? else { - return Ok(Vec::new()); - }; - - Ok(references - .into_iter() - .flat_map(|References { refs, .. }| { - refs.into_iter().flat_map(|(file_id, refs)| { - refs.into_iter().map(move |(range, _)| FileRange { file_id, range }) - }) - }) - .unique() - .collect_vec()) -} - fn call_hierarchy_item_for_nav( snap: &GlobalStateSnapshot, nav: NavTarget, @@ -241,89 +201,22 @@ fn call_hierarchy_item_for_nav( })) } -fn workspace_module_items( +fn call_hierarchy_item_for_module( snap: &GlobalStateSnapshot, -) -> anyhow::Result> { - let mut file_ids = snap.file_ids(); - file_ids.sort_unstable_by_key(|file_id| file_id.0); - file_ids.dedup(); - - let mut items = Vec::new(); - for file_id in file_ids { - let uri = to_proto::url(snap, file_id)?; - let line_info = snap.line_info(file_id)?; - for symbol in snap.analysis.document_symbol(file_id)? { - collect_module_items(&uri, &line_info, symbol, &mut items); - } - } - - Ok(items.into_iter().unique_by(call_hierarchy_item_key).collect()) -} - -fn collect_module_items( - uri: &lsp_types::Url, - line_info: &utils::lines::LineInfo, - symbol: DocumentSymbol, - items: &mut Vec, -) { - if symbol.kind == SymbolKind::Module { - items.push(call_hierarchy_item_for_symbol(uri, line_info, &symbol)); - } - - for child in symbol.children { - collect_module_items(uri, line_info, child, items); - } -} - -fn enclosing_module_item( - snap: &GlobalStateSnapshot, - range: FileRange, -) -> anyhow::Result> { - let uri = to_proto::url(snap, range.file_id)?; - let line_info = snap.line_info(range.file_id)?; - let mut best = None; - for symbol in snap.analysis.document_symbol(range.file_id)? { - find_enclosing_module_symbol(symbol, range.range.start(), &mut best); - } - - Ok(best.map(|symbol| call_hierarchy_item_for_symbol(&uri, &line_info, &symbol))) -} - -fn find_enclosing_module_symbol( - symbol: DocumentSymbol, - offset: TextSize, - best: &mut Option, -) { - if !range_contains_offset(symbol.full_range, offset) { - return; - } - - if symbol.kind == SymbolKind::Module - && best.as_ref().is_none_or(|current| symbol.full_range.len() < current.full_range.len()) - { - *best = Some(symbol.clone()); - } - - for child in symbol.children { - find_enclosing_module_symbol(child, offset, best); - } -} - -fn call_hierarchy_item_for_symbol( - uri: &lsp_types::Url, - line_info: &utils::lines::LineInfo, - symbol: &DocumentSymbol, -) -> lsp_types::CallHierarchyItem { - lsp_types::CallHierarchyItem { - name: symbol.name.clone(), - kind: to_proto::symbol_kind(symbol.kind), + module: &ModuleCallItem, +) -> anyhow::Result { + let uri = to_proto::url(snap, module.file_id)?; + let line_info = snap.line_info(module.file_id)?; + Ok(lsp_types::CallHierarchyItem { + name: module.name.clone(), + kind: to_proto::symbol_kind(SymbolKind::Module), tags: None, - detail: symbol.container_name.clone(), - uri: uri.clone(), - range: to_proto::range(line_info, symbol.full_range), - selection_range: to_proto::range(line_info, symbol.focus_range), + detail: None, + uri, + range: to_proto::range(&line_info, module.full_range), + selection_range: to_proto::range(&line_info, module.name_range), data: None, - } + }) } fn is_call_hierarchy_kind(kind: SymbolKind) -> bool { @@ -363,14 +256,6 @@ fn call_hierarchy_item_key( (item.name.clone(), item.uri.clone(), item.range, item.selection_range) } -fn range_contains_offset(range: TextRange, offset: TextSize) -> bool { - range.start() <= offset && offset < range.end() -} - -fn range_contains_range(container: TextRange, range: TextRange) -> bool { - container.start() <= range.start() && range.end() <= container.end() -} - pub(crate) fn handle_hover( snap: GlobalStateSnapshot, params: lsp_types::HoverParams,