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:
12//!
13//! - `files`: the flat `(Url, Arc<FileIndex>)` list every handler used to
14//! rebuild by hand,
15//! - `classes_by_name`: `name → [ClassRef]` for constant-time prepare /
16//! supertype resolution,
17//! - `subtypes_of`: `name → [ClassRef]` for constant-time subtype /
18//! implementation lookups.
19//!
20//! All lookups on the aggregate run in memory against an already-materialised
21//! `Arc`; edits invalidate the aggregate through `file_index` dependency
22//! tracking as usual.
23//!
24//! Vocabulary note: the `-Ref` types here (`ClassRef`, `DeclRef`) are internal
25//! back-pointers/handles into the index — *not* LSP references. A symbol usage
26//! in code is a "reference" spelled out (see `navigation/references.rs`). See
27//! the crate-root glossary in `lib.rs`.
28
29use std::collections::HashMap;
30use std::sync::Arc;
31
32use tower_lsp::lsp_types::Url;
33
34use crate::index::file_index::FileIndex;
35
36/// Back-pointer into `WorkspaceIndexData.files`: `(file_idx, class_idx)` where
37/// `class_idx` indexes into `files[file_idx].1.classes`.
38#[derive(Debug, Clone, Copy)]
39pub struct ClassRef {
40 pub file: u32,
41 pub class: u32,
42}
43
44/// What kind of declaration a [`DeclRef`] points at. Drives the per-kind
45/// matching rules in [`WorkspaceIndexData::find_declaration`] (e.g. a `$foo`
46/// query matches properties and functions/classes named `foo`, but never
47/// methods or constants).
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub enum DeclKind {
50 Function,
51 Class,
52 Method,
53 Property,
54 Constant,
55 EnumCase,
56}
57
58/// One named declaration, pre-resolved to its file and (zero-width) line.
59/// Stored in encounter order — file order, then within a file: functions
60/// first, then per class: the class itself, methods, properties, constants,
61/// enum cases — which is exactly the precedence the old linear scan in
62/// `find_declaration_in_indexes` had.
63#[derive(Debug, Clone, Copy)]
64pub struct DeclRef {
65 pub file: u32,
66 pub line: u32,
67 pub kind: DeclKind,
68}
69
70/// Aggregated workspace-level index. Constructed once per salsa revision by
71/// `workspace_index` and held behind an `Arc` for cheap cross-request sharing.
72pub struct WorkspaceIndexData {
73 pub files: Vec<(Url, Arc<FileIndex>)>,
74 pub classes_by_name: HashMap<String, Vec<ClassRef>>,
75 /// `parent_or_interface_or_trait_name → [subtype ClassRef]`.
76 /// A class that extends `X` AND implements `Y` contributes separate entries
77 /// under both keys. Keyed by `Arc<str>` so insertions from `ClassDef`'s
78 /// already-interned fields are pointer copies rather than heap allocations.
79 pub subtypes_of: HashMap<Arc<str>, Vec<ClassRef>>,
80 /// `declared_name → [DeclRef]` over every function, class, method,
81 /// property (stored without `$`), class constant, and enum case in the
82 /// workspace; enables O(1) go-to-definition lookups.
83 pub decls_by_name: HashMap<String, Vec<DeclRef>>,
84}
85
86pub(crate) type BuildMapsResult = (
87 HashMap<String, Vec<ClassRef>>,
88 HashMap<Arc<str>, Vec<ClassRef>>,
89 HashMap<String, Vec<DeclRef>>,
90);
91
92pub(crate) fn build_maps(files: &[(Url, Arc<FileIndex>)]) -> BuildMapsResult {
93 let mut classes_by_name: HashMap<String, Vec<ClassRef>> = HashMap::new();
94 let mut subtypes_of: HashMap<Arc<str>, Vec<ClassRef>> = HashMap::new();
95 let mut decls_by_name: HashMap<String, Vec<DeclRef>> = HashMap::new();
96 let push_decl = |map: &mut HashMap<String, Vec<DeclRef>>,
97 name: &str,
98 file: u32,
99 line: u32,
100 kind: DeclKind| {
101 map.entry(name.to_string())
102 .or_default()
103 .push(DeclRef { file, line, kind });
104 };
105 for (file_idx, (_, idx)) in files.iter().enumerate() {
106 let file_idx = file_idx as u32;
107 for f in &idx.functions {
108 push_decl(
109 &mut decls_by_name,
110 &f.name,
111 file_idx,
112 f.start_line,
113 DeclKind::Function,
114 );
115 }
116 for (cls_idx, cls) in idx.classes.iter().enumerate() {
117 let cr = ClassRef {
118 file: file_idx,
119 class: cls_idx as u32,
120 };
121 classes_by_name
122 .entry(cls.name.as_ref().to_string())
123 .or_default()
124 .push(cr);
125 if let Some(parent) = &cls.parent {
126 subtypes_of.entry(Arc::clone(parent)).or_default().push(cr);
127 }
128 for iface in &cls.implements {
129 subtypes_of.entry(Arc::clone(iface)).or_default().push(cr);
130 // If this implements name is a use-import alias, also index by
131 // the short name of the resolved FQN so cursor-on-interface-name
132 // lookups work regardless of how the implementor named the interface.
133 if let Some((_, fqn)) = idx
134 .use_imports
135 .iter()
136 .find(|(alias, _)| alias.as_ref() == iface.as_ref())
137 {
138 let short = crate::text::fqn_short_name(fqn);
139 if short != iface.as_ref() {
140 subtypes_of.entry(Arc::from(short)).or_default().push(cr);
141 }
142 }
143 }
144 for trt in &cls.traits {
145 subtypes_of.entry(Arc::clone(trt)).or_default().push(cr);
146 }
147 push_decl(
148 &mut decls_by_name,
149 &cls.name,
150 file_idx,
151 cls.start_line,
152 DeclKind::Class,
153 );
154 for m in &cls.methods {
155 push_decl(
156 &mut decls_by_name,
157 &m.name,
158 file_idx,
159 m.start_line,
160 DeclKind::Method,
161 );
162 }
163 for p in &cls.properties {
164 push_decl(
165 &mut decls_by_name,
166 &p.name,
167 file_idx,
168 p.start_line,
169 DeclKind::Property,
170 );
171 }
172 for cc in &cls.constants {
173 push_decl(
174 &mut decls_by_name,
175 cc,
176 file_idx,
177 cls.start_line,
178 DeclKind::Constant,
179 );
180 }
181 for case in &cls.cases {
182 push_decl(
183 &mut decls_by_name,
184 case,
185 file_idx,
186 cls.start_line,
187 DeclKind::EnumCase,
188 );
189 }
190 }
191 }
192 (classes_by_name, subtypes_of, decls_by_name)
193}
194
195impl WorkspaceIndexData {
196 /// Resolve a `ClassRef` back to its `(uri, class_def)` pair.
197 pub fn at(&self, r: ClassRef) -> Option<(&Url, &crate::index::file_index::ClassDef)> {
198 let (uri, idx) = self.files.get(r.file as usize)?;
199 let cls = idx.classes.get(r.class as usize)?;
200 Some((uri, cls))
201 }
202
203 /// O(1) replacement for the linear `find_declaration_in_indexes` scan:
204 /// find a declaration by name, optionally excluding one file (the current
205 /// document, which the caller has already searched with accurate AST
206 /// ranges). Matching rules mirror the old scan: a sigil query (`$foo`)
207 /// matches functions, classes, and properties named `foo`; a bare query
208 /// matches every declaration kind. Returns a zero-width line `Location`.
209 pub fn find_declaration(
210 &self,
211 name: &str,
212 exclude: Option<&Url>,
213 ) -> Option<tower_lsp::lsp_types::Location> {
214 let bare = crate::text::strip_variable_sigil(name);
215 let sigil = bare != name;
216 let refs = self.decls_by_name.get(bare)?;
217 for r in refs {
218 if sigil
219 && !matches!(
220 r.kind,
221 DeclKind::Function | DeclKind::Class | DeclKind::Property
222 )
223 {
224 continue;
225 }
226 let (uri, _) = self.files.get(r.file as usize)?;
227 if exclude.is_some_and(|e| e == uri) {
228 continue;
229 }
230 return Some(tower_lsp::lsp_types::Location {
231 uri: uri.clone(),
232 range: crate::text::zero_width_range(r.line),
233 });
234 }
235 None
236 }
237
238 /// Constructor that builds the reverse maps from an already-materialised
239 /// `(Url, Arc<FileIndex>)` slice. Exposed so callers that don't want to
240 /// spin up a full `AnalysisHost` (unit tests of
241 /// `find_implementations_from_workspace`, benchmark crates) can exercise
242 /// the aggregate-shaped helpers directly. Production code goes through
243 /// the `workspace_index` salsa query instead.
244 pub fn from_files(files: Vec<(Url, Arc<FileIndex>)>) -> Self {
245 let (classes_by_name, subtypes_of, decls_by_name) = build_maps(&files);
246 Self {
247 files,
248 classes_by_name,
249 subtypes_of,
250 decls_by_name,
251 }
252 }
253}
254
255/// Arc wrapper with the same `Arc::ptr_eq`-based `Update` impl used throughout
256/// `src/db/`. The inner `WorkspaceIndexData` never compares structurally.
257#[derive(Clone)]
258pub struct WorkspaceIndexArc(pub Arc<WorkspaceIndexData>);
259
260impl WorkspaceIndexArc {
261 #[cfg(test)]
262 pub fn get(&self) -> &WorkspaceIndexData {
263 &self.0
264 }
265}
266
267// SAFETY: same contract as other `*Arc` newtypes — ptr_eq is sufficient because
268// every rebuild allocates a fresh `Arc`.
269crate::impl_arc_update!(WorkspaceIndexArc);