Skip to main content

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    /// One `(lowercase_short_name, ClassRef)` pair per distinct class name,
85    /// sorted by the lowercase key. Built once per revision so completion's
86    /// prefix search can binary-search a range instead of scanning every
87    /// class name (and re-lowercasing it) on every keystroke.
88    pub classes_by_lowercase_name: Vec<(Box<str>, ClassRef)>,
89}
90
91pub(crate) type BuildMapsResult = (
92    HashMap<String, Vec<ClassRef>>,
93    HashMap<Arc<str>, Vec<ClassRef>>,
94    HashMap<String, Vec<DeclRef>>,
95    Vec<(Box<str>, ClassRef)>,
96);
97
98pub(crate) fn build_maps(files: &[(Url, Arc<FileIndex>)]) -> BuildMapsResult {
99    let mut classes_by_name: HashMap<String, Vec<ClassRef>> = HashMap::new();
100    let mut subtypes_of: HashMap<Arc<str>, Vec<ClassRef>> = HashMap::new();
101    let mut decls_by_name: HashMap<String, Vec<DeclRef>> = HashMap::new();
102    let push_decl = |map: &mut HashMap<String, Vec<DeclRef>>,
103                     name: &str,
104                     file: u32,
105                     line: u32,
106                     kind: DeclKind| {
107        map.entry(name.to_string())
108            .or_default()
109            .push(DeclRef { file, line, kind });
110    };
111    for (file_idx, (_, idx)) in files.iter().enumerate() {
112        let file_idx = file_idx as u32;
113        for f in &idx.functions {
114            push_decl(
115                &mut decls_by_name,
116                &f.name,
117                file_idx,
118                f.start_line,
119                DeclKind::Function,
120            );
121        }
122        for (cls_idx, cls) in idx.classes.iter().enumerate() {
123            let cr = ClassRef {
124                file: file_idx,
125                class: cls_idx as u32,
126            };
127            classes_by_name
128                .entry(cls.name.as_ref().to_string())
129                .or_default()
130                .push(cr);
131            if let Some(parent) = &cls.parent {
132                subtypes_of.entry(Arc::clone(parent)).or_default().push(cr);
133            }
134            for iface in &cls.implements {
135                subtypes_of.entry(Arc::clone(iface)).or_default().push(cr);
136                // If this implements name is a use-import alias, also index by
137                // the short name of the resolved FQN so cursor-on-interface-name
138                // lookups work regardless of how the implementor named the interface.
139                if let Some((_, fqn)) = idx
140                    .use_imports
141                    .iter()
142                    .find(|(alias, _)| alias.as_ref() == iface.as_ref())
143                {
144                    let short = crate::text::fqn_short_name(fqn);
145                    if short != iface.as_ref() {
146                        subtypes_of.entry(Arc::from(short)).or_default().push(cr);
147                    }
148                }
149            }
150            for trt in &cls.traits {
151                subtypes_of.entry(Arc::clone(trt)).or_default().push(cr);
152            }
153            push_decl(
154                &mut decls_by_name,
155                &cls.name,
156                file_idx,
157                cls.start_line,
158                DeclKind::Class,
159            );
160            for m in &cls.methods {
161                push_decl(
162                    &mut decls_by_name,
163                    &m.name,
164                    file_idx,
165                    m.start_line,
166                    DeclKind::Method,
167                );
168            }
169            for p in &cls.properties {
170                push_decl(
171                    &mut decls_by_name,
172                    &p.name,
173                    file_idx,
174                    p.start_line,
175                    DeclKind::Property,
176                );
177            }
178            for cc in &cls.constants {
179                push_decl(
180                    &mut decls_by_name,
181                    cc,
182                    file_idx,
183                    cls.start_line,
184                    DeclKind::Constant,
185                );
186            }
187            for case in &cls.cases {
188                push_decl(
189                    &mut decls_by_name,
190                    case,
191                    file_idx,
192                    cls.start_line,
193                    DeclKind::EnumCase,
194                );
195            }
196        }
197    }
198    let mut classes_by_lowercase_name: Vec<(Box<str>, ClassRef)> = classes_by_name
199        .iter()
200        .filter_map(|(name, refs)| refs.first().map(|cr| (name.to_lowercase().into(), *cr)))
201        .collect();
202    classes_by_lowercase_name.sort_unstable_by(|a, b| a.0.cmp(&b.0));
203    (
204        classes_by_name,
205        subtypes_of,
206        decls_by_name,
207        classes_by_lowercase_name,
208    )
209}
210
211impl WorkspaceIndexData {
212    /// Resolve a `ClassRef` back to its `(uri, class_def)` pair.
213    pub fn at(&self, r: ClassRef) -> Option<(&Url, &crate::index::file_index::ClassDef)> {
214        let (uri, idx) = self.files.get(r.file as usize)?;
215        let cls = idx.classes.get(r.class as usize)?;
216        Some((uri, cls))
217    }
218
219    /// O(1) replacement for the linear `find_declaration_in_indexes` scan:
220    /// find a declaration by name, optionally excluding one file (the current
221    /// document, which the caller has already searched with accurate AST
222    /// ranges). Matching rules mirror the old scan: a sigil query (`$foo`)
223    /// matches functions, classes, and properties named `foo`; a bare query
224    /// matches every declaration kind. Returns a zero-width line `Location`.
225    pub fn find_declaration(
226        &self,
227        name: &str,
228        exclude: Option<&Url>,
229    ) -> Option<tower_lsp::lsp_types::Location> {
230        let bare = crate::text::strip_variable_sigil(name);
231        let sigil = bare != name;
232        let refs = self.decls_by_name.get(bare)?;
233        for r in refs {
234            if sigil
235                && !matches!(
236                    r.kind,
237                    DeclKind::Function | DeclKind::Class | DeclKind::Property
238                )
239            {
240                continue;
241            }
242            let (uri, _) = self.files.get(r.file as usize)?;
243            if exclude.is_some_and(|e| e == uri) {
244                continue;
245            }
246            return Some(tower_lsp::lsp_types::Location {
247                uri: uri.clone(),
248                range: crate::text::zero_width_range(r.line),
249            });
250        }
251        None
252    }
253
254    /// Constructor that builds the reverse maps from an already-materialised
255    /// `(Url, Arc<FileIndex>)` slice. Exposed so callers that don't want to
256    /// spin up a full `AnalysisHost` (unit tests of
257    /// `find_implementations_from_workspace`, benchmark crates) can exercise
258    /// the aggregate-shaped helpers directly. Production code goes through
259    /// the `workspace_index` salsa query instead.
260    pub fn from_files(files: Vec<(Url, Arc<FileIndex>)>) -> Self {
261        let (classes_by_name, subtypes_of, decls_by_name, classes_by_lowercase_name) =
262            build_maps(&files);
263        Self {
264            files,
265            classes_by_name,
266            subtypes_of,
267            decls_by_name,
268            classes_by_lowercase_name,
269        }
270    }
271}
272
273/// Arc wrapper with the same `Arc::ptr_eq`-based `Update` impl used throughout
274/// `src/db/`. The inner `WorkspaceIndexData` never compares structurally.
275#[derive(Clone)]
276pub struct WorkspaceIndexArc(pub Arc<WorkspaceIndexData>);
277
278impl WorkspaceIndexArc {
279    #[cfg(test)]
280    pub fn get(&self) -> &WorkspaceIndexData {
281        &self.0
282    }
283}
284
285// SAFETY: same contract as other `*Arc` newtypes — ptr_eq is sufficient because
286// every rebuild allocates a fresh `Arc`.
287crate::impl_arc_update!(WorkspaceIndexArc);