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
128impl Default for DocumentStore {
129    fn default() -> Self {
130        Self::new()
131    }
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137    use perl_tdd_support::must_some;
138
139    #[test]
140    fn test_document_lifecycle() {
141        let store = DocumentStore::new();
142        let uri = "file:///test.pl".to_string();
143
144        // Open document
145        store.open(uri.clone(), 1, "print 'hello';".to_string());
146        assert!(store.is_open(&uri));
147        assert_eq!(store.count(), 1);
148
149        // Get document
150        let doc = must_some(store.get(&uri));
151        assert_eq!(doc.version, 1);
152        assert_eq!(doc.text, "print 'hello';");
153
154        // Update document
155        assert!(store.update(&uri, 2, "print 'world';".to_string()));
156        let doc = must_some(store.get(&uri));
157        assert_eq!(doc.version, 2);
158        assert_eq!(doc.text, "print 'world';");
159
160        // Close document
161        assert!(store.close(&uri));
162        assert!(!store.is_open(&uri));
163        assert_eq!(store.count(), 0);
164    }
165
166    #[test]
167    fn test_uri_drive_letter_normalization() {
168        let uri1 = "file:///C:/test.pl";
169        let uri2 = "file:///c:/test.pl";
170        assert_eq!(DocumentStore::uri_key(uri1), DocumentStore::uri_key(uri2));
171    }
172
173    #[test]
174    fn test_drive_letter_lookup() {
175        let store = DocumentStore::new();
176        let uri_upper = "file:///C:/test.pl".to_string();
177        let uri_lower = "file:///c:/test.pl".to_string();
178
179        store.open(uri_upper.clone(), 1, "# test".to_string());
180        assert!(store.is_open(&uri_lower));
181        assert_eq!(store.get_text(&uri_lower), Some("# test".to_string()));
182        assert!(store.close(&uri_lower));
183        assert_eq!(store.count(), 0);
184    }
185
186    #[test]
187    fn test_multiple_documents() {
188        let store = DocumentStore::new();
189
190        let uri1 = "file:///a.pl".to_string();
191        let uri2 = "file:///b.pl".to_string();
192
193        store.open(uri1.clone(), 1, "# file a".to_string());
194        store.open(uri2.clone(), 1, "# file b".to_string());
195
196        assert_eq!(store.count(), 2);
197        assert_eq!(store.get_text(&uri1), Some("# file a".to_string()));
198        assert_eq!(store.get_text(&uri2), Some("# file b".to_string()));
199
200        let all = store.all_documents();
201        assert_eq!(all.len(), 2);
202    }
203
204    #[test]
205    fn test_uri_with_spaces() {
206        let store = DocumentStore::new();
207        let uri = "file:///path%20with%20spaces/test.pl".to_string();
208
209        store.open(uri.clone(), 1, "# test".to_string());
210        assert!(store.is_open(&uri));
211
212        let doc = must_some(store.get(&uri));
213        assert_eq!(doc.text, "# test");
214    }
215}