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