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