]> git.lizzy.rs Git - rust.git/blob - crates/ide/src/display/navigation_target.rs
Don't expose SyntaxKind from IDE API
[rust.git] / crates / ide / src / display / navigation_target.rs
1 //! FIXME: write short doc here
2
3 use either::Either;
4 use hir::{AssocItem, Documentation, FieldSource, HasAttrs, HasSource, InFile, ModuleSource};
5 use ide_db::{
6     base_db::{FileId, SourceDatabase},
7     symbol_index::FileSymbolKind,
8 };
9 use ide_db::{defs::Definition, RootDatabase};
10 use syntax::{
11     ast::{self, NameOwner},
12     match_ast, AstNode, SmolStr, TextRange,
13 };
14
15 use crate::FileSymbol;
16
17 use super::short_label::ShortLabel;
18
19 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
20 pub enum SymbolKind {
21     Module,
22     Impl,
23     Field,
24     TypeParam,
25     LifetimeParam,
26     SelfParam,
27     Local,
28     Function,
29     Const,
30     Static,
31     Struct,
32     Enum,
33     Variant,
34     Union,
35     TypeAlias,
36     Trait,
37     Macro,
38     // Do we actually need this?
39     DocTest,
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(Debug, 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: SymbolKind,
68     pub container_name: Option<SmolStr>,
69     pub description: Option<String>,
70     pub docs: Option<Documentation>,
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 frange = node.original_file_range(db);
91             let mut res = NavigationTarget::from_syntax(
92                 frange.file_id,
93                 name,
94                 None,
95                 frange.range,
96                 SymbolKind::Module,
97             );
98             res.docs = module.attrs(db).docs();
99             res.description = src.value.short_label();
100             return res;
101         }
102         module.to_nav(db)
103     }
104
105     #[cfg(test)]
106     pub(crate) fn assert_match(&self, expected: &str) {
107         let actual = self.debug_render();
108         test_utils::assert_eq_text!(expected.trim(), actual.trim(),);
109     }
110
111     #[cfg(test)]
112     pub(crate) fn debug_render(&self) -> String {
113         let mut buf =
114             format!("{} {:?} {:?} {:?}", self.name, self.kind, self.file_id, self.full_range);
115         if let Some(focus_range) = self.focus_range {
116             buf.push_str(&format!(" {:?}", focus_range))
117         }
118         if let Some(container_name) = &self.container_name {
119             buf.push_str(&format!(" {}", container_name))
120         }
121         buf
122     }
123
124     /// Allows `NavigationTarget` to be created from a `NameOwner`
125     pub(crate) fn from_named(
126         db: &RootDatabase,
127         node: InFile<&dyn ast::NameOwner>,
128         kind: SymbolKind,
129     ) -> NavigationTarget {
130         let name =
131             node.value.name().map(|it| it.text().clone()).unwrap_or_else(|| SmolStr::new("_"));
132         let focus_range =
133             node.value.name().map(|it| node.with_value(it.syntax()).original_file_range(db).range);
134         let frange = node.map(|it| it.syntax()).original_file_range(db);
135
136         NavigationTarget::from_syntax(frange.file_id, name, focus_range, frange.range, kind)
137     }
138
139     fn from_syntax(
140         file_id: FileId,
141         name: SmolStr,
142         focus_range: Option<TextRange>,
143         full_range: TextRange,
144         kind: SymbolKind,
145     ) -> NavigationTarget {
146         NavigationTarget {
147             file_id,
148             name,
149             kind,
150             full_range,
151             focus_range,
152             container_name: None,
153             description: None,
154             docs: None,
155         }
156     }
157 }
158
159 impl ToNav for FileSymbol {
160     fn to_nav(&self, db: &RootDatabase) -> NavigationTarget {
161         NavigationTarget {
162             file_id: self.file_id,
163             name: self.name.clone(),
164             kind: match self.kind {
165                 FileSymbolKind::Function => SymbolKind::Function,
166                 FileSymbolKind::Struct => SymbolKind::Struct,
167                 FileSymbolKind::Enum => SymbolKind::Enum,
168                 FileSymbolKind::Trait => SymbolKind::Trait,
169                 FileSymbolKind::Module => SymbolKind::Module,
170                 FileSymbolKind::TypeAlias => SymbolKind::TypeAlias,
171                 FileSymbolKind::Const => SymbolKind::Const,
172                 FileSymbolKind::Static => SymbolKind::Static,
173                 FileSymbolKind::Macro => SymbolKind::Macro,
174             },
175             full_range: self.range,
176             focus_range: self.name_range,
177             container_name: self.container_name.clone(),
178             description: description_from_symbol(db, self),
179             docs: None,
180         }
181     }
182 }
183
184 impl TryToNav for Definition {
185     fn try_to_nav(&self, db: &RootDatabase) -> Option<NavigationTarget> {
186         match self {
187             Definition::Macro(it) => {
188                 // FIXME: Currently proc-macro do not have ast-node,
189                 // such that it does not have source
190                 // more discussion: https://github.com/rust-analyzer/rust-analyzer/issues/6913
191                 if it.is_proc_macro() {
192                     return None;
193                 }
194                 Some(it.to_nav(db))
195             }
196             Definition::Field(it) => Some(it.to_nav(db)),
197             Definition::ModuleDef(it) => it.try_to_nav(db),
198             Definition::SelfType(it) => Some(it.to_nav(db)),
199             Definition::Local(it) => Some(it.to_nav(db)),
200             Definition::TypeParam(it) => Some(it.to_nav(db)),
201             Definition::LifetimeParam(it) => Some(it.to_nav(db)),
202         }
203     }
204 }
205
206 impl TryToNav for hir::ModuleDef {
207     fn try_to_nav(&self, db: &RootDatabase) -> Option<NavigationTarget> {
208         let res = match self {
209             hir::ModuleDef::Module(it) => it.to_nav(db),
210             hir::ModuleDef::Function(it) => it.to_nav(db),
211             hir::ModuleDef::Adt(it) => it.to_nav(db),
212             hir::ModuleDef::EnumVariant(it) => it.to_nav(db),
213             hir::ModuleDef::Const(it) => it.to_nav(db),
214             hir::ModuleDef::Static(it) => it.to_nav(db),
215             hir::ModuleDef::Trait(it) => it.to_nav(db),
216             hir::ModuleDef::TypeAlias(it) => it.to_nav(db),
217             hir::ModuleDef::BuiltinType(_) => return None,
218         };
219         Some(res)
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::EnumVariant {
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> ToNav for D
255 where
256     D: HasSource + ToNavFromAst + Copy + HasAttrs,
257     D::Ast: ast::NameOwner + ShortLabel,
258 {
259     fn to_nav(&self, db: &RootDatabase) -> 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 = src.value.short_label();
268         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         };
282         let frange = src.with_value(syntax).original_file_range(db);
283         NavigationTarget::from_syntax(frange.file_id, name, focus, frange.range, SymbolKind::Module)
284     }
285 }
286
287 impl ToNav for hir::Impl {
288     fn to_nav(&self, db: &RootDatabase) -> NavigationTarget {
289         let src = self.source(db);
290         let derive_attr = self.is_builtin_derive(db);
291         let frange = if let Some(item) = &derive_attr {
292             item.syntax().original_file_range(db)
293         } else {
294             src.as_ref().map(|it| it.syntax()).original_file_range(db)
295         };
296         let focus_range = if derive_attr.is_some() {
297             None
298         } else {
299             src.value.self_ty().map(|ty| src.with_value(ty.syntax()).original_file_range(db).range)
300         };
301
302         NavigationTarget::from_syntax(
303             frange.file_id,
304             "impl".into(),
305             focus_range,
306             frange.range,
307             SymbolKind::Impl,
308         )
309     }
310 }
311
312 impl ToNav for hir::Field {
313     fn to_nav(&self, db: &RootDatabase) -> NavigationTarget {
314         let src = self.source(db);
315
316         match &src.value {
317             FieldSource::Named(it) => {
318                 let mut res =
319                     NavigationTarget::from_named(db, src.with_value(it), SymbolKind::Field);
320                 res.docs = self.docs(db);
321                 res.description = it.short_label();
322                 res
323             }
324             FieldSource::Pos(it) => {
325                 let frange = src.with_value(it.syntax()).original_file_range(db);
326                 NavigationTarget::from_syntax(
327                     frange.file_id,
328                     "".into(),
329                     None,
330                     frange.range,
331                     SymbolKind::Field,
332                 )
333             }
334         }
335     }
336 }
337
338 impl ToNav for hir::MacroDef {
339     fn to_nav(&self, db: &RootDatabase) -> NavigationTarget {
340         let src = self.source(db);
341         log::debug!("nav target {:#?}", src.value.syntax());
342         let mut res = NavigationTarget::from_named(
343             db,
344             src.as_ref().map(|it| it as &dyn ast::NameOwner),
345             SymbolKind::Macro,
346         );
347         res.docs = self.docs(db);
348         res
349     }
350 }
351
352 impl ToNav for hir::Adt {
353     fn to_nav(&self, db: &RootDatabase) -> NavigationTarget {
354         match self {
355             hir::Adt::Struct(it) => it.to_nav(db),
356             hir::Adt::Union(it) => it.to_nav(db),
357             hir::Adt::Enum(it) => it.to_nav(db),
358         }
359     }
360 }
361
362 impl ToNav for hir::AssocItem {
363     fn to_nav(&self, db: &RootDatabase) -> NavigationTarget {
364         match self {
365             AssocItem::Function(it) => it.to_nav(db),
366             AssocItem::Const(it) => it.to_nav(db),
367             AssocItem::TypeAlias(it) => it.to_nav(db),
368         }
369     }
370 }
371
372 impl ToNav for hir::Local {
373     fn to_nav(&self, db: &RootDatabase) -> NavigationTarget {
374         let src = self.source(db);
375         let node = match &src.value {
376             Either::Left(bind_pat) => {
377                 bind_pat.name().map_or_else(|| bind_pat.syntax().clone(), |it| it.syntax().clone())
378             }
379             Either::Right(it) => it.syntax().clone(),
380         };
381         let full_range = src.with_value(&node).original_file_range(db);
382         let name = match self.name(db) {
383             Some(it) => it.to_string().into(),
384             None => "".into(),
385         };
386         NavigationTarget {
387             file_id: full_range.file_id,
388             name,
389             kind: SymbolKind::Local,
390             full_range: full_range.range,
391             focus_range: None,
392             container_name: None,
393             description: None,
394             docs: None,
395         }
396     }
397 }
398
399 impl ToNav for hir::TypeParam {
400     fn to_nav(&self, db: &RootDatabase) -> NavigationTarget {
401         let src = self.source(db);
402         let full_range = match &src.value {
403             Either::Left(it) => it.syntax().text_range(),
404             Either::Right(it) => it.syntax().text_range(),
405         };
406         let focus_range = match &src.value {
407             Either::Left(_) => None,
408             Either::Right(it) => it.name().map(|it| it.syntax().text_range()),
409         };
410         NavigationTarget {
411             file_id: src.file_id.original_file(db),
412             name: self.name(db).to_string().into(),
413             kind: SymbolKind::TypeParam,
414             full_range,
415             focus_range,
416             container_name: None,
417             description: None,
418             docs: None,
419         }
420     }
421 }
422
423 impl ToNav for hir::LifetimeParam {
424     fn to_nav(&self, db: &RootDatabase) -> NavigationTarget {
425         let src = self.source(db);
426         let full_range = src.value.syntax().text_range();
427         NavigationTarget {
428             file_id: src.file_id.original_file(db),
429             name: self.name(db).to_string().into(),
430             kind: SymbolKind::LifetimeParam,
431             full_range,
432             focus_range: Some(full_range),
433             container_name: None,
434             description: None,
435             docs: None,
436         }
437     }
438 }
439
440 /// Get a description of a symbol.
441 ///
442 /// e.g. `struct Name`, `enum Name`, `fn Name`
443 pub(crate) fn description_from_symbol(db: &RootDatabase, symbol: &FileSymbol) -> Option<String> {
444     let parse = db.parse(symbol.file_id);
445     let node = symbol.ptr.to_node(parse.tree().syntax());
446
447     match_ast! {
448         match node {
449             ast::Fn(it) => it.short_label(),
450             ast::Struct(it) => it.short_label(),
451             ast::Enum(it) => it.short_label(),
452             ast::Trait(it) => it.short_label(),
453             ast::Module(it) => it.short_label(),
454             ast::TypeAlias(it) => it.short_label(),
455             ast::Const(it) => it.short_label(),
456             ast::Static(it) => it.short_label(),
457             ast::RecordField(it) => it.short_label(),
458             ast::Variant(it) => it.short_label(),
459             _ => None,
460         }
461     }
462 }
463
464 #[cfg(test)]
465 mod tests {
466     use expect_test::expect;
467
468     use crate::{fixture, Query};
469
470     #[test]
471     fn test_nav_for_symbol() {
472         let (analysis, _) = fixture::file(
473             r#"
474 enum FooInner { }
475 fn foo() { enum FooInner { } }
476 "#,
477         );
478
479         let navs = analysis.symbol_search(Query::new("FooInner".to_string())).unwrap();
480         expect![[r#"
481             [
482                 NavigationTarget {
483                     file_id: FileId(
484                         0,
485                     ),
486                     full_range: 0..17,
487                     focus_range: Some(
488                         5..13,
489                     ),
490                     name: "FooInner",
491                     kind: Enum,
492                     container_name: None,
493                     description: Some(
494                         "enum FooInner",
495                     ),
496                     docs: None,
497                 },
498                 NavigationTarget {
499                     file_id: FileId(
500                         0,
501                     ),
502                     full_range: 29..46,
503                     focus_range: Some(
504                         34..42,
505                     ),
506                     name: "FooInner",
507                     kind: Enum,
508                     container_name: Some(
509                         "foo",
510                     ),
511                     description: Some(
512                         "enum FooInner",
513                     ),
514                     docs: None,
515                 },
516             ]
517         "#]]
518         .assert_debug_eq(&navs);
519     }
520
521     #[test]
522     fn test_world_symbols_are_case_sensitive() {
523         let (analysis, _) = fixture::file(
524             r#"
525 fn foo() {}
526 struct Foo;
527 "#,
528         );
529
530         let navs = analysis.symbol_search(Query::new("foo".to_string())).unwrap();
531         assert_eq!(navs.len(), 2)
532     }
533 }