use crate::line_index::LineIndex;
use std::collections::HashMap;
use std::sync::{Arc, RwLock};
#[derive(Debug, Clone)]
pub struct Document {
pub uri: String,
pub version: i32,
pub text: String,
pub line_index: LineIndex,
}
impl Document {
pub fn new(uri: String, version: i32, text: String) -> Self {
let line_index = LineIndex::new(text.clone());
Self { uri, version, text, line_index }
}
pub fn update(&mut self, version: i32, text: String) {
self.version = version;
self.text = text.clone();
self.line_index = LineIndex::new(text);
}
}
#[derive(Debug, Clone)]
pub struct DocumentStore {
documents: Arc<RwLock<HashMap<String, Document>>>,
}
impl DocumentStore {
pub fn new() -> Self {
Self { documents: Arc::new(RwLock::new(HashMap::new())) }
}
pub fn uri_key(uri: &str) -> String {
perl_uri::uri_key(uri)
}
pub fn open(&self, uri: String, version: i32, text: String) {
let key = Self::uri_key(&uri);
let doc = Document::new(uri, version, text);
if let Ok(mut docs) = self.documents.write() {
docs.insert(key, doc);
}
}
pub fn update(&self, uri: &str, version: i32, text: String) -> bool {
let key = Self::uri_key(uri);
let Ok(mut docs) = self.documents.write() else {
return false;
};
if let Some(doc) = docs.get_mut(&key) {
doc.update(version, text);
true
} else {
false
}
}
pub fn close(&self, uri: &str) -> bool {
let key = Self::uri_key(uri);
let Ok(mut docs) = self.documents.write() else {
return false;
};
docs.remove(&key).is_some()
}
pub fn get(&self, uri: &str) -> Option<Document> {
let key = Self::uri_key(uri);
let docs = self.documents.read().ok()?;
docs.get(&key).cloned()
}
pub fn get_text(&self, uri: &str) -> Option<String> {
self.get(uri).map(|doc| doc.text)
}
pub fn all_documents(&self) -> Vec<Document> {
let Ok(docs) = self.documents.read() else {
return Vec::new();
};
docs.values().cloned().collect()
}
pub fn is_open(&self, uri: &str) -> bool {
let key = Self::uri_key(uri);
let Ok(docs) = self.documents.read() else {
return false;
};
docs.contains_key(&key)
}
pub fn count(&self) -> usize {
let Ok(docs) = self.documents.read() else {
return 0;
};
docs.len()
}
#[cfg(feature = "memory-profiling")]
pub fn total_text_bytes(&self) -> usize {
let Ok(docs) = self.documents.read() else {
return 0;
};
docs.values().map(|d| d.text.len()).sum()
}
}
impl Default for DocumentStore {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use perl_tdd_support::must_some;
#[test]
fn test_document_lifecycle() {
let store = DocumentStore::new();
let uri = "file:///test.pl".to_string();
store.open(uri.clone(), 1, "print 'hello';".to_string());
assert!(store.is_open(&uri));
assert_eq!(store.count(), 1);
let doc = must_some(store.get(&uri));
assert_eq!(doc.version, 1);
assert_eq!(doc.text, "print 'hello';");
assert!(store.update(&uri, 2, "print 'world';".to_string()));
let doc = must_some(store.get(&uri));
assert_eq!(doc.version, 2);
assert_eq!(doc.text, "print 'world';");
assert!(store.close(&uri));
assert!(!store.is_open(&uri));
assert_eq!(store.count(), 0);
}
#[test]
fn test_uri_drive_letter_normalization() {
let uri1 = "file:///C:/test.pl";
let uri2 = "file:///c:/test.pl";
assert_eq!(DocumentStore::uri_key(uri1), DocumentStore::uri_key(uri2));
}
#[test]
fn test_drive_letter_lookup() {
let store = DocumentStore::new();
let uri_upper = "file:///C:/test.pl".to_string();
let uri_lower = "file:///c:/test.pl".to_string();
store.open(uri_upper.clone(), 1, "# test".to_string());
assert!(store.is_open(&uri_lower));
assert_eq!(store.get_text(&uri_lower), Some("# test".to_string()));
assert!(store.close(&uri_lower));
assert_eq!(store.count(), 0);
}
#[test]
fn test_multiple_documents() {
let store = DocumentStore::new();
let uri1 = "file:///a.pl".to_string();
let uri2 = "file:///b.pl".to_string();
store.open(uri1.clone(), 1, "# file a".to_string());
store.open(uri2.clone(), 1, "# file b".to_string());
assert_eq!(store.count(), 2);
assert_eq!(store.get_text(&uri1), Some("# file a".to_string()));
assert_eq!(store.get_text(&uri2), Some("# file b".to_string()));
let all = store.all_documents();
assert_eq!(all.len(), 2);
}
#[test]
fn test_uri_with_spaces() {
let store = DocumentStore::new();
let uri = "file:///path%20with%20spaces/test.pl".to_string();
store.open(uri.clone(), 1, "# test".to_string());
assert!(store.is_open(&uri));
let doc = must_some(store.get(&uri));
assert_eq!(doc.text, "# test");
}
}