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