Skip to main content

ide/
navigation_target.rs

1//! See [`NavigationTarget`].
2
3use std::fmt;
4
5use arrayvec::ArrayVec;
6use either::Either;
7use hir::{
8    AssocItem, Crate, FieldSource, HasContainer, HasCrate, HasSource, HirDisplay, HirFileId,
9    InFile, LocalSource, ModuleSource, Name, Semantics, Symbol, sym, symbols::FileSymbol,
10};
11use ide_db::{
12    FileId, FileRange, RootDatabase, SymbolKind,
13    base_db::{CrateOrigin, LangCrateOrigin, all_crates},
14    defs::{Definition, find_std_module},
15    documentation::{Documentation, HasDocs},
16    famous_defs::FamousDefs,
17    ra_fixture::UpmapFromRaFixture,
18};
19use stdx::never;
20use syntax::{
21    AstNode, AstPtr, SyntaxNode, TextRange,
22    ast::{self, HasName},
23};
24
25/// `NavigationTarget` represents an element in the editor's UI which you can
26/// click on to navigate to a particular piece of code.
27///
28/// Typically, a `NavigationTarget` corresponds to some element in the source
29/// code, like a function or a struct, but this is not strictly required.
30#[derive(Clone, PartialEq, Eq, Hash)]
31pub struct NavigationTarget {
32    pub file_id: FileId,
33    /// Range which encompasses the whole element.
34    ///
35    /// Should include body, doc comments, attributes, etc.
36    ///
37    /// Clients should use this range to answer "is the cursor inside the
38    /// element?" question.
39    pub full_range: TextRange,
40    /// A "most interesting" range within the `full_range`.
41    ///
42    /// Typically, `full_range` is the whole syntax node, including doc
43    /// comments, and `focus_range` is the range of the identifier.
44    ///
45    /// Clients should place the cursor on this range when navigating to this target.
46    ///
47    /// This range must be contained within [`Self::full_range`].
48    pub focus_range: Option<TextRange>,
49    pub name: Symbol,
50    pub kind: Option<SymbolKind>,
51    pub container_name: Option<Symbol>,
52    pub description: Option<String>,
53    // FIXME: Use the database lifetime here.
54    pub docs: Option<Documentation<'static>>,
55    /// In addition to a `name` field, a `NavigationTarget` may also be aliased
56    /// In such cases we want a `NavigationTarget` to be accessible by its alias
57    pub alias: Option<Symbol>,
58}
59
60impl fmt::Debug for NavigationTarget {
61    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62        let mut f = f.debug_struct("NavigationTarget");
63        macro_rules! opt {
64            ($($name:ident)*) => {$(
65                if let Some(it) = &self.$name {
66                    f.field(stringify!($name), it);
67                }
68            )*}
69        }
70        f.field("file_id", &self.file_id).field("full_range", &self.full_range);
71        opt!(focus_range);
72        f.field("name", &self.name);
73        opt!(kind container_name description docs);
74        f.finish()
75    }
76}
77
78impl UpmapFromRaFixture for NavigationTarget {
79    fn upmap_from_ra_fixture(
80        self,
81        analysis: &ide_db::ra_fixture::RaFixtureAnalysis,
82        _virtual_file_id: FileId,
83        real_file_id: FileId,
84    ) -> Result<Self, ()> {
85        let virtual_file_id = self.file_id;
86        Ok(NavigationTarget {
87            file_id: real_file_id,
88            full_range: self.full_range.upmap_from_ra_fixture(
89                analysis,
90                virtual_file_id,
91                real_file_id,
92            )?,
93            focus_range: self.focus_range.upmap_from_ra_fixture(
94                analysis,
95                virtual_file_id,
96                real_file_id,
97            )?,
98            name: self.name.upmap_from_ra_fixture(analysis, virtual_file_id, real_file_id)?,
99            kind: self.kind.upmap_from_ra_fixture(analysis, virtual_file_id, real_file_id)?,
100            container_name: self.container_name.upmap_from_ra_fixture(
101                analysis,
102                virtual_file_id,
103                real_file_id,
104            )?,
105            description: self.description.upmap_from_ra_fixture(
106                analysis,
107                virtual_file_id,
108                real_file_id,
109            )?,
110            docs: self.docs.upmap_from_ra_fixture(analysis, virtual_file_id, real_file_id)?,
111            alias: self.alias.upmap_from_ra_fixture(analysis, virtual_file_id, real_file_id)?,
112        })
113    }
114}
115
116pub(crate) trait ToNav {
117    fn to_nav(&self, db: &RootDatabase) -> UpmappingResult<NavigationTarget>;
118}
119
120pub trait TryToNav {
121    fn try_to_nav(
122        &self,
123        sema: &Semantics<'_, RootDatabase>,
124    ) -> Option<UpmappingResult<NavigationTarget>>;
125}
126
127impl<T: TryToNav, U: TryToNav> TryToNav for Either<T, U> {
128    fn try_to_nav(
129        &self,
130        sema: &Semantics<'_, RootDatabase>,
131    ) -> Option<UpmappingResult<NavigationTarget>> {
132        match self {
133            Either::Left(it) => it.try_to_nav(sema),
134            Either::Right(it) => it.try_to_nav(sema),
135        }
136    }
137}
138
139impl NavigationTarget {
140    pub fn focus_or_full_range(&self) -> TextRange {
141        self.focus_range.unwrap_or(self.full_range)
142    }
143
144    pub(crate) fn from_module_to_decl(
145        db: &RootDatabase,
146        module: hir::Module,
147    ) -> UpmappingResult<NavigationTarget> {
148        let name = module.name(db).map(|it| it.symbol().clone()).unwrap_or_else(|| sym::underscore);
149        match module.declaration_source(db) {
150            Some(InFile { value, file_id }) => {
151                orig_range_with_focus(db, file_id, value.syntax(), value.name()).map(
152                    |(FileRange { file_id, range: full_range }, focus_range)| {
153                        let mut res = NavigationTarget::from_syntax(
154                            file_id,
155                            name.clone(),
156                            focus_range,
157                            full_range,
158                            SymbolKind::Module,
159                        );
160                        res.docs = module.docs(db).map(Documentation::into_owned);
161                        res.description = Some(
162                            module.display(db, module.krate(db).to_display_target(db)).to_string(),
163                        );
164                        res
165                    },
166                )
167            }
168            _ => module.to_nav(db),
169        }
170    }
171
172    #[cfg(test)]
173    pub(crate) fn debug_render(&self) -> String {
174        let mut buf = format!(
175            "{} {:?} {:?} {:?}",
176            self.name,
177            self.kind.unwrap(),
178            self.file_id,
179            self.full_range
180        );
181        if let Some(focus_range) = self.focus_range {
182            buf.push_str(&format!(" {focus_range:?}"))
183        }
184        if let Some(container_name) = &self.container_name {
185            buf.push_str(&format!(" {container_name}"))
186        }
187        buf
188    }
189
190    /// Allows `NavigationTarget` to be created from a `NameOwner`
191    pub(crate) fn from_named(
192        db: &RootDatabase,
193        InFile { file_id, value }: InFile<&dyn ast::HasName>,
194        kind: SymbolKind,
195    ) -> UpmappingResult<NavigationTarget> {
196        let name =
197            value.name().map(|it| Symbol::intern(&it.text())).unwrap_or_else(|| sym::underscore);
198
199        orig_range_with_focus(db, file_id, value.syntax(), value.name()).map(
200            |(FileRange { file_id, range: full_range }, focus_range)| {
201                NavigationTarget::from_syntax(file_id, name.clone(), focus_range, full_range, kind)
202            },
203        )
204    }
205
206    pub(crate) fn from_named_with_range(
207        db: &RootDatabase,
208        ranges: InFile<(TextRange, Option<TextRange>)>,
209        name: Option<Name>,
210        kind: SymbolKind,
211    ) -> UpmappingResult<NavigationTarget> {
212        let InFile { file_id, value: (full_range, focus_range) } = ranges;
213        let name = name.map(|name| name.symbol().clone()).unwrap_or_else(|| sym::underscore);
214
215        orig_range_with_focus_r(db, file_id, full_range, focus_range).map(
216            |(FileRange { file_id, range: full_range }, focus_range)| {
217                NavigationTarget::from_syntax(file_id, name.clone(), focus_range, full_range, kind)
218            },
219        )
220    }
221
222    pub(crate) fn from_syntax(
223        file_id: FileId,
224        name: Symbol,
225        focus_range: Option<TextRange>,
226        full_range: TextRange,
227        kind: SymbolKind,
228    ) -> NavigationTarget {
229        NavigationTarget {
230            file_id,
231            name,
232            kind: Some(kind),
233            full_range,
234            focus_range,
235            container_name: None,
236            description: None,
237            docs: None,
238            alias: None,
239        }
240    }
241}
242
243impl<'db> TryToNav for FileSymbol<'db> {
244    fn try_to_nav(
245        &self,
246        sema: &Semantics<'_, RootDatabase>,
247    ) -> Option<UpmappingResult<NavigationTarget>> {
248        let db = sema.db;
249        let display_target = self.def.krate(db).to_display_target(db);
250        Some(
251            orig_range_with_focus_r(
252                db,
253                self.loc.hir_file_id,
254                self.loc.ptr.text_range(),
255                self.loc.name_ptr.map(AstPtr::text_range),
256            )
257            .map(|(FileRange { file_id, range: full_range }, focus_range)| {
258                NavigationTarget {
259                    file_id,
260                    name: self
261                        .is_alias
262                        .then(|| self.def.name(db))
263                        .flatten()
264                        .map_or_else(|| self.name.clone(), |it| it.symbol().clone()),
265                    alias: self.is_alias.then(|| self.name.clone()),
266                    kind: Some(SymbolKind::from_module_def(db, self.def)),
267                    full_range,
268                    focus_range,
269                    container_name: self.container_name.clone(),
270                    description: match self.def {
271                        hir::ModuleDef::Module(it) => {
272                            Some(it.display(db, display_target).to_string())
273                        }
274                        hir::ModuleDef::Function(it) => {
275                            Some(it.display(db, display_target).to_string())
276                        }
277                        hir::ModuleDef::Adt(it) => Some(it.display(db, display_target).to_string()),
278                        hir::ModuleDef::EnumVariant(it) => {
279                            Some(it.display(db, display_target).to_string())
280                        }
281                        hir::ModuleDef::Const(it) => {
282                            Some(it.display(db, display_target).to_string())
283                        }
284                        hir::ModuleDef::Static(it) => {
285                            Some(it.display(db, display_target).to_string())
286                        }
287                        hir::ModuleDef::Trait(it) => {
288                            Some(it.display(db, display_target).to_string())
289                        }
290                        hir::ModuleDef::TypeAlias(it) => {
291                            Some(it.display(db, display_target).to_string())
292                        }
293                        hir::ModuleDef::Macro(it) => {
294                            Some(it.display(db, display_target).to_string())
295                        }
296                        hir::ModuleDef::BuiltinType(_) => None,
297                    },
298                    docs: None,
299                }
300            }),
301        )
302    }
303}
304
305impl TryToNav for Definition {
306    fn try_to_nav(
307        &self,
308        sema: &Semantics<'_, RootDatabase>,
309    ) -> Option<UpmappingResult<NavigationTarget>> {
310        match self {
311            Definition::Local(it) => Some(it.to_nav(sema.db)),
312            Definition::Label(it) => it.try_to_nav(sema),
313            Definition::Module(it) => Some(it.to_nav(sema.db)),
314            Definition::Crate(it) => Some(it.to_nav(sema.db)),
315            Definition::Macro(it) => it.try_to_nav(sema),
316            Definition::Field(it) => it.try_to_nav(sema),
317            Definition::SelfType(it) => it.try_to_nav(sema),
318            Definition::GenericParam(it) => it.try_to_nav(sema),
319            Definition::Function(it) => it.try_to_nav(sema),
320            Definition::Adt(it) => it.try_to_nav(sema),
321            Definition::EnumVariant(it) => it.try_to_nav(sema),
322            Definition::Const(it) => it.try_to_nav(sema),
323            Definition::Static(it) => it.try_to_nav(sema),
324            Definition::Trait(it) => it.try_to_nav(sema),
325            Definition::TypeAlias(it) => it.try_to_nav(sema),
326            Definition::ExternCrateDecl(it) => it.try_to_nav(sema),
327            Definition::InlineAsmOperand(it) => it.try_to_nav(sema),
328            Definition::BuiltinType(it) => it.try_to_nav(sema),
329            Definition::BuiltinLifetime(_)
330            | Definition::TupleField(_)
331            | Definition::ToolModule(_)
332            | Definition::InlineAsmRegOrRegClass(_)
333            | Definition::BuiltinAttr(_) => None,
334            // FIXME: The focus range should be set to the helper declaration
335            Definition::DeriveHelper(it) => it.derive().try_to_nav(sema),
336        }
337    }
338}
339
340impl TryToNav for hir::ModuleDef {
341    fn try_to_nav(
342        &self,
343        sema: &Semantics<'_, RootDatabase>,
344    ) -> Option<UpmappingResult<NavigationTarget>> {
345        match self {
346            hir::ModuleDef::Module(it) => Some(it.to_nav(sema.db)),
347            hir::ModuleDef::Function(it) => it.try_to_nav(sema),
348            hir::ModuleDef::Adt(it) => it.try_to_nav(sema),
349            hir::ModuleDef::EnumVariant(it) => it.try_to_nav(sema),
350            hir::ModuleDef::Const(it) => it.try_to_nav(sema),
351            hir::ModuleDef::Static(it) => it.try_to_nav(sema),
352            hir::ModuleDef::Trait(it) => it.try_to_nav(sema),
353            hir::ModuleDef::TypeAlias(it) => it.try_to_nav(sema),
354            hir::ModuleDef::Macro(it) => it.try_to_nav(sema),
355            hir::ModuleDef::BuiltinType(_) => None,
356        }
357    }
358}
359
360pub(crate) trait ToNavFromAst: Sized {
361    const KIND: SymbolKind;
362    fn container_name(self, db: &RootDatabase) -> Option<Symbol> {
363        _ = db;
364        None
365    }
366}
367
368fn container_name(db: &RootDatabase, t: impl HasContainer) -> Option<Symbol> {
369    match t.container(db) {
370        hir::ItemContainer::Trait(it) => Some(it.name(db).symbol().clone()),
371        // FIXME: Handle owners of blocks correctly here
372        hir::ItemContainer::Module(it) => it.name(db).map(|name| name.symbol().clone()),
373        _ => None,
374    }
375}
376
377impl ToNavFromAst for hir::Function {
378    const KIND: SymbolKind = SymbolKind::Function;
379    fn container_name(self, db: &RootDatabase) -> Option<Symbol> {
380        container_name(db, self)
381    }
382}
383
384impl ToNavFromAst for hir::Const {
385    const KIND: SymbolKind = SymbolKind::Const;
386    fn container_name(self, db: &RootDatabase) -> Option<Symbol> {
387        container_name(db, self)
388    }
389}
390impl ToNavFromAst for hir::Static {
391    const KIND: SymbolKind = SymbolKind::Static;
392    fn container_name(self, db: &RootDatabase) -> Option<Symbol> {
393        container_name(db, self)
394    }
395}
396impl ToNavFromAst for hir::Struct {
397    const KIND: SymbolKind = SymbolKind::Struct;
398    fn container_name(self, db: &RootDatabase) -> Option<Symbol> {
399        container_name(db, self)
400    }
401}
402impl ToNavFromAst for hir::Enum {
403    const KIND: SymbolKind = SymbolKind::Enum;
404    fn container_name(self, db: &RootDatabase) -> Option<Symbol> {
405        container_name(db, self)
406    }
407}
408impl ToNavFromAst for hir::EnumVariant {
409    const KIND: SymbolKind = SymbolKind::Variant;
410}
411impl ToNavFromAst for hir::Union {
412    const KIND: SymbolKind = SymbolKind::Union;
413    fn container_name(self, db: &RootDatabase) -> Option<Symbol> {
414        container_name(db, self)
415    }
416}
417impl ToNavFromAst for hir::TypeAlias {
418    const KIND: SymbolKind = SymbolKind::TypeAlias;
419    fn container_name(self, db: &RootDatabase) -> Option<Symbol> {
420        container_name(db, self)
421    }
422}
423impl ToNavFromAst for hir::Trait {
424    const KIND: SymbolKind = SymbolKind::Trait;
425    fn container_name(self, db: &RootDatabase) -> Option<Symbol> {
426        container_name(db, self)
427    }
428}
429
430impl<D> TryToNav for D
431where
432    D: HasSource
433        + ToNavFromAst
434        + Copy
435        + HasDocs
436        + for<'db> HirDisplay<'db>
437        + HasCrate
438        + hir::HasName,
439    D::Ast: ast::HasName,
440{
441    fn try_to_nav(
442        &self,
443        sema: &Semantics<'_, RootDatabase>,
444    ) -> Option<UpmappingResult<NavigationTarget>> {
445        let db = sema.db;
446        let src = self.source_with_range(db)?;
447        Some(
448            NavigationTarget::from_named_with_range(
449                db,
450                src.map(|(full_range, node)| {
451                    (
452                        full_range,
453                        node.and_then(|node| {
454                            Some(ast::HasName::name(&node)?.syntax().text_range())
455                        }),
456                    )
457                }),
458                self.name(db),
459                D::KIND,
460            )
461            .map(|mut res| {
462                res.docs = self.docs(db).map(Documentation::into_owned);
463                res.description =
464                    Some(self.display(db, self.krate(db).to_display_target(db)).to_string());
465                res.container_name = self.container_name(db);
466                res
467            }),
468        )
469    }
470}
471
472impl ToNav for hir::Module {
473    fn to_nav(&self, db: &RootDatabase) -> UpmappingResult<NavigationTarget> {
474        let InFile { file_id, value } = self.definition_source(db);
475
476        let name = self.name(db).map(|it| it.symbol().clone()).unwrap_or_else(|| sym::underscore);
477        let (syntax, focus) = match &value {
478            ModuleSource::SourceFile(node) => (node.syntax(), None),
479            ModuleSource::Module(node) => (node.syntax(), node.name()),
480            ModuleSource::BlockExpr(node) => (node.syntax(), None),
481        };
482        let kind = if self.is_crate_root(db) { SymbolKind::CrateRoot } else { SymbolKind::Module };
483
484        orig_range_with_focus(db, file_id, syntax, focus).map(
485            |(FileRange { file_id, range: full_range }, focus_range)| {
486                NavigationTarget::from_syntax(file_id, name.clone(), focus_range, full_range, kind)
487            },
488        )
489    }
490}
491
492impl ToNav for hir::Crate {
493    fn to_nav(&self, db: &RootDatabase) -> UpmappingResult<NavigationTarget> {
494        self.root_module(db).to_nav(db)
495    }
496}
497
498impl TryToNav for hir::Impl {
499    fn try_to_nav(
500        &self,
501        sema: &Semantics<'_, RootDatabase>,
502    ) -> Option<UpmappingResult<NavigationTarget>> {
503        let db = sema.db;
504        let InFile { file_id, value: (full_range, source) } = self.source_with_range(db)?;
505
506        Some(
507            orig_range_with_focus_r(
508                db,
509                file_id,
510                full_range,
511                source.and_then(|source| Some(source.self_ty()?.syntax().text_range())),
512            )
513            .map(|(FileRange { file_id, range: full_range }, focus_range)| {
514                NavigationTarget::from_syntax(
515                    file_id,
516                    sym::kw_impl,
517                    focus_range,
518                    full_range,
519                    SymbolKind::Impl,
520                )
521            }),
522        )
523    }
524}
525
526impl TryToNav for hir::ExternCrateDecl {
527    fn try_to_nav(
528        &self,
529        sema: &Semantics<'_, RootDatabase>,
530    ) -> Option<UpmappingResult<NavigationTarget>> {
531        let db = sema.db;
532        let src = self.source(db)?;
533        let InFile { file_id, value } = src;
534        let focus = value
535            .rename()
536            .map_or_else(|| value.name_ref().map(Either::Left), |it| it.name().map(Either::Right));
537        let krate = self.module(db).krate(db);
538
539        Some(orig_range_with_focus(db, file_id, value.syntax(), focus).map(
540            |(FileRange { file_id, range: full_range }, focus_range)| {
541                let mut res = NavigationTarget::from_syntax(
542                    file_id,
543                    self.alias_or_name(db).unwrap_or_else(|| self.name(db)).symbol().clone(),
544                    focus_range,
545                    full_range,
546                    SymbolKind::CrateRoot,
547                );
548
549                res.docs = self.docs(db).map(Documentation::into_owned);
550                res.description = Some(self.display(db, krate.to_display_target(db)).to_string());
551                res.container_name = container_name(db, *self);
552                res
553            },
554        ))
555    }
556}
557
558impl TryToNav for hir::Field {
559    fn try_to_nav(
560        &self,
561        sema: &Semantics<'_, RootDatabase>,
562    ) -> Option<UpmappingResult<NavigationTarget>> {
563        let db = sema.db;
564        let src = self.source(db)?;
565        let krate = self.parent_def(db).module(db).krate(db);
566
567        let field_source = match &src.value {
568            FieldSource::Named(it) => {
569                NavigationTarget::from_named(db, src.with_value(it), SymbolKind::Field).map(
570                    |mut res| {
571                        res.docs = self.docs(db).map(Documentation::into_owned);
572                        res.description =
573                            Some(self.display(db, krate.to_display_target(db)).to_string());
574                        res
575                    },
576                )
577            }
578            FieldSource::Pos(it) => orig_range(db, src.file_id, it.syntax()).map(
579                |(FileRange { file_id, range: full_range }, focus_range)| {
580                    NavigationTarget::from_syntax(
581                        file_id,
582                        sym::Integer::get(self.index()),
583                        focus_range,
584                        full_range,
585                        SymbolKind::Field,
586                    )
587                },
588            ),
589        };
590        Some(field_source)
591    }
592}
593
594impl TryToNav for hir::Macro {
595    fn try_to_nav(
596        &self,
597        sema: &Semantics<'_, RootDatabase>,
598    ) -> Option<UpmappingResult<NavigationTarget>> {
599        let db = sema.db;
600        let src = self.source(db)?;
601        let name_owner: &dyn ast::HasName = match &src.value {
602            Either::Left(it) => it,
603            Either::Right(it) => it,
604        };
605        Some(
606            NavigationTarget::from_named(
607                db,
608                src.as_ref().with_value(name_owner),
609                self.kind(db).into(),
610            )
611            .map(|mut res| {
612                res.docs = self.docs(db).map(Documentation::into_owned);
613                res
614            }),
615        )
616    }
617}
618
619impl TryToNav for hir::Adt {
620    fn try_to_nav(
621        &self,
622        sema: &Semantics<'_, RootDatabase>,
623    ) -> Option<UpmappingResult<NavigationTarget>> {
624        match self {
625            hir::Adt::Struct(it) => it.try_to_nav(sema),
626            hir::Adt::Union(it) => it.try_to_nav(sema),
627            hir::Adt::Enum(it) => it.try_to_nav(sema),
628        }
629    }
630}
631
632impl TryToNav for hir::AssocItem {
633    fn try_to_nav(
634        &self,
635        sema: &Semantics<'_, RootDatabase>,
636    ) -> Option<UpmappingResult<NavigationTarget>> {
637        match self {
638            AssocItem::Function(it) => it.try_to_nav(sema),
639            AssocItem::Const(it) => it.try_to_nav(sema),
640            AssocItem::TypeAlias(it) => it.try_to_nav(sema),
641        }
642    }
643}
644
645impl TryToNav for hir::GenericParam {
646    fn try_to_nav(
647        &self,
648        sema: &Semantics<'_, RootDatabase>,
649    ) -> Option<UpmappingResult<NavigationTarget>> {
650        match self {
651            hir::GenericParam::TypeParam(it) => it.try_to_nav(sema),
652            hir::GenericParam::ConstParam(it) => it.try_to_nav(sema),
653            hir::GenericParam::LifetimeParam(it) => it.try_to_nav(sema),
654        }
655    }
656}
657
658impl ToNav for LocalSource {
659    fn to_nav(&self, db: &RootDatabase) -> UpmappingResult<NavigationTarget> {
660        let InFile { file_id, value } = &self.source;
661        let file_id = *file_id;
662        let local = self.local;
663        let (node, name) = match &value {
664            Either::Left(bind_pat) => (bind_pat.syntax(), bind_pat.name()),
665            Either::Right(it) => (it.syntax(), it.name()),
666        };
667
668        orig_range_with_focus(db, file_id, node, name).map(
669            |(FileRange { file_id, range: full_range }, focus_range)| {
670                let name = local.name(db).symbol().clone();
671                let kind = if local.is_self(db) {
672                    SymbolKind::SelfParam
673                } else if local.is_param(db) {
674                    SymbolKind::ValueParam
675                } else {
676                    SymbolKind::Local
677                };
678                NavigationTarget {
679                    file_id,
680                    name,
681                    alias: None,
682                    kind: Some(kind),
683                    full_range,
684                    focus_range,
685                    container_name: None,
686                    description: None,
687                    docs: None,
688                }
689            },
690        )
691    }
692}
693
694impl ToNav for hir::Local {
695    fn to_nav(&self, db: &RootDatabase) -> UpmappingResult<NavigationTarget> {
696        self.primary_source(db).to_nav(db)
697    }
698}
699
700impl TryToNav for hir::Label {
701    fn try_to_nav(
702        &self,
703        sema: &Semantics<'_, RootDatabase>,
704    ) -> Option<UpmappingResult<NavigationTarget>> {
705        let db = sema.db;
706        let InFile { file_id, value } = self.source(db)?;
707        let name = self.name(db).symbol().clone();
708
709        Some(orig_range_with_focus(db, file_id, value.syntax(), value.lifetime()).map(
710            |(FileRange { file_id, range: full_range }, focus_range)| NavigationTarget {
711                file_id,
712                name: name.clone(),
713                alias: None,
714                kind: Some(SymbolKind::Label),
715                full_range,
716                focus_range,
717                container_name: None,
718                description: None,
719                docs: None,
720            },
721        ))
722    }
723}
724
725impl TryToNav for hir::TypeParam {
726    fn try_to_nav(
727        &self,
728        sema: &Semantics<'_, RootDatabase>,
729    ) -> Option<UpmappingResult<NavigationTarget>> {
730        let db = sema.db;
731        let InFile { file_id, value } = self.merge().source(db)?;
732        let name = self.name(db).symbol().clone();
733
734        let value = match value {
735            Either::Left(ast::TypeOrConstParam::Type(x)) => Either::Left(x),
736            Either::Left(ast::TypeOrConstParam::Const(_)) => {
737                never!();
738                return None;
739            }
740            Either::Right(x) => Either::Right(x),
741        };
742
743        let syntax = match &value {
744            Either::Left(type_param) => type_param.syntax(),
745            Either::Right(trait_) => trait_.syntax(),
746        };
747        let focus = value.as_ref().either(|it| it.name(), |it| it.name());
748
749        Some(orig_range_with_focus(db, file_id, syntax, focus).map(
750            |(FileRange { file_id, range: full_range }, focus_range)| NavigationTarget {
751                file_id,
752                name: name.clone(),
753                alias: None,
754                kind: Some(SymbolKind::TypeParam),
755                full_range,
756                focus_range,
757                container_name: None,
758                description: None,
759                docs: None,
760            },
761        ))
762    }
763}
764
765impl TryToNav for hir::TypeOrConstParam {
766    fn try_to_nav(
767        &self,
768        sema: &Semantics<'_, RootDatabase>,
769    ) -> Option<UpmappingResult<NavigationTarget>> {
770        self.split(sema.db).try_to_nav(sema)
771    }
772}
773
774impl TryToNav for hir::LifetimeParam {
775    fn try_to_nav(
776        &self,
777        sema: &Semantics<'_, RootDatabase>,
778    ) -> Option<UpmappingResult<NavigationTarget>> {
779        let db = sema.db;
780        let InFile { file_id, value } = self.source(db)?;
781        let name = self.name(db).symbol().clone();
782
783        Some(orig_range(db, file_id, value.syntax()).map(
784            |(FileRange { file_id, range: full_range }, focus_range)| NavigationTarget {
785                file_id,
786                name: name.clone(),
787                alias: None,
788                kind: Some(SymbolKind::LifetimeParam),
789                full_range,
790                focus_range,
791                container_name: None,
792                description: None,
793                docs: None,
794            },
795        ))
796    }
797}
798
799impl TryToNav for hir::ConstParam {
800    fn try_to_nav(
801        &self,
802        sema: &Semantics<'_, RootDatabase>,
803    ) -> Option<UpmappingResult<NavigationTarget>> {
804        let db = sema.db;
805        let InFile { file_id, value } = self.merge().source(db)?;
806        let name = self.name(db).symbol().clone();
807
808        let value = match value {
809            Either::Left(ast::TypeOrConstParam::Const(x)) => x,
810            _ => {
811                never!();
812                return None;
813            }
814        };
815
816        Some(orig_range_with_focus(db, file_id, value.syntax(), value.name()).map(
817            |(FileRange { file_id, range: full_range }, focus_range)| NavigationTarget {
818                file_id,
819                name: name.clone(),
820                alias: None,
821                kind: Some(SymbolKind::ConstParam),
822                full_range,
823                focus_range,
824                container_name: None,
825                description: None,
826                docs: None,
827            },
828        ))
829    }
830}
831
832impl TryToNav for hir::InlineAsmOperand {
833    fn try_to_nav(
834        &self,
835        sema: &Semantics<'_, RootDatabase>,
836    ) -> Option<UpmappingResult<NavigationTarget>> {
837        let db = sema.db;
838        let InFile { file_id, value } = &self.source(db)?;
839        let file_id = *file_id;
840        Some(orig_range_with_focus(db, file_id, value.syntax(), value.name()).map(
841            |(FileRange { file_id, range: full_range }, focus_range)| NavigationTarget {
842                file_id,
843                name:
844                    self.name(db).map_or_else(|| sym::underscore.clone(), |it| it.symbol().clone()),
845                alias: None,
846                kind: Some(SymbolKind::Local),
847                full_range,
848                focus_range,
849                container_name: None,
850                description: None,
851                docs: None,
852            },
853        ))
854    }
855}
856
857impl TryToNav for hir::BuiltinType {
858    fn try_to_nav(
859        &self,
860        sema: &Semantics<'_, RootDatabase>,
861    ) -> Option<UpmappingResult<NavigationTarget>> {
862        let db = sema.db;
863        let krate = all_crates(db)
864            .iter()
865            .copied()
866            .find(|&krate| matches!(krate.data(db).origin, CrateOrigin::Lang(LangCrateOrigin::Std)))
867            .map(Crate::from)?;
868        let edition = krate.edition(db);
869
870        let fd = FamousDefs(sema, krate);
871        let primitive_mod = format!("prim_{}", self.name().display(fd.0.db, edition));
872        let doc_owner = find_std_module(&fd, &primitive_mod, edition)?;
873
874        Some(doc_owner.to_nav(db))
875    }
876}
877
878#[derive(Debug)]
879pub struct UpmappingResult<T> {
880    /// The macro call site.
881    pub call_site: T,
882    /// The macro definition site, if relevant.
883    pub def_site: Option<T>,
884}
885
886impl<T> UpmappingResult<T> {
887    pub fn call_site(self) -> T {
888        self.call_site
889    }
890
891    pub fn collect<FI: FromIterator<T>>(self) -> FI {
892        FI::from_iter(self)
893    }
894}
895
896impl<T> IntoIterator for UpmappingResult<T> {
897    type Item = T;
898
899    type IntoIter = <ArrayVec<T, 2> as IntoIterator>::IntoIter;
900
901    fn into_iter(self) -> Self::IntoIter {
902        self.def_site
903            .into_iter()
904            .chain(Some(self.call_site))
905            .collect::<ArrayVec<_, 2>>()
906            .into_iter()
907    }
908}
909
910impl<T> UpmappingResult<T> {
911    pub(crate) fn map<U>(self, f: impl Fn(T) -> U) -> UpmappingResult<U> {
912        UpmappingResult { call_site: f(self.call_site), def_site: self.def_site.map(f) }
913    }
914}
915
916/// Returns the original range of the syntax node, and the range of the name mapped out of macro expansions
917/// May return two results if the mapped node originates from a macro definition in which case the
918/// second result is the creating macro call.
919fn orig_range_with_focus(
920    db: &RootDatabase,
921    hir_file: HirFileId,
922    value: &SyntaxNode,
923    name: Option<impl AstNode>,
924) -> UpmappingResult<(FileRange, Option<TextRange>)> {
925    orig_range_with_focus_r(
926        db,
927        hir_file,
928        value.text_range(),
929        name.map(|it| it.syntax().text_range()),
930    )
931}
932
933pub(crate) fn orig_range_with_focus_r(
934    db: &RootDatabase,
935    hir_file: HirFileId,
936    value: TextRange,
937    focus_range: Option<TextRange>,
938) -> UpmappingResult<(FileRange, Option<TextRange>)> {
939    let Some(name) = focus_range else { return orig_range_r(db, hir_file, value) };
940
941    let call = || hir_file.macro_file().unwrap().loc(db);
942
943    let def_range = || hir_file.macro_file().unwrap().loc(db).def.definition_range(db);
944
945    // FIXME: Also make use of the syntax context to determine which site we are at?
946    let value_range = InFile::new(hir_file, value).original_node_file_range_opt(db);
947    let ((call_site_range, call_site_focus), def_site) =
948        match InFile::new(hir_file, name).original_node_file_range_opt(db) {
949            // call site name
950            Some((focus_range, ctxt)) if ctxt.is_root() => {
951                // Try to upmap the node as well, if it ends up in the def site, go back to the call site
952                (
953                    (
954                        match value_range {
955                            // name is in the node in the macro input so we can return it
956                            Some((range, ctxt))
957                                if ctxt.is_root()
958                                    && range.file_id == focus_range.file_id
959                                    && range.range.contains_range(focus_range.range) =>
960                            {
961                                range
962                            }
963                            // name lies outside the node, so instead point to the macro call which
964                            // *should* contain the name
965                            _ => {
966                                let call = call();
967                                let kind = &call.kind;
968                                let range = kind.clone().original_call_range_with_input(db);
969                                //If the focus range is in the attribute/derive body, we
970                                // need to point the call site to the entire body, if not, fall back
971                                // to the name range of the attribute/derive call
972                                // FIXME: Do this differently, this is very inflexible the caller
973                                // should choose this behavior
974                                if range.file_id == focus_range.file_id
975                                    && range.range.contains_range(focus_range.range)
976                                {
977                                    range
978                                } else {
979                                    kind.original_call_range(db, call.krate)
980                                }
981                            }
982                        },
983                        Some(focus_range),
984                    ),
985                    // no def site relevant
986                    None,
987                )
988            }
989
990            // def site name
991            // FIXME: This can be improved
992            Some((focus_range, _ctxt)) => {
993                match value_range {
994                    // but overall node is in macro input
995                    Some((range, ctxt)) if ctxt.is_root() => (
996                        // node mapped up in call site, show the node
997                        (range, None),
998                        // def site, if the name is in the (possibly) upmapped def site range, show the
999                        // def site
1000                        {
1001                            let (def_site, _) = def_range().original_node_file_range(db);
1002                            (def_site.file_id == focus_range.file_id
1003                                && def_site.range.contains_range(focus_range.range))
1004                            .then_some((def_site, Some(focus_range)))
1005                        },
1006                    ),
1007                    // node is in macro def, just show the focus
1008                    _ => {
1009                        let call = call();
1010                        (
1011                            // show the macro call
1012                            (call.kind.original_call_range(db, call.krate), None),
1013                            Some((focus_range, Some(focus_range))),
1014                        )
1015                    }
1016                }
1017            }
1018            // lost name? can't happen for single tokens
1019            None => return orig_range_r(db, hir_file, value),
1020        };
1021
1022    UpmappingResult {
1023        call_site: (
1024            call_site_range.into_file_id(db),
1025            call_site_focus.and_then(|hir::FileRange { file_id, range }| {
1026                if call_site_range.file_id == file_id && call_site_range.range.contains_range(range)
1027                {
1028                    Some(range)
1029                } else {
1030                    None
1031                }
1032            }),
1033        ),
1034        def_site: def_site.map(|(def_site_range, def_site_focus)| {
1035            (
1036                def_site_range.into_file_id(db),
1037                def_site_focus.and_then(|hir::FileRange { file_id, range }| {
1038                    if def_site_range.file_id == file_id
1039                        && def_site_range.range.contains_range(range)
1040                    {
1041                        Some(range)
1042                    } else {
1043                        None
1044                    }
1045                }),
1046            )
1047        }),
1048    }
1049}
1050
1051fn orig_range(
1052    db: &RootDatabase,
1053    hir_file: HirFileId,
1054    value: &SyntaxNode,
1055) -> UpmappingResult<(FileRange, Option<TextRange>)> {
1056    UpmappingResult {
1057        call_site: (
1058            InFile::new(hir_file, value).original_file_range_rooted(db).into_file_id(db),
1059            None,
1060        ),
1061        def_site: None,
1062    }
1063}
1064
1065fn orig_range_r(
1066    db: &RootDatabase,
1067    hir_file: HirFileId,
1068    value: TextRange,
1069) -> UpmappingResult<(FileRange, Option<TextRange>)> {
1070    UpmappingResult {
1071        call_site: (
1072            InFile::new(hir_file, value).original_node_file_range(db).0.into_file_id(db),
1073            None,
1074        ),
1075        def_site: None,
1076    }
1077}
1078
1079#[cfg(test)]
1080mod tests {
1081    use expect_test::expect;
1082
1083    use crate::{Query, fixture};
1084
1085    #[test]
1086    fn test_nav_for_symbol() {
1087        let (analysis, _) = fixture::file(
1088            r#"
1089enum FooInner { }
1090fn foo() { enum FooInner { } }
1091"#,
1092        );
1093
1094        let navs = analysis.symbol_search(Query::new("FooInner".to_owned()), !0).unwrap();
1095        expect![[r#"
1096            [
1097                NavigationTarget {
1098                    file_id: FileId(
1099                        0,
1100                    ),
1101                    full_range: 0..17,
1102                    focus_range: 5..13,
1103                    name: "FooInner",
1104                    kind: Enum,
1105                    description: "enum FooInner",
1106                },
1107                NavigationTarget {
1108                    file_id: FileId(
1109                        0,
1110                    ),
1111                    full_range: 29..46,
1112                    focus_range: 34..42,
1113                    name: "FooInner",
1114                    kind: Enum,
1115                    container_name: "foo",
1116                    description: "enum FooInner",
1117                },
1118            ]
1119        "#]]
1120        .assert_debug_eq(&navs);
1121    }
1122
1123    #[test]
1124    fn test_world_symbols_are_case_sensitive() {
1125        let (analysis, _) = fixture::file(
1126            r#"
1127fn foo() {}
1128struct Foo;
1129"#,
1130        );
1131
1132        let navs = analysis.symbol_search(Query::new("foo".to_owned()), !0).unwrap();
1133        assert_eq!(navs.len(), 2)
1134    }
1135
1136    #[test]
1137    fn test_ensure_hidden_symbols_are_not_returned() {
1138        let (analysis, _) = fixture::file(
1139            r#"
1140fn foo() {}
1141struct Foo;
1142static __FOO_CALLSITE: () = ();
1143"#,
1144        );
1145
1146        // It doesn't show the hidden symbol
1147        let navs = analysis.symbol_search(Query::new("foo".to_owned()), !0).unwrap();
1148        assert_eq!(navs.len(), 2);
1149        let navs = analysis.symbol_search(Query::new("_foo".to_owned()), !0).unwrap();
1150        assert_eq!(navs.len(), 0);
1151
1152        // Unless we explicitly search for a `__` prefix
1153        let query = Query::new("__foo".to_owned());
1154        let navs = analysis.symbol_search(query, !0).unwrap();
1155        assert_eq!(navs.len(), 1);
1156    }
1157}