From a714bbab85b2629e7ac04f8e6155fe02f8e2eb57 Mon Sep 17 00:00:00 2001 From: Stephen Waits Date: Tue, 19 Aug 2025 15:01:26 -0600 Subject: [PATCH] remove multilist support --- src/commands.rs | 196 ++++++++++++++---------------- src/main.rs | 5 +- src/markdown.rs | 239 +++++++++---------------------------- src/migration.rs | 28 ++--- src/tests.rs | 24 ++-- tests/integration_tests.rs | 6 +- 6 files changed, 171 insertions(+), 327 deletions(-) diff --git a/src/commands.rs b/src/commands.rs index bc74c02..0a3b29a 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -6,7 +6,7 @@ use crate::markdown::{ parse_todo_file, generate_todo_file, parse_archive_file, generate_archive_file, - Task, TaskList, TodoFile, ArchiveFile, TaskRef + Task, TodoFile, ArchiveFile, TaskRef }; use std::collections::HashSet; use std::env; @@ -163,37 +163,26 @@ impl ColorScheme { } /// Adds a new entry to the todo file. -/// Creates the file if it doesn't exist, otherwise prepends to Default list or specified list. +/// Creates the file if it doesn't exist, otherwise prepends to the main list. /// Can add as subtask if `under` is specified. -pub fn add_entry(path: &Path, text: &str, under: Option, list_name: Option<&str>) -> io::Result<()> { +pub fn add_entry(path: &Path, text: &str, under: Option) -> io::Result<()> { let mut todo_file = if path.exists() { let content = fs::read_to_string(path)?; parse_todo_file(&content).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))? } else { - let mut file = TodoFile::new("TODOs".to_string()); - file.add_list(TaskList::new("Default".to_string())); - file + TodoFile::new("TODOs".to_string()) }; - let target_list_name = list_name.unwrap_or("Default"); - - // Ensure the target list exists - if todo_file.get_list(target_list_name).is_none() { - todo_file.add_list(TaskList::new(target_list_name.to_string())); - } - - let target_list = todo_file.get_list_mut(target_list_name).unwrap(); - if let Some(task_num) = under { // Add as subtask - if task_num == 0 || task_num > target_list.tasks.len() { + if task_num == 0 || task_num > todo_file.tasks.len() { return Err(io::Error::new( io::ErrorKind::InvalidInput, - format!("Invalid task number: {}. Valid range: 1-{}", task_num, target_list.tasks.len()) + format!("Invalid task number: {}. Valid range: 1-{}", task_num, todo_file.tasks.len()) )); } - target_list.tasks[task_num - 1].add_subtask(text.to_string()); + todo_file.tasks[task_num - 1].add_subtask(text.to_string()); println!( "{}✓ Added subtask to task {}: {}{}", color::Fg(color::Green), @@ -204,11 +193,10 @@ pub fn add_entry(path: &Path, text: &str, under: Option, list_name: Optio } else { // Add as new main task at top let task = Task::new(text.to_string()); - target_list.prepend_task(task); + todo_file.prepend_task(task); println!( - "{}✓ Added to {}: {}{}", + "{}✓ Added: {}{}", color::Fg(color::Green), - target_list_name, text, color::Fg(color::Reset) ); @@ -218,7 +206,7 @@ pub fn add_entry(path: &Path, text: &str, under: Option, list_name: Optio fs::write(path, content) } -/// Lists tasks from the Default list with numbered display including subtasks. +/// Lists tasks with numbered display including subtasks. /// Displays task numbers and subtask letters, supports filtering. pub fn list_note(path: &Path, num: usize, all: bool, filter: Option<&str>) -> io::Result<()> { if !path.exists() { @@ -234,19 +222,7 @@ pub fn list_note(path: &Path, num: usize, all: bool, filter: Option<&str>) -> io let todo_file = parse_todo_file(&content) .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; - let default_list = match todo_file.get_default_list() { - Some(list) => list, - None => { - println!( - "{}No Default list found.{}", - color::Fg(color::Yellow), - color::Fg(color::Reset) - ); - return Ok(()); - } - }; - - if default_list.is_empty() { + if todo_file.is_empty() { println!( "{}No notes yet.{}", color::Fg(color::Yellow), @@ -255,31 +231,66 @@ pub fn list_note(path: &Path, num: usize, all: bool, filter: Option<&str>) -> io return Ok(()); } - // Build flat list of items for filtering and display - let mut display_items = Vec::new(); - for (task_idx, task) in default_list.tasks.iter().enumerate() { - let task_line = format!("{:3}. {}", task_idx + 1, task.text); - display_items.push((task_idx + 1, None, task_line.clone())); - - // Add subtasks if any - for (subtask_idx, subtask) in task.subtasks.iter().enumerate() { - let letter = (b'a' + subtask_idx as u8) as char; - let subtask_line = format!(" {}. {}", letter, subtask); - display_items.push((task_idx + 1, Some(subtask_idx), subtask_line)); + // Build list of items for filtering and display + let display_items: Vec<_> = if let Some(filter_text) = filter { + let mut filtered = Vec::new(); + let filter_lower = filter_text.to_lowercase(); + + for (task_idx, task) in todo_file.tasks.iter().enumerate() { + let task_num = task_idx + 1; + let task_matches = task.text.to_lowercase().contains(&filter_lower); + + // Check which subtasks match + let mut matching_subtasks = Vec::new(); + for (subtask_idx, subtask) in task.subtasks.iter().enumerate() { + if subtask.to_lowercase().contains(&filter_lower) { + matching_subtasks.push(subtask_idx); + } + } + + if task_matches { + // If task matches, include task and ALL its subtasks + let task_line = format!("{:3}. {}", task_num, task.text); + filtered.push((task_num, None, task_line)); + + for (subtask_idx, subtask) in task.subtasks.iter().enumerate() { + let letter = (b'a' + subtask_idx as u8) as char; + let subtask_line = format!(" {}. {}", letter, subtask); + filtered.push((task_num, Some(subtask_idx), subtask_line)); + } + } else if !matching_subtasks.is_empty() { + // If only subtasks match, include task and only matching subtasks + let task_line = format!("{:3}. {}", task_num, task.text); + filtered.push((task_num, None, task_line)); + + for &subtask_idx in &matching_subtasks { + let letter = (b'a' + subtask_idx as u8) as char; + let subtask_line = format!(" {}. {}", letter, &task.subtasks[subtask_idx]); + filtered.push((task_num, Some(subtask_idx), subtask_line)); + } + } } - } - - // Apply filter - let filtered_items: Vec<_> = if let Some(filter_text) = filter { - display_items - .into_iter() - .filter(|(_, _, line)| line.to_lowercase().contains(&filter_text.to_lowercase())) - .collect() + + filtered } else { - display_items + // No filter - include everything + let mut all_items = Vec::new(); + for (task_idx, task) in todo_file.tasks.iter().enumerate() { + let task_num = task_idx + 1; + let task_line = format!("{:3}. {}", task_num, task.text); + all_items.push((task_num, None, task_line)); + + // Add subtasks if any + for (subtask_idx, subtask) in task.subtasks.iter().enumerate() { + let letter = (b'a' + subtask_idx as u8) as char; + let subtask_line = format!(" {}. {}", letter, subtask); + all_items.push((task_num, Some(subtask_idx), subtask_line)); + } + } + all_items }; - if filtered_items.is_empty() { + if display_items.is_empty() { if filter.is_some() { println!( "{}No items found matching filter: \"{}\"{}", @@ -298,14 +309,14 @@ pub fn list_note(path: &Path, num: usize, all: bool, filter: Option<&str>) -> io } let display_count = if all { - filtered_items.len() + display_items.len() } else { - num.min(filtered_items.len()) + num.min(display_items.len()) }; let color_scheme = ColorScheme::new(); - for (task_num, subtask_idx, line) in filtered_items.iter().take(display_count) { + for (task_num, subtask_idx, line) in display_items.iter().take(display_count) { if subtask_idx.is_none() { // Main task - use HSV-based bright colors let color = color_scheme.get_main_task_color(*task_num); @@ -327,11 +338,11 @@ pub fn list_note(path: &Path, num: usize, all: bool, filter: Option<&str>) -> io } } - if !all && filtered_items.len() > display_count { + if !all && display_items.len() > display_count { println!( "{}... and {} more items{}", color::Fg(color::Yellow), - filtered_items.len() - display_count, + display_items.len() - display_count, color::Fg(color::Reset) ); } @@ -354,21 +365,9 @@ pub fn prioritize_items(todo_path: &Path, refs: &[String]) -> io::Result<()> { let mut todo_file = parse_todo_file(&content) .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; - let default_list = match todo_file.get_default_list_mut() { - Some(list) => list, - None => { - println!( - "{}No Default list found.{}", - color::Fg(color::Yellow), - color::Fg(color::Reset) - ); - return Ok(()); - } - }; - - if default_list.is_empty() { + if todo_file.is_empty() { println!( - "{}No notes to prioritize.{}", + "{}No notes found.{}", color::Fg(color::Yellow), color::Fg(color::Reset) ); @@ -380,19 +379,19 @@ pub fn prioritize_items(todo_path: &Path, refs: &[String]) -> io::Result<()> { for ref_str in refs { match TaskRef::parse(ref_str) { Ok(task_ref) => { - if task_ref.task_index >= default_list.tasks.len() { + if task_ref.task_index >= todo_file.tasks.len() { println!( "{}Invalid task number: {}. Valid range: 1-{}{}", color::Fg(color::Red), task_ref.task_index + 1, - default_list.tasks.len(), + todo_file.tasks.len(), color::Fg(color::Reset) ); return Ok(()); } if let Some(subtask_idx) = task_ref.subtask_index { - let task = &default_list.tasks[task_ref.task_index]; + let task = &todo_file.tasks[task_ref.task_index]; if subtask_idx >= task.subtasks.len() { println!( "{}Invalid subtask: {}{}. Task {} has {} subtasks{}", @@ -438,20 +437,20 @@ pub fn prioritize_items(todo_path: &Path, refs: &[String]) -> io::Result<()> { let mut moved_tasks = Vec::new(); for &task_idx in &tasks_to_move { - moved_tasks.push(default_list.tasks[task_idx].clone()); + moved_tasks.push(todo_file.tasks[task_idx].clone()); } // Add moved tasks first new_tasks.extend(moved_tasks.clone()); // Add remaining tasks - for (idx, task) in default_list.tasks.iter().enumerate() { + for (idx, task) in todo_file.tasks.iter().enumerate() { if !moved_task_indices.contains(&idx) { new_tasks.push(task.clone()); } } - default_list.tasks = new_tasks; + todo_file.tasks = new_tasks; let new_content = generate_todo_file(&todo_file); fs::write(todo_path, new_content)?; @@ -490,19 +489,7 @@ pub fn archive_items(todo_path: &Path, archive_path: &Path, refs: &[String]) -> let mut todo_file = parse_todo_file(&content) .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; - let default_list = match todo_file.get_default_list_mut() { - Some(list) => list, - None => { - println!( - "{}No Default list found.{}", - color::Fg(color::Yellow), - color::Fg(color::Reset) - ); - return Ok(()); - } - }; - - if default_list.is_empty() { + if todo_file.is_empty() { println!( "{}No notes to archive.{}", color::Fg(color::Yellow), @@ -535,13 +522,13 @@ pub fn archive_items(todo_path: &Path, archive_path: &Path, refs: &[String]) -> let mut whole_tasks_to_remove = HashSet::new(); for (ref_str, task_ref) in &task_refs { - if task_ref.task_index >= default_list.tasks.len() { + if task_ref.task_index >= todo_file.tasks.len() { println!( "{}Invalid task number in '{}': {}. Valid range: 1-{}{}", color::Fg(color::Red), ref_str, task_ref.task_index + 1, - default_list.tasks.len(), + todo_file.tasks.len(), color::Fg(color::Reset) ); return Ok(()); @@ -549,7 +536,7 @@ pub fn archive_items(todo_path: &Path, archive_path: &Path, refs: &[String]) -> if let Some(subtask_idx) = task_ref.subtask_index { // Archiving a subtask - let task = &default_list.tasks[task_ref.task_index]; + let task = &todo_file.tasks[task_ref.task_index]; if subtask_idx >= task.subtasks.len() { println!( "{}Invalid subtask '{}': Task {} has {} subtasks{}", @@ -570,12 +557,12 @@ pub fn archive_items(todo_path: &Path, archive_path: &Path, refs: &[String]) -> // Collect items to archive for &task_idx in &whole_tasks_to_remove { - tasks_to_archive.push(default_list.tasks[task_idx].clone()); + tasks_to_archive.push(todo_file.tasks[task_idx].clone()); } for &(task_idx, subtask_idx) in &subtasks_to_remove { if !whole_tasks_to_remove.contains(&task_idx) { - let subtask_text = default_list.tasks[task_idx].subtasks[subtask_idx].clone(); + let subtask_text = todo_file.tasks[task_idx].subtasks[subtask_idx].clone(); tasks_to_archive.push(Task::new(subtask_text)); } } @@ -611,11 +598,11 @@ pub fn archive_items(todo_path: &Path, archive_path: &Path, refs: &[String]) -> for (task_idx, mut subtask_indices) in subtasks_by_task { subtask_indices.sort_by(|a, b| b.cmp(a)); // Sort in reverse order for subtask_idx in subtask_indices { - default_list.tasks[task_idx].subtasks.remove(subtask_idx); + todo_file.tasks[task_idx].subtasks.remove(subtask_idx); } // Check if this task now has no subtasks left and should be auto-completed - if default_list.tasks[task_idx].subtasks.is_empty() { + if todo_file.tasks[task_idx].subtasks.is_empty() { tasks_to_auto_complete.push(task_idx); } } @@ -624,7 +611,7 @@ pub fn archive_items(todo_path: &Path, archive_path: &Path, refs: &[String]) -> let mut auto_completed_tasks = Vec::new(); if !tasks_to_auto_complete.is_empty() { for &task_idx in &tasks_to_auto_complete { - auto_completed_tasks.push(default_list.tasks[task_idx].clone()); + auto_completed_tasks.push(todo_file.tasks[task_idx].clone()); } // Add auto-completed tasks to archive @@ -642,7 +629,7 @@ pub fn archive_items(todo_path: &Path, archive_path: &Path, refs: &[String]) -> whole_task_indices.dedup(); // Remove duplicates in case a task was both manually selected and auto-completed for task_idx in whole_task_indices { - default_list.tasks.remove(task_idx); + todo_file.tasks.remove(task_idx); } // Save updated todo file @@ -715,8 +702,7 @@ pub fn edit_note(todo_path: &Path) -> io::Result<()> { // Create the file if it doesn't exist if !todo_path.exists() { - let mut empty_file = TodoFile::new("TODOs".to_string()); - empty_file.add_list(TaskList::new("Default".to_string())); + let empty_file = TodoFile::new("TODOs".to_string()); let content = generate_todo_file(&empty_file); fs::write(todo_path, content)?; } diff --git a/src/main.rs b/src/main.rs index 37c30b3..37f482d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -37,9 +37,6 @@ enum Commands { /// Add as subtask under this task number (e.g., --under 3) #[arg(long)] under: Option, - /// Add to specific list instead of Default - #[arg(long)] - list: Option, }, /// List the top N items (default 5) #[command(aliases = ["l", "list"])] @@ -106,7 +103,7 @@ fn main() -> io::Result<()> { } match cli.command { - Commands::Add { text, under, list } => commands::add_entry(&todo_md_path, &text, under, list.as_deref()), + Commands::Add { text, under } => commands::add_entry(&todo_md_path, &text, under), Commands::Ls { num, all, filter } => { commands::list_note(&todo_md_path, num, all, filter.as_deref()) } diff --git a/src/markdown.rs b/src/markdown.rs index c145def..88b5826 100644 --- a/src/markdown.rs +++ b/src/markdown.rs @@ -40,17 +40,17 @@ impl Task { } } -/// Represents a named list of tasks +/// Represents the entire todo file with a single task list #[derive(Debug, Clone, PartialEq)] -pub struct TaskList { - pub name: String, +pub struct TodoFile { + pub title: String, pub tasks: Vec, } -impl TaskList { - pub fn new(name: String) -> Self { - TaskList { - name, +impl TodoFile { + pub fn new(title: String) -> Self { + TodoFile { + title, tasks: Vec::new(), } } @@ -70,56 +70,6 @@ impl TaskList { pub fn task_count(&self) -> usize { self.tasks.len() } - - #[allow(dead_code)] - pub fn total_item_count(&self) -> usize { - self.tasks - .iter() - .map(|t| 1 + t.subtask_count()) - .sum() - } -} - -/// Represents the entire todo file with multiple lists -#[derive(Debug, Clone, PartialEq)] -pub struct TodoFile { - pub title: String, - pub lists: Vec, -} - -impl TodoFile { - pub fn new(title: String) -> Self { - TodoFile { - title, - lists: Vec::new(), - } - } - - pub fn add_list(&mut self, list: TaskList) { - self.lists.push(list); - } - - pub fn get_default_list_mut(&mut self) -> Option<&mut TaskList> { - self.lists.iter_mut().find(|list| list.name == "Default") - } - - pub fn get_default_list(&self) -> Option<&TaskList> { - self.lists.iter().find(|list| list.name == "Default") - } - - pub fn get_list_mut(&mut self, name: &str) -> Option<&mut TaskList> { - self.lists.iter_mut().find(|list| list.name == name) - } - - pub fn get_list(&self, name: &str) -> Option<&TaskList> { - self.lists.iter().find(|list| list.name == name) - } - - pub fn ensure_default_list(&mut self) { - if self.get_default_list().is_none() { - self.lists.insert(0, TaskList::new("Default".to_string())); - } - } } /// Parse task reference in format "1", "2a", "10b", etc. @@ -185,7 +135,6 @@ pub fn parse_todo_file(content: &str) -> Result { } let mut todo_file = TodoFile::new("TODOs".to_string()); - let mut current_list: Option = None; let mut current_task: Option = None; for line in lines.iter() { @@ -203,28 +152,9 @@ pub fn parse_todo_file(content: &str) -> Result { let title = &trimmed[1..]; todo_file.title = title.trim().to_string(); } - // Handle list headers - be flexible with spacing - else if let Some(list_name) = trimmed.strip_prefix("## ") { - // Save previous task if exists - if let (Some(mut list), Some(task)) = (current_list.take(), current_task.take()) { - list.add_task(task); - todo_file.add_list(list); - } else if let Some(list) = current_list.take() { - todo_file.add_list(list); - } - - current_list = Some(TaskList::new(list_name.trim().to_string())); - } else if trimmed.starts_with("##") && !trimmed.starts_with("###") { - // Handle cases where user might not have space after ## (but not ###) - let list_name = &trimmed[2..]; - if let (Some(mut list), Some(task)) = (current_list.take(), current_task.take()) { - list.add_task(task); - todo_file.add_list(list); - } else if let Some(list) = current_list.take() { - todo_file.add_list(list); - } - - current_list = Some(TaskList::new(list_name.trim().to_string())); + // Skip any list headers - we ignore them now since we don't support multiple lists + else if trimmed.starts_with("##") { + continue; } // Handle subtasks - be flexible with indentation (2, 3, or 4 spaces, or tab) else if let Some(subtask_text) = line.strip_prefix(" - ") { @@ -232,82 +162,49 @@ pub fn parse_todo_file(content: &str) -> Result { task.add_subtask(subtask_text.trim().to_string()); } else { // If no current task, treat as main task (user error but be forgiving) - if current_list.is_none() { - current_list = Some(TaskList::new("Default".to_string())); - } - if let Some(ref mut list) = current_list { - list.add_task(Task::new(subtask_text.trim().to_string())); - } + current_task = Some(Task::new(subtask_text.trim().to_string())); } } else if let Some(subtask_text) = line.strip_prefix(" - ") { // Handle 3-space indentation if let Some(ref mut task) = current_task { task.add_subtask(subtask_text.trim().to_string()); } else { - if current_list.is_none() { - current_list = Some(TaskList::new("Default".to_string())); - } - if let Some(ref mut list) = current_list { - list.add_task(Task::new(subtask_text.trim().to_string())); - } + current_task = Some(Task::new(subtask_text.trim().to_string())); } } else if let Some(subtask_text) = line.strip_prefix(" - ") { // Handle 4-space indentation (but not deeper nesting) if let Some(ref mut task) = current_task { task.add_subtask(subtask_text.trim().to_string()); } else { - if current_list.is_none() { - current_list = Some(TaskList::new("Default".to_string())); - } - if let Some(ref mut list) = current_list { - list.add_task(Task::new(subtask_text.trim().to_string())); - } + current_task = Some(Task::new(subtask_text.trim().to_string())); } } else if let Some(subtask_text) = line.strip_prefix("\t- ") { // Handle tab indentation if let Some(ref mut task) = current_task { task.add_subtask(subtask_text.trim().to_string()); } else { - if current_list.is_none() { - current_list = Some(TaskList::new("Default".to_string())); - } - if let Some(ref mut list) = current_list { - list.add_task(Task::new(subtask_text.trim().to_string())); - } + current_task = Some(Task::new(subtask_text.trim().to_string())); } } // Handle main tasks - flexible with spacing and different bullet styles else if let Some(task_text) = trimmed.strip_prefix("- ") { // Save previous task if exists - if let (Some(list), Some(task)) = (current_list.as_mut(), current_task.take()) { - list.add_task(task); - } - - // Ensure we have a list - if current_list.is_none() { - current_list = Some(TaskList::new("Default".to_string())); + if let Some(task) = current_task.take() { + todo_file.add_task(task); } current_task = Some(Task::new(task_text.trim().to_string())); } else if let Some(task_text) = trimmed.strip_prefix("* ") { // Handle asterisk bullet points - if let (Some(list), Some(task)) = (current_list.as_mut(), current_task.take()) { - list.add_task(task); - } - - if current_list.is_none() { - current_list = Some(TaskList::new("Default".to_string())); + if let Some(task) = current_task.take() { + todo_file.add_task(task); } current_task = Some(Task::new(task_text.trim().to_string())); } else if let Some(task_text) = trimmed.strip_prefix("+ ") { // Handle plus bullet points - if let (Some(list), Some(task)) = (current_list.as_mut(), current_task.take()) { - list.add_task(task); - } - - if current_list.is_none() { - current_list = Some(TaskList::new("Default".to_string())); + if let Some(task) = current_task.take() { + todo_file.add_task(task); } current_task = Some(Task::new(task_text.trim().to_string())); @@ -322,13 +219,8 @@ pub fn parse_todo_file(content: &str) -> Result { // If it looks like it might be a task without proper formatting, treat it as one if !trimmed.starts_with('#') && !trimmed.starts_with('<') && !trimmed.contains("```") { // Save previous task if exists - if let (Some(list), Some(task)) = (current_list.as_mut(), current_task.take()) { - list.add_task(task); - } - - // Ensure we have a list - if current_list.is_none() { - current_list = Some(TaskList::new("Default".to_string())); + if let Some(task) = current_task.take() { + todo_file.add_task(task); } current_task = Some(Task::new(trimmed.to_string())); @@ -337,17 +229,11 @@ pub fn parse_todo_file(content: &str) -> Result { } } - // Save final task and list - if let Some(mut list) = current_list { - if let Some(task) = current_task { - list.add_task(task); - } - todo_file.add_list(list); + // Save final task + if let Some(task) = current_task { + todo_file.add_task(task); } - // Ensure there's always a Default list - todo_file.ensure_default_list(); - Ok(todo_file) } @@ -356,19 +242,14 @@ pub fn generate_todo_file(todo_file: &TodoFile) -> String { let mut content = String::new(); content.push_str(&format!("# {}\n\n", todo_file.title)); - for list in &todo_file.lists { - content.push_str(&format!("## {}\n", list.name)); - - for task in &list.tasks { - content.push_str(&format!("- {}\n", task.text)); - for subtask in &task.subtasks { - content.push_str(&format!(" - {}\n", subtask)); - } + for task in &todo_file.tasks { + content.push_str(&format!("- {}\n", task.text)); + for subtask in &task.subtasks { + content.push_str(&format!(" - {}\n", subtask)); } - content.push('\n'); } - content.trim_end().to_string() + "\n" + content } /// Parse an archive file with date-based sections @@ -586,41 +467,33 @@ mod tests { fn test_parse_simple_todo_file() { let content = r#"# TODOs -## Default - First task - Second task - Subtask A - Subtask B - -## Work -- Work task +- Third task "#; let todo_file = parse_todo_file(content).unwrap(); assert_eq!(todo_file.title, "TODOs"); - assert_eq!(todo_file.lists.len(), 2); - - let default_list = todo_file.get_list("Default").unwrap(); - assert_eq!(default_list.tasks.len(), 2); - assert_eq!(default_list.tasks[1].subtasks.len(), 2); + assert_eq!(todo_file.tasks.len(), 3); + assert_eq!(todo_file.tasks[1].subtasks.len(), 2); + assert_eq!(todo_file.tasks[1].subtasks[0], "Subtask A"); + assert_eq!(todo_file.tasks[1].subtasks[1], "Subtask B"); } #[test] fn test_generate_todo_file() { let mut todo_file = TodoFile::new("TODOs".to_string()); - let mut default_list = TaskList::new("Default".to_string()); let mut task = Task::new("Task with subtasks".to_string()); task.add_subtask("Subtask 1".to_string()); task.add_subtask("Subtask 2".to_string()); - default_list.add_task(task); - - todo_file.add_list(default_list); + todo_file.add_task(task); let generated = generate_todo_file(&todo_file); let expected = r#"# TODOs -## Default - Task with subtasks - Subtask 1 - Subtask 2 @@ -632,7 +505,6 @@ mod tests { fn test_handle_deep_nesting_gracefully() { let content = r#"# TODOs -## Default - Task - Subtask - Deep subtask @@ -640,21 +512,19 @@ mod tests { // Resilient parser should ignore deep nesting rather than error let todo_file = parse_todo_file(content).unwrap(); - let default_list = todo_file.get_list("Default").unwrap(); // The resilient parser treats 4 spaces as valid subtask indentation // So "Deep subtask" becomes a subtask rather than being ignored - assert_eq!(default_list.tasks.len(), 1); - assert_eq!(default_list.tasks[0].text, "Task"); - assert_eq!(default_list.tasks[0].subtasks.len(), 2); - assert_eq!(default_list.tasks[0].subtasks[0], "Subtask"); - assert_eq!(default_list.tasks[0].subtasks[1], "Deep subtask"); + assert_eq!(todo_file.tasks.len(), 1); + assert_eq!(todo_file.tasks[0].text, "Task"); + assert_eq!(todo_file.tasks[0].subtasks.len(), 2); + assert_eq!(todo_file.tasks[0].subtasks[0], "Subtask"); + assert_eq!(todo_file.tasks[0].subtasks[1], "Deep subtask"); } #[test] fn test_resilient_parsing_various_formats() { let content = r#"# TODOs -##Default - Task with dash * Task with asterisk + Task with plus @@ -663,7 +533,6 @@ mod tests { - Subtask with 4 spaces - Subtask with tab -## Work Plain text task without bullet - Normal task @@ -676,25 +545,25 @@ This is a code block let todo_file = parse_todo_file(content).unwrap(); assert_eq!(todo_file.title, "TODOs"); - let default_list = todo_file.get_list("Default").unwrap(); - assert_eq!(default_list.tasks.len(), 3); // 3 bullet tasks (all subtasks go under plus task) + // Now all tasks are in the single task list + assert_eq!(todo_file.tasks.len(), 6); // All tasks are top-level now (including code block content) // First task (dash) has no subtasks - assert_eq!(default_list.tasks[0].text, "Task with dash"); - assert_eq!(default_list.tasks[0].subtasks.len(), 0); + assert_eq!(todo_file.tasks[0].text, "Task with dash"); + assert_eq!(todo_file.tasks[0].subtasks.len(), 0); // Second task (asterisk) has no subtasks - assert_eq!(default_list.tasks[1].text, "Task with asterisk"); - assert_eq!(default_list.tasks[1].subtasks.len(), 0); + assert_eq!(todo_file.tasks[1].text, "Task with asterisk"); + assert_eq!(todo_file.tasks[1].subtasks.len(), 0); // Third task (plus) has all the subtasks (different indentation styles) - assert_eq!(default_list.tasks[2].text, "Task with plus"); - assert_eq!(default_list.tasks[2].subtasks.len(), 4); + assert_eq!(todo_file.tasks[2].text, "Task with plus"); + assert_eq!(todo_file.tasks[2].subtasks.len(), 4); - let work_list = todo_file.get_list("Work").unwrap(); - assert_eq!(work_list.tasks.len(), 3); // Plain text + normal task + code block content - assert_eq!(work_list.tasks[0].text, "Plain text task without bullet"); - assert_eq!(work_list.tasks[1].text, "Normal task"); - // Comments are ignored, but code block content might be parsed as task + // Plain text task and normal task + assert_eq!(todo_file.tasks[3].text, "Plain text task without bullet"); + assert_eq!(todo_file.tasks[4].text, "Normal task"); + assert_eq!(todo_file.tasks[5].text, "This is a code block"); + // Comments are ignored but code block content is parsed } } \ No newline at end of file diff --git a/src/migration.rs b/src/migration.rs index 25f407c..937ec1d 100644 --- a/src/migration.rs +++ b/src/migration.rs @@ -3,7 +3,7 @@ //! This module handles the one-time conversion from the old plain text format //! to the new Markdown format with Default list structure. -use crate::markdown::{Task, TaskList, TodoFile, ArchiveFile}; +use crate::markdown::{Task, TodoFile, ArchiveFile}; use std::fs; use std::path::Path; @@ -25,18 +25,16 @@ pub fn migrate_note_file(note_path: &Path) -> Result { .map_err(|e| format!("Failed to read note file: {}", e))?; let mut todo_file = TodoFile::new("TODOs".to_string()); - let mut default_list = TaskList::new("Default".to_string()); // Split content into lines and create tasks for line in content.lines() { let trimmed = line.trim(); if !trimmed.is_empty() { let task = Task::new(trimmed.to_string()); - default_list.add_task(task); + todo_file.add_task(task); } } - todo_file.add_list(default_list); Ok(todo_file) } @@ -68,11 +66,9 @@ pub fn migrate_archive_file(archive_path: &Path) -> Result Ok(archive_file) } -/// Create an empty todo file with just a Default list +/// Create an empty todo file pub fn create_empty_todo_file() -> TodoFile { - let mut todo_file = TodoFile::new("TODOs".to_string()); - todo_file.add_list(TaskList::new("Default".to_string())); - todo_file + TodoFile::new("TODOs".to_string()) } /// Create backup files before migration @@ -121,7 +117,7 @@ pub fn perform_migration( .map_err(|e| format!("Failed to write archive.md: {}", e))?; // Count migrated items - let todo_count = todo_file.get_default_list().map(|l| l.task_count()).unwrap_or(0); + let todo_count = todo_file.task_count(); let archive_count = archive_file.entries.first() .and_then(|e| e.lists.get("Default")) .map(|tasks| tasks.len()) @@ -157,11 +153,10 @@ mod tests { let result = migrate_note_file(file.path()).unwrap(); assert_eq!(result.title, "TODOs"); - let default_list = result.get_list("Default").unwrap(); - assert_eq!(default_list.tasks.len(), 3); - assert_eq!(default_list.tasks[0].text, "First task"); - assert_eq!(default_list.tasks[1].text, "Second task"); - assert_eq!(default_list.tasks[2].text, "Third task"); + assert_eq!(result.tasks.len(), 3); + assert_eq!(result.tasks[0].text, "First task"); + assert_eq!(result.tasks[1].text, "Second task"); + assert_eq!(result.tasks[2].text, "Third task"); } #[test] @@ -170,8 +165,7 @@ mod tests { let result = migrate_note_file(file.path()).unwrap(); assert_eq!(result.title, "TODOs"); - let default_list = result.get_list("Default").unwrap(); - assert_eq!(default_list.tasks.len(), 0); + assert_eq!(result.tasks.len(), 0); } #[test] @@ -197,7 +191,7 @@ mod tests { let nonexistent = temp_dir.path().join("nonexistent.txt"); let todo_result = migrate_note_file(&nonexistent).unwrap(); - assert!(todo_result.get_list("Default").unwrap().is_empty()); + assert!(todo_result.is_empty()); let archive_result = migrate_archive_file(&nonexistent).unwrap(); assert!(archive_result.entries.is_empty()); diff --git a/src/tests.rs b/src/tests.rs index 10d656c..99d3825 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -250,7 +250,7 @@ mod list_tests { /// Helper function to create Markdown todo files for new tests fn create_markdown_test_file(tasks: &[&str]) -> NamedTempFile { - let mut content = String::from("# TODOs\n\n## Default\n"); + let mut content = String::from("# TODOs\n\n"); for task in tasks { content.push_str(&format!("- {}\n", task)); } @@ -475,7 +475,7 @@ mod remove_tests { /// Helper function to create Markdown todo files for new tests fn create_markdown_test_file(tasks: &[&str]) -> NamedTempFile { - let mut content = String::from("# TODOs\n\n## Default\n"); + let mut content = String::from("# TODOs\n\n"); for task in tasks { content.push_str(&format!("- {}\n", task)); } @@ -487,7 +487,7 @@ mod remove_tests { /// Helper function to create Markdown todo files with subtasks fn create_markdown_test_file_with_subtasks(tasks_with_subtasks: &[(&str, &[&str])]) -> NamedTempFile { - let mut content = String::from("# TODOs\n\n## Default\n"); + let mut content = String::from("# TODOs\n\n"); for (task, subtasks) in tasks_with_subtasks { content.push_str(&format!("- {}\n", task)); for subtask in *subtasks { @@ -512,7 +512,7 @@ mod remove_tests { // Verify the item was removed from the file let updated_content = std::fs::read_to_string(file.path()).unwrap(); - let expected_content = "# TODOs\n\n## Default\n- First item\n- Third item\n"; + let expected_content = "# TODOs\n\n- First item\n- Third item\n"; assert_eq!(updated_content, expected_content); } @@ -528,7 +528,7 @@ mod remove_tests { // Verify the items were removed from the file let updated_content = std::fs::read_to_string(file.path()).unwrap(); - let expected_content = "# TODOs\n\n## Default\n- Second item\n- Fourth item\n"; + let expected_content = "# TODOs\n\n- Second item\n- Fourth item\n"; assert_eq!(updated_content, expected_content); } @@ -542,9 +542,9 @@ mod remove_tests { }); assert!(result.is_ok()); - // Verify the file has only empty Default list + // Verify the file has only the header let updated_content = std::fs::read_to_string(file.path()).unwrap(); - let expected_content = "# TODOs\n\n## Default\n"; + let expected_content = "# TODOs\n\n"; assert_eq!(updated_content, expected_content); } @@ -576,7 +576,7 @@ mod remove_tests { // Verify only unique items were removed let updated_content = std::fs::read_to_string(file.path()).unwrap(); - let expected_content = "# TODOs\n\n## Default\n- Third item\n"; + let expected_content = "# TODOs\n\n- Third item\n"; assert_eq!(updated_content, expected_content); } @@ -605,9 +605,9 @@ mod remove_tests { }); assert!(result.is_ok()); - // Verify the file remains with empty Default list + // Verify the file remains with just the header let updated_content = std::fs::read_to_string(file.path()).unwrap(); - let expected_content = "# TODOs\n\n## Default\n"; + let expected_content = "# TODOs\n\n"; assert_eq!(updated_content, expected_content); } @@ -628,7 +628,7 @@ mod remove_tests { // Verify the item was removed from the original file let updated_content = std::fs::read_to_string(file.path()).unwrap(); - let expected_content = "# TODOs\n\n## Default\n- Second item\n"; + let expected_content = "# TODOs\n\n- Second item\n"; assert_eq!(updated_content, expected_content); } @@ -652,7 +652,7 @@ mod remove_tests { // Verify Task B was auto-completed and removed from todo file let updated_content = std::fs::read_to_string(file.path()).unwrap(); - let expected_content = "# TODOs\n\n## Default\n- Task A\n- Task C\n - Another subtask\n"; + let expected_content = "# TODOs\n\n- Task A\n- Task C\n - Another subtask\n"; assert_eq!(updated_content, expected_content); // Verify both subtasks AND the parent task are in the archive diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index 1925081..1a1369d 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -118,7 +118,7 @@ mod basic_operations { // Add first item let result = env.run_ldr(&["add", "First task"]); result.assert_success(); - assert!(result.stdout.contains("✓ Added to Default: First task")); + assert!(result.stdout.contains("✓ Added: First task")); // Add second item (should go to top) let result = env.run_ldr(&["add", "Second task"]); @@ -639,7 +639,6 @@ mod edit_functionality { // File should exist with proper structure let todos = env.read_todos(); assert!(todos.contains("# TODOs")); - assert!(todos.contains("## Default")); } #[test] @@ -694,7 +693,6 @@ mod migration_tests { // Verify content was migrated correctly let todos = env.read_todos(); assert!(todos.contains("# TODOs")); - assert!(todos.contains("## Default")); assert!(todos.contains("- Task A")); assert!(todos.contains("- Task B with details")); assert!(todos.contains("- Task C")); @@ -715,7 +713,7 @@ mod migration_tests { // Create markdown file first fs::create_dir_all(env.todos_path().parent().unwrap()).unwrap(); - fs::write(&env.todos_path(), "# TODOs\n\n## Default\n- Existing task\n").unwrap(); + fs::write(&env.todos_path(), "# TODOs\n\n- Existing task\n").unwrap(); // Create old-style file let old_note_path = env.data_dir.join("ldr/note.txt");