From 930c6b9c386a0913e3b01bd25bda33a7ba4c6921 Mon Sep 17 00:00:00 2001 From: xeruf <27jf@pm.me> Date: Thu, 15 Aug 2024 10:16:40 +0300 Subject: [PATCH] fix: improve task filtering, especially with slash - smart case - substring match - less movement needed --- README.md | 8 +++--- src/main.rs | 29 +++++++++++++------- src/tasks.rs | 76 ++++++++++++++++++++++++++-------------------------- 3 files changed, 61 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index 0b58640..c5699c5 100644 --- a/README.md +++ b/README.md @@ -93,17 +93,17 @@ To stop time-tracking completely, simply move to the root of all tasks. `TASK` creation syntax: `NAME: TAG1 TAG2 ...` - `TASK` - create task (prefix with space if you want a task to start with a command character) -- `.` - clear filters and reload +- `.` - clear filters - `.TASK` + activate task by id + match by task name prefix: if one or more tasks match, filter / activate (tries case-sensitive then case-insensitive) + no match: create & activate task -- `.2` - set view depth to `2`, which can be substituted for any number (how many subtask levels to show, default 1) -- `/[TEXT]` - like `.`, but never creates a task and filters beyond currently visible tasks +- `.2` - set view depth to the given number (how many subtask levels to show, default is 1) +- `/[TEXT]` - activate task or filter by smart-case substring match - `||TASK` - create and activate a new task procedure (where subtasks automatically depend on the previously created task) - `|[TASK]` - (un)mark current task as procedure or create a sibling task depending on the current one and move up -Dots and slashes can be repeated to move to parent tasks. +Dot or slash can be repeated to move to parent tasks before acting. - `:[IND][PROP]` - add property column PROP at IND or end, if it already exists remove property column PROP or IND (1-indexed) - `::[PROP]` - Sort by property PROP (multiple space-separated values allowed) diff --git a/src/main.rs b/src/main.rs index 74d0d4c..03a6f96 100644 --- a/src/main.rs +++ b/src/main.rs @@ -407,7 +407,7 @@ async fn main() { None => match tasks.get_position() { None => { tasks.set_filter( - tasks.current_tasks().into_iter() + tasks.filtered_tasks(None) .filter(|t| t.pure_state() == State::Procedure) .map(|t| t.event.id) .collect() @@ -499,8 +499,8 @@ async fn main() { dots += 1; pos = tasks.get_parent(pos).cloned(); } - let slice = input[dots..].trim(); + let slice = input[dots..].trim(); if pos != tasks.get_position() || slice.is_empty() { tasks.move_to(pos); } @@ -522,21 +522,30 @@ async fn main() { dots += 1; pos = tasks.get_parent(pos).cloned(); } - let slice = &input[dots..].trim().to_ascii_lowercase(); + let slice = input[dots..].trim(); if slice.is_empty() { tasks.move_to(pos); + if dots > 1 { + info!("Moving up {} tasks", dots - 1) + } } else if let Ok(depth) = slice.parse::() { - tasks.move_to(pos); tasks.set_depth(depth); } else { - let filtered = tasks - .children_of(pos) - .into_iter() - .filter_map(|child| tasks.get_by_id(&child)) - .filter(|t| t.event.content.to_ascii_lowercase().starts_with(slice)) + let mut transform: Box String> = Box::new(|s: &str| s.to_string()); + if slice.chars().find(|c| c.is_ascii_uppercase()).is_none() { + // Smart-case - case-sensitive if any uppercase char is entered + transform = Box::new(|s| s.to_ascii_lowercase()); + } + + let filtered = tasks.filtered_tasks(pos) + .filter(|t| { + transform(&t.event.content).contains(slice) || t.tags.iter().flatten().any(|tag| + tag.content().is_some_and(|s| transform(s).contains(slice)) + ) + }) .map(|t| t.event.id) - .collect::>(); + .collect_vec(); if filtered.len() == 1 { tasks.move_to(filtered.into_iter().nth(0)); } else { diff --git a/src/tasks.rs b/src/tasks.rs index fab48cd..1522c0d 100644 --- a/src/tasks.rs +++ b/src/tasks.rs @@ -354,17 +354,9 @@ impl Tasks { .map(|t| t.get_id()) } - pub(crate) fn current_tasks(&self) -> Vec<&Task> { - if self.depth == 0 { - return self.get_current_task().into_iter().collect(); - } - let res: Vec<&Task> = self.resolve_tasks(self.view.iter()); - if res.len() > 0 { - // Currently ignores filtered view when it matches nothing - return res; - } + pub(crate) fn filtered_tasks(&self, position: Option) -> impl Iterator { // TODO use ChildrenIterator - self.resolve_tasks(self.children_of(self.position)).into_iter() + self.resolve_tasks(self.children_of(position)).into_iter() .filter(|t| { // TODO apply filters in transit self.state.matches(t) && @@ -377,7 +369,16 @@ impl Tasks { self.tags.iter().all(|tag| iter.any(|t| t == tag)) })) }) - .collect() + } + + pub(crate) fn visible_tasks(&self) -> Vec<&Task> { + if self.depth == 0 { + return self.get_current_task().into_iter().collect(); + } + if self.view.len() > 0 { + return self.resolve_tasks(self.view.iter()); + } + self.filtered_tasks(self.position).collect() } pub(crate) fn print_tasks(&self) -> Result<(), Error> { @@ -408,7 +409,7 @@ impl Tasks { // TODO hide empty columns writeln!(lock, "{}", self.properties.join("\t").bold())?; let mut total_time = 0; - let mut tasks = self.current_tasks(); + let mut tasks = self.visible_tasks(); let count = tasks.len(); tasks.sort_by_cached_key(|task| { self.sorting @@ -534,11 +535,10 @@ impl Tasks { if let Ok(id) = EventId::parse(arg) { return vec![id]; } - let tasks = self.current_tasks(); - let mut filtered: Vec = Vec::with_capacity(tasks.len()); + let mut filtered: Vec = Vec::with_capacity(32); let lowercase_arg = arg.to_ascii_lowercase(); - let mut filtered_more: Vec = Vec::with_capacity(tasks.len()); - for task in tasks { + let mut filtered_more: Vec = Vec::with_capacity(32); + for task in self.filtered_tasks(self.position) { let lowercase = task.event.content.to_ascii_lowercase(); if lowercase == lowercase_arg { return vec![task.event.id]; @@ -1112,59 +1112,59 @@ mod tasks_test { assert_eq!(tasks.depth, 1); assert_eq!(task1.pure_state(), State::Open); debug!("{:?}", tasks); - assert_eq!(tasks.current_tasks().len(), 1); + assert_eq!(tasks.visible_tasks().len(), 1); tasks.depth = 0; - assert_eq!(tasks.current_tasks().len(), 0); + assert_eq!(tasks.visible_tasks().len(), 0); tasks.move_to(Some(t1)); tasks.depth = 2; - assert_eq!(tasks.current_tasks().len(), 0); + assert_eq!(tasks.visible_tasks().len(), 0); let t2 = tasks.make_task("t2"); - assert_eq!(tasks.current_tasks().len(), 1); + assert_eq!(tasks.visible_tasks().len(), 1); assert_eq!(tasks.get_task_path(Some(t2)), "t1>t2"); assert_eq!(tasks.relative_path(t2), "t2"); let t3 = tasks.make_task("t3"); - assert_eq!(tasks.current_tasks().len(), 2); + assert_eq!(tasks.visible_tasks().len(), 2); tasks.move_to(Some(t2)); - assert_eq!(tasks.current_tasks().len(), 0); + assert_eq!(tasks.visible_tasks().len(), 0); let t4 = tasks.make_task("t4"); - assert_eq!(tasks.current_tasks().len(), 1); + assert_eq!(tasks.visible_tasks().len(), 1); assert_eq!(tasks.get_task_path(Some(t4)), "t1>t2>t4"); assert_eq!(tasks.relative_path(t4), "t4"); tasks.depth = 2; - assert_eq!(tasks.current_tasks().len(), 1); + assert_eq!(tasks.visible_tasks().len(), 1); tasks.depth = -1; - assert_eq!(tasks.current_tasks().len(), 1); + assert_eq!(tasks.visible_tasks().len(), 1); tasks.move_to(Some(t1)); assert_eq!(tasks.relative_path(t4), "t2>t4"); - assert_eq!(tasks.current_tasks().len(), 2); + assert_eq!(tasks.visible_tasks().len(), 2); tasks.depth = 2; - assert_eq!(tasks.current_tasks().len(), 3); + assert_eq!(tasks.visible_tasks().len(), 3); tasks.set_filter(vec![t2]); - assert_eq!(tasks.current_tasks().len(), 2); + assert_eq!(tasks.visible_tasks().len(), 2); tasks.depth = 1; - assert_eq!(tasks.current_tasks().len(), 1); + assert_eq!(tasks.visible_tasks().len(), 1); tasks.depth = -1; - assert_eq!(tasks.current_tasks().len(), 1); + assert_eq!(tasks.visible_tasks().len(), 1); tasks.set_filter(vec![t2, t3]); - assert_eq!(tasks.current_tasks().len(), 2); + assert_eq!(tasks.visible_tasks().len(), 2); tasks.depth = 2; - assert_eq!(tasks.current_tasks().len(), 3); + assert_eq!(tasks.visible_tasks().len(), 3); tasks.depth = 1; - assert_eq!(tasks.current_tasks().len(), 2); + assert_eq!(tasks.visible_tasks().len(), 2); tasks.move_to(None); - assert_eq!(tasks.current_tasks().len(), 1); + assert_eq!(tasks.visible_tasks().len(), 1); tasks.depth = 2; - assert_eq!(tasks.current_tasks().len(), 3); + assert_eq!(tasks.visible_tasks().len(), 3); tasks.depth = 3; - assert_eq!(tasks.current_tasks().len(), 4); + assert_eq!(tasks.visible_tasks().len(), 4); tasks.depth = 9; - assert_eq!(tasks.current_tasks().len(), 4); + assert_eq!(tasks.visible_tasks().len(), 4); tasks.depth = -1; - assert_eq!(tasks.current_tasks().len(), 2); + assert_eq!(tasks.visible_tasks().len(), 2); } #[test]