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