diff --git a/crates/ide/src/analysis.rs b/crates/ide/src/analysis.rs index 8cdfde81..58bed7d8 100644 --- a/crates/ide/src/analysis.rs +++ b/crates/ide/src/analysis.rs @@ -19,6 +19,7 @@ use vfs::FileId; use crate::{ Cancellable, FilePosition, RangeInfo, + call_hierarchy::{self, CallHierarchyItem, IncomingCall, OutgoingCall}, code_action::{self, CodeAction, CodeActionDiagnostics, CodeActionResolveStrategy}, code_lens::{self, CodeLens, CodeLensConfig, CodeLensKind}, completion::{ @@ -29,6 +30,10 @@ use crate::{ diagnostics, document_highlight::{self, DocumentHighlight, DocumentHighlightConfig}, document_symbols::{self, DocumentSymbol}, + facts::{ + SemanticFacts, + edit::{EditPlan, EditRequest}, + }, folding_ranges::{self, Fold, FoldingConfig}, formatting::{self, FmtConfig}, goto_declaration, goto_definition, @@ -36,8 +41,8 @@ use crate::{ inlay_hint::{self, InlayHint, InlayHintConfig}, markup::Markup, navigation_target::NavTarget, - references::{self, References, ReferencesConfig}, - rename::{self, RenameConfig, RenameResult}, + references::{References, ReferencesConfig}, + rename::{RecursiveRenameInfo, RenameCollisionInfo, RenameConfig, RenameResult}, selection_ranges, semantic_index::{self, ModuleCallEdge}, semantic_tokens::{self, SemaToken, SemaTokenConfig}, @@ -182,7 +187,32 @@ impl Analysis { position: FilePosition, config: ReferencesConfig, ) -> Cancellable>> { - self.with_db(|db| references::references(db, position, config)) + self.with_db(|db| { + crate::facts::SemanticFacts::new(db).relations().references(position, config) + }) + } + + pub fn prepare_call_hierarchy( + &self, + position: FilePosition, + ) -> Cancellable>> { + self.with_db(|db| call_hierarchy::prepare(db, position)) + } + + pub fn call_hierarchy_incoming( + &self, + item: CallHierarchyItem, + config: ReferencesConfig, + ) -> Cancellable>> { + self.with_db(|db| call_hierarchy::incoming(db, item, config)) + } + + pub fn call_hierarchy_outgoing( + &self, + item: CallHierarchyItem, + config: ReferencesConfig, + ) -> Cancellable>> { + self.with_db(|db| call_hierarchy::outgoing(db, item, config)) } pub fn module_incoming_calls( @@ -206,7 +236,11 @@ impl Analysis { position: FilePosition, config: RenameConfig, ) -> Cancellable> { - self.with_db(|db| rename::prepare_rename(db, position, config)) + self.with_db(|db| { + SemanticFacts::new(db) + .edit_plan(EditRequest::PrepareRename { position, config }) + .map(EditPlan::into_prepare_rename) + }) } pub fn rename( @@ -215,15 +249,23 @@ impl Analysis { config: RenameConfig, new_name: &str, ) -> Cancellable> { - self.with_db(|db| rename::rename(db, position, config, new_name)) + self.with_db(|db| { + SemanticFacts::new(db) + .edit_plan(EditRequest::Rename { position, config, new_name }) + .map(EditPlan::into_source_change) + }) } pub fn rename_expansion_info( &self, position: FilePosition, config: RenameConfig, - ) -> Cancellable> { - self.with_db(|db| rename::rename_expansion_info(db, position, config)) + ) -> Cancellable> { + self.with_db(|db| { + SemanticFacts::new(db) + .edit_plan(EditRequest::RenameExpansionInfo { position, config }) + .map(EditPlan::into_rename_expansion_info) + }) } pub fn expanded_rename( @@ -232,7 +274,11 @@ impl Analysis { config: RenameConfig, new_name: &str, ) -> Cancellable> { - self.with_db(|db| rename::expanded_rename(db, position, config, new_name)) + self.with_db(|db| { + SemanticFacts::new(db) + .edit_plan(EditRequest::ExpandedRename { position, config, new_name }) + .map(EditPlan::into_source_change) + }) } pub fn rename_conflict_info( @@ -241,8 +287,17 @@ impl Analysis { config: RenameConfig, new_name: &str, recursive: bool, - ) -> Cancellable> { - self.with_db(|db| rename::rename_conflict_info(db, position, config, new_name, recursive)) + ) -> Cancellable> { + self.with_db(|db| { + SemanticFacts::new(db) + .edit_plan(EditRequest::RenameConflictInfo { + position, + config, + new_name, + recursive, + }) + .map(EditPlan::into_rename_conflict_info) + }) } pub fn format( diff --git a/crates/ide/src/call_hierarchy.rs b/crates/ide/src/call_hierarchy.rs new file mode 100644 index 00000000..5b03f9c1 --- /dev/null +++ b/crates/ide/src/call_hierarchy.rs @@ -0,0 +1,158 @@ +use itertools::Itertools; + +use crate::{ + FilePosition, FileRange, SymbolKind, + db::root_db::RootDb, + facts::{ + SemanticFacts, + relation::{CallSymbolKey, RelationFacts, RelationKind, RelationQuery}, + symbol::{SymbolId, SymbolInfo}, + }, + references::ReferencesConfig, +}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CallHierarchyItem { + pub symbol: Option, + pub name: String, + pub kind: SymbolKind, + pub detail: Option, + pub full_range: FileRange, + pub selection_range: FileRange, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct IncomingCall { + pub from: CallHierarchyItem, + pub from_ranges: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct OutgoingCall { + pub to: CallHierarchyItem, + pub from_ranges: Vec, +} + +pub(crate) fn prepare(db: &RootDb, position: FilePosition) -> Option> { + let facts = SemanticFacts::new(db); + let relations = facts.relations(); + let items = relations + .definition_symbols(position)? + .into_iter() + .filter_map(|symbol| call_hierarchy_item_for_symbol(&relations, symbol)) + .unique_by(call_hierarchy_item_key) + .collect_vec(); + + (!items.is_empty()).then_some(items) +} + +pub(crate) fn incoming( + db: &RootDb, + target: CallHierarchyItem, + config: ReferencesConfig, +) -> Option> { + let facts = SemanticFacts::new(db); + let relations = facts.relations(); + let target = symbol_for_item(&relations, &target)?; + let relation_set = relations.relations(RelationQuery::Incoming { + target: target.id, + kind: RelationKind::Instantiates, + config, + }); + let mut groups = Vec::<(SymbolId, Vec)>::new(); + for relation in relation_set.relations { + push_call_range(&mut groups, relation.source, relation.range); + } + + let calls: Vec<_> = groups + .into_iter() + .filter_map(|(source, from_ranges)| { + let from = call_hierarchy_item_for_symbol(&relations, relations.symbol(source)?)?; + Some(IncomingCall { from, from_ranges }) + }) + .collect(); + (!calls.is_empty()).then_some(calls) +} + +pub(crate) fn outgoing( + db: &RootDb, + caller: CallHierarchyItem, + config: ReferencesConfig, +) -> Option> { + let facts = SemanticFacts::new(db); + let relations = facts.relations(); + let caller = symbol_for_item(&relations, &caller)?; + let relation_set = relations.relations(RelationQuery::Outgoing { + source: caller.id, + kind: RelationKind::Instantiates, + config, + }); + let mut groups = Vec::<(SymbolId, Vec)>::new(); + for relation in relation_set.relations { + push_call_range(&mut groups, relation.target, relation.range); + } + + let calls: Vec<_> = groups + .into_iter() + .filter_map(|(target, from_ranges)| { + let to = call_hierarchy_item_for_symbol(&relations, relations.symbol(target)?)?; + Some(OutgoingCall { to, from_ranges }) + }) + .collect(); + (!calls.is_empty()).then_some(calls) +} + +fn symbol_for_item(relations: &RelationFacts<'_>, item: &CallHierarchyItem) -> Option { + if let Some(symbol) = item.symbol { + return relations.symbol(symbol); + } + relations.module_symbol_for_item(CallSymbolKey { + full_range: item.full_range, + selection_range: item.selection_range, + }) +} + +fn call_hierarchy_item_for_symbol( + relations: &RelationFacts<'_>, + symbol: SymbolInfo, +) -> Option { + let kind = symbol.kind; + if !is_call_hierarchy_kind(kind) { + return None; + } + + let full_range = symbol.definition_range?; + let selection_range = symbol.selection_range.unwrap_or(full_range); + let name = symbol.name.map(|name| name.to_string()).unwrap_or_else(|| "".to_owned()); + let detail = symbol + .container + .and_then(|container| relations.symbol(container)) + .and_then(|container| container.name.map(|name| name.to_string())); + Some(CallHierarchyItem { + symbol: Some(symbol.id), + name, + kind, + detail, + full_range, + selection_range, + }) +} + +fn is_call_hierarchy_kind(kind: SymbolKind) -> bool { + matches!(kind, SymbolKind::Module) +} + +fn push_call_range(groups: &mut Vec<(SymbolId, Vec)>, item: SymbolId, range: FileRange) { + if let Some((_, ranges)) = groups.iter_mut().find(|(existing, _)| *existing == item) { + if !ranges.contains(&range) { + ranges.push(range); + } + return; + } + + groups.push((item, vec![range])); +} + +fn call_hierarchy_item_key(item: &CallHierarchyItem) -> (String, FileRange, FileRange) { + (item.name.clone(), item.full_range, item.selection_range) +} diff --git a/crates/ide/src/definitions.rs b/crates/ide/src/definitions.rs index 4c7fed91..cb6092d8 100644 --- a/crates/ide/src/definitions.rs +++ b/crates/ide/src/definitions.rs @@ -1,41 +1,20 @@ use hir::{ - base_db::intern::Lookup, - container::{ContainerId, InContainer, InFile, InModule, InSubroutine}, + container::{ContainerId, InContainer, InModule}, db::HirDb, file::HirFileId, - hir_def::{ - block::{BlockId, BlockLoc}, - expr::declarator::DeclId, - file::{config::ConfigDeclId, library::LibraryDeclId, udp::UdpDeclId}, - module::{ - ModuleId, - generate::{GenerateBlockId, GenerateBlockLoc}, - instantiation::InstanceId, - port::NonAnsiPortId, - }, - stmt::StmtId, - subroutine::{SubroutineId, SubroutinePortId}, - typedef::TypedefId, - }, semantics::{Semantics, pathres::PathResolution}, - source_map::{IsNamedSrc, IsSrc, ToAstNode}, }; use smallvec::{SmallVec, smallvec}; -use smol_str::SmolStr; use syntax::{ SyntaxAncestors, SyntaxToken, SyntaxTokenWithParent, ast::{self, AstNode}, has_name::HasName, - has_text_range::{HasTextRange, HasTextRangeIn}, match_ast, token::TokenKindExt, }; -use utils::{ - get::{Get, GetRef}, - impl_from, - line_index::TextRange, -}; +use utils::impl_from; +pub use crate::facts::symbol::DefinitionOrigin; use crate::{ db::root_db::RootDb, module_resolution::{ @@ -44,256 +23,6 @@ use crate::{ }, }; -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] -pub enum DefinitionOrigin { - ModuleId(ModuleId), - Config(InFile), - Library(InFile), - Udp(InFile), - BlockId(BlockId), - GenerateBlockId(GenerateBlockId), - SubroutineId(SubroutineId), - SubroutinePort(InSubroutine), - - NonAnsiPort(InModule), - Decl(InContainer), - Typedef(InContainer), - Instance(InModule), - Stmt(InContainer), -} - -impl_from! { DefinitionOrigin => - ModuleId, - Config(InFile), - Library(InFile), - Udp(InFile), - BlockId, - GenerateBlockId, - SubroutineId, - SubroutinePort(InSubroutine), - NonAnsiPort(InModule), - Decl(InContainer), - Typedef(InContainer), - Instance(InModule), - Stmt(InContainer), -} - -impl DefinitionOrigin { - #[inline] - pub fn container_id(&self, db: &dyn HirDb) -> ContainerId { - match *self { - DefinitionOrigin::ModuleId(InFile { file_id, .. }) => file_id.into(), - DefinitionOrigin::Config(InFile { file_id, .. }) => file_id.into(), - DefinitionOrigin::Library(InFile { file_id, .. }) => file_id.into(), - DefinitionOrigin::Udp(InFile { file_id, .. }) => file_id.into(), - DefinitionOrigin::BlockId(block_id) => block_id.lookup(db).cont_id, - DefinitionOrigin::GenerateBlockId(generate_block_id) => { - generate_block_id.lookup(db).cont_id - } - DefinitionOrigin::SubroutineId(subroutine_id) => { - subroutine_id.lookup(db).cont_id.into() - } - DefinitionOrigin::SubroutinePort(InSubroutine { subroutine, .. }) => { - ContainerId::SubroutineId(subroutine) - } - DefinitionOrigin::NonAnsiPort(InModule { module_id, .. }) => module_id.into(), - DefinitionOrigin::Decl(InContainer { cont_id, .. }) => cont_id, - DefinitionOrigin::Typedef(InContainer { cont_id, .. }) => cont_id, - DefinitionOrigin::Instance(InModule { module_id, .. }) => module_id.into(), - DefinitionOrigin::Stmt(InContainer { cont_id, .. }) => cont_id, - } - } - - pub fn name(&self, db: &dyn HirDb) -> Option { - match *self { - DefinitionOrigin::ModuleId(InFile { value, file_id }) => { - file_id.to_container(db).get(value).name.clone() - } - DefinitionOrigin::Config(InFile { value, file_id }) => { - file_id.to_container(db).get(value).name.clone() - } - DefinitionOrigin::Library(InFile { value, file_id }) => { - file_id.to_container(db).get(value).name.clone() - } - DefinitionOrigin::Udp(InFile { value, file_id }) => { - file_id.to_container(db).get(value).name.clone() - } - DefinitionOrigin::BlockId(block_id) => { - let BlockLoc { cont_id, src: InFile { value, file_id: _ } } = block_id.lookup(db); - let cont = cont_id.to_container(db); - value.hir(&cont, &cont_id.to_container_src_map(db))?.name.clone() - } - DefinitionOrigin::GenerateBlockId(generate_block_id) => { - db.generate_block(generate_block_id).name.clone() - } - DefinitionOrigin::SubroutineId(subroutine_id) => { - db.subroutine(subroutine_id).name.clone() - } - DefinitionOrigin::SubroutinePort(InSubroutine { subroutine, value }) => { - db.subroutine(subroutine).ports.get(value.0 as usize)?.name.clone() - } - DefinitionOrigin::NonAnsiPort(InModule { value, module_id }) => { - module_id.to_container(db).get(value).label.clone() - } - DefinitionOrigin::Decl(InContainer { value, cont_id }) => { - cont_id.to_container(db).get(value).name.clone() - } - DefinitionOrigin::Typedef(InContainer { value, cont_id }) => { - cont_id.to_container(db).get(value).name.clone() - } - DefinitionOrigin::Instance(InModule { value, module_id }) => { - module_id.to_container(db).get(value).name.clone() - } - DefinitionOrigin::Stmt(InContainer { value, cont_id }) => { - cont_id.to_container(db).get(value).label.clone() - } - } - } - - pub fn name_range(&self, db: &dyn HirDb) -> Option> { - match *self { - DefinitionOrigin::ModuleId(InFile { value, file_id }) => { - let range = file_id.to_container_src_map(db).get(value)?.name_range()?; - Some(InFile::new(file_id, range)) - } - DefinitionOrigin::Config(InFile { value, file_id }) => { - let range = file_id.to_container_src_map(db).get(value)?.name_range()?; - Some(InFile::new(file_id, range)) - } - DefinitionOrigin::Library(InFile { value, file_id }) => { - let range = file_id.to_container_src_map(db).get(value)?.name_range()?; - Some(InFile::new(file_id, range)) - } - DefinitionOrigin::Udp(InFile { value, file_id }) => { - let range = file_id.to_container_src_map(db).get(value)?.name_range()?; - Some(InFile::new(file_id, range)) - } - DefinitionOrigin::BlockId(block_id) => { - let BlockLoc { src: InFile { value, file_id }, .. } = block_id.lookup(db); - let range = value.name_range()?; - Some(InFile::new(file_id, range)) - } - DefinitionOrigin::GenerateBlockId(generate_block_id) => { - let GenerateBlockLoc { src: InFile { value, file_id }, .. } = - generate_block_id.lookup(db); - let range = value.name_range()?; - Some(InFile::new(file_id, range)) - } - DefinitionOrigin::SubroutineId(subroutine_id) => { - let src = subroutine_id.lookup(db).src; - Some(InFile::new(src.file_id, src.value.name_or_full_range())) - } - DefinitionOrigin::SubroutinePort(InSubroutine { subroutine, value }) => { - let src = subroutine.lookup(db).src; - let tree = db.parse(src.file_id); - let func = src.value.to_node(&tree)?; - let ports = func - .prototype() - .port_list() - .map(|ports| ports.ports().children().collect::>()) - .unwrap_or_default(); - let port = ports - .into_iter() - .nth(value.0 as usize) - .and_then(|port| port.as_function_port())?; - let declarator = port.declarator(); - let range = declarator.name()?.text_range_in(declarator.syntax())?; - Some(InFile::new(src.file_id, range)) - } - DefinitionOrigin::NonAnsiPort(InModule { value, module_id }) => { - let range = module_id.to_container_src_map(db).get(value)?.name_range()?; - Some(InFile::new(module_id.file_id, range)) - } - DefinitionOrigin::Decl(InContainer { value, cont_id }) => { - let range = cont_id.to_container_src_map(db).get(value)?.name_range()?; - Some(InFile::new(cont_id.file_id(db).into(), range)) - } - DefinitionOrigin::Typedef(InContainer { value, cont_id }) => { - let range = cont_id.to_container_src_map(db).get(value)?.name_range()?; - Some(InFile::new(cont_id.file_id(db).into(), range)) - } - DefinitionOrigin::Instance(InModule { value, module_id }) => { - let range = module_id.to_container_src_map(db).get(value)?.name_range()?; - Some(InFile::new(module_id.file_id, range)) - } - DefinitionOrigin::Stmt(InContainer { value, cont_id }) => { - let range = cont_id.to_container_src_map(db).get(value)?.name_range()?; - Some(InFile::new(cont_id.file_id(db).into(), range)) - } - } - } - - pub fn range(&self, db: &dyn HirDb) -> Option> { - Some(match *self { - DefinitionOrigin::ModuleId(InFile { value, file_id }) => { - let range = file_id.to_container_src_map(db).get(value)?.range(); - InFile::new(file_id, range) - } - DefinitionOrigin::Config(InFile { value, file_id }) => { - let range = file_id.to_container_src_map(db).get(value)?.range(); - InFile::new(file_id, range) - } - DefinitionOrigin::Library(InFile { value, file_id }) => { - let range = file_id.to_container_src_map(db).get(value)?.range(); - InFile::new(file_id, range) - } - DefinitionOrigin::Udp(InFile { value, file_id }) => { - let range = file_id.to_container_src_map(db).get(value)?.range(); - InFile::new(file_id, range) - } - DefinitionOrigin::BlockId(block_id) => { - let BlockLoc { src: InFile { value, file_id }, .. } = block_id.lookup(db); - let range = value.range(); - InFile::new(file_id, range) - } - DefinitionOrigin::GenerateBlockId(generate_block_id) => { - let GenerateBlockLoc { src: InFile { value, file_id }, .. } = - generate_block_id.lookup(db); - let range = value.range(); - InFile::new(file_id, range) - } - DefinitionOrigin::SubroutineId(subroutine_id) => { - let src = subroutine_id.lookup(db).src; - let range = src.value.range(); - InFile::new(src.file_id, range) - } - DefinitionOrigin::SubroutinePort(InSubroutine { subroutine, value }) => { - let src = subroutine.lookup(db).src; - let tree = db.parse(src.file_id); - let func = src.value.to_node(&tree)?; - let ports = func.prototype().port_list()?; - let port = ports - .ports() - .children() - .nth(value.0 as usize) - .and_then(|port| port.as_function_port())?; - let range = port.syntax().text_range()?; - InFile::new(src.file_id, range) - } - DefinitionOrigin::NonAnsiPort(InModule { value, module_id }) => { - let range = module_id.to_container_src_map(db).get(value)?.range(); - InFile::new(module_id.file_id, range) - } - DefinitionOrigin::Decl(InContainer { value, cont_id }) => { - let range = cont_id.to_container_src_map(db).get(value)?.range(); - InFile::new(cont_id.file_id(db).into(), range) - } - DefinitionOrigin::Typedef(InContainer { value, cont_id }) => { - let range = cont_id.to_container_src_map(db).get(value)?.range(); - InFile::new(cont_id.file_id(db).into(), range) - } - DefinitionOrigin::Instance(InModule { value, module_id }) => { - let range = module_id.to_container_src_map(db).get(value)?.range(); - InFile::new(module_id.file_id, range) - } - DefinitionOrigin::Stmt(InContainer { value, cont_id }) => { - let range = cont_id.to_container_src_map(db).get(value)?.range(); - InFile::new(cont_id.file_id(db).into(), range) - } - }) - } -} - // Definition may have multiple origins, e.g. non-ansi port #[derive(Debug, Clone, PartialEq, Eq)] pub struct Definition(pub PathResolution); diff --git a/crates/ide/src/document_highlight.rs b/crates/ide/src/document_highlight.rs index 2dc8fb27..972adc1c 100644 --- a/crates/ide/src/document_highlight.rs +++ b/crates/ide/src/document_highlight.rs @@ -1,5 +1,5 @@ use hir::{container::InFile, file::HirFileId, semantics::Semantics}; -use syntax::{SyntaxTokenWithParent, TokenKind, token::TokenKindExt}; +use syntax::{SyntaxTokenWithParent, has_text_range::HasTextRange, token::pair_token}; use utils::line_index::TextRange; use vfs::FileId; @@ -7,11 +7,14 @@ use crate::{ FilePosition, ScopeVisibility, db::root_db::RootDb, definitions::{Definition, DefinitionClass}, + facts::{ + SemanticFacts, TargetQuery, + target::{SemanticTarget, TargetIntent}, + }, references::{ - self, ReferenceCategory, ReferencesConfig, + ReferenceCategory, ReferencesConfig, search::{ReferencesCtx, SearchScope}, }, - semantic_target::{SemanticTarget, TargetIntent, resolve_semantic_target}, }; #[derive(Debug, Clone)] @@ -33,7 +36,12 @@ pub(crate) fn document_highlight( let sema = Semantics::new(db); let hir_file_id = file_id.into(); let parsed_file = sema.parse_file(file_id); - let target = resolve_semantic_target(db, file_id, offset, parsed_file.root(), token_precedence); + let target = SemanticFacts::new(db).target_at(TargetQuery { + file_id, + offset, + intent: TargetIntent::Highlight, + root: parsed_file.root(), + }); let SemanticTarget::Source(target) = target.unique_for_intent(TargetIntent::Highlight)? else { return None; }; @@ -46,25 +54,19 @@ pub(crate) fn document_highlight( (!highlights.is_empty()).then_some(highlights) } -fn token_precedence(kind: TokenKind) -> usize { - match kind { - _ if kind.name_like() => 4, - _ if kind.is_pair_token() => 4, - _ => 1, - } -} - fn handle_ctrl_flow_kw( - sema: &Semantics<'_, RootDb>, - file_id: HirFileId, - tp: SyntaxTokenWithParent, + tp @ SyntaxTokenWithParent { .. }: SyntaxTokenWithParent, ) -> Option> { - let cur_file_id = file_id.file_id(); - let highlights = references::handle_ctrl_flow_kw(sema, file_id, tp)? + let pair = pair_token(tp)?; + let pair = pair.either(|token| token, |token| token); + let highlights = [tp, pair] .into_iter() - .filter_map(|mut r| r.refs.remove(&cur_file_id)) - .flatten() - .map(|(range, category)| DocumentHighlight { range, category }) + .filter_map(|token| { + Some(DocumentHighlight { + range: token.text_range()?, + category: ReferenceCategory::empty(), + }) + }) .collect(); Some(highlights) } @@ -76,7 +78,7 @@ fn highlight_for_token( token: SyntaxTokenWithParent, config: DocumentHighlightConfig, ) -> Option> { - handle_ctrl_flow_kw(sema, hir_file_id, token).or_else(|| { + handle_ctrl_flow_kw(token).or_else(|| { let def = match DefinitionClass::resolve(sema, hir_file_id, token)? { DefinitionClass::Definition(def) => def, DefinitionClass::PortConnShorthand { local, .. } => local, diff --git a/crates/ide/src/facts/edit.rs b/crates/ide/src/facts/edit.rs new file mode 100644 index 00000000..39314eda --- /dev/null +++ b/crates/ide/src/facts/edit.rs @@ -0,0 +1,411 @@ +use hir::{container::InFile, semantics::Semantics}; +use smol_str::SmolStr; +use utils::{line_index::TextRange, uniq_vec::UniqVec}; + +use crate::{ + FilePosition, FileRange, + db::root_db::RootDb, + definitions::{Definition, DefinitionOrigin}, + rename::{ + self, RecursiveRenameInfo, RenameCollisionInfo, RenameConfig, RenameResult, + ResolvedRenameTarget, + }, + source_change::SourceChange, +}; + +pub(crate) enum EditRequest<'a> { + PrepareRename { + position: FilePosition, + config: RenameConfig, + }, + Rename { + position: FilePosition, + config: RenameConfig, + new_name: &'a str, + }, + RenameExpansionInfo { + position: FilePosition, + config: RenameConfig, + }, + ExpandedRename { + position: FilePosition, + config: RenameConfig, + new_name: &'a str, + }, + RenameConflictInfo { + position: FilePosition, + config: RenameConfig, + new_name: &'a str, + recursive: bool, + }, +} + +pub(crate) enum EditPlan { + PrepareRename(RenamePreparePlan), + Rename(RenameEditPlan), + RenameExpansionInfo(RenameExpansionPlan), + RenameConflictInfo(RenameConflictPlan), +} + +#[derive(Debug, Clone)] +#[allow(dead_code)] +pub(crate) struct RenameTargetPlan { + pub(crate) range: TextRange, + pub(crate) selected_symbols: Vec, + pub(crate) related_symbols: Vec, +} + +#[derive(Debug, Clone)] +#[allow(dead_code)] +pub(crate) struct RenameSymbolPlan { + pub(crate) symbols: Vec, + pub(crate) definition_ranges: Vec, + pub(crate) reference_ranges: Vec, +} + +#[derive(Debug, Clone)] +#[allow(dead_code)] +pub(crate) struct RenameSymbolEditPlan { + pub(crate) symbol: RenameSymbolPlan, + pub(crate) change: SourceChange, +} + +#[derive(Debug, Clone)] +#[allow(dead_code)] +pub(crate) struct RenamePreparePlan { + pub(crate) target: RenameTargetPlan, + pub(crate) editable_range: TextRange, +} + +#[derive(Debug, Clone)] +#[allow(dead_code)] +pub(crate) struct RenameEditPlan { + pub(crate) target: RenameTargetPlan, + pub(crate) recursive: bool, + pub(crate) new_name: String, + pub(crate) symbols: Vec, + pub(crate) change: SourceChange, +} + +#[derive(Debug, Clone)] +#[allow(dead_code)] +pub(crate) struct RenameExpansionPlan { + pub(crate) target: RenameTargetPlan, + pub(crate) symbols: Vec, + pub(crate) info: RecursiveRenameInfo, +} + +#[derive(Debug, Clone)] +#[allow(dead_code)] +pub(crate) struct RenameConflictPlan { + pub(crate) target: RenameTargetPlan, + pub(crate) symbols: Vec, + pub(crate) info: RenameCollisionInfo, +} + +impl EditPlan { + pub(crate) fn into_prepare_rename(self) -> TextRange { + match self { + EditPlan::PrepareRename(plan) => plan.editable_range, + _ => unreachable!("edit request and edit plan variant should match"), + } + } + + pub(crate) fn into_source_change(self) -> SourceChange { + match self { + EditPlan::Rename(plan) => plan.change, + _ => unreachable!("edit request and edit plan variant should match"), + } + } + + pub(crate) fn into_rename_expansion_info(self) -> RecursiveRenameInfo { + match self { + EditPlan::RenameExpansionInfo(plan) => plan.info, + _ => unreachable!("edit request and edit plan variant should match"), + } + } + + pub(crate) fn into_rename_conflict_info(self) -> RenameCollisionInfo { + match self { + EditPlan::RenameConflictInfo(plan) => plan.info, + _ => unreachable!("edit request and edit plan variant should match"), + } + } +} + +pub(crate) fn edit_plan(db: &RootDb, request: EditRequest<'_>) -> RenameResult { + let facts = EditFacts { db }; + match request { + EditRequest::PrepareRename { position, config } => { + facts.prepare_rename(position, config).map(EditPlan::PrepareRename) + } + EditRequest::Rename { position, config, new_name } => { + facts.rename(position, config, new_name).map(EditPlan::Rename) + } + EditRequest::RenameExpansionInfo { position, config } => { + facts.rename_expansion_info(position, config).map(EditPlan::RenameExpansionInfo) + } + EditRequest::ExpandedRename { position, config, new_name } => { + facts.expanded_rename(position, config, new_name).map(EditPlan::Rename) + } + EditRequest::RenameConflictInfo { position, config, new_name, recursive } => facts + .rename_conflict_info(position, config, new_name, recursive) + .map(EditPlan::RenameConflictInfo), + } +} + +struct EditFacts<'db> { + db: &'db RootDb, +} + +impl<'db> EditFacts<'db> { + fn prepare_rename( + &self, + position @ FilePosition { file_id, .. }: FilePosition, + config: RenameConfig, + ) -> RenameResult { + let sema = Semantics::new(self.db); + let resolved = rename::resolve_rename_target(&sema, position)?; + let _ = config.references_config(self.db, &resolved.selected_def, file_id)?; + Ok(RenamePreparePlan { + editable_range: resolved.range, + target: rename_target_plan(&resolved), + }) + } + + fn rename( + &self, + position @ FilePosition { file_id, .. }: FilePosition, + config: RenameConfig, + new_name: &str, + ) -> RenameResult { + let sema = Semantics::new(self.db); + let resolved = rename::resolve_rename_target(&sema, position)?; + let refs = rename::references_for_definition( + self.db, + &sema, + file_id, + &config, + &resolved.selected_def, + )?; + let symbol = + self.symbol_edit_plan(&sema, &resolved.selected_def, new_name, None, &refs, &[])?; + Ok(RenameEditPlan { + target: rename_target_plan(&resolved), + recursive: false, + new_name: new_name.to_owned(), + change: symbol.change.clone(), + symbols: vec![symbol], + }) + } + + fn rename_expansion_info( + &self, + position: FilePosition, + config: RenameConfig, + ) -> RenameResult { + let sema = Semantics::new(self.db); + let resolved = rename::resolve_rename_target(&sema, position)?; + let target = rename_target_plan(&resolved); + let targets = rename::recursive_rename_targets( + self.db, + &sema, + position.file_id, + &config, + resolved.targets, + )?; + let symbols = targets + .iter() + .map(|target| self.symbol_plan(&target.def, &target.refs)) + .collect::>(); + + Ok(RenameExpansionPlan { + target, + info: RecursiveRenameInfo { additional_symbols: targets.len().saturating_sub(1) }, + symbols, + }) + } + + fn expanded_rename( + &self, + position: FilePosition, + config: RenameConfig, + new_name: &str, + ) -> RenameResult { + let sema = Semantics::new(self.db); + let resolved = rename::resolve_rename_target(&sema, position)?; + let target = rename_target_plan(&resolved); + let targets = rename::recursive_rename_targets( + self.db, + &sema, + position.file_id, + &config, + resolved.targets, + )?; + let rename_targets = rename_target_index(&targets); + let mut change = SourceChange::default(); + let mut symbols = Vec::new(); + + for target in &targets { + let symbol = self.symbol_edit_plan( + &sema, + &target.def, + new_name, + Some(&rename_targets), + &target.refs, + &target.same_name_refs, + )?; + merge_source_change(&mut change, symbol.change.clone())?; + symbols.push(symbol); + } + + Ok(RenameEditPlan { + target, + recursive: true, + new_name: new_name.to_owned(), + symbols, + change, + }) + } + + fn rename_conflict_info( + &self, + position: FilePosition, + config: RenameConfig, + new_name: &str, + recursive: bool, + ) -> RenameResult { + let sema = Semantics::new(self.db); + let resolved = rename::resolve_rename_target(&sema, position)?; + let target = rename_target_plan(&resolved); + let defs: Vec = if recursive { + rename::recursive_rename_targets( + self.db, + &sema, + position.file_id, + &config, + resolved.targets, + )? + .into_iter() + .map(|target| target.def) + .collect() + } else { + vec![resolved.selected_def] + }; + + let new_name = SmolStr::new(new_name); + let mut target_index = UniqVec::<(), DefinitionOrigin>::default(); + for target in &defs { + target_index.push(target.origins(), ()); + } + let mut conflicts = UniqVec::::default(); + for collision in defs.iter().flat_map(|target| target.origins()).filter_map(|origin| { + sema.resolve_name(origin.container_id(self.db), &new_name).map(Definition::from) + }) { + if collision.origins().iter().any(|origin| target_index.contains(origin)) { + continue; + } + conflicts.push(collision.origins(), collision); + } + + Ok(RenameConflictPlan { + target, + symbols: definitions_symbols(&defs), + info: RenameCollisionInfo { conflicts: conflicts.len() }, + }) + } + + fn symbol_edit_plan( + &self, + sema: &Semantics<'_, RootDb>, + def: &Definition, + new_name: &str, + rename_targets: Option<&UniqVec<(), DefinitionOrigin>>, + refs: &rename::ReferenceSearchResult, + same_name_refs: &[rename::SameNameConnectionRef], + ) -> RenameResult { + let symbol = self.symbol_plan(def, refs); + let change = rename::rename_definition_with_refs( + self.db, + sema, + def, + new_name, + rename_targets, + refs, + same_name_refs, + )?; + Ok(RenameSymbolEditPlan { symbol, change }) + } + + fn symbol_plan( + &self, + def: &Definition, + refs: &rename::ReferenceSearchResult, + ) -> RenameSymbolPlan { + RenameSymbolPlan { + symbols: definition_symbols(def), + definition_ranges: definition_ranges(self.db, def), + reference_ranges: reference_ranges(refs), + } + } +} + +fn rename_target_plan(resolved: &ResolvedRenameTarget) -> RenameTargetPlan { + RenameTargetPlan { + range: resolved.range, + selected_symbols: definition_symbols(&resolved.selected_def), + related_symbols: definitions_symbols(&resolved.targets), + } +} + +fn rename_target_index(targets: &[rename::RecursiveRenameTarget]) -> UniqVec<(), DefinitionOrigin> { + let mut index = UniqVec::<(), DefinitionOrigin>::default(); + for target in targets { + index.push(target.def.origins(), ()); + } + index +} + +fn definitions_symbols(defs: &[Definition]) -> Vec { + let mut symbols = UniqVec::::default(); + for def in defs { + for symbol in def.origins() { + symbols.push_unique(symbol); + } + } + symbols.into_vec() +} + +fn definition_symbols(def: &Definition) -> Vec { + definitions_symbols(std::slice::from_ref(def)) +} + +fn definition_ranges(db: &RootDb, def: &Definition) -> Vec { + let mut ranges = UniqVec::::default(); + for origin in def.origins() { + let Some(InFile { file_id, value: range }) = origin.name_range(db) else { + continue; + }; + ranges.push_unique(FileRange { file_id: file_id.file_id(), range }); + } + ranges.into_vec() +} + +fn reference_ranges(refs: &rename::ReferenceSearchResult) -> Vec { + let mut ranges = UniqVec::::default(); + for (&file_id, refs) in refs { + for reference in refs { + ranges.push_unique(FileRange { file_id, range: reference.range() }); + } + } + ranges.into_vec() +} + +fn merge_source_change(target: &mut SourceChange, change: SourceChange) -> RenameResult<()> { + for (file_id, edit) in change.text_edits { + target + .insert_text_edit(file_id, edit) + .map_err(|_| rename::RenameError::OverlappingEdits)?; + } + Ok(()) +} diff --git a/crates/ide/src/facts/mod.rs b/crates/ide/src/facts/mod.rs new file mode 100644 index 00000000..2726969f --- /dev/null +++ b/crates/ide/src/facts/mod.rs @@ -0,0 +1,42 @@ +use crate::db::root_db::RootDb; + +pub(crate) mod edit; +pub(crate) mod relation; +pub(crate) mod source_target; +pub(crate) mod symbol; +pub(crate) mod target; + +pub(crate) use target::TargetQuery; + +pub(crate) struct SemanticFacts<'db> { + db: &'db RootDb, +} + +impl<'db> SemanticFacts<'db> { + pub(crate) fn new(db: &'db RootDb) -> Self { + Self { db } + } + + pub(crate) fn target_at<'tree>( + &self, + query: TargetQuery<'tree>, + ) -> target::TargetResolution<'tree> { + target::target_at(self.db, query) + } + + pub(crate) fn relations(&self) -> relation::RelationFacts<'db> { + relation::RelationFacts::new(self.db) + } + + #[allow(dead_code)] + pub(crate) fn symbol(&self, id: symbol::SymbolId) -> Option { + id.info(self.db) + } + + pub(crate) fn edit_plan( + &self, + request: edit::EditRequest<'_>, + ) -> crate::rename::RenameResult { + edit::edit_plan(self.db, request) + } +} diff --git a/crates/ide/src/facts/relation.rs b/crates/ide/src/facts/relation.rs new file mode 100644 index 00000000..6de47107 --- /dev/null +++ b/crates/ide/src/facts/relation.rs @@ -0,0 +1,578 @@ +use hir::{ + base_db::source_db::SourceDb, + db::HirDb, + file::HirFileId, + hir_def::{file::FileItem, module::ModuleId}, + preproc::{ + MacroDefinition, MacroParamDefinition, MacroReferenceIndexStatus, macro_param_references, + macro_references, + }, + semantics::Semantics, +}; +use itertools::Itertools; +use nohash_hasher::IntMap; +use syntax::{has_text_range::HasTextRange, token::pair_token}; +use utils::line_index::TextRange; +use vfs::FileId; + +use crate::{ + FilePosition, FileRange, RangeInfo, + db::root_db::RootDb, + definitions::{Definition, DefinitionClass}, + facts::{ + SemanticFacts, TargetQuery, + symbol::{SymbolId, SymbolInfo}, + target::{PreprocMacroTarget, SemanticTarget, SourceTarget, TargetIntent}, + }, + goto_definition, + navigation_target::{NavTarget, ToNav}, + references::{ + ReferenceCategory, References, ReferencesConfig, ReferencesPartialReason, ReferencesStatus, + search::ReferencesCtx, + }, +}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[allow(dead_code)] +pub(crate) enum RelationKind { + Defines, + References, + Contains, + MemberOf, + Instantiates, + Imports, + ExpandsFrom, + Includes, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub(crate) struct Relation { + pub kind: RelationKind, + pub source: SymbolId, + pub target: SymbolId, + pub range: FileRange, +} + +#[derive(Debug, Clone)] +#[allow(dead_code)] +pub(crate) enum RelationQuery { + Incoming { target: SymbolId, kind: RelationKind, config: ReferencesConfig }, + Outgoing { source: SymbolId, kind: RelationKind, config: ReferencesConfig }, + Workspace { kind: RelationKind, config: ReferencesConfig }, + At { position: FilePosition, kind: RelationKind, config: ReferencesConfig }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct RelationSet { + pub relations: Vec, + pub target_symbols: Vec, + pub reference_status: ReferencesStatus, +} + +impl Default for RelationSet { + fn default() -> Self { + Self { + relations: Vec::new(), + target_symbols: Vec::new(), + reference_status: ReferencesStatus::Complete, + } + } +} + +pub(crate) struct RelationFacts<'db> { + db: &'db RootDb, +} + +impl<'db> RelationFacts<'db> { + pub(crate) fn new(db: &'db RootDb) -> Self { + Self { db } + } + + pub(crate) fn definition_targets( + &self, + position: FilePosition, + ) -> Option>> { + goto_definition::goto_definition(self.db, position) + } + + pub(crate) fn references( + &self, + position: FilePosition, + config: ReferencesConfig, + ) -> Option> { + let relations = + self.relations(RelationQuery::At { position, kind: RelationKind::References, config }); + self.references_from_relations(relations) + } + + pub(crate) fn reference_ranges( + &self, + position: FilePosition, + config: ReferencesConfig, + ) -> Vec { + self.references(position, config) + .into_iter() + .flatten() + .flat_map(|References { refs, .. }| { + refs.into_iter().flat_map(|(file_id, refs)| { + refs.into_iter().map(move |(range, _)| FileRange { file_id, range }) + }) + }) + .unique() + .collect() + } + + pub(crate) fn relations(&self, query: RelationQuery) -> RelationSet { + match query { + RelationQuery::Incoming { target, kind, config } => { + let mut set = self.workspace_relations(kind, config); + set.relations.retain(|relation| relation.target == target); + set + } + RelationQuery::Outgoing { source, kind, config } => { + let mut set = self.workspace_relations(kind, config); + set.relations.retain(|relation| relation.source == source); + set + } + RelationQuery::Workspace { kind, config } => self.workspace_relations(kind, config), + RelationQuery::At { position, kind, config } => { + self.relations_at(position, kind, config).unwrap_or_default() + } + } + } + + pub(crate) fn symbol(&self, id: SymbolId) -> Option { + id.info(self.db) + } + + pub(crate) fn definition_symbols(&self, position: FilePosition) -> Option> { + let nav_info = self.definition_targets(position)?; + let module_symbols = self.module_symbols(); + let symbols = nav_info + .info + .into_iter() + .filter_map(|target| { + module_symbols.iter().find(|symbol| nav_matches_symbol(&target, symbol)).cloned() + }) + .unique_by(|symbol| symbol.id) + .collect_vec(); + (!symbols.is_empty()).then_some(symbols) + } + + pub(crate) fn module_symbol_for_item(&self, item: CallSymbolKey) -> Option { + self.module_symbols().into_iter().find(|symbol| { + symbol.definition_range == Some(item.full_range) + && symbol.selection_range == Some(item.selection_range) + }) + } + + fn workspace_relations(&self, kind: RelationKind, config: ReferencesConfig) -> RelationSet { + match kind { + RelationKind::Instantiates => self.instantiation_relations(config), + _ => RelationSet::default(), + } + } + + fn relations_at( + &self, + position: FilePosition, + kind: RelationKind, + config: ReferencesConfig, + ) -> Option { + match kind { + RelationKind::References => self.reference_relations(position, config), + _ => None, + } + } + + fn reference_relations( + &self, + FilePosition { file_id, offset }: FilePosition, + config: ReferencesConfig, + ) -> Option { + let sema = Semantics::new(self.db); + let parsed_file = sema.parse_file(file_id); + let target = SemanticFacts::new(self.db).target_at(TargetQuery { + file_id, + offset, + intent: TargetIntent::FindReferences, + root: parsed_file.root(), + }); + + match target.unique_for_intent(TargetIntent::FindReferences)? { + SemanticTarget::Source(target) => { + self.source_reference_relations(&sema, file_id.into(), target, config) + } + SemanticTarget::PreprocMacro(target) => { + self.preproc_reference_relations(file_id, target, config) + } + SemanticTarget::Include(_) => None, + } + } + + fn preproc_reference_relations( + &self, + file_id: FileId, + target: PreprocMacroTarget, + config: ReferencesConfig, + ) -> Option { + match target { + PreprocMacroTarget::ParamDefinition(definition) => { + self.macro_param_reference_relations(file_id, definition, &config) + } + PreprocMacroTarget::ParamReference(resolution) => { + self.collect_reference_sets(resolution.definitions.into_iter().filter_map( + |definition| self.macro_param_reference_relations(file_id, definition, &config), + )) + } + PreprocMacroTarget::Definition(definition) => { + self.macro_reference_relations(file_id, definition, &config) + } + PreprocMacroTarget::Reference(resolution) => { + self.collect_reference_sets(resolution.definitions.into_iter().filter_map( + |definition| self.macro_reference_relations(file_id, definition, &config), + )) + } + } + } + + fn macro_param_reference_relations( + &self, + file_id: FileId, + definition: MacroParamDefinition, + config: &ReferencesConfig, + ) -> Option { + let target = SymbolId::preproc_macro_param(&definition); + let relations = macro_param_references(self.db, file_id, &definition) + .ok()? + .references + .into_iter() + .filter(|usage| reference_range_allowed(config, usage.file_id, usage.range)) + .map(|usage| Relation { + kind: RelationKind::References, + source: target, + target, + range: FileRange { file_id: usage.file_id, range: usage.range }, + }) + .unique() + .collect::>(); + Some(RelationSet { + relations, + target_symbols: vec![target], + reference_status: ReferencesStatus::Complete, + }) + } + + fn macro_reference_relations( + &self, + file_id: FileId, + definition: MacroDefinition, + config: &ReferencesConfig, + ) -> Option { + let target = SymbolId::preproc_macro(&definition); + let references = macro_references(self.db, file_id, &definition).ok()?; + let relations = references + .references + .into_iter() + .filter(|usage| reference_range_allowed(config, usage.file_id, usage.range)) + .map(|usage| Relation { + kind: RelationKind::References, + source: target, + target, + range: FileRange { file_id: usage.file_id, range: usage.range }, + }) + .unique() + .collect::>(); + Some(RelationSet { + relations, + target_symbols: vec![target], + reference_status: references_status_from_macro_index(references.status), + }) + } + + fn collect_reference_sets( + &self, + sets: impl IntoIterator, + ) -> Option { + let mut relations = Vec::new(); + let mut target_symbols = Vec::new(); + let mut status = ReferencesStatus::Complete; + for set in sets { + relations.extend(set.relations); + target_symbols.extend(set.target_symbols); + status = merge_reference_status(status, set.reference_status); + } + (!relations.is_empty() || !target_symbols.is_empty()).then_some(RelationSet { + relations: relations.into_iter().unique().collect(), + target_symbols: target_symbols.into_iter().unique().collect(), + reference_status: status, + }) + } + + fn source_reference_relations( + &self, + sema: &Semantics<'_, RootDb>, + file_id: HirFileId, + target: SourceTarget<'_>, + config: ReferencesConfig, + ) -> Option { + let mut relations = Vec::new(); + let mut target_symbols = Vec::new(); + for token in target.into_tokens() { + if let Some(reference_set) = self.control_flow_reference_relations(file_id, token) { + relations.extend(reference_set.relations); + target_symbols.extend(reference_set.target_symbols); + continue; + } + let def = match DefinitionClass::resolve(sema, file_id, token)? { + DefinitionClass::Definition(def) => def, + DefinitionClass::PortConnShorthand { local, .. } => local, + DefinitionClass::Ambiguous(_) => return None, + }; + let relation_set = + self.reference_relations_for_definition(sema, &def, config.clone())?; + relations.extend(relation_set.relations); + target_symbols.extend(relation_set.target_symbols); + } + + (!relations.is_empty() || !target_symbols.is_empty()).then_some(RelationSet { + relations: relations.into_iter().unique().collect(), + target_symbols: target_symbols.into_iter().unique().collect(), + reference_status: ReferencesStatus::Complete, + }) + } + + fn control_flow_reference_relations( + &self, + file_id: HirFileId, + token: syntax::SyntaxTokenWithParent<'_>, + ) -> Option { + let target = + SymbolId::SourceToken { file_id: file_id.file_id(), range: token.text_range()? }; + let pair = pair_token(token)?; + let pair = pair.either(|token| token, |token| token); + let relations = [token, pair] + .into_iter() + .filter_map(|token| token.text_range()) + .map(|range| Relation { + kind: RelationKind::References, + source: target, + target, + range: FileRange { file_id: file_id.file_id(), range }, + }) + .unique() + .collect::>(); + Some(RelationSet { + relations, + target_symbols: vec![target], + reference_status: ReferencesStatus::Complete, + }) + } + + fn reference_relations_for_definition( + &self, + sema: &Semantics<'_, RootDb>, + def: &Definition, + config: ReferencesConfig, + ) -> Option { + let origins = def.origins(); + let [target] = origins.as_slice() else { + return None; + }; + let target = *target; + let relations = ReferencesCtx::new(sema, def, config) + .search() + .into_iter() + .flat_map(|(file_id, refs)| { + refs.into_iter().map(move |reference| Relation { + kind: RelationKind::References, + source: target, + target, + range: FileRange { file_id, range: reference.range() }, + }) + }) + .unique() + .collect::>(); + + Some(RelationSet { + relations, + target_symbols: vec![target], + reference_status: ReferencesStatus::Complete, + }) + } + + fn references_from_relations(&self, set: RelationSet) -> Option> { + let mut grouped = set + .target_symbols + .into_iter() + .unique() + .map(|target| (target, IntMap::default())) + .collect::>)>>(); + for relation in set.relations { + let Some((_, refs)) = grouped.iter_mut().find(|(target, _)| *target == relation.target) + else { + let mut refs = IntMap::default(); + refs.insert( + relation.range.file_id, + vec![(relation.range.range, ReferenceCategory::empty())], + ); + grouped.push((relation.target, refs)); + continue; + }; + refs.entry(relation.range.file_id) + .or_default() + .push((relation.range.range, ReferenceCategory::empty())); + } + + let references = grouped + .into_iter() + .filter_map(|(target, refs)| { + let def = if matches!(target, SymbolId::SourceToken { .. }) { + None + } else { + Some(vec![target.to_nav(self.db)?]) + }; + Some(References { def, refs, status: set.reference_status }) + }) + .collect_vec(); + (!references.is_empty()).then_some(references) + } + + fn instantiation_relations(&self, config: ReferencesConfig) -> RelationSet { + let modules = self.module_symbols(); + let mut relations = Vec::new(); + + for target in &modules { + let Some(selection_range) = target.selection_range else { + continue; + }; + let position = FilePosition { + file_id: selection_range.file_id, + offset: selection_range.range.start(), + }; + + for reference in self.reference_ranges(position, config.clone()) { + if reference == selection_range { + continue; + } + let Some(source) = enclosing_module_symbol(&modules, reference) else { + continue; + }; + if source.id == target.id { + continue; + } + relations.push(Relation { + kind: RelationKind::Instantiates, + source: source.id, + target: target.id, + range: reference, + }); + } + } + + RelationSet { + relations: relations.into_iter().unique().collect(), + target_symbols: Vec::new(), + reference_status: ReferencesStatus::Complete, + } + } + + fn module_symbols(&self) -> Vec { + let mut symbols = Vec::new(); + let mut file_ids = self.file_ids(); + file_ids.sort_unstable_by_key(|file_id| file_id.0); + file_ids.dedup(); + + for file_id in file_ids { + let hir_file_id = HirFileId::File(file_id); + let (_file, src_map) = self.db.hir_file_with_source_map(hir_file_id); + for item in src_map.items.iter() { + if let FileItem::LocalModuleId(module_id) = *item { + let module_id = ModuleId::new(hir_file_id, module_id); + if let Some(symbol) = SymbolId::ModuleId(module_id).info(self.db) { + symbols.push(symbol); + } + } + } + } + + symbols + } + + pub(crate) fn file_ids(&self) -> Vec { + self.db.files().iter().copied().collect() + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct CallSymbolKey { + pub full_range: FileRange, + pub selection_range: FileRange, +} + +fn nav_matches_symbol(nav: &NavTarget, symbol: &SymbolInfo) -> bool { + symbol.kind == nav.kind.unwrap_or(symbol.kind) + && symbol.definition_range + == Some(FileRange { file_id: nav.file_id, range: nav.full_range }) + && symbol.selection_range + == Some(FileRange { file_id: nav.file_id, range: nav.focus_or_full_range() }) +} + +fn enclosing_module_symbol(symbols: &[SymbolInfo], range: FileRange) -> Option<&SymbolInfo> { + symbols + .iter() + .filter(|symbol| { + let Some(definition_range) = symbol.definition_range else { + return false; + }; + definition_range.file_id == range.file_id + && range_contains_range(definition_range.range, range.range) + }) + .min_by_key(|symbol| { + symbol.definition_range.map(|range| range.range.len()).unwrap_or_default() + }) +} + +fn range_contains_range( + container: utils::text_edit::TextRange, + range: utils::text_edit::TextRange, +) -> bool { + container.start() <= range.start() && range.end() <= container.end() +} + +fn reference_range_allowed(config: &ReferencesConfig, file_id: FileId, range: TextRange) -> bool { + config.search_scope.as_ref().is_none_or(|scope| { + scope.range_for_file(file_id).is_some_and(|scope_range| { + scope_range.is_none_or(|scope_range| scope_range.intersect(range).is_some()) + }) + }) +} + +fn references_status_from_macro_index(status: MacroReferenceIndexStatus) -> ReferencesStatus { + match status { + MacroReferenceIndexStatus::Complete => ReferencesStatus::Complete, + MacroReferenceIndexStatus::Partial { issue_count } => ReferencesStatus::Partial { + reason: ReferencesPartialReason::PreprocMacroIndex, + issue_count, + }, + } +} + +fn merge_reference_status(left: ReferencesStatus, right: ReferencesStatus) -> ReferencesStatus { + match (left, right) { + (ReferencesStatus::Complete, status) | (status, ReferencesStatus::Complete) => status, + ( + ReferencesStatus::Partial { + reason: ReferencesPartialReason::PreprocMacroIndex, + issue_count: left, + }, + ReferencesStatus::Partial { + reason: ReferencesPartialReason::PreprocMacroIndex, + issue_count: right, + }, + ) => ReferencesStatus::Partial { + reason: ReferencesPartialReason::PreprocMacroIndex, + issue_count: left + right, + }, + } +} diff --git a/crates/ide/src/source_targets.rs b/crates/ide/src/facts/source_target.rs similarity index 100% rename from crates/ide/src/source_targets.rs rename to crates/ide/src/facts/source_target.rs diff --git a/crates/ide/src/source_targets/macro_gate.rs b/crates/ide/src/facts/source_target/macro_gate.rs similarity index 100% rename from crates/ide/src/source_targets/macro_gate.rs rename to crates/ide/src/facts/source_target/macro_gate.rs diff --git a/crates/ide/src/source_targets/preproc.rs b/crates/ide/src/facts/source_target/preproc.rs similarity index 100% rename from crates/ide/src/source_targets/preproc.rs rename to crates/ide/src/facts/source_target/preproc.rs diff --git a/crates/ide/src/source_targets/tests.rs b/crates/ide/src/facts/source_target/tests.rs similarity index 95% rename from crates/ide/src/source_targets/tests.rs rename to crates/ide/src/facts/source_target/tests.rs index d5f6974f..f7dc16cb 100644 --- a/crates/ide/src/source_targets/tests.rs +++ b/crates/ide/src/facts/source_target/tests.rs @@ -11,7 +11,7 @@ mod cache; mod macro_gate; #[test] -fn source_targets_origin_source_range_hit_test_is_half_open() { +fn source_target_origin_source_range_hit_test_is_half_open() { let file_id = FileId(0); let range = TextRange::new(5.into(), 10.into()); let origin = Origin::File { file: file_id, range }; @@ -31,7 +31,7 @@ fn source_targets_origin_source_range_hit_test_is_half_open() { } #[test] -fn source_targets_source_token_range_mismatch_uses_original_syntax_hit() { +fn source_target_source_token_range_mismatch_uses_original_syntax_hit() { let (root, offset, parser_range) = root_and_offset("module m; wire payload_i; endmodule\n", "payload_i", 2); let file_id = FileId(0); @@ -58,7 +58,7 @@ fn source_targets_source_token_range_mismatch_uses_original_syntax_hit() { } #[test] -fn source_targets_macro_argument_selects_syntax_token_by_trace_identity() { +fn source_target_macro_argument_selects_syntax_token_by_trace_identity() { let db = RootDb::new(None); let model_file = FileId(0); let source = r#"`define ID(x) x @@ -123,7 +123,7 @@ endmodule } #[test] -fn source_targets_macro_argument_selects_only_the_hit_emitted_token() { +fn source_target_macro_argument_selects_only_the_hit_emitted_token() { let db = RootDb::new(None); let model_file = FileId(0); let source = r#"`define DUP(x) x x @@ -186,7 +186,7 @@ endmodule } #[test] -fn source_targets_preproc_owned_unresolved_does_not_use_normal_syntax_fallback() { +fn source_target_preproc_owned_unresolved_does_not_use_normal_syntax_fallback() { let (root, offset, parser_range) = root_and_offset("module m; wire payload_i; endmodule\n", "payload_i", 0); assert!( @@ -210,7 +210,7 @@ fn source_targets_preproc_owned_unresolved_does_not_use_normal_syntax_fallback() } #[test] -fn source_targets_normal_syntax_path_still_selects_non_preproc_offsets() { +fn source_target_normal_syntax_path_still_selects_non_preproc_offsets() { let (root, offset, parser_range) = root_and_offset("module m; wire payload_i; endmodule\n", "payload_i", 0); let SourceTargetProviderResult::Resolved(selection) = @@ -225,7 +225,7 @@ fn source_targets_normal_syntax_path_still_selects_non_preproc_offsets() { } #[test] -fn source_targets_same_origin_hits_are_available_without_dropping_emitted_tokens() { +fn source_target_same_origin_hits_are_available_without_dropping_emitted_tokens() { let (root, offset, parser_range) = root_and_offset("module m; wire payload_i; endmodule\n", "payload_i", 0); let file_id = FileId(0); @@ -245,7 +245,7 @@ fn source_targets_same_origin_hits_are_available_without_dropping_emitted_tokens } #[test] -fn source_targets_reports_ambiguous_preproc_hits_for_conflicting_targets() { +fn source_target_reports_ambiguous_preproc_hits_for_conflicting_targets() { let (root, offset, parser_range) = root_and_offset("module m; wire payload_i; endmodule\n", "payload_i", 2); let file_id = FileId(0); diff --git a/crates/ide/src/source_targets/tests/cache.rs b/crates/ide/src/facts/source_target/tests/cache.rs similarity index 100% rename from crates/ide/src/source_targets/tests/cache.rs rename to crates/ide/src/facts/source_target/tests/cache.rs diff --git a/crates/ide/src/source_targets/tests/macro_gate.rs b/crates/ide/src/facts/source_target/tests/macro_gate.rs similarity index 73% rename from crates/ide/src/source_targets/tests/macro_gate.rs rename to crates/ide/src/facts/source_target/tests/macro_gate.rs index eac485b7..867e0882 100644 --- a/crates/ide/src/source_targets/tests/macro_gate.rs +++ b/crates/ide/src/facts/source_target/tests/macro_gate.rs @@ -1,14 +1,14 @@ use super::*; #[test] -fn source_targets_macro_origin_gate_skips_plain_identifiers() { +fn source_target_macro_origin_gate_skips_plain_identifiers() { let text = "module m; wire payload_i; endmodule\n"; assert!(!source_macro_invocation_may_cover_offset(text, offset(text, "payload_i"))); } #[test] -fn source_targets_macro_origin_gate_keeps_macro_names_and_arguments() { +fn source_target_macro_origin_gate_keeps_macro_names_and_arguments() { let text = "module m; wire `MAKE_DECL(payload_i); endmodule\n"; assert!(source_macro_invocation_may_cover_offset(text, offset(text, "`MAKE_DECL"))); @@ -16,7 +16,7 @@ fn source_targets_macro_origin_gate_keeps_macro_names_and_arguments() { } #[test] -fn source_targets_macro_origin_gate_keeps_outer_arguments_after_nested_macros() { +fn source_target_macro_origin_gate_keeps_outer_arguments_after_nested_macros() { let text = "assign y = `OUTER(a, `INNER(b), payload_i);\n"; assert!(source_macro_invocation_may_cover_offset(text, offset(text, "payload_i"))); diff --git a/crates/ide/src/facts/symbol.rs b/crates/ide/src/facts/symbol.rs new file mode 100644 index 00000000..793a211e --- /dev/null +++ b/crates/ide/src/facts/symbol.rs @@ -0,0 +1,464 @@ +use hir::{ + base_db::intern::Lookup, + container::{ContainerId, InContainer, InFile, InModule, InSubroutine}, + db::HirDb, + file::HirFileId, + hir_def::{ + block::{BlockId, BlockLoc}, + declaration::Declaration, + expr::declarator::{DeclId, DeclaratorParent}, + file::{config::ConfigDeclId, library::LibraryDeclId, udp::UdpDeclId}, + module::{ + ModuleId, + generate::{GenerateBlockId, GenerateBlockLoc}, + instantiation::InstanceId, + port::NonAnsiPortId, + }, + stmt::StmtId, + subroutine::{SubroutineId, SubroutinePortId}, + typedef::TypedefId, + }, + preproc::{MacroDefinition, MacroDefinitionId, MacroParamDefinition}, + source_map::{IsNamedSrc, IsSrc, ToAstNode}, +}; +use smol_str::SmolStr; +use syntax::{ + ast::AstNode, + has_text_range::{HasTextRange, HasTextRangeIn}, +}; +use utils::{ + get::{Get, GetRef}, + impl_from, + line_index::TextRange, +}; +use vfs::FileId; + +use crate::{FileRange, SymbolKind}; + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub enum SymbolId { + ModuleId(ModuleId), + Config(InFile), + Library(InFile), + Udp(InFile), + BlockId(BlockId), + GenerateBlockId(GenerateBlockId), + SubroutineId(SubroutineId), + SubroutinePort(InSubroutine), + + NonAnsiPort(InModule), + Decl(InContainer), + Typedef(InContainer), + Instance(InModule), + Stmt(InContainer), + PreprocMacro { + id: MacroDefinitionId, + file_id: FileId, + name_range: TextRange, + directive_range: TextRange, + }, + PreprocMacroParam { + macro_id: MacroDefinitionId, + file_id: FileId, + macro_name_range: TextRange, + macro_directive_range: TextRange, + param_index: usize, + name_range: TextRange, + param_range: Option, + }, + Include { + source_file: FileId, + included_file: Option, + range: TextRange, + }, + SourceToken { + file_id: FileId, + range: TextRange, + }, +} + +pub type DefinitionOrigin = SymbolId; + +impl_from! { SymbolId => + ModuleId, + Config(InFile), + Library(InFile), + Udp(InFile), + BlockId, + GenerateBlockId, + SubroutineId, + SubroutinePort(InSubroutine), + NonAnsiPort(InModule), + Decl(InContainer), + Typedef(InContainer), + Instance(InModule), + Stmt(InContainer), +} + +impl SymbolId { + pub(crate) fn preproc_macro(definition: &MacroDefinition) -> Self { + Self::PreprocMacro { + id: definition.id, + file_id: definition.file_id, + name_range: definition.name_range, + directive_range: definition.directive_range, + } + } + + pub(crate) fn preproc_macro_param(definition: &MacroParamDefinition) -> Self { + Self::PreprocMacroParam { + macro_id: definition.macro_definition.id, + file_id: definition.macro_definition.file_id, + macro_name_range: definition.macro_definition.name_range, + macro_directive_range: definition.macro_definition.directive_range, + param_index: definition.param_index, + name_range: definition.range, + param_range: definition.param_range, + } + } + + pub fn info(&self, db: &dyn HirDb) -> Option { + Some(SymbolInfo { + id: *self, + name: self.name(db), + kind: self.kind(db), + definition_range: self.range(db).map(into_file_range), + selection_range: self.name_range(db).map(into_file_range), + container: self.container_symbol(db), + }) + } + + pub fn kind(&self, db: &dyn HirDb) -> SymbolKind { + match *self { + SymbolId::ModuleId(_) => SymbolKind::Module, + SymbolId::Config(_) => SymbolKind::Config, + SymbolId::Library(_) => SymbolKind::Library, + SymbolId::Udp(_) => SymbolKind::Primitive, + SymbolId::BlockId(_) => SymbolKind::Block, + SymbolId::GenerateBlockId(_) => SymbolKind::Generate, + SymbolId::SubroutineId(_) => SymbolKind::Fn, + SymbolId::SubroutinePort(_) => SymbolKind::PortDecl, + SymbolId::NonAnsiPort(_) => SymbolKind::NonAnsiPortLabel, + SymbolId::Decl(InContainer { value, cont_id }) => { + let cont = cont_id.to_container(db); + let decl = cont.get(value); + match decl.parent { + DeclaratorParent::PortDeclId(_) => SymbolKind::PortDecl, + DeclaratorParent::DeclarationId(idx) => match cont.get(idx) { + Declaration::DataDecl(_) => SymbolKind::DataDecl, + Declaration::NetDecl(_) => SymbolKind::NetDecl, + Declaration::ParamDecl(_) => SymbolKind::ParamDecl, + Declaration::GenvarDecl(_) => SymbolKind::Genvar, + Declaration::SpecparamDecl(_) => SymbolKind::Specparam, + }, + DeclaratorParent::StmtId(_) => SymbolKind::DataDecl, + } + } + SymbolId::Typedef(_) => SymbolKind::Typedef, + SymbolId::Instance(_) => SymbolKind::Instance, + SymbolId::Stmt(_) => SymbolKind::Stmt, + SymbolId::PreprocMacro { .. } => SymbolKind::Macro, + SymbolId::PreprocMacroParam { .. } => SymbolKind::MacroParam, + SymbolId::Include { .. } => SymbolKind::Include, + SymbolId::SourceToken { .. } => SymbolKind::Unknown, + } + } + + #[inline] + pub fn container_id(&self, db: &dyn HirDb) -> ContainerId { + match *self { + SymbolId::ModuleId(InFile { file_id, .. }) => file_id.into(), + SymbolId::Config(InFile { file_id, .. }) => file_id.into(), + SymbolId::Library(InFile { file_id, .. }) => file_id.into(), + SymbolId::Udp(InFile { file_id, .. }) => file_id.into(), + SymbolId::BlockId(block_id) => block_id.lookup(db).cont_id, + SymbolId::GenerateBlockId(generate_block_id) => generate_block_id.lookup(db).cont_id, + SymbolId::SubroutineId(subroutine_id) => subroutine_id.lookup(db).cont_id.into(), + SymbolId::SubroutinePort(InSubroutine { subroutine, .. }) => { + ContainerId::SubroutineId(subroutine) + } + SymbolId::NonAnsiPort(InModule { module_id, .. }) => module_id.into(), + SymbolId::Decl(InContainer { cont_id, .. }) => cont_id, + SymbolId::Typedef(InContainer { cont_id, .. }) => cont_id, + SymbolId::Instance(InModule { module_id, .. }) => module_id.into(), + SymbolId::Stmt(InContainer { cont_id, .. }) => cont_id, + SymbolId::PreprocMacro { file_id, .. } + | SymbolId::PreprocMacroParam { file_id, .. } + | SymbolId::Include { source_file: file_id, .. } + | SymbolId::SourceToken { file_id, .. } => HirFileId::File(file_id).into(), + } + } + + fn container_symbol(&self, db: &dyn HirDb) -> Option { + if let SymbolId::PreprocMacroParam { + macro_id, + file_id, + macro_name_range, + macro_directive_range, + .. + } = *self + { + return Some(SymbolId::PreprocMacro { + id: macro_id, + file_id, + name_range: macro_name_range, + directive_range: macro_directive_range, + }); + } + + container_symbol_id(self.container_id(db), db) + } + + pub fn name(&self, db: &dyn HirDb) -> Option { + match *self { + SymbolId::ModuleId(InFile { value, file_id }) => { + file_id.to_container(db).get(value).name.clone() + } + SymbolId::Config(InFile { value, file_id }) => { + file_id.to_container(db).get(value).name.clone() + } + SymbolId::Library(InFile { value, file_id }) => { + file_id.to_container(db).get(value).name.clone() + } + SymbolId::Udp(InFile { value, file_id }) => { + file_id.to_container(db).get(value).name.clone() + } + SymbolId::BlockId(block_id) => { + let BlockLoc { cont_id, src: InFile { value, file_id: _ } } = block_id.lookup(db); + let cont = cont_id.to_container(db); + value.hir(&cont, &cont_id.to_container_src_map(db))?.name.clone() + } + SymbolId::GenerateBlockId(generate_block_id) => { + db.generate_block(generate_block_id).name.clone() + } + SymbolId::SubroutineId(subroutine_id) => db.subroutine(subroutine_id).name.clone(), + SymbolId::SubroutinePort(InSubroutine { subroutine, value }) => { + db.subroutine(subroutine).ports.get(value.0 as usize)?.name.clone() + } + SymbolId::NonAnsiPort(InModule { value, module_id }) => { + module_id.to_container(db).get(value).label.clone() + } + SymbolId::Decl(InContainer { value, cont_id }) => { + cont_id.to_container(db).get(value).name.clone() + } + SymbolId::Typedef(InContainer { value, cont_id }) => { + cont_id.to_container(db).get(value).name.clone() + } + SymbolId::Instance(InModule { value, module_id }) => { + module_id.to_container(db).get(value).name.clone() + } + SymbolId::Stmt(InContainer { value, cont_id }) => { + cont_id.to_container(db).get(value).label.clone() + } + SymbolId::PreprocMacro { file_id, name_range, .. } => { + text_for_range(db, file_id, name_range) + } + SymbolId::PreprocMacroParam { file_id, name_range, .. } => { + text_for_range(db, file_id, name_range) + } + SymbolId::Include { source_file, range, .. } => text_for_range(db, source_file, range), + SymbolId::SourceToken { file_id, range } => text_for_range(db, file_id, range), + } + } + + pub fn name_range(&self, db: &dyn HirDb) -> Option> { + match *self { + SymbolId::ModuleId(InFile { value, file_id }) => { + let range = file_id.to_container_src_map(db).get(value)?.name_range()?; + Some(InFile::new(file_id, range)) + } + SymbolId::Config(InFile { value, file_id }) => { + let range = file_id.to_container_src_map(db).get(value)?.name_range()?; + Some(InFile::new(file_id, range)) + } + SymbolId::Library(InFile { value, file_id }) => { + let range = file_id.to_container_src_map(db).get(value)?.name_range()?; + Some(InFile::new(file_id, range)) + } + SymbolId::Udp(InFile { value, file_id }) => { + let range = file_id.to_container_src_map(db).get(value)?.name_range()?; + Some(InFile::new(file_id, range)) + } + SymbolId::BlockId(block_id) => { + let BlockLoc { src: InFile { value, file_id }, .. } = block_id.lookup(db); + let range = value.name_range()?; + Some(InFile::new(file_id, range)) + } + SymbolId::GenerateBlockId(generate_block_id) => { + let GenerateBlockLoc { src: InFile { value, file_id }, .. } = + generate_block_id.lookup(db); + let range = value.name_range()?; + Some(InFile::new(file_id, range)) + } + SymbolId::SubroutineId(subroutine_id) => { + let src = subroutine_id.lookup(db).src; + Some(InFile::new(src.file_id, src.value.name_or_full_range())) + } + SymbolId::SubroutinePort(InSubroutine { subroutine, value }) => { + let src = subroutine.lookup(db).src; + let tree = db.parse(src.file_id); + let func = src.value.to_node(&tree)?; + let ports = func + .prototype() + .port_list() + .map(|ports| ports.ports().children().collect::>()) + .unwrap_or_default(); + let port = ports + .into_iter() + .nth(value.0 as usize) + .and_then(|port| port.as_function_port())?; + let declarator = port.declarator(); + let range = declarator.name()?.text_range_in(declarator.syntax())?; + Some(InFile::new(src.file_id, range)) + } + SymbolId::NonAnsiPort(InModule { value, module_id }) => { + let range = module_id.to_container_src_map(db).get(value)?.name_range()?; + Some(InFile::new(module_id.file_id, range)) + } + SymbolId::Decl(InContainer { value, cont_id }) => { + let range = cont_id.to_container_src_map(db).get(value)?.name_range()?; + Some(InFile::new(cont_id.file_id(db).into(), range)) + } + SymbolId::Typedef(InContainer { value, cont_id }) => { + let range = cont_id.to_container_src_map(db).get(value)?.name_range()?; + Some(InFile::new(cont_id.file_id(db).into(), range)) + } + SymbolId::Instance(InModule { value, module_id }) => { + let range = module_id.to_container_src_map(db).get(value)?.name_range()?; + Some(InFile::new(module_id.file_id, range)) + } + SymbolId::Stmt(InContainer { value, cont_id }) => { + let range = cont_id.to_container_src_map(db).get(value)?.name_range()?; + Some(InFile::new(cont_id.file_id(db).into(), range)) + } + SymbolId::PreprocMacro { file_id, name_range, .. } => { + Some(InFile::new(HirFileId::File(file_id), name_range)) + } + SymbolId::PreprocMacroParam { file_id, name_range, .. } => { + Some(InFile::new(HirFileId::File(file_id), name_range)) + } + SymbolId::Include { source_file, range, .. } => { + Some(InFile::new(HirFileId::File(source_file), range)) + } + SymbolId::SourceToken { file_id, range } => { + Some(InFile::new(HirFileId::File(file_id), range)) + } + } + } + + pub fn range(&self, db: &dyn HirDb) -> Option> { + Some(match *self { + SymbolId::ModuleId(InFile { value, file_id }) => { + let range = file_id.to_container_src_map(db).get(value)?.range(); + InFile::new(file_id, range) + } + SymbolId::Config(InFile { value, file_id }) => { + let range = file_id.to_container_src_map(db).get(value)?.range(); + InFile::new(file_id, range) + } + SymbolId::Library(InFile { value, file_id }) => { + let range = file_id.to_container_src_map(db).get(value)?.range(); + InFile::new(file_id, range) + } + SymbolId::Udp(InFile { value, file_id }) => { + let range = file_id.to_container_src_map(db).get(value)?.range(); + InFile::new(file_id, range) + } + SymbolId::BlockId(block_id) => { + let BlockLoc { src: InFile { value, file_id }, .. } = block_id.lookup(db); + let range = value.range(); + InFile::new(file_id, range) + } + SymbolId::GenerateBlockId(generate_block_id) => { + let GenerateBlockLoc { src: InFile { value, file_id }, .. } = + generate_block_id.lookup(db); + let range = value.range(); + InFile::new(file_id, range) + } + SymbolId::SubroutineId(subroutine_id) => { + let src = subroutine_id.lookup(db).src; + let range = src.value.range(); + InFile::new(src.file_id, range) + } + SymbolId::SubroutinePort(InSubroutine { subroutine, value }) => { + let src = subroutine.lookup(db).src; + let tree = db.parse(src.file_id); + let func = src.value.to_node(&tree)?; + let ports = func.prototype().port_list()?; + let port = ports + .ports() + .children() + .nth(value.0 as usize) + .and_then(|port| port.as_function_port())?; + let range = port.syntax().text_range()?; + InFile::new(src.file_id, range) + } + SymbolId::NonAnsiPort(InModule { value, module_id }) => { + let range = module_id.to_container_src_map(db).get(value)?.range(); + InFile::new(module_id.file_id, range) + } + SymbolId::Decl(InContainer { value, cont_id }) => { + let range = cont_id.to_container_src_map(db).get(value)?.range(); + InFile::new(cont_id.file_id(db).into(), range) + } + SymbolId::Typedef(InContainer { value, cont_id }) => { + let range = cont_id.to_container_src_map(db).get(value)?.range(); + InFile::new(cont_id.file_id(db).into(), range) + } + SymbolId::Instance(InModule { value, module_id }) => { + let range = module_id.to_container_src_map(db).get(value)?.range(); + InFile::new(module_id.file_id, range) + } + SymbolId::Stmt(InContainer { value, cont_id }) => { + let range = cont_id.to_container_src_map(db).get(value)?.range(); + InFile::new(cont_id.file_id(db).into(), range) + } + SymbolId::PreprocMacro { file_id, directive_range, .. } => { + InFile::new(HirFileId::File(file_id), directive_range) + } + SymbolId::PreprocMacroParam { file_id, param_range, name_range, .. } => { + InFile::new(HirFileId::File(file_id), param_range.unwrap_or(name_range)) + } + SymbolId::Include { source_file, range, .. } => { + InFile::new(HirFileId::File(source_file), range) + } + SymbolId::SourceToken { file_id, range } => { + InFile::new(HirFileId::File(file_id), range) + } + }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SymbolInfo { + pub id: SymbolId, + pub name: Option, + pub kind: SymbolKind, + pub definition_range: Option, + pub selection_range: Option, + pub container: Option, +} + +fn container_symbol_id(container_id: ContainerId, db: &dyn HirDb) -> Option { + match container_id { + ContainerId::HirFileId(_) => None, + ContainerId::ModuleId(module_id) => Some(SymbolId::ModuleId(module_id)), + ContainerId::GenerateBlockId(generate_block_id) => { + Some(SymbolId::GenerateBlockId(generate_block_id)) + } + ContainerId::BlockId(block_id) => Some(SymbolId::BlockId(block_id)), + ContainerId::SubroutineId(subroutine_id) => { + let _ = db; + Some(SymbolId::SubroutineId(subroutine_id)) + } + } +} + +fn into_file_range(InFile { file_id, value: range }: InFile) -> FileRange { + FileRange { file_id: file_id.file_id(), range } +} + +fn text_for_range(db: &dyn HirDb, file_id: FileId, range: TextRange) -> Option { + let text = db.file_text(file_id); + Some(SmolStr::new(&text[range])) +} diff --git a/crates/ide/src/semantic_target.rs b/crates/ide/src/facts/target.rs similarity index 57% rename from crates/ide/src/semantic_target.rs rename to crates/ide/src/facts/target.rs index 0d549461..e9b19ad5 100644 --- a/crates/ide/src/semantic_target.rs +++ b/crates/ide/src/facts/target.rs @@ -1,19 +1,29 @@ use hir::preproc::{ - IncludeDirective, MacroDefinition, MacroParamDefinition, MacroParamReferenceDefinitions, - MacroReferenceDefinitions, include_directives_at, macro_definition_at, - macro_param_definition_at, macro_param_reference_definitions_at, + IncludeDirective, IncludeTarget, MacroDefinition, MacroParamDefinition, + MacroParamReferenceDefinitions, MacroReferenceDefinitions, include_directives_at, + macro_definition_at, macro_param_definition_at, macro_param_reference_definitions_at, macro_reference_definitions_at, }; -use syntax::{SyntaxNode, TokenKind}; +use syntax::{ + SyntaxAncestors, SyntaxKind, SyntaxNode, SyntaxNodeExt, TokenKind, + ast::{self, AstNode}, + has_text_range::HasTextRange, + match_ast_kind, + token::TokenKindExt, +}; use utils::line_index::{TextRange, TextSize}; use vfs::FileId; +pub(crate) use crate::facts::source_target::SourceTarget; use crate::{ db::root_db::RootDb, - source_targets::{ - SourceTarget, SourceTargetAlternatives, SourceTargetAmbiguity, SourceTargetBlock, - SourceTargetBlockReason, SourceTargetDomain, SourceTargetResolution, - source_target_at_offset, + facts::{ + source_target::{ + SourceTargetAlternatives, SourceTargetAmbiguity, SourceTargetBlock, + SourceTargetBlockReason, SourceTargetDomain, SourceTargetResolution, + source_target_at_offset, + }, + symbol::SymbolId, }, }; @@ -38,6 +48,38 @@ impl TargetIntent { } } +pub(crate) struct TargetQuery<'tree> { + pub(crate) file_id: FileId, + pub(crate) offset: TextSize, + pub(crate) intent: TargetIntent, + pub(crate) root: Option>, +} + +pub(crate) fn target_at<'tree>( + db: &RootDb, + TargetQuery { file_id, offset, intent, root }: TargetQuery<'tree>, +) -> TargetResolution<'tree> { + resolve_semantic_target(db, file_id, offset, root, move |kind| token_precedence(intent, kind)) +} + +fn token_precedence(intent: TargetIntent, kind: TokenKind) -> usize { + match intent { + TargetIntent::Describe => match kind { + _ if kind.name_like() => 4, + _ if kind.is_literal() => 3, + _ => 1, + }, + TargetIntent::Navigate | TargetIntent::FindReferences | TargetIntent::Highlight => { + match kind { + _ if kind.name_like() => 4, + _ if kind.is_pair_token() => 4, + _ => 1, + } + } + TargetIntent::Rename => usize::from(kind.name_like()), + } +} + bitflags::bitflags! { #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub(crate) struct TargetCapability: u8 { @@ -158,6 +200,7 @@ pub(crate) struct TargetAnchor { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum TargetOrigin { MacroExpansion, + SourceSyntax, } impl TargetOrigin { @@ -231,6 +274,15 @@ pub(crate) struct TargetBlock { #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) enum TargetBlockReason { PreprocUnavailable, + LanguageFact(LanguageFactStatus), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[allow(dead_code)] +pub(crate) enum LanguageFactStatus { + Lowered, + SyntaxOnly, + Skipped, } #[derive(Debug, Clone)] @@ -249,6 +301,22 @@ pub(crate) enum PreprocMacroTarget { } impl PreprocMacroTarget { + #[allow(dead_code)] + pub(crate) fn symbols(&self) -> Vec { + match self { + PreprocMacroTarget::ParamDefinition(definition) => { + vec![SymbolId::preproc_macro_param(definition)] + } + PreprocMacroTarget::ParamReference(resolution) => { + resolution.definitions.iter().map(SymbolId::preproc_macro_param).collect() + } + PreprocMacroTarget::Definition(definition) => vec![SymbolId::preproc_macro(definition)], + PreprocMacroTarget::Reference(resolution) => { + resolution.definitions.iter().map(SymbolId::preproc_macro).collect() + } + } + } + fn capabilities(&self) -> TargetCapability { let mut capabilities = TargetCapability::DESCRIBE; let has_definitions = match self { @@ -263,6 +331,21 @@ impl PreprocMacroTarget { } } +#[allow(dead_code)] +pub(crate) fn include_symbols(includes: &[IncludeDirective]) -> Vec { + includes + .iter() + .map(|include| SymbolId::Include { + source_file: include.file_id, + included_file: match include.target { + IncludeTarget::Literal { resolved_file, .. } => resolved_file, + IncludeTarget::Token { .. } => None, + }, + range: include.range, + }) + .collect() +} + pub(crate) fn resolve_semantic_target<'tree, F>( db: &RootDb, file_id: FileId, @@ -284,11 +367,103 @@ where let Some(root) = root else { return TargetResolution::Unresolved; }; + if let Some(block) = language_fact_block_at(file_id, root, offset) { + return TargetResolution::Blocked(block); + } source_target_at_offset(db, file_id, root, offset, precedence) .map(|resolution| TargetResolution::from_source_resolution(file_id, resolution)) .unwrap_or(TargetResolution::Unresolved) } +fn language_fact_block_at<'tree>( + file_id: FileId, + root: SyntaxNode<'tree>, + offset: TextSize, +) -> Option { + let token = root + .token_at_offset(offset) + .pick_bext_token(|kind: TokenKind| usize::from(kind.name_like()))?; + let fallback_range = token.text_range()?; + let skipped = SyntaxAncestors::start_from(token.parent) + .find(|node| language_fact_status_for_node(*node) == LanguageFactStatus::Skipped)?; + let range = skipped.text_range().unwrap_or(fallback_range); + Some(TargetBlock { + anchor: TargetAnchor { file_id, range, origin: TargetOrigin::SourceSyntax }, + reason: TargetBlockReason::LanguageFact(LanguageFactStatus::Skipped), + }) +} + +fn language_fact_status_for_node(node: SyntaxNode<'_>) -> LanguageFactStatus { + if ast::ModuleDeclaration::cast(node).is_some() + && SyntaxAncestors::start_from(node) + .skip(1) + .any(|ancestor| ast::ModuleDeclaration::cast(ancestor).is_some()) + { + return LanguageFactStatus::Skipped; + } + + language_fact_status(node.kind()) +} + +fn language_fact_status(kind: SyntaxKind) -> LanguageFactStatus { + match_ast_kind! { kind, + ast::ConcurrentAssertionStatement + | ast::CheckerInstanceStatement + | ast::DisableForkStatement + | ast::ForeachLoopStatement + | ast::ImmediateAssertionStatement + | ast::RandCaseStatement + | ast::RandSequenceStatement + | ast::VoidCastedCallStatement + | ast::WaitForkStatement + | ast::WaitOrderStatement + | ast::NetTypeDeclaration + | ast::ForwardTypedefDeclaration + | ast::UserDefinedNetDeclaration + | ast::CheckerInstantiation + | ast::PackageImportDeclaration + | ast::ClassDeclaration + | ast::TimeUnitsDeclaration + | ast::ClockingDeclaration + | ast::DefaultClockingReference + | ast::ClockingItem + | ast::PropertyDeclaration + | ast::SequenceDeclaration + | ast::ImmediateAssertionMember + | ast::ConcurrentAssertionMember + | ast::CovergroupDeclaration + | ast::Coverpoint + | ast::CoverCross + | ast::CoverageBins + | ast::BinsSelection + | ast::CoverageOption + | ast::DefaultSkewItem + | ast::DPIImport + | ast::DPIExport + | ast::ExternInterfaceMethod + | ast::ExternModuleDecl + | ast::ExternUdpDecl + | ast::NetAlias + | ast::ModportDeclaration + | ast::ModportClockingPort + | ast::ModportSimplePortList + | ast::ModportSubroutinePortList + | ast::ClassPropertyDeclaration + | ast::ClassMethodDeclaration + | ast::ClassMethodPrototype + | ast::CheckerDeclaration + | ast::CheckerDataDeclaration + | ast::ConstraintDeclaration + | ast::ConstraintPrototype + | ast::BindDirective + | ast::PackageExportDeclaration + | ast::PackageExportAllDeclaration + | ast::LetDeclaration + | ast::DefaultDisableDeclaration => LanguageFactStatus::Skipped, + _ => LanguageFactStatus::Lowered, + } +} + fn preproc_macro_target_at( db: &RootDb, file_id: FileId, @@ -378,6 +553,29 @@ mod tests { (host, file_id, range.start(), range) } + #[test] + fn semantic_facts_target_at_projects_source_target_by_intent() { + use crate::facts::{SemanticFacts, TargetQuery}; + + let (host, file_id, offset, _) = + setup("module m; wire payload_i; endmodule\n", "payload_i"); + let sema = Semantics::new(host.raw_db()); + let parsed = sema.parse_file(file_id); + let facts = SemanticFacts::new(host.raw_db()); + + let resolution = facts.target_at(TargetQuery { + file_id, + offset, + intent: TargetIntent::Rename, + root: parsed.root(), + }); + + assert!(matches!( + resolution.unique_for_intent(TargetIntent::Rename), + Some(SemanticTarget::Source(_)) + )); + } + #[test] fn source_token_target_is_complete_and_source_origin() { let (host, file_id, offset, range) = @@ -408,17 +606,91 @@ mod tests { assert_eq!(target.range, range); } + #[test] + fn skipped_language_fact_blocks_before_syntax_fallback() { + use crate::facts::{SemanticFacts, TargetQuery}; + + let (host, file_id, offset, _) = setup( + "module m; initial begin foreach (items[i]) payload = i; end endmodule\n", + "payload", + ); + let sema = Semantics::new(host.raw_db()); + let parsed = sema.parse_file(file_id); + let facts = SemanticFacts::new(host.raw_db()); + + let resolution = facts.target_at(TargetQuery { + file_id, + offset, + intent: TargetIntent::Rename, + root: parsed.root(), + }); + + let TargetResolution::Blocked(block) = resolution else { + panic!("skipped language fact should block target resolution"); + }; + assert_eq!(block.anchor.origin, TargetOrigin::SourceSyntax); + assert_eq!(block.reason, TargetBlockReason::LanguageFact(LanguageFactStatus::Skipped)); + } + + #[test] + fn skipped_module_item_fact_blocks_before_syntax_fallback() { + use crate::facts::{SemanticFacts, TargetQuery}; + + let (host, file_id, offset, _) = + setup("module m; class payload; endclass endmodule\n", "payload"); + let sema = Semantics::new(host.raw_db()); + let parsed = sema.parse_file(file_id); + let facts = SemanticFacts::new(host.raw_db()); + + let resolution = facts.target_at(TargetQuery { + file_id, + offset, + intent: TargetIntent::Rename, + root: parsed.root(), + }); + + let TargetResolution::Blocked(block) = resolution else { + panic!("skipped module item should block target resolution"); + }; + assert_eq!(block.anchor.origin, TargetOrigin::SourceSyntax); + assert_eq!(block.reason, TargetBlockReason::LanguageFact(LanguageFactStatus::Skipped)); + } + + #[test] + fn nested_module_fact_blocks_before_syntax_fallback() { + use crate::facts::{SemanticFacts, TargetQuery}; + + let (host, file_id, offset, _) = + setup("module outer; module payload; endmodule endmodule\n", "payload"); + let sema = Semantics::new(host.raw_db()); + let parsed = sema.parse_file(file_id); + let facts = SemanticFacts::new(host.raw_db()); + + let resolution = facts.target_at(TargetQuery { + file_id, + offset, + intent: TargetIntent::Navigate, + root: parsed.root(), + }); + + let TargetResolution::Blocked(block) = resolution else { + panic!("nested module should block target resolution"); + }; + assert_eq!(block.anchor.origin, TargetOrigin::SourceSyntax); + assert_eq!(block.reason, TargetBlockReason::LanguageFact(LanguageFactStatus::Skipped)); + } + #[test] fn source_target_block_is_reported_without_syntax_fallback() { - let block = crate::source_targets::SourceTargetBlock { - domain: crate::source_targets::SourceTargetDomain::Preproc, + let block = crate::facts::source_target::SourceTargetBlock { + domain: crate::facts::source_target::SourceTargetDomain::Preproc, range: TextRange::new(TextSize::from(1), TextSize::from(4)), - reason: crate::source_targets::SourceTargetBlockReason::Unavailable, + reason: crate::facts::source_target::SourceTargetBlockReason::Unavailable, }; let resolution = TargetResolution::from_source_resolution( FileId(0), - crate::source_targets::SourceTargetResolution::Blocked(block.clone()), + crate::facts::source_target::SourceTargetResolution::Blocked(block.clone()), ); let TargetResolution::Blocked(target) = resolution else { @@ -432,15 +704,17 @@ mod tests { #[test] fn ambiguous_source_target_block_is_reported_as_ambiguous() { - let block = crate::source_targets::SourceTargetBlock { - domain: crate::source_targets::SourceTargetDomain::Preproc, + let block = crate::facts::source_target::SourceTargetBlock { + domain: crate::facts::source_target::SourceTargetDomain::Preproc, range: TextRange::new(TextSize::from(1), TextSize::from(4)), - reason: crate::source_targets::SourceTargetBlockReason::Ambiguous { hits: Vec::new() }, + reason: crate::facts::source_target::SourceTargetBlockReason::Ambiguous { + hits: Vec::new(), + }, }; let resolution = TargetResolution::from_source_resolution( FileId(0), - crate::source_targets::SourceTargetResolution::Blocked(block), + crate::facts::source_target::SourceTargetResolution::Blocked(block), ); let TargetResolution::Ambiguous(ambiguity) = resolution else { @@ -455,21 +729,23 @@ mod tests { fn ambiguous_source_target_alternatives_project_as_candidates() { let range = TextRange::new(TextSize::from(1), TextSize::from(4)); let target_range = TextRange::new(TextSize::from(2), TextSize::from(3)); - let target = crate::source_targets::SourceTarget { - origin: crate::source_targets::SourceTargetOrigin::NormalSyntax, + let target = crate::facts::source_target::SourceTarget { + origin: crate::facts::source_target::SourceTargetOrigin::NormalSyntax, range: target_range, tokens: Vec::new(), }; - let alternatives = crate::source_targets::SourceTargetAlternatives { - domain: crate::source_targets::SourceTargetDomain::Preproc, + let alternatives = crate::facts::source_target::SourceTargetAlternatives { + domain: crate::facts::source_target::SourceTargetDomain::Preproc, range, - reason: crate::source_targets::SourceTargetAmbiguity::PreprocHits { hit_count: 2 }, + reason: crate::facts::source_target::SourceTargetAmbiguity::PreprocHits { + hit_count: 2, + }, targets: vec![target.clone(), target], }; let resolution = TargetResolution::from_source_resolution( FileId(0), - crate::source_targets::SourceTargetResolution::Ambiguous(alternatives), + crate::facts::source_target::SourceTargetResolution::Ambiguous(alternatives), ); assert!(resolution.clone().unique_for_intent(TargetIntent::Describe).is_none()); diff --git a/crates/ide/src/goto_declaration.rs b/crates/ide/src/goto_declaration.rs index e30929b0..77da38ec 100644 --- a/crates/ide/src/goto_declaration.rs +++ b/crates/ide/src/goto_declaration.rs @@ -6,10 +6,11 @@ use crate::{ FilePosition, RangeInfo, db::root_db::RootDb, definitions::DefinitionClass, - goto_definition, + facts::{ + SemanticFacts, TargetQuery, + target::{SemanticTarget, SourceTarget, TargetIntent}, + }, navigation_target::{NavTarget, ToNav}, - semantic_target::{SemanticTarget, TargetIntent, resolve_semantic_target}, - source_targets::SourceTarget, }; pub(crate) fn goto_declaration( @@ -19,13 +20,12 @@ pub(crate) fn goto_declaration( let sema = Semantics::new(db); let hir_file_id = file_id.into(); let parsed_file = sema.parse_file(file_id); - let target = resolve_semantic_target( - db, + let target = SemanticFacts::new(db).target_at(TargetQuery { file_id, offset, - parsed_file.root(), - goto_definition::token_precedence, - ); + intent: TargetIntent::Navigate, + root: parsed_file.root(), + }); render_declaration_target( db, hir_file_id, diff --git a/crates/ide/src/goto_definition.rs b/crates/ide/src/goto_definition.rs index 9f4d35bf..67fa8339 100644 --- a/crates/ide/src/goto_definition.rs +++ b/crates/ide/src/goto_definition.rs @@ -6,10 +6,9 @@ use hir::{ semantics::Semantics, }; use itertools::Itertools; -use syntax::{ - SyntaxTokenWithParent, TokenKind, - token::{TokenKindExt, pair_token}, -}; +use syntax::{SyntaxTokenWithParent, token::pair_token}; +#[cfg(test)] +use syntax::{TokenKind, token::TokenKindExt}; use utils::line_index::{TextRange, TextSize, covering_range}; use vfs::FileId; @@ -17,11 +16,13 @@ use crate::{ FilePosition, RangeInfo, db::root_db::RootDb, definitions::DefinitionClass, - navigation_target::{NavTarget, ToNav}, - semantic_target::{ - PreprocMacroTarget, SemanticTarget, TargetIntent, TargetResolution, resolve_semantic_target, + facts::{ + SemanticFacts, TargetQuery, + target::{ + PreprocMacroTarget, SemanticTarget, SourceTarget, TargetIntent, TargetResolution, + }, }, - source_targets::SourceTarget, + navigation_target::{NavTarget, ToNav}, }; pub(crate) fn goto_definition( @@ -30,7 +31,12 @@ pub(crate) fn goto_definition( ) -> Option>> { let sema = Semantics::new(db); let parsed_file = sema.parse_file(file_id); - let target = resolve_semantic_target(db, file_id, offset, parsed_file.root(), token_precedence); + let target = SemanticFacts::new(db).target_at(TargetQuery { + file_id, + offset, + intent: TargetIntent::Navigate, + root: parsed_file.root(), + }); render_definition_target(db, file_id, &sema, target) } @@ -196,6 +202,7 @@ fn handle_ctrl_flow_kw( } } +#[cfg(test)] pub(crate) fn token_precedence(kind: TokenKind) -> usize { match kind { _ if kind.name_like() => 4, diff --git a/crates/ide/src/hover.rs b/crates/ide/src/hover.rs index 32caeae9..cce0bc19 100644 --- a/crates/ide/src/hover.rs +++ b/crates/ide/src/hover.rs @@ -3,7 +3,7 @@ use hir::{ semantics::Semantics, }; use syntax::{ - SyntaxTokenWithParent, TokenKind, + SyntaxTokenWithParent, ast::{self, AstNode}, has_text_range::HasTextRange, token::TokenKindExt, @@ -19,14 +19,16 @@ use crate::{ FilePosition, RangeInfo, db::root_db::RootDb, definitions::DefinitionClass, + facts::{ + SemanticFacts, TargetQuery, + target::{SemanticTarget, SourceTarget, TargetIntent, TargetResolution}, + }, hover::{ include::render_include_hover, macro_hover::{render_macro_hover_target, with_expanded_macro_hover}, }, markup::{Markup, inline_code}, render, - semantic_target::{SemanticTarget, TargetIntent, TargetResolution, resolve_semantic_target}, - source_targets::SourceTarget, }; mod include; @@ -53,7 +55,12 @@ pub(crate) fn hover( ) -> Option> { let sema = Semantics::new(db); let parsed_file = sema.parse_file(file_id); - let target = resolve_semantic_target(db, file_id, offset, parsed_file.root(), token_precedence); + let target = SemanticFacts::new(db).target_at(TargetQuery { + file_id, + offset, + intent: TargetIntent::Describe, + root: parsed_file.root(), + }); render_hover_target(db, file_id, offset, &sema, target) } @@ -115,14 +122,6 @@ fn hover_for_token_selection( Some(RangeInfo::new(range, res)) } -pub(crate) fn token_precedence(kind: TokenKind) -> usize { - match kind { - _ if kind.name_like() => 4, - _ if kind.is_literal() => 3, - _ => 1, - } -} - fn handle_literal( sema: &Semantics, file_id: HirFileId, diff --git a/crates/ide/src/hover/macro_hover.rs b/crates/ide/src/hover/macro_hover.rs index 214826fc..85a9fae7 100644 --- a/crates/ide/src/hover/macro_hover.rs +++ b/crates/ide/src/hover/macro_hover.rs @@ -1,7 +1,7 @@ use utils::line_index::TextSize; use vfs::FileId; -use crate::{RangeInfo, db::root_db::RootDb, markup::Markup, semantic_target::PreprocMacroTarget}; +use crate::{RangeInfo, db::root_db::RootDb, facts::target::PreprocMacroTarget, markup::Markup}; mod expansion; mod markup; diff --git a/crates/ide/src/inlay_hint.rs b/crates/ide/src/inlay_hint.rs index 228437e7..d7ef2d2e 100644 --- a/crates/ide/src/inlay_hint.rs +++ b/crates/ide/src/inlay_hint.rs @@ -27,7 +27,9 @@ use utils::{ }; use vfs::FileId; -use crate::{db::root_db::RootDb, markup::Markup, module_resolution::resolve_module_name}; +use crate::{ + FileRange, db::root_db::RootDb, markup::Markup, module_resolution::resolve_module_name, +}; #[derive(Debug)] pub struct InlayHintConfig { @@ -55,7 +57,7 @@ pub enum InlayKind { pub struct InlayHint { pub label: String, pub tooltip: Option, - pub target_location: Option>, + pub target_location: Option, pub padding_left: bool, pub padding_right: bool, @@ -141,7 +143,7 @@ impl InlayHintCollector { } let (tooltip, target_location) = if let Some(InFile { value: src, file_id }) = target_src { - let location = InFile::new(file_id, src.range()); + let location = FileRange { file_id: file_id.file_id(), range: src.range() }; (Some(Markup::new()), Some(location)) } else { (None, None) @@ -175,7 +177,7 @@ impl InlayHintCollector { fn collect_range_hint( &mut self, anchor: HintAnchor, - target_location: Option>, + target_location: Option, label: String, ) { if !self.intersect(anchor.range) { @@ -286,7 +288,7 @@ fn collect_macro_argument_hints_for_call( }; collector.collect_range_hint( HintAnchor::macro_argument(argument_range), - Some(InFile::new(HirFileId::File(resolution.definition.file_id), param_range)), + Some(FileRange { file_id: resolution.definition.file_id, range: param_range }), format!("{param_name}:"), ); } @@ -709,7 +711,7 @@ mod tests { let target = hint .target_location .as_ref() - .map(|target| (usize::from(target.value.start()), usize::from(target.value.end()))); + .map(|target| (usize::from(target.range.start()), usize::from(target.range.end()))); let edit = hint.text_edit.as_ref().map(|edit| format!("{edit:?}")); out.push_str(&format!( "{:?} @ {} {:?} padding=({}, {}) target={:?} edit={:?}\n", diff --git a/crates/ide/src/lib.rs b/crates/ide/src/lib.rs index 5cba0852..dca50281 100644 --- a/crates/ide/src/lib.rs +++ b/crates/ide/src/lib.rs @@ -14,6 +14,7 @@ pub type Cancellable = Result; pub mod analysis; pub mod analysis_host; +pub mod call_hierarchy; pub mod definitions; pub mod markup; pub(crate) mod module_resolution; @@ -28,6 +29,7 @@ pub mod db; pub mod diagnostics; pub mod document_highlight; pub mod document_symbols; +pub(crate) mod facts; pub mod folding_ranges; pub mod formatting; pub mod goto_declaration; @@ -41,10 +43,8 @@ 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; -pub(crate) mod source_targets; #[cfg(test)] mod test_utils; #[cfg(test)] @@ -72,6 +72,9 @@ pub enum SymbolKind { Specify, Interface, Library, + Macro, + MacroParam, + Include, Region, Unknown, } diff --git a/crates/ide/src/navigation_target.rs b/crates/ide/src/navigation_target.rs index 7dda6ab8..b75aa6f6 100644 --- a/crates/ide/src/navigation_target.rs +++ b/crates/ide/src/navigation_target.rs @@ -72,6 +72,22 @@ impl ToNav for DefinitionOrigin { DefinitionOrigin::Typedef(typedef_id) => typedef_id.to_nav(db), DefinitionOrigin::Instance(instance_id) => instance_id.to_nav(db), DefinitionOrigin::Stmt(stmt_id) => stmt_id.to_nav(db), + DefinitionOrigin::PreprocMacro { .. } + | DefinitionOrigin::PreprocMacroParam { .. } + | DefinitionOrigin::Include { .. } + | DefinitionOrigin::SourceToken { .. } => { + let info = self.info(db)?; + let full_range = info.definition_range?; + Some(NavTarget { + file_id: full_range.file_id, + full_range: full_range.range, + focus_range: info.selection_range.map(|range| range.range), + name: info.name, + kind: Some(info.kind), + container_name: None, + description: None, + }) + } } } } diff --git a/crates/ide/src/references.rs b/crates/ide/src/references.rs index 1d73e55c..c871559c 100644 --- a/crates/ide/src/references.rs +++ b/crates/ide/src/references.rs @@ -1,26 +1,13 @@ -use hir::{file::HirFileId, semantics::Semantics}; -use itertools::Itertools; use nohash_hasher::IntMap; -use search::{ReferencesCtx, SearchScope}; -use syntax::{ - SyntaxTokenWithParent, TokenKind, - has_text_range::HasTextRange, - token::{TokenKindExt, pair_token}, -}; +use search::SearchScope; +use syntax::SyntaxTokenWithParent; use utils::line_index::TextRange; use vfs::FileId; -use self::preproc::render_preproc_references_target; use crate::{ - FilePosition, ScopeVisibility, - db::root_db::RootDb, - definitions::{Definition, DefinitionClass}, - navigation_target::{NavTarget, ToNav}, - semantic_target::{SemanticTarget, TargetIntent, TargetResolution, resolve_semantic_target}, - source_targets::SourceTarget, + ScopeVisibility, db::root_db::RootDb, definitions::Definition, navigation_target::NavTarget, }; -mod preproc; pub(crate) mod search; bitflags::bitflags! { @@ -84,119 +71,3 @@ impl ReferencesStatus { } } } - -pub(crate) fn references( - db: &RootDb, - FilePosition { file_id, offset }: FilePosition, - config: ReferencesConfig, -) -> Option> { - let sema = Semantics::new(db); - let parsed_file = sema.parse_file(file_id); - let target = resolve_semantic_target(db, file_id, offset, parsed_file.root(), token_precedence); - render_references_target(db, file_id, &sema, target, config) -} - -fn render_references_target( - db: &RootDb, - file_id: FileId, - sema: &Semantics, - target: TargetResolution<'_>, - config: ReferencesConfig, -) -> Option> { - match target.unique_for_intent(TargetIntent::FindReferences)? { - SemanticTarget::PreprocMacro(target) => { - render_preproc_references_target(db, file_id, target, &config) - } - SemanticTarget::Include(_) => None, - SemanticTarget::Source(target) => { - render_source_references_target(sema, file_id, target, config) - } - } -} - -fn render_source_references_target( - sema: &Semantics, - file_id: FileId, - target: SourceTarget<'_>, - config: ReferencesConfig, -) -> Option> { - let hir_file_id = file_id.into(); - let tokens = target.into_tokens(); - let references = tokens - .into_iter() - .filter_map(|token| references_for_token(sema, hir_file_id, token, config.clone())) - .flatten() - .collect_vec(); - (!references.is_empty()).then_some(references) -} - -fn references_for_token( - sema: &Semantics, - hir_file_id: HirFileId, - token: SyntaxTokenWithParent, - config: ReferencesConfig, -) -> Option> { - handle_ctrl_flow_kw(sema, hir_file_id, token).or_else(|| { - let def = match DefinitionClass::resolve(sema, hir_file_id, token)? { - DefinitionClass::Definition(def) => def, - DefinitionClass::PortConnShorthand { local, .. } => local, - DefinitionClass::Ambiguous(_) => return None, - }; - Some(vec![search_refs(sema, def, config)]) - }) -} - -pub(crate) fn handle_ctrl_flow_kw( - _sema: &Semantics<'_, RootDb>, - file_id: HirFileId, - tp @ SyntaxTokenWithParent { .. }: SyntaxTokenWithParent, -) -> Option> { - let kind = tp.kind(); - - let mut refs = vec![]; - let mut add_ref = |tok: SyntaxTokenWithParent| { - if let Some(range) = tok.text_range() { - refs.push((range, ReferenceCategory::empty())); - } - }; - - match kind { - _ if let Some(pair) = pair_token(tp) => { - let pair = pair.either(|tok| tok, |tok| tok); - add_ref(tp); - add_ref(pair); - } - _ => return None, - } - - Some(vec![References { - def: None, - refs: IntMap::from_iter([(file_id.file_id(), refs)]), - status: ReferencesStatus::Complete, - }]) -} - -fn search_refs<'a>( - sema: &'a Semantics<'a, RootDb>, - def: Definition, - config: ReferencesConfig, -) -> References { - let refs = ReferencesCtx::new(sema, &def, config) - .search() - .into_iter() - .map(|(file_id, tokens)| { - let res = tokens.into_iter().map(|token| (token.range(), token.category())).collect(); - (file_id, res) - }) - .collect(); - let def = def.origins().into_iter().filter_map(|def| def.to_nav(sema.db)).collect_vec().into(); - References { def, refs, status: ReferencesStatus::Complete } -} - -fn token_precedence(kind: TokenKind) -> usize { - match kind { - _ if kind.name_like() => 4, - _ if kind.is_pair_token() => 4, - _ => 1, - } -} diff --git a/crates/ide/src/references/preproc.rs b/crates/ide/src/references/preproc.rs deleted file mode 100644 index ef5e2b9a..00000000 --- a/crates/ide/src/references/preproc.rs +++ /dev/null @@ -1,183 +0,0 @@ -use hir::preproc::{ - MacroDefinition, MacroParamDefinition, MacroReferenceIndexStatus, macro_param_references, - macro_references, -}; -use itertools::Itertools; -use vfs::FileId; - -use super::{ - ReferenceCategory, References, ReferencesConfig, ReferencesPartialReason, ReferencesStatus, -}; -use crate::{ - db::root_db::RootDb, navigation_target::NavTarget, semantic_target::PreprocMacroTarget, -}; - -pub(super) fn render_preproc_references_target( - db: &RootDb, - file_id: FileId, - target: PreprocMacroTarget, - config: &ReferencesConfig, -) -> Option> { - match target { - PreprocMacroTarget::ParamDefinition(definition) => { - Some(vec![macro_param_references_for_definition(db, file_id, definition, config)?]) - } - PreprocMacroTarget::ParamReference(resolution) => { - let definitions = resolution.definitions; - if definitions.is_empty() { - return None; - } - definitions - .into_iter() - .map(|definition| { - macro_param_references_for_definition(db, file_id, definition, config) - }) - .collect() - } - PreprocMacroTarget::Definition(definition) => { - Some(vec![macro_references_for_definition(db, file_id, definition, config)?]) - } - PreprocMacroTarget::Reference(resolution) => { - let definitions = resolution.definitions; - if definitions.is_empty() { - return None; - } - definitions - .into_iter() - .map(|definition| macro_references_for_definition(db, file_id, definition, config)) - .collect() - } - } -} - -fn macro_param_references_for_definition( - db: &RootDb, - file_id: FileId, - definition: MacroParamDefinition, - config: &ReferencesConfig, -) -> Option { - let refs = macro_param_references(db, file_id, &definition) - .ok()? - .references - .into_iter() - .filter(|usage| { - config.search_scope.as_ref().is_none_or(|scope| { - scope.range_for_file(usage.file_id).is_some_and(|range| { - range.is_none_or(|range| range.intersect(usage.range).is_some()) - }) - }) - }) - .into_group_map_by(|usage| usage.file_id) - .into_iter() - .map(|(file_id, usages)| { - ( - file_id, - usages - .into_iter() - .map(|usage| (usage.range, ReferenceCategory::empty())) - .collect_vec(), - ) - }) - .collect(); - Some(References { - def: Some(vec![macro_param_nav_target(definition)]), - refs, - status: ReferencesStatus::Complete, - }) -} - -fn macro_references_for_definition( - db: &RootDb, - file_id: FileId, - definition: MacroDefinition, - config: &ReferencesConfig, -) -> Option { - let references = macro_references(db, file_id, &definition).ok()?; - let status = references_status_from_macro_index(references.status); - let refs = references - .references - .into_iter() - .filter(|usage| { - config.search_scope.as_ref().is_none_or(|scope| { - scope.range_for_file(usage.file_id).is_some_and(|range| { - range.is_none_or(|range| range.intersect(usage.range).is_some()) - }) - }) - }) - .into_group_map_by(|usage| usage.file_id) - .into_iter() - .map(|(file_id, usages)| { - ( - file_id, - usages - .into_iter() - .map(|usage| (usage.range, ReferenceCategory::empty())) - .collect_vec(), - ) - }) - .collect(); - Some(References { def: Some(vec![macro_nav_target(definition)]), refs, status }) -} - -fn references_status_from_macro_index(status: MacroReferenceIndexStatus) -> ReferencesStatus { - match status { - MacroReferenceIndexStatus::Complete => ReferencesStatus::Complete, - MacroReferenceIndexStatus::Partial { issue_count } => ReferencesStatus::Partial { - reason: ReferencesPartialReason::PreprocMacroIndex, - issue_count, - }, - } -} - -fn macro_param_nav_target(definition: MacroParamDefinition) -> NavTarget { - NavTarget { - file_id: definition.macro_definition.file_id, - full_range: definition.range, - focus_range: Some(definition.range), - name: Some(definition.name), - kind: None, - container_name: Some(definition.macro_definition.name), - description: Some("macro parameter".to_owned()), - } -} - -fn macro_nav_target(definition: MacroDefinition) -> NavTarget { - NavTarget { - file_id: definition.file_id, - full_range: definition.name_range, - focus_range: Some(definition.name_range), - name: Some(definition.name), - kind: None, - container_name: None, - description: Some("macro definition".to_owned()), - } -} - -#[cfg(test)] -mod tests { - use hir::preproc::MacroReferenceIndexStatus; - - use super::*; - - #[test] - fn macro_reference_index_status_maps_to_reference_status() { - assert_eq!( - references_status_from_macro_index(MacroReferenceIndexStatus::Complete), - ReferencesStatus::Complete - ); - - let status = references_status_from_macro_index(MacroReferenceIndexStatus::Partial { - issue_count: 1, - }); - - assert_eq!( - status, - ReferencesStatus::Partial { - reason: ReferencesPartialReason::PreprocMacroIndex, - issue_count: 1, - } - ); - assert!(status.is_partial()); - assert_eq!(status.issue_count(), 1); - } -} diff --git a/crates/ide/src/rename.rs b/crates/ide/src/rename.rs index 7bee6371..59cfcd33 100644 --- a/crates/ide/src/rename.rs +++ b/crates/ide/src/rename.rs @@ -1,13 +1,11 @@ use hir::{base_db::source_db::SourceDb, container::InFile, semantics::Semantics}; use nohash_hasher::IntMap; use rustc_hash::FxHashMap; -use smol_str::SmolStr; use syntax::{ - SyntaxAncestors, SyntaxTokenWithParent, TokenKind, + SyntaxAncestors, SyntaxTokenWithParent, ast::{self, AstNode, Expression, Name}, has_text_range::{HasTextRange, HasTextRangeIn}, match_ast, - token::TokenKindExt, }; use thiserror::Error; use utils::{line_index::TextRange, text_edit::TextEdit, uniq_vec::UniqVec}; @@ -17,11 +15,14 @@ use crate::{ FilePosition, ScopeVisibility, db::root_db::RootDb, definitions::{Definition, DefinitionClass, DefinitionOrigin}, + facts::{ + SemanticFacts, TargetQuery, + target::{SemanticTarget, TargetIntent}, + }, references::{ ReferencesConfig, search::{ReferenceToken, ReferencesCtx, SearchScope}, }, - semantic_target::{SemanticTarget, TargetIntent, resolve_semantic_target}, source_change::SourceChange, }; @@ -49,7 +50,7 @@ impl RenameConfig { self } - fn references_config( + pub(crate) fn references_config( &self, db: &RootDb, def: &Definition, @@ -95,138 +96,32 @@ pub struct RenameCollisionInfo { pub conflicts: usize, } -pub(crate) fn prepare_rename( - db: &RootDb, - position @ FilePosition { file_id, .. }: FilePosition, - config: RenameConfig, -) -> RenameResult { - let sema = Semantics::new(db); - let target = resolve_rename_target(&sema, position)?; - let _ = config.references_config(db, &target.selected_def, file_id)?; - Ok(target.range) -} - -pub(crate) fn rename( - db: &RootDb, - position @ FilePosition { file_id, .. }: FilePosition, - config: RenameConfig, - new_name: &str, -) -> RenameResult { - let sema = Semantics::new(db); - let ResolvedRenameTarget { selected_def, .. } = resolve_rename_target(&sema, position)?; - rename_definition(db, &sema, file_id, &config, &selected_def, new_name, None) -} - -pub(crate) fn rename_expansion_info( - db: &RootDb, - position: FilePosition, - config: RenameConfig, -) -> RenameResult { - let sema = Semantics::new(db); - let resolved = resolve_rename_target(&sema, position)?; - let targets = recursive_rename_targets(db, &sema, position.file_id, &config, resolved.targets)?; - let additional_symbols = targets.len().saturating_sub(1); - Ok(RecursiveRenameInfo { additional_symbols }) -} - -pub(crate) fn expanded_rename( - db: &RootDb, - position: FilePosition, - config: RenameConfig, - new_name: &str, -) -> RenameResult { - let sema = Semantics::new(db); - let resolved = resolve_rename_target(&sema, position)?; - let targets = recursive_rename_targets(db, &sema, position.file_id, &config, resolved.targets)?; - let mut rename_targets = UniqVec::<(), DefinitionOrigin>::default(); - for target in &targets { - rename_targets.push(target.def.origins(), ()); - } - let mut source_changes = SourceChange::default(); - - for target in &targets { - let changes = rename_definition_with_refs( - db, - &sema, - &target.def, - new_name, - Some(&rename_targets), - &target.refs, - &target.same_name_refs, - )?; - for (file_id, edit) in changes.text_edits { - source_changes - .insert_text_edit(file_id, edit) - .map_err(|_| RenameError::OverlappingEdits)?; - } - } - - Ok(source_changes) -} - -pub(crate) fn rename_conflict_info( - db: &RootDb, - position: FilePosition, - config: RenameConfig, - new_name: &str, - recursive: bool, -) -> RenameResult { - let sema = Semantics::new(db); - let resolved = resolve_rename_target(&sema, position)?; - let targets: Vec = if recursive { - recursive_rename_targets(db, &sema, position.file_id, &config, resolved.targets)? - .into_iter() - .map(|target| target.def) - .collect() - } else { - vec![resolved.selected_def] - }; - - let new_name = SmolStr::new(new_name); - let mut target_index = UniqVec::<(), DefinitionOrigin>::default(); - for target in &targets { - target_index.push(target.origins(), ()); - } - let mut conflicts = UniqVec::::default(); - for collision in targets.iter().flat_map(|target| target.origins()).filter_map(|origin| { - sema.resolve_name(origin.container_id(db), &new_name).map(Definition::from) - }) { - if collision.origins().iter().any(|origin| target_index.contains(origin)) { - continue; - } - conflicts.push(collision.origins(), collision); - } - - Ok(RenameCollisionInfo { conflicts: conflicts.len() }) +pub(crate) struct ResolvedRenameTarget { + pub(crate) range: TextRange, + pub(crate) selected_def: Definition, + pub(crate) targets: Vec, } -struct ResolvedRenameTarget { - range: TextRange, - selected_def: Definition, - targets: Vec, -} - -type ReferenceSearchResult = IntMap>; +pub(crate) type ReferenceSearchResult = IntMap>; -struct RecursiveRenameTarget { - def: Definition, - refs: ReferenceSearchResult, - same_name_refs: Vec, +pub(crate) struct RecursiveRenameTarget { + pub(crate) def: Definition, + pub(crate) refs: ReferenceSearchResult, + pub(crate) same_name_refs: Vec, } -fn resolve_rename_target( +pub(crate) fn resolve_rename_target( sema: &Semantics<'_, RootDb>, FilePosition { file_id, offset }: FilePosition, ) -> RenameResult { let hir_file_id = file_id.into(); let parsed_file = sema.parse_file(file_id); - let target = resolve_semantic_target( - sema.db, + let target = SemanticFacts::new(sema.db).target_at(TargetQuery { file_id, offset, - parsed_file.root(), - rename_token_precedence, - ); + intent: TargetIntent::Rename, + root: parsed_file.root(), + }); let SemanticTarget::Source(target) = target.unique_for_intent(TargetIntent::Rename).ok_or(RenameError::NoRefFound)? else { @@ -266,33 +161,20 @@ fn resolve_rename_target( } #[derive(Debug, Clone, PartialEq, Eq)] -struct SameNameConnection { +pub(crate) struct SameNameConnection { port: Definition, local: Definition, collapse_range: TextRange, } #[derive(Debug, Clone, PartialEq, Eq)] -struct SameNameConnectionRef { - file_id: FileId, - range: TextRange, - conn: SameNameConnection, +pub(crate) struct SameNameConnectionRef { + pub(crate) file_id: FileId, + pub(crate) range: TextRange, + pub(crate) conn: SameNameConnection, } -fn rename_definition( - db: &RootDb, - sema: &Semantics<'_, RootDb>, - request_file_id: FileId, - config: &RenameConfig, - def: &Definition, - new_name: &str, - rename_targets: Option<&UniqVec<(), DefinitionOrigin>>, -) -> RenameResult { - let refs = references_for_definition(db, sema, request_file_id, config, def)?; - rename_definition_with_refs(db, sema, def, new_name, rename_targets, &refs, &[]) -} - -fn references_for_definition( +pub(crate) fn references_for_definition( db: &RootDb, sema: &Semantics<'_, RootDb>, request_file_id: FileId, @@ -303,7 +185,7 @@ fn references_for_definition( Ok(ReferencesCtx::new(sema, def, refs_config).search()) } -fn rename_definition_with_refs( +pub(crate) fn rename_definition_with_refs( db: &RootDb, sema: &Semantics<'_, RootDb>, def: &Definition, @@ -353,7 +235,7 @@ fn rename_definition_with_refs( Ok(source_changes) } -fn recursive_rename_targets( +pub(crate) fn recursive_rename_targets( db: &RootDb, sema: &Semantics<'_, RootDb>, file_id: FileId, @@ -585,7 +467,3 @@ fn edits_from_refs( (file_id, text_edit.finish()) } - -fn rename_token_precedence(kind: TokenKind) -> usize { - usize::from(kind.name_like()) -} diff --git a/crates/ide/src/render.rs b/crates/ide/src/render.rs index b2ce904f..2ee74ef9 100644 --- a/crates/ide/src/render.rs +++ b/crates/ide/src/render.rs @@ -301,6 +301,10 @@ fn render_definition_title(db: &RootDb, origin: &DefinitionOrigin) -> Option "Typedef", DefinitionOrigin::Instance(_) => "Instance", DefinitionOrigin::Stmt(_) => "Statement", + DefinitionOrigin::PreprocMacro { .. } => "Macro", + DefinitionOrigin::PreprocMacroParam { .. } => "Macro parameter", + DefinitionOrigin::Include { .. } => "Include", + DefinitionOrigin::SourceToken { .. } => "Token", }; Some(format!("{kind} {}", inline_code(name.as_str()))) @@ -635,7 +639,11 @@ fn render_label_signature(db: &RootDb, origin: &DefinitionOrigin) -> Option return None, + | DefinitionOrigin::Decl(_) + | DefinitionOrigin::PreprocMacro { .. } + | DefinitionOrigin::PreprocMacroParam { .. } + | DefinitionOrigin::Include { .. } + | DefinitionOrigin::SourceToken { .. } => return None, }; Some(format!("{kind} {name}")) } diff --git a/crates/ide/src/semantic_index.rs b/crates/ide/src/semantic_index.rs index dc2ca6fd..cb83f741 100644 --- a/crates/ide/src/semantic_index.rs +++ b/crates/ide/src/semantic_index.rs @@ -29,9 +29,9 @@ use crate::{ }, }, definitions::{Definition, DefinitionClass, DefinitionOrigin}, + facts::target::{SemanticTarget, TargetIntent, resolve_semantic_target}, module_resolution::resolve_hir_instantiation_target, references::ReferenceCategory, - semantic_target::{SemanticTarget, TargetIntent, resolve_semantic_target}, }; #[derive(Debug, Clone, PartialEq, Eq, Hash)] diff --git a/crates/ide/src/verilog_2005.rs b/crates/ide/src/verilog_2005.rs index b51503d3..c82cb777 100644 --- a/crates/ide/src/verilog_2005.rs +++ b/crates/ide/src/verilog_2005.rs @@ -28,12 +28,18 @@ use utils::{ use vfs::{ChangeKind, ChangedFile, FileId, FileSet, VfsPath}; use crate::{ - FilePosition, ScopeVisibility, + FilePosition, FileRange, ScopeVisibility, analysis_host::AnalysisHost, completion::{CompletionItem, CompletionItemKind, context::TriggerChar}, db::root_db::RootDb, document_highlight::DocumentHighlightConfig, document_symbols::DocumentSymbol, + facts::{ + SemanticFacts, TargetQuery, + edit::{EditPlan, EditRequest}, + relation::{RelationKind, RelationQuery}, + target::{SemanticTarget, TargetIntent, include_symbols}, + }, folding_ranges::FoldingConfig, hover::{HoverConfig, HoverFormat}, references::{ReferencesConfig, search::SearchScope}, @@ -958,6 +964,327 @@ endconfig } } +#[test] +fn call_hierarchy_queries_module_relations_from_ide() { + let text = r#" +module top; + /*marker:child_ref*/child u_child(); +endmodule + +module /*marker:child_def*/child; + /*marker:leaf_ref*/leaf u_leaf(); +endmodule + +module /*marker:leaf_def*/leaf; +endmodule +"#; + let (host, file_id, _clean_text, markers) = setup_marked(text); + let analysis = host.make_analysis(); + + let items = analysis + .prepare_call_hierarchy(position(file_id, &markers, "child_def")) + .unwrap() + .expect("child call hierarchy item expected"); + let child = + items.into_iter().find(|item| item.name == "child").expect("child item should be prepared"); + + let incoming = analysis + .call_hierarchy_incoming( + child.clone(), + ReferencesConfig::new(ScopeVisibility::Public, None), + ) + .unwrap() + .expect("child incoming calls expected"); + assert!( + incoming.iter().any(|call| { + call.from.name == "top" + && call.from_ranges.iter().any(|range| range.range.start() == markers["child_ref"]) + }), + "incoming calls should include top instantiating child: {incoming:?}" + ); + + let outgoing = analysis + .call_hierarchy_outgoing(child, ReferencesConfig::new(ScopeVisibility::Public, None)) + .unwrap() + .expect("child outgoing calls expected"); + assert!( + outgoing.iter().any(|call| { + call.to.name == "leaf" + && call.from_ranges.iter().any(|range| range.range.start() == markers["leaf_ref"]) + }), + "outgoing calls should include child instantiating leaf: {outgoing:?}" + ); +} + +#[test] +fn semantic_facts_relation_model_reports_module_instantiations() { + let text = r#" +module top; + /*marker:child_ref*/child u_child(); +endmodule + +module /*marker:child_def*/child; + /*marker:leaf_ref*/leaf u_leaf(); +endmodule + +module /*marker:leaf_def*/leaf; +endmodule +"#; + let (host, file_id, _clean_text, markers) = setup_marked(text); + let facts = SemanticFacts::new(host.raw_db()); + let relations = facts.relations(); + let child = relations + .definition_symbols(position(file_id, &markers, "child_def")) + .expect("child definition symbol expected") + .into_iter() + .find(|symbol| symbol.name.as_deref() == Some("child")) + .expect("child symbol expected"); + let leaf = relations + .definition_symbols(position(file_id, &markers, "leaf_def")) + .expect("leaf definition symbol expected") + .into_iter() + .find(|symbol| symbol.name.as_deref() == Some("leaf")) + .expect("leaf symbol expected"); + let set = relations.relations(RelationQuery::Workspace { + kind: RelationKind::Instantiates, + config: ReferencesConfig::new(ScopeVisibility::Public, None), + }); + + assert!( + set.relations.iter().any(|relation| { + relation.target == child.id && relation.range.range.start() == markers["child_ref"] + }), + "workspace relations should include an instantiation of child: {set:?}" + ); + assert!( + set.relations.iter().any(|relation| { + relation.target == leaf.id && relation.range.range.start() == markers["leaf_ref"] + }), + "workspace relations should include an instantiation of leaf: {set:?}" + ); +} + +#[test] +fn semantic_facts_relation_model_reports_symbol_references() { + let text = r#" +module top; + logic /*marker:def*/sig; + assign /*marker:ref*/sig = 1'b0; +endmodule +"#; + let (host, file_id, _clean_text, markers) = setup_marked(text); + let facts = SemanticFacts::new(host.raw_db()); + let relations = facts.relations(); + let reference_range = + FileRange { file_id, range: marked_range(&markers, "ref", TextSize::of("sig")) }; + let set = relations.relations(RelationQuery::At { + position: position(file_id, &markers, "def"), + kind: RelationKind::References, + config: ReferencesConfig::new(ScopeVisibility::Public, None), + }); + + assert!( + set.relations.iter().any(|relation| { + relation.kind == RelationKind::References + && relation.source == relation.target + && relation.range == reference_range + }), + "reference relation facts should include the signal use: {set:?}" + ); + + let refs = relations + .references( + position(file_id, &markers, "def"), + ReferencesConfig::new(ScopeVisibility::Public, None), + ) + .expect("references presentation expected"); + assert!( + refs.iter().any(|refs| { + refs.refs.get(&file_id).is_some_and(|ranges| { + ranges.iter().any(|(range, _)| *range == reference_range.range) + }) + }), + "references presentation should be backed by relation facts: {refs:?}" + ); +} + +#[test] +fn semantic_facts_relation_model_reports_preproc_macro_references() { + let text = r#" +`define /*marker:def*/FOO 1 +module top; + localparam int x = /*marker:usage_range*/`/*marker:usage*/FOO; +endmodule +"#; + let (host, file_id, _clean_text, markers) = setup_marked(text); + let facts = SemanticFacts::new(host.raw_db()); + let relations = facts.relations(); + let usage_range = + FileRange { file_id, range: marked_range(&markers, "usage_range", TextSize::of("`FOO")) }; + let set = relations.relations(RelationQuery::At { + position: position(file_id, &markers, "usage"), + kind: RelationKind::References, + config: ReferencesConfig::new(ScopeVisibility::Public, None), + }); + + assert!( + set.relations.iter().any(|relation| { + relation.kind == RelationKind::References + && matches!(relation.target, crate::facts::symbol::SymbolId::PreprocMacro { .. }) + && relation.range == usage_range + }), + "macro usage should be represented as reference relation facts: {set:?}" + ); +} + +#[test] +fn semantic_facts_relation_model_reports_control_flow_token_references() { + let text = r#" +module top; + initial /*marker:begin*/begin + /*marker:end*/end +endmodule +"#; + let (host, file_id, _clean_text, markers) = setup_marked(text); + let facts = SemanticFacts::new(host.raw_db()); + let relations = facts.relations(); + let begin_range = + FileRange { file_id, range: marked_range(&markers, "begin", TextSize::of("begin")) }; + let end_range = + FileRange { file_id, range: marked_range(&markers, "end", TextSize::of("end")) }; + let set = relations.relations(RelationQuery::At { + position: position(file_id, &markers, "begin"), + kind: RelationKind::References, + config: ReferencesConfig::new(ScopeVisibility::Public, None), + }); + + assert!( + set.relations.iter().any(|relation| { + matches!(relation.target, crate::facts::symbol::SymbolId::SourceToken { .. }) + && relation.range == begin_range + }) && set.relations.iter().any(|relation| { + matches!(relation.target, crate::facts::symbol::SymbolId::SourceToken { .. }) + && relation.range == end_range + }), + "control-flow pair tokens should be represented as reference relation facts: {set:?}" + ); +} + +#[test] +fn semantic_facts_edit_plan_models_rename_symbols_and_ranges() { + let text = r#" +module top; + logic /*marker:def*/sig; + assign /*marker:ref*/sig = 1'b0; +endmodule +"#; + let (host, file_id, _clean_text, markers) = setup_marked(text); + let facts = SemanticFacts::new(host.raw_db()); + let plan = facts + .edit_plan(EditRequest::Rename { + position: position(file_id, &markers, "ref"), + config: RenameConfig::workspace(ScopeVisibility::Public), + new_name: "renamed_sig", + }) + .expect("rename edit plan expected"); + let EditPlan::Rename(plan) = plan else { + panic!("rename request should produce a rename edit plan"); + }; + let definition_range = + FileRange { file_id, range: marked_range(&markers, "def", TextSize::of("sig")) }; + let reference_range = + FileRange { file_id, range: marked_range(&markers, "ref", TextSize::of("sig")) }; + + assert!(!plan.recursive); + assert_eq!(plan.new_name, "renamed_sig"); + assert!(!plan.target.selected_symbols.is_empty()); + assert!( + plan.target + .selected_symbols + .iter() + .all(|symbol| plan.target.related_symbols.contains(symbol)), + "selected symbols should be part of the related rename target set: {plan:?}" + ); + assert!( + plan.symbols.iter().any(|symbol| { + symbol.symbol.definition_ranges.contains(&definition_range) + && symbol.symbol.reference_ranges.contains(&reference_range) + }), + "rename plan should expose definition and reference ranges: {plan:?}" + ); + assert!( + plan.change.text_edits.contains_key(&file_id), + "rename plan should carry final source edits: {plan:?}" + ); +} + +#[test] +fn semantic_facts_models_preproc_macro_target_as_symbol() { + let text = r#" +`define /*marker:def*/FOO 1 +module top; + localparam int x = `/*marker:usage*/FOO; +endmodule +"#; + let (host, file_id, _clean_text, markers) = setup_marked(text); + let db = host.raw_db(); + let parsed = db.parse_src(file_id); + let facts = SemanticFacts::new(db); + let target = facts.target_at(TargetQuery { + file_id, + offset: markers["usage"], + intent: TargetIntent::Navigate, + root: parsed.root(), + }); + let Some(SemanticTarget::PreprocMacro(target)) = + target.unique_for_intent(TargetIntent::Navigate) + else { + panic!("macro usage should resolve to a preproc macro target"); + }; + let symbols = target.symbols(); + let info = facts.symbol(symbols[0]).expect("macro symbol info expected"); + + assert_eq!(info.kind, crate::SymbolKind::Macro); + assert_eq!(info.name.as_deref(), Some("FOO")); + assert_eq!( + info.selection_range, + Some(FileRange { file_id, range: marked_range(&markers, "def", TextSize::of("FOO")) }) + ); +} + +#[test] +fn semantic_facts_models_include_target_as_symbol() { + let fixture = setup_include_macro_project( + r#" +`include "/*marker:include*/defs.vh" +module top; +endmodule +"#, + r#" +`define HEADER_VALUE 1 +"#, + ); + let db = fixture.host.raw_db(); + let parsed = db.parse_src(fixture.top_file_id); + let facts = SemanticFacts::new(db); + let target = facts.target_at(TargetQuery { + file_id: fixture.top_file_id, + offset: fixture.top_markers["include"], + intent: TargetIntent::Navigate, + root: parsed.root(), + }); + let Some(SemanticTarget::Include(includes)) = target.unique_for_intent(TargetIntent::Navigate) + else { + panic!("include directive should resolve to an include target"); + }; + let symbols = include_symbols(&includes); + let info = facts.symbol(symbols[0]).expect("include symbol info expected"); + + assert_eq!(info.kind, crate::SymbolKind::Include); + assert!(info.name.as_deref().is_some_and(|name| name.contains("defs.vh"))); + assert_eq!(info.definition_range.map(|range| range.file_id), Some(fixture.top_file_id)); +} + #[test] fn verilog_2005_completion_keywords_cover_core_contexts() { let text = r#" diff --git a/src/global_state/handlers/request/navigation.rs b/src/global_state/handlers/request/navigation.rs index 4417e2a4..06b0b155 100644 --- a/src/global_state/handlers/request/navigation.rs +++ b/src/global_state/handlers/request/navigation.rs @@ -1,6 +1,7 @@ use ide::{ - FileRange, SymbolKind, navigation_target::NavTarget, references::References, - semantic_index::ModuleCallItem, + FileRange, SymbolKind, + call_hierarchy::{CallHierarchyItem, IncomingCall, OutgoingCall}, + references::References, }; use itertools::Itertools; @@ -48,17 +49,16 @@ pub(crate) fn handle_prepare_call_hierarchy( params: lsp_types::CallHierarchyPrepareParams, ) -> anyhow::Result>> { let position = from_proto::file_position(&snap, params.text_document_position_params)?; - let Some(nav_info) = snap.analysis.goto_definition(position)? else { + let Some(items) = snap.analysis.prepare_call_hierarchy(position)? else { return Ok(None); }; - let items = nav_info - .info + let items = items .into_iter() - .filter_map(|nav| call_hierarchy_item_for_nav(&snap, nav).transpose()) + .map(|item| lsp_call_hierarchy_item(&snap, item)) .collect::>>()? .into_iter() - .unique_by(call_hierarchy_item_key) + .unique_by(lsp_call_hierarchy_item_key) .collect_vec(); Ok((!items.is_empty()).then_some(items)) @@ -68,22 +68,15 @@ pub(crate) fn handle_call_hierarchy_incoming( snap: GlobalStateSnapshot, params: lsp_types::CallHierarchyIncomingCallsParams, ) -> anyhow::Result>> { - let target = params.item; - let target_file_id = from_proto::file_id(&snap, &target.uri)?; - let target_line_info = snap.line_info(target_file_id)?; - let target_selection_range = from_proto::text_range(&target_line_info, target.selection_range)?; - - let mut groups = Vec::<(lsp_types::CallHierarchyItem, Vec)>::new(); - 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 + let target = call_hierarchy_item_from_lsp(&snap, params.item)?; + let config = snap.config.references(); + let Some(calls) = snap.analysis.call_hierarchy_incoming(target, config)? else { + return Ok(None); + }; + let calls = calls .into_iter() - .map(|(from, from_ranges)| lsp_types::CallHierarchyIncomingCall { from, from_ranges }) - .collect_vec(); + .map(|call| lsp_incoming_call(&snap, call)) + .collect::>>()?; Ok((!calls.is_empty()).then_some(calls)) } @@ -91,25 +84,15 @@ pub(crate) fn handle_call_hierarchy_outgoing( snap: GlobalStateSnapshot, params: lsp_types::CallHierarchyOutgoingCallsParams, ) -> anyhow::Result>> { - 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_selection_range = from_proto::text_range(&caller_line_info, caller.selection_range)?; - - let mut groups = Vec::<(lsp_types::CallHierarchyItem, Vec)>::new(); - 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; - } - - push_call_range(&mut groups, callee, to_proto::range(&caller_line_info, edge.call_range)); - } - - let calls = groups + let caller = call_hierarchy_item_from_lsp(&snap, params.item)?; + let config = snap.config.references(); + let Some(calls) = snap.analysis.call_hierarchy_outgoing(caller, config)? else { + return Ok(None); + }; + let calls = calls .into_iter() - .map(|(to, from_ranges)| lsp_types::CallHierarchyOutgoingCall { to, from_ranges }) - .collect_vec(); + .map(|call| lsp_outgoing_call(&snap, call)) + .collect::>>()?; Ok((!calls.is_empty()).then_some(calls)) } @@ -168,89 +151,84 @@ pub(crate) fn handle_references( Ok(Some(locations)) } -fn call_hierarchy_item_for_nav( +fn lsp_incoming_call( snap: &GlobalStateSnapshot, - nav: NavTarget, -) -> anyhow::Result> { - let Some(kind) = nav.kind else { - return Ok(None); - }; - if !is_call_hierarchy_kind(kind) { - return Ok(None); - } - - let line_info = snap.line_info(nav.file_id)?; - let uri = to_proto::url(snap, nav.file_id)?; - let range = to_proto::range(&line_info, nav.full_range); - let selection_range = to_proto::range(&line_info, nav.focus_or_full_range()); - let name = nav - .name - .map(|name| name.to_string()) - .unwrap_or_else(|| nav.description.clone().unwrap_or_else(|| "".to_owned())); - let detail = nav.container_name.map(|name| name.to_string()).or(nav.description); + IncomingCall { from, from_ranges }: IncomingCall, +) -> anyhow::Result { + Ok(lsp_types::CallHierarchyIncomingCall { + from: lsp_call_hierarchy_item(snap, from)?, + from_ranges: lsp_call_ranges(snap, from_ranges)?, + }) +} - Ok(Some(lsp_types::CallHierarchyItem { - name, - kind: to_proto::symbol_kind(kind), - tags: None, - detail, - uri, - range, - selection_range, - data: None, - })) +fn lsp_outgoing_call( + snap: &GlobalStateSnapshot, + OutgoingCall { to, from_ranges }: OutgoingCall, +) -> anyhow::Result { + Ok(lsp_types::CallHierarchyOutgoingCall { + to: lsp_call_hierarchy_item(snap, to)?, + from_ranges: lsp_call_ranges(snap, from_ranges)?, + }) } -fn call_hierarchy_item_for_module( +fn lsp_call_hierarchy_item( snap: &GlobalStateSnapshot, - module: &ModuleCallItem, + item: CallHierarchyItem, ) -> anyhow::Result { - let uri = to_proto::url(snap, module.file_id)?; - let line_info = snap.line_info(module.file_id)?; + let line_info = snap.line_info(item.full_range.file_id)?; + let uri = to_proto::url(snap, item.full_range.file_id)?; Ok(lsp_types::CallHierarchyItem { - name: module.name.clone(), - kind: to_proto::symbol_kind(SymbolKind::Module), + name: item.name, + kind: to_proto::symbol_kind(item.kind), tags: None, - detail: None, + detail: item.detail, uri, - range: to_proto::range(&line_info, module.full_range), - selection_range: to_proto::range(&line_info, module.name_range), + range: to_proto::range(&line_info, item.full_range.range), + selection_range: to_proto::range(&line_info, item.selection_range.range), data: None, }) } -fn is_call_hierarchy_kind(kind: SymbolKind) -> bool { - matches!(kind, SymbolKind::Module) +fn call_hierarchy_item_from_lsp( + 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)?; + Ok(CallHierarchyItem { + symbol: None, + name: item.name, + kind: symbol_kind_from_lsp(item.kind), + detail: item.detail, + full_range: FileRange { file_id, range: from_proto::text_range(&line_info, item.range)? }, + selection_range: FileRange { + file_id, + range: from_proto::text_range(&line_info, item.selection_range)?, + }, + }) } -fn push_call_range( - groups: &mut Vec<(lsp_types::CallHierarchyItem, Vec)>, - item: lsp_types::CallHierarchyItem, - range: lsp_types::Range, -) { - if let Some((_, ranges)) = - groups.iter_mut().find(|(existing, _)| same_call_hierarchy_item(existing, &item)) - { - if !ranges.contains(&range) { - ranges.push(range); - } - return; +fn symbol_kind_from_lsp(kind: lsp_types::SymbolKind) -> SymbolKind { + match kind { + lsp_types::SymbolKind::MODULE => SymbolKind::Module, + _ => SymbolKind::Unknown, } - - groups.push((item, vec![range])); } -fn same_call_hierarchy_item( - lhs: &lsp_types::CallHierarchyItem, - rhs: &lsp_types::CallHierarchyItem, -) -> bool { - lhs.name == rhs.name - && lhs.uri == rhs.uri - && lhs.range == rhs.range - && lhs.selection_range == rhs.selection_range +fn lsp_call_ranges( + snap: &GlobalStateSnapshot, + ranges: Vec, +) -> anyhow::Result> { + ranges + .into_iter() + .map(|range| { + let line_info = snap.line_info(range.file_id)?; + Ok(to_proto::range(&line_info, range.range)) + }) + .collect() } -fn call_hierarchy_item_key( +fn lsp_call_hierarchy_item_key( item: &lsp_types::CallHierarchyItem, ) -> (String, lsp_types::Url, lsp_types::Range, lsp_types::Range) { (item.name.clone(), item.uri.clone(), item.range, item.selection_range) diff --git a/src/lsp_ext/to_proto.rs b/src/lsp_ext/to_proto.rs index d13b6fb6..c71e1df9 100644 --- a/src/lsp_ext/to_proto.rs +++ b/src/lsp_ext/to_proto.rs @@ -1,7 +1,6 @@ use std::sync::atomic::{AtomicU32, Ordering}; use anyhow::{Context, Error}; -use hir::container::InFile; use ide::{ FilePosition, FileRange, SymbolKind, code_action::{CodeAction, CodeActionKind}, @@ -275,6 +274,9 @@ pub(crate) fn symbol_kind(symbol_kind: SymbolKind) -> lsp_types::SymbolKind { SymbolKind::Specify => LspSymbolKind::NAMESPACE, SymbolKind::Interface => LspSymbolKind::INTERFACE, SymbolKind::Library => LspSymbolKind::NAMESPACE, + SymbolKind::Macro => LspSymbolKind::CONSTANT, + SymbolKind::MacroParam => LspSymbolKind::TYPE_PARAMETER, + SymbolKind::Include => LspSymbolKind::FILE, SymbolKind::Region => LspSymbolKind::NAMESPACE, SymbolKind::Unknown => LspSymbolKind::NAMESPACE, } @@ -594,10 +596,7 @@ pub(crate) fn inlay_hint( value: tooltip.into(), }) }), - location: target_location.and_then(|InFile { value, file_id }| { - let file_range = FileRange { file_id: file_id.file_id(), range: value }; - self::location(snap, file_range).ok() - }), + location: target_location.and_then(|file_range| self::location(snap, file_range).ok()), command: None, };