Skip to main content

perl_workspace/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            if version < doc.version {
75                return false;
76            }
77            doc.update(version, text);
78            true
79        } else {
80            false
81        }
82    }
83
84    /// Close a document
85    pub fn close(&self, uri: &str) -> bool {
86        let key = Self::uri_key(uri);
87        let Ok(mut docs) = self.documents.write() else {
88            return false;
89        };
90        docs.remove(&key).is_some()
91    }
92
93    /// Get a document by URI
94    pub fn get(&self, uri: &str) -> Option<Document> {
95        let key = Self::uri_key(uri);
96        let docs = self.documents.read().ok()?;
97        docs.get(&key).cloned()
98    }
99
100    /// Get the text content of a document
101    pub fn get_text(&self, uri: &str) -> Option<String> {
102        let key = Self::uri_key(uri);
103        let docs = self.documents.read().ok()?;
104        docs.get(&key).map(|doc| doc.text.clone())
105    }
106
107    /// Get all open documents
108    pub fn all_documents(&self) -> Vec<Document> {
109        let Ok(docs) = self.documents.read() else {
110            return Vec::new();
111        };
112        docs.values().cloned().collect()
113    }
114
115    /// Check if a document is open
116    pub fn is_open(&self, uri: &str) -> bool {
117        let key = Self::uri_key(uri);
118        let Ok(docs) = self.documents.read() else {
119            return false;
120        };
121        docs.contains_key(&key)
122    }
123
124    /// Get the count of open documents
125    pub fn count(&self) -> usize {
126        let Ok(docs) = self.documents.read() else {
127            return 0;
128        };
129        docs.len()
130    }
131
132    /// Estimate total bytes used by all stored document texts.
133    ///
134    /// Only available when the `memory-profiling` feature is enabled.
135    /// Returns the sum of `text.len()` for every open document; does not
136    /// account for `Document` struct overhead or other metadata overhead.
137    #[cfg(feature = "memory-profiling")]
138    pub fn total_text_bytes(&self) -> usize {
139        let Ok(docs) = self.documents.read() else {
140            return 0;
141        };
142        docs.values().map(|d| d.text.len()).sum()
143    }
144}
145
146impl Default for DocumentStore {
147    fn default() -> Self {
148        Self::new()
149    }
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155    use perl_tdd_support::must_some;
156
157    #[test]
158    fn test_document_lifecycle() {
159        let store = DocumentStore::new();
160        let uri = "file:///test.pl".to_string();
161
162        // Open document
163        store.open(uri.clone(), 1, "print 'hello';".to_string());
164        assert!(store.is_open(&uri));
165        assert_eq!(store.count(), 1);
166
167        // Get document
168        let doc = must_some(store.get(&uri));
169        assert_eq!(doc.version, 1);
170        assert_eq!(doc.text, "print 'hello';");
171
172        // Update document
173        assert!(store.update(&uri, 2, "print 'world';".to_string()));
174        let doc = must_some(store.get(&uri));
175        assert_eq!(doc.version, 2);
176        assert_eq!(doc.text, "print 'world';");
177
178        // Close document
179        assert!(store.close(&uri));
180        assert!(!store.is_open(&uri));
181        assert_eq!(store.count(), 0);
182    }
183
184    #[test]
185    fn test_uri_drive_letter_normalization() {
186        let uri1 = "file:///C:/test.pl";
187        let uri2 = "file:///c:/test.pl";
188        assert_eq!(DocumentStore::uri_key(uri1), DocumentStore::uri_key(uri2));
189    }
190
191    #[test]
192    fn test_drive_letter_lookup() {
193        let store = DocumentStore::new();
194        let uri_upper = "file:///C:/test.pl".to_string();
195        let uri_lower = "file:///c:/test.pl".to_string();
196
197        store.open(uri_upper.clone(), 1, "# test".to_string());
198        assert!(store.is_open(&uri_lower));
199        assert_eq!(store.get_text(&uri_lower), Some("# test".to_string()));
200        assert!(store.close(&uri_lower));
201        assert_eq!(store.count(), 0);
202    }
203
204    #[test]
205    fn test_multiple_documents() {
206        let store = DocumentStore::new();
207
208        let uri1 = "file:///a.pl".to_string();
209        let uri2 = "file:///b.pl".to_string();
210
211        store.open(uri1.clone(), 1, "# file a".to_string());
212        store.open(uri2.clone(), 1, "# file b".to_string());
213
214        assert_eq!(store.count(), 2);
215        assert_eq!(store.get_text(&uri1), Some("# file a".to_string()));
216        assert_eq!(store.get_text(&uri2), Some("# file b".to_string()));
217
218        let all = store.all_documents();
219        assert_eq!(all.len(), 2);
220    }
221
222    #[test]
223    fn test_uri_with_spaces() {
224        let store = DocumentStore::new();
225        let uri = "file:///path%20with%20spaces/test.pl".to_string();
226
227        store.open(uri.clone(), 1, "# test".to_string());
228        assert!(store.is_open(&uri));
229
230        let doc = must_some(store.get(&uri));
231        assert_eq!(doc.text, "# test");
232    }
233
234    #[test]
235    fn test_update_rejects_stale_version() {
236        let store = DocumentStore::new();
237        let uri = "file:///versioned.pl".to_string();
238
239        store.open(uri.clone(), 3, "current".to_string());
240        assert!(!store.update(&uri, 2, "stale".to_string()));
241
242        let doc = must_some(store.get(&uri));
243        assert_eq!(doc.version, 3);
244        assert_eq!(doc.text, "current");
245    }
246
247    #[test]
248    fn test_update_accepts_same_version() {
249        let store = DocumentStore::new();
250        let uri = "file:///same-version.pl".to_string();
251
252        store.open(uri.clone(), 5, "first".to_string());
253        assert!(store.update(&uri, 5, "second".to_string()));
254
255        let doc = must_some(store.get(&uri));
256        assert_eq!(doc.version, 5);
257        assert_eq!(doc.text, "second");
258    }
259
260    #[test]
261    fn test_open_replaces_existing_document() {
262        let store = DocumentStore::new();
263        let uri = "file:///replace.pl".to_string();
264
265        store.open(uri.clone(), 1, "old".to_string());
266        store.open(uri.clone(), 7, "new".to_string());
267
268        assert_eq!(store.count(), 1);
269        let doc = must_some(store.get(&uri));
270        assert_eq!(doc.version, 7);
271        assert_eq!(doc.text, "new");
272    }
273
274    #[test]
275    fn test_close_returns_false_for_missing_document() {
276        let store = DocumentStore::new();
277        assert!(!store.close("file:///missing.pl"));
278    }
279
280    #[test]
281    fn test_update_rebuilds_line_index() {
282        let store = DocumentStore::new();
283        let uri = "file:///lines.pl".to_string();
284
285        store.open(uri.clone(), 1, "line1\nline2".to_string());
286        assert!(store.update(&uri, 2, "line1\nline2\nline3".to_string()));
287
288        let doc = must_some(store.get(&uri));
289        assert_eq!(doc.line_index.offset_to_position(12), (2, 0));
290    }
291}