ide_db/
symbol_index.rs

1//! This module handles fuzzy-searching of functions, structs and other symbols
2//! by name across the whole workspace and dependencies.
3//!
4//! It works by building an incrementally-updated text-search index of all
5//! symbols. The backbone of the index is the **awesome** `fst` crate by
6//! @BurntSushi.
7//!
8//! In a nutshell, you give a set of strings to `fst`, and it builds a
9//! finite state machine describing this set of strings. The strings which
10//! could fuzzy-match a pattern can also be described by a finite state machine.
11//! What is freaking cool is that you can now traverse both state machines in
12//! lock-step to enumerate the strings which are both in the input set and
13//! fuzz-match the query. Or, more formally, given two languages described by
14//! FSTs, one can build a product FST which describes the intersection of the
15//! languages.
16//!
17//! `fst` does not support cheap updating of the index, but it supports unioning
18//! of state machines. So, to account for changing source code, we build an FST
19//! for each library (which is assumed to never change) and an FST for each Rust
20//! file in the current workspace, and run a query against the union of all
21//! those FSTs.
22
23use std::{
24    cmp::Ordering,
25    fmt,
26    hash::{Hash, Hasher},
27    ops::ControlFlow,
28};
29
30use base_db::{CrateOrigin, LangCrateOrigin, LibraryRoots, LocalRoots, RootQueryDb, SourceRootId};
31use fst::{Automaton, Streamer, raw::IndexedValue};
32use hir::{
33    Crate, Module,
34    db::HirDatabase,
35    import_map::{AssocSearchMode, SearchMode},
36    symbols::{FileSymbol, SymbolCollector},
37};
38use itertools::Itertools;
39use rayon::prelude::*;
40use salsa::Update;
41
42use crate::RootDatabase;
43
44/// A query for searching symbols in the workspace or dependencies.
45///
46/// This struct configures how symbol search is performed, including the search text,
47/// matching strategy, and filtering options. It is used by [`world_symbols`] to find
48/// symbols across the codebase.
49///
50/// # Example
51/// ```ignore
52/// let mut query = Query::new("MyStruct".to_string());
53/// query.only_types();  // Only search for type definitions
54/// query.libs();        // Include library dependencies
55/// query.exact();       // Use exact matching instead of fuzzy
56/// ```
57#[derive(Debug, Clone)]
58pub struct Query {
59    /// The item name to search for (last segment of the path, or full query if no path).
60    /// When empty with a non-empty `path_filter`, returns all items in that module.
61    query: String,
62    /// Lowercase version of [`Self::query`], pre-computed for efficiency.
63    /// Used to build FST automata for case-insensitive index lookups.
64    lowercased: String,
65    /// Path segments to filter by (all segments except the last).
66    /// Empty if no `::` in the original query.
67    path_filter: Vec<String>,
68    /// If true, the first path segment must be a crate name (query started with `::`).
69    anchor_to_crate: bool,
70    /// The search strategy to use when matching symbols.
71    /// - [`SearchMode::Exact`]: Symbol name must exactly match the query.
72    /// - [`SearchMode::Fuzzy`]: Symbol name must contain all query characters in order (subsequence match).
73    /// - [`SearchMode::Prefix`]: Symbol name must start with the query string.
74    ///
75    /// Defaults to [`SearchMode::Fuzzy`].
76    mode: SearchMode,
77    /// Controls filtering of trait-associated items (methods, constants, types).
78    /// - [`AssocSearchMode::Include`]: Include both associated and non-associated items.
79    /// - [`AssocSearchMode::Exclude`]: Exclude trait-associated items from results.
80    /// - [`AssocSearchMode::AssocItemsOnly`]: Only return trait-associated items.
81    ///
82    /// Defaults to [`AssocSearchMode::Include`].
83    assoc_mode: AssocSearchMode,
84    /// Whether the final symbol name comparison should be case-sensitive.
85    /// When `false`, matching is case-insensitive (e.g., "foo" matches "Foo").
86    ///
87    /// Defaults to `false`.
88    case_sensitive: bool,
89    /// When `true`, only return type definitions: structs, enums, unions,
90    /// type aliases, built-in types, and traits. Functions, constants, statics,
91    /// and modules are excluded.
92    ///
93    /// Defaults to `false`.
94    only_types: bool,
95    /// When `true`, search library dependency roots instead of local workspace crates.
96    /// This enables finding symbols in external dependencies including the standard library.
97    ///
98    /// Defaults to `false` (search local workspace only).
99    libs: bool,
100    /// When `true`, exclude re-exported/imported symbols from results,
101    /// showing only the original definitions.
102    ///
103    /// Defaults to `false`.
104    exclude_imports: bool,
105}
106
107impl Query {
108    pub fn new(query: String) -> Query {
109        let (path_filter, item_query, anchor_to_crate) = Self::parse_path_query(&query);
110        let lowercased = item_query.to_lowercase();
111        Query {
112            query: item_query,
113            lowercased,
114            path_filter,
115            anchor_to_crate,
116            only_types: false,
117            libs: false,
118            mode: SearchMode::Fuzzy,
119            assoc_mode: AssocSearchMode::Include,
120            case_sensitive: false,
121            exclude_imports: false,
122        }
123    }
124
125    /// Parse a query string that may contain path segments.
126    ///
127    /// Returns (path_filter, item_query, anchor_to_crate) where:
128    /// - `path_filter`: Path segments to match (all but the last segment)
129    /// - `item_query`: The item name to search for (last segment)
130    /// - `anchor_to_crate`: Whether the first segment must be a crate name
131    fn parse_path_query(query: &str) -> (Vec<String>, String, bool) {
132        // Check for leading :: (absolute path / crate search)
133        let (query, anchor_to_crate) = match query.strip_prefix("::") {
134            Some(q) => (q, true),
135            None => (query, false),
136        };
137
138        let Some((prefix, query)) = query.rsplit_once("::") else {
139            return (vec![], query.to_owned(), anchor_to_crate);
140        };
141
142        let prefix: Vec<_> =
143            prefix.split("::").filter(|s| !s.is_empty()).map(ToOwned::to_owned).collect();
144
145        (prefix, query.to_owned(), anchor_to_crate)
146    }
147
148    /// Returns true if this query is searching for crates
149    /// (i.e., the query was "::" alone or "::foo" for fuzzy crate search)
150    fn is_crate_search(&self) -> bool {
151        self.anchor_to_crate && self.path_filter.is_empty()
152    }
153
154    pub fn only_types(&mut self) {
155        self.only_types = true;
156    }
157
158    pub fn libs(&mut self) {
159        self.libs = true;
160    }
161
162    pub fn fuzzy(&mut self) {
163        self.mode = SearchMode::Fuzzy;
164    }
165
166    pub fn exact(&mut self) {
167        self.mode = SearchMode::Exact;
168    }
169
170    pub fn prefix(&mut self) {
171        self.mode = SearchMode::Prefix;
172    }
173
174    /// Specifies whether we want to include associated items in the result.
175    pub fn assoc_search_mode(&mut self, assoc_mode: AssocSearchMode) {
176        self.assoc_mode = assoc_mode;
177    }
178
179    pub fn case_sensitive(&mut self) {
180        self.case_sensitive = true;
181    }
182
183    pub fn exclude_imports(&mut self) {
184        self.exclude_imports = true;
185    }
186}
187
188/// The symbol indices of modules that make up a given crate.
189pub fn crate_symbols(db: &dyn HirDatabase, krate: Crate) -> Box<[&SymbolIndex<'_>]> {
190    let _p = tracing::info_span!("crate_symbols").entered();
191    krate.modules(db).into_iter().map(|module| SymbolIndex::module_symbols(db, module)).collect()
192}
193
194// Feature: Workspace Symbol
195//
196// Uses fuzzy-search to find types, modules and functions by name across your
197// project and dependencies. This is **the** most useful feature, which improves code
198// navigation tremendously. It mostly works on top of the built-in LSP
199// functionality, however `#` and `*` symbols can be used to narrow down the
200// search. Specifically,
201//
202// - `Foo` searches for `Foo` type in the current workspace
203// - `foo#` searches for `foo` function in the current workspace
204// - `Foo*` searches for `Foo` type among dependencies, including `stdlib`
205// - `foo#*` searches for `foo` function among dependencies
206//
207// That is, `#` switches from "types" to all symbols, `*` switches from the current
208// workspace to dependencies.
209//
210// This also supports general Rust path syntax with the usual rules.
211//
212// Note that paths do not currently work in VSCode due to the editor never
213// sending the special symbols to the language server. Some other editors might not support the # or
214// * search either, instead, you can configure the filtering via the
215// `rust-analyzer.workspace.symbol.search.scope` and `rust-analyzer.workspace.symbol.search.kind`
216// settings. Symbols prefixed with `__` are hidden from the search results unless configured
217// otherwise.
218//
219// | Editor  | Shortcut |
220// |---------|-----------|
221// | VS Code | <kbd>Ctrl+T</kbd>
222pub fn world_symbols(db: &RootDatabase, mut query: Query) -> Vec<FileSymbol<'_>> {
223    let _p = tracing::info_span!("world_symbols", query = ?query.query).entered();
224
225    // Search for crates by name (handles "::" and "::foo" queries)
226    let indices: Vec<_> = if query.is_crate_search() {
227        query.only_types = false;
228        vec![SymbolIndex::extern_prelude_symbols(db)]
229        // If we have a path filter, resolve it to target modules
230    } else if !query.path_filter.is_empty() {
231        query.only_types = false;
232        let target_modules = resolve_path_to_modules(
233            db,
234            &query.path_filter,
235            query.anchor_to_crate,
236            query.case_sensitive,
237        );
238
239        if target_modules.is_empty() {
240            return vec![];
241        }
242
243        target_modules.iter().map(|&module| SymbolIndex::module_symbols(db, module)).collect()
244    } else if query.libs {
245        LibraryRoots::get(db)
246            .roots(db)
247            .par_iter()
248            .for_each_with(db.clone(), |snap, &root| _ = SymbolIndex::library_symbols(snap, root));
249        LibraryRoots::get(db)
250            .roots(db)
251            .iter()
252            .map(|&root| SymbolIndex::library_symbols(db, root))
253            .collect()
254    } else {
255        let mut crates = Vec::new();
256
257        for &root in LocalRoots::get(db).roots(db).iter() {
258            crates.extend(db.source_root_crates(root).iter().copied())
259        }
260        crates
261            .par_iter()
262            .for_each_with(db.clone(), |snap, &krate| _ = crate_symbols(snap, krate.into()));
263        crates
264            .into_iter()
265            .flat_map(|krate| Vec::from(crate_symbols(db, krate.into())))
266            .chain(std::iter::once(SymbolIndex::extern_prelude_symbols(db)))
267            .collect()
268    };
269
270    let mut res = vec![];
271
272    // Normal search: use FST to match item name
273    query.search::<()>(db, &indices, |f| {
274        res.push(f.clone());
275        ControlFlow::Continue(())
276    });
277
278    res
279}
280
281/// Resolve a path filter to the target module(s) it points to.
282/// Returns the modules whose symbol indices should be searched.
283///
284/// The path_filter contains segments like ["std", "vec"] for a query like "std::vec::Vec".
285/// We resolve this by:
286/// 1. Finding crates matching the first segment
287/// 2. Walking down the module tree following subsequent segments
288fn resolve_path_to_modules(
289    db: &dyn HirDatabase,
290    path_filter: &[String],
291    anchor_to_crate: bool,
292    case_sensitive: bool,
293) -> Vec<Module> {
294    let [first_segment, rest_segments @ ..] = path_filter else {
295        return vec![];
296    };
297
298    // Helper for name comparison
299    let names_match = |actual: &str, expected: &str| -> bool {
300        if case_sensitive { actual == expected } else { actual.eq_ignore_ascii_case(expected) }
301    };
302
303    // Find crates matching the first segment
304    let matching_crates: Vec<Crate> = Crate::all(db)
305        .into_iter()
306        .filter(|krate| {
307            krate
308                .display_name(db)
309                .is_some_and(|name| names_match(name.crate_name().as_str(), first_segment))
310        })
311        .collect();
312
313    // If anchor_to_crate is true, first segment MUST be a crate name
314    // If anchor_to_crate is false, first segment could be a crate OR a module in local crates
315    let mut candidate_modules: Vec<(Module, bool)> = vec![];
316
317    // Add crate root modules for matching crates
318    for krate in matching_crates {
319        candidate_modules.push((krate.root_module(db), krate.origin(db).is_local()));
320    }
321
322    // If not anchored to crate, also search for modules matching first segment in local crates
323    if !anchor_to_crate {
324        for &root in LocalRoots::get(db).roots(db).iter() {
325            for &krate in db.source_root_crates(root).iter() {
326                let root_module = Crate::from(krate).root_module(db);
327                for child in root_module.children(db) {
328                    if let Some(name) = child.name(db)
329                        && names_match(name.as_str(), first_segment)
330                    {
331                        candidate_modules.push((child, true));
332                    }
333                }
334            }
335        }
336    }
337
338    // Walk down the module tree for remaining path segments
339    for segment in rest_segments {
340        candidate_modules = candidate_modules
341            .into_iter()
342            .flat_map(|(module, local)| {
343                module
344                    .modules_in_scope(db, !local)
345                    .into_iter()
346                    .filter(|(name, _)| names_match(name.as_str(), segment))
347                    .map(move |(_, module)| (module, local))
348            })
349            .unique()
350            .collect();
351
352        if candidate_modules.is_empty() {
353            break;
354        }
355    }
356
357    candidate_modules.into_iter().map(|(module, _)| module).collect()
358}
359
360#[derive(Default)]
361pub struct SymbolIndex<'db> {
362    symbols: Box<[FileSymbol<'db>]>,
363    map: fst::Map<Vec<u8>>,
364}
365
366impl<'db> SymbolIndex<'db> {
367    /// The symbol index for a given source root within library_roots.
368    pub fn library_symbols(
369        db: &'db dyn HirDatabase,
370        source_root_id: SourceRootId,
371    ) -> &'db SymbolIndex<'db> {
372        // FIXME:
373        #[salsa::interned]
374        struct InternedSourceRootId {
375            id: SourceRootId,
376        }
377        #[salsa::tracked(returns(ref))]
378        fn library_symbols<'db>(
379            db: &'db dyn HirDatabase,
380            source_root_id: InternedSourceRootId<'db>,
381        ) -> SymbolIndex<'db> {
382            let _p = tracing::info_span!("library_symbols").entered();
383
384            // We call this without attaching because this runs in parallel, so we need to attach here.
385            hir::attach_db(db, || {
386                let mut symbol_collector = SymbolCollector::new(db, true);
387
388                db.source_root_crates(source_root_id.id(db))
389                    .iter()
390                    .flat_map(|&krate| Crate::from(krate).modules(db))
391                    // we specifically avoid calling other SymbolsDatabase queries here, even though they do the same thing,
392                    // as the index for a library is not going to really ever change, and we do not want to store
393                    // the module or crate indices for those in salsa unless we need to.
394                    .for_each(|module| symbol_collector.collect(module));
395
396                SymbolIndex::new(symbol_collector.finish())
397            })
398        }
399        library_symbols(db, InternedSourceRootId::new(db, source_root_id))
400    }
401
402    /// The symbol index for a given module. These modules should only be in source roots that
403    /// are inside local_roots.
404    pub fn module_symbols(db: &dyn HirDatabase, module: Module) -> &SymbolIndex<'_> {
405        // FIXME:
406        #[salsa::interned]
407        struct InternedModuleId {
408            id: hir::ModuleId,
409        }
410
411        #[salsa::tracked(returns(ref))]
412        fn module_symbols<'db>(
413            db: &'db dyn HirDatabase,
414            module: InternedModuleId<'db>,
415        ) -> SymbolIndex<'db> {
416            let _p = tracing::info_span!("module_symbols").entered();
417
418            // We call this without attaching because this runs in parallel, so we need to attach here.
419            hir::attach_db(db, || {
420                let module: Module = module.id(db).into();
421                SymbolIndex::new(SymbolCollector::new_module(
422                    db,
423                    module,
424                    !module.krate(db).origin(db).is_local(),
425                ))
426            })
427        }
428
429        module_symbols(db, InternedModuleId::new(db, hir::ModuleId::from(module)))
430    }
431
432    /// The symbol index for all extern prelude crates.
433    pub fn extern_prelude_symbols(db: &dyn HirDatabase) -> &SymbolIndex<'_> {
434        #[salsa::tracked(returns(ref))]
435        fn extern_prelude_symbols<'db>(db: &'db dyn HirDatabase) -> SymbolIndex<'db> {
436            let _p = tracing::info_span!("extern_prelude_symbols").entered();
437
438            // We call this without attaching because this runs in parallel, so we need to attach here.
439            hir::attach_db(db, || {
440                let mut collector = SymbolCollector::new(db, false);
441
442                for krate in Crate::all(db) {
443                    if krate
444                        .display_name(db)
445                        .is_none_or(|name| name.canonical_name().as_str() == "build-script-build")
446                    {
447                        continue;
448                    }
449                    if let CrateOrigin::Lang(LangCrateOrigin::Dependency | LangCrateOrigin::Other) =
450                        krate.origin(db)
451                    {
452                        // don't show dependencies of the sysroot
453                        continue;
454                    }
455                    collector.push_crate_root(krate);
456                }
457
458                SymbolIndex::new(collector.finish())
459            })
460        }
461
462        extern_prelude_symbols(db)
463    }
464}
465
466impl fmt::Debug for SymbolIndex<'_> {
467    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
468        f.debug_struct("SymbolIndex").field("n_symbols", &self.symbols.len()).finish()
469    }
470}
471
472impl PartialEq for SymbolIndex<'_> {
473    fn eq(&self, other: &SymbolIndex<'_>) -> bool {
474        self.symbols == other.symbols
475    }
476}
477
478impl Eq for SymbolIndex<'_> {}
479
480impl Hash for SymbolIndex<'_> {
481    fn hash<H: Hasher>(&self, hasher: &mut H) {
482        self.symbols.hash(hasher)
483    }
484}
485
486unsafe impl Update for SymbolIndex<'_> {
487    unsafe fn maybe_update(old_pointer: *mut Self, new_value: Self) -> bool {
488        let this = unsafe { &mut *old_pointer };
489        if *this == new_value {
490            false
491        } else {
492            *this = new_value;
493            true
494        }
495    }
496}
497
498impl<'db> SymbolIndex<'db> {
499    fn new(mut symbols: Box<[FileSymbol<'db>]>) -> SymbolIndex<'db> {
500        fn cmp(lhs: &FileSymbol<'_>, rhs: &FileSymbol<'_>) -> Ordering {
501            let lhs_chars = lhs.name.as_str().chars().map(|c| c.to_ascii_lowercase());
502            let rhs_chars = rhs.name.as_str().chars().map(|c| c.to_ascii_lowercase());
503            lhs_chars.cmp(rhs_chars)
504        }
505
506        symbols.par_sort_by(cmp);
507
508        let mut builder = fst::MapBuilder::memory();
509
510        let mut last_batch_start = 0;
511
512        for idx in 0..symbols.len() {
513            if let Some(next_symbol) = symbols.get(idx + 1)
514                && cmp(&symbols[last_batch_start], next_symbol) == Ordering::Equal
515            {
516                continue;
517            }
518
519            let start = last_batch_start;
520            let end = idx + 1;
521            last_batch_start = end;
522
523            let key = symbols[start].name.as_str().to_ascii_lowercase();
524            let value = SymbolIndex::range_to_map_value(start, end);
525
526            builder.insert(key, value).unwrap();
527        }
528
529        let map = builder
530            .into_inner()
531            .and_then(|mut buf| {
532                fst::Map::new({
533                    buf.shrink_to_fit();
534                    buf
535                })
536            })
537            .unwrap();
538        SymbolIndex { symbols, map }
539    }
540
541    pub fn len(&self) -> usize {
542        self.symbols.len()
543    }
544
545    pub fn memory_size(&self) -> usize {
546        self.map.as_fst().size() + self.symbols.len() * size_of::<FileSymbol<'_>>()
547    }
548
549    fn range_to_map_value(start: usize, end: usize) -> u64 {
550        debug_assert![start <= (u32::MAX as usize)];
551        debug_assert![end <= (u32::MAX as usize)];
552
553        ((start as u64) << 32) | end as u64
554    }
555
556    fn map_value_to_range(value: u64) -> (usize, usize) {
557        let end = value as u32 as usize;
558        let start = (value >> 32) as usize;
559        (start, end)
560    }
561}
562
563impl Query {
564    /// Search symbols in the given indices.
565    pub(crate) fn search<'db, T>(
566        &self,
567        db: &'db RootDatabase,
568        indices: &[&'db SymbolIndex<'db>],
569        cb: impl FnMut(&'db FileSymbol<'db>) -> ControlFlow<T>,
570    ) -> Option<T> {
571        let _p = tracing::info_span!("symbol_index::Query::search").entered();
572
573        let mut op = fst::map::OpBuilder::new();
574        match self.mode {
575            SearchMode::Exact => {
576                let automaton = fst::automaton::Str::new(&self.lowercased);
577
578                for index in indices.iter() {
579                    op = op.add(index.map.search(&automaton));
580                }
581                self.search_maps(db, indices, op.union(), cb)
582            }
583            SearchMode::Fuzzy => {
584                let automaton = fst::automaton::Subsequence::new(&self.lowercased);
585
586                for index in indices.iter() {
587                    op = op.add(index.map.search(&automaton));
588                }
589                self.search_maps(db, indices, op.union(), cb)
590            }
591            SearchMode::Prefix => {
592                let automaton = fst::automaton::Str::new(&self.lowercased).starts_with();
593
594                for index in indices.iter() {
595                    op = op.add(index.map.search(&automaton));
596                }
597                self.search_maps(db, indices, op.union(), cb)
598            }
599        }
600    }
601
602    fn search_maps<'db, T>(
603        &self,
604        db: &'db RootDatabase,
605        indices: &[&'db SymbolIndex<'db>],
606        mut stream: fst::map::Union<'_>,
607        mut cb: impl FnMut(&'db FileSymbol<'db>) -> ControlFlow<T>,
608    ) -> Option<T> {
609        let ignore_underscore_prefixed = !self.query.starts_with("__");
610        while let Some((_, indexed_values)) = stream.next() {
611            for &IndexedValue { index, value } in indexed_values {
612                let symbol_index = indices[index];
613                let (start, end) = SymbolIndex::map_value_to_range(value);
614
615                for symbol in &symbol_index.symbols[start..end] {
616                    let non_type_for_type_only_query = self.only_types
617                        && !(matches!(
618                            symbol.def,
619                            hir::ModuleDef::Adt(..)
620                                | hir::ModuleDef::TypeAlias(..)
621                                | hir::ModuleDef::BuiltinType(..)
622                                | hir::ModuleDef::Trait(..)
623                        ) || matches!(
624                            symbol.def,
625                            hir::ModuleDef::Module(module) if module.is_crate_root(db)
626                        ));
627                    if non_type_for_type_only_query || !self.matches_assoc_mode(symbol.is_assoc) {
628                        continue;
629                    }
630                    // Hide symbols that start with `__` unless the query starts with `__`
631                    let symbol_name = symbol.name.as_str();
632                    if ignore_underscore_prefixed && symbol_name.starts_with("__") {
633                        continue;
634                    }
635                    if self.exclude_imports && symbol.is_import {
636                        continue;
637                    }
638                    if self.mode.check(&self.query, self.case_sensitive, symbol_name)
639                        && let Some(b) = cb(symbol).break_value()
640                    {
641                        return Some(b);
642                    }
643                }
644            }
645        }
646        None
647    }
648
649    fn matches_assoc_mode(&self, is_trait_assoc_item: bool) -> bool {
650        !matches!(
651            (is_trait_assoc_item, self.assoc_mode),
652            (true, AssocSearchMode::Exclude) | (false, AssocSearchMode::AssocItemsOnly)
653        )
654    }
655}
656
657#[cfg(test)]
658mod tests {
659
660    use expect_test::expect_file;
661    use rustc_hash::FxHashSet;
662    use salsa::Setter;
663    use test_fixture::{WORKSPACE, WithFixture};
664
665    use super::*;
666
667    #[test]
668    fn test_symbol_index_collection() {
669        let (db, _) = RootDatabase::with_many_files(
670            r#"
671//- /main.rs
672
673macro_rules! macro_rules_macro {
674    () => {}
675};
676
677macro_rules! define_struct {
678    () => {
679        struct StructFromMacro;
680    }
681};
682
683define_struct!();
684
685macro Macro { }
686
687struct Struct;
688enum Enum {
689    A, B
690}
691union Union {}
692
693impl Struct {
694    fn impl_fn() {}
695}
696
697struct StructT<T>;
698
699impl <T> StructT<T> {
700    fn generic_impl_fn() {}
701}
702
703trait Trait {
704    fn trait_fn(&self);
705}
706
707fn main() {
708    struct StructInFn;
709}
710
711const CONST: u32 = 1;
712static STATIC: &'static str = "2";
713type Alias = Struct;
714
715mod a_mod {
716    struct StructInModA;
717}
718
719const _: () = {
720    struct StructInUnnamedConst;
721
722    ()
723};
724
725const CONST_WITH_INNER: () = {
726    struct StructInNamedConst;
727
728    ()
729};
730
731mod b_mod;
732
733
734use define_struct as really_define_struct;
735use Macro as ItemLikeMacro;
736use Macro as Trait; // overlay namespaces
737//- /b_mod.rs
738struct StructInModB;
739pub(self) use super::Macro as SuperItemLikeMacro;
740pub(self) use crate::b_mod::StructInModB as ThisStruct;
741pub(self) use crate::Trait as IsThisJustATrait;
742"#,
743        );
744
745        let symbols: Vec<_> = Crate::from(db.test_crate())
746            .modules(&db)
747            .into_iter()
748            .map(|module_id| {
749                let mut symbols = SymbolCollector::new_module(&db, module_id, false);
750                symbols.sort_by_key(|it| it.name.as_str().to_owned());
751                (module_id, symbols)
752            })
753            .collect();
754
755        expect_file!["./test_data/test_symbol_index_collection.txt"].assert_debug_eq(&symbols);
756    }
757
758    #[test]
759    fn test_doc_alias() {
760        let (db, _) = RootDatabase::with_single_file(
761            r#"
762#[doc(alias="s1")]
763#[doc(alias="s2")]
764#[doc(alias("mul1","mul2"))]
765struct Struct;
766
767#[doc(alias="s1")]
768struct Duplicate;
769        "#,
770        );
771
772        let symbols: Vec<_> = Crate::from(db.test_crate())
773            .modules(&db)
774            .into_iter()
775            .map(|module_id| {
776                let mut symbols = SymbolCollector::new_module(&db, module_id, false);
777                symbols.sort_by_key(|it| it.name.as_str().to_owned());
778                (module_id, symbols)
779            })
780            .collect();
781
782        expect_file!["./test_data/test_doc_alias.txt"].assert_debug_eq(&symbols);
783    }
784
785    #[test]
786    fn test_exclude_imports() {
787        let (mut db, _) = RootDatabase::with_many_files(
788            r#"
789//- /lib.rs
790mod foo;
791pub use foo::Foo;
792
793//- /foo.rs
794pub struct Foo;
795"#,
796        );
797
798        let mut local_roots = FxHashSet::default();
799        local_roots.insert(WORKSPACE);
800        LocalRoots::get(&db).set_roots(&mut db).to(local_roots);
801
802        let mut query = Query::new("Foo".to_owned());
803        let mut symbols = world_symbols(&db, query.clone());
804        symbols.sort_by_key(|x| x.is_import);
805        expect_file!["./test_data/test_symbols_with_imports.txt"].assert_debug_eq(&symbols);
806
807        query.exclude_imports();
808        let symbols = world_symbols(&db, query);
809        expect_file!["./test_data/test_symbols_exclude_imports.txt"].assert_debug_eq(&symbols);
810    }
811
812    #[test]
813    fn test_parse_path_query() {
814        // Plain query - no path
815        let (path, item, anchor) = Query::parse_path_query("Item");
816        assert_eq!(path, Vec::<String>::new());
817        assert_eq!(item, "Item");
818        assert!(!anchor);
819
820        // Path with item
821        let (path, item, anchor) = Query::parse_path_query("foo::Item");
822        assert_eq!(path, vec!["foo"]);
823        assert_eq!(item, "Item");
824        assert!(!anchor);
825
826        // Multi-segment path
827        let (path, item, anchor) = Query::parse_path_query("foo::bar::Item");
828        assert_eq!(path, vec!["foo", "bar"]);
829        assert_eq!(item, "Item");
830        assert!(!anchor);
831
832        // Leading :: (anchor to crate)
833        let (path, item, anchor) = Query::parse_path_query("::std::vec::Vec");
834        assert_eq!(path, vec!["std", "vec"]);
835        assert_eq!(item, "Vec");
836        assert!(anchor);
837
838        // Just "::" - return all crates
839        let (path, item, anchor) = Query::parse_path_query("::");
840        assert_eq!(path, Vec::<String>::new());
841        assert_eq!(item, "");
842        assert!(anchor);
843
844        // "::foo" - fuzzy search crate names
845        let (path, item, anchor) = Query::parse_path_query("::foo");
846        assert_eq!(path, Vec::<String>::new());
847        assert_eq!(item, "foo");
848        assert!(anchor);
849
850        // Trailing ::
851        let (path, item, anchor) = Query::parse_path_query("foo::");
852        assert_eq!(path, vec!["foo"]);
853        assert_eq!(item, "");
854        assert!(!anchor);
855
856        // Full path with trailing ::
857        let (path, item, anchor) = Query::parse_path_query("foo::bar::");
858        assert_eq!(path, vec!["foo", "bar"]);
859        assert_eq!(item, "");
860        assert!(!anchor);
861
862        // Absolute path with trailing ::
863        let (path, item, anchor) = Query::parse_path_query("::std::vec::");
864        assert_eq!(path, vec!["std", "vec"]);
865        assert_eq!(item, "");
866        assert!(anchor);
867
868        // Empty segments should be filtered
869        let (path, item, anchor) = Query::parse_path_query("foo::::bar");
870        assert_eq!(path, vec!["foo"]);
871        assert_eq!(item, "bar");
872        assert!(!anchor);
873    }
874
875    #[test]
876    fn test_path_search() {
877        let (mut db, _) = RootDatabase::with_many_files(
878            r#"
879//- /lib.rs crate:main
880mod inner;
881pub struct RootStruct;
882
883//- /inner.rs
884pub struct InnerStruct;
885pub mod nested {
886    pub struct NestedStruct;
887}
888"#,
889        );
890
891        let mut local_roots = FxHashSet::default();
892        local_roots.insert(WORKSPACE);
893        LocalRoots::get(&db).set_roots(&mut db).to(local_roots);
894
895        // Search for item in specific module
896        let query = Query::new("inner::InnerStruct".to_owned());
897        let symbols = world_symbols(&db, query);
898        let names: Vec<_> = symbols.iter().map(|s| s.name.as_str()).collect();
899        assert!(names.contains(&"InnerStruct"), "Expected InnerStruct in {:?}", names);
900
901        // Search for item in nested module
902        let query = Query::new("inner::nested::NestedStruct".to_owned());
903        let symbols = world_symbols(&db, query);
904        let names: Vec<_> = symbols.iter().map(|s| s.name.as_str()).collect();
905        assert!(names.contains(&"NestedStruct"), "Expected NestedStruct in {:?}", names);
906
907        // Search with crate prefix
908        let query = Query::new("main::inner::InnerStruct".to_owned());
909        let symbols = world_symbols(&db, query);
910        let names: Vec<_> = symbols.iter().map(|s| s.name.as_str()).collect();
911        assert!(names.contains(&"InnerStruct"), "Expected InnerStruct in {:?}", names);
912
913        // Wrong path should return empty
914        let query = Query::new("wrong::InnerStruct".to_owned());
915        let symbols = world_symbols(&db, query);
916        assert!(symbols.is_empty(), "Expected empty results for wrong path");
917    }
918
919    #[test]
920    fn test_path_search_module() {
921        let (mut db, _) = RootDatabase::with_many_files(
922            r#"
923//- /lib.rs crate:main
924mod mymod;
925
926//- /mymod.rs
927pub struct MyStruct;
928pub fn my_func() {}
929pub const MY_CONST: u32 = 1;
930"#,
931        );
932
933        let mut local_roots = FxHashSet::default();
934        local_roots.insert(WORKSPACE);
935        LocalRoots::get(&db).set_roots(&mut db).to(local_roots);
936
937        // Browse all items in module
938        let query = Query::new("main::mymod::".to_owned());
939        let symbols = world_symbols(&db, query);
940        let names: Vec<_> = symbols.iter().map(|s| s.name.as_str()).collect();
941
942        assert!(names.contains(&"MyStruct"), "Expected MyStruct in {:?}", names);
943        assert!(names.contains(&"my_func"), "Expected my_func in {:?}", names);
944        assert!(names.contains(&"MY_CONST"), "Expected MY_CONST in {:?}", names);
945    }
946
947    #[test]
948    fn test_fuzzy_item_with_path() {
949        let (mut db, _) = RootDatabase::with_many_files(
950            r#"
951//- /lib.rs crate:main
952mod mymod;
953
954//- /mymod.rs
955pub struct MyLongStructName;
956"#,
957        );
958
959        let mut local_roots = FxHashSet::default();
960        local_roots.insert(WORKSPACE);
961        LocalRoots::get(&db).set_roots(&mut db).to(local_roots);
962
963        // Fuzzy match on item name with exact path
964        let query = Query::new("main::mymod::MyLong".to_owned());
965        let symbols = world_symbols(&db, query);
966        let names: Vec<_> = symbols.iter().map(|s| s.name.as_str()).collect();
967        assert!(
968            names.contains(&"MyLongStructName"),
969            "Expected fuzzy match for MyLongStructName in {:?}",
970            names
971        );
972    }
973
974    #[test]
975    fn test_case_insensitive_path() {
976        let (mut db, _) = RootDatabase::with_many_files(
977            r#"
978//- /lib.rs crate:main
979mod MyMod;
980
981//- /MyMod.rs
982pub struct MyStruct;
983"#,
984        );
985
986        let mut local_roots = FxHashSet::default();
987        local_roots.insert(WORKSPACE);
988        LocalRoots::get(&db).set_roots(&mut db).to(local_roots);
989
990        // Case insensitive path matching (default)
991        let query = Query::new("main::mymod::MyStruct".to_owned());
992        let symbols = world_symbols(&db, query);
993        let names: Vec<_> = symbols.iter().map(|s| s.name.as_str()).collect();
994        assert!(names.contains(&"MyStruct"), "Expected case-insensitive match in {:?}", names);
995    }
996
997    #[test]
998    fn test_absolute_path_search() {
999        let (mut db, _) = RootDatabase::with_many_files(
1000            r#"
1001//- /lib.rs crate:mycrate
1002mod inner;
1003pub struct CrateRoot;
1004
1005//- /inner.rs
1006pub struct InnerItem;
1007"#,
1008        );
1009
1010        let mut local_roots = FxHashSet::default();
1011        local_roots.insert(WORKSPACE);
1012        LocalRoots::get(&db).set_roots(&mut db).to(local_roots);
1013
1014        // Absolute path with leading ::
1015        let query = Query::new("::mycrate::inner::InnerItem".to_owned());
1016        let symbols = world_symbols(&db, query);
1017        let names: Vec<_> = symbols.iter().map(|s| s.name.as_str()).collect();
1018        assert!(
1019            names.contains(&"InnerItem"),
1020            "Expected InnerItem with absolute path in {:?}",
1021            names
1022        );
1023
1024        // Absolute path should NOT match if crate name is wrong
1025        let query = Query::new("::wrongcrate::inner::InnerItem".to_owned());
1026        let symbols = world_symbols(&db, query);
1027        assert!(symbols.is_empty(), "Expected empty results for wrong crate name");
1028    }
1029
1030    #[test]
1031    fn test_wrong_path_returns_empty() {
1032        let (mut db, _) = RootDatabase::with_many_files(
1033            r#"
1034//- /lib.rs crate:main
1035mod existing;
1036
1037//- /existing.rs
1038pub struct MyStruct;
1039"#,
1040        );
1041
1042        let mut local_roots = FxHashSet::default();
1043        local_roots.insert(WORKSPACE);
1044        LocalRoots::get(&db).set_roots(&mut db).to(local_roots);
1045
1046        // Non-existent module path
1047        let query = Query::new("nonexistent::MyStruct".to_owned());
1048        let symbols = world_symbols(&db, query);
1049        assert!(symbols.is_empty(), "Expected empty results for non-existent path");
1050
1051        // Correct item, wrong module
1052        let query = Query::new("wrongmod::MyStruct".to_owned());
1053        let symbols = world_symbols(&db, query);
1054        assert!(symbols.is_empty(), "Expected empty results for wrong module");
1055    }
1056
1057    #[test]
1058    fn test_root_module_items() {
1059        let (mut db, _) = RootDatabase::with_many_files(
1060            r#"
1061//- /lib.rs crate:mylib
1062pub struct RootItem;
1063pub fn root_fn() {}
1064"#,
1065        );
1066
1067        let mut local_roots = FxHashSet::default();
1068        local_roots.insert(WORKSPACE);
1069        LocalRoots::get(&db).set_roots(&mut db).to(local_roots);
1070
1071        // Items at crate root - path is just the crate name
1072        let query = Query::new("mylib::RootItem".to_owned());
1073        let symbols = world_symbols(&db, query);
1074        let names: Vec<_> = symbols.iter().map(|s| s.name.as_str()).collect();
1075        assert!(names.contains(&"RootItem"), "Expected RootItem at crate root in {:?}", names);
1076
1077        let query = Query::new("mylib::".to_owned());
1078        let symbols = world_symbols(&db, query);
1079        let names: Vec<_> = symbols.iter().map(|s| s.name.as_str()).collect();
1080        assert!(names.contains(&"RootItem"), "Expected RootItem {:?}", names);
1081        assert!(names.contains(&"root_fn"), "Expected root_fn {:?}", names);
1082    }
1083
1084    #[test]
1085    fn test_crate_search_all() {
1086        // Test that sole "::" returns all crates
1087        let (mut db, _) = RootDatabase::with_many_files(
1088            r#"
1089//- /lib.rs crate:alpha
1090pub struct AlphaStruct;
1091
1092//- /beta.rs crate:beta
1093pub struct BetaStruct;
1094
1095//- /gamma.rs crate:gamma
1096pub struct GammaStruct;
1097"#,
1098        );
1099
1100        let mut local_roots = FxHashSet::default();
1101        local_roots.insert(WORKSPACE);
1102        LocalRoots::get(&db).set_roots(&mut db).to(local_roots);
1103
1104        // Sole "::" should return all crates (as module symbols)
1105        let query = Query::new("::".to_owned());
1106        let symbols = world_symbols(&db, query);
1107        let names: Vec<_> = symbols.iter().map(|s| s.name.as_str()).collect();
1108
1109        assert!(names.contains(&"alpha"), "Expected alpha crate in {:?}", names);
1110        assert!(names.contains(&"beta"), "Expected beta crate in {:?}", names);
1111        assert!(names.contains(&"gamma"), "Expected gamma crate in {:?}", names);
1112        assert_eq!(symbols.len(), 3, "Expected exactly 3 crates, got {:?}", names);
1113    }
1114
1115    #[test]
1116    fn test_crate_search_fuzzy() {
1117        // Test that "::foo" fuzzy-matches crate names
1118        let (mut db, _) = RootDatabase::with_many_files(
1119            r#"
1120//- /lib.rs crate:my_awesome_lib
1121pub struct AwesomeStruct;
1122
1123//- /other.rs crate:another_lib
1124pub struct OtherStruct;
1125
1126//- /foo.rs crate:foobar
1127pub struct FooStruct;
1128"#,
1129        );
1130
1131        let mut local_roots = FxHashSet::default();
1132        local_roots.insert(WORKSPACE);
1133        LocalRoots::get(&db).set_roots(&mut db).to(local_roots);
1134
1135        // "::foo" should fuzzy-match crate names containing "foo"
1136        let query = Query::new("::foo".to_owned());
1137        let symbols = world_symbols(&db, query);
1138        let names: Vec<_> = symbols.iter().map(|s| s.name.as_str()).collect();
1139
1140        assert!(names.contains(&"foobar"), "Expected foobar crate in {:?}", names);
1141        assert_eq!(symbols.len(), 1, "Expected only foobar crate, got {:?}", names);
1142
1143        // "::awesome" should match my_awesome_lib
1144        let query = Query::new("::awesome".to_owned());
1145        let symbols = world_symbols(&db, query);
1146        let names: Vec<_> = symbols.iter().map(|s| s.name.as_str()).collect();
1147
1148        assert!(names.contains(&"my_awesome_lib"), "Expected my_awesome_lib crate in {:?}", names);
1149        assert_eq!(symbols.len(), 1, "Expected only my_awesome_lib crate, got {:?}", names);
1150
1151        // "::lib" should match multiple crates
1152        let query = Query::new("::lib".to_owned());
1153        let symbols = world_symbols(&db, query);
1154        let names: Vec<_> = symbols.iter().map(|s| s.name.as_str()).collect();
1155
1156        assert!(names.contains(&"my_awesome_lib"), "Expected my_awesome_lib in {:?}", names);
1157        assert!(names.contains(&"another_lib"), "Expected another_lib in {:?}", names);
1158        assert_eq!(symbols.len(), 2, "Expected 2 crates matching 'lib', got {:?}", names);
1159
1160        // "::nonexistent" should return empty
1161        let query = Query::new("::nonexistent".to_owned());
1162        let symbols = world_symbols(&db, query);
1163        assert!(symbols.is_empty(), "Expected empty results for non-matching crate pattern");
1164    }
1165
1166    #[test]
1167    fn test_path_search_with_use_reexport() {
1168        // Test that module resolution works for `use` items (re-exports), not just `mod` items
1169        let (mut db, _) = RootDatabase::with_many_files(
1170            r#"
1171//- /lib.rs crate:main
1172mod inner;
1173pub use inner::nested;
1174
1175//- /inner.rs
1176pub mod nested {
1177    pub struct NestedStruct;
1178    pub fn nested_fn() {}
1179}
1180"#,
1181        );
1182
1183        let mut local_roots = FxHashSet::default();
1184        local_roots.insert(WORKSPACE);
1185        LocalRoots::get(&db).set_roots(&mut db).to(local_roots);
1186
1187        // Search via the re-exported path (main::nested::NestedStruct)
1188        // This should work because `nested` is in scope via `pub use inner::nested`
1189        let query = Query::new("main::nested::NestedStruct".to_owned());
1190        let symbols = world_symbols(&db, query);
1191        let names: Vec<_> = symbols.iter().map(|s| s.name.as_str()).collect();
1192        assert!(
1193            names.contains(&"NestedStruct"),
1194            "Expected NestedStruct via re-exported path in {:?}",
1195            names
1196        );
1197
1198        // Also verify the original path still works
1199        let query = Query::new("main::inner::nested::NestedStruct".to_owned());
1200        let symbols = world_symbols(&db, query);
1201        let names: Vec<_> = symbols.iter().map(|s| s.name.as_str()).collect();
1202        assert!(
1203            names.contains(&"NestedStruct"),
1204            "Expected NestedStruct via original path in {:?}",
1205            names
1206        );
1207
1208        // Browse the re-exported module
1209        let query = Query::new("main::nested::".to_owned());
1210        let symbols = world_symbols(&db, query);
1211        let names: Vec<_> = symbols.iter().map(|s| s.name.as_str()).collect();
1212        assert!(
1213            names.contains(&"NestedStruct"),
1214            "Expected NestedStruct when browsing re-exported module in {:?}",
1215            names
1216        );
1217        assert!(
1218            names.contains(&"nested_fn"),
1219            "Expected nested_fn when browsing re-exported module in {:?}",
1220            names
1221        );
1222    }
1223}