Skip to main content

ide/
navigation_target.rs

1//! See [`NavigationTarget`].
2
3use std::fmt;
4
5use arrayvec::ArrayVec;
6use either::Either;
7use hir::{
8    AssocItem, Crate, FieldSource, HasContainer, HasCrate, HasSource, HirDisplay, HirFileId,
9    InFile, LocalSource, ModuleSource, Name, Semantics, Symbol, sym, symbols::FileSymbol,
10};
11use ide_db::{
12    FileId, FileRange, RootDatabase, SymbolKind,
13    base_db::{CrateOrigin, LangCrateOrigin, all_crates},
14    defs::{Definition, find_std_module},
15    documentation::HasDocs,
16    famous_defs::FamousDefs,
17    ra_fixture::UpmapFromRaFixture,
18};
19use stdx::never;
20use syntax::{
21    AstNode, AstPtr, SyntaxNode, TextRange,
22    ast::{self, HasName},
23};
24
25/// `NavigationTarget` represents an element in the editor's UI which you can
26/// click on to navigate to a particular piece of code.
27///
28/// Typically, a `NavigationTarget` corresponds to some element in the source
29/// code, like a function or a struct, but this is not strictly required.
30#[derive(Clone, PartialEq, Eq, Hash)]
31pub struct NavigationTarget {
32    pub file_id: FileId,
33    /// Range which encompasses the whole element.
34    ///
35    /// Should include body, doc comments, attributes, etc.
36    ///
37    /// Clients should use this range to answer "is the cursor inside the
38    /// element?" question.
39    pub full_range: TextRange,
40    /// A "most interesting" range within the `full_range`.
41    ///
42    /// Typically, `full_range` is the whole syntax node, including doc
43    /// comments, and `focus_range` is the range of the identifier.
44    ///
45    /// Clients should place the cursor on this range when navigating to this target.
46    ///
47    /// This range must be contained within [`Self::full_range`].
48    pub focus_range: Option<TextRange>,
49    pub name: Symbol,
50    pub kind: Option<SymbolKind>,
51    pub container_name: Option<Symbol>,
52    pub description: Option<String>,
53    /// In addition to a `name` field, a `NavigationTarget` may also be aliased
54    /// In such cases we want a `NavigationTarget` to be accessible by its alias
55    pub alias: Option<Symbol>,
56}
57
58impl fmt::Debug for NavigationTarget {
59    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60        let mut f = f.debug_struct("NavigationTarget");
61        macro_rules! opt {
62            ($($name:ident)*) => {$(
63                if let Some(it) = &self.$name {
64                    f.field(stringify!($name), it);
65                }
66            )*}
67        }
68        f.field("file_id", &self.file_id).field("full_range", &self.full_range);
69        opt!(focus_range);
70        f.field("name", &self.name);
71        opt!(kind container_name description);
72        f.finish()
73    }
74}
75
76impl UpmapFromRaFixture for NavigationTarget {
77    fn upmap_from_ra_fixture(
78        self,
79        analysis: &ide_db::ra_fixture::RaFixtureAnalysis,
80        _virtual_file_id: FileId,
81        real_file_id: FileId,
82    ) -> Result<Self, ()> {
83        let virtual_file_id = self.file_id;
84        Ok(NavigationTarget {
85            file_id: real_file_id,
86            full_range: self.full_range.upmap_from_ra_fixture(
87                analysis,
88                virtual_file_id,
89                real_file_id,
90            )?,
91            focus_range: self.focus_range.upmap_from_ra_fixture(
92                analysis,
93                virtual_file_id,
94                real_file_id,
95            )?,
96            name: self.name.upmap_from_ra_fixture(analysis, virtual_file_id, real_file_id)?,
97            kind: self.kind.upmap_from_ra_fixture(analysis, virtual_file_id, real_file_id)?,
98            container_name: self.container_name.upmap_from_ra_fixture(
99                analysis,
100                virtual_file_id,
101                real_file_id,
102            )?,
103            description: self.description.upmap_from_ra_fixture(
104                analysis,
105                virtual_file_id,
106                real_file_id,
107            )?,
108            alias: self.alias.upmap_from_ra_fixture(analysis, virtual_file_id, real_file_id)?,
109        })
110    }
111}
112
113pub(crate) trait ToNav {
114    fn to_nav(&self, db: &RootDatabase) -> UpmappingResult<NavigationTarget>;
115}
116
117pub trait TryToNav {
118    fn try_to_nav(
119        &self,
120        sema: &Semantics<'_, RootDatabase>,
121    ) -> Option<UpmappingResult<NavigationTarget>>;
122}
123
124impl<T: TryToNav, U: TryToNav> TryToNav for Either<T, U> {
125    fn try_to_nav(
126        &self,
127        sema: &Semantics<'_, RootDatabase>,
128    ) -> Option<UpmappingResult<NavigationTarget>> {
129        match self {
130            Either::Left(it) => it.try_to_nav(sema),
131            Either::Right(it) => it.try_to_nav(sema),
132        }
133    }
134}
135
136impl NavigationTarget {
137    pub fn focus_or_full_range(&self) -> TextRange {
138        self.focus_range.unwrap_or(self.full_range)
139    }
140
141    pub(crate) fn from_module_to_decl(
142        db: &RootDatabase,
143        module: hir::Module,
144    ) -> UpmappingResult<NavigationTarget> {
145        let name = module.name(db).map(|it| it.symbol().clone()).unwrap_or_else(|| sym::underscore);
146        match module.declaration_source(db) {
147            Some(InFile { value, file_id }) => {
148                orig_range_with_focus(db, file_id, value.syntax(), value.name()).map(
149                    |(FileRange { file_id, range: full_range }, focus_range)| {
150                        let mut res = NavigationTarget::from_syntax(
151                            file_id,
152                            name.clone(),
153                            focus_range,
154                            full_range,
155                            SymbolKind::Module,
156                        );
157                        res.description = Some(
158                            module.display(db, module.krate(db).to_display_target(db)).to_string(),
159                        );
160                        res
161                    },
162                )
163            }
164            _ => module.to_nav(db),
165        }
166    }
167
168    #[cfg(test)]
169    pub(crate) fn debug_render(&self) -> String {
170        let mut buf = format!(
171            "{} {:?} {:?} {:?}",
172            self.name,
173            self.kind.unwrap(),
174            self.file_id,
175            self.full_range
176        );
177        if let Some(focus_range) = self.focus_range {
178            buf.push_str(&format!(" {focus_range:?}"))
179        }
180        if let Some(container_name) = &self.container_name {
181            buf.push_str(&format!(" {container_name}"))
182        }
183        buf
184    }
185
186    /// Allows `NavigationTarget` to be created from a `NameOwner`
187    pub(crate) fn from_named(
188        db: &RootDatabase,
189        InFile { file_id, value }: InFile<&dyn ast::HasName>,
190        kind: SymbolKind,
191    ) -> UpmappingResult<NavigationTarget> {
192        let name =
193            value.name().map(|it| Symbol::intern(&it.text())).unwrap_or_else(|| sym::underscore);
194
195        orig_range_with_focus(db, file_id, value.syntax(), value.name()).map(
196            |(FileRange { file_id, range: full_range }, focus_range)| {
197                NavigationTarget::from_syntax(file_id, name.clone(), focus_range, full_range, kind)
198            },
199        )
200    }
201
202    pub(crate) fn from_named_with_range(
203        db: &RootDatabase,
204        ranges: InFile<(TextRange, Option<TextRange>)>,
205        name: Option<Name>,
206        kind: SymbolKind,
207    ) -> UpmappingResult<NavigationTarget> {
208        let InFile { file_id, value: (full_range, focus_range) } = ranges;
209        let name = name.map(|name| name.symbol().clone()).unwrap_or_else(|| sym::underscore);
210
211        orig_range_with_focus_r(db, file_id, full_range, focus_range).map(
212            |(FileRange { file_id, range: full_range }, focus_range)| {
213                NavigationTarget::from_syntax(file_id, name.clone(), focus_range, full_range, kind)
214            },
215        )
216    }
217
218    pub(crate) fn from_syntax(
219        file_id: FileId,
220        name: Symbol,
221        focus_range: Option<TextRange>,
222        full_range: TextRange,
223        kind: SymbolKind,
224    ) -> NavigationTarget {
225        NavigationTarget {
226            file_id,
227            name,
228            kind: Some(kind),
229            full_range,
230            focus_range,
231            container_name: None,
232            description: None,
233            alias: None,
234        }
235    }
236}
237
238impl<'db> TryToNav for FileSymbol<'db> {
239    fn try_to_nav(
240        &self,
241        sema: &Semantics<'_, RootDatabase>,
242    ) -> Option<UpmappingResult<NavigationTarget>> {
243        let db = sema.db;
244        let display_target = self.def.krate(db).to_display_target(db);
245        Some(
246            orig_range_with_focus_r(
247                db,
248                self.loc.hir_file_id,
249                self.loc.ptr.text_range(),
250                self.loc.name_ptr.map(AstPtr::text_range),
251            )
252            .map(|(FileRange { file_id, range: full_range }, focus_range)| {
253                NavigationTarget {
254                    file_id,
255                    name: self
256                        .is_alias
257                        .then(|| self.def.name(db))
258                        .flatten()
259                        .map_or_else(|| self.name.clone(), |it| it.symbol().clone()),
260                    alias: self.is_alias.then(|| self.name.clone()),
261                    kind: Some(SymbolKind::from_module_def(db, self.def)),
262                    full_range,
263                    focus_range,
264                    container_name: self.container_name.clone(),
265                    description: match self.def {
266                        hir::ModuleDef::Module(it) => {
267                            Some(it.display(db, display_target).to_string())
268                        }
269                        hir::ModuleDef::Function(it) => {
270                            Some(it.display(db, display_target).to_string())
271                        }
272                        hir::ModuleDef::Adt(it) => Some(it.display(db, display_target).to_string()),
273                        hir::ModuleDef::EnumVariant(it) => {
274                            Some(it.display(db, display_target).to_string())
275                        }
276                        hir::ModuleDef::Const(it) => {
277                            Some(it.display(db, display_target).to_string())
278                        }
279                        hir::ModuleDef::Static(it) => {
280                            Some(it.display(db, display_target).to_string())
281                        }
282                        hir::ModuleDef::Trait(it) => {
283                            Some(it.display(db, display_target).to_string())
284                        }
285                        hir::ModuleDef::TypeAlias(it) => {
286                            Some(it.display(db, display_target).to_string())
287                        }
288                        hir::ModuleDef::Macro(it) => {
289                            Some(it.display(db, display_target).to_string())
290                        }
291                        hir::ModuleDef::BuiltinType(_) => None,
292                    },
293                }
294            }),
295        )
296    }
297}
298
299impl TryToNav for Definition<'_> {
300    fn try_to_nav(
301        &self,
302        sema: &Semantics<'_, RootDatabase>,
303    ) -> Option<UpmappingResult<NavigationTarget>> {
304        match self {
305            Definition::Local(it) => Some(it.to_nav(sema.db)),
306            Definition::Label(it) => it.try_to_nav(sema),
307            Definition::Module(it) => Some(it.to_nav(sema.db)),
308            Definition::Crate(it) => Some(it.to_nav(sema.db)),
309            Definition::Macro(it) => it.try_to_nav(sema),
310            Definition::Field(it) => it.try_to_nav(sema),
311            Definition::SelfType(it) => it.try_to_nav(sema),
312            Definition::GenericParam(it) => it.try_to_nav(sema),
313            Definition::Function(it) => it.try_to_nav(sema),
314            Definition::Adt(it) => it.try_to_nav(sema),
315            Definition::EnumVariant(it) => it.try_to_nav(sema),
316            Definition::Const(it) => it.try_to_nav(sema),
317            Definition::Static(it) => it.try_to_nav(sema),
318            Definition::Trait(it) => it.try_to_nav(sema),
319            Definition::TypeAlias(it) => it.try_to_nav(sema),
320            Definition::ExternCrateDecl(it) => it.try_to_nav(sema),
321            Definition::InlineAsmOperand(it) => it.try_to_nav(sema),
322            Definition::BuiltinType(it) => it.try_to_nav(sema),
323            Definition::BuiltinLifetime(_)
324            | Definition::TupleField(_)
325            | Definition::ToolModule(_)
326            | Definition::InlineAsmRegOrRegClass(_)
327            | Definition::BuiltinAttr(_) => None,
328            // FIXME: The focus range should be set to the helper declaration
329            Definition::DeriveHelper(it) => it.derive().try_to_nav(sema),
330        }
331    }
332}
333
334impl TryToNav for hir::ModuleDef {
335    fn try_to_nav(
336        &self,
337        sema: &Semantics<'_, RootDatabase>,
338    ) -> Option<UpmappingResult<NavigationTarget>> {
339        match self {
340            hir::ModuleDef::Module(it) => Some(it.to_nav(sema.db)),
341            hir::ModuleDef::Function(it) => it.try_to_nav(sema),
342            hir::ModuleDef::Adt(it) => it.try_to_nav(sema),
343            hir::ModuleDef::EnumVariant(it) => it.try_to_nav(sema),
344            hir::ModuleDef::Const(it) => it.try_to_nav(sema),
345            hir::ModuleDef::Static(it) => it.try_to_nav(sema),
346            hir::ModuleDef::Trait(it) => it.try_to_nav(sema),
347            hir::ModuleDef::TypeAlias(it) => it.try_to_nav(sema),
348            hir::ModuleDef::Macro(it) => it.try_to_nav(sema),
349            hir::ModuleDef::BuiltinType(_) => None,
350        }
351    }
352}
353
354pub(crate) trait ToNavFromAst: Sized {
355    const KIND: SymbolKind;
356    fn container_name(self, db: &RootDatabase) -> Option<Symbol> {
357        _ = db;
358        None
359    }
360}
361
362fn container_name(db: &RootDatabase, t: impl HasContainer) -> Option<Symbol> {
363    match t.container(db) {
364        hir::ItemContainer::Trait(it) => Some(it.name(db).symbol().clone()),
365        // FIXME: Handle owners of blocks correctly here
366        hir::ItemContainer::Module(it) => it.name(db).map(|name| name.symbol().clone()),
367        _ => None,
368    }
369}
370
371impl ToNavFromAst for hir::Function {
372    const KIND: SymbolKind = SymbolKind::Function;
373    fn container_name(self, db: &RootDatabase) -> Option<Symbol> {
374        container_name(db, self)
375    }
376}
377
378impl ToNavFromAst for hir::Const {
379    const KIND: SymbolKind = SymbolKind::Const;
380    fn container_name(self, db: &RootDatabase) -> Option<Symbol> {
381        container_name(db, self)
382    }
383}
384impl ToNavFromAst for hir::Static {
385    const KIND: SymbolKind = SymbolKind::Static;
386    fn container_name(self, db: &RootDatabase) -> Option<Symbol> {
387        container_name(db, self)
388    }
389}
390impl ToNavFromAst for hir::Struct {
391    const KIND: SymbolKind = SymbolKind::Struct;
392    fn container_name(self, db: &RootDatabase) -> Option<Symbol> {
393        container_name(db, self)
394    }
395}
396impl ToNavFromAst for hir::Enum {
397    const KIND: SymbolKind = SymbolKind::Enum;
398    fn container_name(self, db: &RootDatabase) -> Option<Symbol> {
399        container_name(db, self)
400    }
401}
402impl ToNavFromAst for hir::EnumVariant {
403    const KIND: SymbolKind = SymbolKind::Variant;
404}
405impl ToNavFromAst for hir::Union {
406    const KIND: SymbolKind = SymbolKind::Union;
407    fn container_name(self, db: &RootDatabase) -> Option<Symbol> {
408        container_name(db, self)
409    }
410}
411impl ToNavFromAst for hir::TypeAlias {
412    const KIND: SymbolKind = SymbolKind::TypeAlias;
413    fn container_name(self, db: &RootDatabase) -> Option<Symbol> {
414        container_name(db, self)
415    }
416}
417impl ToNavFromAst for hir::Trait {
418    const KIND: SymbolKind = SymbolKind::Trait;
419    fn container_name(self, db: &RootDatabase) -> Option<Symbol> {
420        container_name(db, self)
421    }
422}
423
424impl<D> TryToNav for D
425where
426    D: HasSource
427        + ToNavFromAst
428        + Copy
429        + HasDocs
430        + for<'db> HirDisplay<'db>
431        + HasCrate
432        + hir::HasName,
433    D::Ast: ast::HasName,
434{
435    fn try_to_nav(
436        &self,
437        sema: &Semantics<'_, RootDatabase>,
438    ) -> Option<UpmappingResult<NavigationTarget>> {
439        let db = sema.db;
440        let src = self.source_with_range(db)?;
441        Some(
442            NavigationTarget::from_named_with_range(
443                db,
444                src.map(|(full_range, node)| {
445                    (
446                        full_range,
447                        node.and_then(|node| {
448                            Some(ast::HasName::name(&node)?.syntax().text_range())
449                        }),
450                    )
451                }),
452                self.name(db),
453                D::KIND,
454            )
455            .map(|mut res| {
456                res.description =
457                    Some(self.display(db, self.krate(db).to_display_target(db)).to_string());
458                res.container_name = self.container_name(db);
459                res
460            }),
461        )
462    }
463}
464
465impl ToNav for hir::Module {
466    fn to_nav(&self, db: &RootDatabase) -> UpmappingResult<NavigationTarget> {
467        let InFile { file_id, value } = self.definition_source(db);
468
469        let name = self.name(db).map(|it| it.symbol().clone()).unwrap_or_else(|| sym::underscore);
470        let (syntax, focus) = match &value {
471            ModuleSource::SourceFile(node) => (node.syntax(), None),
472            ModuleSource::Module(node) => (node.syntax(), node.name()),
473            ModuleSource::BlockExpr(node) => (node.syntax(), None),
474        };
475        let kind = if self.is_crate_root(db) { SymbolKind::CrateRoot } else { SymbolKind::Module };
476
477        orig_range_with_focus(db, file_id, syntax, focus).map(
478            |(FileRange { file_id, range: full_range }, focus_range)| {
479                NavigationTarget::from_syntax(file_id, name.clone(), focus_range, full_range, kind)
480            },
481        )
482    }
483}
484
485impl ToNav for hir::Crate {
486    fn to_nav(&self, db: &RootDatabase) -> UpmappingResult<NavigationTarget> {
487        self.root_module(db).to_nav(db)
488    }
489}
490
491impl TryToNav for hir::Impl {
492    fn try_to_nav(
493        &self,
494        sema: &Semantics<'_, RootDatabase>,
495    ) -> Option<UpmappingResult<NavigationTarget>> {
496        let db = sema.db;
497        let InFile { file_id, value: (full_range, source) } = self.source_with_range(db)?;
498
499        Some(
500            orig_range_with_focus_r(
501                db,
502                file_id,
503                full_range,
504                source.and_then(|source| Some(source.self_ty()?.syntax().text_range())),
505            )
506            .map(|(FileRange { file_id, range: full_range }, focus_range)| {
507                NavigationTarget::from_syntax(
508                    file_id,
509                    sym::kw_impl,
510                    focus_range,
511                    full_range,
512                    SymbolKind::Impl,
513                )
514            }),
515        )
516    }
517}
518
519impl TryToNav for hir::ExternCrateDecl {
520    fn try_to_nav(
521        &self,
522        sema: &Semantics<'_, RootDatabase>,
523    ) -> Option<UpmappingResult<NavigationTarget>> {
524        let db = sema.db;
525        let src = self.source(db)?;
526        let InFile { file_id, value } = src;
527        let focus = value
528            .rename()
529            .map_or_else(|| value.name_ref().map(Either::Left), |it| it.name().map(Either::Right));
530        let krate = self.module(db).krate(db);
531
532        Some(orig_range_with_focus(db, file_id, value.syntax(), focus).map(
533            |(FileRange { file_id, range: full_range }, focus_range)| {
534                let mut res = NavigationTarget::from_syntax(
535                    file_id,
536                    self.alias_or_name(db).unwrap_or_else(|| self.name(db)).symbol().clone(),
537                    focus_range,
538                    full_range,
539                    SymbolKind::CrateRoot,
540                );
541
542                res.description = Some(self.display(db, krate.to_display_target(db)).to_string());
543                res.container_name = container_name(db, *self);
544                res
545            },
546        ))
547    }
548}
549
550impl TryToNav for hir::Field {
551    fn try_to_nav(
552        &self,
553        sema: &Semantics<'_, RootDatabase>,
554    ) -> Option<UpmappingResult<NavigationTarget>> {
555        let db = sema.db;
556        let src = self.source(db)?;
557        let krate = self.parent_def(db).module(db).krate(db);
558
559        let field_source = match &src.value {
560            FieldSource::Named(it) => {
561                NavigationTarget::from_named(db, src.with_value(it), SymbolKind::Field).map(
562                    |mut res| {
563                        res.description =
564                            Some(self.display(db, krate.to_display_target(db)).to_string());
565                        res
566                    },
567                )
568            }
569            FieldSource::Pos(it) => orig_range(db, src.file_id, it.syntax()).map(
570                |(FileRange { file_id, range: full_range }, focus_range)| {
571                    NavigationTarget::from_syntax(
572                        file_id,
573                        sym::Integer::get(self.index()),
574                        focus_range,
575                        full_range,
576                        SymbolKind::Field,
577                    )
578                },
579            ),
580        };
581        Some(field_source)
582    }
583}
584
585impl TryToNav for hir::Macro {
586    fn try_to_nav(
587        &self,
588        sema: &Semantics<'_, RootDatabase>,
589    ) -> Option<UpmappingResult<NavigationTarget>> {
590        let db = sema.db;
591        let src = self.source(db)?;
592        let name_owner: &dyn ast::HasName = match &src.value {
593            Either::Left(it) => it,
594            Either::Right(it) => it,
595        };
596        Some(NavigationTarget::from_named(
597            db,
598            src.as_ref().with_value(name_owner),
599            self.kind(db).into(),
600        ))
601    }
602}
603
604impl TryToNav for hir::Adt {
605    fn try_to_nav(
606        &self,
607        sema: &Semantics<'_, RootDatabase>,
608    ) -> Option<UpmappingResult<NavigationTarget>> {
609        match self {
610            hir::Adt::Struct(it) => it.try_to_nav(sema),
611            hir::Adt::Union(it) => it.try_to_nav(sema),
612            hir::Adt::Enum(it) => it.try_to_nav(sema),
613        }
614    }
615}
616
617impl TryToNav for hir::AssocItem {
618    fn try_to_nav(
619        &self,
620        sema: &Semantics<'_, RootDatabase>,
621    ) -> Option<UpmappingResult<NavigationTarget>> {
622        match self {
623            AssocItem::Function(it) => it.try_to_nav(sema),
624            AssocItem::Const(it) => it.try_to_nav(sema),
625            AssocItem::TypeAlias(it) => it.try_to_nav(sema),
626        }
627    }
628}
629
630impl TryToNav for hir::GenericParam {
631    fn try_to_nav(
632        &self,
633        sema: &Semantics<'_, RootDatabase>,
634    ) -> Option<UpmappingResult<NavigationTarget>> {
635        match self {
636            hir::GenericParam::TypeParam(it) => it.try_to_nav(sema),
637            hir::GenericParam::ConstParam(it) => it.try_to_nav(sema),
638            hir::GenericParam::LifetimeParam(it) => it.try_to_nav(sema),
639        }
640    }
641}
642
643impl ToNav for LocalSource<'_> {
644    fn to_nav(&self, db: &RootDatabase) -> UpmappingResult<NavigationTarget> {
645        let InFile { file_id, value } = &self.source;
646        let file_id = *file_id;
647        let local = self.local;
648        let (node, name) = match &value {
649            Either::Left(bind_pat) => (bind_pat.syntax(), bind_pat.name()),
650            Either::Right(it) => (it.syntax(), it.name()),
651        };
652
653        orig_range_with_focus(db, file_id, node, name).map(
654            |(FileRange { file_id, range: full_range }, focus_range)| {
655                let name = local.name(db).symbol().clone();
656                let kind = if local.is_self(db) {
657                    SymbolKind::SelfParam
658                } else if local.is_param(db) {
659                    SymbolKind::ValueParam
660                } else {
661                    SymbolKind::Local
662                };
663                NavigationTarget {
664                    file_id,
665                    name,
666                    alias: None,
667                    kind: Some(kind),
668                    full_range,
669                    focus_range,
670                    container_name: None,
671                    description: None,
672                }
673            },
674        )
675    }
676}
677
678impl ToNav for hir::Local<'_> {
679    fn to_nav(&self, db: &RootDatabase) -> UpmappingResult<NavigationTarget> {
680        self.primary_source(db).to_nav(db)
681    }
682}
683
684impl TryToNav for hir::Label {
685    fn try_to_nav(
686        &self,
687        sema: &Semantics<'_, RootDatabase>,
688    ) -> Option<UpmappingResult<NavigationTarget>> {
689        let db = sema.db;
690        let InFile { file_id, value } = self.source(db)?;
691        let name = self.name(db).symbol().clone();
692
693        Some(orig_range_with_focus(db, file_id, value.syntax(), value.lifetime()).map(
694            |(FileRange { file_id, range: full_range }, focus_range)| NavigationTarget {
695                file_id,
696                name: name.clone(),
697                alias: None,
698                kind: Some(SymbolKind::Label),
699                full_range,
700                focus_range,
701                container_name: None,
702                description: None,
703            },
704        ))
705    }
706}
707
708impl TryToNav for hir::TypeParam {
709    fn try_to_nav(
710        &self,
711        sema: &Semantics<'_, RootDatabase>,
712    ) -> Option<UpmappingResult<NavigationTarget>> {
713        let db = sema.db;
714        let InFile { file_id, value } = self.merge().source(db)?;
715        let name = self.name(db).symbol().clone();
716
717        let value = match value {
718            Either::Left(ast::TypeOrConstParam::Type(x)) => Either::Left(x),
719            Either::Left(ast::TypeOrConstParam::Const(_)) => {
720                never!();
721                return None;
722            }
723            Either::Right(x) => Either::Right(x),
724        };
725
726        let syntax = match &value {
727            Either::Left(type_param) => type_param.syntax(),
728            Either::Right(trait_) => trait_.syntax(),
729        };
730        let focus = value.as_ref().either(|it| it.name(), |it| it.name());
731
732        Some(orig_range_with_focus(db, file_id, syntax, focus).map(
733            |(FileRange { file_id, range: full_range }, focus_range)| NavigationTarget {
734                file_id,
735                name: name.clone(),
736                alias: None,
737                kind: Some(SymbolKind::TypeParam),
738                full_range,
739                focus_range,
740                container_name: None,
741                description: None,
742            },
743        ))
744    }
745}
746
747impl TryToNav for hir::TypeOrConstParam {
748    fn try_to_nav(
749        &self,
750        sema: &Semantics<'_, RootDatabase>,
751    ) -> Option<UpmappingResult<NavigationTarget>> {
752        self.split(sema.db).try_to_nav(sema)
753    }
754}
755
756impl TryToNav for hir::LifetimeParam {
757    fn try_to_nav(
758        &self,
759        sema: &Semantics<'_, RootDatabase>,
760    ) -> Option<UpmappingResult<NavigationTarget>> {
761        let db = sema.db;
762        let InFile { file_id, value } = self.source(db)?;
763        let name = self.name(db).symbol().clone();
764
765        Some(orig_range(db, file_id, value.syntax()).map(
766            |(FileRange { file_id, range: full_range }, focus_range)| NavigationTarget {
767                file_id,
768                name: name.clone(),
769                alias: None,
770                kind: Some(SymbolKind::LifetimeParam),
771                full_range,
772                focus_range,
773                container_name: None,
774                description: None,
775            },
776        ))
777    }
778}
779
780impl TryToNav for hir::ConstParam {
781    fn try_to_nav(
782        &self,
783        sema: &Semantics<'_, RootDatabase>,
784    ) -> Option<UpmappingResult<NavigationTarget>> {
785        let db = sema.db;
786        let InFile { file_id, value } = self.merge().source(db)?;
787        let name = self.name(db).symbol().clone();
788
789        let value = match value {
790            Either::Left(ast::TypeOrConstParam::Const(x)) => x,
791            _ => {
792                never!();
793                return None;
794            }
795        };
796
797        Some(orig_range_with_focus(db, file_id, value.syntax(), value.name()).map(
798            |(FileRange { file_id, range: full_range }, focus_range)| NavigationTarget {
799                file_id,
800                name: name.clone(),
801                alias: None,
802                kind: Some(SymbolKind::ConstParam),
803                full_range,
804                focus_range,
805                container_name: None,
806                description: None,
807            },
808        ))
809    }
810}
811
812impl TryToNav for hir::InlineAsmOperand {
813    fn try_to_nav(
814        &self,
815        sema: &Semantics<'_, RootDatabase>,
816    ) -> Option<UpmappingResult<NavigationTarget>> {
817        let db = sema.db;
818        let InFile { file_id, value } = &self.source(db)?;
819        let file_id = *file_id;
820        Some(orig_range_with_focus(db, file_id, value.syntax(), value.name()).map(
821            |(FileRange { file_id, range: full_range }, focus_range)| NavigationTarget {
822                file_id,
823                name:
824                    self.name(db).map_or_else(|| sym::underscore.clone(), |it| it.symbol().clone()),
825                alias: None,
826                kind: Some(SymbolKind::Local),
827                full_range,
828                focus_range,
829                container_name: None,
830                description: None,
831            },
832        ))
833    }
834}
835
836impl TryToNav for hir::BuiltinType {
837    fn try_to_nav(
838        &self,
839        sema: &Semantics<'_, RootDatabase>,
840    ) -> Option<UpmappingResult<NavigationTarget>> {
841        let db = sema.db;
842        let krate = all_crates(db)
843            .iter()
844            .copied()
845            .find(|&krate| matches!(krate.data(db).origin, CrateOrigin::Lang(LangCrateOrigin::Std)))
846            .map(Crate::from)?;
847        let edition = krate.edition(db);
848
849        let fd = FamousDefs(sema, krate);
850        let primitive_mod = format!("prim_{}", self.name().display(fd.0.db, edition));
851        let doc_owner = find_std_module(&fd, &primitive_mod, edition)?;
852
853        Some(doc_owner.to_nav(db))
854    }
855}
856
857#[derive(Debug)]
858pub struct UpmappingResult<T> {
859    /// The macro call site.
860    pub call_site: T,
861    /// The macro definition site, if relevant.
862    pub def_site: Option<T>,
863}
864
865impl<T> UpmappingResult<T> {
866    pub fn call_site(self) -> T {
867        self.call_site
868    }
869
870    pub fn collect<FI: FromIterator<T>>(self) -> FI {
871        FI::from_iter(self)
872    }
873}
874
875impl<T> IntoIterator for UpmappingResult<T> {
876    type Item = T;
877
878    type IntoIter = <ArrayVec<T, 2> as IntoIterator>::IntoIter;
879
880    fn into_iter(self) -> Self::IntoIter {
881        self.def_site
882            .into_iter()
883            .chain(Some(self.call_site))
884            .collect::<ArrayVec<_, 2>>()
885            .into_iter()
886    }
887}
888
889impl<T> UpmappingResult<T> {
890    pub(crate) fn map<U>(self, f: impl Fn(T) -> U) -> UpmappingResult<U> {
891        UpmappingResult { call_site: f(self.call_site), def_site: self.def_site.map(f) }
892    }
893}
894
895/// Returns the original range of the syntax node, and the range of the name mapped out of macro expansions
896/// May return two results if the mapped node originates from a macro definition in which case the
897/// second result is the creating macro call.
898fn orig_range_with_focus(
899    db: &RootDatabase,
900    hir_file: HirFileId,
901    value: &SyntaxNode,
902    name: Option<impl AstNode>,
903) -> UpmappingResult<(FileRange, Option<TextRange>)> {
904    orig_range_with_focus_r(
905        db,
906        hir_file,
907        value.text_range(),
908        name.map(|it| it.syntax().text_range()),
909    )
910}
911
912pub(crate) fn orig_range_with_focus_r(
913    db: &RootDatabase,
914    hir_file: HirFileId,
915    value: TextRange,
916    focus_range: Option<TextRange>,
917) -> UpmappingResult<(FileRange, Option<TextRange>)> {
918    let Some(name) = focus_range else { return orig_range_r(db, hir_file, value) };
919
920    let call = || hir_file.macro_file().unwrap().loc(db);
921
922    let def_range = || hir_file.macro_file().unwrap().loc(db).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}