Skip to main content

ryo_analysis/symbol/
file_registry.rs

1//! FileRegistry - Central file management registry.
2//!
3//! # Single Point of Access
4//!
5//! All file operations must go through FileRegistry:
6//! - Get FileId: `register()` or `lookup()`
7//! - Get metadata: `path()`, `crate_name()`, etc.
8//! - FileId is the only way to reference files internally
9
10use super::FileId;
11use ryo_symbol::WorkspaceFilePath;
12use serde::Serialize;
13use slotmap::SlotMap;
14use std::collections::HashMap;
15
16/// Invalid FileId error.
17///
18/// Indicates access to a non-existent or removed file.
19#[derive(Debug, Clone, thiserror::Error)]
20#[error("invalid file id: {0:?}")]
21pub struct InvalidFileId(pub FileId);
22
23/// Central file management registry.
24///
25/// # Responsibilities
26/// - Bidirectional conversion: WorkspaceFilePath ↔ FileId
27/// - File lifecycle management
28///
29/// # Thread Safety
30/// - Read operations are thread-safe
31/// - Write operations require exclusive control
32#[derive(Clone, Serialize)]
33pub struct FileRegistry {
34    /// FileId → WorkspaceFilePath (reverse lookup)
35    id_to_path: SlotMap<FileId, WorkspaceFilePath>,
36
37    /// WorkspaceFilePath → FileId (forward lookup)
38    path_to_id: HashMap<WorkspaceFilePath, FileId>,
39}
40
41impl FileRegistry {
42    /// Create a new registry.
43    pub fn new() -> Self {
44        Self {
45            id_to_path: SlotMap::with_key(),
46            path_to_id: HashMap::new(),
47        }
48    }
49
50    /// Create a registry with pre-allocated capacity.
51    pub fn with_capacity(capacity: usize) -> Self {
52        Self {
53            id_to_path: SlotMap::with_capacity_and_key(capacity),
54            path_to_id: HashMap::with_capacity(capacity),
55        }
56    }
57
58    // === Registration ===
59
60    /// Register a file (returns existing ID if already registered).
61    ///
62    /// # Returns
63    /// - `FileId`: Registration success (new or existing)
64    pub fn register(&mut self, path: WorkspaceFilePath) -> FileId {
65        // Check if already registered
66        if let Some(&existing_id) = self.path_to_id.get(&path) {
67            return existing_id;
68        }
69
70        // Register new file
71        let id = self.id_to_path.insert(path.clone());
72        self.path_to_id.insert(path, id);
73
74        id
75    }
76
77    /// Remove a file from the registry.
78    ///
79    /// # Returns
80    /// - `Some(WorkspaceFilePath)`: The removed file's path
81    /// - `None`: File not found
82    pub fn remove(&mut self, id: FileId) -> Option<WorkspaceFilePath> {
83        let path = self.id_to_path.remove(id)?;
84        self.path_to_id.remove(&path);
85        Some(path)
86    }
87
88    // === Lookup ===
89
90    /// WorkspaceFilePath → FileId (O(1) hash lookup).
91    #[inline]
92    pub fn lookup(&self, path: &WorkspaceFilePath) -> Option<FileId> {
93        self.path_to_id.get(path).copied()
94    }
95
96    /// FileId → WorkspaceFilePath (O(1) array access).
97    #[inline]
98    pub fn path(&self, id: FileId) -> Option<&WorkspaceFilePath> {
99        self.id_to_path.get(id)
100    }
101
102    /// FileId → crate name (convenience method).
103    #[inline]
104    pub fn crate_name(&self, id: FileId) -> Option<&str> {
105        self.id_to_path.get(id).map(|p| p.crate_name().as_str())
106    }
107
108    /// Check if FileId is valid (including generation check).
109    #[inline]
110    pub fn contains(&self, id: FileId) -> bool {
111        self.id_to_path.contains_key(id)
112    }
113
114    // === Metadata Mutation ===
115
116    /// Update file path (e.g., after rename/move).
117    ///
118    /// This updates the path while preserving the FileId.
119    pub fn update_path(
120        &mut self,
121        id: FileId,
122        new_path: WorkspaceFilePath,
123    ) -> Result<WorkspaceFilePath, InvalidFileId> {
124        let old_path = self.id_to_path.get(id).ok_or(InvalidFileId(id))?.clone();
125
126        // Update path mappings
127        self.path_to_id.remove(&old_path);
128        self.path_to_id.insert(new_path.clone(), id);
129        self.id_to_path[id] = new_path;
130
131        Ok(old_path)
132    }
133
134    // === Iteration ===
135
136    /// Iterate over all files.
137    pub fn iter(&self) -> impl Iterator<Item = (FileId, &WorkspaceFilePath)> {
138        self.id_to_path.iter()
139    }
140
141    /// Iterate over files in a specific crate.
142    pub fn iter_in_crate<'a>(&'a self, crate_name: &'a str) -> impl Iterator<Item = FileId> + 'a {
143        self.id_to_path
144            .iter()
145            .filter(move |(_, path)| path.crate_name().as_str() == crate_name)
146            .map(|(id, _)| id)
147    }
148
149    // === Statistics ===
150
151    /// Get the number of registered files.
152    #[inline]
153    pub fn len(&self) -> usize {
154        self.id_to_path.len()
155    }
156
157    /// Check if the registry is empty.
158    #[inline]
159    pub fn is_empty(&self) -> bool {
160        self.id_to_path.is_empty()
161    }
162}
163
164impl Default for FileRegistry {
165    fn default() -> Self {
166        Self::new()
167    }
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173
174    fn test_path(relative: &str, crate_name: &str) -> WorkspaceFilePath {
175        WorkspaceFilePath::new_for_test(relative, "/project", crate_name)
176    }
177
178    #[test]
179    fn test_register_and_lookup() {
180        let mut registry = FileRegistry::new();
181        let path = test_path("src/lib.rs", "my-crate");
182
183        let id = registry.register(path.clone());
184
185        assert!(registry.contains(id));
186        assert_eq!(registry.lookup(&path), Some(id));
187        assert_eq!(registry.path(id), Some(&path));
188        assert_eq!(registry.crate_name(id), Some("my-crate"));
189    }
190
191    #[test]
192    fn test_register_duplicate() {
193        let mut registry = FileRegistry::new();
194        let path = test_path("src/lib.rs", "my-crate");
195
196        let id1 = registry.register(path.clone());
197        let id2 = registry.register(path);
198
199        assert_eq!(id1, id2);
200    }
201
202    #[test]
203    fn test_remove() {
204        let mut registry = FileRegistry::new();
205        let path = test_path("src/lib.rs", "my-crate");
206
207        let id = registry.register(path.clone());
208        assert!(registry.contains(id));
209
210        let removed = registry.remove(id);
211        assert!(removed.is_some());
212        assert!(!registry.contains(id));
213        assert!(registry.lookup(&path).is_none());
214    }
215
216    #[test]
217    fn test_update_path() {
218        let mut registry = FileRegistry::new();
219        let old_path = test_path("src/old.rs", "my-crate");
220        let new_path = test_path("src/new.rs", "my-crate");
221
222        let id = registry.register(old_path.clone());
223
224        // Update path
225        let returned_old = registry.update_path(id, new_path.clone()).unwrap();
226        assert_eq!(returned_old, old_path);
227
228        // Verify state
229        assert!(registry.lookup(&old_path).is_none());
230        assert_eq!(registry.lookup(&new_path), Some(id));
231        assert_eq!(registry.path(id), Some(&new_path));
232    }
233
234    #[test]
235    fn test_iter_in_crate() {
236        let mut registry = FileRegistry::new();
237
238        registry.register(test_path("src/lib.rs", "crate-a"));
239        registry.register(test_path("src/foo.rs", "crate-a"));
240        registry.register(test_path("src/lib.rs", "crate-b"));
241
242        let crate_a_files: Vec<_> = registry.iter_in_crate("crate-a").collect();
243        assert_eq!(crate_a_files.len(), 2);
244
245        let crate_b_files: Vec<_> = registry.iter_in_crate("crate-b").collect();
246        assert_eq!(crate_b_files.len(), 1);
247    }
248}