aboutsummaryrefslogtreecommitdiff
path: root/crates/ide/src/display/navigation_target.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/ide/src/display/navigation_target.rs')
-rw-r--r--crates/ide/src/display/navigation_target.rs491
1 files changed, 491 insertions, 0 deletions
diff --git a/crates/ide/src/display/navigation_target.rs b/crates/ide/src/display/navigation_target.rs
new file mode 100644
index 000000000..1ee80c2dd
--- /dev/null
+++ b/crates/ide/src/display/navigation_target.rs
@@ -0,0 +1,491 @@
1//! FIXME: write short doc here
2
3use base_db::{FileId, SourceDatabase};
4use either::Either;
5use hir::{original_range, AssocItem, FieldSource, HasSource, InFile, ModuleSource};
6use ide_db::{defs::Definition, RootDatabase};
7use syntax::{
8 ast::{self, DocCommentsOwner, NameOwner},
9 match_ast, AstNode, SmolStr,
10 SyntaxKind::{self, IDENT_PAT, TYPE_PARAM},
11 TextRange,
12};
13
14use crate::FileSymbol;
15
16use 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)]
24pub 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
49pub(crate) trait ToNav {
50 fn to_nav(&self, db: &RootDatabase) -> NavigationTarget;
51}
52
53pub(crate) trait TryToNav {
54 fn try_to_nav(&self, db: &RootDatabase) -> Option<NavigationTarget>;
55}
56
57impl 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
158impl 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
173impl 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
186impl 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
203pub(crate) trait ToNavFromAst {}
204impl ToNavFromAst for hir::Function {}
205impl ToNavFromAst for hir::Const {}
206impl ToNavFromAst for hir::Static {}
207impl ToNavFromAst for hir::Struct {}
208impl ToNavFromAst for hir::Enum {}
209impl ToNavFromAst for hir::EnumVariant {}
210impl ToNavFromAst for hir::Union {}
211impl ToNavFromAst for hir::TypeAlias {}
212impl ToNavFromAst for hir::Trait {}
213
214impl<D> ToNav for D
215where
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
229impl 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
244impl 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
269impl 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
294impl 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
305impl 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
315impl 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
325impl 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
352impl 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
376pub(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`
401pub(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)]
423mod 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#"
432enum FooInner { }
433fn 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 1,
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 1,
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#"
483fn foo() {}
484struct Foo;
485"#,
486 );
487
488 let navs = analysis.symbol_search(Query::new("foo".to_string())).unwrap();
489 assert_eq!(navs.len(), 2)
490 }
491}