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