#![allow(clippy::arc_with_non_send_sync)]
use crate::commands::{CommandResult, EditorCommand};
use crate::core::errors::EditorError;
use crate::core::{EditorDocument, Result};
#[cfg(feature = "std")]
use std::sync::{Arc, Mutex, RwLock, RwLockReadGuard, RwLockWriteGuard};
#[cfg(not(feature = "std"))]
use alloc::sync::Arc;
#[cfg(feature = "concurrency")]
#[derive(Debug, Clone)]
pub struct SyncDocument {
inner: Arc<RwLock<EditorDocument>>,
command_lock: Arc<Mutex<()>>,
}
#[cfg(feature = "concurrency")]
impl SyncDocument {
pub fn new(document: EditorDocument) -> Self {
Self {
inner: Arc::new(RwLock::new(document)),
command_lock: Arc::new(Mutex::new(())),
}
}
pub fn from_document(document: EditorDocument) -> Self {
Self::new(document)
}
pub fn read(&self) -> Result<RwLockReadGuard<'_, EditorDocument>> {
self.inner
.read()
.map_err(|_| EditorError::ThreadSafetyError {
message: "Failed to acquire read lock".to_string(),
})
}
pub fn write(&self) -> Result<RwLockWriteGuard<'_, EditorDocument>> {
self.inner
.write()
.map_err(|_| EditorError::ThreadSafetyError {
message: "Failed to acquire write lock".to_string(),
})
}
pub fn execute_command<C: EditorCommand>(&self, command: C) -> Result<CommandResult> {
let _guard = self
.command_lock
.lock()
.map_err(|_| EditorError::ThreadSafetyError {
message: "Failed to acquire command lock".to_string(),
})?;
let mut doc = self.write()?;
command.execute(&mut doc)
}
pub fn try_read(&self) -> Option<RwLockReadGuard<'_, EditorDocument>> {
self.inner.try_read().ok()
}
pub fn try_write(&self) -> Option<RwLockWriteGuard<'_, EditorDocument>> {
self.inner.try_write().ok()
}
pub fn text(&self) -> Result<String> {
let doc = self.read()?;
Ok(doc.text())
}
pub fn len(&self) -> Result<usize> {
let doc = self.read()?;
Ok(doc.len())
}
pub fn is_empty(&self) -> Result<bool> {
let doc = self.read()?;
Ok(doc.is_empty())
}
pub fn id(&self) -> Result<String> {
let doc = self.read()?;
Ok(doc.id().to_string())
}
pub fn with_read<F, R>(&self, f: F) -> Result<R>
where
F: FnOnce(&EditorDocument) -> R,
{
let doc = self.read()?;
Ok(f(&doc))
}
pub fn with_write<F, R>(&self, f: F) -> Result<R>
where
F: FnOnce(&mut EditorDocument) -> Result<R>,
{
let mut doc = self.write()?;
f(&mut doc)
}
pub fn clone_document(&self) -> Result<EditorDocument> {
let doc = self.read()?;
EditorDocument::from_content(&doc.text())
}
pub fn validate(&self) -> Result<()> {
let doc = self.read()?;
doc.validate()
}
pub fn validate_comprehensive(&self) -> Result<Vec<crate::utils::validator::ValidationIssue>> {
self.with_write(|doc| {
let result = doc.validate_comprehensive()?;
Ok(result.issues)
})
}
}
#[cfg(feature = "concurrency")]
#[derive(Debug, Clone)]
pub struct DocumentPool {
documents: Arc<RwLock<std::collections::HashMap<String, SyncDocument>>>,
}
#[cfg(feature = "concurrency")]
impl DocumentPool {
pub fn new() -> Self {
Self {
documents: Arc::new(RwLock::new(std::collections::HashMap::new())),
}
}
pub fn add_document(&self, document: EditorDocument) -> Result<String> {
let id = document.id().to_string();
let sync_doc = SyncDocument::new(document);
let mut docs = self
.documents
.write()
.map_err(|_| EditorError::ThreadSafetyError {
message: "Failed to acquire pool write lock".to_string(),
})?;
docs.insert(id.clone(), sync_doc);
Ok(id)
}
pub fn get_document(&self, id: &str) -> Result<SyncDocument> {
let docs = self
.documents
.read()
.map_err(|_| EditorError::ThreadSafetyError {
message: "Failed to acquire pool read lock".to_string(),
})?;
docs.get(id)
.cloned()
.ok_or_else(|| EditorError::ValidationError {
message: format!("Document not found: {id}"),
})
}
pub fn remove_document(&self, id: &str) -> Result<()> {
let mut docs = self
.documents
.write()
.map_err(|_| EditorError::ThreadSafetyError {
message: "Failed to acquire pool write lock".to_string(),
})?;
docs.remove(id);
Ok(())
}
pub fn list_documents(&self) -> Result<Vec<String>> {
let docs = self
.documents
.read()
.map_err(|_| EditorError::ThreadSafetyError {
message: "Failed to acquire pool read lock".to_string(),
})?;
Ok(docs.keys().cloned().collect())
}
pub fn document_count(&self) -> Result<usize> {
let docs = self
.documents
.read()
.map_err(|_| EditorError::ThreadSafetyError {
message: "Failed to acquire pool read lock".to_string(),
})?;
Ok(docs.len())
}
}
#[cfg(feature = "concurrency")]
impl Default for DocumentPool {
fn default() -> Self {
Self::new()
}
}
#[cfg(feature = "concurrency")]
pub struct ScopedDocumentLock<'a> {
_guard: RwLockWriteGuard<'a, EditorDocument>,
}
#[cfg(feature = "concurrency")]
impl<'a> ScopedDocumentLock<'a> {
pub fn new(document: &'a SyncDocument) -> Result<Self> {
let guard = document.write()?;
Ok(Self { _guard: guard })
}
pub fn document(&mut self) -> &mut EditorDocument {
&mut self._guard
}
}
#[cfg(all(feature = "concurrency", feature = "async"))]
pub struct AsyncDocument {
sync_doc: SyncDocument,
}
#[cfg(all(feature = "concurrency", feature = "async"))]
impl AsyncDocument {
pub fn new(document: EditorDocument) -> Self {
Self {
sync_doc: SyncDocument::new(document),
}
}
pub async fn text_async(&self) -> Result<String> {
self.sync_doc.text()
}
pub async fn execute_command_async<C: EditorCommand + Send + 'static>(
&self,
command: C,
) -> Result<CommandResult> {
self.sync_doc.execute_command(command)
}
}
#[cfg(test)]
#[cfg(feature = "concurrency")]
mod tests {
use super::*;
use crate::commands::InsertTextCommand;
use crate::core::Position;
#[cfg(not(feature = "std"))]
use alloc::{format, string::ToString};
#[test]
fn test_sync_document_creation() {
let doc = EditorDocument::from_content("[Script Info]\nTitle: Test").unwrap();
let sync_doc = SyncDocument::new(doc);
let text = sync_doc.text().unwrap();
assert!(text.contains("Title: Test"));
}
#[test]
fn test_sync_document_modification() {
let doc = EditorDocument::from_content("[Script Info]\nTitle: Test").unwrap();
let sync_doc = SyncDocument::new(doc);
sync_doc
.with_write(|doc| doc.insert(Position::new(doc.len()), "\nAuthor: Test"))
.unwrap();
let text = sync_doc.text().unwrap();
assert!(text.contains("Author: Test"));
}
#[test]
fn test_document_pool() {
let pool = DocumentPool::new();
let doc1 = EditorDocument::from_content("[Script Info]\nTitle: Doc1").unwrap();
let id1 = pool.add_document(doc1).unwrap();
let doc2 = EditorDocument::from_content("[Script Info]\nTitle: Doc2").unwrap();
let id2 = pool.add_document(doc2).unwrap();
assert_eq!(pool.document_count().unwrap(), 2);
let sync_doc1 = pool.get_document(&id1).unwrap();
assert!(sync_doc1.text().unwrap().contains("Doc1"));
let sync_doc2 = pool.get_document(&id2).unwrap();
assert!(sync_doc2.text().unwrap().contains("Doc2"));
pool.remove_document(&id1).unwrap();
assert_eq!(pool.document_count().unwrap(), 1);
}
#[test]
fn test_concurrent_usage() {
let doc = EditorDocument::from_content("[Script Info]\nTitle: Test").unwrap();
let sync_doc = SyncDocument::new(doc);
for i in 0..5 {
sync_doc
.with_write(|doc| {
let pos = Position::new(doc.len());
doc.insert(pos, &format!("\nComment: Update {i}"))
})
.unwrap();
}
let final_text = sync_doc.text().unwrap();
assert!(final_text.contains("Comment: Update 4"));
let _write_guard = sync_doc.write().unwrap();
assert!(sync_doc.try_read().is_none());
}
#[test]
fn test_try_lock_operations() {
let doc = EditorDocument::from_content("[Script Info]\nTitle: Test").unwrap();
let sync_doc = SyncDocument::new(doc);
let _write_guard = sync_doc.write().unwrap();
assert!(sync_doc.try_read().is_none());
assert!(sync_doc.try_write().is_none());
}
#[test]
fn test_thread_safe_command_execution() {
let doc = EditorDocument::from_content("[Script Info]\nTitle: Test").unwrap();
let sync_doc = SyncDocument::new(doc);
let command = InsertTextCommand::new(Position::new(0), "[V4+ Styles]\n".to_string());
let result = sync_doc.execute_command(command).unwrap();
assert!(result.success);
assert!(result.content_changed);
let text = sync_doc.text().unwrap();
assert!(text.starts_with("[V4+ Styles]"));
}
#[test]
fn test_sync_document_validation() {
let doc = EditorDocument::from_content(
"[Script Info]\nTitle: Test\n\n[Events]\nDialogue: 0,0:00:00.00,0:00:05.00,Default,,0,0,0,,Test"
).unwrap();
let sync_doc = SyncDocument::new(doc);
sync_doc.validate().unwrap();
let issues = sync_doc.validate_comprehensive().unwrap();
for issue in &issues {
println!("Validation issue: {issue:?}");
}
assert!(issues.len() <= 1); }
#[test]
fn test_scoped_lock() {
let doc = EditorDocument::from_content("[Script Info]\nTitle: Test").unwrap();
let sync_doc = SyncDocument::new(doc);
{
let mut lock = ScopedDocumentLock::new(&sync_doc).unwrap();
let doc = lock.document();
doc.insert(Position::new(doc.len()), "\nAuthor: Test")
.unwrap();
doc.insert(Position::new(doc.len()), "\nVersion: 1.0")
.unwrap();
}
let text = sync_doc.text().unwrap();
assert!(text.contains("Author: Test"));
assert!(text.contains("Version: 1.0"));
}
#[test]
fn test_command_send_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<InsertTextCommand>();
assert_send_sync::<crate::commands::DeleteTextCommand>();
assert_send_sync::<crate::commands::ReplaceTextCommand>();
assert_send_sync::<crate::commands::BatchCommand>();
}
}