forked from janek/mostr
fix: improve task filtering, especially with slash
- smart case - substring match - less movement needed
This commit is contained in:
parent
c93b2f2d91
commit
930c6b9c38
|
@ -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)
|
||||||
|
|
29
src/main.rs
29
src/main.rs
|
@ -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 {
|
||||||
|
|
76
src/tasks.rs
76
src/tasks.rs
|
@ -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]
|
||||||
|
|
Loading…
Reference in New Issue