From eea7810ff4b751ac5a1f9fb5576d9b15837e341d Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Thu, 25 Jun 2026 23:32:09 +0800 Subject: [PATCH 1/8] refactor(ide): centralize semantic target resolution --- crates/ide/src/document_highlight.rs | 15 +- crates/ide/src/goto_declaration.rs | 13 +- crates/ide/src/goto_definition.rs | 108 ++------ crates/ide/src/hover.rs | 60 ++-- crates/ide/src/hover/include.rs | 13 +- crates/ide/src/hover/macro_hover.rs | 51 +--- crates/ide/src/lib.rs | 1 + crates/ide/src/references.rs | 52 ++-- crates/ide/src/references/preproc.rs | 94 +++---- crates/ide/src/semantic_target.rs | 399 +++++++++++++++++++++++++++ 10 files changed, 527 insertions(+), 279 deletions(-) create mode 100644 crates/ide/src/semantic_target.rs diff --git a/crates/ide/src/document_highlight.rs b/crates/ide/src/document_highlight.rs index 98fe48b3..01463668 100644 --- a/crates/ide/src/document_highlight.rs +++ b/crates/ide/src/document_highlight.rs @@ -11,6 +11,7 @@ use crate::{ self, ReferenceCategory, ReferencesConfig, search::{ReferencesCtx, SearchScope}, }, + semantic_target::{SemanticTarget, TargetCapability, TargetIntent, resolve_semantic_target}, }; #[derive(Debug, Clone)] @@ -32,16 +33,18 @@ 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 root = parsed_file.root()?; - let tokens = crate::source_targets::source_target_at_offset( + let target = resolve_semantic_target( db, file_id, - root, offset, + parsed_file.root(), + TargetIntent::Highlight, token_precedence, - )? - .resolved()? - .into_tokens(); + )?; + let SemanticTarget::Source(target) = target.into_target(TargetCapability::HIGHLIGHT)? else { + return None; + }; + let tokens = target.into_tokens(); let highlights = tokens .into_iter() .filter_map(|token| highlight_for_token(&sema, file_id, hir_file_id, token, config.clone())) diff --git a/crates/ide/src/goto_declaration.rs b/crates/ide/src/goto_declaration.rs index 3835c37d..626faf09 100644 --- a/crates/ide/src/goto_declaration.rs +++ b/crates/ide/src/goto_declaration.rs @@ -7,6 +7,7 @@ use crate::{ definitions::DefinitionClass, goto_definition, navigation_target::{NavTarget, ToNav}, + semantic_target::{SemanticTarget, TargetCapability, TargetIntent, resolve_semantic_target}, }; pub(crate) fn goto_declaration( @@ -16,15 +17,17 @@ 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 root = parsed_file.root()?; - let target = crate::source_targets::source_target_at_offset( + let target = resolve_semantic_target( db, file_id, - root, offset, + parsed_file.root(), + TargetIntent::Navigate, goto_definition::token_precedence, - )? - .resolved()?; + )?; + let SemanticTarget::Source(target) = target.into_target(TargetCapability::NAVIGATE)? else { + return None; + }; let (range, tokens) = target.into_parts(); let origins = tokens diff --git a/crates/ide/src/goto_definition.rs b/crates/ide/src/goto_definition.rs index 383c668b..902991b2 100644 --- a/crates/ide/src/goto_definition.rs +++ b/crates/ide/src/goto_definition.rs @@ -2,17 +2,12 @@ use hir::{ base_db::source_db::SourceDb, container::InFile, file::HirFileId, - preproc::{ - 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, - }, + preproc::{IncludeDirective, IncludeTarget, MacroDefinition, MacroParamDefinition}, semantics::Semantics, }; use itertools::Itertools; use syntax::{ - SyntaxNode, SyntaxTokenWithParent, TokenKind, + SyntaxTokenWithParent, TokenKind, token::{TokenKindExt, pair_token}, }; use utils::line_index::{TextRange, TextSize}; @@ -23,60 +18,40 @@ use crate::{ db::root_db::RootDb, definitions::DefinitionClass, navigation_target::{NavTarget, ToNav}, - source_targets::{SourceTarget, source_target_at_offset}, + semantic_target::{ + PreprocMacroTarget, SemanticTarget, SemanticTargetResolution, TargetCapability, + TargetIntent, resolve_semantic_target, + }, + source_targets::SourceTarget, }; -enum DefinitionTarget<'tree> { - Preproc(Box), - Include(Vec), - Source(SourceTarget<'tree>), -} - -enum PreprocDefinitionTarget { - ParamDefinition(MacroParamDefinition), - ParamReference(MacroParamReferenceDefinitions), - Definition(MacroDefinition), - Reference(MacroReferenceDefinitions), -} - pub(crate) fn goto_definition( db: &RootDb, FilePosition { file_id, offset }: FilePosition, ) -> Option>> { let sema = Semantics::new(db); let parsed_file = sema.parse_file(file_id); - let target = dispatch_definition_target(db, file_id, offset, parsed_file.root())?; + let target = resolve_semantic_target( + db, + file_id, + offset, + parsed_file.root(), + TargetIntent::Navigate, + token_precedence, + )?; render_definition_target(db, file_id, &sema, target) } -fn dispatch_definition_target<'tree>( - db: &RootDb, - file_id: FileId, - offset: TextSize, - root: Option>, -) -> Option> { - if let Some(target) = dispatch_preproc_definition_target(db, file_id, offset) { - return Some(DefinitionTarget::Preproc(Box::new(target))); - } - if let Some(includes) = dispatch_include_definition_target(db, file_id, offset) { - return Some(DefinitionTarget::Include(includes)); - } - let root = root?; - let target = - source_target_at_offset(db, file_id, root, offset, token_precedence)?.resolved()?; - Some(DefinitionTarget::Source(target)) -} - fn render_definition_target( db: &RootDb, file_id: FileId, sema: &Semantics, - target: DefinitionTarget<'_>, + target: SemanticTargetResolution<'_>, ) -> Option>> { - match target { - DefinitionTarget::Preproc(target) => render_preproc_definition_target(*target), - DefinitionTarget::Include(includes) => render_include_definition_target(db, includes), - DefinitionTarget::Source(target) => { + match target.into_target(TargetCapability::NAVIGATE)? { + SemanticTarget::PreprocMacro(target) => render_preproc_definition_target(target), + SemanticTarget::Include(includes) => render_include_definition_target(db, includes), + SemanticTarget::Source(target) => { render_source_definition_target(db, file_id, sema, target) } } @@ -120,47 +95,23 @@ fn nav_targets_for_token( }) } -fn dispatch_preproc_definition_target( - db: &RootDb, - file_id: FileId, - offset: TextSize, -) -> Option { - if let Ok(Some(definition)) = macro_param_definition_at(db, file_id, offset) { - return Some(PreprocDefinitionTarget::ParamDefinition(definition)); - } - - if let Ok(Some(resolution)) = macro_param_reference_definitions_at(db, file_id, offset) { - return Some(PreprocDefinitionTarget::ParamReference(resolution)); - } - - if let Ok(Some(definition)) = macro_definition_at(db, file_id, offset) { - return Some(PreprocDefinitionTarget::Definition(definition)); - } - - if let Ok(Some(resolution)) = macro_reference_definitions_at(db, file_id, offset) { - return Some(PreprocDefinitionTarget::Reference(resolution)); - } - - None -} - fn render_preproc_definition_target( - target: PreprocDefinitionTarget, + target: PreprocMacroTarget, ) -> Option>> { match target { - PreprocDefinitionTarget::ParamDefinition(definition) => { + PreprocMacroTarget::ParamDefinition(definition) => { Some(RangeInfo::new(definition.range, vec![macro_param_nav_target(definition)])) } - PreprocDefinitionTarget::ParamReference(resolution) => { + PreprocMacroTarget::ParamReference(resolution) => { let reference_range = resolution.range; let targets = resolution.definitions.into_iter().map(macro_param_nav_target).collect_vec(); (!targets.is_empty()).then_some(RangeInfo::new(reference_range, targets)) } - PreprocDefinitionTarget::Definition(definition) => { + PreprocMacroTarget::Definition(definition) => { Some(RangeInfo::new(definition.name_range, vec![macro_nav_target(definition)])) } - PreprocDefinitionTarget::Reference(resolution) => { + PreprocMacroTarget::Reference(resolution) => { let reference_range = resolution.range; let targets = resolution.definitions.into_iter().map(macro_nav_target).collect_vec(); (!targets.is_empty()).then_some(RangeInfo::new(reference_range, targets)) @@ -192,15 +143,6 @@ fn macro_nav_target(definition: MacroDefinition) -> NavTarget { } } -fn dispatch_include_definition_target( - db: &RootDb, - file_id: FileId, - offset: TextSize, -) -> Option> { - let includes = include_directives_at(db, file_id, offset).ok()?; - (!includes.is_empty()).then_some(includes) -} - fn render_include_definition_target( db: &RootDb, includes: Vec, diff --git a/crates/ide/src/hover.rs b/crates/ide/src/hover.rs index b594e6bd..061e35cd 100644 --- a/crates/ide/src/hover.rs +++ b/crates/ide/src/hover.rs @@ -3,7 +3,7 @@ use hir::{ semantics::Semantics, }; use syntax::{ - SyntaxNode, SyntaxTokenWithParent, TokenKind, + SyntaxTokenWithParent, TokenKind, ast::{self, AstNode}, has_text_range::HasTextRange, token::TokenKindExt, @@ -20,15 +20,16 @@ use crate::{ db::root_db::RootDb, definitions::DefinitionClass, hover::{ - include::{dispatch_include_hover_target, render_include_hover}, - macro_hover::{ - MacroHoverTarget, dispatch_macro_hover_target, render_macro_hover_target, - with_expanded_macro_hover, - }, + include::render_include_hover, + macro_hover::{render_macro_hover_target, with_expanded_macro_hover}, }, markup::{Markup, inline_code}, render, - source_targets::{SourceTarget, source_target_at_offset}, + semantic_target::{ + SemanticTarget, SemanticTargetResolution, TargetCapability, TargetIntent, + resolve_semantic_target, + }, + source_targets::SourceTarget, }; mod include; @@ -37,12 +38,6 @@ mod macro_hover; #[cfg(test)] use macro_hover::macro_expansion_hover_text; -enum HoverTarget<'tree> { - Macro(Box), - Include(Vec), - Source(SourceTarget<'tree>), -} - #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum HoverFormat { Markdown, @@ -61,39 +56,30 @@ pub(crate) fn hover( ) -> Option> { let sema = Semantics::new(db); let parsed_file = sema.parse_file(file_id); - let target = dispatch_hover_target(db, file_id, offset, parsed_file.root())?; + let target = resolve_semantic_target( + db, + file_id, + offset, + parsed_file.root(), + TargetIntent::Describe, + token_precedence, + )?; render_hover_target(db, file_id, offset, &sema, target) } -fn dispatch_hover_target<'tree>( - db: &RootDb, - file_id: FileId, - offset: TextSize, - root: Option>, -) -> Option> { - if let Some(macro_target) = dispatch_macro_hover_target(db, file_id, offset) { - return Some(HoverTarget::Macro(Box::new(macro_target))); - } - if let Some(includes) = dispatch_include_hover_target(db, file_id, offset) { - return Some(HoverTarget::Include(includes)); - } - let root = root?; - let target = - source_target_at_offset(db, file_id, root, offset, token_precedence)?.resolved()?; - Some(HoverTarget::Source(target)) -} - fn render_hover_target( db: &RootDb, file_id: FileId, offset: TextSize, sema: &Semantics, - target: HoverTarget<'_>, + target: SemanticTargetResolution<'_>, ) -> Option> { - match target { - HoverTarget::Macro(target) => render_macro_hover_target(db, file_id, offset, *target), - HoverTarget::Include(includes) => render_include_hover(db, includes), - HoverTarget::Source(target) => { + match target.into_target(TargetCapability::DESCRIBE)? { + SemanticTarget::PreprocMacro(target) => { + render_macro_hover_target(db, file_id, offset, target) + } + SemanticTarget::Include(includes) => render_include_hover(db, includes), + SemanticTarget::Source(target) => { let hover = hover_for_source_target(sema, file_id.into(), target)?; Some(with_expanded_macro_hover(db, file_id, offset, hover)) } diff --git a/crates/ide/src/hover/include.rs b/crates/ide/src/hover/include.rs index e650d793..764507fc 100644 --- a/crates/ide/src/hover/include.rs +++ b/crates/ide/src/hover/include.rs @@ -1,6 +1,4 @@ -use hir::preproc::{IncludeDirective, IncludeTarget, include_directives_at}; -use utils::line_index::TextSize; -use vfs::FileId; +use hir::preproc::{IncludeDirective, IncludeTarget}; use crate::{ RangeInfo, @@ -9,15 +7,6 @@ use crate::{ render, }; -pub(super) fn dispatch_include_hover_target( - db: &RootDb, - file_id: FileId, - offset: TextSize, -) -> Option> { - let includes = include_directives_at(db, file_id, offset).ok()?; - (!includes.is_empty()).then_some(includes) -} - pub(super) fn render_include_hover( db: &RootDb, includes: Vec, diff --git a/crates/ide/src/hover/macro_hover.rs b/crates/ide/src/hover/macro_hover.rs index e57afaa8..214826fc 100644 --- a/crates/ide/src/hover/macro_hover.rs +++ b/crates/ide/src/hover/macro_hover.rs @@ -1,12 +1,7 @@ -use hir::preproc::{ - MacroDefinition, MacroParamDefinition, MacroParamReferenceDefinitions, - MacroReferenceDefinitions, macro_definition_at, macro_param_definition_at, - macro_param_reference_definitions_at, macro_reference_definitions_at, -}; use utils::line_index::TextSize; use vfs::FileId; -use crate::{RangeInfo, db::root_db::RootDb, markup::Markup}; +use crate::{RangeInfo, db::root_db::RootDb, markup::Markup, semantic_target::PreprocMacroTarget}; mod expansion; mod markup; @@ -15,60 +10,26 @@ mod markup; pub(super) use expansion::macro_expansion_hover_text; pub(super) use expansion::with_expanded_macro_hover; -pub(super) enum MacroHoverTarget { - ParamDefinition(MacroParamDefinition), - ParamReference(MacroParamReferenceDefinitions), - Definition(MacroDefinition), - Reference(MacroReferenceDefinitions), -} - -pub(super) fn dispatch_macro_hover_target( - db: &RootDb, - file_id: FileId, - offset: TextSize, -) -> Option { - if let Ok(Some(definition)) = macro_param_definition_at(db, file_id, offset) { - return Some(MacroHoverTarget::ParamDefinition(definition)); - } - - if let Ok(Some(param_resolution)) = macro_param_reference_definitions_at(db, file_id, offset) { - if param_resolution.definitions.is_empty() { - return None; - } - return Some(MacroHoverTarget::ParamReference(param_resolution)); - } - - if let Ok(Some(definition)) = macro_definition_at(db, file_id, offset) { - return Some(MacroHoverTarget::Definition(definition)); - } - - if let Ok(Some(resolution)) = macro_reference_definitions_at(db, file_id, offset) { - return Some(MacroHoverTarget::Reference(resolution)); - } - - None -} - pub(super) fn render_macro_hover_target( db: &RootDb, file_id: FileId, offset: TextSize, - target: MacroHoverTarget, + target: PreprocMacroTarget, ) -> Option> { match target { - MacroHoverTarget::ParamDefinition(definition) => Some(RangeInfo::new( + PreprocMacroTarget::ParamDefinition(definition) => Some(RangeInfo::new( definition.range, markup::macro_param_definition_markup(&definition), )), - MacroHoverTarget::ParamReference(param_resolution) => Some(RangeInfo::new( + PreprocMacroTarget::ParamReference(param_resolution) => Some(RangeInfo::new( param_resolution.range, markup::macro_param_definitions_markup(¶m_resolution.definitions), )), - MacroHoverTarget::Definition(definition) => Some(RangeInfo::new( + PreprocMacroTarget::Definition(definition) => Some(RangeInfo::new( definition.name_range, markup::macro_definition_markup(db, file_id, &definition), )), - MacroHoverTarget::Reference(resolution) => { + PreprocMacroTarget::Reference(resolution) => { if resolution.definitions.is_empty() { return expansion::expanded_macro_hover(db, file_id, offset, Some(&resolution)); } diff --git a/crates/ide/src/lib.rs b/crates/ide/src/lib.rs index 3df27baf..a47c820a 100644 --- a/crates/ide/src/lib.rs +++ b/crates/ide/src/lib.rs @@ -40,6 +40,7 @@ pub mod range; pub mod references; pub mod rename; pub mod selection_ranges; +pub(crate) mod semantic_target; pub mod semantic_tokens; pub mod signature_help; pub(crate) mod source_targets; diff --git a/crates/ide/src/references.rs b/crates/ide/src/references.rs index c299c5ee..eeb61afb 100644 --- a/crates/ide/src/references.rs +++ b/crates/ide/src/references.rs @@ -3,32 +3,29 @@ use itertools::Itertools; use nohash_hasher::IntMap; use search::{ReferencesCtx, SearchScope}; use syntax::{ - SyntaxNode, SyntaxTokenWithParent, TokenKind, + SyntaxTokenWithParent, TokenKind, has_text_range::HasTextRange, token::{TokenKindExt, pair_token}, }; -use utils::line_index::{TextRange, TextSize}; +use utils::line_index::TextRange; use vfs::FileId; -use self::preproc::{ - PreprocReferencesTarget, dispatch_preproc_references_target, render_preproc_references_target, -}; +use self::preproc::render_preproc_references_target; use crate::{ FilePosition, ScopeVisibility, db::root_db::RootDb, definitions::{Definition, DefinitionClass}, navigation_target::{NavTarget, ToNav}, - source_targets::{SourceTarget, source_target_at_offset}, + semantic_target::{ + SemanticTarget, SemanticTargetResolution, TargetCapability, TargetIntent, + resolve_semantic_target, + }, + source_targets::SourceTarget, }; mod preproc; pub(crate) mod search; -enum ReferencesTarget<'tree> { - Preproc(PreprocReferencesTarget), - Source(SourceTarget<'tree>), -} - bitflags::bitflags! { #[derive(Copy, Clone, Default, PartialEq, Eq, Hash, Debug)] pub struct ReferenceCategory: u8 { @@ -98,37 +95,30 @@ pub(crate) fn references( ) -> Option> { let sema = Semantics::new(db); let parsed_file = sema.parse_file(file_id); - let target = dispatch_references_target(db, file_id, offset, parsed_file.root())?; + let target = resolve_semantic_target( + db, + file_id, + offset, + parsed_file.root(), + TargetIntent::FindReferences, + token_precedence, + )?; render_references_target(db, file_id, &sema, target, config) } -fn dispatch_references_target<'tree>( - db: &RootDb, - file_id: FileId, - offset: TextSize, - root: Option>, -) -> Option> { - if let Some(target) = dispatch_preproc_references_target(db, file_id, offset) { - return Some(ReferencesTarget::Preproc(target)); - } - let root = root?; - let target = - source_target_at_offset(db, file_id, root, offset, token_precedence)?.resolved()?; - Some(ReferencesTarget::Source(target)) -} - fn render_references_target( db: &RootDb, file_id: FileId, sema: &Semantics, - target: ReferencesTarget<'_>, + target: SemanticTargetResolution<'_>, config: ReferencesConfig, ) -> Option> { - match target { - ReferencesTarget::Preproc(target) => { + match target.into_target(TargetCapability::REFERENCES)? { + SemanticTarget::PreprocMacro(target) => { render_preproc_references_target(db, file_id, target, &config) } - ReferencesTarget::Source(target) => { + SemanticTarget::Include(_) => None, + SemanticTarget::Source(target) => { render_source_references_target(sema, file_id, target, config) } } diff --git a/crates/ide/src/references/preproc.rs b/crates/ide/src/references/preproc.rs index 5a3fc041..ef5e2b9a 100644 --- a/crates/ide/src/references/preproc.rs +++ b/crates/ide/src/references/preproc.rs @@ -1,78 +1,52 @@ use hir::preproc::{ - MacroDefinition, MacroParamDefinition, MacroReferenceIndexStatus, macro_definition_at, - macro_param_definition_at, macro_param_reference_definitions_at, macro_param_references, - macro_reference_definitions_at, macro_references, + MacroDefinition, MacroParamDefinition, MacroReferenceIndexStatus, macro_param_references, + macro_references, }; use itertools::Itertools; -use utils::line_index::TextSize; use vfs::FileId; use super::{ ReferenceCategory, References, ReferencesConfig, ReferencesPartialReason, ReferencesStatus, }; -use crate::{db::root_db::RootDb, navigation_target::NavTarget}; - -pub(super) enum PreprocReferencesTarget { - MacroParams(Vec), - Macros(Vec), -} - -pub(super) fn dispatch_preproc_references_target( - db: &RootDb, - file_id: FileId, - offset: TextSize, -) -> Option { - if let Some(target) = dispatch_preproc_macro_param_references_target(db, file_id, offset) { - return Some(target); - } - - let definitions = if let Some(definition) = macro_definition_at(db, file_id, offset).ok()? { - vec![definition] - } else { - macro_reference_definitions_at(db, file_id, offset).ok()??.definitions - }; - if definitions.is_empty() { - return None; - } - - Some(PreprocReferencesTarget::Macros(definitions)) -} - -fn dispatch_preproc_macro_param_references_target( - db: &RootDb, - file_id: FileId, - offset: TextSize, -) -> Option { - let definitions = - if let Some(definition) = macro_param_definition_at(db, file_id, offset).ok()? { - vec![definition] - } else { - macro_param_reference_definitions_at(db, file_id, offset).ok()??.definitions - }; - if definitions.is_empty() { - return None; - } - - Some(PreprocReferencesTarget::MacroParams(definitions)) -} +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: PreprocReferencesTarget, + target: PreprocMacroTarget, config: &ReferencesConfig, ) -> Option> { match target { - PreprocReferencesTarget::MacroParams(definitions) => definitions - .into_iter() - .map(|definition| { - macro_param_references_for_definition(db, file_id, definition, config) - }) - .collect(), - PreprocReferencesTarget::Macros(definitions) => definitions - .into_iter() - .map(|definition| macro_references_for_definition(db, file_id, definition, config)) - .collect(), + 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() + } } } diff --git a/crates/ide/src/semantic_target.rs b/crates/ide/src/semantic_target.rs new file mode 100644 index 00000000..ac316ca4 --- /dev/null +++ b/crates/ide/src/semantic_target.rs @@ -0,0 +1,399 @@ +use hir::preproc::{ + IncludeDirective, 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 utils::line_index::{TextRange, TextSize}; +use vfs::FileId; + +use crate::{ + db::root_db::RootDb, + source_targets::{ + SourceTarget, SourceTargetBlock, SourceTargetBlockReason, SourceTargetDomain, + SourceTargetOrigin, SourceTargetResolution, source_target_at_offset, + }, +}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum TargetIntent { + Describe, + Navigate, + FindReferences, + Highlight, + #[allow(dead_code)] + Rename, +} + +bitflags::bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] + pub(crate) struct TargetCapability: u8 { + const DESCRIBE = 1 << 0; + const NAVIGATE = 1 << 1; + const REFERENCES = 1 << 2; + const HIGHLIGHT = 1 << 3; + const RENAME = 1 << 4; + } +} + +#[derive(Debug, Clone)] +pub(crate) struct SemanticTargetResolution<'tree> { + pub selection: TargetSelection, + pub target: Option>, + #[allow(dead_code)] + pub alternatives: Vec>, + pub status: TargetStatus, + pub capabilities: TargetCapability, +} + +impl<'tree> SemanticTargetResolution<'tree> { + pub(crate) fn into_target(self, required: TargetCapability) -> Option> { + let Self { selection: _selection, target, alternatives: _, status, capabilities } = self; + if status != TargetStatus::Complete || !capabilities.contains(required) { + return None; + } + target + } + + pub(crate) fn from_source_resolution( + file_id: FileId, + resolution: SourceTargetResolution<'tree>, + ) -> Self { + match resolution { + SourceTargetResolution::Resolved(target) => { + let origin = TargetOrigin::from_source_origin(&target.origin); + let range = target.range; + let capabilities = source_capabilities(); + Self { + selection: TargetSelection { file_id, range, origin }, + target: Some(SemanticTarget::Source(target)), + alternatives: Vec::new(), + status: TargetStatus::Complete, + capabilities, + } + } + SourceTargetResolution::Blocked(block) => { + let SourceTargetBlock { range, .. } = block.clone(); + let status = TargetStatus::from_source_block(block.clone()); + Self { + selection: TargetSelection { + file_id, + range, + origin: TargetOrigin::from_source_block(&block), + }, + target: None, + alternatives: Vec::new(), + status, + capabilities: TargetCapability::empty(), + } + } + } + } + + fn from_preproc_macro(file_id: FileId, target: PreprocMacroTarget) -> Self { + let capabilities = target.capabilities(); + Self { + selection: TargetSelection { + file_id, + range: target.range(), + origin: TargetOrigin::PreprocMacro, + }, + target: Some(SemanticTarget::PreprocMacro(target)), + alternatives: Vec::new(), + status: TargetStatus::Complete, + capabilities, + } + } + + fn from_include(file_id: FileId, includes: Vec) -> Option { + let range = includes.first()?.range; + Some(Self { + selection: TargetSelection { file_id, range, origin: TargetOrigin::IncludeDirective }, + target: Some(SemanticTarget::Include(includes)), + alternatives: Vec::new(), + status: TargetStatus::Complete, + capabilities: TargetCapability::DESCRIBE | TargetCapability::NAVIGATE, + }) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct TargetSelection { + pub file_id: FileId, + pub range: TextRange, + pub origin: TargetOrigin, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum TargetOrigin { + Source, + MacroExpansion, + PreprocMacro, + IncludeDirective, +} + +impl TargetOrigin { + fn from_source_origin(origin: &SourceTargetOrigin) -> Self { + match origin { + SourceTargetOrigin::NormalSyntax => TargetOrigin::Source, + SourceTargetOrigin::Preproc { .. } => TargetOrigin::MacroExpansion, + } + } + + fn from_source_block(block: &SourceTargetBlock) -> Self { + match block.domain { + SourceTargetDomain::Preproc => TargetOrigin::MacroExpansion, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum TargetStatus { + Complete, + Ambiguous(TargetAmbiguity), + Blocked(TargetBlockReason), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum TargetAmbiguity { + PreprocHits { candidate_count: usize }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum TargetBlockReason { + PreprocUnavailable, +} + +impl TargetStatus { + fn from_source_block(block: SourceTargetBlock) -> Self { + match (block.domain, block.reason) { + (SourceTargetDomain::Preproc, SourceTargetBlockReason::Unavailable) => { + TargetStatus::Blocked(TargetBlockReason::PreprocUnavailable) + } + (SourceTargetDomain::Preproc, SourceTargetBlockReason::Ambiguous { hits }) => { + TargetStatus::Ambiguous(TargetAmbiguity::PreprocHits { + candidate_count: hits.len(), + }) + } + } + } +} + +#[derive(Debug, Clone)] +pub(crate) enum SemanticTarget<'tree> { + Source(SourceTarget<'tree>), + PreprocMacro(PreprocMacroTarget), + Include(Vec), +} + +#[derive(Debug, Clone)] +pub(crate) enum PreprocMacroTarget { + ParamDefinition(MacroParamDefinition), + ParamReference(MacroParamReferenceDefinitions), + Definition(MacroDefinition), + Reference(MacroReferenceDefinitions), +} + +impl PreprocMacroTarget { + pub(crate) fn range(&self) -> TextRange { + match self { + PreprocMacroTarget::ParamDefinition(definition) => definition.range, + PreprocMacroTarget::ParamReference(resolution) => resolution.range, + PreprocMacroTarget::Definition(definition) => definition.name_range, + PreprocMacroTarget::Reference(resolution) => resolution.range, + } + } + + fn capabilities(&self) -> TargetCapability { + let mut capabilities = TargetCapability::DESCRIBE; + let has_definitions = match self { + PreprocMacroTarget::ParamDefinition(_) | PreprocMacroTarget::Definition(_) => true, + PreprocMacroTarget::ParamReference(resolution) => !resolution.definitions.is_empty(), + PreprocMacroTarget::Reference(resolution) => !resolution.definitions.is_empty(), + }; + if has_definitions { + capabilities |= TargetCapability::NAVIGATE | TargetCapability::REFERENCES; + } + capabilities + } +} + +pub(crate) fn resolve_semantic_target<'tree, F>( + db: &RootDb, + file_id: FileId, + offset: TextSize, + root: Option>, + _intent: TargetIntent, + precedence: F, +) -> Option> +where + F: Fn(TokenKind) -> usize, +{ + if let Some(target) = preproc_macro_target_at(db, file_id, offset) { + return Some(SemanticTargetResolution::from_preproc_macro(file_id, target)); + } + + if let Some(includes) = include_target_at(db, file_id, offset) { + return SemanticTargetResolution::from_include(file_id, includes); + } + + let root = root?; + source_target_at_offset(db, file_id, root, offset, precedence) + .map(|resolution| SemanticTargetResolution::from_source_resolution(file_id, resolution)) +} + +fn preproc_macro_target_at( + db: &RootDb, + file_id: FileId, + offset: TextSize, +) -> Option { + if let Ok(Some(definition)) = macro_param_definition_at(db, file_id, offset) { + return Some(PreprocMacroTarget::ParamDefinition(definition)); + } + + if let Ok(Some(resolution)) = macro_param_reference_definitions_at(db, file_id, offset) { + if !resolution.definitions.is_empty() { + return Some(PreprocMacroTarget::ParamReference(resolution)); + } + } + + if let Ok(Some(definition)) = macro_definition_at(db, file_id, offset) { + return Some(PreprocMacroTarget::Definition(definition)); + } + + if let Ok(Some(resolution)) = macro_reference_definitions_at(db, file_id, offset) { + return Some(PreprocMacroTarget::Reference(resolution)); + } + + None +} + +fn include_target_at( + db: &RootDb, + file_id: FileId, + offset: TextSize, +) -> Option> { + let includes = include_directives_at(db, file_id, offset).ok()?; + (!includes.is_empty()).then_some(includes) +} + +fn source_capabilities() -> TargetCapability { + TargetCapability::DESCRIBE + | TargetCapability::NAVIGATE + | TargetCapability::REFERENCES + | TargetCapability::HIGHLIGHT + | TargetCapability::RENAME +} + +#[cfg(test)] +mod tests { + use hir::{ + base_db::{change::Change, source_root::SourceRoot}, + semantics::Semantics, + }; + use syntax::token::TokenKindExt; + use triomphe::Arc; + use utils::{ + line_index::{TextRange, TextSize}, + lines::LineEnding, + }; + use vfs::{ChangeKind, ChangedFile, FileId, FileSet, VfsPath}; + + use super::*; + use crate::analysis_host::AnalysisHost; + + fn token_precedence(kind: syntax::TokenKind) -> usize { + usize::from(kind.name_like()) + } + + fn setup(text: &str, needle: &str) -> (AnalysisHost, FileId, TextSize, TextRange) { + let file_id = FileId(0); + let path = VfsPath::new_virtual_path("/test.sv".to_string()); + let mut file_set = FileSet::default(); + file_set.insert(file_id, path); + let root = SourceRoot::new_local(file_set); + + let mut change = Change::new(); + change.set_roots(vec![root]); + change.add_changed_file(ChangedFile { + file_id, + change_kind: ChangeKind::Create(Arc::from(text), LineEnding::Unix), + }); + + let mut host = AnalysisHost::default(); + host.apply_change(change); + + let start = text.find(needle).expect("needle should exist"); + let range = TextRange::new( + TextSize::from(start as u32), + TextSize::from((start + needle.len()) as u32), + ); + (host, file_id, range.start(), range) + } + + #[test] + fn source_token_target_is_complete_and_source_origin() { + let (host, file_id, offset, range) = + 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 root = parsed.root().expect("test source should parse"); + + let target = resolve_semantic_target( + host.raw_db(), + file_id, + offset, + Some(root), + TargetIntent::Describe, + token_precedence, + ) + .expect("source token target expected"); + + assert_eq!(target.selection.range, range); + assert_eq!(target.selection.origin, TargetOrigin::Source); + assert_eq!(target.status, TargetStatus::Complete); + assert!(target.capabilities.contains(TargetCapability::DESCRIBE)); + assert!(matches!(target.target, Some(SemanticTarget::Source(_)))); + } + + #[test] + fn source_target_block_is_reported_without_syntax_fallback() { + let block = crate::source_targets::SourceTargetBlock { + domain: crate::source_targets::SourceTargetDomain::Preproc, + range: TextRange::new(TextSize::from(1), TextSize::from(4)), + reason: crate::source_targets::SourceTargetBlockReason::Unavailable, + }; + + let target = SemanticTargetResolution::from_source_resolution( + FileId(0), + crate::source_targets::SourceTargetResolution::Blocked(block.clone()), + ); + + assert_eq!(target.selection.range, block.range); + assert_eq!(target.selection.origin, TargetOrigin::MacroExpansion); + assert_eq!(target.status, TargetStatus::Blocked(TargetBlockReason::PreprocUnavailable)); + assert!(target.target.is_none()); + assert!(target.capabilities.is_empty()); + } + + #[test] + fn ambiguous_source_target_block_is_reported_as_ambiguous() { + let block = crate::source_targets::SourceTargetBlock { + domain: crate::source_targets::SourceTargetDomain::Preproc, + range: TextRange::new(TextSize::from(1), TextSize::from(4)), + reason: crate::source_targets::SourceTargetBlockReason::Ambiguous { hits: Vec::new() }, + }; + + let target = SemanticTargetResolution::from_source_resolution( + FileId(0), + crate::source_targets::SourceTargetResolution::Blocked(block), + ); + + assert_eq!( + target.status, + TargetStatus::Ambiguous(TargetAmbiguity::PreprocHits { candidate_count: 0 }) + ); + assert!(target.target.is_none()); + } +} From 41770d44850a8bfd7dd63797262c34ed5097d9f4 Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Thu, 25 Jun 2026 23:44:14 +0800 Subject: [PATCH 2/8] refactor(ide): make target resolution state explicit --- crates/ide/src/document_highlight.rs | 6 +- crates/ide/src/goto_declaration.rs | 6 +- crates/ide/src/goto_definition.rs | 9 +- crates/ide/src/hover.rs | 11 +- crates/ide/src/references.rs | 11 +- crates/ide/src/semantic_target.rs | 278 ++++++++++++++++++--------- 6 files changed, 200 insertions(+), 121 deletions(-) diff --git a/crates/ide/src/document_highlight.rs b/crates/ide/src/document_highlight.rs index 01463668..47762bb8 100644 --- a/crates/ide/src/document_highlight.rs +++ b/crates/ide/src/document_highlight.rs @@ -11,7 +11,7 @@ use crate::{ self, ReferenceCategory, ReferencesConfig, search::{ReferencesCtx, SearchScope}, }, - semantic_target::{SemanticTarget, TargetCapability, TargetIntent, resolve_semantic_target}, + semantic_target::{SemanticTarget, TargetIntent, resolve_semantic_target}, }; #[derive(Debug, Clone)] @@ -40,8 +40,8 @@ pub(crate) fn document_highlight( parsed_file.root(), TargetIntent::Highlight, token_precedence, - )?; - let SemanticTarget::Source(target) = target.into_target(TargetCapability::HIGHLIGHT)? else { + ); + let SemanticTarget::Source(target) = target.for_highlight()? else { return None; }; let tokens = target.into_tokens(); diff --git a/crates/ide/src/goto_declaration.rs b/crates/ide/src/goto_declaration.rs index 626faf09..8663c7fe 100644 --- a/crates/ide/src/goto_declaration.rs +++ b/crates/ide/src/goto_declaration.rs @@ -7,7 +7,7 @@ use crate::{ definitions::DefinitionClass, goto_definition, navigation_target::{NavTarget, ToNav}, - semantic_target::{SemanticTarget, TargetCapability, TargetIntent, resolve_semantic_target}, + semantic_target::{SemanticTarget, TargetIntent, resolve_semantic_target}, }; pub(crate) fn goto_declaration( @@ -24,8 +24,8 @@ pub(crate) fn goto_declaration( parsed_file.root(), TargetIntent::Navigate, goto_definition::token_precedence, - )?; - let SemanticTarget::Source(target) = target.into_target(TargetCapability::NAVIGATE)? else { + ); + let SemanticTarget::Source(target) = target.for_navigation()? else { return None; }; let (range, tokens) = target.into_parts(); diff --git a/crates/ide/src/goto_definition.rs b/crates/ide/src/goto_definition.rs index 902991b2..7a340e2c 100644 --- a/crates/ide/src/goto_definition.rs +++ b/crates/ide/src/goto_definition.rs @@ -19,8 +19,7 @@ use crate::{ definitions::DefinitionClass, navigation_target::{NavTarget, ToNav}, semantic_target::{ - PreprocMacroTarget, SemanticTarget, SemanticTargetResolution, TargetCapability, - TargetIntent, resolve_semantic_target, + PreprocMacroTarget, SemanticTarget, TargetIntent, TargetResolution, resolve_semantic_target, }, source_targets::SourceTarget, }; @@ -38,7 +37,7 @@ pub(crate) fn goto_definition( parsed_file.root(), TargetIntent::Navigate, token_precedence, - )?; + ); render_definition_target(db, file_id, &sema, target) } @@ -46,9 +45,9 @@ fn render_definition_target( db: &RootDb, file_id: FileId, sema: &Semantics, - target: SemanticTargetResolution<'_>, + target: TargetResolution<'_>, ) -> Option>> { - match target.into_target(TargetCapability::NAVIGATE)? { + match target.for_navigation()? { SemanticTarget::PreprocMacro(target) => render_preproc_definition_target(target), SemanticTarget::Include(includes) => render_include_definition_target(db, includes), SemanticTarget::Source(target) => { diff --git a/crates/ide/src/hover.rs b/crates/ide/src/hover.rs index 061e35cd..1760d02f 100644 --- a/crates/ide/src/hover.rs +++ b/crates/ide/src/hover.rs @@ -25,10 +25,7 @@ use crate::{ }, markup::{Markup, inline_code}, render, - semantic_target::{ - SemanticTarget, SemanticTargetResolution, TargetCapability, TargetIntent, - resolve_semantic_target, - }, + semantic_target::{SemanticTarget, TargetIntent, TargetResolution, resolve_semantic_target}, source_targets::SourceTarget, }; @@ -63,7 +60,7 @@ pub(crate) fn hover( parsed_file.root(), TargetIntent::Describe, token_precedence, - )?; + ); render_hover_target(db, file_id, offset, &sema, target) } @@ -72,9 +69,9 @@ fn render_hover_target( file_id: FileId, offset: TextSize, sema: &Semantics, - target: SemanticTargetResolution<'_>, + target: TargetResolution<'_>, ) -> Option> { - match target.into_target(TargetCapability::DESCRIBE)? { + match target.for_hover()? { SemanticTarget::PreprocMacro(target) => { render_macro_hover_target(db, file_id, offset, target) } diff --git a/crates/ide/src/references.rs b/crates/ide/src/references.rs index eeb61afb..420c3ed0 100644 --- a/crates/ide/src/references.rs +++ b/crates/ide/src/references.rs @@ -16,10 +16,7 @@ use crate::{ db::root_db::RootDb, definitions::{Definition, DefinitionClass}, navigation_target::{NavTarget, ToNav}, - semantic_target::{ - SemanticTarget, SemanticTargetResolution, TargetCapability, TargetIntent, - resolve_semantic_target, - }, + semantic_target::{SemanticTarget, TargetIntent, TargetResolution, resolve_semantic_target}, source_targets::SourceTarget, }; @@ -102,7 +99,7 @@ pub(crate) fn references( parsed_file.root(), TargetIntent::FindReferences, token_precedence, - )?; + ); render_references_target(db, file_id, &sema, target, config) } @@ -110,10 +107,10 @@ fn render_references_target( db: &RootDb, file_id: FileId, sema: &Semantics, - target: SemanticTargetResolution<'_>, + target: TargetResolution<'_>, config: ReferencesConfig, ) -> Option> { - match target.into_target(TargetCapability::REFERENCES)? { + match target.for_references()? { SemanticTarget::PreprocMacro(target) => { render_preproc_references_target(db, file_id, target, &config) } diff --git a/crates/ide/src/semantic_target.rs b/crates/ide/src/semantic_target.rs index ac316ca4..acdc963b 100644 --- a/crates/ide/src/semantic_target.rs +++ b/crates/ide/src/semantic_target.rs @@ -38,22 +38,45 @@ bitflags::bitflags! { } #[derive(Debug, Clone)] -pub(crate) struct SemanticTargetResolution<'tree> { - pub selection: TargetSelection, - pub target: Option>, - #[allow(dead_code)] - pub alternatives: Vec>, - pub status: TargetStatus, - pub capabilities: TargetCapability, +pub(crate) enum TargetResolution<'tree> { + Resolved(TargetSet<'tree>), + Ambiguous(TargetAmbiguity<'tree>), + Blocked(TargetBlock), + Unresolved, } -impl<'tree> SemanticTargetResolution<'tree> { - pub(crate) fn into_target(self, required: TargetCapability) -> Option> { - let Self { selection: _selection, target, alternatives: _, status, capabilities } = self; - if status != TargetStatus::Complete || !capabilities.contains(required) { - return None; +impl<'tree> TargetResolution<'tree> { + pub(crate) fn for_hover(self) -> Option> { + self.into_primary(TargetCapability::DESCRIBE) + } + + pub(crate) fn for_navigation(self) -> Option> { + self.into_primary(TargetCapability::NAVIGATE) + } + + pub(crate) fn for_references(self) -> Option> { + self.into_primary(TargetCapability::REFERENCES) + } + + pub(crate) fn for_highlight(self) -> Option> { + self.into_primary(TargetCapability::HIGHLIGHT) + } + + fn into_primary(self, required: TargetCapability) -> Option> { + match self { + TargetResolution::Resolved(set) => set.into_primary(required), + TargetResolution::Ambiguous(ambiguity) => { + let TargetAmbiguity { anchor, reason, candidates } = ambiguity; + let _ = (anchor, reason, candidates); + None + } + TargetResolution::Blocked(block) => { + let TargetBlock { anchor, reason } = block; + let _ = (anchor, reason); + None + } + TargetResolution::Unresolved => None, } - target } pub(crate) fn from_source_resolution( @@ -65,61 +88,60 @@ impl<'tree> SemanticTargetResolution<'tree> { let origin = TargetOrigin::from_source_origin(&target.origin); let range = target.range; let capabilities = source_capabilities(); - Self { - selection: TargetSelection { file_id, range, origin }, - target: Some(SemanticTarget::Source(target)), - alternatives: Vec::new(), - status: TargetStatus::Complete, + Self::Resolved(TargetSet::single( + TargetAnchor { file_id, range, origin }, + SemanticTarget::Source(target), capabilities, - } + )) } SourceTargetResolution::Blocked(block) => { let SourceTargetBlock { range, .. } = block.clone(); - let status = TargetStatus::from_source_block(block.clone()); - Self { - selection: TargetSelection { - file_id, - range, - origin: TargetOrigin::from_source_block(&block), - }, - target: None, - alternatives: Vec::new(), - status, - capabilities: TargetCapability::empty(), - } + let anchor = TargetAnchor { + file_id, + range, + origin: TargetOrigin::from_source_block(&block), + }; + Self::from_source_block(anchor, block) } } } fn from_preproc_macro(file_id: FileId, target: PreprocMacroTarget) -> Self { let capabilities = target.capabilities(); - Self { - selection: TargetSelection { - file_id, - range: target.range(), - origin: TargetOrigin::PreprocMacro, - }, - target: Some(SemanticTarget::PreprocMacro(target)), - alternatives: Vec::new(), - status: TargetStatus::Complete, + Self::Resolved(TargetSet::single( + TargetAnchor { file_id, range: target.range(), origin: TargetOrigin::PreprocMacro }, + SemanticTarget::PreprocMacro(target), capabilities, - } + )) } fn from_include(file_id: FileId, includes: Vec) -> Option { let range = includes.first()?.range; - Some(Self { - selection: TargetSelection { file_id, range, origin: TargetOrigin::IncludeDirective }, - target: Some(SemanticTarget::Include(includes)), - alternatives: Vec::new(), - status: TargetStatus::Complete, - capabilities: TargetCapability::DESCRIBE | TargetCapability::NAVIGATE, - }) + Some(Self::Resolved(TargetSet::single( + TargetAnchor { file_id, range, origin: TargetOrigin::IncludeDirective }, + SemanticTarget::Include(includes), + TargetCapability::DESCRIBE | TargetCapability::NAVIGATE, + ))) + } + + fn from_source_block(anchor: TargetAnchor, block: SourceTargetBlock) -> Self { + match (block.domain, block.reason) { + (SourceTargetDomain::Preproc, SourceTargetBlockReason::Unavailable) => { + Self::Blocked(TargetBlock { anchor, reason: TargetBlockReason::PreprocUnavailable }) + } + (SourceTargetDomain::Preproc, SourceTargetBlockReason::Ambiguous { hits }) => { + Self::Ambiguous(TargetAmbiguity { + anchor, + reason: TargetAmbiguityReason::PreprocHits { candidate_count: hits.len() }, + candidates: Vec::new(), + }) + } + } } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) struct TargetSelection { +pub(crate) struct TargetAnchor { pub file_id: FileId, pub range: TextRange, pub origin: TargetOrigin, @@ -148,38 +170,92 @@ impl TargetOrigin { } } -#[derive(Debug, Clone, PartialEq, Eq)] -pub(crate) enum TargetStatus { - Complete, - Ambiguous(TargetAmbiguity), - Blocked(TargetBlockReason), +#[derive(Debug, Clone)] +pub(crate) struct TargetSet<'tree> { + pub anchor: TargetAnchor, + pub primary: TargetCandidate<'tree>, + pub related: Vec>, + pub quality: TargetQuality, +} + +impl<'tree> TargetSet<'tree> { + fn single( + anchor: TargetAnchor, + target: SemanticTarget<'tree>, + capabilities: TargetCapability, + ) -> Self { + Self { + anchor, + primary: TargetCandidate { + target, + role: TargetRole::Primary, + capabilities, + quality: TargetQuality::Exact, + }, + related: Vec::new(), + quality: TargetQuality::Exact, + } + } + + fn into_primary(self, required: TargetCapability) -> Option> { + let Self { anchor, primary, related, quality } = self; + let _ = (anchor, related, quality); + primary.into_target(required) + } +} + +#[derive(Debug, Clone)] +pub(crate) struct TargetCandidate<'tree> { + pub target: SemanticTarget<'tree>, + pub role: TargetRole, + pub capabilities: TargetCapability, + pub quality: TargetQuality, +} + +impl<'tree> TargetCandidate<'tree> { + fn into_target(self, required: TargetCapability) -> Option> { + let Self { target, role, capabilities, quality } = self; + let _ = (role, quality); + if !capabilities.contains(required) { + return None; + } + Some(target) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum TargetRole { + Primary, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum TargetQuality { + Exact, +} + +#[derive(Debug, Clone)] +pub(crate) struct TargetAmbiguity<'tree> { + pub anchor: TargetAnchor, + pub reason: TargetAmbiguityReason, + pub candidates: Vec>, } #[derive(Debug, Clone, PartialEq, Eq)] -pub(crate) enum TargetAmbiguity { +pub(crate) enum TargetAmbiguityReason { PreprocHits { candidate_count: usize }, } +#[derive(Debug, Clone)] +pub(crate) struct TargetBlock { + pub anchor: TargetAnchor, + pub reason: TargetBlockReason, +} + #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) enum TargetBlockReason { PreprocUnavailable, } -impl TargetStatus { - fn from_source_block(block: SourceTargetBlock) -> Self { - match (block.domain, block.reason) { - (SourceTargetDomain::Preproc, SourceTargetBlockReason::Unavailable) => { - TargetStatus::Blocked(TargetBlockReason::PreprocUnavailable) - } - (SourceTargetDomain::Preproc, SourceTargetBlockReason::Ambiguous { hits }) => { - TargetStatus::Ambiguous(TargetAmbiguity::PreprocHits { - candidate_count: hits.len(), - }) - } - } - } -} - #[derive(Debug, Clone)] pub(crate) enum SemanticTarget<'tree> { Source(SourceTarget<'tree>), @@ -226,21 +302,25 @@ pub(crate) fn resolve_semantic_target<'tree, F>( root: Option>, _intent: TargetIntent, precedence: F, -) -> Option> +) -> TargetResolution<'tree> where F: Fn(TokenKind) -> usize, { if let Some(target) = preproc_macro_target_at(db, file_id, offset) { - return Some(SemanticTargetResolution::from_preproc_macro(file_id, target)); + return TargetResolution::from_preproc_macro(file_id, target); } if let Some(includes) = include_target_at(db, file_id, offset) { - return SemanticTargetResolution::from_include(file_id, includes); + return TargetResolution::from_include(file_id, includes) + .unwrap_or(TargetResolution::Unresolved); } - let root = root?; + let Some(root) = root else { + return TargetResolution::Unresolved; + }; source_target_at_offset(db, file_id, root, offset, precedence) - .map(|resolution| SemanticTargetResolution::from_source_resolution(file_id, resolution)) + .map(|resolution| TargetResolution::from_source_resolution(file_id, resolution)) + .unwrap_or(TargetResolution::Unresolved) } fn preproc_macro_target_at( @@ -340,21 +420,24 @@ mod tests { let parsed = sema.parse_file(file_id); let root = parsed.root().expect("test source should parse"); - let target = resolve_semantic_target( + let resolution = resolve_semantic_target( host.raw_db(), file_id, offset, Some(root), TargetIntent::Describe, token_precedence, - ) - .expect("source token target expected"); - - assert_eq!(target.selection.range, range); - assert_eq!(target.selection.origin, TargetOrigin::Source); - assert_eq!(target.status, TargetStatus::Complete); - assert!(target.capabilities.contains(TargetCapability::DESCRIBE)); - assert!(matches!(target.target, Some(SemanticTarget::Source(_)))); + ); + assert!(matches!(resolution.clone().for_hover(), Some(SemanticTarget::Source(_)))); + + let TargetResolution::Resolved(target) = resolution else { + panic!("source token should resolve"); + }; + + assert_eq!(target.anchor.range, range); + assert_eq!(target.anchor.origin, TargetOrigin::Source); + assert_eq!(target.quality, TargetQuality::Exact); + assert!(target.primary.capabilities.contains(TargetCapability::DESCRIBE)); } #[test] @@ -365,16 +448,18 @@ mod tests { reason: crate::source_targets::SourceTargetBlockReason::Unavailable, }; - let target = SemanticTargetResolution::from_source_resolution( + let resolution = TargetResolution::from_source_resolution( FileId(0), crate::source_targets::SourceTargetResolution::Blocked(block.clone()), ); - assert_eq!(target.selection.range, block.range); - assert_eq!(target.selection.origin, TargetOrigin::MacroExpansion); - assert_eq!(target.status, TargetStatus::Blocked(TargetBlockReason::PreprocUnavailable)); - assert!(target.target.is_none()); - assert!(target.capabilities.is_empty()); + let TargetResolution::Blocked(target) = resolution else { + panic!("unavailable source target should be blocked"); + }; + + assert_eq!(target.anchor.range, block.range); + assert_eq!(target.anchor.origin, TargetOrigin::MacroExpansion); + assert_eq!(target.reason, TargetBlockReason::PreprocUnavailable); } #[test] @@ -385,15 +470,16 @@ mod tests { reason: crate::source_targets::SourceTargetBlockReason::Ambiguous { hits: Vec::new() }, }; - let target = SemanticTargetResolution::from_source_resolution( + let resolution = TargetResolution::from_source_resolution( FileId(0), crate::source_targets::SourceTargetResolution::Blocked(block), ); - assert_eq!( - target.status, - TargetStatus::Ambiguous(TargetAmbiguity::PreprocHits { candidate_count: 0 }) - ); - assert!(target.target.is_none()); + let TargetResolution::Ambiguous(ambiguity) = resolution else { + panic!("conflicting source target should be ambiguous"); + }; + + assert_eq!(ambiguity.reason, TargetAmbiguityReason::PreprocHits { candidate_count: 0 }); + assert!(ambiguity.candidates.is_empty()); } } From abfa7aab4a03f6f14988285c18d0acd131c0d162 Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Thu, 25 Jun 2026 23:48:00 +0800 Subject: [PATCH 3/8] refactor(ide): gate rename through semantic targets --- crates/ide/src/rename.rs | 43 +++++++++++++++++++++---------- crates/ide/src/semantic_target.rs | 5 ++++ 2 files changed, 34 insertions(+), 14 deletions(-) diff --git a/crates/ide/src/rename.rs b/crates/ide/src/rename.rs index 948700af..135ab147 100644 --- a/crates/ide/src/rename.rs +++ b/crates/ide/src/rename.rs @@ -3,18 +3,14 @@ use nohash_hasher::IntMap; use rustc_hash::FxHashMap; use smol_str::SmolStr; use syntax::{ - SyntaxAncestors, SyntaxNode, SyntaxNodeExt, SyntaxTokenWithParent, + SyntaxAncestors, SyntaxTokenWithParent, TokenKind, ast::{self, AstNode, Expression, Name}, has_text_range::{HasTextRange, HasTextRangeIn}, match_ast, token::TokenKindExt, }; use thiserror::Error; -use utils::{ - line_index::{TextRange, TextSize}, - text_edit::TextEdit, - uniq_vec::UniqVec, -}; +use utils::{line_index::TextRange, text_edit::TextEdit, uniq_vec::UniqVec}; use vfs::FileId; use crate::{ @@ -25,6 +21,7 @@ use crate::{ ReferencesConfig, search::{ReferenceToken, ReferencesCtx, SearchScope}, }, + semantic_target::{SemanticTarget, TargetIntent, resolve_semantic_target}, source_change::SourceChange, }; @@ -106,8 +103,18 @@ pub(crate) fn prepare_rename( let sema = Semantics::new(db); let hir_file_id = file_id.into(); let parsed_file = sema.parse_file(file_id); - let root = parsed_file.root().ok_or(RenameError::NoRefFound)?; - let token = pick_token(root, offset)?; + let target = resolve_semantic_target( + db, + file_id, + offset, + parsed_file.root(), + TargetIntent::Rename, + rename_token_precedence, + ); + let SemanticTarget::Source(target) = target.for_rename().ok_or(RenameError::NoRefFound)? else { + return Err(RenameError::NoRefFound); + }; + let token = target.into_tokens().into_iter().next().ok_or(RenameError::NoRefFound)?; let text_range = token.text_range().ok_or(RenameError::NoRefFound)?; let def = match DefinitionClass::resolve(&sema, hir_file_id, token).ok_or(RenameError::NoDefFound)? { @@ -231,8 +238,18 @@ fn resolve_rename_target( FilePosition { file_id, offset }: FilePosition, ) -> RenameResult { let parsed_file = sema.parse_file(file_id); - let root = parsed_file.root().ok_or(RenameError::NoRefFound)?; - let token = pick_token(root, offset)?; + let target = resolve_semantic_target( + sema.db, + file_id, + offset, + parsed_file.root(), + TargetIntent::Rename, + rename_token_precedence, + ); + let SemanticTarget::Source(target) = target.for_rename().ok_or(RenameError::NoRefFound)? else { + return Err(RenameError::NoRefFound); + }; + let token = target.into_tokens().into_iter().next().ok_or(RenameError::NoRefFound)?; let mut targets = UniqVec::::default(); let selected_def = match DefinitionClass::resolve(sema, file_id.into(), token) .ok_or(RenameError::NoDefFound)? @@ -572,8 +589,6 @@ fn edits_from_refs( (file_id, text_edit.finish()) } -fn pick_token(node: SyntaxNode, offset: TextSize) -> RenameResult { - node.token_at_offset(offset) - .pick_bext_token(|kind| kind.name_like().into()) - .ok_or(RenameError::NoRefFound) +fn rename_token_precedence(kind: TokenKind) -> usize { + usize::from(kind.name_like()) } diff --git a/crates/ide/src/semantic_target.rs b/crates/ide/src/semantic_target.rs index acdc963b..ee2adb1d 100644 --- a/crates/ide/src/semantic_target.rs +++ b/crates/ide/src/semantic_target.rs @@ -62,6 +62,10 @@ impl<'tree> TargetResolution<'tree> { self.into_primary(TargetCapability::HIGHLIGHT) } + pub(crate) fn for_rename(self) -> Option> { + self.into_primary(TargetCapability::RENAME) + } + fn into_primary(self, required: TargetCapability) -> Option> { match self { TargetResolution::Resolved(set) => set.into_primary(required), @@ -429,6 +433,7 @@ mod tests { token_precedence, ); assert!(matches!(resolution.clone().for_hover(), Some(SemanticTarget::Source(_)))); + assert!(matches!(resolution.clone().for_rename(), Some(SemanticTarget::Source(_)))); let TargetResolution::Resolved(target) = resolution else { panic!("source token should resolve"); From dadd0fd4938824d0680c8e335bbca8e65879f357 Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Thu, 25 Jun 2026 23:55:01 +0800 Subject: [PATCH 4/8] refactor(ide): project semantic targets by intent --- crates/ide/src/document_highlight.rs | 11 +- crates/ide/src/goto_declaration.rs | 3 +- crates/ide/src/goto_definition.rs | 11 +- crates/ide/src/hover.rs | 11 +- crates/ide/src/references.rs | 11 +- crates/ide/src/rename.rs | 79 ++++++------ crates/ide/src/semantic_target.rs | 180 ++++++++------------------- 7 files changed, 96 insertions(+), 210 deletions(-) diff --git a/crates/ide/src/document_highlight.rs b/crates/ide/src/document_highlight.rs index 47762bb8..e1ad96ac 100644 --- a/crates/ide/src/document_highlight.rs +++ b/crates/ide/src/document_highlight.rs @@ -33,15 +33,8 @@ 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(), - TargetIntent::Highlight, - token_precedence, - ); - let SemanticTarget::Source(target) = target.for_highlight()? else { + let target = resolve_semantic_target(db, file_id, offset, parsed_file.root(), token_precedence); + let SemanticTarget::Source(target) = target.for_intent(TargetIntent::Highlight)? else { return None; }; let tokens = target.into_tokens(); diff --git a/crates/ide/src/goto_declaration.rs b/crates/ide/src/goto_declaration.rs index 8663c7fe..4c022e84 100644 --- a/crates/ide/src/goto_declaration.rs +++ b/crates/ide/src/goto_declaration.rs @@ -22,10 +22,9 @@ pub(crate) fn goto_declaration( file_id, offset, parsed_file.root(), - TargetIntent::Navigate, goto_definition::token_precedence, ); - let SemanticTarget::Source(target) = target.for_navigation()? else { + let SemanticTarget::Source(target) = target.for_intent(TargetIntent::Navigate)? else { return None; }; let (range, tokens) = target.into_parts(); diff --git a/crates/ide/src/goto_definition.rs b/crates/ide/src/goto_definition.rs index 7a340e2c..ece5b8bc 100644 --- a/crates/ide/src/goto_definition.rs +++ b/crates/ide/src/goto_definition.rs @@ -30,14 +30,7 @@ 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(), - TargetIntent::Navigate, - token_precedence, - ); + let target = resolve_semantic_target(db, file_id, offset, parsed_file.root(), token_precedence); render_definition_target(db, file_id, &sema, target) } @@ -47,7 +40,7 @@ fn render_definition_target( sema: &Semantics, target: TargetResolution<'_>, ) -> Option>> { - match target.for_navigation()? { + match target.for_intent(TargetIntent::Navigate)? { SemanticTarget::PreprocMacro(target) => render_preproc_definition_target(target), SemanticTarget::Include(includes) => render_include_definition_target(db, includes), SemanticTarget::Source(target) => { diff --git a/crates/ide/src/hover.rs b/crates/ide/src/hover.rs index 1760d02f..5cd572e4 100644 --- a/crates/ide/src/hover.rs +++ b/crates/ide/src/hover.rs @@ -53,14 +53,7 @@ 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(), - TargetIntent::Describe, - token_precedence, - ); + let target = resolve_semantic_target(db, file_id, offset, parsed_file.root(), token_precedence); render_hover_target(db, file_id, offset, &sema, target) } @@ -71,7 +64,7 @@ fn render_hover_target( sema: &Semantics, target: TargetResolution<'_>, ) -> Option> { - match target.for_hover()? { + match target.for_intent(TargetIntent::Describe)? { SemanticTarget::PreprocMacro(target) => { render_macro_hover_target(db, file_id, offset, target) } diff --git a/crates/ide/src/references.rs b/crates/ide/src/references.rs index 420c3ed0..af25ea52 100644 --- a/crates/ide/src/references.rs +++ b/crates/ide/src/references.rs @@ -92,14 +92,7 @@ pub(crate) fn references( ) -> 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(), - TargetIntent::FindReferences, - token_precedence, - ); + let target = resolve_semantic_target(db, file_id, offset, parsed_file.root(), token_precedence); render_references_target(db, file_id, &sema, target, config) } @@ -110,7 +103,7 @@ fn render_references_target( target: TargetResolution<'_>, config: ReferencesConfig, ) -> Option> { - match target.for_references()? { + match target.for_intent(TargetIntent::FindReferences)? { SemanticTarget::PreprocMacro(target) => { render_preproc_references_target(db, file_id, target, &config) } diff --git a/crates/ide/src/rename.rs b/crates/ide/src/rename.rs index 135ab147..94603894 100644 --- a/crates/ide/src/rename.rs +++ b/crates/ide/src/rename.rs @@ -97,33 +97,13 @@ pub struct RenameCollisionInfo { pub(crate) fn prepare_rename( db: &RootDb, - FilePosition { file_id, offset }: FilePosition, + position @ FilePosition { file_id, .. }: FilePosition, config: RenameConfig, ) -> RenameResult { 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(), - TargetIntent::Rename, - rename_token_precedence, - ); - let SemanticTarget::Source(target) = target.for_rename().ok_or(RenameError::NoRefFound)? else { - return Err(RenameError::NoRefFound); - }; - let token = target.into_tokens().into_iter().next().ok_or(RenameError::NoRefFound)?; - let text_range = token.text_range().ok_or(RenameError::NoRefFound)?; - let def = - match DefinitionClass::resolve(&sema, hir_file_id, token).ok_or(RenameError::NoDefFound)? { - DefinitionClass::Definition(def) => def, - DefinitionClass::PortConnShorthand { local, .. } => local, - DefinitionClass::Ambiguous(_) => return Err(RenameError::NoDefFound), - }; - let _ = config.references_config(db, &def, file_id)?; - Ok(text_range) + let target = resolve_rename_target(&sema, position)?; + let _ = config.references_config(db, &target.selected_def, file_id)?; + Ok(target.range) } pub(crate) fn rename( @@ -221,6 +201,7 @@ pub(crate) fn rename_conflict_info( } struct ResolvedRenameTarget { + range: TextRange, selected_def: Definition, targets: Vec, } @@ -237,35 +218,51 @@ 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, file_id, offset, parsed_file.root(), - TargetIntent::Rename, rename_token_precedence, ); - let SemanticTarget::Source(target) = target.for_rename().ok_or(RenameError::NoRefFound)? else { + let SemanticTarget::Source(target) = + target.for_intent(TargetIntent::Rename).ok_or(RenameError::NoRefFound)? + else { return Err(RenameError::NoRefFound); }; - let token = target.into_tokens().into_iter().next().ok_or(RenameError::NoRefFound)?; + let (range, tokens) = target.into_parts(); + let mut selected_def = None; let mut targets = UniqVec::::default(); - let selected_def = match DefinitionClass::resolve(sema, file_id.into(), token) - .ok_or(RenameError::NoDefFound)? - { - DefinitionClass::Definition(def) => { - targets.push(def.origins(), def.clone()); - def - } - DefinitionClass::PortConnShorthand { port, local } => { - targets.push(local.origins(), local.clone()); - targets.push(port.origins(), port); - local + + for token in tokens { + let token_selected = match DefinitionClass::resolve(sema, hir_file_id, token) + .ok_or(RenameError::NoDefFound)? + { + DefinitionClass::Definition(def) => { + targets.push(def.origins(), def.clone()); + def + } + DefinitionClass::PortConnShorthand { port, local } => { + targets.push(local.origins(), local.clone()); + targets.push(port.origins(), port); + local + } + DefinitionClass::Ambiguous(_) => return Err(RenameError::NoDefFound), + }; + + match &selected_def { + Some(selected_def) if selected_def != &token_selected => { + return Err(RenameError::NoDefFound); + } + Some(_) => {} + None => selected_def = Some(token_selected), } - DefinitionClass::Ambiguous(_) => return Err(RenameError::NoDefFound), - }; - Ok(ResolvedRenameTarget { selected_def, targets: targets.into_vec() }) + } + + let selected_def = selected_def.ok_or(RenameError::NoDefFound)?; + Ok(ResolvedRenameTarget { range, selected_def, targets: targets.into_vec() }) } #[derive(Debug, Clone, PartialEq, Eq)] diff --git a/crates/ide/src/semantic_target.rs b/crates/ide/src/semantic_target.rs index ee2adb1d..4d37d61c 100644 --- a/crates/ide/src/semantic_target.rs +++ b/crates/ide/src/semantic_target.rs @@ -12,7 +12,7 @@ use crate::{ db::root_db::RootDb, source_targets::{ SourceTarget, SourceTargetBlock, SourceTargetBlockReason, SourceTargetDomain, - SourceTargetOrigin, SourceTargetResolution, source_target_at_offset, + SourceTargetResolution, source_target_at_offset, }, }; @@ -22,10 +22,21 @@ pub(crate) enum TargetIntent { Navigate, FindReferences, Highlight, - #[allow(dead_code)] Rename, } +impl TargetIntent { + fn capability(self) -> TargetCapability { + match self { + TargetIntent::Describe => TargetCapability::DESCRIBE, + TargetIntent::Navigate => TargetCapability::NAVIGATE, + TargetIntent::FindReferences => TargetCapability::REFERENCES, + TargetIntent::Highlight => TargetCapability::HIGHLIGHT, + TargetIntent::Rename => TargetCapability::RENAME, + } + } +} + bitflags::bitflags! { #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub(crate) struct TargetCapability: u8 { @@ -39,39 +50,23 @@ bitflags::bitflags! { #[derive(Debug, Clone)] pub(crate) enum TargetResolution<'tree> { - Resolved(TargetSet<'tree>), - Ambiguous(TargetAmbiguity<'tree>), + Resolved(TargetCandidate<'tree>), + Ambiguous(TargetAmbiguity), Blocked(TargetBlock), Unresolved, } impl<'tree> TargetResolution<'tree> { - pub(crate) fn for_hover(self) -> Option> { - self.into_primary(TargetCapability::DESCRIBE) - } - - pub(crate) fn for_navigation(self) -> Option> { - self.into_primary(TargetCapability::NAVIGATE) - } - - pub(crate) fn for_references(self) -> Option> { - self.into_primary(TargetCapability::REFERENCES) - } - - pub(crate) fn for_highlight(self) -> Option> { - self.into_primary(TargetCapability::HIGHLIGHT) - } - - pub(crate) fn for_rename(self) -> Option> { - self.into_primary(TargetCapability::RENAME) + pub(crate) fn for_intent(self, intent: TargetIntent) -> Option> { + self.into_primary(intent.capability()) } fn into_primary(self, required: TargetCapability) -> Option> { match self { - TargetResolution::Resolved(set) => set.into_primary(required), + TargetResolution::Resolved(candidate) => candidate.into_target(required), TargetResolution::Ambiguous(ambiguity) => { - let TargetAmbiguity { anchor, reason, candidates } = ambiguity; - let _ = (anchor, reason, candidates); + let TargetAmbiguity { anchor, reason } = ambiguity; + let _ = (anchor, reason); None } TargetResolution::Blocked(block) => { @@ -89,14 +84,8 @@ impl<'tree> TargetResolution<'tree> { ) -> Self { match resolution { SourceTargetResolution::Resolved(target) => { - let origin = TargetOrigin::from_source_origin(&target.origin); - let range = target.range; let capabilities = source_capabilities(); - Self::Resolved(TargetSet::single( - TargetAnchor { file_id, range, origin }, - SemanticTarget::Source(target), - capabilities, - )) + Self::Resolved(TargetCandidate::new(SemanticTarget::Source(target), capabilities)) } SourceTargetResolution::Blocked(block) => { let SourceTargetBlock { range, .. } = block.clone(); @@ -110,19 +99,14 @@ impl<'tree> TargetResolution<'tree> { } } - fn from_preproc_macro(file_id: FileId, target: PreprocMacroTarget) -> Self { + fn from_preproc_macro(target: PreprocMacroTarget) -> Self { let capabilities = target.capabilities(); - Self::Resolved(TargetSet::single( - TargetAnchor { file_id, range: target.range(), origin: TargetOrigin::PreprocMacro }, - SemanticTarget::PreprocMacro(target), - capabilities, - )) + Self::Resolved(TargetCandidate::new(SemanticTarget::PreprocMacro(target), capabilities)) } - fn from_include(file_id: FileId, includes: Vec) -> Option { - let range = includes.first()?.range; - Some(Self::Resolved(TargetSet::single( - TargetAnchor { file_id, range, origin: TargetOrigin::IncludeDirective }, + fn from_include(includes: Vec) -> Option { + includes.first()?; + Some(Self::Resolved(TargetCandidate::new( SemanticTarget::Include(includes), TargetCapability::DESCRIBE | TargetCapability::NAVIGATE, ))) @@ -137,7 +121,6 @@ impl<'tree> TargetResolution<'tree> { Self::Ambiguous(TargetAmbiguity { anchor, reason: TargetAmbiguityReason::PreprocHits { candidate_count: hits.len() }, - candidates: Vec::new(), }) } } @@ -153,20 +136,10 @@ pub(crate) struct TargetAnchor { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum TargetOrigin { - Source, MacroExpansion, - PreprocMacro, - IncludeDirective, } impl TargetOrigin { - fn from_source_origin(origin: &SourceTargetOrigin) -> Self { - match origin { - SourceTargetOrigin::NormalSyntax => TargetOrigin::Source, - SourceTargetOrigin::Preproc { .. } => TargetOrigin::MacroExpansion, - } - } - fn from_source_block(block: &SourceTargetBlock) -> Self { match block.domain { SourceTargetDomain::Preproc => TargetOrigin::MacroExpansion, @@ -174,52 +147,19 @@ impl TargetOrigin { } } -#[derive(Debug, Clone)] -pub(crate) struct TargetSet<'tree> { - pub anchor: TargetAnchor, - pub primary: TargetCandidate<'tree>, - pub related: Vec>, - pub quality: TargetQuality, -} - -impl<'tree> TargetSet<'tree> { - fn single( - anchor: TargetAnchor, - target: SemanticTarget<'tree>, - capabilities: TargetCapability, - ) -> Self { - Self { - anchor, - primary: TargetCandidate { - target, - role: TargetRole::Primary, - capabilities, - quality: TargetQuality::Exact, - }, - related: Vec::new(), - quality: TargetQuality::Exact, - } - } - - fn into_primary(self, required: TargetCapability) -> Option> { - let Self { anchor, primary, related, quality } = self; - let _ = (anchor, related, quality); - primary.into_target(required) - } -} - #[derive(Debug, Clone)] pub(crate) struct TargetCandidate<'tree> { pub target: SemanticTarget<'tree>, - pub role: TargetRole, pub capabilities: TargetCapability, - pub quality: TargetQuality, } impl<'tree> TargetCandidate<'tree> { + fn new(target: SemanticTarget<'tree>, capabilities: TargetCapability) -> Self { + Self { target, capabilities } + } + fn into_target(self, required: TargetCapability) -> Option> { - let Self { target, role, capabilities, quality } = self; - let _ = (role, quality); + let Self { target, capabilities } = self; if !capabilities.contains(required) { return None; } @@ -227,21 +167,10 @@ impl<'tree> TargetCandidate<'tree> { } } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) enum TargetRole { - Primary, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) enum TargetQuality { - Exact, -} - #[derive(Debug, Clone)] -pub(crate) struct TargetAmbiguity<'tree> { +pub(crate) struct TargetAmbiguity { pub anchor: TargetAnchor, pub reason: TargetAmbiguityReason, - pub candidates: Vec>, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -276,15 +205,6 @@ pub(crate) enum PreprocMacroTarget { } impl PreprocMacroTarget { - pub(crate) fn range(&self) -> TextRange { - match self { - PreprocMacroTarget::ParamDefinition(definition) => definition.range, - PreprocMacroTarget::ParamReference(resolution) => resolution.range, - PreprocMacroTarget::Definition(definition) => definition.name_range, - PreprocMacroTarget::Reference(resolution) => resolution.range, - } - } - fn capabilities(&self) -> TargetCapability { let mut capabilities = TargetCapability::DESCRIBE; let has_definitions = match self { @@ -304,19 +224,17 @@ pub(crate) fn resolve_semantic_target<'tree, F>( file_id: FileId, offset: TextSize, root: Option>, - _intent: TargetIntent, precedence: F, ) -> TargetResolution<'tree> where F: Fn(TokenKind) -> usize, { if let Some(target) = preproc_macro_target_at(db, file_id, offset) { - return TargetResolution::from_preproc_macro(file_id, target); + return TargetResolution::from_preproc_macro(target); } if let Some(includes) = include_target_at(db, file_id, offset) { - return TargetResolution::from_include(file_id, includes) - .unwrap_or(TargetResolution::Unresolved); + return TargetResolution::from_include(includes).unwrap_or(TargetResolution::Unresolved); } let Some(root) = root else { @@ -424,25 +342,26 @@ mod tests { let parsed = sema.parse_file(file_id); let root = parsed.root().expect("test source should parse"); - let resolution = resolve_semantic_target( - host.raw_db(), - file_id, - offset, - Some(root), - TargetIntent::Describe, - token_precedence, - ); - assert!(matches!(resolution.clone().for_hover(), Some(SemanticTarget::Source(_)))); - assert!(matches!(resolution.clone().for_rename(), Some(SemanticTarget::Source(_)))); + let resolution = + resolve_semantic_target(host.raw_db(), file_id, offset, Some(root), token_precedence); + assert!(matches!( + resolution.clone().for_intent(TargetIntent::Describe), + Some(SemanticTarget::Source(_)) + )); + assert!(matches!( + resolution.clone().for_intent(TargetIntent::Rename), + Some(SemanticTarget::Source(_)) + )); let TargetResolution::Resolved(target) = resolution else { panic!("source token should resolve"); }; - assert_eq!(target.anchor.range, range); - assert_eq!(target.anchor.origin, TargetOrigin::Source); - assert_eq!(target.quality, TargetQuality::Exact); - assert!(target.primary.capabilities.contains(TargetCapability::DESCRIBE)); + assert!(target.capabilities.contains(TargetCapability::DESCRIBE)); + let SemanticTarget::Source(target) = target.target else { + panic!("source token should resolve as source target"); + }; + assert_eq!(target.range, range); } #[test] @@ -485,6 +404,5 @@ mod tests { }; assert_eq!(ambiguity.reason, TargetAmbiguityReason::PreprocHits { candidate_count: 0 }); - assert!(ambiguity.candidates.is_empty()); } } From 17167600b7eaf6a51e2a09ce0e5695f3d1007f65 Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Fri, 26 Jun 2026 00:03:09 +0800 Subject: [PATCH 5/8] refactor(ide): model semantic target alternatives --- crates/ide/src/document_highlight.rs | 2 +- crates/ide/src/goto_declaration.rs | 2 +- crates/ide/src/goto_definition.rs | 31 ++++-- crates/ide/src/hover.rs | 40 ++++++-- crates/ide/src/references.rs | 2 +- crates/ide/src/rename.rs | 2 +- crates/ide/src/semantic_target.rs | 121 ++++++++++++++++++----- crates/ide/src/source_targets.rs | 36 ++++++- crates/ide/src/source_targets/preproc.rs | 53 +++++++++- crates/ide/src/source_targets/tests.rs | 23 +++-- 10 files changed, 257 insertions(+), 55 deletions(-) diff --git a/crates/ide/src/document_highlight.rs b/crates/ide/src/document_highlight.rs index e1ad96ac..2dc8fb27 100644 --- a/crates/ide/src/document_highlight.rs +++ b/crates/ide/src/document_highlight.rs @@ -34,7 +34,7 @@ pub(crate) fn document_highlight( 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 SemanticTarget::Source(target) = target.for_intent(TargetIntent::Highlight)? else { + let SemanticTarget::Source(target) = target.unique_for_intent(TargetIntent::Highlight)? else { return None; }; let tokens = target.into_tokens(); diff --git a/crates/ide/src/goto_declaration.rs b/crates/ide/src/goto_declaration.rs index 4c022e84..a071d43f 100644 --- a/crates/ide/src/goto_declaration.rs +++ b/crates/ide/src/goto_declaration.rs @@ -24,7 +24,7 @@ pub(crate) fn goto_declaration( parsed_file.root(), goto_definition::token_precedence, ); - let SemanticTarget::Source(target) = target.for_intent(TargetIntent::Navigate)? else { + let SemanticTarget::Source(target) = target.unique_for_intent(TargetIntent::Navigate)? else { return None; }; let (range, tokens) = target.into_parts(); diff --git a/crates/ide/src/goto_definition.rs b/crates/ide/src/goto_definition.rs index ece5b8bc..f131fad7 100644 --- a/crates/ide/src/goto_definition.rs +++ b/crates/ide/src/goto_definition.rs @@ -40,13 +40,32 @@ fn render_definition_target( sema: &Semantics, target: TargetResolution<'_>, ) -> Option>> { - match target.for_intent(TargetIntent::Navigate)? { - SemanticTarget::PreprocMacro(target) => render_preproc_definition_target(target), - SemanticTarget::Include(includes) => render_include_definition_target(db, includes), - SemanticTarget::Source(target) => { - render_source_definition_target(db, file_id, sema, target) - } + let mut ranges = Vec::new(); + let mut navs = Vec::new(); + for target in target.targets_for_intent(TargetIntent::Navigate) { + let target = match target { + SemanticTarget::PreprocMacro(target) => render_preproc_definition_target(target), + SemanticTarget::Include(includes) => render_include_definition_target(db, includes), + SemanticTarget::Source(target) => { + render_source_definition_target(db, file_id, sema, target) + } + }?; + ranges.push(target.range); + navs.extend(target.info); + } + + if navs.is_empty() { + return None; } + + let range = covering_range(&ranges)?; + Some(RangeInfo::new(range, navs.into_iter().unique().collect())) +} + +fn covering_range(ranges: &[TextRange]) -> Option { + let start = ranges.iter().map(|range| range.start()).min()?; + let end = ranges.iter().map(|range| range.end()).max()?; + Some(TextRange::new(start, end)) } fn render_source_definition_target( diff --git a/crates/ide/src/hover.rs b/crates/ide/src/hover.rs index 5cd572e4..e36e76d2 100644 --- a/crates/ide/src/hover.rs +++ b/crates/ide/src/hover.rs @@ -64,16 +64,38 @@ fn render_hover_target( sema: &Semantics, target: TargetResolution<'_>, ) -> Option> { - match target.for_intent(TargetIntent::Describe)? { - SemanticTarget::PreprocMacro(target) => { - render_macro_hover_target(db, file_id, offset, target) - } - SemanticTarget::Include(includes) => render_include_hover(db, includes), - SemanticTarget::Source(target) => { - let hover = hover_for_source_target(sema, file_id.into(), target)?; - Some(with_expanded_macro_hover(db, file_id, offset, hover)) - } + let mut ranges = Vec::new(); + let mut markups = Vec::new(); + let mut has_source_target = false; + + for target in target.targets_for_intent(TargetIntent::Describe) { + let hover = match target { + SemanticTarget::PreprocMacro(target) => { + render_macro_hover_target(db, file_id, offset, target) + } + SemanticTarget::Include(includes) => render_include_hover(db, includes), + SemanticTarget::Source(target) => { + has_source_target = true; + hover_for_source_target(sema, file_id.into(), target) + } + }?; + ranges.push(hover.range); + markups.push(hover.info); } + + let range = covering_range(&ranges)?; + let hover = RangeInfo::new(range, merge_hover_results(markups)?); + Some(if has_source_target { + with_expanded_macro_hover(db, file_id, offset, hover) + } else { + hover + }) +} + +fn covering_range(ranges: &[TextRange]) -> Option { + let start = ranges.iter().map(|range| range.start()).min()?; + let end = ranges.iter().map(|range| range.end()).max()?; + Some(TextRange::new(start, end)) } fn hover_for_source_target( diff --git a/crates/ide/src/references.rs b/crates/ide/src/references.rs index af25ea52..1d73e55c 100644 --- a/crates/ide/src/references.rs +++ b/crates/ide/src/references.rs @@ -103,7 +103,7 @@ fn render_references_target( target: TargetResolution<'_>, config: ReferencesConfig, ) -> Option> { - match target.for_intent(TargetIntent::FindReferences)? { + match target.unique_for_intent(TargetIntent::FindReferences)? { SemanticTarget::PreprocMacro(target) => { render_preproc_references_target(db, file_id, target, &config) } diff --git a/crates/ide/src/rename.rs b/crates/ide/src/rename.rs index 94603894..7bee6371 100644 --- a/crates/ide/src/rename.rs +++ b/crates/ide/src/rename.rs @@ -228,7 +228,7 @@ fn resolve_rename_target( rename_token_precedence, ); let SemanticTarget::Source(target) = - target.for_intent(TargetIntent::Rename).ok_or(RenameError::NoRefFound)? + target.unique_for_intent(TargetIntent::Rename).ok_or(RenameError::NoRefFound)? else { return Err(RenameError::NoRefFound); }; diff --git a/crates/ide/src/semantic_target.rs b/crates/ide/src/semantic_target.rs index 4d37d61c..e8ccf33b 100644 --- a/crates/ide/src/semantic_target.rs +++ b/crates/ide/src/semantic_target.rs @@ -11,8 +11,9 @@ use vfs::FileId; use crate::{ db::root_db::RootDb, source_targets::{ - SourceTarget, SourceTargetBlock, SourceTargetBlockReason, SourceTargetDomain, - SourceTargetResolution, source_target_at_offset, + SourceTarget, SourceTargetAlternatives, SourceTargetAmbiguity, SourceTargetBlock, + SourceTargetBlockReason, SourceTargetDomain, SourceTargetResolution, + source_target_at_offset, }, }; @@ -51,30 +52,30 @@ bitflags::bitflags! { #[derive(Debug, Clone)] pub(crate) enum TargetResolution<'tree> { Resolved(TargetCandidate<'tree>), - Ambiguous(TargetAmbiguity), + Ambiguous(TargetAlternatives<'tree>), Blocked(TargetBlock), Unresolved, } impl<'tree> TargetResolution<'tree> { - pub(crate) fn for_intent(self, intent: TargetIntent) -> Option> { - self.into_primary(intent.capability()) + pub(crate) fn unique_for_intent(self, intent: TargetIntent) -> Option> { + let mut targets = self.targets_for_intent(intent); + (targets.len() == 1).then(|| targets.pop().expect("single target should exist")) } - fn into_primary(self, required: TargetCapability) -> Option> { + pub(crate) fn targets_for_intent(self, intent: TargetIntent) -> Vec> { + let required = intent.capability(); match self { - TargetResolution::Resolved(candidate) => candidate.into_target(required), - TargetResolution::Ambiguous(ambiguity) => { - let TargetAmbiguity { anchor, reason } = ambiguity; - let _ = (anchor, reason); - None + TargetResolution::Resolved(candidate) => { + candidate.into_target(required).into_iter().collect() } + TargetResolution::Ambiguous(alternatives) => alternatives.into_targets(required), TargetResolution::Blocked(block) => { let TargetBlock { anchor, reason } = block; let _ = (anchor, reason); - None + Vec::new() } - TargetResolution::Unresolved => None, + TargetResolution::Unresolved => Vec::new(), } } @@ -87,6 +88,9 @@ impl<'tree> TargetResolution<'tree> { let capabilities = source_capabilities(); Self::Resolved(TargetCandidate::new(SemanticTarget::Source(target), capabilities)) } + SourceTargetResolution::Ambiguous(alternatives) => { + Self::from_source_alternatives(file_id, alternatives) + } SourceTargetResolution::Blocked(block) => { let SourceTargetBlock { range, .. } = block.clone(); let anchor = TargetAnchor { @@ -99,6 +103,22 @@ impl<'tree> TargetResolution<'tree> { } } + fn from_source_alternatives( + file_id: FileId, + alternatives: SourceTargetAlternatives<'tree>, + ) -> Self { + let SourceTargetAlternatives { domain, range, reason, targets } = alternatives; + let anchor = + TargetAnchor { file_id, range, origin: TargetOrigin::from_source_domain(domain) }; + let reason = TargetAmbiguityReason::from_source(reason); + let capabilities = source_capabilities(); + let candidates = targets + .into_iter() + .map(|target| TargetCandidate::new(SemanticTarget::Source(target), capabilities)) + .collect(); + Self::Ambiguous(TargetAlternatives { anchor, reason, candidates }) + } + fn from_preproc_macro(target: PreprocMacroTarget) -> Self { let capabilities = target.capabilities(); Self::Resolved(TargetCandidate::new(SemanticTarget::PreprocMacro(target), capabilities)) @@ -118,9 +138,10 @@ impl<'tree> TargetResolution<'tree> { Self::Blocked(TargetBlock { anchor, reason: TargetBlockReason::PreprocUnavailable }) } (SourceTargetDomain::Preproc, SourceTargetBlockReason::Ambiguous { hits }) => { - Self::Ambiguous(TargetAmbiguity { + Self::Ambiguous(TargetAlternatives { anchor, - reason: TargetAmbiguityReason::PreprocHits { candidate_count: hits.len() }, + reason: TargetAmbiguityReason::PreprocHits { hit_count: hits.len() }, + candidates: Vec::new(), }) } } @@ -140,11 +161,15 @@ pub(crate) enum TargetOrigin { } impl TargetOrigin { - fn from_source_block(block: &SourceTargetBlock) -> Self { - match block.domain { + fn from_source_domain(domain: SourceTargetDomain) -> Self { + match domain { SourceTargetDomain::Preproc => TargetOrigin::MacroExpansion, } } + + fn from_source_block(block: &SourceTargetBlock) -> Self { + Self::from_source_domain(block.domain) + } } #[derive(Debug, Clone)] @@ -168,14 +193,33 @@ impl<'tree> TargetCandidate<'tree> { } #[derive(Debug, Clone)] -pub(crate) struct TargetAmbiguity { +pub(crate) struct TargetAlternatives<'tree> { pub anchor: TargetAnchor, pub reason: TargetAmbiguityReason, + pub candidates: Vec>, +} + +impl<'tree> TargetAlternatives<'tree> { + fn into_targets(self, required: TargetCapability) -> Vec> { + let Self { anchor, reason, candidates } = self; + let _ = (anchor, reason); + candidates.into_iter().filter_map(|candidate| candidate.into_target(required)).collect() + } } #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) enum TargetAmbiguityReason { - PreprocHits { candidate_count: usize }, + PreprocHits { hit_count: usize }, +} + +impl TargetAmbiguityReason { + fn from_source(reason: SourceTargetAmbiguity) -> Self { + match reason { + SourceTargetAmbiguity::PreprocHits { hit_count } => { + TargetAmbiguityReason::PreprocHits { hit_count } + } + } + } } #[derive(Debug, Clone)] @@ -345,11 +389,11 @@ mod tests { let resolution = resolve_semantic_target(host.raw_db(), file_id, offset, Some(root), token_precedence); assert!(matches!( - resolution.clone().for_intent(TargetIntent::Describe), + resolution.clone().unique_for_intent(TargetIntent::Describe), Some(SemanticTarget::Source(_)) )); assert!(matches!( - resolution.clone().for_intent(TargetIntent::Rename), + resolution.clone().unique_for_intent(TargetIntent::Rename), Some(SemanticTarget::Source(_)) )); @@ -403,6 +447,39 @@ mod tests { panic!("conflicting source target should be ambiguous"); }; - assert_eq!(ambiguity.reason, TargetAmbiguityReason::PreprocHits { candidate_count: 0 }); + assert_eq!(ambiguity.reason, TargetAmbiguityReason::PreprocHits { hit_count: 0 }); + assert!(ambiguity.candidates.is_empty()); + } + + #[test] + 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, + range: target_range, + tokens: Vec::new(), + }; + let alternatives = crate::source_targets::SourceTargetAlternatives { + domain: crate::source_targets::SourceTargetDomain::Preproc, + range, + reason: crate::source_targets::SourceTargetAmbiguity::PreprocHits { hit_count: 2 }, + targets: vec![target.clone(), target], + }; + + let resolution = TargetResolution::from_source_resolution( + FileId(0), + crate::source_targets::SourceTargetResolution::Ambiguous(alternatives), + ); + + assert!(resolution.clone().unique_for_intent(TargetIntent::Describe).is_none()); + assert_eq!(resolution.clone().targets_for_intent(TargetIntent::Describe).len(), 2); + + let TargetResolution::Ambiguous(alternatives) = resolution else { + panic!("source alternatives should stay ambiguous"); + }; + assert_eq!(alternatives.anchor.range, range); + assert_eq!(alternatives.reason, TargetAmbiguityReason::PreprocHits { hit_count: 2 }); + assert_eq!(alternatives.candidates.len(), 2); } } diff --git a/crates/ide/src/source_targets.rs b/crates/ide/src/source_targets.rs index b69a0fcc..6d759212 100644 --- a/crates/ide/src/source_targets.rs +++ b/crates/ide/src/source_targets.rs @@ -15,11 +15,14 @@ mod preproc; use macro_gate::source_macro_invocation_may_cover_offset; use preproc::preproc_source_target_at_offset; #[cfg(test)] -use preproc::{push_unique_preproc_hit, syntax_tokens_for_preproc_hit}; +use preproc::{ + ambiguous_preproc_source_targets, push_unique_preproc_hit, syntax_tokens_for_preproc_hit, +}; #[derive(Debug, Clone)] pub(crate) enum SourceTargetResolution<'tree> { Resolved(SourceTarget<'tree>), + Ambiguous(SourceTargetAlternatives<'tree>), Blocked(SourceTargetBlock), } @@ -27,6 +30,7 @@ impl<'tree> SourceTargetResolution<'tree> { pub(crate) fn resolved(self) -> Option> { match self { Self::Resolved(selection) => Some(selection), + Self::Ambiguous(SourceTargetAlternatives { .. }) => None, Self::Blocked(SourceTargetBlock { .. }) => None, } } @@ -79,6 +83,34 @@ pub(crate) enum SourceTargetDomain { Preproc, } +#[derive(Debug, Clone)] +pub(crate) struct SourceTargetAlternatives<'tree> { + pub domain: SourceTargetDomain, + pub range: TextRange, + pub reason: SourceTargetAmbiguity, + pub targets: Vec>, +} + +impl<'tree> SourceTargetAlternatives<'tree> { + fn preproc_ambiguous( + range: TextRange, + hit_count: usize, + targets: Vec>, + ) -> Self { + Self { + domain: SourceTargetDomain::Preproc, + range, + reason: SourceTargetAmbiguity::PreprocHits { hit_count }, + targets, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum SourceTargetAmbiguity { + PreprocHits { hit_count: usize }, +} + #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) struct SourceTargetBlock { pub domain: SourceTargetDomain, @@ -198,6 +230,7 @@ fn normal_syntax_source_target_at_offset<'tree>( enum SourceTargetProviderResult<'tree> { Resolved(SourceTarget<'tree>), + Ambiguous(SourceTargetAlternatives<'tree>), Blocked(SourceTargetBlock), NotApplicable, } @@ -206,6 +239,7 @@ impl<'tree> SourceTargetProviderResult<'tree> { fn into_resolution(self) -> Option> { match self { Self::Resolved(selection) => Some(SourceTargetResolution::Resolved(selection)), + Self::Ambiguous(alternatives) => Some(SourceTargetResolution::Ambiguous(alternatives)), Self::Blocked(block) => Some(SourceTargetResolution::Blocked(block)), Self::NotApplicable => None, } diff --git a/crates/ide/src/source_targets/preproc.rs b/crates/ide/src/source_targets/preproc.rs index 36ffc811..d65ee52a 100644 --- a/crates/ide/src/source_targets/preproc.rs +++ b/crates/ide/src/source_targets/preproc.rs @@ -8,8 +8,8 @@ use utils::line_index::{TextRange, TextSize}; use vfs::FileId; use super::{ - PreprocTokenHit, SourceTarget, SourceTargetBlock, SourceTargetProviderResult, - SourceTargetRequestCache, SourceTargetResolution, covering_range, + PreprocTokenHit, SourceTarget, SourceTargetAlternatives, SourceTargetBlock, + SourceTargetProviderResult, SourceTargetRequestCache, SourceTargetResolution, covering_range, macro_gate::source_macro_invocation_may_cover_offset, normal_syntax_source_target_at_offset, }; use crate::db::root_db::RootDb; @@ -45,7 +45,14 @@ pub(super) fn preproc_source_target_at_offset<'tree>( SourceTargetProviderResult::Blocked(SourceTargetBlock::preproc_unavailable(range)) } PreprocHitLookup::Ambiguous { range, hits } => { - SourceTargetProviderResult::Blocked(SourceTargetBlock::preproc_ambiguous(range, hits)) + let block_hits = hits.clone(); + ambiguous_preproc_source_targets(root, offset, precedence, range, hits) + .map(SourceTargetProviderResult::Ambiguous) + .unwrap_or_else(|| { + SourceTargetProviderResult::Blocked(SourceTargetBlock::preproc_ambiguous( + range, block_hits, + )) + }) } } } @@ -128,6 +135,46 @@ fn hits_have_one_origin(hits: &[PreprocTokenHit]) -> bool { hits.iter().all(|hit| hit.origin == first.origin) } +pub(super) fn ambiguous_preproc_source_targets<'tree>( + root: SyntaxNode<'tree>, + offset: TextSize, + precedence: &impl Fn(TokenKind) -> usize, + range: TextRange, + hits: Vec, +) -> Option> { + let hit_count = hits.len(); + let groups = group_preproc_hits_by_origin(hits); + if groups.len() <= 1 { + return None; + } + + let mut targets = Vec::with_capacity(groups.len()); + for group in groups { + let group_range = + covering_range(&group.iter().map(|hit| hit.source_range).collect::>()) + .unwrap_or(range); + let tokens = syntax_tokens_for_preproc_hit(root, offset, precedence, &group)?; + targets.push(SourceTarget::preproc(group_range, group, tokens)); + } + + Some(SourceTargetAlternatives::preproc_ambiguous(range, hit_count, targets)) +} + +fn group_preproc_hits_by_origin(hits: Vec) -> Vec> { + let mut groups: Vec> = Vec::new(); + for hit in hits { + if let Some(group) = groups + .iter_mut() + .find(|group| group.first().is_some_and(|candidate| candidate.origin == hit.origin)) + { + group.push(hit); + } else { + groups.push(vec![hit]); + } + } + groups +} + pub(super) fn syntax_tokens_for_preproc_hit<'tree>( root: SyntaxNode<'tree>, offset: TextSize, diff --git a/crates/ide/src/source_targets/tests.rs b/crates/ide/src/source_targets/tests.rs index 4866c2d7..4b2c9ad3 100644 --- a/crates/ide/src/source_targets/tests.rs +++ b/crates/ide/src/source_targets/tests.rs @@ -252,15 +252,14 @@ fn source_targets_reports_ambiguous_preproc_hits_for_conflicting_targets() { let second = TextRange::new(parser_range.start() + TextSize::from(1), parser_range.end()); let hits = vec![test_source_hit(file_id, first, 0), test_source_hit(file_id, second, 1)]; - let SourceTargetProviderResult::Blocked(SourceTargetBlock { - reason: SourceTargetBlockReason::Ambiguous { hits }, - .. - }) = preproc_provider_result_from_hits(root, offset, &test_precedence, hits, parser_range) + let SourceTargetProviderResult::Ambiguous(alternatives) = + preproc_provider_result_from_hits(root, offset, &test_precedence, hits, parser_range) else { - panic!("conflicting preproc targets should be ambiguous"); + panic!("conflicting preproc targets should produce alternatives"); }; - assert_eq!(hits.len(), 2); + assert_eq!(alternatives.reason, SourceTargetAmbiguity::PreprocHits { hit_count: 2 }); + assert_eq!(alternatives.targets.len(), 2); } fn root_and_offset<'tree>( @@ -343,10 +342,14 @@ fn preproc_provider_result_from_hits<'tree>( .first() .is_some_and(|first| unique_hits.iter().any(|hit| hit.origin != first.origin)); if has_conflicting_origin { - return SourceTargetProviderResult::Blocked(SourceTargetBlock::preproc_ambiguous( - range, - unique_hits, - )); + let block_hits = unique_hits.clone(); + return ambiguous_preproc_source_targets(root, offset, precedence, range, unique_hits) + .map(SourceTargetProviderResult::Ambiguous) + .unwrap_or_else(|| { + SourceTargetProviderResult::Blocked(SourceTargetBlock::preproc_ambiguous( + range, block_hits, + )) + }); } let Some(tokens) = syntax_tokens_for_preproc_hit(root, offset, precedence, &unique_hits) else { return SourceTargetProviderResult::Blocked(SourceTargetBlock::preproc_unavailable(range)); From c4af9d0f53125ccbeb1d6d99e6b07b31b8f4236d Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Fri, 26 Jun 2026 00:04:18 +0800 Subject: [PATCH 6/8] refactor(ide): use target alternatives for declarations --- crates/ide/src/goto_declaration.rs | 50 +++++++++++++++++++++++++++--- 1 file changed, 45 insertions(+), 5 deletions(-) diff --git a/crates/ide/src/goto_declaration.rs b/crates/ide/src/goto_declaration.rs index a071d43f..79954f24 100644 --- a/crates/ide/src/goto_declaration.rs +++ b/crates/ide/src/goto_declaration.rs @@ -1,5 +1,6 @@ -use hir::semantics::Semantics; +use hir::{file::HirFileId, semantics::Semantics}; use itertools::Itertools; +use utils::line_index::TextRange; use crate::{ FilePosition, RangeInfo, @@ -8,6 +9,7 @@ use crate::{ goto_definition, navigation_target::{NavTarget, ToNav}, semantic_target::{SemanticTarget, TargetIntent, resolve_semantic_target}, + source_targets::SourceTarget, }; pub(crate) fn goto_declaration( @@ -24,14 +26,46 @@ pub(crate) fn goto_declaration( parsed_file.root(), goto_definition::token_precedence, ); - let SemanticTarget::Source(target) = target.unique_for_intent(TargetIntent::Navigate)? else { - return None; - }; + render_declaration_target( + db, + hir_file_id, + &sema, + target.targets_for_intent(TargetIntent::Navigate), + ) +} + +fn render_declaration_target( + db: &RootDb, + hir_file_id: HirFileId, + sema: &Semantics, + targets: Vec>, +) -> Option>> { + let mut ranges = Vec::new(); + let mut navs = Vec::new(); + for target in targets { + let SemanticTarget::Source(target) = target else { + return None; + }; + let target = render_source_declaration_target(db, hir_file_id, sema, target)?; + ranges.push(target.range); + navs.extend(target.info); + } + + let range = covering_range(&ranges)?; + Some(RangeInfo::new(range, navs.into_iter().unique().collect())) +} + +fn render_source_declaration_target( + db: &RootDb, + hir_file_id: HirFileId, + sema: &Semantics, + target: SourceTarget<'_>, +) -> Option>> { let (range, tokens) = target.into_parts(); let origins = tokens .into_iter() - .filter_map(|token| match DefinitionClass::resolve(&sema, hir_file_id, token)? { + .filter_map(|token| match DefinitionClass::resolve(sema, hir_file_id, token)? { DefinitionClass::Definition(definition) => { Some(definition.declaration_origins().into_iter().collect_vec()) } @@ -52,3 +86,9 @@ pub(crate) fn goto_declaration( Some(RangeInfo::new(range, navs)) } + +fn covering_range(ranges: &[TextRange]) -> Option { + let start = ranges.iter().map(|range| range.start()).min()?; + let end = ranges.iter().map(|range| range.end()).max()?; + Some(TextRange::new(start, end)) +} From e9901de0c8131559019815d4681652ff2f7fb17b Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Fri, 26 Jun 2026 00:28:54 +0800 Subject: [PATCH 7/8] refactor(utils): share covering text range helper --- crates/ide/src/goto_declaration.rs | 8 +------- crates/ide/src/goto_definition.rs | 8 +------- crates/ide/src/hover.rs | 8 +------- crates/ide/src/hover/macro_hover/expansion.rs | 8 +------- crates/ide/src/source_targets.rs | 6 ------ crates/ide/src/source_targets/preproc.rs | 4 ++-- crates/ide/src/source_targets/tests.rs | 1 + crates/utils/src/line_index.rs | 6 ++++++ 8 files changed, 13 insertions(+), 36 deletions(-) diff --git a/crates/ide/src/goto_declaration.rs b/crates/ide/src/goto_declaration.rs index 79954f24..e30929b0 100644 --- a/crates/ide/src/goto_declaration.rs +++ b/crates/ide/src/goto_declaration.rs @@ -1,6 +1,6 @@ use hir::{file::HirFileId, semantics::Semantics}; use itertools::Itertools; -use utils::line_index::TextRange; +use utils::line_index::covering_range; use crate::{ FilePosition, RangeInfo, @@ -86,9 +86,3 @@ fn render_source_declaration_target( Some(RangeInfo::new(range, navs)) } - -fn covering_range(ranges: &[TextRange]) -> Option { - let start = ranges.iter().map(|range| range.start()).min()?; - let end = ranges.iter().map(|range| range.end()).max()?; - Some(TextRange::new(start, end)) -} diff --git a/crates/ide/src/goto_definition.rs b/crates/ide/src/goto_definition.rs index f131fad7..9f4d35bf 100644 --- a/crates/ide/src/goto_definition.rs +++ b/crates/ide/src/goto_definition.rs @@ -10,7 +10,7 @@ use syntax::{ SyntaxTokenWithParent, TokenKind, token::{TokenKindExt, pair_token}, }; -use utils::line_index::{TextRange, TextSize}; +use utils::line_index::{TextRange, TextSize, covering_range}; use vfs::FileId; use crate::{ @@ -62,12 +62,6 @@ fn render_definition_target( Some(RangeInfo::new(range, navs.into_iter().unique().collect())) } -fn covering_range(ranges: &[TextRange]) -> Option { - let start = ranges.iter().map(|range| range.start()).min()?; - let end = ranges.iter().map(|range| range.end()).max()?; - Some(TextRange::new(start, end)) -} - fn render_source_definition_target( db: &RootDb, file_id: FileId, diff --git a/crates/ide/src/hover.rs b/crates/ide/src/hover.rs index e36e76d2..32caeae9 100644 --- a/crates/ide/src/hover.rs +++ b/crates/ide/src/hover.rs @@ -10,7 +10,7 @@ use syntax::{ }; use utils::{ get::GetRef, - line_index::{TextRange, TextSize}, + line_index::{TextRange, TextSize, covering_range}, uniq_vec::UniqVec, }; use vfs::FileId; @@ -92,12 +92,6 @@ fn render_hover_target( }) } -fn covering_range(ranges: &[TextRange]) -> Option { - let start = ranges.iter().map(|range| range.start()).min()?; - let end = ranges.iter().map(|range| range.end()).max()?; - Some(TextRange::new(start, end)) -} - fn hover_for_source_target( sema: &Semantics, hir_file_id: HirFileId, diff --git a/crates/ide/src/hover/macro_hover/expansion.rs b/crates/ide/src/hover/macro_hover/expansion.rs index 61cd3614..cde0733d 100644 --- a/crates/ide/src/hover/macro_hover/expansion.rs +++ b/crates/ide/src/hover/macro_hover/expansion.rs @@ -3,7 +3,7 @@ use hir::{ hir_def::macro_file::{MacroFileExpansion, macro_file_expansion, macro_files_at_offset}, preproc::{MacroReferenceDefinitions, macro_reference_definitions_at}, }; -use utils::line_index::{TextRange, TextSize}; +use utils::line_index::{TextRange, TextSize, covering_range}; use vfs::FileId; use super::markup::{macro_expansion_source_fact, render_macro_expansion_header}; @@ -150,9 +150,3 @@ fn common_whitespace_prefix<'a>(left: &'a str, right: &'a str) -> &'a str { let end = left.bytes().zip(right.bytes()).take_while(|(left, right)| left == right).count(); &left[..end] } - -fn covering_range(ranges: &[TextRange]) -> Option { - let start = ranges.iter().map(|range| range.start()).min()?; - let end = ranges.iter().map(|range| range.end()).max()?; - Some(TextRange::new(start, end)) -} diff --git a/crates/ide/src/source_targets.rs b/crates/ide/src/source_targets.rs index 6d759212..094e6a06 100644 --- a/crates/ide/src/source_targets.rs +++ b/crates/ide/src/source_targets.rs @@ -246,11 +246,5 @@ impl<'tree> SourceTargetProviderResult<'tree> { } } -fn covering_range(ranges: &[TextRange]) -> Option { - let start = ranges.iter().map(|range| range.start()).min()?; - let end = ranges.iter().map(|range| range.end()).max()?; - Some(TextRange::new(start, end)) -} - #[cfg(test)] mod tests; diff --git a/crates/ide/src/source_targets/preproc.rs b/crates/ide/src/source_targets/preproc.rs index d65ee52a..9264aeb9 100644 --- a/crates/ide/src/source_targets/preproc.rs +++ b/crates/ide/src/source_targets/preproc.rs @@ -4,12 +4,12 @@ use hir::{ hir_def::macro_file::{ExpansionSourceHit, MacroFileId, Origin, SourceEmittedTokenId}, }; use syntax::{SyntaxElement, SyntaxNode, SyntaxTokenWithParent, TokenKind, WalkEvent}; -use utils::line_index::{TextRange, TextSize}; +use utils::line_index::{TextRange, TextSize, covering_range}; use vfs::FileId; use super::{ PreprocTokenHit, SourceTarget, SourceTargetAlternatives, SourceTargetBlock, - SourceTargetProviderResult, SourceTargetRequestCache, SourceTargetResolution, covering_range, + SourceTargetProviderResult, SourceTargetRequestCache, SourceTargetResolution, macro_gate::source_macro_invocation_may_cover_offset, normal_syntax_source_target_at_offset, }; use crate::db::root_db::RootDb; diff --git a/crates/ide/src/source_targets/tests.rs b/crates/ide/src/source_targets/tests.rs index 4b2c9ad3..d5f6974f 100644 --- a/crates/ide/src/source_targets/tests.rs +++ b/crates/ide/src/source_targets/tests.rs @@ -3,6 +3,7 @@ use syntax::{ SyntaxElement, SyntaxNode, SyntaxTree, SyntaxTreeOptions, WalkEvent, preproc::TokenOrigin, token::TokenKindExt, }; +use utils::line_index::covering_range; use super::*; diff --git a/crates/utils/src/line_index.rs b/crates/utils/src/line_index.rs index 4693d94c..84c66629 100644 --- a/crates/utils/src/line_index.rs +++ b/crates/utils/src/line_index.rs @@ -5,6 +5,12 @@ use std::ops::Range; use nohash_hasher::IntMap; pub use text_size::{TextRange, TextSize}; +pub fn covering_range(ranges: &[TextRange]) -> Option { + let start = ranges.iter().map(|range| range.start()).min()?; + let end = ranges.iter().map(|range| range.end()).max()?; + Some(TextRange::new(start, end)) +} + /// `(line, column)` information in the native, UTF-8 encoding. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct LineCol { From 202047641122ef34d42a2e4ec42a8e0aa5350b86 Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Fri, 26 Jun 2026 00:41:34 +0800 Subject: [PATCH 8/8] chore(ide): satisfy semantic target clippy lint --- crates/ide/src/semantic_target.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/ide/src/semantic_target.rs b/crates/ide/src/semantic_target.rs index e8ccf33b..0d549461 100644 --- a/crates/ide/src/semantic_target.rs +++ b/crates/ide/src/semantic_target.rs @@ -298,10 +298,10 @@ fn preproc_macro_target_at( return Some(PreprocMacroTarget::ParamDefinition(definition)); } - if let Ok(Some(resolution)) = macro_param_reference_definitions_at(db, file_id, offset) { - if !resolution.definitions.is_empty() { - return Some(PreprocMacroTarget::ParamReference(resolution)); - } + if let Ok(Some(resolution)) = macro_param_reference_definitions_at(db, file_id, offset) + && !resolution.definitions.is_empty() + { + return Some(PreprocMacroTarget::ParamReference(resolution)); } if let Ok(Some(definition)) = macro_definition_at(db, file_id, offset) {