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