use std::collections::HashSet;
use std::io::Result;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, RwLock};
use crate::crdt::{BodyDocManager, FileMetadata, WorkspaceCrdt};
use crate::frontmatter;
use crate::fs::{AsyncFileSystem, BoxFuture};
use crate::link_parser;
pub struct CrdtFs<FS: AsyncFileSystem> {
inner: FS,
workspace_crdt: Arc<WorkspaceCrdt>,
body_doc_manager: Arc<BodyDocManager>,
enabled: AtomicBool,
local_writes_in_progress: RwLock<HashSet<PathBuf>>,
sync_writes_in_progress: RwLock<HashSet<PathBuf>>,
}
impl<FS: AsyncFileSystem> CrdtFs<FS> {
pub fn new(
inner: FS,
workspace_crdt: Arc<WorkspaceCrdt>,
body_doc_manager: Arc<BodyDocManager>,
) -> Self {
Self {
inner,
workspace_crdt,
body_doc_manager,
enabled: AtomicBool::new(true),
local_writes_in_progress: RwLock::new(HashSet::new()),
sync_writes_in_progress: RwLock::new(HashSet::new()),
}
}
fn normalize_crdt_path(path: &Path) -> String {
let path_str = path.to_string_lossy();
path_str
.trim_start_matches("./")
.trim_start_matches('/')
.to_string()
}
pub fn is_enabled(&self) -> bool {
self.enabled.load(Ordering::SeqCst)
}
pub fn set_enabled(&self, enabled: bool) {
self.enabled.store(enabled, Ordering::SeqCst);
}
pub fn workspace_crdt(&self) -> &Arc<WorkspaceCrdt> {
&self.workspace_crdt
}
pub fn body_doc_manager(&self) -> &Arc<BodyDocManager> {
&self.body_doc_manager
}
pub fn inner(&self) -> &FS {
&self.inner
}
pub fn is_local_write_in_progress(&self, path: &Path) -> bool {
let writes = self.local_writes_in_progress.read().unwrap();
writes.contains(&path.to_path_buf())
}
fn mark_local_write_start(&self, path: &Path) {
let mut writes = self.local_writes_in_progress.write().unwrap();
writes.insert(path.to_path_buf());
}
fn mark_local_write_end(&self, path: &Path) {
let mut writes = self.local_writes_in_progress.write().unwrap();
writes.remove(&path.to_path_buf());
}
pub fn is_sync_write_in_progress(&self, path: &Path) -> bool {
let writes = self.sync_writes_in_progress.read().unwrap();
writes.contains(&path.to_path_buf())
}
fn mark_sync_write_start_internal(&self, path: &Path) {
let mut writes = self.sync_writes_in_progress.write().unwrap();
writes.insert(path.to_path_buf());
log::debug!(
"CrdtFs: Marked sync write start for {:?} (total: {})",
path,
writes.len()
);
}
fn mark_sync_write_end_internal(&self, path: &Path) {
let mut writes = self.sync_writes_in_progress.write().unwrap();
writes.remove(&path.to_path_buf());
log::debug!(
"CrdtFs: Marked sync write end for {:?} (remaining: {})",
path,
writes.len()
);
}
fn extract_metadata(&self, path: &Path, content: &str) -> FileMetadata {
let mut metadata = match frontmatter::parse_or_empty(content) {
Ok(parsed) => self.frontmatter_to_metadata(&parsed.frontmatter),
Err(_) => FileMetadata::default(),
};
metadata.filename = path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("")
.to_string();
if let Some(ref part_of) = metadata.part_of {
let parsed = link_parser::parse_link(part_of);
let canonical = if parsed.path_type == link_parser::PathType::WorkspaceRoot {
parsed.path.clone()
} else {
link_parser::to_canonical(&parsed, path)
};
metadata.part_of = Some(canonical);
}
if let Some(ref contents) = metadata.contents {
metadata.contents = Some(
contents
.iter()
.map(|link_str| {
let parsed = link_parser::parse_link(link_str);
if parsed.path_type == link_parser::PathType::WorkspaceRoot {
parsed.path.clone()
} else {
link_parser::to_canonical(&parsed, path)
}
})
.collect(),
);
}
metadata
}
fn path_to_doc_id(&self, path: &Path, _metadata: &FileMetadata) -> Option<String> {
let normalized = Self::normalize_crdt_path(path);
Some(normalized)
}
fn frontmatter_to_metadata(
&self,
fm: &indexmap::IndexMap<String, serde_yaml::Value>,
) -> FileMetadata {
fn parse_updated_value(value: &serde_yaml::Value) -> Option<i64> {
if let Some(num) = value.as_i64() {
return Some(num);
}
if let Some(num) = value.as_f64() {
return Some(num as i64);
}
if let Some(raw) = value.as_str() {
if let Ok(num) = raw.parse::<i64>() {
return Some(num);
}
if let Ok(parsed) = chrono::DateTime::parse_from_rfc3339(raw) {
return Some(parsed.timestamp_millis());
}
}
None
}
if let Ok(json_value) = serde_json::to_value(fm)
&& let Ok(metadata) = serde_json::from_value::<FileMetadata>(json_value)
{
let mut metadata = metadata;
if let Some(updated) = fm.get("updated").and_then(parse_updated_value) {
metadata.modified_at = updated;
}
if metadata.modified_at == 0 {
metadata.modified_at = chrono::Utc::now().timestamp_millis();
}
return metadata;
}
let mut metadata = FileMetadata::default();
if let Some(title) = fm.get("title") {
metadata.title = title.as_str().map(String::from);
}
if let Some(part_of) = fm.get("part_of") {
metadata.part_of = part_of.as_str().map(String::from);
}
if let Some(contents) = fm.get("contents")
&& let Some(seq) = contents.as_sequence()
{
metadata.contents = Some(
seq.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect(),
);
}
if let Some(audience) = fm.get("audience")
&& let Some(seq) = audience.as_sequence()
{
metadata.audience = Some(
seq.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect(),
);
}
if let Some(description) = fm.get("description") {
metadata.description = description.as_str().map(String::from);
}
let known_fields = [
"title",
"part_of",
"contents",
"audience",
"description",
"attachments",
"deleted",
"modified_at",
"updated",
];
for (key, value) in fm {
if !known_fields.contains(&key.as_str())
&& let Ok(json_value) = serde_json::to_value(value)
{
metadata.extra.insert(key.clone(), json_value);
}
}
if let Some(updated) = fm.get("updated").and_then(parse_updated_value) {
metadata.modified_at = updated;
} else if metadata.modified_at == 0 {
metadata.modified_at = chrono::Utc::now().timestamp_millis();
}
metadata
}
fn update_crdt_for_file(&self, path: &Path, content: &str) {
self.update_crdt_for_file_internal(path, content, false);
}
fn update_crdt_for_new_file(&self, path: &Path, content: &str) {
self.update_crdt_for_file_internal(path, content, true);
}
fn update_crdt_for_file_internal(&self, path: &Path, content: &str, is_new_file: bool) {
if !self.is_enabled() {
return;
}
if self.is_sync_write_in_progress(path) {
log::debug!("CrdtFs: Skipping CRDT update for sync write: {:?}", path);
return;
}
let path_str = path.to_string_lossy().to_string();
if path_str.ends_with(".tmp") || path_str.ends_with(".bak") || path_str.ends_with(".swap") {
log::debug!(
"CrdtFs: Skipping CRDT update for temporary file: {}",
path_str
);
return;
}
let body = frontmatter::extract_body(content);
log::trace!(
"[CrdtFs] update_crdt_for_file_internal: path_str='{}', is_new_file={}, body_preview='{}'",
path_str,
is_new_file,
body.chars().take(50).collect::<String>()
);
let metadata = self.extract_metadata(path, content);
let doc_key = self
.path_to_doc_id(path, &metadata)
.unwrap_or(path_str.clone());
log::trace!("[CrdtFs] BEFORE set_file: doc_key={}", doc_key);
if let Err(e) = self.workspace_crdt.set_file(&doc_key, metadata.clone()) {
log::warn!("[CrdtFs] set_file FAILED: {}: {}", doc_key, e);
} else {
log::trace!("[CrdtFs] set_file SUCCESS: {}", doc_key);
let verify = self.workspace_crdt.get_file(&doc_key);
log::trace!(
"[CrdtFs] set_file VERIFY: {} -> {:?}",
doc_key,
verify.is_some()
);
}
let body = frontmatter::extract_body(content);
let body_doc = if is_new_file {
let _ = self.body_doc_manager.delete(&doc_key);
self.body_doc_manager.create(&doc_key)
} else {
self.body_doc_manager.get_or_create(&doc_key)
};
let _ = body_doc.set_body(body);
}
fn update_parent_contents(&self, old_path: &str, new_path: Option<&str>) {
if !self.is_enabled() {
return;
}
let old_metadata = match self.workspace_crdt.get_file(old_path) {
Some(m) => m,
None => return,
};
if let Some(ref parent_path) = old_metadata.part_of
&& let Some(mut parent) = self.workspace_crdt.get_file(parent_path)
&& let Some(ref mut contents) = parent.contents
{
let old_filename = std::path::Path::new(old_path)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or(old_path);
if let Some(idx) = contents
.iter()
.position(|e| e == old_filename || e == old_path)
{
match new_path {
Some(np) => {
let new_filename = std::path::Path::new(np)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or(np);
contents[idx] = new_filename.to_string();
}
None => {
contents.remove(idx);
}
}
parent.modified_at = chrono::Utc::now().timestamp_millis();
let _ = self.workspace_crdt.set_file(parent_path, parent);
}
}
}
}
impl<FS: AsyncFileSystem + Clone> Clone for CrdtFs<FS> {
fn clone(&self) -> Self {
Self {
inner: self.inner.clone(),
workspace_crdt: Arc::clone(&self.workspace_crdt),
body_doc_manager: Arc::clone(&self.body_doc_manager),
enabled: AtomicBool::new(self.enabled.load(Ordering::SeqCst)),
local_writes_in_progress: RwLock::new(HashSet::new()),
sync_writes_in_progress: RwLock::new(HashSet::new()),
}
}
}
#[cfg(not(target_arch = "wasm32"))]
impl<FS: AsyncFileSystem + Send + Sync> AsyncFileSystem for CrdtFs<FS> {
fn read_to_string<'a>(&'a self, path: &'a Path) -> BoxFuture<'a, Result<String>> {
self.inner.read_to_string(path)
}
fn write_file<'a>(&'a self, path: &'a Path, content: &'a str) -> BoxFuture<'a, Result<()>> {
Box::pin(async move {
self.mark_local_write_start(path);
let result = self.inner.write_file(path, content).await;
if result.is_ok() {
self.update_crdt_for_file(path, content);
}
self.mark_local_write_end(path);
result
})
}
fn create_new<'a>(&'a self, path: &'a Path, content: &'a str) -> BoxFuture<'a, Result<()>> {
Box::pin(async move {
log::info!(
"[CrdtFs] create_new CALLED: path='{}', enabled={}, content_len={}",
path.display(),
self.is_enabled(),
content.len()
);
self.mark_local_write_start(path);
let result = self.inner.create_new(path, content).await;
log::info!(
"[CrdtFs] create_new RESULT: path='{}', success={}, err={:?}",
path.display(),
result.is_ok(),
result.as_ref().err()
);
if result.is_ok() {
log::info!(
"[CrdtFs] create_new calling update_crdt_for_new_file: path='{}'",
path.display()
);
self.update_crdt_for_new_file(path, content);
}
self.mark_local_write_end(path);
result
})
}
fn delete_file<'a>(&'a self, path: &'a Path) -> BoxFuture<'a, Result<()>> {
Box::pin(async move {
self.mark_local_write_start(path);
let result = self.inner.delete_file(path).await;
if result.is_ok() && self.is_enabled() {
let path_str = Self::normalize_crdt_path(path);
self.update_parent_contents(&path_str, None);
if let Err(e) = self.workspace_crdt.delete_file(&path_str) {
log::warn!(
"Failed to mark file as deleted in CRDT for {}: {}",
path_str,
e
);
}
}
self.mark_local_write_end(path);
result
})
}
fn list_md_files<'a>(&'a self, dir: &'a Path) -> BoxFuture<'a, Result<Vec<PathBuf>>> {
self.inner.list_md_files(dir)
}
fn exists<'a>(&'a self, path: &'a Path) -> BoxFuture<'a, bool> {
self.inner.exists(path)
}
fn create_dir_all<'a>(&'a self, path: &'a Path) -> BoxFuture<'a, Result<()>> {
self.inner.create_dir_all(path)
}
fn is_dir<'a>(&'a self, path: &'a Path) -> BoxFuture<'a, bool> {
self.inner.is_dir(path)
}
fn move_file<'a>(&'a self, from: &'a Path, to: &'a Path) -> BoxFuture<'a, Result<()>> {
Box::pin(async move {
self.mark_local_write_start(from);
self.mark_local_write_start(to);
let result = self.inner.move_file(from, to).await;
if result.is_ok() && self.is_enabled() {
let from_str = Self::normalize_crdt_path(from);
let to_str = Self::normalize_crdt_path(to);
if let Some(doc_id) = self.workspace_crdt.find_by_path(from) {
let new_filename = to
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("")
.to_string();
let from_parent = from.parent();
let to_parent = to.parent();
let is_rename = from_parent == to_parent;
if is_rename {
log::debug!(
"CrdtFs: Renaming doc_id={} from {:?} to {}",
doc_id,
from,
new_filename
);
if let Err(e) = self.workspace_crdt.rename_file(&doc_id, &new_filename) {
log::warn!("Failed to rename file in CRDT: {}", e);
}
} else {
let new_parent_id =
to_parent.and_then(|p| self.workspace_crdt.find_by_path(p));
log::debug!(
"CrdtFs: Moving doc_id={} to parent={:?}, new_filename={}",
doc_id,
new_parent_id,
new_filename
);
if let Err(e) = self
.workspace_crdt
.move_file(&doc_id, new_parent_id.as_deref())
{
log::warn!("Failed to move file in CRDT: {}", e);
}
if let Some(meta) = self.workspace_crdt.get_file(&doc_id)
&& meta.filename != new_filename
&& let Err(e) = self.workspace_crdt.rename_file(&doc_id, &new_filename)
{
log::warn!("Failed to rename file during move in CRDT: {}", e);
}
}
self.update_parent_contents(&from_str, Some(&to_str));
} else {
log::debug!(
"CrdtFs: No doc_id found for {:?}, using legacy move behavior",
from
);
self.update_parent_contents(&from_str, Some(&to_str));
if let Err(e) = self.workspace_crdt.delete_file(&from_str) {
log::warn!("Failed to mark old path as deleted in CRDT: {}", e);
}
if let Ok(content) = self.inner.read_to_string(to).await {
self.update_crdt_for_file(to, &content);
}
}
}
self.mark_local_write_end(from);
self.mark_local_write_end(to);
result
})
}
fn read_binary<'a>(&'a self, path: &'a Path) -> BoxFuture<'a, Result<Vec<u8>>> {
self.inner.read_binary(path)
}
fn write_binary<'a>(&'a self, path: &'a Path, content: &'a [u8]) -> BoxFuture<'a, Result<()>> {
self.inner.write_binary(path, content)
}
fn list_files<'a>(&'a self, dir: &'a Path) -> BoxFuture<'a, Result<Vec<PathBuf>>> {
self.inner.list_files(dir)
}
fn get_modified_time<'a>(&'a self, path: &'a Path) -> BoxFuture<'a, Option<i64>> {
self.inner.get_modified_time(path)
}
fn mark_sync_write_start(&self, path: &Path) {
self.mark_sync_write_start_internal(path);
}
fn mark_sync_write_end(&self, path: &Path) {
self.mark_sync_write_end_internal(path);
}
}
#[cfg(target_arch = "wasm32")]
impl<FS: AsyncFileSystem> AsyncFileSystem for CrdtFs<FS> {
fn read_to_string<'a>(&'a self, path: &'a Path) -> BoxFuture<'a, Result<String>> {
self.inner.read_to_string(path)
}
fn write_file<'a>(&'a self, path: &'a Path, content: &'a str) -> BoxFuture<'a, Result<()>> {
Box::pin(async move {
self.mark_local_write_start(path);
let result = self.inner.write_file(path, content).await;
if result.is_ok() {
self.update_crdt_for_file(path, content);
}
self.mark_local_write_end(path);
result
})
}
fn create_new<'a>(&'a self, path: &'a Path, content: &'a str) -> BoxFuture<'a, Result<()>> {
Box::pin(async move {
log::info!(
"[CrdtFs] create_new CALLED: path='{}', enabled={}, content_len={}",
path.display(),
self.is_enabled(),
content.len()
);
self.mark_local_write_start(path);
let result = self.inner.create_new(path, content).await;
log::info!(
"[CrdtFs] create_new RESULT: path='{}', success={}, err={:?}",
path.display(),
result.is_ok(),
result.as_ref().err()
);
if result.is_ok() {
log::info!(
"[CrdtFs] create_new calling update_crdt_for_new_file: path='{}'",
path.display()
);
self.update_crdt_for_new_file(path, content);
}
self.mark_local_write_end(path);
result
})
}
fn delete_file<'a>(&'a self, path: &'a Path) -> BoxFuture<'a, Result<()>> {
Box::pin(async move {
self.mark_local_write_start(path);
let result = self.inner.delete_file(path).await;
if result.is_ok() && self.is_enabled() {
let path_str = Self::normalize_crdt_path(path);
self.update_parent_contents(&path_str, None);
if let Err(e) = self.workspace_crdt.delete_file(&path_str) {
log::warn!(
"Failed to mark file as deleted in CRDT for {}: {}",
path_str,
e
);
}
}
self.mark_local_write_end(path);
result
})
}
fn list_md_files<'a>(&'a self, dir: &'a Path) -> BoxFuture<'a, Result<Vec<PathBuf>>> {
self.inner.list_md_files(dir)
}
fn exists<'a>(&'a self, path: &'a Path) -> BoxFuture<'a, bool> {
self.inner.exists(path)
}
fn create_dir_all<'a>(&'a self, path: &'a Path) -> BoxFuture<'a, Result<()>> {
self.inner.create_dir_all(path)
}
fn is_dir<'a>(&'a self, path: &'a Path) -> BoxFuture<'a, bool> {
self.inner.is_dir(path)
}
fn move_file<'a>(&'a self, from: &'a Path, to: &'a Path) -> BoxFuture<'a, Result<()>> {
Box::pin(async move {
self.mark_local_write_start(from);
self.mark_local_write_start(to);
let result = self.inner.move_file(from, to).await;
if result.is_ok() && self.is_enabled() {
let from_str = Self::normalize_crdt_path(from);
let to_str = Self::normalize_crdt_path(to);
if let Some(doc_id) = self.workspace_crdt.find_by_path(from) {
let new_filename = to
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("")
.to_string();
let from_parent = from.parent();
let to_parent = to.parent();
let is_rename = from_parent == to_parent;
if is_rename {
log::debug!(
"CrdtFs: Renaming doc_id={} from {:?} to {}",
doc_id,
from,
new_filename
);
if let Err(e) = self.workspace_crdt.rename_file(&doc_id, &new_filename) {
log::warn!("Failed to rename file in CRDT: {}", e);
}
} else {
let new_parent_id =
to_parent.and_then(|p| self.workspace_crdt.find_by_path(p));
log::debug!(
"CrdtFs: Moving doc_id={} to parent={:?}, new_filename={}",
doc_id,
new_parent_id,
new_filename
);
if let Err(e) = self
.workspace_crdt
.move_file(&doc_id, new_parent_id.as_deref())
{
log::warn!("Failed to move file in CRDT: {}", e);
}
if let Some(meta) = self.workspace_crdt.get_file(&doc_id) {
if meta.filename != new_filename {
if let Err(e) =
self.workspace_crdt.rename_file(&doc_id, &new_filename)
{
log::warn!("Failed to rename file during move in CRDT: {}", e);
}
}
}
}
self.update_parent_contents(&from_str, Some(&to_str));
} else {
log::debug!(
"CrdtFs: No doc_id found for {:?}, using legacy move behavior",
from
);
self.update_parent_contents(&from_str, Some(&to_str));
if let Err(e) = self.workspace_crdt.delete_file(&from_str) {
log::warn!("Failed to mark old path as deleted in CRDT: {}", e);
}
if let Ok(content) = self.inner.read_to_string(to).await {
self.update_crdt_for_file(to, &content);
}
}
}
self.mark_local_write_end(from);
self.mark_local_write_end(to);
result
})
}
fn read_binary<'a>(&'a self, path: &'a Path) -> BoxFuture<'a, Result<Vec<u8>>> {
self.inner.read_binary(path)
}
fn write_binary<'a>(&'a self, path: &'a Path, content: &'a [u8]) -> BoxFuture<'a, Result<()>> {
self.inner.write_binary(path, content)
}
fn list_files<'a>(&'a self, dir: &'a Path) -> BoxFuture<'a, Result<Vec<PathBuf>>> {
self.inner.list_files(dir)
}
fn get_modified_time<'a>(&'a self, path: &'a Path) -> BoxFuture<'a, Option<i64>> {
self.inner.get_modified_time(path)
}
fn mark_sync_write_start(&self, path: &Path) {
self.mark_sync_write_start_internal(path);
}
fn mark_sync_write_end(&self, path: &Path) {
self.mark_sync_write_end_internal(path);
}
}
impl<FS: AsyncFileSystem> std::fmt::Debug for CrdtFs<FS> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CrdtFs")
.field("enabled", &self.is_enabled())
.field("workspace_crdt", &self.workspace_crdt)
.field("body_doc_manager", &self.body_doc_manager)
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::crdt::{CrdtStorage, MemoryStorage};
use crate::fs::{InMemoryFileSystem, SyncToAsyncFs};
fn create_test_crdt_fs() -> CrdtFs<SyncToAsyncFs<InMemoryFileSystem>> {
let inner = SyncToAsyncFs::new(InMemoryFileSystem::new());
let storage: Arc<dyn CrdtStorage> = Arc::new(MemoryStorage::new());
let workspace_crdt = Arc::new(WorkspaceCrdt::new(Arc::clone(&storage)));
let body_manager = Arc::new(BodyDocManager::new(storage));
CrdtFs::new(inner, workspace_crdt, body_manager)
}
#[test]
fn test_write_updates_crdt() {
let fs = create_test_crdt_fs();
let content = "---\ntitle: Test\npart_of: index.md\n---\nBody content";
futures_lite::future::block_on(async {
fs.write_file(Path::new("test.md"), content).await.unwrap();
});
let metadata = fs.workspace_crdt.get_file("test.md").unwrap();
assert_eq!(metadata.title, Some("Test".to_string()));
assert_eq!(metadata.part_of, Some("index.md".to_string()));
}
#[test]
fn test_delete_marks_deleted_in_crdt() {
let fs = create_test_crdt_fs();
let content = "---\ntitle: Test\n---\nBody";
futures_lite::future::block_on(async {
fs.write_file(Path::new("test.md"), content).await.unwrap();
fs.delete_file(Path::new("test.md")).await.unwrap();
});
let metadata = fs.workspace_crdt.get_file("test.md").unwrap();
assert!(metadata.deleted);
}
#[test]
fn test_disabled_skips_crdt_updates() {
let fs = create_test_crdt_fs();
fs.set_enabled(false);
let content = "---\ntitle: Test\n---\nBody";
futures_lite::future::block_on(async {
fs.write_file(Path::new("test.md"), content).await.unwrap();
});
assert!(fs.workspace_crdt.get_file("test.md").is_none());
}
#[test]
fn test_toggle_enabled() {
let fs = create_test_crdt_fs();
assert!(fs.is_enabled());
fs.set_enabled(false);
assert!(!fs.is_enabled());
fs.set_enabled(true);
assert!(fs.is_enabled());
}
#[test]
fn test_local_write_tracking() {
let fs = create_test_crdt_fs();
assert!(!fs.is_local_write_in_progress(Path::new("test.md")));
fs.mark_local_write_start(Path::new("test.md"));
assert!(fs.is_local_write_in_progress(Path::new("test.md")));
fs.mark_local_write_end(Path::new("test.md"));
assert!(!fs.is_local_write_in_progress(Path::new("test.md")));
}
#[test]
fn test_sync_write_tracking() {
let fs = create_test_crdt_fs();
assert!(!fs.is_sync_write_in_progress(Path::new("test.md")));
fs.mark_sync_write_start(Path::new("test.md"));
assert!(fs.is_sync_write_in_progress(Path::new("test.md")));
fs.mark_sync_write_end(Path::new("test.md"));
assert!(!fs.is_sync_write_in_progress(Path::new("test.md")));
}
#[test]
fn test_sync_write_skips_crdt_update() {
let fs = create_test_crdt_fs();
let content = "---\ntitle: Sync Write Test\n---\nBody content";
futures_lite::future::block_on(async {
fs.write_file(Path::new("test1.md"), content).await.unwrap();
});
assert!(fs.workspace_crdt.get_file("test1.md").is_some());
fs.mark_sync_write_start(Path::new("test2.md"));
futures_lite::future::block_on(async {
fs.write_file(Path::new("test2.md"), content).await.unwrap();
});
fs.mark_sync_write_end(Path::new("test2.md"));
assert!(futures_lite::future::block_on(
fs.exists(Path::new("test2.md"))
));
assert!(
fs.workspace_crdt.get_file("test2.md").is_none(),
"CRDT should not have been updated for sync write"
);
}
#[test]
fn test_markdown_link_part_of_converts_to_canonical() {
let fs = create_test_crdt_fs();
let content =
"---\ntitle: Child\npart_of: \"[Parent Index](/Folder/parent.md)\"\n---\nContent";
futures_lite::future::block_on(async {
fs.write_file(Path::new("Folder/child.md"), content)
.await
.unwrap();
});
let metadata = fs.workspace_crdt.get_file("Folder/child.md").unwrap();
assert_eq!(metadata.part_of, Some("Folder/parent.md".to_string()));
}
#[test]
fn test_relative_part_of_converts_to_canonical() {
let fs = create_test_crdt_fs();
let content = "---\ntitle: Child\npart_of: ../index.md\n---\nContent";
futures_lite::future::block_on(async {
fs.write_file(Path::new("Folder/Sub/child.md"), content)
.await
.unwrap();
});
let metadata = fs.workspace_crdt.get_file("Folder/Sub/child.md").unwrap();
assert_eq!(metadata.part_of, Some("Folder/index.md".to_string()));
}
#[test]
fn test_plain_part_of_at_root_stays_canonical() {
let fs = create_test_crdt_fs();
let content = "---\ntitle: Child\npart_of: index.md\n---\nContent";
futures_lite::future::block_on(async {
fs.write_file(Path::new("child.md"), content).await.unwrap();
});
let metadata = fs.workspace_crdt.get_file("child.md").unwrap();
assert_eq!(metadata.part_of, Some("index.md".to_string()));
}
#[test]
fn test_markdown_link_contents_converts_to_canonical() {
let fs = create_test_crdt_fs();
let content = r#"---
title: Parent Index
contents:
- "[Child 1](/Folder/child1.md)"
- "[Child 2](/Folder/Sub/child2.md)"
---
Content"#;
futures_lite::future::block_on(async {
fs.write_file(Path::new("Folder/index.md"), content)
.await
.unwrap();
});
let metadata = fs.workspace_crdt.get_file("Folder/index.md").unwrap();
assert_eq!(
metadata.contents,
Some(vec![
"Folder/child1.md".to_string(),
"Folder/Sub/child2.md".to_string()
])
);
}
#[test]
fn test_relative_contents_converts_to_canonical() {
let fs = create_test_crdt_fs();
let content = r#"---
title: Parent Index
contents:
- child1.md
- Sub/child2.md
---
Content"#;
futures_lite::future::block_on(async {
fs.write_file(Path::new("Folder/index.md"), content)
.await
.unwrap();
});
let metadata = fs.workspace_crdt.get_file("Folder/index.md").unwrap();
assert_eq!(
metadata.contents,
Some(vec![
"Folder/child1.md".to_string(),
"Folder/Sub/child2.md".to_string()
])
);
}
#[test]
fn test_mixed_format_links_all_convert_to_canonical() {
let fs = create_test_crdt_fs();
let content = r#"---
title: Parent Index
part_of: "[Root](/README.md)"
contents:
- child1.md
- "[Child 2](/Folder/Sub/child2.md)"
- ../sibling.md
---
Content"#;
futures_lite::future::block_on(async {
fs.write_file(Path::new("Folder/index.md"), content)
.await
.unwrap();
});
let metadata = fs.workspace_crdt.get_file("Folder/index.md").unwrap();
assert_eq!(metadata.part_of, Some("README.md".to_string()));
assert_eq!(
metadata.contents,
Some(vec![
"Folder/child1.md".to_string(),
"Folder/Sub/child2.md".to_string(),
"sibling.md".to_string(),
])
);
}
}