diff --git a/crates/ide/src/document_highlight.rs b/crates/ide/src/document_highlight.rs index 98fe48b3..2dc8fb27 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, TargetIntent, resolve_semantic_target}, }; #[derive(Debug, Clone)] @@ -32,16 +33,11 @@ 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( - db, - file_id, - root, - offset, - token_precedence, - )? - .resolved()? - .into_tokens(); + let target = resolve_semantic_target(db, file_id, offset, parsed_file.root(), token_precedence); + let SemanticTarget::Source(target) = target.unique_for_intent(TargetIntent::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..e30929b0 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::covering_range; use crate::{ FilePosition, RangeInfo, @@ -7,6 +8,8 @@ use crate::{ definitions::DefinitionClass, goto_definition, navigation_target::{NavTarget, ToNav}, + semantic_target::{SemanticTarget, TargetIntent, resolve_semantic_target}, + source_targets::SourceTarget, }; pub(crate) fn goto_declaration( @@ -16,20 +19,53 @@ 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(), goto_definition::token_precedence, - )? - .resolved()?; + ); + 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()) } diff --git a/crates/ide/src/goto_definition.rs b/crates/ide/src/goto_definition.rs index 383c668b..9f4d35bf 100644 --- a/crates/ide/src/goto_definition.rs +++ b/crates/ide/src/goto_definition.rs @@ -2,20 +2,15 @@ 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}; +use utils::line_index::{TextRange, TextSize, covering_range}; use vfs::FileId; use crate::{ @@ -23,63 +18,48 @@ use crate::{ db::root_db::RootDb, definitions::DefinitionClass, navigation_target::{NavTarget, ToNav}, - source_targets::{SourceTarget, source_target_at_offset}, + semantic_target::{ + PreprocMacroTarget, SemanticTarget, TargetIntent, TargetResolution, 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(), 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: TargetResolution<'_>, ) -> Option>> { - match target { - DefinitionTarget::Preproc(target) => render_preproc_definition_target(*target), - DefinitionTarget::Include(includes) => render_include_definition_target(db, includes), - DefinitionTarget::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 render_source_definition_target( @@ -120,47 +100,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 +148,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..32caeae9 100644 --- a/crates/ide/src/hover.rs +++ b/crates/ide/src/hover.rs @@ -3,14 +3,14 @@ use hir::{ semantics::Semantics, }; use syntax::{ - SyntaxNode, SyntaxTokenWithParent, TokenKind, + SyntaxTokenWithParent, TokenKind, ast::{self, AstNode}, has_text_range::HasTextRange, token::TokenKindExt, }; use utils::{ get::GetRef, - line_index::{TextRange, TextSize}, + line_index::{TextRange, TextSize, covering_range}, uniq_vec::UniqVec, }; use vfs::FileId; @@ -20,15 +20,13 @@ 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, TargetIntent, TargetResolution, resolve_semantic_target}, + source_targets::SourceTarget, }; mod include; @@ -37,12 +35,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,43 +53,43 @@ 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(), 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: TargetResolution<'_>, ) -> 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) => { - 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 hover_for_source_target( 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/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/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..1d73e55c 100644 --- a/crates/ide/src/references.rs +++ b/crates/ide/src/references.rs @@ -3,32 +3,26 @@ 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, TargetIntent, TargetResolution, 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 +92,23 @@ 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(), 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: TargetResolution<'_>, config: ReferencesConfig, ) -> Option> { - match target { - ReferencesTarget::Preproc(target) => { + match target.unique_for_intent(TargetIntent::FindReferences)? { + 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/rename.rs b/crates/ide/src/rename.rs index 948700af..7bee6371 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, }; @@ -100,23 +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 root = parsed_file.root().ok_or(RenameError::NoRefFound)?; - let token = pick_token(root, offset)?; - 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( @@ -214,6 +201,7 @@ pub(crate) fn rename_conflict_info( } struct ResolvedRenameTarget { + range: TextRange, selected_def: Definition, targets: Vec, } @@ -230,25 +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 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(), + rename_token_precedence, + ); + let SemanticTarget::Source(target) = + target.unique_for_intent(TargetIntent::Rename).ok_or(RenameError::NoRefFound)? + else { + return Err(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)] @@ -572,8 +586,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 new file mode 100644 index 00000000..0d549461 --- /dev/null +++ b/crates/ide/src/semantic_target.rs @@ -0,0 +1,485 @@ +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, SourceTargetAlternatives, SourceTargetAmbiguity, SourceTargetBlock, + SourceTargetBlockReason, SourceTargetDomain, SourceTargetResolution, + source_target_at_offset, + }, +}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum TargetIntent { + Describe, + Navigate, + FindReferences, + Highlight, + 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 { + 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) enum TargetResolution<'tree> { + Resolved(TargetCandidate<'tree>), + Ambiguous(TargetAlternatives<'tree>), + Blocked(TargetBlock), + Unresolved, +} + +impl<'tree> TargetResolution<'tree> { + 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")) + } + + pub(crate) fn targets_for_intent(self, intent: TargetIntent) -> Vec> { + let required = intent.capability(); + match self { + 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); + Vec::new() + } + TargetResolution::Unresolved => Vec::new(), + } + } + + pub(crate) fn from_source_resolution( + file_id: FileId, + resolution: SourceTargetResolution<'tree>, + ) -> Self { + match resolution { + SourceTargetResolution::Resolved(target) => { + 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 { + file_id, + range, + origin: TargetOrigin::from_source_block(&block), + }; + Self::from_source_block(anchor, block) + } + } + } + + 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)) + } + + fn from_include(includes: Vec) -> Option { + includes.first()?; + Some(Self::Resolved(TargetCandidate::new( + 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(TargetAlternatives { + anchor, + reason: TargetAmbiguityReason::PreprocHits { hit_count: hits.len() }, + candidates: Vec::new(), + }) + } + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct TargetAnchor { + pub file_id: FileId, + pub range: TextRange, + pub origin: TargetOrigin, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum TargetOrigin { + MacroExpansion, +} + +impl TargetOrigin { + 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)] +pub(crate) struct TargetCandidate<'tree> { + pub target: SemanticTarget<'tree>, + pub capabilities: TargetCapability, +} + +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, capabilities } = self; + if !capabilities.contains(required) { + return None; + } + Some(target) + } +} + +#[derive(Debug, Clone)] +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 { hit_count: usize }, +} + +impl TargetAmbiguityReason { + fn from_source(reason: SourceTargetAmbiguity) -> Self { + match reason { + SourceTargetAmbiguity::PreprocHits { hit_count } => { + TargetAmbiguityReason::PreprocHits { hit_count } + } + } + } +} + +#[derive(Debug, Clone)] +pub(crate) struct TargetBlock { + pub anchor: TargetAnchor, + pub reason: TargetBlockReason, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum TargetBlockReason { + PreprocUnavailable, +} + +#[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 { + 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>, + 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(target); + } + + if let Some(includes) = include_target_at(db, file_id, offset) { + return TargetResolution::from_include(includes).unwrap_or(TargetResolution::Unresolved); + } + + let Some(root) = root else { + return TargetResolution::Unresolved; + }; + source_target_at_offset(db, file_id, root, offset, precedence) + .map(|resolution| TargetResolution::from_source_resolution(file_id, resolution)) + .unwrap_or(TargetResolution::Unresolved) +} + +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) + && !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 resolution = + resolve_semantic_target(host.raw_db(), file_id, offset, Some(root), token_precedence); + assert!(matches!( + resolution.clone().unique_for_intent(TargetIntent::Describe), + Some(SemanticTarget::Source(_)) + )); + assert!(matches!( + resolution.clone().unique_for_intent(TargetIntent::Rename), + Some(SemanticTarget::Source(_)) + )); + + let TargetResolution::Resolved(target) = resolution else { + panic!("source token should resolve"); + }; + + 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] + 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 resolution = TargetResolution::from_source_resolution( + FileId(0), + crate::source_targets::SourceTargetResolution::Blocked(block.clone()), + ); + + 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] + 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 resolution = TargetResolution::from_source_resolution( + FileId(0), + crate::source_targets::SourceTargetResolution::Blocked(block), + ); + + let TargetResolution::Ambiguous(ambiguity) = resolution else { + panic!("conflicting source target should be ambiguous"); + }; + + 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..094e6a06 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,17 +239,12 @@ 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, } } } -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 36ffc811..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, SourceTargetBlock, SourceTargetProviderResult, - SourceTargetRequestCache, SourceTargetResolution, covering_range, + PreprocTokenHit, SourceTarget, SourceTargetAlternatives, SourceTargetBlock, + SourceTargetProviderResult, SourceTargetRequestCache, SourceTargetResolution, 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..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::*; @@ -252,15 +253,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 +343,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)); 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 {