ide/
navigation_target.rs

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