perl_workspace/workspace/
document_store.rs1use crate::line_index::LineIndex;
7use std::collections::HashMap;
8use std::sync::{Arc, RwLock};
9
10#[derive(Debug, Clone)]
12pub struct Document {
13 pub uri: String,
15 pub version: i32,
17 pub text: String,
19 pub line_index: LineIndex,
21}
22
23impl Document {
24 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 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#[derive(Debug, Clone)]
40pub struct DocumentStore {
41 documents: Arc<RwLock<HashMap<String, Document>>>,
42}
43
44impl DocumentStore {
45 pub fn new() -> Self {
47 Self { documents: Arc::new(RwLock::new(HashMap::new())) }
48 }
49
50 pub fn uri_key(uri: &str) -> String {
53 perl_uri::uri_key(uri)
54 }
55
56 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 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 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 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 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 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 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 pub fn count(&self) -> usize {
126 let Ok(docs) = self.documents.read() else {
127 return 0;
128 };
129 docs.len()
130 }
131
132 #[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 store.open(uri.clone(), 1, "print 'hello';".to_string());
164 assert!(store.is_open(&uri));
165 assert_eq!(store.count(), 1);
166
167 let doc = must_some(store.get(&uri));
169 assert_eq!(doc.version, 1);
170 assert_eq!(doc.text, "print 'hello';");
171
172 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 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}