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 `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}