Skip to main content

perl_workspace_index/workspace/
document_store.rs

1//! Document store for managing in-memory text content
2//!
3//! Maintains the current state of all open documents, tracking
4//! versions and content without relying on filesystem state.
5
6use crate::line_index::LineIndex;
7use std::collections::HashMap;
8use std::sync::{Arc, RwLock};
9
10/// A document in the store
11#[derive(Debug, Clone)]
12pub struct Document {
13    /// The document URI
14    pub uri: String,
15    /// LSP version number
16    pub version: i32,
17    /// The full text content
18    pub text: String,
19    /// Line index for position calculations
20    pub line_index: LineIndex,
21}
22
23impl Document {
24    /// Create a new document
25    pub fn new(uri: String, version: i32, text: String) -> Self {
26        let line_index = LineIndex::new(text.clone());
27        Self { uri, version, text, line_index }
28    }
29
30    /// Update the document content
31    pub fn update(&mut self, version: i32, text: String) {
32        self.version = version;
33        self.text = text.clone();
34        self.line_index = LineIndex::new(text);
35    }
36}
37
38/// Thread-safe document store
39#[derive(Debug, Clone)]
40pub struct DocumentStore {
41    documents: Arc<RwLock<HashMap<String, Document>>>,
42}
43
44impl DocumentStore {
45    /// Create a new empty store
46    pub fn new() -> Self {
47        Self { documents: Arc::new(RwLock::new(HashMap::new())) }
48    }
49
50    /// Normalize a URI to a consistent key
51    /// This handles platform differences and ensures consistent lookups
52    pub fn uri_key(uri: &str) -> String {
53        perl_uri::uri_key(uri)
54    }
55
56    /// Open or update a document
57    pub fn open(&self, uri: String, version: i32, text: String) {
58        let key = Self::uri_key(&uri);
59        let doc = Document::new(uri, version, text);
60
61        if let Ok(mut docs) = self.documents.write() {
62            docs.insert(key, doc);
63        }
64    }
65
66    /// Update a document's content
67    pub fn update(&self, uri: &str, version: i32, text: String) -> bool {
68        let key = Self::uri_key(uri);
69
70        let Ok(mut docs) = self.documents.write() else {
71            return false;
72        };
73        if let Some(doc) = docs.get_mut(&key) {
74            doc.update(version, text);
75            true
76        } else {
77            false
78        }
79    }
80
81    /// Close a document
82    pub fn close(&self, uri: &str) -> bool {
83        let key = Self::uri_key(uri);
84        let Ok(mut docs) = self.documents.write() else {
85            return false;
86        };
87        docs.remove(&key).is_some()
88    }
89
90    /// Get a document by URI
91    pub fn get(&self, uri: &str) -> Option<Document> {
92        let key = Self::uri_key(uri);
93        let docs = self.documents.read().ok()?;
94        docs.get(&key).cloned()
95    }
96
97    /// Get the text content of a document
98    pub fn get_text(&self, uri: &str) -> Option<String> {
99        self.get(uri).map(|doc| doc.text)
100    }
101
102    /// Get all open documents
103    pub fn all_documents(&self) -> Vec<Document> {
104        let Ok(docs) = self.documents.read() else {
105            return Vec::new();
106        };
107        docs.values().cloned().collect()
108    }
109
110    /// Check if a document is open
111    pub fn is_open(&self, uri: &str) -> bool {
112        let key = Self::uri_key(uri);
113        let Ok(docs) = self.documents.read() else {
114            return false;
115        };
116        docs.contains_key(&key)
117    }
118
119    /// Get the count of open documents
120    pub fn count(&self) -> usize {
121        let Ok(docs) = self.documents.read() else {
122            return 0;
123        };
124        docs.len()
125    }
126
127    /// Estimate total bytes used by all stored document texts.
128    ///
129    /// Only available when the `memory-profiling` feature is enabled.
130    /// Returns the sum of `text.len()` for every open document; does not
131    /// account for `Document` struct overhead or other metadata overhead.
132    #[cfg(feature = "memory-profiling")]
133    pub fn total_text_bytes(&self) -> usize {
134        let Ok(docs) = self.documents.read() else {
135            return 0;
136        };
137        docs.values().map(|d| d.text.len()).sum()
138    }
139}
140
141impl Default for DocumentStore {
142    fn default() -> Self {
143        Self::new()
144    }
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150    use perl_tdd_support::must_some;
151
152    #[test]
153    fn test_document_lifecycle() {
154        let store = DocumentStore::new();
155        let uri = "file:///test.pl".to_string();
156
157        // Open document
158        store.open(uri.clone(), 1, "print 'hello';".to_string());
159        assert!(store.is_open(&uri));
160        assert_eq!(store.count(), 1);
161
162        // Get document
163        let doc = must_some(store.get(&uri));
164        assert_eq!(doc.version, 1);
165        assert_eq!(doc.text, "print 'hello';");
166
167        // Update document
168        assert!(store.update(&uri, 2, "print 'world';".to_string()));
169        let doc = must_some(store.get(&uri));
170        assert_eq!(doc.version, 2);
171        assert_eq!(doc.text, "print 'world';");
172
173        // Close document
174        assert!(store.close(&uri));
175        assert!(!store.is_open(&uri));
176        assert_eq!(store.count(), 0);
177    }
178
179    #[test]
180    fn test_uri_drive_letter_normalization() {
181        let uri1 = "file:///C:/test.pl";
182        let uri2 = "file:///c:/test.pl";
183        assert_eq!(DocumentStore::uri_key(uri1), DocumentStore::uri_key(uri2));
184    }
185
186    #[test]
187    fn test_drive_letter_lookup() {
188        let store = DocumentStore::new();
189        let uri_upper = "file:///C:/test.pl".to_string();
190        let uri_lower = "file:///c:/test.pl".to_string();
191
192        store.open(uri_upper.clone(), 1, "# test".to_string());
193        assert!(store.is_open(&uri_lower));
194        assert_eq!(store.get_text(&uri_lower), Some("# test".to_string()));
195        assert!(store.close(&uri_lower));
196        assert_eq!(store.count(), 0);
197    }
198
199    #[test]
200    fn test_multiple_documents() {
201        let store = DocumentStore::new();
202
203        let uri1 = "file:///a.pl".to_string();
204        let uri2 = "file:///b.pl".to_string();
205
206        store.open(uri1.clone(), 1, "# file a".to_string());
207        store.open(uri2.clone(), 1, "# file b".to_string());
208
209        assert_eq!(store.count(), 2);
210        assert_eq!(store.get_text(&uri1), Some("# file a".to_string()));
211        assert_eq!(store.get_text(&uri2), Some("# file b".to_string()));
212
213        let all = store.all_documents();
214        assert_eq!(all.len(), 2);
215    }
216
217    #[test]
218    fn test_uri_with_spaces() {
219        let store = DocumentStore::new();
220        let uri = "file:///path%20with%20spaces/test.pl".to_string();
221
222        store.open(uri.clone(), 1, "# test".to_string());
223        assert!(store.is_open(&uri));
224
225        let doc = must_some(store.get(&uri));
226        assert_eq!(doc.text, "# test");
227    }
228}