remove scan subcommand, rely solely on edit now

This commit is contained in:
swaits committed 2025-08-19 14:28:14 -06:00
1 parent e6bb65d37b
commit d658da2e99
3 files changed
+187 -81

No files matched your search

+1 -46
View File
@@ -1,14 +1,13 @@
//! Command implementations for the ldr todo system with Markdown support.
//!
//! This module contains all the core functionality for managing todo items,
//! including adding, listing, prioritizing, archiving, and interactive review.
//! including adding, listing, prioritizing, archiving, and editing.
//! Now supports subtasks and multiple lists in Markdown format.
use crate::markdown::{
parse_todo_file, generate_todo_file, parse_archive_file, generate_archive_file,
Task, TaskList, TodoFile, ArchiveFile, TaskRef
};
// use crate::input::read_key_input; // TODO: Implement full interactive review
use std::collections::HashSet;
use std::env;
use std::fs;
@@ -559,50 +558,6 @@ pub fn remove_items(todo_path: &Path, refs: &[String]) -> io::Result<()> {
result
}
/// Interactive review mode with subtask support
pub fn review_note(todo_path: &Path, _archive_path: &Path) -> io::Result<()> {
if !todo_path.exists() {
println!("No notes to review.");
return Ok(());
}
let content = fs::read_to_string(todo_path)?;
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.");
return Ok(());
}
};
if default_list.is_empty() {
println!("No notes to review.");
return Ok(());
}
// For now, implement a simplified review that works with whole tasks
// Full subtask-aware review would be a significant expansion
println!(
"{}=== LDR Review Mode ==={}",
color::Fg(color::Cyan),
color::Fg(color::Reset)
);
println!(
"{}Interactive review with subtasks not yet fully implemented.{}",
color::Fg(color::Yellow),
color::Fg(color::Reset)
);
println!(
"{}Use 'ldr edit' to manually edit todos.md for now.{}",
color::Fg(color::Yellow),
color::Fg(color::Reset)
);
Ok(())
}
/// Opens the todo file in the user's preferred editor
pub fn edit_note(todo_path: &Path) -> io::Result<()> {
-4
View File
@@ -70,9 +70,6 @@ enum Commands {
/// Item references to remove (e.g., "1", "2a", "3b")
refs: Vec<String>,
},
/// Scan and review items interactively, from top to bottom
#[command(aliases = ["s", "r", "review"])]
Scan,
/// Edit items in your $EDITOR
#[command(alias = "e")]
Edit,
@@ -116,7 +113,6 @@ fn main() -> io::Result<()> {
Commands::Up { refs } => commands::prioritize_items(&todo_md_path, &refs),
Commands::Do { refs } => commands::archive_items(&todo_md_path, &archive_md_path, &refs),
Commands::Rm { refs } => commands::remove_items(&todo_md_path, &refs),
Commands::Scan => commands::review_note(&todo_md_path, &archive_md_path),
Commands::Edit => commands::edit_note(&todo_md_path),
}
}
+186 -31
View File
@@ -177,7 +177,7 @@ impl TaskRef {
}
}
/// Parse a markdown todo file
/// Parse a markdown todo file with resilient handling of user edits
pub fn parse_todo_file(content: &str) -> Result<TodoFile, String> {
let lines: Vec<&str> = content.lines().collect();
if lines.is_empty() {
@@ -188,16 +188,23 @@ pub fn parse_todo_file(content: &str) -> Result<TodoFile, String> {
let mut current_list: Option<TaskList> = None;
let mut current_task: Option<Task> = None;
for (line_num, line) in lines.iter().enumerate() {
for line in lines.iter() {
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
if let Some(title) = line.strip_prefix("# ") {
todo_file.title = title.to_string();
} else if let Some(list_name) = line.strip_prefix("## ") {
// Handle title - be flexible with spacing
if let Some(title) = trimmed.strip_prefix("# ") {
todo_file.title = title.trim().to_string();
} else if trimmed.starts_with("#") && !trimmed.starts_with("##") {
// Handle cases where user might not have space after # (but not ##)
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);
@@ -206,36 +213,127 @@ pub fn parse_todo_file(content: &str) -> Result<TodoFile, String> {
todo_file.add_list(list);
}
current_list = Some(TaskList::new(list_name.to_string()));
} else if let Some(subtask_text) = line.strip_prefix(" - ") {
if let Some(ref mut task) = current_task {
task.add_subtask(subtask_text.to_string());
} else {
return Err(format!(
"Subtask found without parent task at line {}: {}",
line_num + 1,
line
));
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);
}
} else if let Some(task_text) = line.strip_prefix("- ") {
current_list = Some(TaskList::new(list_name.trim().to_string()));
}
// Handle subtasks - be flexible with indentation (2, 3, or 4 spaces, or tab)
else if let Some(subtask_text) = line.strip_prefix(" - ") {
if let Some(ref mut task) = current_task {
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()));
}
}
} 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()));
}
}
} 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()));
}
}
} 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()));
}
}
}
// 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);
}
current_task = Some(Task::new(task_text.to_string()));
} else if line.starts_with(" - ") {
return Err(format!(
"Deep nesting not allowed. Only single-level subtasks are supported at line {}: {}",
line_num + 1,
line
));
} else if !trimmed.is_empty() {
return Err(format!(
"Invalid markdown format at line {}: {}",
line_num + 1,
line
));
// Ensure we have a list
if current_list.is_none() {
current_list = Some(TaskList::new("Default".to_string()));
}
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()));
}
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()));
}
current_task = Some(Task::new(task_text.trim().to_string()));
}
// Check for overly deep nesting (5+ spaces or multiple tabs)
else if line.starts_with(" - ") || line.starts_with("\t\t") {
// Skip deep nesting but don't error - just ignore these lines
continue;
}
// Handle non-markdown lines gracefully - ignore unknown formatting
else if !trimmed.is_empty() {
// 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()));
}
current_task = Some(Task::new(trimmed.to_string()));
}
// Otherwise just skip unknown lines (comments, HTML, code blocks, etc.)
}
}
@@ -531,7 +629,7 @@ mod tests {
}
#[test]
fn test_reject_deep_nesting() {
fn test_handle_deep_nesting_gracefully() {
let content = r#"# TODOs
## Default
@@ -540,6 +638,63 @@ mod tests {
- Deep subtask
"#;
assert!(parse_todo_file(content).is_err());
// 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");
}
#[test]
fn test_resilient_parsing_various_formats() {
let content = r#"# TODOs
##Default
- Task with dash
* Task with asterisk
+ Task with plus
- Subtask with 2 spaces
- Subtask with 3 spaces
- Subtask with 4 spaces
- Subtask with tab
## Work
Plain text task without bullet
- Normal task
<!-- This is a comment -->
```
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)
// 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);
// 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);
// 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);
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
}
}