fix: improve task filtering, especially with slash

- smart case
- substring match
- less movement needed
This commit is contained in:
xeruf 2024-08-15 10:16:40 +03:00
parent c93b2f2d91
commit 930c6b9c38
3 changed files with 61 additions and 52 deletions

View File

@ -93,17 +93,17 @@ To stop time-tracking completely, simply move to the root of all tasks.
`TASK` creation syntax: `NAME: TAG1 TAG2 ...` `TASK` creation syntax: `NAME: TAG1 TAG2 ...`
- `TASK` - create task (prefix with space if you want a task to start with a command character) - `TASK` - create task (prefix with space if you want a task to start with a command character)
- `.` - clear filters and reload - `.` - clear filters
- `.TASK` - `.TASK`
+ activate task by id + activate task by id
+ match by task name prefix: if one or more tasks match, filter / activate (tries case-sensitive then case-insensitive) + match by task name prefix: if one or more tasks match, filter / activate (tries case-sensitive then case-insensitive)
+ no match: create & activate task + 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) - `.2` - set view depth to the given number (how many subtask levels to show, default is 1)
- `/[TEXT]` - like `.`, but never creates a task and filters beyond currently visible tasks - `/[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` - 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 - `|[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) - `:[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) - `::[PROP]` - Sort by property PROP (multiple space-separated values allowed)

View File

