]> git.proxmox.com Git - rustc.git/blob - src/tools/rust-analyzer/crates/ide/src/navigation_target.rs
New upstream version 1.74.1+dfsg1
[rustc.git] / src / tools / rust-analyzer / crates / ide / src / navigation_target.rs
1 //! See [`NavigationTarget`].
2
3 use std::fmt;
4
5 use either::Either;
6 use hir::{
7 symbols::FileSymbol, AssocItem, FieldSource, HasContainer, HasSource, HirDisplay, HirFileId,
8 InFile, LocalSource, ModuleSource,
9 };
10 use ide_db::{
11 base_db::{FileId, FileRange},
12 defs::Definition,
13 documentation::{Documentation, HasDocs},
14 RootDatabase, SymbolKind,
15 };
16 use stdx::never;
17 use syntax::{
18 ast::{self, HasName},
19 AstNode, SmolStr, SyntaxNode, TextRange,
20 };
21
22 /// `NavigationTarget` represents an element in the editor's UI which you can
23 /// click on to navigate to a particular piece of code.
24 ///
25 /// Typically, a `NavigationTarget` corresponds to some element in the source
26 /// code, like a function or a struct, but this is not strictly required.
27 #[derive(Clone, PartialEq, Eq, Hash)]
28 pub struct NavigationTarget {
29 pub file_id: FileId,
30 /// Range which encompasses the whole element.
31 ///
32 /// Should include body, doc comments, attributes, etc.
33 ///
34 /// Clients should use this range to answer "is the cursor inside the
35 /// element?" question.
36 pub full_range: TextRange,
37 /// A "most interesting" range within the `full_range`.
38 ///
39 /// Typically, `full_range` is the whole syntax node, including doc
40 /// comments, and `focus_range` is the range of the identifier.
41 ///
42 /// Clients should place the cursor on this range when navigating to this target.
43 pub focus_range: Option<TextRange>,
44 pub name: SmolStr,
45 pub kind: Option<SymbolKind>,
46 pub container_name: Option<SmolStr>,
47 pub description: Option<String>,
48 pub docs: Option<Documentation>,
49 /// In addition to a `name` field, a `NavigationTarget` may also be aliased
50 /// In such cases we want a `NavigationTarget` to be accessible by its alias
51 pub alias: Option<SmolStr>,
52 }
53
54 impl fmt::Debug for NavigationTarget {
55 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56 let mut f = f.debug_struct("NavigationTarget");
57 macro_rules! opt {
58 ($($name:ident)*) => {$(
59 if let Some(it) = &self.$name {
60 f.field(stringify!($name), it);
61 }
62 )*}
63 }
64 f.field("file_id", &self.file_id).field("full_range", &self.full_range);
65 opt!(focus_range);
66 f.field("name", &self.name);
67 opt!(kind container_name description docs);
68 f.finish()
69 }
70 }
71
72 pub(crate) trait ToNav {
73 fn to_nav(&self, db: &RootDatabase) -> NavigationTarget;
74 }
75
76 pub(crate) trait TryToNav {
77 fn try_to_nav(&self, db: &RootDatabase) -> Option<NavigationTarget>;
78 }
79
80 impl<T: TryToNav, U: TryToNav> TryToNav for Either<T, U> {
81 fn try_to_nav(&self, db: &RootDatabase) -> Option<NavigationTarget> {
82 match self {
83 Either::Left(it) => it.try_to_nav(db),
84 Either::Right(it) => it.try_to_nav(db),
85 }
86 }
87 }
88
89 impl NavigationTarget {
90 pub fn focus_or_full_range(&self) -> TextRange {
91 self.focus_range.unwrap_or(self.full_range)
92 }
93
94 pub(crate) fn from_module_to_decl(db: &RootDatabase, module: hir::Module) -> NavigationTarget {
95 let name = module.name(db).map(|it| it.to_smol_str()).unwrap_or_default();
96 if let Some(InFile { value, file_id }) = &module.declaration_source(db) {
97 let (file_id, full_range, focus_range) =
98 orig_range_with_focus(db, *file_id, value.syntax(), value.name());
99 let mut res = NavigationTarget::from_syntax(
100 file_id,
101 name,
102 focus_range,
103 full_range,
104 SymbolKind::Module,
105 );
106 res.docs = module.docs(db);
107 res.description = Some(module.display(db).to_string());
108 return res;
109 }
110 module.to_nav(db)
111 }
112
113 #[cfg(test)]
114 pub(crate) fn debug_render(&self) -> String {
115 let mut buf = format!(
116 "{} {:?} {:?} {:?}",
117 self.name,
118 self.kind.unwrap(),
119 self.file_id,
120 self.full_range
121 );
122 if let Some(focus_range) = self.focus_range {
123 buf.push_str(&format!(" {focus_range:?}"))
124 }
125 if let Some(container_name) = &self.container_name {
126 buf.push_str(&format!(" {container_name}"))
127 }
128 buf
129 }
130
131 /// Allows `NavigationTarget` to be created from a `NameOwner`
132 pub(crate) fn from_named(
133 db: &RootDatabase,
134 InFile { file_id, value }: InFile<&dyn ast::HasName>,
135 kind: SymbolKind,
136 ) -> NavigationTarget {
137 let name = value.name().map(|it| it.text().into()).unwrap_or_else(|| "_".into());
138
139 let (file_id, full_range, focus_range) =
140 orig_range_with_focus(db, file_id, value.syntax(), value.name());
141
142 NavigationTarget::from_syntax(file_id, name, focus_range, full_range, kind)
143 }
144
145 fn from_syntax(
146 file_id: FileId,
147 name: SmolStr,
148 focus_range: Option<TextRange>,
149 full_range: TextRange,
150 kind: SymbolKind,
151 ) -> NavigationTarget {
152 NavigationTarget {
153 file_id,
154 name,
155 kind: Some(kind),
156 full_range,
157 focus_range,
158 container_name: None,
159 description: None,
160 docs: None,
161 alias: None,
162 }
163 }
164 }
165
166 impl TryToNav for FileSymbol {
167 fn try_to_nav(&self, db: &RootDatabase) -> Option<NavigationTarget> {
168 let full_range = self.loc.original_range(db);
169 let focus_range = self.loc.original_name_range(db).and_then(|it| {
170 if it.file_id == full_range.file_id {
171 Some(it.range)
172 } else {
173 None
174 }
175 });
176
177 Some(NavigationTarget {
178 file_id: full_range.file_id,
179 name: self
180 .is_alias
181 .then(|| self.def.name(db))
182 .flatten()
183 .map_or_else(|| self.name.clone(), |it| it.to_smol_str()),
184 alias: self.is_alias.then(|| self.name.clone()),
185 kind: Some(hir::ModuleDefId::from(self.def).into()),
186 full_range: full_range.range,
187 focus_range,
188 container_name: self.container_name.clone(),
189 description: match self.def {
190 hir::ModuleDef::Module(it) => Some(it.display(db).to_string()),
191 hir::ModuleDef::Function(it) => Some(it.display(db).to_string()),
192 hir::ModuleDef::Adt(it) => Some(it.display(db).to_string()),
193 hir::ModuleDef::Variant(it) => Some(it.display(db).to_string()),
194 hir::ModuleDef::Const(it) => Some(it.display(db).to_string()),
195 hir::ModuleDef::Static(it) => Some(it.display(db).to_string()),
196 hir::ModuleDef::Trait(it) => Some(it.display(db).to_string()),
197 hir::ModuleDef::TraitAlias(it) => Some(it.display(db).to_string()),
198 hir::ModuleDef::TypeAlias(it) => Some(it.display(db).to_string()),
199 hir::ModuleDef::Macro(it) => Some(it.display(db).to_string()),
200 hir::ModuleDef::BuiltinType(_) => None,
201 },
202 docs: None,
203 })
204 }
205 }
206
207 impl TryToNav for Definition {
208 fn try_to_nav(&self, db: &RootDatabase) -> Option<NavigationTarget> {
209 match self {
210 Definition::Local(it) => Some(it.to_nav(db)),
211 Definition::Label(it) => Some(it.to_nav(db)),
212 Definition::Module(it) => Some(it.to_nav(db)),
213 Definition::Macro(it) => it.try_to_nav(db),
214 Definition::Field(it) => it.try_to_nav(db),
215 Definition::SelfType(it) => it.try_to_nav(db),
216 Definition::GenericParam(it) => it.try_to_nav(db),
217 Definition::Function(it) => it.try_to_nav(db),
218 Definition::Adt(it) => it.try_to_nav(db),
219 Definition::Variant(it) => it.try_to_nav(db),
220 Definition::Const(it) => it.try_to_nav(db),
221 Definition::Static(it) => it.try_to_nav(db),
222 Definition::Trait(it) => it.try_to_nav(db),
223 Definition::TraitAlias(it) => it.try_to_nav(db),
224 Definition::TypeAlias(it) => it.try_to_nav(db),
225 Definition::ExternCrateDecl(it) => Some(it.try_to_nav(db)?),
226 Definition::BuiltinType(_) => None,
227 Definition::ToolModule(_) => None,
228 Definition::BuiltinAttr(_) => None,
229 // FIXME: The focus range should be set to the helper declaration
230 Definition::DeriveHelper(it) => it.derive().try_to_nav(db),
231 }
232 }
233 }
234
235 impl TryToNav for hir::ModuleDef {
236 fn try_to_nav(&self, db: &RootDatabase) -> Option<NavigationTarget> {
237 match self {
238 hir::ModuleDef::Module(it) => Some(it.to_nav(db)),
239 hir::ModuleDef::Function(it) => it.try_to_nav(db),
240 hir::ModuleDef::Adt(it) => it.try_to_nav(db),
241 hir::ModuleDef::Variant(it) => it.try_to_nav(db),
242 hir::ModuleDef::Const(it) => it.try_to_nav(db),
243 hir::ModuleDef::Static(it) => it.try_to_nav(db),
244 hir::ModuleDef::Trait(it) => it.try_to_nav(db),
245 hir::ModuleDef::TraitAlias(it) => it.try_to_nav(db),
246 hir::ModuleDef::TypeAlias(it) => it.try_to_nav(db),
247 hir::ModuleDef::Macro(it) => it.try_to_nav(db),
248 hir::ModuleDef::BuiltinType(_) => None,
249 }
250 }
251 }
252
253 pub(crate) trait ToNavFromAst: Sized {
254 const KIND: SymbolKind;
255 fn container_name(self, db: &RootDatabase) -> Option<SmolStr> {
256 _ = db;
257 None
258 }
259 }
260
261 fn container_name(db: &RootDatabase, t: impl HasContainer) -> Option<SmolStr> {
262 match t.container(db) {
263 hir::ItemContainer::Trait(it) => Some(it.name(db).to_smol_str()),
264 // FIXME: Handle owners of blocks correctly here
265 hir::ItemContainer::Module(it) => it.name(db).map(|name| name.to_smol_str()),
266 _ => None,
267 }
268 }
269
270 impl ToNavFromAst for hir::Function {
271 const KIND: SymbolKind = SymbolKind::Function;
272 fn container_name(self, db: &RootDatabase) -> Option<SmolStr> {
273 container_name(db, self)
274 }
275 }
276
277 impl ToNavFromAst for hir::Const {
278 const KIND: SymbolKind = SymbolKind::Const;
279 fn container_name(self, db: &RootDatabase) -> Option<SmolStr> {
280 container_name(db, self)
281 }
282 }
283 impl ToNavFromAst for hir::Static {
284 const KIND: SymbolKind = SymbolKind::Static;
285 fn container_name(self, db: &RootDatabase) -> Option<SmolStr> {
286 container_name(db, self)
287 }
288 }
289 impl ToNavFromAst for hir::Struct {
290 const KIND: SymbolKind = SymbolKind::Struct;
291 fn container_name(self, db: &RootDatabase) -> Option<SmolStr> {
292 container_name(db, self)
293 }
294 }
295 impl ToNavFromAst for hir::Enum {
296 const KIND: SymbolKind = SymbolKind::Enum;
297 fn container_name(self, db: &RootDatabase) -> Option<SmolStr> {
298 container_name(db, self)
299 }
300 }
301 impl ToNavFromAst for hir::Variant {
302 const KIND: SymbolKind = SymbolKind::Variant;
303 }
304 impl ToNavFromAst for hir::Union {
305 const KIND: SymbolKind = SymbolKind::Union;
306 fn container_name(self, db: &RootDatabase) -> Option<SmolStr> {
307 container_name(db, self)
308 }
309 }
310 impl ToNavFromAst for hir::TypeAlias {
311 const KIND: SymbolKind = SymbolKind::TypeAlias;
312 fn container_name(self, db: &RootDatabase) -> Option<SmolStr> {
313 container_name(db, self)
314 }
315 }
316 impl ToNavFromAst for hir::Trait {
317 const KIND: SymbolKind = SymbolKind::Trait;
318 fn container_name(self, db: &RootDatabase) -> Option<SmolStr> {
319 container_name(db, self)
320 }
321 }
322 impl ToNavFromAst for hir::TraitAlias {
323 const KIND: SymbolKind = SymbolKind::TraitAlias;
324 fn container_name(self, db: &RootDatabase) -> Option<SmolStr> {
325 container_name(db, self)
326 }
327 }
328
329 impl<D> TryToNav for D
330 where
331 D: HasSource + ToNavFromAst + Copy + HasDocs + HirDisplay,
332 D::Ast: ast::HasName,
333 {
334 fn try_to_nav(&self, db: &RootDatabase) -> Option<NavigationTarget> {
335 let src = self.source(db)?;
336 let mut res = NavigationTarget::from_named(
337 db,
338 src.as_ref().map(|it| it as &dyn ast::HasName),
339 D::KIND,
340 );
341 res.docs = self.docs(db);
342 res.description = Some(self.display(db).to_string());
343 res.container_name = self.container_name(db);
344 Some(res)
345 }
346 }
347
348 impl ToNav for hir::Module {
349 fn to_nav(&self, db: &RootDatabase) -> NavigationTarget {
350 let InFile { file_id, value } = self.definition_source(db);
351
352 let name = self.name(db).map(|it| it.to_smol_str()).unwrap_or_default();
353 let (syntax, focus) = match &value {
354 ModuleSource::SourceFile(node) => (node.syntax(), None),
355 ModuleSource::Module(node) => (node.syntax(), node.name()),
356 ModuleSource::BlockExpr(node) => (node.syntax(), None),
357 };
358 let (file_id, full_range, focus_range) = orig_range_with_focus(db, file_id, syntax, focus);
359 NavigationTarget::from_syntax(file_id, name, focus_range, full_range, SymbolKind::Module)
360 }
361 }
362
363 impl TryToNav for hir::Impl {
364 fn try_to_nav(&self, db: &RootDatabase) -> Option<NavigationTarget> {
365 let InFile { file_id, value } = self.source(db)?;
366 let derive_attr = self.as_builtin_derive(db);
367
368 let (focus, syntax) = match &derive_attr {
369 Some(attr) => (None, attr.value.syntax()),
370 None => (value.self_ty(), value.syntax()),
371 };
372
373 let (file_id, full_range, focus_range) = orig_range_with_focus(db, file_id, syntax, focus);
374 Some(NavigationTarget::from_syntax(
375 file_id,
376 "impl".into(),
377 focus_range,
378 full_range,
379 SymbolKind::Impl,
380 ))
381 }
382 }
383
384 impl TryToNav for hir::ExternCrateDecl {
385 fn try_to_nav(&self, db: &RootDatabase) -> Option<NavigationTarget> {
386 let src = self.source(db)?;
387 let InFile { file_id, value } = src;
388 let focus = value
389 .rename()
390 .map_or_else(|| value.name_ref().map(Either::Left), |it| it.name().map(Either::Right));
391 let (file_id, full_range, focus_range) =
392 orig_range_with_focus(db, file_id, value.syntax(), focus);
393 let mut res = NavigationTarget::from_syntax(
394 file_id,
395 self.alias_or_name(db).unwrap_or_else(|| self.name(db)).to_smol_str(),
396 focus_range,
397 full_range,
398 SymbolKind::Module,
399 );
400
401 res.docs = self.docs(db);
402 res.description = Some(self.display(db).to_string());
403 res.container_name = container_name(db, *self);
404 Some(res)
405 }
406 }
407
408 impl TryToNav for hir::Field {
409 fn try_to_nav(&self, db: &RootDatabase) -> Option<NavigationTarget> {
410 let src = self.source(db)?;
411
412 let field_source = match &src.value {
413 FieldSource::Named(it) => {
414 let mut res =
415 NavigationTarget::from_named(db, src.with_value(it), SymbolKind::Field);
416 res.docs = self.docs(db);
417 res.description = Some(self.display(db).to_string());
418 res
419 }
420 FieldSource::Pos(it) => {
421 let FileRange { file_id, range } =
422 src.with_value(it.syntax()).original_file_range(db);
423 NavigationTarget::from_syntax(file_id, "".into(), None, range, SymbolKind::Field)
424 }
425 };
426 Some(field_source)
427 }
428 }
429
430 impl TryToNav for hir::Macro {
431 fn try_to_nav(&self, db: &RootDatabase) -> Option<NavigationTarget> {
432 let src = self.source(db)?;
433 let name_owner: &dyn ast::HasName = match &src.value {
434 Either::Left(it) => it,
435 Either::Right(it) => it,
436 };
437 let mut res = NavigationTarget::from_named(
438 db,
439 src.as_ref().with_value(name_owner),
440 self.kind(db).into(),
441 );
442 res.docs = self.docs(db);
443 Some(res)
444 }
445 }
446
447 impl TryToNav for hir::Adt {
448 fn try_to_nav(&self, db: &RootDatabase) -> Option<NavigationTarget> {
449 match self {
450 hir::Adt::Struct(it) => it.try_to_nav(db),
451 hir::Adt::Union(it) => it.try_to_nav(db),
452 hir::Adt::Enum(it) => it.try_to_nav(db),
453 }
454 }
455 }
456
457 impl TryToNav for hir::AssocItem {
458 fn try_to_nav(&self, db: &RootDatabase) -> Option<NavigationTarget> {
459 match self {
460 AssocItem::Function(it) => it.try_to_nav(db),
461 AssocItem::Const(it) => it.try_to_nav(db),
462 AssocItem::TypeAlias(it) => it.try_to_nav(db),
463 }
464 }
465 }
466
467 impl TryToNav for hir::GenericParam {
468 fn try_to_nav(&self, db: &RootDatabase) -> Option<NavigationTarget> {
469 match self {
470 hir::GenericParam::TypeParam(it) => it.try_to_nav(db),
471 hir::GenericParam::ConstParam(it) => it.try_to_nav(db),
472 hir::GenericParam::LifetimeParam(it) => it.try_to_nav(db),
473 }
474 }
475 }
476
477 impl ToNav for LocalSource {
478 fn to_nav(&self, db: &RootDatabase) -> NavigationTarget {
479 let InFile { file_id, value } = &self.source;
480 let file_id = *file_id;
481 let local = self.local;
482 let (node, name) = match &value {
483 Either::Left(bind_pat) => (bind_pat.syntax(), bind_pat.name()),
484 Either::Right(it) => (it.syntax(), it.name()),
485 };
486
487 let (file_id, full_range, focus_range) = orig_range_with_focus(db, file_id, node, name);
488
489 let name = local.name(db).to_smol_str();
490 let kind = if local.is_self(db) {
491 SymbolKind::SelfParam
492 } else if local.is_param(db) {
493 SymbolKind::ValueParam
494 } else {
495 SymbolKind::Local
496 };
497 NavigationTarget {
498 file_id,
499 name,
500 alias: None,
501 kind: Some(kind),
502 full_range,
503 focus_range,
504 container_name: None,
505 description: None,
506 docs: None,
507 }
508 }
509 }
510
511 impl ToNav for hir::Local {
512 fn to_nav(&self, db: &RootDatabase) -> NavigationTarget {
513 self.primary_source(db).to_nav(db)
514 }
515 }
516
517 impl ToNav for hir::Label {
518 fn to_nav(&self, db: &RootDatabase) -> NavigationTarget {
519 let InFile { file_id, value } = self.source(db);
520 let name = self.name(db).to_smol_str();
521
522 let (file_id, full_range, focus_range) =
523 orig_range_with_focus(db, file_id, value.syntax(), value.lifetime());
524
525 NavigationTarget {
526 file_id,
527 name,
528 alias: None,
529 kind: Some(SymbolKind::Label),
530 full_range,
531 focus_range,
532 container_name: None,
533 description: None,
534 docs: None,
535 }
536 }
537 }
538
539 impl TryToNav for hir::TypeParam {
540 fn try_to_nav(&self, db: &RootDatabase) -> Option<NavigationTarget> {
541 let InFile { file_id, value } = self.merge().source(db)?;
542 let name = self.name(db).to_smol_str();
543
544 let value = match value {
545 Either::Left(ast::TypeOrConstParam::Type(x)) => Either::Left(x),
546 Either::Left(ast::TypeOrConstParam::Const(_)) => {
547 never!();
548 return None;
549 }
550 Either::Right(x) => Either::Right(x),
551 };
552
553 let syntax = match &value {
554 Either::Left(type_param) => type_param.syntax(),
555 Either::Right(trait_) => trait_.syntax(),
556 };
557 let focus = value.as_ref().either(|it| it.name(), |it| it.name());
558
559 let (file_id, full_range, focus_range) = orig_range_with_focus(db, file_id, syntax, focus);
560
561 Some(NavigationTarget {
562 file_id,
563 name,
564 alias: None,
565 kind: Some(SymbolKind::TypeParam),
566 full_range,
567 focus_range,
568 container_name: None,
569 description: None,
570 docs: None,
571 })
572 }
573 }
574
575 impl TryToNav for hir::TypeOrConstParam {
576 fn try_to_nav(&self, db: &RootDatabase) -> Option<NavigationTarget> {
577 self.split(db).try_to_nav(db)
578 }
579 }
580
581 impl TryToNav for hir::LifetimeParam {
582 fn try_to_nav(&self, db: &RootDatabase) -> Option<NavigationTarget> {
583 let InFile { file_id, value } = self.source(db)?;
584 let name = self.name(db).to_smol_str();
585
586 let FileRange { file_id, range } =
587 InFile::new(file_id, value.syntax()).original_file_range(db);
588 Some(NavigationTarget {
589 file_id,
590 name,
591 alias: None,
592 kind: Some(SymbolKind::LifetimeParam),
593 full_range: range,
594 focus_range: Some(range),
595 container_name: None,
596 description: None,
597 docs: None,
598 })
599 }
600 }
601
602 impl TryToNav for hir::ConstParam {
603 fn try_to_nav(&self, db: &RootDatabase) -> Option<NavigationTarget> {
604 let InFile { file_id, value } = self.merge().source(db)?;
605 let name = self.name(db).to_smol_str();
606
607 let value = match value {
608 Either::Left(ast::TypeOrConstParam::Const(x)) => x,
609 _ => {
610 never!();
611 return None;
612 }
613 };
614
615 let (file_id, full_range, focus_range) =
616 orig_range_with_focus(db, file_id, value.syntax(), value.name());
617 Some(NavigationTarget {
618 file_id,
619 name,
620 alias: None,
621 kind: Some(SymbolKind::ConstParam),
622 full_range,
623 focus_range,
624 container_name: None,
625 description: None,
626 docs: None,
627 })
628 }
629 }
630
631 fn orig_range_with_focus(
632 db: &RootDatabase,
633 hir_file: HirFileId,
634 value: &SyntaxNode,
635 name: Option<impl AstNode>,
636 ) -> (FileId, TextRange, Option<TextRange>) {
637 let FileRange { file_id, range: full_range } =
638 InFile::new(hir_file, value).original_file_range(db);
639 let focus_range = name
640 .and_then(|it| InFile::new(hir_file, it.syntax()).original_file_range_opt(db))
641 .and_then(|range| if range.file_id == file_id { Some(range.range) } else { None });
642
643 (file_id, full_range, focus_range)
644 }
645
646 #[cfg(test)]
647 mod tests {
648 use expect_test::expect;
649
650 use crate::{fixture, Query};
651
652 #[test]
653 fn test_nav_for_symbol() {
654 let (analysis, _) = fixture::file(
655 r#"
656 enum FooInner { }
657 fn foo() { enum FooInner { } }
658 "#,
659 );
660
661 let navs = analysis.symbol_search(Query::new("FooInner".to_string())).unwrap();
662 expect![[r#"
663 [
664 NavigationTarget {
665 file_id: FileId(
666 0,
667 ),
668 full_range: 0..17,
669 focus_range: 5..13,
670 name: "FooInner",
671 kind: Enum,
672 description: "enum FooInner",
673 },
674 NavigationTarget {
675 file_id: FileId(
676 0,
677 ),
678 full_range: 29..46,
679 focus_range: 34..42,
680 name: "FooInner",
681 kind: Enum,
682 container_name: "foo",
683 description: "enum FooInner",
684 },
685 ]
686 "#]]
687 .assert_debug_eq(&navs);
688 }
689
690 #[test]
691 fn test_world_symbols_are_case_sensitive() {
692 let (analysis, _) = fixture::file(
693 r#"
694 fn foo() {}
695 struct Foo;
696 "#,
697 );
698
699 let navs = analysis.symbol_search(Query::new("foo".to_string())).unwrap();
700 assert_eq!(navs.len(), 2)
701 }
702 }