diff options
Diffstat (limited to 'crates/ra_ide_api/src/display/navigation_target.rs')
-rw-r--r-- | crates/ra_ide_api/src/display/navigation_target.rs | 329 |
1 files changed, 329 insertions, 0 deletions
diff --git a/crates/ra_ide_api/src/display/navigation_target.rs b/crates/ra_ide_api/src/display/navigation_target.rs new file mode 100644 index 000000000..3c518faf5 --- /dev/null +++ b/crates/ra_ide_api/src/display/navigation_target.rs | |||
@@ -0,0 +1,329 @@ | |||
1 | use ra_db::{FileId, SourceDatabase}; | ||
2 | use ra_syntax::{ | ||
3 | SyntaxNode, SyntaxNodePtr, AstNode, SmolStr, TextRange, TreeArc, | ||
4 | SyntaxKind::{self, NAME}, | ||
5 | ast::{self, NameOwner, VisibilityOwner, TypeAscriptionOwner}, | ||
6 | algo::visit::{visitor, Visitor}, | ||
7 | }; | ||
8 | use hir::{ModuleSource, FieldSource, Name, ImplItem}; | ||
9 | |||
10 | use crate::{FileSymbol, db::RootDatabase}; | ||
11 | |||
12 | /// `NavigationTarget` represents and element in the editor's UI which you can | ||
13 | /// click on to navigate to a particular piece of code. | ||
14 | /// | ||
15 | /// Typically, a `NavigationTarget` corresponds to some element in the source | ||
16 | /// code, like a function or a struct, but this is not strictly required. | ||
17 | #[derive(Debug, Clone)] | ||
18 | pub struct NavigationTarget { | ||
19 | file_id: FileId, | ||
20 | name: SmolStr, | ||
21 | kind: SyntaxKind, | ||
22 | full_range: TextRange, | ||
23 | focus_range: Option<TextRange>, | ||
24 | container_name: Option<SmolStr>, | ||
25 | } | ||
26 | |||
27 | impl NavigationTarget { | ||
28 | /// When `focus_range` is specified, returns it. otherwise | ||
29 | /// returns `full_range` | ||
30 | pub fn range(&self) -> TextRange { | ||
31 | self.focus_range.unwrap_or(self.full_range) | ||
32 | } | ||
33 | |||
34 | pub fn name(&self) -> &SmolStr { | ||
35 | &self.name | ||
36 | } | ||
37 | |||
38 | pub fn container_name(&self) -> Option<&SmolStr> { | ||
39 | self.container_name.as_ref() | ||
40 | } | ||
41 | |||
42 | pub fn kind(&self) -> SyntaxKind { | ||
43 | self.kind | ||
44 | } | ||
45 | |||
46 | pub fn file_id(&self) -> FileId { | ||
47 | self.file_id | ||
48 | } | ||
49 | |||
50 | pub fn full_range(&self) -> TextRange { | ||
51 | self.full_range | ||
52 | } | ||
53 | |||
54 | /// A "most interesting" range withing the `full_range`. | ||
55 | /// | ||
56 | /// Typically, `full_range` is the whole syntax node, | ||
57 | /// including doc comments, and `focus_range` is the range of the identifier. | ||
58 | pub fn focus_range(&self) -> Option<TextRange> { | ||
59 | self.focus_range | ||
60 | } | ||
61 | |||
62 | pub(crate) fn from_bind_pat(file_id: FileId, pat: &ast::BindPat) -> NavigationTarget { | ||
63 | NavigationTarget::from_named(file_id, pat) | ||
64 | } | ||
65 | |||
66 | pub(crate) fn from_symbol(symbol: FileSymbol) -> NavigationTarget { | ||
67 | NavigationTarget { | ||
68 | file_id: symbol.file_id, | ||
69 | name: symbol.name.clone(), | ||
70 | kind: symbol.ptr.kind(), | ||
71 | full_range: symbol.ptr.range(), | ||
72 | focus_range: symbol.name_range, | ||
73 | container_name: symbol.container_name.clone(), | ||
74 | } | ||
75 | } | ||
76 | |||
77 | pub(crate) fn from_scope_entry( | ||
78 | file_id: FileId, | ||
79 | name: Name, | ||
80 | ptr: SyntaxNodePtr, | ||
81 | ) -> NavigationTarget { | ||
82 | NavigationTarget { | ||
83 | file_id, | ||
84 | name: name.to_string().into(), | ||
85 | full_range: ptr.range(), | ||
86 | focus_range: None, | ||
87 | kind: NAME, | ||
88 | container_name: None, | ||
89 | } | ||
90 | } | ||
91 | |||
92 | pub(crate) fn from_module(db: &RootDatabase, module: hir::Module) -> NavigationTarget { | ||
93 | let (file_id, source) = module.definition_source(db); | ||
94 | let file_id = file_id.as_original_file(); | ||
95 | let name = module.name(db).map(|it| it.to_string().into()).unwrap_or_default(); | ||
96 | match source { | ||
97 | ModuleSource::SourceFile(node) => { | ||
98 | NavigationTarget::from_syntax(file_id, name, None, node.syntax()) | ||
99 | } | ||
100 | ModuleSource::Module(node) => { | ||
101 | NavigationTarget::from_syntax(file_id, name, None, node.syntax()) | ||
102 | } | ||
103 | } | ||
104 | } | ||
105 | |||
106 | pub(crate) fn from_module_to_decl(db: &RootDatabase, module: hir::Module) -> NavigationTarget { | ||
107 | let name = module.name(db).map(|it| it.to_string().into()).unwrap_or_default(); | ||
108 | if let Some((file_id, source)) = module.declaration_source(db) { | ||
109 | let file_id = file_id.as_original_file(); | ||
110 | return NavigationTarget::from_syntax(file_id, name, None, source.syntax()); | ||
111 | } | ||
112 | NavigationTarget::from_module(db, module) | ||
113 | } | ||
114 | |||
115 | pub(crate) fn from_function(db: &RootDatabase, func: hir::Function) -> NavigationTarget { | ||
116 | let (file_id, fn_def) = func.source(db); | ||
117 | NavigationTarget::from_named(file_id.original_file(db), &*fn_def) | ||
118 | } | ||
119 | |||
120 | pub(crate) fn from_field(db: &RootDatabase, field: hir::StructField) -> NavigationTarget { | ||
121 | let (file_id, field) = field.source(db); | ||
122 | let file_id = file_id.original_file(db); | ||
123 | match field { | ||
124 | FieldSource::Named(it) => NavigationTarget::from_named(file_id, &*it), | ||
125 | FieldSource::Pos(it) => { | ||
126 | NavigationTarget::from_syntax(file_id, "".into(), None, it.syntax()) | ||
127 | } | ||
128 | } | ||
129 | } | ||
130 | |||
131 | pub(crate) fn from_adt_def(db: &RootDatabase, adt_def: hir::AdtDef) -> NavigationTarget { | ||
132 | match adt_def { | ||
133 | hir::AdtDef::Struct(s) => { | ||
134 | let (file_id, node) = s.source(db); | ||
135 | NavigationTarget::from_named(file_id.original_file(db), &*node) | ||
136 | } | ||
137 | hir::AdtDef::Enum(s) => { | ||
138 | let (file_id, node) = s.source(db); | ||
139 | NavigationTarget::from_named(file_id.original_file(db), &*node) | ||
140 | } | ||
141 | } | ||
142 | } | ||
143 | |||
144 | pub(crate) fn from_def(db: &RootDatabase, module_def: hir::ModuleDef) -> NavigationTarget { | ||
145 | match module_def { | ||
146 | hir::ModuleDef::Module(module) => NavigationTarget::from_module(db, module), | ||
147 | hir::ModuleDef::Function(func) => NavigationTarget::from_function(db, func), | ||
148 | hir::ModuleDef::Struct(s) => { | ||
149 | let (file_id, node) = s.source(db); | ||
150 | NavigationTarget::from_named(file_id.original_file(db), &*node) | ||
151 | } | ||
152 | hir::ModuleDef::Const(s) => { | ||
153 | let (file_id, node) = s.source(db); | ||
154 | NavigationTarget::from_named(file_id.original_file(db), &*node) | ||
155 | } | ||
156 | hir::ModuleDef::Static(s) => { | ||
157 | let (file_id, node) = s.source(db); | ||
158 | NavigationTarget::from_named(file_id.original_file(db), &*node) | ||
159 | } | ||
160 | hir::ModuleDef::Enum(e) => { | ||
161 | let (file_id, node) = e.source(db); | ||
162 | NavigationTarget::from_named(file_id.original_file(db), &*node) | ||
163 | } | ||
164 | hir::ModuleDef::EnumVariant(var) => { | ||
165 | let (file_id, node) = var.source(db); | ||
166 | NavigationTarget::from_named(file_id.original_file(db), &*node) | ||
167 | } | ||
168 | hir::ModuleDef::Trait(e) => { | ||
169 | let (file_id, node) = e.source(db); | ||
170 | NavigationTarget::from_named(file_id.original_file(db), &*node) | ||
171 | } | ||
172 | hir::ModuleDef::TypeAlias(e) => { | ||
173 | let (file_id, node) = e.source(db); | ||
174 | NavigationTarget::from_named(file_id.original_file(db), &*node) | ||
175 | } | ||
176 | } | ||
177 | } | ||
178 | |||
179 | pub(crate) fn from_impl_block( | ||
180 | db: &RootDatabase, | ||
181 | impl_block: hir::ImplBlock, | ||
182 | ) -> NavigationTarget { | ||
183 | let (file_id, node) = impl_block.source(db); | ||
184 | NavigationTarget::from_syntax( | ||
185 | file_id.as_original_file(), | ||
186 | "impl".into(), | ||
187 | None, | ||
188 | node.syntax(), | ||
189 | ) | ||
190 | } | ||
191 | |||
192 | pub(crate) fn from_impl_item(db: &RootDatabase, impl_item: hir::ImplItem) -> NavigationTarget { | ||
193 | match impl_item { | ||
194 | ImplItem::Method(f) => NavigationTarget::from_function(db, f), | ||
195 | ImplItem::Const(c) => { | ||
196 | let (file_id, node) = c.source(db); | ||
197 | NavigationTarget::from_named(file_id.original_file(db), &*node) | ||
198 | } | ||
199 | ImplItem::TypeAlias(a) => { | ||
200 | let (file_id, node) = a.source(db); | ||
201 | NavigationTarget::from_named(file_id.original_file(db), &*node) | ||
202 | } | ||
203 | } | ||
204 | } | ||
205 | |||
206 | #[cfg(test)] | ||
207 | pub(crate) fn assert_match(&self, expected: &str) { | ||
208 | let actual = self.debug_render(); | ||
209 | test_utils::assert_eq_text!(expected.trim(), actual.trim(),); | ||
210 | } | ||
211 | |||
212 | #[cfg(test)] | ||
213 | pub(crate) fn debug_render(&self) -> String { | ||
214 | let mut buf = format!( | ||
215 | "{} {:?} {:?} {:?}", | ||
216 | self.name(), | ||
217 | self.kind(), | ||
218 | self.file_id(), | ||
219 | self.full_range() | ||
220 | ); | ||
221 | if let Some(focus_range) = self.focus_range() { | ||
222 | buf.push_str(&format!(" {:?}", focus_range)) | ||
223 | } | ||
224 | if let Some(container_name) = self.container_name() { | ||
225 | buf.push_str(&format!(" {}", container_name)) | ||
226 | } | ||
227 | buf | ||
228 | } | ||
229 | |||
230 | /// Allows `NavigationTarget` to be created from a `NameOwner` | ||
231 | pub(crate) fn from_named(file_id: FileId, node: &impl ast::NameOwner) -> NavigationTarget { | ||
232 | let name = node.name().map(|it| it.text().clone()).unwrap_or_default(); | ||
233 | let focus_range = node.name().map(|it| it.syntax().range()); | ||
234 | NavigationTarget::from_syntax(file_id, name, focus_range, node.syntax()) | ||
235 | } | ||
236 | |||
237 | fn from_syntax( | ||
238 | file_id: FileId, | ||
239 | name: SmolStr, | ||
240 | focus_range: Option<TextRange>, | ||
241 | node: &SyntaxNode, | ||
242 | ) -> NavigationTarget { | ||
243 | NavigationTarget { | ||
244 | file_id, | ||
245 | name, | ||
246 | kind: node.kind(), | ||
247 | full_range: node.range(), | ||
248 | focus_range, | ||
249 | // ptr: Some(LocalSyntaxPtr::new(node)), | ||
250 | container_name: None, | ||
251 | } | ||
252 | } | ||
253 | |||
254 | pub(crate) fn node(&self, db: &RootDatabase) -> Option<TreeArc<SyntaxNode>> { | ||
255 | let source_file = db.parse(self.file_id()); | ||
256 | let source_file = source_file.syntax(); | ||
257 | let node = source_file | ||
258 | .descendants() | ||
259 | .find(|node| node.kind() == self.kind() && node.range() == self.full_range())? | ||
260 | .to_owned(); | ||
261 | Some(node) | ||
262 | } | ||
263 | |||
264 | pub(crate) fn docs(&self, db: &RootDatabase) -> Option<String> { | ||
265 | let node = self.node(db)?; | ||
266 | fn doc_comments<N: ast::DocCommentsOwner>(node: &N) -> Option<String> { | ||
267 | node.doc_comment_text() | ||
268 | } | ||
269 | |||
270 | visitor() | ||
271 | .visit(doc_comments::<ast::FnDef>) | ||
272 | .visit(doc_comments::<ast::StructDef>) | ||
273 | .visit(doc_comments::<ast::EnumDef>) | ||
274 | .visit(doc_comments::<ast::TraitDef>) | ||
275 | .visit(doc_comments::<ast::Module>) | ||
276 | .visit(doc_comments::<ast::TypeAliasDef>) | ||
277 | .visit(doc_comments::<ast::ConstDef>) | ||
278 | .visit(doc_comments::<ast::StaticDef>) | ||
279 | .visit(doc_comments::<ast::NamedFieldDef>) | ||
280 | .visit(doc_comments::<ast::EnumVariant>) | ||
281 | .accept(&node)? | ||
282 | } | ||
283 | |||
284 | /// Get a description of this node. | ||
285 | /// | ||
286 | /// e.g. `struct Name`, `enum Name`, `fn Name` | ||
287 | pub(crate) fn description(&self, db: &RootDatabase) -> Option<String> { | ||
288 | // FIXME: After type inference is done, add type information to improve the output | ||
289 | let node = self.node(db)?; | ||
290 | |||
291 | fn visit_ascribed_node<T>(node: &T, prefix: &str) -> Option<String> | ||
292 | where | ||
293 | T: NameOwner + VisibilityOwner + TypeAscriptionOwner, | ||
294 | { | ||
295 | let mut string = visit_node(node, prefix)?; | ||
296 | |||
297 | if let Some(type_ref) = node.ascribed_type() { | ||
298 | string.push_str(": "); | ||
299 | type_ref.syntax().text().push_to(&mut string); | ||
300 | } | ||
301 | |||
302 | Some(string) | ||
303 | } | ||
304 | |||
305 | fn visit_node<T>(node: &T, label: &str) -> Option<String> | ||
306 | where | ||
307 | T: NameOwner + VisibilityOwner, | ||
308 | { | ||
309 | let mut string = | ||
310 | node.visibility().map(|v| format!("{} ", v.syntax().text())).unwrap_or_default(); | ||
311 | string.push_str(label); | ||
312 | string.push_str(node.name()?.text().as_str()); | ||
313 | Some(string) | ||
314 | } | ||
315 | |||
316 | visitor() | ||
317 | .visit(|node: &ast::FnDef| Some(crate::display::function_label(node))) | ||
318 | .visit(|node: &ast::StructDef| visit_node(node, "struct ")) | ||
319 | .visit(|node: &ast::EnumDef| visit_node(node, "enum ")) | ||
320 | .visit(|node: &ast::TraitDef| visit_node(node, "trait ")) | ||
321 | .visit(|node: &ast::Module| visit_node(node, "mod ")) | ||
322 | .visit(|node: &ast::TypeAliasDef| visit_node(node, "type ")) | ||
323 | .visit(|node: &ast::ConstDef| visit_ascribed_node(node, "const ")) | ||
324 | .visit(|node: &ast::StaticDef| visit_ascribed_node(node, "static ")) | ||
325 | .visit(|node: &ast::NamedFieldDef| visit_ascribed_node(node, "")) | ||
326 | .visit(|node: &ast::EnumVariant| Some(node.name()?.text().to_string())) | ||
327 | .accept(&node)? | ||
328 | } | ||
329 | } | ||