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