ide/
navigation_target.rs

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