php_lsp/db/workspace_index.rs
1//! `workspace_index` salsa query — aggregates every file's `FileIndex` into a
2//! single structure with pre-built reverse maps.
3//!
4//! Before Phase J, cross-file queries (`workspace_symbols`,
5//! `prepare_type_hierarchy`, `supertypes_of`, `subtypes_of`,
6//! `find_implementations`) called `DocumentStore::all_indexes()` on every
7//! request. `all_indexes()` takes the host mutex once per file via
8//! `get_index_salsa` → `snapshot_query`, so a workspace with 1600 files
9//! paid 1600 lock acquisitions per lookup.
10//!
11//! This query runs once per workspace revision and returns `files`: the flat
12//! `(Uri, Arc<FileIndex>)` list every handler used to rebuild by hand, plus
13//! `classes_by_lowercase_name` for completion's prefix search.
14//!
15//! All lookups on the aggregate run in memory against an already-materialised
16//! `Arc`; edits invalidate the aggregate through `file_index` dependency
17//! tracking as usual.
18//!
19//! Vocabulary note: `ClassRef` is an internal back-pointer/handle into the
20//! index — *not* an LSP reference. A symbol usage in code is a "reference"
21//! spelled out (see `navigation/references.rs`). See the crate-root glossary
22//! in `lib.rs`.
23//!
24//! Short-name class lookup and disambiguation (`DocumentStore::
25//! resolve_class_ref`/`class_candidates`) and arbitrary-name declaration
26//! lookup (`DocumentStore::declaration_candidate_files`/`find_declaration`)
27//! no longer have eagerly rebuilt-per-revision maps here. Both narrow
28//! candidates via mir's persistent per-file mention cache instead: an edit
29//! invalidates only that file's cached mention set, not the whole
30//! workspace's. Only `classes_by_lowercase_name` remains eager — prefix
31//! search genuinely needs to enumerate every distinct name, which a
32//! point-query mention cache can't answer.
33
34use std::collections::HashMap;
35use std::sync::Arc;
36
37use tower_lsp_server::ls_types::Uri;
38
39use crate::index::file_index::FileIndex;
40
41/// Back-pointer into `WorkspaceIndexData.files`: `(file_idx, class_idx)` where
42/// `class_idx` indexes into `files[file_idx].1.classes`.
43#[derive(Debug, Clone, Copy)]
44pub struct ClassRef {
45 pub file: u32,
46 pub class: u32,
47}
48
49/// Aggregated workspace-level index. Constructed once per salsa revision by
50/// `workspace_index` and held behind an `Arc` for cheap cross-request sharing.
51pub struct WorkspaceIndexData {
52 pub files: Vec<(Uri, Arc<FileIndex>)>,
53 /// `files`' URIs as mir path strings, precomputed once per revision so
54 /// `DocumentStore::declaration_candidate_files`/`class_candidates` don't
55 /// re-allocate one `Arc<str>` per file on every call.
56 pub(crate) file_paths: Vec<Arc<str>>,
57 /// `file_paths[i] → i`, for O(1) resolution of a mention-index candidate
58 /// path back to its `files`/`file_paths` index (no per-candidate linear
59 /// scan or `Uri` round-trip).
60 pub(crate) path_to_file_idx: HashMap<Arc<str>, u32>,
61 /// One `(lowercase_short_name, ClassRef)` pair per distinct class name,
62 /// sorted by the lowercase key. Built once per revision so completion's
63 /// prefix search can binary-search a range instead of scanning every
64 /// class name (and re-lowercasing it) on every keystroke.
65 pub classes_by_lowercase_name: Vec<(Box<str>, ClassRef)>,
66}
67
68/// One entry per distinct class name (first-encountered `ClassRef` wins —
69/// matches the old `classes_by_name`-derived construction), for
70/// [`WorkspaceIndexData::classes_by_lowercase_name`]'s sorted table. No
71/// longer builds a full name→candidates map or a subtype reverse map: every
72/// other caller now resolves those via mir's mention index
73/// (`DocumentStore::class_candidates`/`resolve_class_ref`/
74/// `declaration_candidate_files`) instead of an eagerly rebuilt structure.
75pub(crate) fn build_maps(files: &[(Uri, Arc<FileIndex>)]) -> Vec<(Box<str>, ClassRef)> {
76 let mut first_by_name: HashMap<Box<str>, ClassRef> = HashMap::new();
77 for (file_idx, (_, idx)) in files.iter().enumerate() {
78 let file_idx = file_idx as u32;
79 for (cls_idx, cls) in idx.classes.iter().enumerate() {
80 first_by_name
81 .entry(cls.name.as_ref().into())
82 .or_insert(ClassRef {
83 file: file_idx,
84 class: cls_idx as u32,
85 });
86 }
87 }
88 let mut classes_by_lowercase_name: Vec<(Box<str>, ClassRef)> = first_by_name
89 .into_iter()
90 .map(|(name, cr)| (name.to_lowercase().into(), cr))
91 .collect();
92 classes_by_lowercase_name.sort_unstable_by(|a, b| a.0.cmp(&b.0));
93 classes_by_lowercase_name
94}
95
96impl WorkspaceIndexData {
97 /// Resolve a `ClassRef` back to its `(uri, class_def)` pair.
98 pub fn at(&self, r: ClassRef) -> Option<(&Uri, &crate::index::file_index::ClassDef)> {
99 let (uri, idx) = self.files.get(r.file as usize)?;
100 let cls = idx.classes.get(r.class as usize)?;
101 Some((uri, cls))
102 }
103
104 /// Visit every class-like declaration stored in the aggregate.
105 pub fn for_each_class(&self, mut f: impl FnMut(&Uri, &crate::index::file_index::ClassDef)) {
106 for (uri, idx) in &self.files {
107 for cls in &idx.classes {
108 f(uri, cls);
109 }
110 }
111 }
112
113 /// Visit every class-like declaration in the files named by `uris`.
114 /// Missing paths are skipped; callers typically source `uris` from mir's
115 /// mention or subtype indexes, which are candidate sets rather than proof.
116 pub fn for_each_class_in_uris(
117 &self,
118 uris: &[Uri],
119 mut f: impl FnMut(&Uri, &crate::index::file_index::ClassDef),
120 ) {
121 for uri in uris {
122 let Some(&file_idx) = self.path_to_file_idx.get(uri.as_str()) else {
123 continue;
124 };
125 let Some((stored_uri, idx)) = self.files.get(file_idx as usize) else {
126 continue;
127 };
128 for cls in &idx.classes {
129 f(stored_uri, cls);
130 }
131 }
132 }
133
134 /// Constructor that builds the reverse maps from an already-materialised
135 /// `(Uri, Arc<FileIndex>)` slice. Exposed so callers that don't want to
136 /// spin up a full `AnalysisHost` (unit tests of
137 /// `find_implementations_from_workspace`, benchmark crates) can exercise
138 /// the aggregate-shaped helpers directly. Production code goes through
139 /// the `workspace_index` salsa query instead.
140 pub fn from_files(files: Vec<(Uri, Arc<FileIndex>)>) -> Self {
141 let classes_by_lowercase_name = build_maps(&files);
142 let file_paths: Vec<Arc<str>> = files.iter().map(|(u, _)| Arc::from(u.as_str())).collect();
143 let path_to_file_idx = file_paths
144 .iter()
145 .enumerate()
146 .map(|(i, p)| (Arc::clone(p), i as u32))
147 .collect();
148 Self {
149 files,
150 file_paths,
151 path_to_file_idx,
152 classes_by_lowercase_name,
153 }
154 }
155}
156
157/// Arc wrapper for `workspace_index`, which is `#[salsa::tracked(no_eq)]` —
158/// every rebuild allocates a fresh `Arc` and salsa never attempts to compare
159/// `WorkspaceIndexData` structurally.
160#[derive(Clone)]
161pub struct WorkspaceIndexArc(pub Arc<WorkspaceIndexData>);
162
163impl WorkspaceIndexArc {
164 #[cfg(test)]
165 pub fn get(&self) -> &WorkspaceIndexData {
166 &self.0
167 }
168}