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