Skip to main content

ide/
navigation_target.rs

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