forked from janek/mostr
1
0
Fork 0

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` - 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)

View File

@ -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::<i8>() {
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<dyn Fn(&str) -> 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::<Vec<_>>();
.collect_vec();
if filtered.len() == 1 {
tasks.move_to(filtered.into_iter().nth(0));
} else {

View File

@ -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<EventId>) -> impl Iterator<Item=&Task> {
// 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<EventId> = Vec::with_capacity(tasks.len());
let mut filtered: Vec<EventId> = Vec::with_capacity(32);
let lowercase_arg = arg.to_ascii_lowercase();
let mut filtered_more: Vec<EventId> = Vec::with_capacity(tasks.len());
for task in tasks {
let mut filtered_more: Vec<EventId> = 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]