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/// Back-pointer into `WorkspaceIndexData.files`: `(file_idx, function_idx)`
50/// where `function_idx` indexes into `files[file_idx].1.functions`.
51#[derive(Debug, Clone, Copy)]
52pub struct FunctionRef {
53 pub file: u32,
54 pub function: u32,
55}
56
57/// Aggregated workspace-level index. Constructed once per salsa revision by
58/// `workspace_index` and held behind an `Arc` for cheap cross-request sharing.
59pub struct WorkspaceIndexData {
60 pub files: Vec<(Uri, Arc<FileIndex>)>,
61 /// `files`' URIs as mir path strings, precomputed once per revision so
62 /// `DocumentStore::declaration_candidate_files`/`class_candidates` don't
63 /// re-allocate one `Arc<str>` per file on every call.
64 pub(crate) file_paths: Vec<Arc<str>>,
65 /// `file_paths[i] → i`, for O(1) resolution of a mention-index candidate
66 /// path back to its `files`/`file_paths` index (no per-candidate linear
67 /// scan or `Uri` round-trip).
68 pub(crate) path_to_file_idx: HashMap<Arc<str>, u32>,
69 /// One `(lowercase_short_name, ClassRef)` pair per distinct class name,
70 /// sorted by the lowercase key. Built once per revision so completion's
71 /// prefix search can binary-search a range instead of scanning every
72 /// class name (and re-lowercasing it) on every keystroke.
73 pub classes_by_lowercase_name: Vec<(Box<str>, ClassRef)>,
74}
75
76/// One entry per distinct class name (first-encountered `ClassRef` wins —
77/// matches the old `classes_by_name`-derived construction), for
78/// [`WorkspaceIndexData::classes_by_lowercase_name`]'s sorted table. No
79/// longer builds a full name→candidates map or a subtype reverse map: every
80/// other caller now resolves those via mir's mention index
81/// (`DocumentStore::class_candidates`/`resolve_class_ref`/
82/// `declaration_candidate_files`) instead of an eagerly rebuilt structure.
83pub(crate) fn build_maps(files: &[(Uri, Arc<FileIndex>)]) -> Vec<(Box<str>, ClassRef)> {
84 let mut first_by_name: HashMap<Box<str>, ClassRef> = HashMap::new();
85 for (file_idx, (_, idx)) in files.iter().enumerate() {
86 let file_idx = file_idx as u32;
87 for (cls_idx, cls) in idx.classes.iter().enumerate() {
88 first_by_name
89 .entry(cls.name.as_ref().into())
90 .or_insert(ClassRef {
91 file: file_idx,
92 class: cls_idx as u32,
93 });
94 }
95 }
96 let mut classes_by_lowercase_name: Vec<(Box<str>, ClassRef)> = first_by_name
97 .into_iter()
98 .map(|(name, cr)| (name.to_lowercase().into(), cr))
99 .collect();
100 classes_by_lowercase_name.sort_unstable_by(|a, b| a.0.cmp(&b.0));
101 classes_by_lowercase_name
102}
103
104impl WorkspaceIndexData {
105 /// Resolve a `ClassRef` back to its `(uri, class_def)` pair.
106 pub fn at(&self, r: ClassRef) -> Option<(&Uri, &crate::index::file_index::ClassDef)> {
107 let (uri, idx) = self.files.get(r.file as usize)?;
108 let cls = idx.classes.get(r.class as usize)?;
109 Some((uri, cls))
110 }
111
112 /// Resolve a `FunctionRef` back to its `(uri, function_def)` pair.
113 pub fn function_at(
114 &self,
115 r: FunctionRef,
116 ) -> Option<(&Uri, &crate::index::file_index::FunctionDef)> {
117 let (uri, idx) = self.files.get(r.file as usize)?;
118 let function = idx.functions.get(r.function as usize)?;
119 Some((uri, function))
120 }
121
122 /// Return the canonical FQN when exactly one workspace class has `name`
123 /// as its short name. A duplicate FQN is harmless; distinct FQNs make a
124 /// short-name lookup ambiguous.
125 ///
126 /// This is deliberately a scan rather than another eager reverse map:
127 /// short-name disambiguation is only needed by infrequent interactive
128 /// operations, whereas completion needs its dedicated sorted index.
129 pub fn unique_class_fqn_by_short_name(&self, name: &str) -> Option<&str> {
130 let mut candidate = None;
131 for (_, index) in &self.files {
132 for class in &index.classes {
133 if !class.name.eq_ignore_ascii_case(name) {
134 continue;
135 }
136 match candidate {
137 None => candidate = Some(class.fqn.as_ref()),
138 Some(fqn) if fqn.eq_ignore_ascii_case(&class.fqn) => {}
139 Some(_) => return None,
140 }
141 }
142 }
143 candidate
144 }
145
146 /// Visit every class-like declaration stored in the aggregate.
147 pub fn for_each_class(&self, mut f: impl FnMut(&Uri, &crate::index::file_index::ClassDef)) {
148 for (uri, idx) in &self.files {
149 for cls in &idx.classes {
150 f(uri, cls);
151 }
152 }
153 }
154
155 /// Visit every class-like declaration in the files named by `uris`.
156 /// Missing paths are skipped; callers typically source `uris` from mir's
157 /// mention or subtype indexes, which are candidate sets rather than proof.
158 pub fn for_each_class_in_uris(
159 &self,
160 uris: &[Uri],
161 mut f: impl FnMut(&Uri, &crate::index::file_index::ClassDef),
162 ) {
163 for uri in uris {
164 let Some(&file_idx) = self.path_to_file_idx.get(uri.as_str()) else {
165 continue;
166 };
167 let Some((stored_uri, idx)) = self.files.get(file_idx as usize) else {
168 continue;
169 };
170 for cls in &idx.classes {
171 f(stored_uri, cls);
172 }
173 }
174 }
175
176 /// Constructor that builds the reverse maps from an already-materialised
177 /// `(Uri, Arc<FileIndex>)` slice. Exposed so callers that don't want to
178 /// spin up a full `AnalysisHost` (unit tests of
179 /// `find_implementations_from_workspace`, benchmark crates) can exercise
180 /// the aggregate-shaped helpers directly. Production code goes through
181 /// the `workspace_index` salsa query instead.
182 pub fn from_files(files: Vec<(Uri, Arc<FileIndex>)>) -> Self {
183 let classes_by_lowercase_name = build_maps(&files);
184 let file_paths: Vec<Arc<str>> = files.iter().map(|(u, _)| Arc::from(u.as_str())).collect();
185 let path_to_file_idx = file_paths
186 .iter()
187 .enumerate()
188 .map(|(i, p)| (Arc::clone(p), i as u32))
189 .collect();
190 Self {
191 files,
192 file_paths,
193 path_to_file_idx,
194 classes_by_lowercase_name,
195 }
196 }
197}
198
199/// Arc wrapper for `workspace_index`, which is `#[salsa::tracked(no_eq)]` —
200/// every rebuild allocates a fresh `Arc` and salsa never attempts to compare
201/// `WorkspaceIndexData` structurally.
202#[derive(Clone)]
203pub struct WorkspaceIndexArc(pub Arc<WorkspaceIndexData>);
204
205impl WorkspaceIndexArc {
206 #[cfg(test)]
207 pub fn get(&self) -> &WorkspaceIndexData {
208 &self.0
209 }
210}