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