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, 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, 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_syntax(
208        file_id: FileId,
209        name: Symbol,
210        focus_range: Option<TextRange>,
211        full_range: TextRange,
212        kind: SymbolKind,
213    ) -> NavigationTarget {
214        NavigationTarget {
215            file_id,
216            name,
217            kind: Some(kind),
218            full_range,
219            focus_range,
220            container_name: None,
221            description: None,
222            docs: None,
223            alias: None,
224        }
225    }
226}
227
228impl<'db> TryToNav for FileSymbol<'db> {
229    fn try_to_nav(
230        &self,
231        sema: &Semantics<'_, RootDatabase>,
232    ) -> Option<UpmappingResult<NavigationTarget>> {
233        let db = sema.db;
234        let display_target = self.def.krate(db).to_display_target(db);
235        Some(
236            orig_range_with_focus_r(
237                db,
238                self.loc.hir_file_id,
239                self.loc.ptr.text_range(),
240                Some(self.loc.name_ptr.text_range()),
241            )
242            .map(|(FileRange { file_id, range: full_range }, focus_range)| {
243                NavigationTarget {
244                    file_id,
245                    name: self
246                        .is_alias
247                        .then(|| self.def.name(db))
248                        .flatten()
249                        .map_or_else(|| self.name.clone(), |it| it.symbol().clone()),
250                    alias: self.is_alias.then(|| self.name.clone()),
251                    kind: Some(self.def.into()),
252                    full_range,
253                    focus_range,
254                    container_name: self.container_name.clone(),
255                    description: match self.def {
256                        hir::ModuleDef::Module(it) => {
257                            Some(it.display(db, display_target).to_string())
258                        }
259                        hir::ModuleDef::Function(it) => {
260                            Some(it.display(db, display_target).to_string())
261                        }
262                        hir::ModuleDef::Adt(it) => Some(it.display(db, display_target).to_string()),
263                        hir::ModuleDef::Variant(it) => {
264                            Some(it.display(db, display_target).to_string())
265                        }
266                        hir::ModuleDef::Const(it) => {
267                            Some(it.display(db, display_target).to_string())
268                        }
269                        hir::ModuleDef::Static(it) => {
270                            Some(it.display(db, display_target).to_string())
271                        }
272                        hir::ModuleDef::Trait(it) => {
273                            Some(it.display(db, display_target).to_string())
274                        }
275                        hir::ModuleDef::TypeAlias(it) => {
276                            Some(it.display(db, display_target).to_string())
277                        }
278                        hir::ModuleDef::Macro(it) => {
279                            Some(it.display(db, display_target).to_string())
280                        }
281                        hir::ModuleDef::BuiltinType(_) => None,
282                    },
283                    docs: None,
284                }
285            }),
286        )
287    }
288}
289
290impl TryToNav for Definition {
291    fn try_to_nav(
292        &self,
293        sema: &Semantics<'_, RootDatabase>,
294    ) -> Option<UpmappingResult<NavigationTarget>> {
295        match self {
296            Definition::Local(it) => Some(it.to_nav(sema.db)),
297            Definition::Label(it) => it.try_to_nav(sema),
298            Definition::Module(it) => Some(it.to_nav(sema.db)),
299            Definition::Crate(it) => Some(it.to_nav(sema.db)),
300            Definition::Macro(it) => it.try_to_nav(sema),
301            Definition::Field(it) => it.try_to_nav(sema),
302            Definition::SelfType(it) => it.try_to_nav(sema),
303            Definition::GenericParam(it) => it.try_to_nav(sema),
304            Definition::Function(it) => it.try_to_nav(sema),
305            Definition::Adt(it) => it.try_to_nav(sema),
306            Definition::Variant(it) => it.try_to_nav(sema),
307            Definition::Const(it) => it.try_to_nav(sema),
308            Definition::Static(it) => it.try_to_nav(sema),
309            Definition::Trait(it) => it.try_to_nav(sema),
310            Definition::TypeAlias(it) => it.try_to_nav(sema),
311            Definition::ExternCrateDecl(it) => it.try_to_nav(sema),
312            Definition::InlineAsmOperand(it) => it.try_to_nav(sema),
313            Definition::BuiltinType(it) => it.try_to_nav(sema),
314            Definition::BuiltinLifetime(_)
315            | Definition::TupleField(_)
316            | Definition::ToolModule(_)
317            | Definition::InlineAsmRegOrRegClass(_)
318            | Definition::BuiltinAttr(_) => None,
319            // FIXME: The focus range should be set to the helper declaration
320            Definition::DeriveHelper(it) => it.derive().try_to_nav(sema),
321        }
322    }
323}
324
325impl TryToNav for hir::ModuleDef {
326    fn try_to_nav(
327        &self,
328        sema: &Semantics<'_, RootDatabase>,
329    ) -> Option<UpmappingResult<NavigationTarget>> {
330        match self {
331            hir::ModuleDef::Module(it) => Some(it.to_nav(sema.db)),
332            hir::ModuleDef::Function(it) => it.try_to_nav(sema),
333            hir::ModuleDef::Adt(it) => it.try_to_nav(sema),
334            hir::ModuleDef::Variant(it) => it.try_to_nav(sema),
335            hir::ModuleDef::Const(it) => it.try_to_nav(sema),
336            hir::ModuleDef::Static(it) => it.try_to_nav(sema),
337            hir::ModuleDef::Trait(it) => it.try_to_nav(sema),
338            hir::ModuleDef::TypeAlias(it) => it.try_to_nav(sema),
339            hir::ModuleDef::Macro(it) => it.try_to_nav(sema),
340            hir::ModuleDef::BuiltinType(_) => None,
341        }
342    }
343}
344
345pub(crate) trait ToNavFromAst: Sized {
346    const KIND: SymbolKind;
347    fn container_name(self, db: &RootDatabase) -> Option<Symbol> {
348        _ = db;
349        None
350    }
351}
352
353fn container_name(db: &RootDatabase, t: impl HasContainer) -> Option<Symbol> {
354    match t.container(db) {
355        hir::ItemContainer::Trait(it) => Some(it.name(db).symbol().clone()),
356        // FIXME: Handle owners of blocks correctly here
357        hir::ItemContainer::Module(it) => it.name(db).map(|name| name.symbol().clone()),
358        _ => None,
359    }
360}
361
362impl ToNavFromAst for hir::Function {
363    const KIND: SymbolKind = SymbolKind::Function;
364    fn container_name(self, db: &RootDatabase) -> Option<Symbol> {
365        container_name(db, self)
366    }
367}
368
369impl ToNavFromAst for hir::Const {
370    const KIND: SymbolKind = SymbolKind::Const;
371    fn container_name(self, db: &RootDatabase) -> Option<Symbol> {
372        container_name(db, self)
373    }
374}
375impl ToNavFromAst for hir::Static {
376    const KIND: SymbolKind = SymbolKind::Static;
377    fn container_name(self, db: &RootDatabase) -> Option<Symbol> {
378        container_name(db, self)
379    }
380}
381impl ToNavFromAst for hir::Struct {
382    const KIND: SymbolKind = SymbolKind::Struct;
383    fn container_name(self, db: &RootDatabase) -> Option<Symbol> {
384        container_name(db, self)
385    }
386}
387impl ToNavFromAst for hir::Enum {
388    const KIND: SymbolKind = SymbolKind::Enum;
389    fn container_name(self, db: &RootDatabase) -> Option<Symbol> {
390        container_name(db, self)
391    }
392}
393impl ToNavFromAst for hir::Variant {
394    const KIND: SymbolKind = SymbolKind::Variant;
395}
396impl ToNavFromAst for hir::Union {
397    const KIND: SymbolKind = SymbolKind::Union;
398    fn container_name(self, db: &RootDatabase) -> Option<Symbol> {
399        container_name(db, self)
400    }
401}
402impl ToNavFromAst for hir::TypeAlias {
403    const KIND: SymbolKind = SymbolKind::TypeAlias;
404    fn container_name(self, db: &RootDatabase) -> Option<Symbol> {
405        container_name(db, self)
406    }
407}
408impl ToNavFromAst for hir::Trait {
409    const KIND: SymbolKind = SymbolKind::Trait;
410    fn container_name(self, db: &RootDatabase) -> Option<Symbol> {
411        container_name(db, self)
412    }
413}
414
415impl<D> TryToNav for D
416where
417    D: HasSource + ToNavFromAst + Copy + HasDocs + for<'db> HirDisplay<'db> + HasCrate,
418    D::Ast: ast::HasName,
419{
420    fn try_to_nav(
421        &self,
422        sema: &Semantics<'_, RootDatabase>,
423    ) -> Option<UpmappingResult<NavigationTarget>> {
424        let db = sema.db;
425        let src = self.source(db)?;
426        Some(
427            NavigationTarget::from_named(
428                db,
429                src.as_ref().map(|it| it as &dyn ast::HasName),
430                D::KIND,
431            )
432            .map(|mut res| {
433                res.docs = self.docs(db).map(Documentation::into_owned);
434                res.description = hir::attach_db(db, || {
435                    Some(self.display(db, self.krate(db).to_display_target(db)).to_string())
436                });
437                res.container_name = self.container_name(db);
438                res
439            }),
440        )
441    }
442}
443
444impl ToNav for hir::Module {
445    fn to_nav(&self, db: &RootDatabase) -> UpmappingResult<NavigationTarget> {
446        let InFile { file_id, value } = self.definition_source(db);
447
448        let name = self.name(db).map(|it| it.symbol().clone()).unwrap_or_else(|| sym::underscore);
449        let (syntax, focus) = match &value {
450            ModuleSource::SourceFile(node) => (node.syntax(), None),
451            ModuleSource::Module(node) => (node.syntax(), node.name()),
452            ModuleSource::BlockExpr(node) => (node.syntax(), None),
453        };
454
455        orig_range_with_focus(db, file_id, syntax, focus).map(
456            |(FileRange { file_id, range: full_range }, focus_range)| {
457                NavigationTarget::from_syntax(
458                    file_id,
459                    name.clone(),
460                    focus_range,
461                    full_range,
462                    SymbolKind::Module,
463                )
464            },
465        )
466    }
467}
468
469impl ToNav for hir::Crate {
470    fn to_nav(&self, db: &RootDatabase) -> UpmappingResult<NavigationTarget> {
471        self.root_module(db).to_nav(db)
472    }
473}
474
475impl TryToNav for hir::Impl {
476    fn try_to_nav(
477        &self,
478        sema: &Semantics<'_, RootDatabase>,
479    ) -> Option<UpmappingResult<NavigationTarget>> {
480        let db = sema.db;
481        let InFile { file_id, value } = self.source(db)?;
482        let derive_path = self.as_builtin_derive_path(db);
483
484        let (file_id, focus, syntax) = match &derive_path {
485            Some(attr) => (attr.file_id.into(), None, attr.value.syntax()),
486            None => (file_id, value.self_ty(), value.syntax()),
487        };
488
489        Some(orig_range_with_focus(db, file_id, syntax, focus).map(
490            |(FileRange { file_id, range: full_range }, focus_range)| {
491                NavigationTarget::from_syntax(
492                    file_id,
493                    sym::kw_impl,
494                    focus_range,
495                    full_range,
496                    SymbolKind::Impl,
497                )
498            },
499        ))
500    }
501}
502
503impl TryToNav for hir::ExternCrateDecl {
504    fn try_to_nav(
505        &self,
506        sema: &Semantics<'_, RootDatabase>,
507    ) -> Option<UpmappingResult<NavigationTarget>> {
508        let db = sema.db;
509        let src = self.source(db)?;
510        let InFile { file_id, value } = src;
511        let focus = value
512            .rename()
513            .map_or_else(|| value.name_ref().map(Either::Left), |it| it.name().map(Either::Right));
514        let krate = self.module(db).krate(db);
515
516        Some(orig_range_with_focus(db, file_id, value.syntax(), focus).map(
517            |(FileRange { file_id, range: full_range }, focus_range)| {
518                let mut res = NavigationTarget::from_syntax(
519                    file_id,
520                    self.alias_or_name(db).unwrap_or_else(|| self.name(db)).symbol().clone(),
521                    focus_range,
522                    full_range,
523                    SymbolKind::Module,
524                );
525
526                res.docs = self.docs(db).map(Documentation::into_owned);
527                res.description = Some(self.display(db, krate.to_display_target(db)).to_string());
528                res.container_name = container_name(db, *self);
529                res
530            },
531        ))
532    }
533}
534
535impl TryToNav for hir::Field {
536    fn try_to_nav(
537        &self,
538        sema: &Semantics<'_, RootDatabase>,
539    ) -> Option<UpmappingResult<NavigationTarget>> {
540        let db = sema.db;
541        let src = self.source(db)?;
542        let krate = self.parent_def(db).module(db).krate(db);
543
544        let field_source = match &src.value {
545            FieldSource::Named(it) => {
546                NavigationTarget::from_named(db, src.with_value(it), SymbolKind::Field).map(
547                    |mut res| {
548                        res.docs = self.docs(db).map(Documentation::into_owned);
549                        res.description =
550                            Some(self.display(db, krate.to_display_target(db)).to_string());
551                        res
552                    },
553                )
554            }
555            FieldSource::Pos(it) => orig_range(db, src.file_id, it.syntax()).map(
556                |(FileRange { file_id, range: full_range }, focus_range)| {
557                    NavigationTarget::from_syntax(
558                        file_id,
559                        Symbol::integer(self.index()),
560                        focus_range,
561                        full_range,
562                        SymbolKind::Field,
563                    )
564                },
565            ),
566        };
567        Some(field_source)
568    }
569}
570
571impl TryToNav for hir::Macro {
572    fn try_to_nav(
573        &self,
574        sema: &Semantics<'_, RootDatabase>,
575    ) -> Option<UpmappingResult<NavigationTarget>> {
576        let db = sema.db;
577        let src = self.source(db)?;
578        let name_owner: &dyn ast::HasName = match &src.value {
579            Either::Left(it) => it,
580            Either::Right(it) => it,
581        };
582        Some(
583            NavigationTarget::from_named(
584                db,
585                src.as_ref().with_value(name_owner),
586                self.kind(db).into(),
587            )
588            .map(|mut res| {
589                res.docs = self.docs(db).map(Documentation::into_owned);
590                res
591            }),
592        )
593    }
594}
595
596impl TryToNav for hir::Adt {
597    fn try_to_nav(
598        &self,
599        sema: &Semantics<'_, RootDatabase>,
600    ) -> Option<UpmappingResult<NavigationTarget>> {
601        match self {
602            hir::Adt::Struct(it) => it.try_to_nav(sema),
603            hir::Adt::Union(it) => it.try_to_nav(sema),
604            hir::Adt::Enum(it) => it.try_to_nav(sema),
605        }
606    }
607}
608
609impl TryToNav for hir::AssocItem {
610    fn try_to_nav(
611        &self,
612        sema: &Semantics<'_, RootDatabase>,
613    ) -> Option<UpmappingResult<NavigationTarget>> {
614        match self {
615            AssocItem::Function(it) => it.try_to_nav(sema),
616            AssocItem::Const(it) => it.try_to_nav(sema),
617            AssocItem::TypeAlias(it) => it.try_to_nav(sema),
618        }
619    }
620}
621
622impl TryToNav for hir::GenericParam {
623    fn try_to_nav(
624        &self,
625        sema: &Semantics<'_, RootDatabase>,
626    ) -> Option<UpmappingResult<NavigationTarget>> {
627        match self {
628            hir::GenericParam::TypeParam(it) => it.try_to_nav(sema),
629            hir::GenericParam::ConstParam(it) => it.try_to_nav(sema),
630            hir::GenericParam::LifetimeParam(it) => it.try_to_nav(sema),
631        }
632    }
633}
634
635impl ToNav for LocalSource {
636    fn to_nav(&self, db: &RootDatabase) -> UpmappingResult<NavigationTarget> {
637        let InFile { file_id, value } = &self.source;
638        let file_id = *file_id;
639        let local = self.local;
640        let (node, name) = match &value {
641            Either::Left(bind_pat) => (bind_pat.syntax(), bind_pat.name()),
642            Either::Right(it) => (it.syntax(), it.name()),
643        };
644
645        orig_range_with_focus(db, file_id, node, name).map(
646            |(FileRange { file_id, range: full_range }, focus_range)| {
647                let name = local.name(db).symbol().clone();
648                let kind = if local.is_self(db) {
649                    SymbolKind::SelfParam
650                } else if local.is_param(db) {
651                    SymbolKind::ValueParam
652                } else {
653                    SymbolKind::Local
654                };
655                NavigationTarget {
656                    file_id,
657                    name,
658                    alias: None,
659                    kind: Some(kind),
660                    full_range,
661                    focus_range,
662                    container_name: None,
663                    description: None,
664                    docs: None,
665                }
666            },
667        )
668    }
669}
670
671impl ToNav for hir::Local {
672    fn to_nav(&self, db: &RootDatabase) -> UpmappingResult<NavigationTarget> {
673        self.primary_source(db).to_nav(db)
674    }
675}
676
677impl TryToNav for hir::Label {
678    fn try_to_nav(
679        &self,
680        sema: &Semantics<'_, RootDatabase>,
681    ) -> Option<UpmappingResult<NavigationTarget>> {
682        let db = sema.db;
683        let InFile { file_id, value } = self.source(db)?;
684        let name = self.name(db).symbol().clone();
685
686        Some(orig_range_with_focus(db, file_id, value.syntax(), value.lifetime()).map(
687            |(FileRange { file_id, range: full_range }, focus_range)| NavigationTarget {
688                file_id,
689                name: name.clone(),
690                alias: None,
691                kind: Some(SymbolKind::Label),
692                full_range,
693                focus_range,
694                container_name: None,
695                description: None,
696                docs: None,
697            },
698        ))
699    }
700}
701
702impl TryToNav for hir::TypeParam {
703    fn try_to_nav(
704        &self,
705        sema: &Semantics<'_, RootDatabase>,
706    ) -> Option<UpmappingResult<NavigationTarget>> {
707        let db = sema.db;
708        let InFile { file_id, value } = self.merge().source(db)?;
709        let name = self.name(db).symbol().clone();
710
711        let value = match value {
712            Either::Left(ast::TypeOrConstParam::Type(x)) => Either::Left(x),
713            Either::Left(ast::TypeOrConstParam::Const(_)) => {
714                never!();
715                return None;
716            }
717            Either::Right(x) => Either::Right(x),
718        };
719
720        let syntax = match &value {
721            Either::Left(type_param) => type_param.syntax(),
722            Either::Right(trait_) => trait_.syntax(),
723        };
724        let focus = value.as_ref().either(|it| it.name(), |it| it.name());
725
726        Some(orig_range_with_focus(db, file_id, syntax, focus).map(
727            |(FileRange { file_id, range: full_range }, focus_range)| NavigationTarget {
728                file_id,
729                name: name.clone(),
730                alias: None,
731                kind: Some(SymbolKind::TypeParam),
732                full_range,
733                focus_range,
734                container_name: None,
735                description: None,
736                docs: None,
737            },
738        ))
739    }
740}
741
742impl TryToNav for hir::TypeOrConstParam {
743    fn try_to_nav(
744        &self,
745        sema: &Semantics<'_, RootDatabase>,
746    ) -> Option<UpmappingResult<NavigationTarget>> {
747        self.split(sema.db).try_to_nav(sema)
748    }
749}
750
751impl TryToNav for hir::LifetimeParam {
752    fn try_to_nav(
753        &self,
754        sema: &Semantics<'_, RootDatabase>,
755    ) -> Option<UpmappingResult<NavigationTarget>> {
756        let db = sema.db;
757        let InFile { file_id, value } = self.source(db)?;
758        let name = self.name(db).symbol().clone();
759
760        Some(orig_range(db, file_id, value.syntax()).map(
761            |(FileRange { file_id, range: full_range }, focus_range)| NavigationTarget {
762                file_id,
763                name: name.clone(),
764                alias: None,
765                kind: Some(SymbolKind::LifetimeParam),
766                full_range,
767                focus_range,
768                container_name: None,
769                description: None,
770                docs: None,
771            },
772        ))
773    }
774}
775
776impl TryToNav for hir::ConstParam {
777    fn try_to_nav(
778        &self,
779        sema: &Semantics<'_, RootDatabase>,
780    ) -> Option<UpmappingResult<NavigationTarget>> {
781        let db = sema.db;
782        let InFile { file_id, value } = self.merge().source(db)?;
783        let name = self.name(db).symbol().clone();
784
785        let value = match value {
786            Either::Left(ast::TypeOrConstParam::Const(x)) => x,
787            _ => {
788                never!();
789                return None;
790            }
791        };
792
793        Some(orig_range_with_focus(db, file_id, value.syntax(), value.name()).map(
794            |(FileRange { file_id, range: full_range }, focus_range)| NavigationTarget {
795                file_id,
796                name: name.clone(),
797                alias: None,
798                kind: Some(SymbolKind::ConstParam),
799                full_range,
800                focus_range,
801                container_name: None,
802                description: None,
803                docs: None,
804            },
805        ))
806    }
807}
808
809impl TryToNav for hir::InlineAsmOperand {
810    fn try_to_nav(
811        &self,
812        sema: &Semantics<'_, RootDatabase>,
813    ) -> Option<UpmappingResult<NavigationTarget>> {
814        let db = sema.db;
815        let InFile { file_id, value } = &self.source(db)?;
816        let file_id = *file_id;
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:
821                    self.name(db).map_or_else(|| sym::underscore.clone(), |it| it.symbol().clone()),
822                alias: None,
823                kind: Some(SymbolKind::Local),
824                full_range,
825                focus_range,
826                container_name: None,
827                description: None,
828                docs: None,
829            },
830        ))
831    }
832}
833
834impl TryToNav for hir::BuiltinType {
835    fn try_to_nav(
836        &self,
837        sema: &Semantics<'_, RootDatabase>,
838    ) -> Option<UpmappingResult<NavigationTarget>> {
839        let db = sema.db;
840        let krate = db
841            .all_crates()
842            .iter()
843            .copied()
844            .find(|&krate| matches!(krate.data(db).origin, CrateOrigin::Lang(LangCrateOrigin::Std)))
845            .map(Crate::from)?;
846        let edition = krate.edition(db);
847
848        let fd = FamousDefs(sema, krate);
849        let primitive_mod = format!("prim_{}", self.name().display(fd.0.db, edition));
850        let doc_owner = find_std_module(&fd, &primitive_mod, edition)?;
851
852        Some(doc_owner.to_nav(db))
853    }
854}
855
856#[derive(Debug)]
857pub struct UpmappingResult<T> {
858    /// The macro call site.
859    pub call_site: T,
860    /// The macro definition site, if relevant.
861    pub def_site: Option<T>,
862}
863
864impl<T> UpmappingResult<T> {
865    pub fn call_site(self) -> T {
866        self.call_site
867    }
868
869    pub fn collect<FI: FromIterator<T>>(self) -> FI {
870        FI::from_iter(self)
871    }
872}
873
874impl<T> IntoIterator for UpmappingResult<T> {
875    type Item = T;
876
877    type IntoIter = <ArrayVec<T, 2> as IntoIterator>::IntoIter;
878
879    fn into_iter(self) -> Self::IntoIter {
880        self.def_site
881            .into_iter()
882            .chain(Some(self.call_site))
883            .collect::<ArrayVec<_, 2>>()
884            .into_iter()
885    }
886}
887
888impl<T> UpmappingResult<T> {
889    pub(crate) fn map<U>(self, f: impl Fn(T) -> U) -> UpmappingResult<U> {
890        UpmappingResult { call_site: f(self.call_site), def_site: self.def_site.map(f) }
891    }
892}
893
894/// Returns the original range of the syntax node, and the range of the name mapped out of macro expansions
895/// May return two results if the mapped node originates from a macro definition in which case the
896/// second result is the creating macro call.
897fn orig_range_with_focus(
898    db: &RootDatabase,
899    hir_file: HirFileId,
900    value: &SyntaxNode,
901    name: Option<impl AstNode>,
902) -> UpmappingResult<(FileRange, Option<TextRange>)> {
903    orig_range_with_focus_r(
904        db,
905        hir_file,
906        value.text_range(),
907        name.map(|it| it.syntax().text_range()),
908    )
909}
910
911pub(crate) fn orig_range_with_focus_r(
912    db: &RootDatabase,
913    hir_file: HirFileId,
914    value: TextRange,
915    focus_range: Option<TextRange>,
916) -> UpmappingResult<(FileRange, Option<TextRange>)> {
917    let Some(name) = focus_range else { return orig_range_r(db, hir_file, value) };
918
919    let call = || db.lookup_intern_macro_call(hir_file.macro_file().unwrap());
920
921    let def_range =
922        || db.lookup_intern_macro_call(hir_file.macro_file().unwrap()).def.definition_range(db);
923
924    // FIXME: Also make use of the syntax context to determine which site we are at?
925    let value_range = InFile::new(hir_file, value).original_node_file_range_opt(db);
926    let ((call_site_range, call_site_focus), def_site) =
927        match InFile::new(hir_file, name).original_node_file_range_opt(db) {
928            // call site name
929            Some((focus_range, ctxt)) if ctxt.is_root() => {
930                // Try to upmap the node as well, if it ends up in the def site, go back to the call site
931                (
932                    (
933                        match value_range {
934                            // name is in the node in the macro input so we can return it
935                            Some((range, ctxt))
936                                if ctxt.is_root()
937                                    && range.file_id == focus_range.file_id
938                                    && range.range.contains_range(focus_range.range) =>
939                            {
940                                range
941                            }
942                            // name lies outside the node, so instead point to the macro call which
943                            // *should* contain the name
944                            _ => {
945                                let call = call();
946                                let kind = call.kind;
947                                let range = kind.clone().original_call_range_with_input(db);
948                                //If the focus range is in the attribute/derive body, we
949                                // need to point the call site to the entire body, if not, fall back
950                                // to the name range of the attribute/derive call
951                                // FIXME: Do this differently, this is very inflexible the caller
952                                // should choose this behavior
953                                if range.file_id == focus_range.file_id
954                                    && range.range.contains_range(focus_range.range)
955                                {
956                                    range
957                                } else {
958                                    kind.original_call_range(db, call.krate)
959                                }
960                            }
961                        },
962                        Some(focus_range),
963                    ),
964                    // no def site relevant
965                    None,
966                )
967            }
968
969            // def site name
970            // FIXME: This can be improved
971            Some((focus_range, _ctxt)) => {
972                match value_range {
973                    // but overall node is in macro input
974                    Some((range, ctxt)) if ctxt.is_root() => (
975                        // node mapped up in call site, show the node
976                        (range, None),
977                        // def site, if the name is in the (possibly) upmapped def site range, show the
978                        // def site
979                        {
980                            let (def_site, _) = def_range().original_node_file_range(db);
981                            (def_site.file_id == focus_range.file_id
982                                && def_site.range.contains_range(focus_range.range))
983                            .then_some((def_site, Some(focus_range)))
984                        },
985                    ),
986                    // node is in macro def, just show the focus
987                    _ => {
988                        let call = call();
989                        (
990                            // show the macro call
991                            (call.kind.original_call_range(db, call.krate), None),
992                            Some((focus_range, Some(focus_range))),
993                        )
994                    }
995                }
996            }
997            // lost name? can't happen for single tokens
998            None => return orig_range_r(db, hir_file, value),
999        };
1000
1001    UpmappingResult {
1002        call_site: (
1003            call_site_range.into_file_id(db),
1004            call_site_focus.and_then(|hir::FileRange { file_id, range }| {
1005                if call_site_range.file_id == file_id && call_site_range.range.contains_range(range)
1006                {
1007                    Some(range)
1008                } else {
1009                    None
1010                }
1011            }),
1012        ),
1013        def_site: def_site.map(|(def_site_range, def_site_focus)| {
1014            (
1015                def_site_range.into_file_id(db),
1016                def_site_focus.and_then(|hir::FileRange { file_id, range }| {
1017                    if def_site_range.file_id == file_id
1018                        && def_site_range.range.contains_range(range)
1019                    {
1020                        Some(range)
1021                    } else {
1022                        None
1023                    }
1024                }),
1025            )
1026        }),
1027    }
1028}
1029
1030fn orig_range(
1031    db: &RootDatabase,
1032    hir_file: HirFileId,
1033    value: &SyntaxNode,
1034) -> UpmappingResult<(FileRange, Option<TextRange>)> {
1035    UpmappingResult {
1036        call_site: (
1037            InFile::new(hir_file, value).original_file_range_rooted(db).into_file_id(db),
1038            None,
1039        ),
1040        def_site: None,
1041    }
1042}
1043
1044fn orig_range_r(
1045    db: &RootDatabase,
1046    hir_file: HirFileId,
1047    value: TextRange,
1048) -> UpmappingResult<(FileRange, Option<TextRange>)> {
1049    UpmappingResult {
1050        call_site: (
1051            InFile::new(hir_file, value).original_node_file_range(db).0.into_file_id(db),
1052            None,
1053        ),
1054        def_site: None,
1055    }
1056}
1057
1058#[cfg(test)]
1059mod tests {
1060    use expect_test::expect;
1061
1062    use crate::{Query, fixture};
1063
1064    #[test]
1065    fn test_nav_for_symbol() {
1066        let (analysis, _) = fixture::file(
1067            r#"
1068enum FooInner { }
1069fn foo() { enum FooInner { } }
1070"#,
1071        );
1072
1073        let navs = analysis.symbol_search(Query::new("FooInner".to_owned()), !0).unwrap();
1074        expect![[r#"
1075            [
1076                NavigationTarget {
1077                    file_id: FileId(
1078                        0,
1079                    ),
1080                    full_range: 0..17,
1081                    focus_range: 5..13,
1082                    name: "FooInner",
1083                    kind: Enum,
1084                    description: "enum FooInner",
1085                },
1086                NavigationTarget {
1087                    file_id: FileId(
1088                        0,
1089                    ),
1090                    full_range: 29..46,
1091                    focus_range: 34..42,
1092                    name: "FooInner",
1093                    kind: Enum,
1094                    container_name: "foo",
1095                    description: "enum FooInner",
1096                },
1097            ]
1098        "#]]
1099        .assert_debug_eq(&navs);
1100    }
1101
1102    #[test]
1103    fn test_world_symbols_are_case_sensitive() {
1104        let (analysis, _) = fixture::file(
1105            r#"
1106fn foo() {}
1107struct Foo;
1108"#,
1109        );
1110
1111        let navs = analysis.symbol_search(Query::new("foo".to_owned()), !0).unwrap();
1112        assert_eq!(navs.len(), 2)
1113    }
1114
1115    #[test]
1116    fn test_ensure_hidden_symbols_are_not_returned() {
1117        let (analysis, _) = fixture::file(
1118            r#"
1119fn foo() {}
1120struct Foo;
1121static __FOO_CALLSITE: () = ();
1122"#,
1123        );
1124
1125        // It doesn't show the hidden symbol
1126        let navs = analysis.symbol_search(Query::new("foo".to_owned()), !0).unwrap();
1127        assert_eq!(navs.len(), 2);
1128        let navs = analysis.symbol_search(Query::new("_foo".to_owned()), !0).unwrap();
1129        assert_eq!(navs.len(), 0);
1130
1131        // Unless we explicitly search for a `__` prefix
1132        let query = Query::new("__foo".to_owned());
1133        let navs = analysis.symbol_search(query, !0).unwrap();
1134        assert_eq!(navs.len(), 1);
1135    }
1136}