remove scan subcommand, rely solely on edit now
This commit is contained in:
3 files changed
+187
-81
No files matched your search
+1
-46
@@ -1,14 +1,13 @@
|
|||||||
//! Command implementations for the ldr todo system with Markdown support.
|
//! Command implementations for the ldr todo system with Markdown support.
|
||||||
//!
|
//!
|
||||||
//! This module contains all the core functionality for managing todo items,
|
//! 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.
|
//! Now supports subtasks and multiple lists in Markdown format.
|
||||||
|
|
||||||
use crate::markdown::{
|
use crate::markdown::{
|
||||||
parse_todo_file, generate_todo_file, parse_archive_file, generate_archive_file,
|
parse_todo_file, generate_todo_file, parse_archive_file, generate_archive_file,
|
||||||
Task, TaskList, TodoFile, ArchiveFile, TaskRef
|
Task, TaskList, TodoFile, ArchiveFile, TaskRef
|
||||||
};
|
};
|
||||||
// use crate::input::read_key_input; // TODO: Implement full interactive review
|
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
@@ -559,50 +558,6 @@ pub fn remove_items(todo_path: &Path, refs: &[String]) -> io::Result<()> {
|
|||||||
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
|
/// Opens the todo file in the user's preferred editor
|
||||||
pub fn edit_note(todo_path: &Path) -> io::Result<()> {
|
pub fn edit_note(todo_path: &Path) -> io::Result<()> {
|
||||||
|
|||||||
@@ -70,9 +70,6 @@ enum Commands {
|
|||||||
/// Item references to remove (e.g., "1", "2a", "3b")
|
/// Item references to remove (e.g., "1", "2a", "3b")
|
||||||
refs: Vec<String>,
|
refs: Vec<String>,
|
||||||
},
|
},
|
||||||
/// Scan and review items interactively, from top to bottom
|
|
||||||
#[command(aliases = ["s", "r", "review"])]
|
|
||||||
Scan,
|
|
||||||
/// Edit items in your $EDITOR
|
/// Edit items in your $EDITOR
|
||||||
#[command(alias = "e")]
|
#[command(alias = "e")]
|
||||||
Edit,
|
Edit,
|
||||||
@@ -116,7 +113,6 @@ fn main() -> io::Result<()> {
|
|||||||
Commands::Up { refs } => commands::prioritize_items(&todo_md_path, &refs),
|
Commands::Up { refs } => commands::prioritize_items(&todo_md_path, &refs),
|
||||||
Commands::Do { refs } => commands::archive_items(&todo_md_path, &archive_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::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),
|
Commands::Edit => commands::edit_note(&todo_md_path),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+186
-31
@@ -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> {
|
pub fn parse_todo_file(content: &str) -> Result<TodoFile, String> {
|
||||||
let lines: Vec<&str> = content.lines().collect();
|
let lines: Vec<&str> = content.lines().collect();
|
||||||
if lines.is_empty() {
|
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_list: Option<TaskList> = None;
|
||||||
let mut current_task: Option<Task> = 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();
|
let trimmed = line.trim();
|
||||||
|
|
||||||
if trimmed.is_empty() {
|
if trimmed.is_empty() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(title) = line.strip_prefix("# ") {
|
// Handle title - be flexible with spacing
|
||||||
todo_file.title = title.to_string();
|
if let Some(title) = trimmed.strip_prefix("# ") {
|
||||||
} else if let Some(list_name) = line.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
|
// Save previous task if exists
|
||||||
if let (Some(mut list), Some(task)) = (current_list.take(), current_task.take()) {
|
if let (Some(mut list), Some(task)) = (current_list.take(), current_task.take()) {
|
||||||
list.add_task(task);
|
list.add_task(task);
|
||||||
@@ -206,36 +213,127 @@ pub fn parse_todo_file(content: &str) -> Result<TodoFile, String> {
|
|||||||
todo_file.add_list(list);
|
todo_file.add_list(list);
|
||||||
}
|
}
|
||||||
|
|
||||||
current_list = Some(TaskList::new(list_name.to_string()));
|
current_list = Some(TaskList::new(list_name.trim().to_string()));
|
||||||
} else if let Some(subtask_text) = line.strip_prefix(" - ") {
|
} else if trimmed.starts_with("##") && !trimmed.starts_with("###") {
|
||||||
if let Some(ref mut task) = current_task {
|
// Handle cases where user might not have space after ## (but not ###)
|
||||||
task.add_subtask(subtask_text.to_string());
|
let list_name = &trimmed[2..];
|
||||||
} else {
|
if let (Some(mut list), Some(task)) = (current_list.take(), current_task.take()) {
|
||||||
return Err(format!(
|
list.add_task(task);
|
||||||
"Subtask found without parent task at line {}: {}",
|
todo_file.add_list(list);
|
||||||
line_num + 1,
|
} else if let Some(list) = current_list.take() {
|
||||||
line
|
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
|
// Save previous task if exists
|
||||||
if let (Some(list), Some(task)) = (current_list.as_mut(), current_task.take()) {
|
if let (Some(list), Some(task)) = (current_list.as_mut(), current_task.take()) {
|
||||||
list.add_task(task);
|
list.add_task(task);
|
||||||
}
|
}
|
||||||
|
|
||||||
current_task = Some(Task::new(task_text.to_string()));
|
// Ensure we have a list
|
||||||
} else if line.starts_with(" - ") {
|
if current_list.is_none() {
|
||||||
return Err(format!(
|
current_list = Some(TaskList::new("Default".to_string()));
|
||||||
"Deep nesting not allowed. Only single-level subtasks are supported at line {}: {}",
|
}
|
||||||
line_num + 1,
|
|
||||||
line
|
current_task = Some(Task::new(task_text.trim().to_string()));
|
||||||
));
|
} else if let Some(task_text) = trimmed.strip_prefix("* ") {
|
||||||
} else if !trimmed.is_empty() {
|
// Handle asterisk bullet points
|
||||||
return Err(format!(
|
if let (Some(list), Some(task)) = (current_list.as_mut(), current_task.take()) {
|
||||||
"Invalid markdown format at line {}: {}",
|
list.add_task(task);
|
||||||
line_num + 1,
|
}
|
||||||
line
|
|
||||||
));
|
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]
|
#[test]
|
||||||
fn test_reject_deep_nesting() {
|
fn test_handle_deep_nesting_gracefully() {
|
||||||
let content = r#"# TODOs
|
let content = r#"# TODOs
|
||||||
|
|
||||||
## Default
|
## Default
|
||||||
@@ -540,6 +638,63 @@ mod tests {
|
|||||||
- Deep subtask
|
- 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user