]> git.lizzy.rs Git - rust.git/blob - crates/ra_ide/src/display/navigation_target.rs
New runnables API
[rust.git] / crates / ra_ide / src / display / navigation_target.rs
1 //! FIXME: write short doc here
2
3 use either::Either;
4 use hir::{original_range, AssocItem, FieldSource, HasSource, InFile, ModuleSource};
5 use ra_db::{FileId, SourceDatabase};
6 use ra_ide_db::{defs::Definition, RootDatabase};
7 use ra_syntax::{
8     ast::{self, DocCommentsOwner, NameOwner},
9     match_ast, AstNode, SmolStr,
10     SyntaxKind::{self, BIND_PAT, TYPE_PARAM},
11     TextRange,
12 };
13
14 use crate::{FileRange, 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     // FIXME: use FileRange?
26     file_id: FileId,
27     full_range: TextRange,
28     name: SmolStr,
29     kind: SyntaxKind,
30     focus_range: Option<TextRange>,
31     container_name: Option<SmolStr>,
32     description: Option<String>,
33     docs: Option<String>,
34 }
35
36 pub(crate) trait ToNav {
37     fn to_nav(&self, db: &RootDatabase) -> NavigationTarget;
38 }
39
40 pub(crate) trait TryToNav {
41     fn try_to_nav(&self, db: &RootDatabase) -> Option<NavigationTarget>;
42 }
43
44 impl NavigationTarget {
45     /// When `focus_range` is specified, returns it. otherwise
46     /// returns `full_range`
47     pub fn range(&self) -> TextRange {
48         self.focus_range.unwrap_or(self.full_range)
49     }
50
51     pub fn name(&self) -> &SmolStr {
52         &self.name
53     }
54
55     pub fn container_name(&self) -> Option<&SmolStr> {
56         self.container_name.as_ref()
57     }
58
59     pub fn kind(&self) -> SyntaxKind {
60         self.kind
61     }
62
63     pub fn file_id(&self) -> FileId {
64         self.file_id
65     }
66
67     pub fn file_range(&self) -> FileRange {
68         FileRange { file_id: self.file_id, range: self.full_range }
69     }
70
71     pub fn full_range(&self) -> TextRange {
72         self.full_range
73     }
74
75     pub fn docs(&self) -> Option<&str> {
76         self.docs.as_deref()
77     }
78
79     pub fn description(&self) -> Option<&str> {
80         self.description.as_deref()
81     }
82
83     /// A "most interesting" range withing the `full_range`.
84     ///
85     /// Typically, `full_range` is the whole syntax node,
86     /// including doc comments, and `focus_range` is the range of the identifier.
87     pub fn focus_range(&self) -> Option<TextRange> {
88         self.focus_range
89     }
90
91     pub(crate) fn from_module_to_decl(db: &RootDatabase, module: hir::Module) -> NavigationTarget {
92         let name = module.name(db).map(|it| it.to_string().into()).unwrap_or_default();
93         if let Some(src) = module.declaration_source(db) {
94             let frange = original_range(db, src.as_ref().map(|it| it.syntax()));
95             let mut res = NavigationTarget::from_syntax(
96                 frange.file_id,
97                 name,
98                 None,
99                 frange.range,
100                 src.value.syntax().kind(),
101             );
102             res.docs = src.value.doc_comment_text();
103             res.description = src.value.short_label();
104             return res;
105         }
106         module.to_nav(db)
107     }
108
109     #[cfg(test)]
110     pub(crate) fn assert_match(&self, expected: &str) {
111         let actual = self.debug_render();
112         test_utils::assert_eq_text!(expected.trim(), actual.trim(),);
113     }
114
115     #[cfg(test)]
116     pub(crate) fn debug_render(&self) -> String {
117         let mut buf = format!(
118             "{} {:?} {:?} {:?}",
119             self.name(),
120             self.kind(),
121             self.file_id(),
122             self.full_range()
123         );
124         if let Some(focus_range) = self.focus_range() {
125             buf.push_str(&format!(" {:?}", focus_range))
126         }
127         if let Some(container_name) = self.container_name() {
128             buf.push_str(&format!(" {}", container_name))
129         }
130         buf
131     }
132
133     /// Allows `NavigationTarget` to be created from a `NameOwner`
134     pub(crate) fn from_named(
135         db: &RootDatabase,
136         node: InFile<&dyn ast::NameOwner>,
137     ) -> NavigationTarget {
138         //FIXME: use `_` instead of empty string
139         let name = node.value.name().map(|it| it.text().clone()).unwrap_or_default();
140         let focus_range =
141             node.value.name().map(|it| original_range(db, node.with_value(it.syntax())).range);
142         let frange = original_range(db, node.map(|it| it.syntax()));
143
144         NavigationTarget::from_syntax(
145             frange.file_id,
146             name,
147             focus_range,
148             frange.range,
149             node.value.syntax().kind(),
150         )
151     }
152
153     fn from_syntax(
154         file_id: FileId,
155         name: SmolStr,
156         focus_range: Option<TextRange>,
157         full_range: TextRange,
158         kind: SyntaxKind,
159     ) -> NavigationTarget {
160         NavigationTarget {
161             file_id,
162             name,
163             kind,
164             full_range,
165             focus_range,
166             container_name: None,
167             description: None,
168             docs: None,
169         }
170     }
171 }
172
173 impl ToNav for FileSymbol {
174     fn to_nav(&self, db: &RootDatabase) -> NavigationTarget {
175         NavigationTarget {
176             file_id: self.file_id,
177             name: self.name.clone(),
178             kind: self.kind,
179             full_range: self.range,
180             focus_range: self.name_range,
181             container_name: self.container_name.clone(),
182             description: description_from_symbol(db, self),
183             docs: docs_from_symbol(db, self),
184         }
185     }
186 }
187
188 impl TryToNav for Definition {
189     fn try_to_nav(&self, db: &RootDatabase) -> Option<NavigationTarget> {
190         match self {
191             Definition::Macro(it) => Some(it.to_nav(db)),
192             Definition::Field(it) => Some(it.to_nav(db)),
193             Definition::ModuleDef(it) => it.try_to_nav(db),
194             Definition::SelfType(it) => Some(it.to_nav(db)),
195             Definition::Local(it) => Some(it.to_nav(db)),
196             Definition::TypeParam(it) => Some(it.to_nav(db)),
197         }
198     }
199 }
200
201 impl TryToNav for hir::ModuleDef {
202     fn try_to_nav(&self, db: &RootDatabase) -> Option<NavigationTarget> {
203         let res = match self {
204             hir::ModuleDef::Module(it) => it.to_nav(db),
205             hir::ModuleDef::Function(it) => it.to_nav(db),
206             hir::ModuleDef::Adt(it) => it.to_nav(db),
207             hir::ModuleDef::EnumVariant(it) => it.to_nav(db),
208             hir::ModuleDef::Const(it) => it.to_nav(db),
209             hir::ModuleDef::Static(it) => it.to_nav(db),
210             hir::ModuleDef::Trait(it) => it.to_nav(db),
211             hir::ModuleDef::TypeAlias(it) => it.to_nav(db),
212             hir::ModuleDef::BuiltinType(_) => return None,
213         };
214         Some(res)
215     }
216 }
217
218 pub(crate) trait ToNavFromAst {}
219 impl ToNavFromAst for hir::Function {}
220 impl ToNavFromAst for hir::Const {}
221 impl ToNavFromAst for hir::Static {}
222 impl ToNavFromAst for hir::Struct {}
223 impl ToNavFromAst for hir::Enum {}
224 impl ToNavFromAst for hir::EnumVariant {}
225 impl ToNavFromAst for hir::Union {}
226 impl ToNavFromAst for hir::TypeAlias {}
227 impl ToNavFromAst for hir::Trait {}
228
229 impl<D> ToNav for D
230 where
231     D: HasSource + ToNavFromAst + Copy,
232     D::Ast: ast::DocCommentsOwner + ast::NameOwner + ShortLabel,
233 {
234     fn to_nav(&self, db: &RootDatabase) -> NavigationTarget {
235         let src = self.source(db);
236         let mut res =
237             NavigationTarget::from_named(db, src.as_ref().map(|it| it as &dyn ast::NameOwner));
238         res.docs = src.value.doc_comment_text();
239         res.description = src.value.short_label();
240         res
241     }
242 }
243
244 impl ToNav for hir::Module {
245     fn to_nav(&self, db: &RootDatabase) -> NavigationTarget {
246         let src = self.definition_source(db);
247         let name = self.name(db).map(|it| it.to_string().into()).unwrap_or_default();
248         let (syntax, focus) = match &src.value {
249             ModuleSource::SourceFile(node) => (node.syntax(), None),
250             ModuleSource::Module(node) => {
251                 (node.syntax(), node.name().map(|it| it.syntax().text_range()))
252             }
253         };
254         let frange = original_range(db, src.with_value(syntax));
255         NavigationTarget::from_syntax(frange.file_id, name, focus, frange.range, syntax.kind())
256     }
257 }
258
259 impl ToNav for hir::ImplDef {
260     fn to_nav(&self, db: &RootDatabase) -> NavigationTarget {
261         let src = self.source(db);
262         let frange = if let Some(item) = self.is_builtin_derive(db) {
263             original_range(db, item.syntax())
264         } else {
265             original_range(db, src.as_ref().map(|it| it.syntax()))
266         };
267
268         NavigationTarget::from_syntax(
269             frange.file_id,
270             "impl".into(),
271             None,
272             frange.range,
273             src.value.syntax().kind(),
274         )
275     }
276 }
277
278 impl ToNav for hir::Field {
279     fn to_nav(&self, db: &RootDatabase) -> NavigationTarget {
280         let src = self.source(db);
281
282         match &src.value {
283             FieldSource::Named(it) => {
284                 let mut res = NavigationTarget::from_named(db, src.with_value(it));
285                 res.docs = it.doc_comment_text();
286                 res.description = it.short_label();
287                 res
288             }
289             FieldSource::Pos(it) => {
290                 let frange = original_range(db, src.with_value(it.syntax()));
291                 NavigationTarget::from_syntax(
292                     frange.file_id,
293                     "".into(),
294                     None,
295                     frange.range,
296                     it.syntax().kind(),
297                 )
298             }
299         }
300     }
301 }
302
303 impl ToNav for hir::MacroDef {
304     fn to_nav(&self, db: &RootDatabase) -> NavigationTarget {
305         let src = self.source(db);
306         log::debug!("nav target {:#?}", src.value.syntax());
307         let mut res =
308             NavigationTarget::from_named(db, src.as_ref().map(|it| it as &dyn ast::NameOwner));
309         res.docs = src.value.doc_comment_text();
310         res
311     }
312 }
313
314 impl ToNav for hir::Adt {
315     fn to_nav(&self, db: &RootDatabase) -> NavigationTarget {
316         match self {
317             hir::Adt::Struct(it) => it.to_nav(db),
318             hir::Adt::Union(it) => it.to_nav(db),
319             hir::Adt::Enum(it) => it.to_nav(db),
320         }
321     }
322 }
323
324 impl ToNav for hir::AssocItem {
325     fn to_nav(&self, db: &RootDatabase) -> NavigationTarget {
326         match self {
327             AssocItem::Function(it) => it.to_nav(db),
328             AssocItem::Const(it) => it.to_nav(db),
329             AssocItem::TypeAlias(it) => it.to_nav(db),
330         }
331     }
332 }
333
334 impl ToNav for hir::Local {
335     fn to_nav(&self, db: &RootDatabase) -> NavigationTarget {
336         let src = self.source(db);
337         let node = match &src.value {
338             Either::Left(bind_pat) => {
339                 bind_pat.name().map_or_else(|| bind_pat.syntax().clone(), |it| it.syntax().clone())
340             }
341             Either::Right(it) => it.syntax().clone(),
342         };
343         let full_range = original_range(db, src.with_value(&node));
344         let name = match self.name(db) {
345             Some(it) => it.to_string().into(),
346             None => "".into(),
347         };
348         NavigationTarget {
349             file_id: full_range.file_id,
350             name,
351             kind: BIND_PAT,
352             full_range: full_range.range,
353             focus_range: None,
354             container_name: None,
355             description: None,
356             docs: None,
357         }
358     }
359 }
360
361 impl ToNav for hir::TypeParam {
362     fn to_nav(&self, db: &RootDatabase) -> NavigationTarget {
363         let src = self.source(db);
364         let full_range = match &src.value {
365             Either::Left(it) => it.syntax().text_range(),
366             Either::Right(it) => it.syntax().text_range(),
367         };
368         let focus_range = match &src.value {
369             Either::Left(_) => None,
370             Either::Right(it) => it.name().map(|it| it.syntax().text_range()),
371         };
372         NavigationTarget {
373             file_id: src.file_id.original_file(db),
374             name: self.name(db).to_string().into(),
375             kind: TYPE_PARAM,
376             full_range,
377             focus_range,
378             container_name: None,
379             description: None,
380             docs: None,
381         }
382     }
383 }
384
385 pub(crate) fn docs_from_symbol(db: &RootDatabase, symbol: &FileSymbol) -> Option<String> {
386     let parse = db.parse(symbol.file_id);
387     let node = symbol.ptr.to_node(parse.tree().syntax());
388
389     match_ast! {
390         match node {
391             ast::FnDef(it) => it.doc_comment_text(),
392             ast::StructDef(it) => it.doc_comment_text(),
393             ast::EnumDef(it) => it.doc_comment_text(),
394             ast::TraitDef(it) => it.doc_comment_text(),
395             ast::Module(it) => it.doc_comment_text(),
396             ast::TypeAliasDef(it) => it.doc_comment_text(),
397             ast::ConstDef(it) => it.doc_comment_text(),
398             ast::StaticDef(it) => it.doc_comment_text(),
399             ast::RecordFieldDef(it) => it.doc_comment_text(),
400             ast::EnumVariant(it) => it.doc_comment_text(),
401             ast::MacroCall(it) => it.doc_comment_text(),
402             _ => None,
403         }
404     }
405 }
406
407 /// Get a description of a symbol.
408 ///
409 /// e.g. `struct Name`, `enum Name`, `fn Name`
410 pub(crate) fn description_from_symbol(db: &RootDatabase, symbol: &FileSymbol) -> Option<String> {
411     let parse = db.parse(symbol.file_id);
412     let node = symbol.ptr.to_node(parse.tree().syntax());
413
414     match_ast! {
415         match node {
416             ast::FnDef(it) => it.short_label(),
417             ast::StructDef(it) => it.short_label(),
418             ast::EnumDef(it) => it.short_label(),
419             ast::TraitDef(it) => it.short_label(),
420             ast::Module(it) => it.short_label(),
421             ast::TypeAliasDef(it) => it.short_label(),
422             ast::ConstDef(it) => it.short_label(),
423             ast::StaticDef(it) => it.short_label(),
424             ast::RecordFieldDef(it) => it.short_label(),
425             ast::EnumVariant(it) => it.short_label(),
426             _ => None,
427         }
428     }
429 }