]> git.lizzy.rs Git - rust.git/blob - crates/ide/src/display/navigation_target.rs
Merge #6769
[rust.git] / crates / ide / src / display / navigation_target.rs
1 //! FIXME: write short doc here
2
3 use either::Either;
4 use hir::{AssocItem, FieldSource, HasSource, InFile, ModuleSource};
5 use ide_db::base_db::{FileId, SourceDatabase};
6 use ide_db::{defs::Definition, RootDatabase};
7 use syntax::{
8     ast::{self, DocCommentsOwner, NameOwner},
9     match_ast, AstNode, SmolStr,
10     SyntaxKind::{self, IDENT_PAT, 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<String>,
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 = src.value.doc_comment_text();
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     /// Allows `NavigationTarget` to be created from a `DocCommentsOwner` and a `NameOwner`
121     pub(crate) fn from_doc_commented(
122         db: &RootDatabase,
123         named: InFile<&dyn ast::NameOwner>,
124         node: InFile<&dyn ast::DocCommentsOwner>,
125     ) -> NavigationTarget {
126         let name =
127             named.value.name().map(|it| it.text().clone()).unwrap_or_else(|| SmolStr::new("_"));
128         let frange = node.map(|it| it.syntax()).original_file_range(db);
129
130         NavigationTarget::from_syntax(
131             frange.file_id,
132             name,
133             None,
134             frange.range,
135             node.value.syntax().kind(),
136         )
137     }
138
139     fn from_syntax(
140         file_id: FileId,
141         name: SmolStr,
142         focus_range: Option<TextRange>,
143         full_range: TextRange,
144         kind: SyntaxKind,
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: self.kind,
165             full_range: self.range,
166             focus_range: self.name_range,
167             container_name: self.container_name.clone(),
168             description: description_from_symbol(db, self),
169             docs: docs_from_symbol(db, self),
170         }
171     }
172 }
173
174 impl TryToNav for Definition {
175     fn try_to_nav(&self, db: &RootDatabase) -> Option<NavigationTarget> {
176         match self {
177             Definition::Macro(it) => Some(it.to_nav(db)),
178             Definition::Field(it) => Some(it.to_nav(db)),
179             Definition::ModuleDef(it) => it.try_to_nav(db),
180             Definition::SelfType(it) => Some(it.to_nav(db)),
181             Definition::Local(it) => Some(it.to_nav(db)),
182             Definition::TypeParam(it) => Some(it.to_nav(db)),
183         }
184     }
185 }
186
187 impl TryToNav for hir::ModuleDef {
188     fn try_to_nav(&self, db: &RootDatabase) -> Option<NavigationTarget> {
189         let res = match self {
190             hir::ModuleDef::Module(it) => it.to_nav(db),
191             hir::ModuleDef::Function(it) => it.to_nav(db),
192             hir::ModuleDef::Adt(it) => it.to_nav(db),
193             hir::ModuleDef::EnumVariant(it) => it.to_nav(db),
194             hir::ModuleDef::Const(it) => it.to_nav(db),
195             hir::ModuleDef::Static(it) => it.to_nav(db),
196             hir::ModuleDef::Trait(it) => it.to_nav(db),
197             hir::ModuleDef::TypeAlias(it) => it.to_nav(db),
198             hir::ModuleDef::BuiltinType(_) => return None,
199         };
200         Some(res)
201     }
202 }
203
204 pub(crate) trait ToNavFromAst {}
205 impl ToNavFromAst for hir::Function {}
206 impl ToNavFromAst for hir::Const {}
207 impl ToNavFromAst for hir::Static {}
208 impl ToNavFromAst for hir::Struct {}
209 impl ToNavFromAst for hir::Enum {}
210 impl ToNavFromAst for hir::EnumVariant {}
211 impl ToNavFromAst for hir::Union {}
212 impl ToNavFromAst for hir::TypeAlias {}
213 impl ToNavFromAst for hir::Trait {}
214
215 impl<D> ToNav for D
216 where
217     D: HasSource + ToNavFromAst + Copy,
218     D::Ast: ast::DocCommentsOwner + ast::NameOwner + ShortLabel,
219 {
220     fn to_nav(&self, db: &RootDatabase) -> NavigationTarget {
221         let src = self.source(db);
222         let mut res =
223             NavigationTarget::from_named(db, src.as_ref().map(|it| it as &dyn ast::NameOwner));
224         res.docs = src.value.doc_comment_text();
225         res.description = src.value.short_label();
226         res
227     }
228 }
229
230 impl ToNav for hir::Module {
231     fn to_nav(&self, db: &RootDatabase) -> NavigationTarget {
232         let src = self.definition_source(db);
233         let name = self.name(db).map(|it| it.to_string().into()).unwrap_or_default();
234         let (syntax, focus) = match &src.value {
235             ModuleSource::SourceFile(node) => (node.syntax(), None),
236             ModuleSource::Module(node) => {
237                 (node.syntax(), node.name().map(|it| it.syntax().text_range()))
238             }
239         };
240         let frange = src.with_value(syntax).original_file_range(db);
241         NavigationTarget::from_syntax(frange.file_id, name, focus, frange.range, syntax.kind())
242     }
243 }
244
245 impl ToNav for hir::ImplDef {
246     fn to_nav(&self, db: &RootDatabase) -> NavigationTarget {
247         let src = self.source(db);
248         let derive_attr = self.is_builtin_derive(db);
249         let frange = if let Some(item) = &derive_attr {
250             item.syntax().original_file_range(db)
251         } else {
252             src.as_ref().map(|it| it.syntax()).original_file_range(db)
253         };
254         let focus_range = if derive_attr.is_some() {
255             None
256         } else {
257             src.value.self_ty().map(|ty| src.with_value(ty.syntax()).original_file_range(db).range)
258         };
259
260         NavigationTarget::from_syntax(
261             frange.file_id,
262             "impl".into(),
263             focus_range,
264             frange.range,
265             src.value.syntax().kind(),
266         )
267     }
268 }
269
270 impl ToNav for hir::Field {
271     fn to_nav(&self, db: &RootDatabase) -> NavigationTarget {
272         let src = self.source(db);
273
274         match &src.value {
275             FieldSource::Named(it) => {
276                 let mut res = NavigationTarget::from_named(db, src.with_value(it));
277                 res.docs = it.doc_comment_text();
278                 res.description = it.short_label();
279                 res
280             }
281             FieldSource::Pos(it) => {
282                 let frange = src.with_value(it.syntax()).original_file_range(db);
283                 NavigationTarget::from_syntax(
284                     frange.file_id,
285                     "".into(),
286                     None,
287                     frange.range,
288                     it.syntax().kind(),
289                 )
290             }
291         }
292     }
293 }
294
295 impl ToNav for hir::MacroDef {
296     fn to_nav(&self, db: &RootDatabase) -> NavigationTarget {
297         let src = self.source(db);
298         log::debug!("nav target {:#?}", src.value.syntax());
299         let mut res =
300             NavigationTarget::from_named(db, src.as_ref().map(|it| it as &dyn ast::NameOwner));
301         res.docs = src.value.doc_comment_text();
302         res
303     }
304 }
305
306 impl ToNav for hir::Adt {
307     fn to_nav(&self, db: &RootDatabase) -> NavigationTarget {
308         match self {
309             hir::Adt::Struct(it) => it.to_nav(db),
310             hir::Adt::Union(it) => it.to_nav(db),
311             hir::Adt::Enum(it) => it.to_nav(db),
312         }
313     }
314 }
315
316 impl ToNav for hir::AssocItem {
317     fn to_nav(&self, db: &RootDatabase) -> NavigationTarget {
318         match self {
319             AssocItem::Function(it) => it.to_nav(db),
320             AssocItem::Const(it) => it.to_nav(db),
321             AssocItem::TypeAlias(it) => it.to_nav(db),
322         }
323     }
324 }
325
326 impl ToNav for hir::Local {
327     fn to_nav(&self, db: &RootDatabase) -> NavigationTarget {
328         let src = self.source(db);
329         let node = match &src.value {
330             Either::Left(bind_pat) => {
331                 bind_pat.name().map_or_else(|| bind_pat.syntax().clone(), |it| it.syntax().clone())
332             }
333             Either::Right(it) => it.syntax().clone(),
334         };
335         let full_range = src.with_value(&node).original_file_range(db);
336         let name = match self.name(db) {
337             Some(it) => it.to_string().into(),
338             None => "".into(),
339         };
340         NavigationTarget {
341             file_id: full_range.file_id,
342             name,
343             kind: IDENT_PAT,
344             full_range: full_range.range,
345             focus_range: None,
346             container_name: None,
347             description: None,
348             docs: None,
349         }
350     }
351 }
352
353 impl ToNav for hir::TypeParam {
354     fn to_nav(&self, db: &RootDatabase) -> NavigationTarget {
355         let src = self.source(db);
356         let full_range = match &src.value {
357             Either::Left(it) => it.syntax().text_range(),
358             Either::Right(it) => it.syntax().text_range(),
359         };
360         let focus_range = match &src.value {
361             Either::Left(_) => None,
362             Either::Right(it) => it.name().map(|it| it.syntax().text_range()),
363         };
364         NavigationTarget {
365             file_id: src.file_id.original_file(db),
366             name: self.name(db).to_string().into(),
367             kind: TYPE_PARAM,
368             full_range,
369             focus_range,
370             container_name: None,
371             description: None,
372             docs: None,
373         }
374     }
375 }
376
377 pub(crate) fn docs_from_symbol(db: &RootDatabase, symbol: &FileSymbol) -> Option<String> {
378     let parse = db.parse(symbol.file_id);
379     let node = symbol.ptr.to_node(parse.tree().syntax());
380
381     match_ast! {
382         match node {
383             ast::Fn(it) => it.doc_comment_text(),
384             ast::Struct(it) => it.doc_comment_text(),
385             ast::Enum(it) => it.doc_comment_text(),
386             ast::Trait(it) => it.doc_comment_text(),
387             ast::Module(it) => it.doc_comment_text(),
388             ast::TypeAlias(it) => it.doc_comment_text(),
389             ast::Const(it) => it.doc_comment_text(),
390             ast::Static(it) => it.doc_comment_text(),
391             ast::RecordField(it) => it.doc_comment_text(),
392             ast::Variant(it) => it.doc_comment_text(),
393             ast::MacroCall(it) => it.doc_comment_text(),
394             _ => None,
395         }
396     }
397 }
398
399 /// Get a description of a symbol.
400 ///
401 /// e.g. `struct Name`, `enum Name`, `fn Name`
402 pub(crate) fn description_from_symbol(db: &RootDatabase, symbol: &FileSymbol) -> Option<String> {
403     let parse = db.parse(symbol.file_id);
404     let node = symbol.ptr.to_node(parse.tree().syntax());
405
406     match_ast! {
407         match node {
408             ast::Fn(it) => it.short_label(),
409             ast::Struct(it) => it.short_label(),
410             ast::Enum(it) => it.short_label(),
411             ast::Trait(it) => it.short_label(),
412             ast::Module(it) => it.short_label(),
413             ast::TypeAlias(it) => it.short_label(),
414             ast::Const(it) => it.short_label(),
415             ast::Static(it) => it.short_label(),
416             ast::RecordField(it) => it.short_label(),
417             ast::Variant(it) => it.short_label(),
418             _ => None,
419         }
420     }
421 }
422
423 #[cfg(test)]
424 mod tests {
425     use expect_test::expect;
426
427     use crate::{fixture, Query};
428
429     #[test]
430     fn test_nav_for_symbol() {
431         let (analysis, _) = fixture::file(
432             r#"
433 enum FooInner { }
434 fn foo() { enum FooInner { } }
435 "#,
436         );
437
438         let navs = analysis.symbol_search(Query::new("FooInner".to_string())).unwrap();
439         expect![[r#"
440             [
441                 NavigationTarget {
442                     file_id: FileId(
443                         0,
444                     ),
445                     full_range: 0..17,
446                     focus_range: Some(
447                         5..13,
448                     ),
449                     name: "FooInner",
450                     kind: ENUM,
451                     container_name: None,
452                     description: Some(
453                         "enum FooInner",
454                     ),
455                     docs: None,
456                 },
457                 NavigationTarget {
458                     file_id: FileId(
459                         0,
460                     ),
461                     full_range: 29..46,
462                     focus_range: Some(
463                         34..42,
464                     ),
465                     name: "FooInner",
466                     kind: ENUM,
467                     container_name: Some(
468                         "foo",
469                     ),
470                     description: Some(
471                         "enum FooInner",
472                     ),
473                     docs: None,
474                 },
475             ]
476         "#]]
477         .assert_debug_eq(&navs);
478     }
479
480     #[test]
481     fn test_world_symbols_are_case_sensitive() {
482         let (analysis, _) = fixture::file(
483             r#"
484 fn foo() {}
485 struct Foo;
486 "#,
487         );
488
489         let navs = analysis.symbol_search(Query::new("foo".to_string())).unwrap();
490         assert_eq!(navs.len(), 2)
491     }
492 }