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    pub docs: Option<Documentation>,
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);
161                        res.description = Some(
162                            module.display(db, module.krate().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_syntax(
207        file_id: FileId,
208        name: Symbol,
209        focus_range: Option<TextRange>,
210        full_range: TextRange,
211        kind: SymbolKind,
212    ) -> NavigationTarget {
213        NavigationTarget {
214            file_id,
215            name,
216            kind: Some(kind),
217            full_range,
218            focus_range,
219            container_name: None,
220            description: None,
221            docs: None,
222            alias: None,
223        }
224    }
225}
226
227impl TryToNav for FileSymbol {
228    fn try_to_nav(
229        &self,
230        sema: &Semantics<'_, RootDatabase>,
231    ) -> Option<UpmappingResult<NavigationTarget>> {
232        let db = sema.db;
233        let display_target = self.def.krate(db).to_display_target(db);
234        Some(
235            orig_range_with_focus_r(
236                db,
237                self.loc.hir_file_id,
238                self.loc.ptr.text_range(),
239                Some(self.loc.name_ptr.text_range()),
240            )
241            .map(|(FileRange { file_id, range: full_range }, focus_range)| {
242                NavigationTarget {
243                    file_id,
244                    name: self
245                        .is_alias
246                        .then(|| self.def.name(db))
247                        .flatten()
248                        .map_or_else(|| self.name.clone(), |it| it.symbol().clone()),
249                    alias: self.is_alias.then(|| self.name.clone()),
250                    kind: Some(self.def.into()),
251                    full_range,
252                    focus_range,
253                    container_name: self.container_name.clone(),
254                    description: match self.def {
255                        hir::ModuleDef::Module(it) => {
256                            Some(it.display(db, display_target).to_string())
257                        }
258                        hir::ModuleDef::Function(it) => {
259                            Some(it.display(db, display_target).to_string())
260                        }
261                        hir::ModuleDef::Adt(it) => Some(it.display(db, display_target).to_string()),
262                        hir::ModuleDef::Variant(it) => {
263                            Some(it.display(db, display_target).to_string())
264                        }
265                        hir::ModuleDef::Const(it) => {
266                            Some(it.display(db, display_target).to_string())
267                        }
268                        hir::ModuleDef::Static(it) => {
269                            Some(it.display(db, display_target).to_string())
270                        }
271                        hir::ModuleDef::Trait(it) => {
272                            Some(it.display(db, display_target).to_string())
273                        }
274                        hir::ModuleDef::TypeAlias(it) => {
275                            Some(it.display(db, display_target).to_string())
276                        }
277                        hir::ModuleDef::Macro(it) => {
278                            Some(it.display(db, display_target).to_string())
279                        }
280                        hir::ModuleDef::BuiltinType(_) => None,
281                    },
282                    docs: None,
283                }
284            }),
285        )
286    }
287}
288
289impl TryToNav for Definition {
290    fn try_to_nav(
291        &self,
292        sema: &Semantics<'_, RootDatabase>,
293    ) -> Option<UpmappingResult<NavigationTarget>> {
294        match self {
295            Definition::Local(it) => Some(it.to_nav(sema.db)),
296            Definition::Label(it) => it.try_to_nav(sema),
297            Definition::Module(it) => Some(it.to_nav(sema.db)),
298            Definition::Crate(it) => Some(it.to_nav(sema.db)),
299            Definition::Macro(it) => it.try_to_nav(sema),
300            Definition::Field(it) => it.try_to_nav(sema),
301            Definition::SelfType(it) => it.try_to_nav(sema),
302            Definition::GenericParam(it) => it.try_to_nav(sema),
303            Definition::Function(it) => it.try_to_nav(sema),
304            Definition::Adt(it) => it.try_to_nav(sema),
305            Definition::Variant(it) => it.try_to_nav(sema),
306            Definition::Const(it) => it.try_to_nav(sema),
307            Definition::Static(it) => it.try_to_nav(sema),
308            Definition::Trait(it) => it.try_to_nav(sema),
309            Definition::TypeAlias(it) => it.try_to_nav(sema),
310            Definition::ExternCrateDecl(it) => it.try_to_nav(sema),
311            Definition::InlineAsmOperand(it) => it.try_to_nav(sema),
312            Definition::BuiltinType(it) => it.try_to_nav(sema),
313            Definition::BuiltinLifetime(_)
314            | Definition::TupleField(_)
315            | Definition::ToolModule(_)
316            | Definition::InlineAsmRegOrRegClass(_)
317            | Definition::BuiltinAttr(_) => None,
318            // FIXME: The focus range should be set to the helper declaration
319            Definition::DeriveHelper(it) => it.derive().try_to_nav(sema),
320        }
321    }
322}
323
324impl TryToNav for hir::ModuleDef {
325    fn try_to_nav(
326        &self,
327        sema: &Semantics<'_, RootDatabase>,
328    ) -> Option<UpmappingResult<NavigationTarget>> {
329        match self {
330            hir::ModuleDef::Module(it) => Some(it.to_nav(sema.db)),
331            hir::ModuleDef::Function(it) => it.try_to_nav(sema),
332            hir::ModuleDef::Adt(it) => it.try_to_nav(sema),
333            hir::ModuleDef::Variant(it) => it.try_to_nav(sema),
334            hir::ModuleDef::Const(it) => it.try_to_nav(sema),
335            hir::ModuleDef::Static(it) => it.try_to_nav(sema),
336            hir::ModuleDef::Trait(it) => it.try_to_nav(sema),
337            hir::ModuleDef::TypeAlias(it) => it.try_to_nav(sema),
338            hir::ModuleDef::Macro(it) => it.try_to_nav(sema),
339            hir::ModuleDef::BuiltinType(_) => None,
340        }
341    }
342}
343
344pub(crate) trait ToNavFromAst: Sized {
345    const KIND: SymbolKind;
346    fn container_name(self, db: &RootDatabase) -> Option<Symbol> {
347        _ = db;
348        None
349    }
350}
351
352fn container_name(db: &RootDatabase, t: impl HasContainer) -> Option<Symbol> {
353    match t.container(db) {
354        hir::ItemContainer::Trait(it) => Some(it.name(db).symbol().clone()),
355        // FIXME: Handle owners of blocks correctly here
356        hir::ItemContainer::Module(it) => it.name(db).map(|name| name.symbol().clone()),
357        _ => None,
358    }
359}
360
361impl ToNavFromAst for hir::Function {
362    const KIND: SymbolKind = SymbolKind::Function;
363    fn container_name(self, db: &RootDatabase) -> Option<Symbol> {
364        container_name(db, self)
365    }
366}
367
368impl ToNavFromAst for hir::Const {
369    const KIND: SymbolKind = SymbolKind::Const;
370    fn container_name(self, db: &RootDatabase) -> Option<Symbol> {
371        container_name(db, self)
372    }
373}
374impl ToNavFromAst for hir::Static {
375    const KIND: SymbolKind = SymbolKind::Static;
376    fn container_name(self, db: &RootDatabase) -> Option<Symbol> {
377        container_name(db, self)
378    }
379}
380impl ToNavFromAst for hir::Struct {
381    const KIND: SymbolKind = SymbolKind::Struct;
382    fn container_name(self, db: &RootDatabase) -> Option<Symbol> {
383        container_name(db, self)
384    }
385}
386impl ToNavFromAst for hir::Enum {
387    const KIND: SymbolKind = SymbolKind::Enum;
388    fn container_name(self, db: &RootDatabase) -> Option<Symbol> {
389        container_name(db, self)
390    }
391}
392impl ToNavFromAst for hir::Variant {
393    const KIND: SymbolKind = SymbolKind::Variant;
394}
395impl ToNavFromAst for hir::Union {
396    const KIND: SymbolKind = SymbolKind::Union;
397    fn container_name(self, db: &RootDatabase) -> Option<Symbol> {
398        container_name(db, self)
399    }
400}
401impl ToNavFromAst for hir::TypeAlias {
402    const KIND: SymbolKind = SymbolKind::TypeAlias;
403    fn container_name(self, db: &RootDatabase) -> Option<Symbol> {
404        container_name(db, self)
405    }
406}
407impl ToNavFromAst for hir::Trait {
408    const KIND: SymbolKind = SymbolKind::Trait;
409    fn container_name(self, db: &RootDatabase) -> Option<Symbol> {
410        container_name(db, self)
411    }
412}
413
414impl<D> TryToNav for D
415where
416    D: HasSource + ToNavFromAst + Copy + HasDocs + for<'db> HirDisplay<'db> + HasCrate,
417    D::Ast: ast::HasName,
418{
419    fn try_to_nav(
420        &self,
421        sema: &Semantics<'_, RootDatabase>,
422    ) -> Option<UpmappingResult<NavigationTarget>> {
423        let db = sema.db;
424        let src = self.source(db)?;
425        Some(
426            NavigationTarget::from_named(
427                db,
428                src.as_ref().map(|it| it as &dyn ast::HasName),
429                D::KIND,
430            )
431            .map(|mut res| {
432                res.docs = self.docs(db);
433                res.description = hir::attach_db(db, || {
434                    Some(self.display(db, self.krate(db).to_display_target(db)).to_string())
435                });
436                res.container_name = self.container_name(db);
437                res
438            }),
439        )
440    }
441}
442
443impl ToNav for hir::Module {
444    fn to_nav(&self, db: &RootDatabase) -> UpmappingResult<NavigationTarget> {
445        let InFile { file_id, value } = self.definition_source(db);
446
447        let name = self.name(db).map(|it| it.symbol().clone()).unwrap_or_else(|| sym::underscore);
448        let (syntax, focus) = match &value {
449            ModuleSource::SourceFile(node) => (node.syntax(), None),
450            ModuleSource::Module(node) => (node.syntax(), node.name()),
451            ModuleSource::BlockExpr(node) => (node.syntax(), None),
452        };
453
454        orig_range_with_focus(db, file_id, syntax, focus).map(
455            |(FileRange { file_id, range: full_range }, focus_range)| {
456                NavigationTarget::from_syntax(
457                    file_id,
458                    name.clone(),
459                    focus_range,
460                    full_range,
461                    SymbolKind::Module,
462                )
463            },
464        )
465    }
466}
467
468impl ToNav for hir::Crate {
469    fn to_nav(&self, db: &RootDatabase) -> UpmappingResult<NavigationTarget> {
470        self.root_module().to_nav(db)
471    }
472}
473
474impl TryToNav for hir::Impl {
475    fn try_to_nav(
476        &self,
477        sema: &Semantics<'_, RootDatabase>,
478    ) -> Option<UpmappingResult<NavigationTarget>> {
479        let db = sema.db;
480        let InFile { file_id, value } = self.source(db)?;
481        let derive_path = self.as_builtin_derive_path(db);
482
483        let (file_id, focus, syntax) = match &derive_path {
484            Some(attr) => (attr.file_id.into(), None, attr.value.syntax()),
485            None => (file_id, value.self_ty(), value.syntax()),
486        };
487
488        Some(orig_range_with_focus(db, file_id, syntax, focus).map(
489            |(FileRange { file_id, range: full_range }, focus_range)| {
490                NavigationTarget::from_syntax(
491                    file_id,
492                    sym::kw_impl,
493                    focus_range,
494                    full_range,
495                    SymbolKind::Impl,
496                )
497            },
498        ))
499    }
500}
501
502impl TryToNav for hir::ExternCrateDecl {
503    fn try_to_nav(
504        &self,
505        sema: &Semantics<'_, RootDatabase>,
506    ) -> Option<UpmappingResult<NavigationTarget>> {
507        let db = sema.db;
508        let src = self.source(db)?;
509        let InFile { file_id, value } = src;
510        let focus = value
511            .rename()
512            .map_or_else(|| value.name_ref().map(Either::Left), |it| it.name().map(Either::Right));
513        let krate = self.module(db).krate();
514
515        Some(orig_range_with_focus(db, file_id, value.syntax(), focus).map(
516            |(FileRange { file_id, range: full_range }, focus_range)| {
517                let mut res = NavigationTarget::from_syntax(
518                    file_id,
519                    self.alias_or_name(db).unwrap_or_else(|| self.name(db)).symbol().clone(),
520                    focus_range,
521                    full_range,
522                    SymbolKind::Module,
523                );
524
525                res.docs = self.docs(db);
526                res.description = Some(self.display(db, krate.to_display_target(db)).to_string());
527                res.container_name = container_name(db, *self);
528                res
529            },
530        ))
531    }
532}
533
534impl TryToNav for hir::Field {
535    fn try_to_nav(
536        &self,
537        sema: &Semantics<'_, RootDatabase>,
538    ) -> Option<UpmappingResult<NavigationTarget>> {
539        let db = sema.db;
540        let src = self.source(db)?;
541        let krate = self.parent_def(db).module(db).krate();
542
543        let field_source = match &src.value {
544            FieldSource::Named(it) => {
545                NavigationTarget::from_named(db, src.with_value(it), SymbolKind::Field).map(
546                    |mut res| {
547                        res.docs = self.docs(db);
548                        res.description = hir::attach_db(db, || {
549                            Some(self.display(db, krate.to_display_target(db)).to_string())
550                        });
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);
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_kind = || db.lookup_intern_macro_call(hir_file.macro_file().unwrap()).kind;
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 kind = call_kind();
946                                let range = kind.clone().original_call_range_with_input(db);
947                                //If the focus range is in the attribute/derive body, we
948                                // need to point the call site to the entire body, if not, fall back
949                                // to the name range of the attribute/derive call
950                                // FIXME: Do this differently, this is very inflexible the caller
951                                // should choose this behavior
952                                if range.file_id == focus_range.file_id
953                                    && range.range.contains_range(focus_range.range)
954                                {
955                                    range
956                                } else {
957                                    kind.original_call_range(db)
958                                }
959                            }
960                        },
961                        Some(focus_range),
962                    ),
963                    // no def site relevant
964                    None,
965                )
966            }
967
968            // def site name
969            // FIXME: This can be improved
970            Some((focus_range, _ctxt)) => {
971                match value_range {
972                    // but overall node is in macro input
973                    Some((range, ctxt)) if ctxt.is_root() => (
974                        // node mapped up in call site, show the node
975                        (range, None),
976                        // def site, if the name is in the (possibly) upmapped def site range, show the
977                        // def site
978                        {
979                            let (def_site, _) = def_range().original_node_file_range(db);
980                            (def_site.file_id == focus_range.file_id
981                                && def_site.range.contains_range(focus_range.range))
982                            .then_some((def_site, Some(focus_range)))
983                        },
984                    ),
985                    // node is in macro def, just show the focus
986                    _ => (
987                        // show the macro call
988                        (call_kind().original_call_range(db), None),
989                        Some((focus_range, Some(focus_range))),
990                    ),
991                }
992            }
993            // lost name? can't happen for single tokens
994            None => return orig_range_r(db, hir_file, value),
995        };
996
997    UpmappingResult {
998        call_site: (
999            call_site_range.into_file_id(db),
1000            call_site_focus.and_then(|hir::FileRange { file_id, range }| {
1001                if call_site_range.file_id == file_id && call_site_range.range.contains_range(range)
1002                {
1003                    Some(range)
1004                } else {
1005                    None
1006                }
1007            }),
1008        ),
1009        def_site: def_site.map(|(def_site_range, def_site_focus)| {
1010            (
1011                def_site_range.into_file_id(db),
1012                def_site_focus.and_then(|hir::FileRange { file_id, range }| {
1013                    if def_site_range.file_id == file_id
1014                        && def_site_range.range.contains_range(range)
1015                    {
1016                        Some(range)
1017                    } else {
1018                        None
1019                    }
1020                }),
1021            )
1022        }),
1023    }
1024}
1025
1026fn orig_range(
1027    db: &RootDatabase,
1028    hir_file: HirFileId,
1029    value: &SyntaxNode,
1030) -> UpmappingResult<(FileRange, Option<TextRange>)> {
1031    UpmappingResult {
1032        call_site: (
1033            InFile::new(hir_file, value).original_file_range_rooted(db).into_file_id(db),
1034            None,
1035        ),
1036        def_site: None,
1037    }
1038}
1039
1040fn orig_range_r(
1041    db: &RootDatabase,
1042    hir_file: HirFileId,
1043    value: TextRange,
1044) -> UpmappingResult<(FileRange, Option<TextRange>)> {
1045    UpmappingResult {
1046        call_site: (
1047            InFile::new(hir_file, value).original_node_file_range(db).0.into_file_id(db),
1048            None,
1049        ),
1050        def_site: None,
1051    }
1052}
1053
1054#[cfg(test)]
1055mod tests {
1056    use expect_test::expect;
1057
1058    use crate::{Query, fixture};
1059
1060    #[test]
1061    fn test_nav_for_symbol() {
1062        let (analysis, _) = fixture::file(
1063            r#"
1064enum FooInner { }
1065fn foo() { enum FooInner { } }
1066"#,
1067        );
1068
1069        let navs = analysis.symbol_search(Query::new("FooInner".to_owned()), !0).unwrap();
1070        expect![[r#"
1071            [
1072                NavigationTarget {
1073                    file_id: FileId(
1074                        0,
1075                    ),
1076                    full_range: 0..17,
1077                    focus_range: 5..13,
1078                    name: "FooInner",
1079                    kind: Enum,
1080                    description: "enum FooInner",
1081                },
1082                NavigationTarget {
1083                    file_id: FileId(
1084                        0,
1085                    ),
1086                    full_range: 29..46,
1087                    focus_range: 34..42,
1088                    name: "FooInner",
1089                    kind: Enum,
1090                    container_name: "foo",
1091                    description: "enum FooInner",
1092                },
1093            ]
1094        "#]]
1095        .assert_debug_eq(&navs);
1096    }
1097
1098    #[test]
1099    fn test_world_symbols_are_case_sensitive() {
1100        let (analysis, _) = fixture::file(
1101            r#"
1102fn foo() {}
1103struct Foo;
1104"#,
1105        );
1106
1107        let navs = analysis.symbol_search(Query::new("foo".to_owned()), !0).unwrap();
1108        assert_eq!(navs.len(), 2)
1109    }
1110
1111    #[test]
1112    fn test_ensure_hidden_symbols_are_not_returned() {
1113        let (analysis, _) = fixture::file(
1114            r#"
1115fn foo() {}
1116struct Foo;
1117static __FOO_CALLSITE: () = ();
1118"#,
1119        );
1120
1121        // It doesn't show the hidden symbol
1122        let navs = analysis.symbol_search(Query::new("foo".to_owned()), !0).unwrap();
1123        assert_eq!(navs.len(), 2);
1124        let navs = analysis.symbol_search(Query::new("_foo".to_owned()), !0).unwrap();
1125        assert_eq!(navs.len(), 0);
1126
1127        // Unless we explicitly search for a `__` prefix
1128        let query = Query::new("__foo".to_owned());
1129        let navs = analysis.symbol_search(query, !0).unwrap();
1130        assert_eq!(navs.len(), 1);
1131    }
1132}