]> git.lizzy.rs Git - rust.git/blob - crates/ide/src/display/navigation_target.rs
Move impls of ToNav that use source() to TryToNav
[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::TypeParam(it) => it.try_to_nav(db),
219             Definition::LifetimeParam(it) => it.try_to_nav(db),
220             Definition::Label(it) => Some(it.to_nav(db)),
221             Definition::ConstParam(it) => Some(it.to_nav(db)),
222         }
223     }
224 }
225
226 impl TryToNav for hir::ModuleDef {
227     fn try_to_nav(&self, db: &RootDatabase) -> Option<NavigationTarget> {
228         match self {
229             hir::ModuleDef::Module(it) => Some(it.to_nav(db)),
230             hir::ModuleDef::Function(it) => it.try_to_nav(db),
231             hir::ModuleDef::Adt(it) => it.try_to_nav(db),
232             hir::ModuleDef::Variant(it) => it.try_to_nav(db),
233             hir::ModuleDef::Const(it) => it.try_to_nav(db),
234             hir::ModuleDef::Static(it) => it.try_to_nav(db),
235             hir::ModuleDef::Trait(it) => it.try_to_nav(db),
236             hir::ModuleDef::TypeAlias(it) => it.try_to_nav(db),
237             hir::ModuleDef::BuiltinType(_) => None,
238         }
239     }
240 }
241
242 pub(crate) trait ToNavFromAst {
243     const KIND: SymbolKind;
244 }
245 impl ToNavFromAst for hir::Function {
246     const KIND: SymbolKind = SymbolKind::Function;
247 }
248 impl ToNavFromAst for hir::Const {
249     const KIND: SymbolKind = SymbolKind::Const;
250 }
251 impl ToNavFromAst for hir::Static {
252     const KIND: SymbolKind = SymbolKind::Static;
253 }
254 impl ToNavFromAst for hir::Struct {
255     const KIND: SymbolKind = SymbolKind::Struct;
256 }
257 impl ToNavFromAst for hir::Enum {
258     const KIND: SymbolKind = SymbolKind::Enum;
259 }
260 impl ToNavFromAst for hir::Variant {
261     const KIND: SymbolKind = SymbolKind::Variant;
262 }
263 impl ToNavFromAst for hir::Union {
264     const KIND: SymbolKind = SymbolKind::Union;
265 }
266 impl ToNavFromAst for hir::TypeAlias {
267     const KIND: SymbolKind = SymbolKind::TypeAlias;
268 }
269 impl ToNavFromAst for hir::Trait {
270     const KIND: SymbolKind = SymbolKind::Trait;
271 }
272
273 impl<D> TryToNav for D
274 where
275     D: HasSource + ToNavFromAst + Copy + HasAttrs,
276     D::Ast: ast::NameOwner + ShortLabel,
277 {
278     fn try_to_nav(&self, db: &RootDatabase) -> Option<NavigationTarget> {
279         let src = self.source(db)?;
280         let mut res = NavigationTarget::from_named(
281             db,
282             src.as_ref().map(|it| it as &dyn ast::NameOwner),
283             D::KIND,
284         );
285         res.docs = self.docs(db);
286         res.description = src.value.short_label();
287         Some(res)
288     }
289 }
290
291 impl ToNav for hir::Module {
292     fn to_nav(&self, db: &RootDatabase) -> NavigationTarget {
293         let src = self.definition_source(db);
294         let name = self.name(db).map(|it| it.to_string().into()).unwrap_or_default();
295         let (syntax, focus) = match &src.value {
296             ModuleSource::SourceFile(node) => (node.syntax(), None),
297             ModuleSource::Module(node) => {
298                 (node.syntax(), node.name().map(|it| it.syntax().text_range()))
299             }
300         };
301         let frange = src.with_value(syntax).original_file_range(db);
302         NavigationTarget::from_syntax(frange.file_id, name, focus, frange.range, SymbolKind::Module)
303     }
304 }
305
306 impl TryToNav for hir::Impl {
307     fn try_to_nav(&self, db: &RootDatabase) -> Option<NavigationTarget> {
308         let src = self.source(db)?;
309         let derive_attr = self.is_builtin_derive(db);
310         let frange = if let Some(item) = &derive_attr {
311             item.syntax().original_file_range(db)
312         } else {
313             src.as_ref().map(|it| it.syntax()).original_file_range(db)
314         };
315         let focus_range = if derive_attr.is_some() {
316             None
317         } else {
318             src.value.self_ty().map(|ty| src.with_value(ty.syntax()).original_file_range(db).range)
319         };
320
321         Some(NavigationTarget::from_syntax(
322             frange.file_id,
323             "impl".into(),
324             focus_range,
325             frange.range,
326             SymbolKind::Impl,
327         ))
328     }
329 }
330
331 impl TryToNav for hir::Field {
332     fn try_to_nav(&self, db: &RootDatabase) -> Option<NavigationTarget> {
333         let src = self.source(db)?;
334
335         let field_source = match &src.value {
336             FieldSource::Named(it) => {
337                 let mut res =
338                     NavigationTarget::from_named(db, src.with_value(it), SymbolKind::Field);
339                 res.docs = self.docs(db);
340                 res.description = it.short_label();
341                 res
342             }
343             FieldSource::Pos(it) => {
344                 let frange = src.with_value(it.syntax()).original_file_range(db);
345                 NavigationTarget::from_syntax(
346                     frange.file_id,
347                     "".into(),
348                     None,
349                     frange.range,
350                     SymbolKind::Field,
351                 )
352             }
353         };
354         Some(field_source)
355     }
356 }
357
358 impl TryToNav for hir::MacroDef {
359     fn try_to_nav(&self, db: &RootDatabase) -> Option<NavigationTarget> {
360         let src = self.source(db)?;
361         log::debug!("nav target {:#?}", src.value.syntax());
362         let mut res = NavigationTarget::from_named(
363             db,
364             src.as_ref().map(|it| it as &dyn ast::NameOwner),
365             SymbolKind::Macro,
366         );
367         res.docs = self.docs(db);
368         Some(res)
369     }
370 }
371
372 impl TryToNav for hir::Adt {
373     fn try_to_nav(&self, db: &RootDatabase) -> Option<NavigationTarget> {
374         match self {
375             hir::Adt::Struct(it) => it.try_to_nav(db),
376             hir::Adt::Union(it) => it.try_to_nav(db),
377             hir::Adt::Enum(it) => it.try_to_nav(db),
378         }
379     }
380 }
381
382 impl TryToNav for hir::AssocItem {
383     fn try_to_nav(&self, db: &RootDatabase) -> Option<NavigationTarget> {
384         match self {
385             AssocItem::Function(it) => it.try_to_nav(db),
386             AssocItem::Const(it) => it.try_to_nav(db),
387             AssocItem::TypeAlias(it) => it.try_to_nav(db),
388         }
389     }
390 }
391
392 impl ToNav for hir::Local {
393     fn to_nav(&self, db: &RootDatabase) -> NavigationTarget {
394         let src = self.source(db);
395         let node = match &src.value {
396             Either::Left(bind_pat) => {
397                 bind_pat.name().map_or_else(|| bind_pat.syntax().clone(), |it| it.syntax().clone())
398             }
399             Either::Right(it) => it.syntax().clone(),
400         };
401         let full_range = src.with_value(&node).original_file_range(db);
402         let name = match self.name(db) {
403             Some(it) => it.to_string().into(),
404             None => "".into(),
405         };
406         let kind = if self.is_param(db) { SymbolKind::ValueParam } else { SymbolKind::Local };
407         NavigationTarget {
408             file_id: full_range.file_id,
409             name,
410             kind: Some(kind),
411             full_range: full_range.range,
412             focus_range: None,
413             container_name: None,
414             description: None,
415             docs: None,
416         }
417     }
418 }
419
420 impl ToNav for hir::Label {
421     fn to_nav(&self, db: &RootDatabase) -> NavigationTarget {
422         let src = self.source(db);
423         let node = src.value.syntax();
424         let FileRange { file_id, range } = src.with_value(node).original_file_range(db);
425         let focus_range =
426             src.value.lifetime().and_then(|lt| lt.lifetime_ident_token()).map(|lt| lt.text_range());
427         let name = self.name(db).to_string().into();
428         NavigationTarget {
429             file_id,
430             name,
431             kind: Some(SymbolKind::Label),
432             full_range: range,
433             focus_range,
434             container_name: None,
435             description: None,
436             docs: None,
437         }
438     }
439 }
440
441 impl TryToNav for hir::TypeParam {
442     fn try_to_nav(&self, db: &RootDatabase) -> Option<NavigationTarget> {
443         let src = self.source(db)?;
444         let full_range = match &src.value {
445             Either::Left(it) => it.syntax().text_range(),
446             Either::Right(it) => it.syntax().text_range(),
447         };
448         let focus_range = match &src.value {
449             Either::Left(_) => None,
450             Either::Right(it) => it.name().map(|it| it.syntax().text_range()),
451         };
452         Some(NavigationTarget {
453             file_id: src.file_id.original_file(db),
454             name: self.name(db).to_string().into(),
455             kind: Some(SymbolKind::TypeParam),
456             full_range,
457             focus_range,
458             container_name: None,
459             description: None,
460             docs: None,
461         })
462     }
463 }
464
465 impl TryToNav for hir::LifetimeParam {
466     fn try_to_nav(&self, db: &RootDatabase) -> Option<NavigationTarget> {
467         let src = self.source(db)?;
468         let full_range = src.value.syntax().text_range();
469         Some(NavigationTarget {
470             file_id: src.file_id.original_file(db),
471             name: self.name(db).to_string().into(),
472             kind: Some(SymbolKind::LifetimeParam),
473             full_range,
474             focus_range: Some(full_range),
475             container_name: None,
476             description: None,
477             docs: None,
478         })
479     }
480 }
481
482 impl ToNav for hir::ConstParam {
483     fn to_nav(&self, db: &RootDatabase) -> NavigationTarget {
484         let src = self.source(db);
485         let full_range = src.value.syntax().text_range();
486         NavigationTarget {
487             file_id: src.file_id.original_file(db),
488             name: self.name(db).to_string().into(),
489             kind: Some(SymbolKind::ConstParam),
490             full_range,
491             focus_range: src.value.name().map(|n| n.syntax().text_range()),
492             container_name: None,
493             description: None,
494             docs: None,
495         }
496     }
497 }
498
499 /// Get a description of a symbol.
500 ///
501 /// e.g. `struct Name`, `enum Name`, `fn Name`
502 pub(crate) fn description_from_symbol(db: &RootDatabase, symbol: &FileSymbol) -> Option<String> {
503     let parse = db.parse(symbol.file_id);
504     let node = symbol.ptr.to_node(parse.tree().syntax());
505
506     match_ast! {
507         match node {
508             ast::Fn(it) => it.short_label(),
509             ast::Struct(it) => it.short_label(),
510             ast::Enum(it) => it.short_label(),
511             ast::Trait(it) => it.short_label(),
512             ast::Module(it) => it.short_label(),
513             ast::TypeAlias(it) => it.short_label(),
514             ast::Const(it) => it.short_label(),
515             ast::Static(it) => it.short_label(),
516             ast::RecordField(it) => it.short_label(),
517             ast::Variant(it) => it.short_label(),
518             _ => None,
519         }
520     }
521 }
522
523 #[cfg(test)]
524 mod tests {
525     use expect_test::expect;
526
527     use crate::{fixture, Query};
528
529     #[test]
530     fn test_nav_for_symbol() {
531         let (analysis, _) = fixture::file(
532             r#"
533 enum FooInner { }
534 fn foo() { enum FooInner { } }
535 "#,
536         );
537
538         let navs = analysis.symbol_search(Query::new("FooInner".to_string())).unwrap();
539         expect![[r#"
540             [
541                 NavigationTarget {
542                     file_id: FileId(
543                         0,
544                     ),
545                     full_range: 0..17,
546                     focus_range: 5..13,
547                     name: "FooInner",
548                     kind: Enum,
549                     description: "enum FooInner",
550                 },
551                 NavigationTarget {
552                     file_id: FileId(
553                         0,
554                     ),
555                     full_range: 29..46,
556                     focus_range: 34..42,
557                     name: "FooInner",
558                     kind: Enum,
559                     container_name: "foo",
560                     description: "enum FooInner",
561                 },
562             ]
563         "#]]
564         .assert_debug_eq(&navs);
565     }
566
567     #[test]
568     fn test_world_symbols_are_case_sensitive() {
569         let (analysis, _) = fixture::file(
570             r#"
571 fn foo() {}
572 struct Foo;
573 "#,
574         );
575
576         let navs = analysis.symbol_search(Query::new("foo".to_string())).unwrap();
577         assert_eq!(navs.len(), 2)
578     }
579 }