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