]> git.lizzy.rs Git - rust.git/blob - crates/ide/src/display/navigation_target.rs
Wrap remaining self/super/crate in Name{Ref}
[rust.git] / crates / ide / src / display / navigation_target.rs
1 //! FIXME: write short doc here
2
3 use std::fmt;
4
5 use either::Either;
6 use hir::{AssocItem, Documentation, FieldSource, HasAttrs, HasSource, InFile, ModuleSource};
7 use ide_db::{
8     base_db::{FileId, FileRange, SourceDatabase},
9     symbol_index::FileSymbolKind,
10 };
11 use ide_db::{defs::Definition, RootDatabase};
12 use syntax::{
13     ast::{self, NameOwner},
14     match_ast, AstNode, SmolStr, TextRange,
15 };
16
17 use crate::FileSymbol;
18
19 use super::short_label::ShortLabel;
20
21 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
22 pub enum SymbolKind {
23     Module,
24     Impl,
25     Field,
26     TypeParam,
27     ConstParam,
28     LifetimeParam,
29     ValueParam,
30     SelfParam,
31     Local,
32     Label,
33     Function,
34     Const,
35     Static,
36     Struct,
37     Enum,
38     Variant,
39     Union,
40     TypeAlias,
41     Trait,
42     Macro,
43 }
44
45 /// `NavigationTarget` represents and element in the editor's UI which you can
46 /// click on to navigate to a particular piece of code.
47 ///
48 /// Typically, a `NavigationTarget` corresponds to some element in the source
49 /// code, like a function or a struct, but this is not strictly required.
50 #[derive(Clone, PartialEq, Eq, Hash)]
51 pub struct NavigationTarget {
52     pub file_id: FileId,
53     /// Range which encompasses the whole element.
54     ///
55     /// Should include body, doc comments, attributes, etc.
56     ///
57     /// Clients should use this range to answer "is the cursor inside the
58     /// element?" question.
59     pub full_range: TextRange,
60     /// A "most interesting" range withing the `full_range`.
61     ///
62     /// Typically, `full_range` is the whole syntax node, including doc
63     /// comments, and `focus_range` is the range of the identifier. "Most
64     /// interesting" range within the full range, typically the range of
65     /// identifier.
66     ///
67     /// Clients should place the cursor on this range when navigating to this target.
68     pub focus_range: Option<TextRange>,
69     pub name: SmolStr,
70     pub kind: Option<SymbolKind>,
71     pub container_name: Option<SmolStr>,
72     pub description: Option<String>,
73     pub docs: Option<Documentation>,
74 }
75
76 impl fmt::Debug for NavigationTarget {
77     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78         let mut f = f.debug_struct("NavigationTarget");
79         macro_rules! opt {
80             ($($name:ident)*) => {$(
81                 if let Some(it) = &self.$name {
82                     f.field(stringify!($name), it);
83                 }
84             )*}
85         }
86         f.field("file_id", &self.file_id).field("full_range", &self.full_range);
87         opt!(focus_range);
88         f.field("name", &self.name);
89         opt!(kind container_name description docs);
90         f.finish()
91     }
92 }
93
94 pub(crate) trait ToNav {
95     fn to_nav(&self, db: &RootDatabase) -> NavigationTarget;
96 }
97
98 pub(crate) trait TryToNav {
99     fn try_to_nav(&self, db: &RootDatabase) -> Option<NavigationTarget>;
100 }
101
102 impl NavigationTarget {
103     pub fn focus_or_full_range(&self) -> TextRange {
104         self.focus_range.unwrap_or(self.full_range)
105     }
106
107     pub(crate) fn from_module_to_decl(db: &RootDatabase, module: hir::Module) -> NavigationTarget {
108         let name = module.name(db).map(|it| it.to_string().into()).unwrap_or_default();
109         if let Some(src) = module.declaration_source(db) {
110             let node = src.as_ref().map(|it| it.syntax());
111             let frange = node.original_file_range(db);
112             let mut res = NavigationTarget::from_syntax(
113                 frange.file_id,
114                 name,
115                 None,
116                 frange.range,
117                 SymbolKind::Module,
118             );
119             res.docs = module.attrs(db).docs();
120             res.description = src.value.short_label();
121             return res;
122         }
123         module.to_nav(db)
124     }
125
126     #[cfg(test)]
127     pub(crate) fn assert_match(&self, expected: &str) {
128         let actual = self.debug_render();
129         test_utils::assert_eq_text!(expected.trim(), actual.trim(),);
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         node: InFile<&dyn ast::NameOwner>,
154         kind: SymbolKind,
155     ) -> NavigationTarget {
156         let name =
157             node.value.name().map(|it| it.text().clone()).unwrap_or_else(|| SmolStr::new("_"));
158         let focus_range =
159             node.value.name().map(|it| node.with_value(it.syntax()).original_file_range(db).range);
160         let frange = node.map(|it| it.syntax()).original_file_range(db);
161
162         NavigationTarget::from_syntax(frange.file_id, name, focus_range, frange.range, kind)
163     }
164
165     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         }
182     }
183 }
184
185 impl ToNav for FileSymbol {
186     fn to_nav(&self, db: &RootDatabase) -> NavigationTarget {
187         NavigationTarget {
188             file_id: self.file_id,
189             name: self.name.clone(),
190             kind: Some(match self.kind {
191                 FileSymbolKind::Function => SymbolKind::Function,
192                 FileSymbolKind::Struct => SymbolKind::Struct,
193                 FileSymbolKind::Enum => SymbolKind::Enum,
194                 FileSymbolKind::Trait => SymbolKind::Trait,
195                 FileSymbolKind::Module => SymbolKind::Module,
196                 FileSymbolKind::TypeAlias => SymbolKind::TypeAlias,
197                 FileSymbolKind::Const => SymbolKind::Const,
198                 FileSymbolKind::Static => SymbolKind::Static,
199                 FileSymbolKind::Macro => SymbolKind::Macro,
200             }),
201             full_range: self.range,
202             focus_range: self.name_range,
203             container_name: self.container_name.clone(),
204             description: description_from_symbol(db, self),
205             docs: None,
206         }
207     }
208 }
209
210 impl TryToNav for Definition {
211     fn try_to_nav(&self, db: &RootDatabase) -> Option<NavigationTarget> {
212         match self {
213             Definition::Macro(it) => it.try_to_nav(db),
214             Definition::Field(it) => it.try_to_nav(db),
215             Definition::ModuleDef(it) => it.try_to_nav(db),
216             Definition::SelfType(it) => it.try_to_nav(db),
217             Definition::Local(it) => Some(it.to_nav(db)),
218             Definition::GenericParam(it) => it.try_to_nav(db),
219             Definition::Label(it) => Some(it.to_nav(db)),
220         }
221     }
222 }
223
224 impl TryToNav for hir::ModuleDef {
225     fn try_to_nav(&self, db: &RootDatabase) -> Option<NavigationTarget> {
226         match self {
227             hir::ModuleDef::Module(it) => Some(it.to_nav(db)),
228             hir::ModuleDef::Function(it) => it.try_to_nav(db),
229             hir::ModuleDef::Adt(it) => it.try_to_nav(db),
230             hir::ModuleDef::Variant(it) => it.try_to_nav(db),
231             hir::ModuleDef::Const(it) => it.try_to_nav(db),
232             hir::ModuleDef::Static(it) => it.try_to_nav(db),
233             hir::ModuleDef::Trait(it) => it.try_to_nav(db),
234             hir::ModuleDef::TypeAlias(it) => it.try_to_nav(db),
235             hir::ModuleDef::BuiltinType(_) => None,
236         }
237     }
238 }
239
240 pub(crate) trait ToNavFromAst {
241     const KIND: SymbolKind;
242 }
243 impl ToNavFromAst for hir::Function {
244     const KIND: SymbolKind = SymbolKind::Function;
245 }
246 impl ToNavFromAst for hir::Const {
247     const KIND: SymbolKind = SymbolKind::Const;
248 }
249 impl ToNavFromAst for hir::Static {
250     const KIND: SymbolKind = SymbolKind::Static;
251 }
252 impl ToNavFromAst for hir::Struct {
253     const KIND: SymbolKind = SymbolKind::Struct;
254 }
255 impl ToNavFromAst for hir::Enum {
256     const KIND: SymbolKind = SymbolKind::Enum;
257 }
258 impl ToNavFromAst for hir::Variant {
259     const KIND: SymbolKind = SymbolKind::Variant;
260 }
261 impl ToNavFromAst for hir::Union {
262     const KIND: SymbolKind = SymbolKind::Union;
263 }
264 impl ToNavFromAst for hir::TypeAlias {
265     const KIND: SymbolKind = SymbolKind::TypeAlias;
266 }
267 impl ToNavFromAst for hir::Trait {
268     const KIND: SymbolKind = SymbolKind::Trait;
269 }
270
271 impl<D> TryToNav for D
272 where
273     D: HasSource + ToNavFromAst + Copy + HasAttrs,
274     D::Ast: ast::NameOwner + ShortLabel,
275 {
276     fn try_to_nav(&self, db: &RootDatabase) -> Option<NavigationTarget> {
277         let src = self.source(db)?;
278         let mut res = NavigationTarget::from_named(
279             db,
280             src.as_ref().map(|it| it as &dyn ast::NameOwner),
281             D::KIND,
282         );
283         res.docs = self.docs(db);
284         res.description = src.value.short_label();
285         Some(res)
286     }
287 }
288
289 impl ToNav for hir::Module {
290     fn to_nav(&self, db: &RootDatabase) -> NavigationTarget {
291         let src = self.definition_source(db);
292         let name = self.name(db).map(|it| it.to_string().into()).unwrap_or_default();
293         let (syntax, focus) = match &src.value {
294             ModuleSource::SourceFile(node) => (node.syntax(), None),
295             ModuleSource::Module(node) => {
296                 (node.syntax(), node.name().map(|it| it.syntax().text_range()))
297             }
298         };
299         let frange = src.with_value(syntax).original_file_range(db);
300         NavigationTarget::from_syntax(frange.file_id, name, focus, frange.range, SymbolKind::Module)
301     }
302 }
303
304 impl TryToNav for hir::Impl {
305     fn try_to_nav(&self, db: &RootDatabase) -> Option<NavigationTarget> {
306         let src = self.source(db)?;
307         let derive_attr = self.is_builtin_derive(db);
308         let frange = if let Some(item) = &derive_attr {
309             item.syntax().original_file_range(db)
310         } else {
311             src.as_ref().map(|it| it.syntax()).original_file_range(db)
312         };
313         let focus_range = if derive_attr.is_some() {
314             None
315         } else {
316             src.value.self_ty().map(|ty| src.with_value(ty.syntax()).original_file_range(db).range)
317         };
318
319         Some(NavigationTarget::from_syntax(
320             frange.file_id,
321             "impl".into(),
322             focus_range,
323             frange.range,
324             SymbolKind::Impl,
325         ))
326     }
327 }
328
329 impl TryToNav for hir::Field {
330     fn try_to_nav(&self, db: &RootDatabase) -> Option<NavigationTarget> {
331         let src = self.source(db)?;
332
333         let field_source = match &src.value {
334             FieldSource::Named(it) => {
335                 let mut res =
336                     NavigationTarget::from_named(db, src.with_value(it), SymbolKind::Field);
337                 res.docs = self.docs(db);
338                 res.description = it.short_label();
339                 res
340             }
341             FieldSource::Pos(it) => {
342                 let frange = src.with_value(it.syntax()).original_file_range(db);
343                 NavigationTarget::from_syntax(
344                     frange.file_id,
345                     "".into(),
346                     None,
347                     frange.range,
348                     SymbolKind::Field,
349                 )
350             }
351         };
352         Some(field_source)
353     }
354 }
355
356 impl TryToNav for hir::MacroDef {
357     fn try_to_nav(&self, db: &RootDatabase) -> Option<NavigationTarget> {
358         let src = self.source(db)?;
359         log::debug!("nav target {:#?}", src.value.syntax());
360         let mut res = NavigationTarget::from_named(
361             db,
362             src.as_ref().map(|it| it as &dyn ast::NameOwner),
363             SymbolKind::Macro,
364         );
365         res.docs = self.docs(db);
366         Some(res)
367     }
368 }
369
370 impl TryToNav for hir::Adt {
371     fn try_to_nav(&self, db: &RootDatabase) -> Option<NavigationTarget> {
372         match self {
373             hir::Adt::Struct(it) => it.try_to_nav(db),
374             hir::Adt::Union(it) => it.try_to_nav(db),
375             hir::Adt::Enum(it) => it.try_to_nav(db),
376         }
377     }
378 }
379
380 impl TryToNav for hir::AssocItem {
381     fn try_to_nav(&self, db: &RootDatabase) -> Option<NavigationTarget> {
382         match self {
383             AssocItem::Function(it) => it.try_to_nav(db),
384             AssocItem::Const(it) => it.try_to_nav(db),
385             AssocItem::TypeAlias(it) => it.try_to_nav(db),
386         }
387     }
388 }
389
390 impl TryToNav for hir::GenericParam {
391     fn try_to_nav(&self, db: &RootDatabase) -> Option<NavigationTarget> {
392         match self {
393             hir::GenericParam::TypeParam(it) => it.try_to_nav(db),
394             hir::GenericParam::ConstParam(it) => it.try_to_nav(db),
395             hir::GenericParam::LifetimeParam(it) => it.try_to_nav(db),
396         }
397     }
398 }
399
400 impl ToNav for hir::Local {
401     fn to_nav(&self, db: &RootDatabase) -> NavigationTarget {
402         let src = self.source(db);
403         let (node, name) = match &src.value {
404             Either::Left(bind_pat) => (bind_pat.syntax().clone(), bind_pat.name()),
405             Either::Right(it) => (it.syntax().clone(), it.name()),
406         };
407         let focus_range =
408             name.map(|it| src.with_value(&it.syntax().clone()).original_file_range(db).range);
409
410         let full_range = src.with_value(&node).original_file_range(db);
411         let name = match self.name(db) {
412             Some(it) => it.to_string().into(),
413             None => "".into(),
414         };
415         let kind = if self.is_self(db) {
416             SymbolKind::SelfParam
417         } else if self.is_param(db) {
418             SymbolKind::ValueParam
419         } else {
420             SymbolKind::Local
421         };
422         NavigationTarget {
423             file_id: full_range.file_id,
424             name,
425             kind: Some(kind),
426             full_range: full_range.range,
427             focus_range,
428             container_name: None,
429             description: None,
430             docs: None,
431         }
432     }
433 }
434
435 impl ToNav for hir::Label {
436     fn to_nav(&self, db: &RootDatabase) -> NavigationTarget {
437         let src = self.source(db);
438         let node = src.value.syntax();
439         let FileRange { file_id, range } = src.with_value(node).original_file_range(db);
440         let focus_range =
441             src.value.lifetime().and_then(|lt| lt.lifetime_ident_token()).map(|lt| lt.text_range());
442         let name = self.name(db).to_string().into();
443         NavigationTarget {
444             file_id,
445             name,
446             kind: Some(SymbolKind::Label),
447             full_range: range,
448             focus_range,
449             container_name: None,
450             description: None,
451             docs: None,
452         }
453     }
454 }
455
456 impl TryToNav for hir::TypeParam {
457     fn try_to_nav(&self, db: &RootDatabase) -> Option<NavigationTarget> {
458         let src = self.source(db)?;
459         let full_range = match &src.value {
460             Either::Left(it) => it.syntax().text_range(),
461             Either::Right(it) => it.syntax().text_range(),
462         };
463         let focus_range = match &src.value {
464             Either::Left(_) => None,
465             Either::Right(it) => it.name().map(|it| it.syntax().text_range()),
466         };
467         Some(NavigationTarget {
468             file_id: src.file_id.original_file(db),
469             name: self.name(db).to_string().into(),
470             kind: Some(SymbolKind::TypeParam),
471             full_range,
472             focus_range,
473             container_name: None,
474             description: None,
475             docs: None,
476         })
477     }
478 }
479
480 impl TryToNav for hir::LifetimeParam {
481     fn try_to_nav(&self, db: &RootDatabase) -> Option<NavigationTarget> {
482         let src = self.source(db)?;
483         let full_range = src.value.syntax().text_range();
484         Some(NavigationTarget {
485             file_id: src.file_id.original_file(db),
486             name: self.name(db).to_string().into(),
487             kind: Some(SymbolKind::LifetimeParam),
488             full_range,
489             focus_range: Some(full_range),
490             container_name: None,
491             description: None,
492             docs: None,
493         })
494     }
495 }
496
497 impl TryToNav for hir::ConstParam {
498     fn try_to_nav(&self, db: &RootDatabase) -> Option<NavigationTarget> {
499         let src = self.source(db)?;
500         let full_range = src.value.syntax().text_range();
501         Some(NavigationTarget {
502             file_id: src.file_id.original_file(db),
503             name: self.name(db).to_string().into(),
504             kind: Some(SymbolKind::ConstParam),
505             full_range,
506             focus_range: src.value.name().map(|n| n.syntax().text_range()),
507             container_name: None,
508             description: None,
509             docs: None,
510         })
511     }
512 }
513
514 /// Get a description of a symbol.
515 ///
516 /// e.g. `struct Name`, `enum Name`, `fn Name`
517 pub(crate) fn description_from_symbol(db: &RootDatabase, symbol: &FileSymbol) -> Option<String> {
518     let parse = db.parse(symbol.file_id);
519     let node = symbol.ptr.to_node(parse.tree().syntax());
520
521     match_ast! {
522         match node {
523             ast::Fn(it) => it.short_label(),
524             ast::Struct(it) => it.short_label(),
525             ast::Enum(it) => it.short_label(),
526             ast::Trait(it) => it.short_label(),
527             ast::Module(it) => it.short_label(),
528             ast::TypeAlias(it) => it.short_label(),
529             ast::Const(it) => it.short_label(),
530             ast::Static(it) => it.short_label(),
531             ast::RecordField(it) => it.short_label(),
532             ast::Variant(it) => it.short_label(),
533             _ => None,
534         }
535     }
536 }
537
538 #[cfg(test)]
539 mod tests {
540     use expect_test::expect;
541
542     use crate::{fixture, Query};
543
544     #[test]
545     fn test_nav_for_symbol() {
546         let (analysis, _) = fixture::file(
547             r#"
548 enum FooInner { }
549 fn foo() { enum FooInner { } }
550 "#,
551         );
552
553         let navs = analysis.symbol_search(Query::new("FooInner".to_string())).unwrap();
554         expect![[r#"
555             [
556                 NavigationTarget {
557                     file_id: FileId(
558                         0,
559                     ),
560                     full_range: 0..17,
561                     focus_range: 5..13,
562                     name: "FooInner",
563                     kind: Enum,
564                     description: "enum FooInner",
565                 },
566                 NavigationTarget {
567                     file_id: FileId(
568                         0,
569                     ),
570                     full_range: 29..46,
571                     focus_range: 34..42,
572                     name: "FooInner",
573                     kind: Enum,
574                     container_name: "foo",
575                     description: "enum FooInner",
576                 },
577             ]
578         "#]]
579         .assert_debug_eq(&navs);
580     }
581
582     #[test]
583     fn test_world_symbols_are_case_sensitive() {
584         let (analysis, _) = fixture::file(
585             r#"
586 fn foo() {}
587 struct Foo;
588 "#,
589         );
590
591         let navs = analysis.symbol_search(Query::new("foo".to_string())).unwrap();
592         assert_eq!(navs.len(), 2)
593     }
594 }