@ -407,7 +407,7 @@ async fn main() {
None => match tasks.get_position() { None => match tasks.get_position() {
None => { None => {
tasks.set_filter( tasks.set_filter(
tasks.current_tasks().into_iter() tasks.filtered_tasks(None)
.filter(|t| t.pure_state() == State::Procedure) .filter(|t| t.pure_state() == State::Procedure)
.map(|t| t.event.id) .map(|t| t.event.id)
.collect() .collect()
@ -499,8 +499,8 @@ async fn main() {
dots += 1; dots += 1;
pos = tasks.get_parent(pos).cloned(); pos = tasks.get_parent(pos).cloned();
} }
let slice = input[dots..].trim();
let slice = input[dots..].trim();
if pos != tasks.get_position() || slice.is_empty() { if pos != tasks.get_position() || slice.is_empty() {
tasks.move_to(pos); tasks.move_to(pos);
} }
@ -522,21 +522,30 @@ async fn main() {
dots += 1; dots += 1;
pos = tasks.get_parent(pos).cloned(); pos = tasks.get_parent(pos).cloned();
} }
let slice = &input[dots..].trim().to_ascii_lowercase();
let slice = input[dots..].trim();
if slice.is_empty() { if slice.is_empty() {
tasks.move_to(pos); tasks.move_to(pos);
if dots > 1 {
info!("Moving up {} tasks", dots - 1)
}
} else if let Ok(depth) = slice.parse::<i8>() { } else if let Ok(depth) = slice.parse::<i8>() {
tasks.move_to(pos);
tasks.set_depth(depth); tasks.set_depth(depth);
} else { } else {
let filtered = tasks let mut transform: Box<dyn Fn(&str) -> String> = Box::new(|s: &str| s.to_string());
.children_of(pos) if slice.chars().find(|c| c.is_ascii_uppercase()).is_none() {
.into_iter() // Smart-case - case-sensitive if any uppercase char is entered
.filter_map(|child| tasks.get_by_id(&child)) transform = Box::new(|s| s.to_ascii_lowercase());
.filter(|t| t.event.content.to_ascii_lowercase().starts_with(slice)) }
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) .map(|t| t.event.id)
.collect::<Vec<_>>(); .collect_vec();
if filtered.len() == 1 { if filtered.len() == 1 {
tasks.move_to(filtered.into_iter().nth(0)); tasks.move_to(filtered.into_iter().nth(0));
} else { } else {

View File

@ -354,17 +354,9 @@ impl Tasks {
.map(|t| t.get_id()) .map(|t| t.get_id())
} }
pub(crate) fn current_tasks(&self) -> Vec<&Task> { pub(crate) fn filtered_tasks(&self, position: Option<EventId>) -> impl Iterator<Item=&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;
}
// TODO use ChildrenIterator // TODO use ChildrenIterator
self.resolve_tasks(self.children_of(self.position)).into_iter() self.resolve_tasks(self.children_of(position)).into_iter()
.filter(|t| { .filter(|t| {
// TODO apply filters in transit // TODO apply filters in transit
self.state.matches(t) && self.state.matches(t) &&
@ -377,7 +369,16 @@ impl Tasks {
self.tags.iter().all(|tag| iter.any(|t| t == tag)) 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> { pub(crate) fn print_tasks(&self) -> Result<(), Error> {
@ -408,7 +409,7 @@ impl Tasks {
// TODO hide empty columns // TODO hide empty columns
writeln!(lock, "{}", self.properties.join("\t").bold())?; writeln!(lock, "{}", self.properties.join("\t").bold())?;
let mut total_time = 0; let mut total_time = 0;
let mut tasks = self.current_tasks(); let mut tasks = self.visible_tasks();
let count = tasks.len(); let count = tasks.len();
tasks.sort_by_cached_key(|task| { tasks.sort_by_cached_key(|task| {
self.sorting self.sorting
@ -534,11 +535,10 @@ impl Tasks {
if let Ok(id) = EventId::parse(arg) { if let Ok(id) = EventId::parse(arg) {
return vec![id]; return vec![id];
} }
let tasks = self.current_tasks(); let mut filtered: Vec<EventId> = Vec::with_capacity(32);
let mut filtered: Vec<EventId> = Vec::with_capacity(tasks.len());
let lowercase_arg = arg.to_ascii_lowercase(); let lowercase_arg = arg.to_ascii_lowercase();
let mut filtered_more: Vec<EventId> = Vec::with_capacity(tasks.len()); let mut filtered_more: Vec<EventId> = Vec::with_capacity(32);
for task in tasks { for task in self.filtered_tasks(self.position) {
let lowercase = task.event.content.to_ascii_lowercase(); let lowercase = task.event.content.to_ascii_lowercase();
if lowercase == lowercase_arg { if lowercase == lowercase_arg {
return vec![task.event.id]; return vec![task.event.id];
@ -1112,59 +1112,59 @@ mod tasks_test {
assert_eq!(tasks.depth, 1); assert_eq!(tasks.depth, 1);
assert_eq!(task1.pure_state(), State::Open); assert_eq!(task1.pure_state(), State::Open);
debug!("{:?}", tasks); debug!("{:?}", tasks);
assert_eq!(tasks.current_tasks().len(), 1); assert_eq!(tasks.visible_tasks().len(), 1);
tasks.depth = 0; tasks.depth = 0;
assert_eq!(tasks.current_tasks().len(), 0); assert_eq!(tasks.visible_tasks().len(), 0);
tasks.move_to(Some(t1)); tasks.move_to(Some(t1));
tasks.depth = 2; tasks.depth = 2;
assert_eq!(tasks.current_tasks().len(), 0); assert_eq!(tasks.visible_tasks().len(), 0);
let t2 = tasks.make_task("t2"); 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.get_task_path(Some(t2)), "t1>t2");
assert_eq!(tasks.relative_path(t2), "t2"); assert_eq!(tasks.relative_path(t2), "t2");
let t3 = tasks.make_task("t3"); 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)); 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"); 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.get_task_path(Some(t4)), "t1>t2>t4");
assert_eq!(tasks.relative_path(t4), "t4"); assert_eq!(tasks.relative_path(t4), "t4");
tasks.depth = 2; tasks.depth = 2;
assert_eq!(tasks.current_tasks().len(), 1); assert_eq!(tasks.visible_tasks().len(), 1);
tasks.depth = -1; tasks.depth = -1;
assert_eq!(tasks.current_tasks().len(), 1); assert_eq!(tasks.visible_tasks().len(), 1);
tasks.move_to(Some(t1)); tasks.move_to(Some(t1));
assert_eq!(tasks.relative_path(t4), "t2>t4"); 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; tasks.depth = 2;
assert_eq!(tasks.current_tasks().len(), 3); assert_eq!(tasks.visible_tasks().len(), 3);
tasks.set_filter(vec![t2]); tasks.set_filter(vec![t2]);
assert_eq!(tasks.current_tasks().len(), 2); assert_eq!(tasks.visible_tasks().len(), 2);
tasks.depth = 1; tasks.depth = 1;
assert_eq!(tasks.current_tasks().len(), 1); assert_eq!(tasks.visible_tasks().len(), 1);
tasks.depth = -1; tasks.depth = -1;
assert_eq!(tasks.current_tasks().len(), 1); assert_eq!(tasks.visible_tasks().len(), 1);
tasks.set_filter(vec![t2, t3]); tasks.set_filter(vec![t2, t3]);
assert_eq!(tasks.current_tasks().len(), 2); assert_eq!(tasks.visible_tasks().len(), 2);
tasks.depth = 2; tasks.depth = 2;
assert_eq!(tasks.current_tasks().len(), 3); assert_eq!(tasks.visible_tasks().len(), 3);
tasks.depth = 1; tasks.depth = 1;
assert_eq!(tasks.current_tasks().len(), 2); assert_eq!(tasks.visible_tasks().len(), 2);
tasks.move_to(None); tasks.move_to(None);
assert_eq!(tasks.current_tasks().len(), 1); assert_eq!(tasks.visible_tasks().len(), 1);
tasks.depth = 2; tasks.depth = 2;
assert_eq!(tasks.current_tasks().len(), 3); assert_eq!(tasks.visible_tasks().len(), 3);
tasks.depth = 3; tasks.depth = 3;
assert_eq!(tasks.current_tasks().len(), 4); assert_eq!(tasks.visible_tasks().len(), 4);
tasks.depth = 9; tasks.depth = 9;
assert_eq!(tasks.current_tasks().len(), 4); assert_eq!(tasks.visible_tasks().len(), 4);
tasks.depth = -1; tasks.depth = -1;
assert_eq!(tasks.current_tasks().len(), 2); assert_eq!(tasks.visible_tasks().len(), 2);
} }
#[test] #[test]