ide/
navigation_target.rs

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