use crate::todo_extractor::MarkedItem;
use crate::todo_md_internal::TodoCollection;
use regex::Regex;
use std::fs;
use std::path::Path;
use std::path::PathBuf;
pub fn read_todo_file(todo_path: &Path) -> Vec<MarkedItem> {
let mut todos = Vec::new();
let content = fs::read_to_string(todo_path).unwrap_or_default();
let section_re = Regex::new(r"^##\s+(.*)$").unwrap();
let todo_re = Regex::new(r"^\*\s+\[(.+):(\d+)\]\(.+#L\d+\):\s*(.+)$").unwrap();
let mut current_file: Option<String> = None;
for line in content.lines() {
let line = line.trim();
if line.is_empty() {
continue;
}
if let Some(caps) = section_re.captures(line) {
current_file = Some(caps[1].trim().to_string());
continue;
}
if let Some(caps) = todo_re.captures(line) {
let file_path_str = current_file.clone().unwrap_or_else(|| caps[1].to_string());
let file_path = PathBuf::from(file_path_str);
let line_number = caps[2].parse::<usize>().unwrap_or(0);
let message = caps[3].to_string();
todos.push(MarkedItem {
file_path,
line_number,
message,
});
}
}
todos
}
pub fn sync_todo_file(
todo_path: &Path,
new_todos: Vec<MarkedItem>,
scanned_files: Vec<PathBuf>,
deleted_files: Vec<PathBuf>,
) -> Result<(), std::io::Error> {
let existing_todos = read_todo_file(todo_path);
let mut existing_collection = TodoCollection::new();
for item in existing_todos {
existing_collection.add_item(item);
}
let mut new_collection = TodoCollection::new();
for item in new_todos {
new_collection.add_item(item);
}
existing_collection.merge(new_collection, scanned_files, deleted_files);
let merged_todos = existing_collection.to_sorted_vec();
write_todo_file(todo_path, &merged_todos)
}
pub fn write_todo_file(todo_path: &Path, todos: &[MarkedItem]) -> std::io::Result<()> {
let mut collection = TodoCollection::new();
for todo in todos {
collection.add_item(todo.clone());
}
let mut files: Vec<_> = collection.todos.keys().collect();
files.sort_by_key(|a| a.display().to_string());
let mut content = String::new();
for file in files {
content.push_str(&format!("## {}\n", file.display()));
let mut items = collection.todos.get(file).unwrap().clone();
items.sort_by_key(|item| item.line_number);
for item in items {
content.push_str(&format!(
"* [{}:{}]({}#L{}): {}\n",
item.file_path.display(),
item.line_number,
item.file_path.display(),
item.line_number,
item.message
));
}
content.push('\n');
}
fs::write(todo_path, content)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::todo_extractor::MarkedItem;
use std::fs;
use std::path::PathBuf;
use tempfile::tempdir;
#[test]
fn test_sync_todo_file() {
let temp_dir = tempdir().unwrap();
let todo_path = temp_dir.path().join("TODO.md");
let new_todos = vec![
MarkedItem {
file_path: PathBuf::from("src/main.rs"),
line_number: 10,
message: "Refactor this function".to_string(),
},
MarkedItem {
file_path: PathBuf::from("src/lib.rs"),
line_number: 5,
message: "Add error handling".to_string(),
},
];
let _ = sync_todo_file(&todo_path, new_todos.clone(), vec![], vec![]);
let content = fs::read_to_string(&todo_path).unwrap();
assert!(content.contains("src/main.rs:10"));
assert!(content.contains("Refactor this function"));
assert!(content.contains("src/lib.rs:5"));
assert!(content.contains("Add error handling"));
}
#[test]
fn test_read_todo_file_with_markdown_parser() {
let content = r#"
## src/main.rs
* [src/main.rs:12](src/main.rs#L12): Refactor this function
## src/lib.rs
* [src/lib.rs:5](src/lib.rs#L5): Add error handling
"#;
let temp_dir = tempdir().unwrap();
let todo_path = temp_dir.path().join("TODO.md");
fs::write(&todo_path, content).unwrap();
let todos = read_todo_file(&todo_path);
assert_eq!(todos.len(), 2);
assert_eq!(
todos[0],
MarkedItem {
file_path: PathBuf::from("src/main.rs"),
line_number: 12,
message: "Refactor this function".to_string(),
}
);
assert_eq!(
todos[1],
MarkedItem {
file_path: PathBuf::from("src/lib.rs"),
line_number: 5,
message: "Add error handling".to_string(),
}
);
}
#[test]
fn test_write_todo_file_sectioned() {
let temp_dir = tempdir().unwrap();
let todo_path = temp_dir.path().join("TODO.md");
let items = vec![
MarkedItem {
file_path: PathBuf::from("src/foo.rs"),
line_number: 20,
message: "Fix bug in foo".to_string(),
},
MarkedItem {
file_path: PathBuf::from("src/bar.rs"),
line_number: 10,
message: "Refactor bar".to_string(),
},
MarkedItem {
file_path: PathBuf::from("src/foo.rs"),
line_number: 30,
message: "Add tests for foo".to_string(),
},
];
let result = write_todo_file(&todo_path, &items);
assert!(result.is_ok());
let content = fs::read_to_string(&todo_path).unwrap();
assert!(
content.contains("## src/bar.rs"),
"Missing section for src/bar.rs"
);
assert!(
content.contains("## src/foo.rs"),
"Missing section for src/foo.rs"
);
let expected_bar = "* [src/bar.rs:10](src/bar.rs#L10): Refactor bar";
let expected_foo_20 = "* [src/foo.rs:20](src/foo.rs#L20): Fix bug in foo";
let expected_foo_30 = "* [src/foo.rs:30](src/foo.rs#L30): Add tests for foo";
assert!(content.contains(expected_bar));
assert!(content.contains(expected_foo_20));
assert!(content.contains(expected_foo_30));
let bar_index = content.find("## src/bar.rs").unwrap();
let foo_index = content.find("## src/foo.rs").unwrap();
assert!(bar_index < foo_index, "Section ordering is incorrect");
}
}