mirror of
https://github.com/GothenburgBitFactory/taskwarrior.git
synced 2025-07-07 20:06:36 +02:00
support filtering by tags
This commit is contained in:
parent
161c38807d
commit
9c94a7b753
4 changed files with 140 additions and 13 deletions
|
@ -1,6 +1,6 @@
|
|||
use super::args::{arg_matching, id_list, TaskId};
|
||||
use super::args::{arg_matching, id_list, minus_tag, plus_tag, TaskId};
|
||||
use super::ArgList;
|
||||
use nom::{combinator::*, multi::fold_many0, IResult};
|
||||
use nom::{branch::alt, combinator::*, multi::fold_many0, IResult};
|
||||
|
||||
/// A filter represents a selection of a particular set of tasks.
|
||||
///
|
||||
|
@ -9,8 +9,12 @@ use nom::{combinator::*, multi::fold_many0, IResult};
|
|||
/// pending tasks, or all tasks.
|
||||
#[derive(Debug, PartialEq, Default, Clone)]
|
||||
pub(crate) struct Filter {
|
||||
/// A list of numeric IDs or prefixes of UUIDs
|
||||
/// The universe of tasks from which this filter can select
|
||||
pub(crate) universe: Universe,
|
||||
|
||||
/// A set of filter conditions, all of which must match a task in order for that task to be
|
||||
/// selected.
|
||||
pub(crate) conditions: Vec<Condition>,
|
||||
}
|
||||
|
||||
/// The universe of tasks over which a filter should be applied.
|
||||
|
@ -39,15 +43,26 @@ impl Default for Universe {
|
|||
}
|
||||
}
|
||||
|
||||
/// A condition which tasks must match to be accepted by the filter.
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub(crate) enum Condition {
|
||||
/// Task has the given tag
|
||||
HasTag(String),
|
||||
|
||||
/// Task does not have the given tag
|
||||
NoTag(String),
|
||||
}
|
||||
|
||||
/// Internal struct representing a parsed filter argument
|
||||
enum FilterArg {
|
||||
IdList(Vec<TaskId>),
|
||||
Condition(Condition),
|
||||
}
|
||||
|
||||
impl Filter {
|
||||
pub(super) fn parse(input: ArgList) -> IResult<ArgList, Filter> {
|
||||
fold_many0(
|
||||
Self::id_list,
|
||||
alt((Self::id_list, Self::plus_tag, Self::minus_tag)),
|
||||
Filter {
|
||||
..Default::default()
|
||||
},
|
||||
|
@ -68,6 +83,9 @@ impl Filter {
|
|||
acc.universe = Universe::IdList(id_list);
|
||||
}
|
||||
}
|
||||
FilterArg::Condition(cond) => {
|
||||
acc.conditions.push(cond);
|
||||
}
|
||||
}
|
||||
acc
|
||||
}
|
||||
|
@ -78,6 +96,20 @@ impl Filter {
|
|||
}
|
||||
map_res(arg_matching(id_list), to_filterarg)(input)
|
||||
}
|
||||
|
||||
fn plus_tag(input: ArgList) -> IResult<ArgList, FilterArg> {
|
||||
fn to_filterarg(input: &str) -> Result<FilterArg, ()> {
|
||||
Ok(FilterArg::Condition(Condition::HasTag(input.to_owned())))
|
||||
}
|
||||
map_res(arg_matching(plus_tag), to_filterarg)(input)
|
||||
}
|
||||
|
||||
fn minus_tag(input: ArgList) -> IResult<ArgList, FilterArg> {
|
||||
fn to_filterarg(input: &str) -> Result<FilterArg, ()> {
|
||||
Ok(FilterArg::Condition(Condition::NoTag(input.to_owned())))
|
||||
}
|
||||
map_res(arg_matching(minus_tag), to_filterarg)(input)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -159,4 +191,21 @@ mod test {
|
|||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tags() {
|
||||
let (input, filter) = Filter::parse(argv!["1", "+yes", "-no"]).unwrap();
|
||||
assert_eq!(input.len(), 0);
|
||||
assert_eq!(
|
||||
filter,
|
||||
Filter {
|
||||
universe: Universe::IdList(vec![TaskId::WorkingSetId(1),]),
|
||||
conditions: vec![
|
||||
Condition::HasTag("yes".into()),
|
||||
Condition::NoTag("no".into()),
|
||||
],
|
||||
..Default::default()
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ mod subcommand;
|
|||
|
||||
pub(crate) use args::TaskId;
|
||||
pub(crate) use command::Command;
|
||||
pub(crate) use filter::{Filter, Universe};
|
||||
pub(crate) use filter::{Condition, Filter, Universe};
|
||||
pub(crate) use modification::{DescriptionMod, Modification};
|
||||
pub(crate) use report::Report;
|
||||
pub(crate) use subcommand::Subcommand;
|
||||
|
|
|
@ -1,10 +1,28 @@
|
|||
use crate::argparse::{Filter, TaskId, Universe};
|
||||
use crate::argparse::{Condition, Filter, TaskId, Universe};
|
||||
use failure::Fallible;
|
||||
use std::collections::HashSet;
|
||||
use taskchampion::{Replica, Task};
|
||||
use std::convert::TryInto;
|
||||
use taskchampion::{Replica, Tag, Task};
|
||||
|
||||
fn match_task(_filter: &Filter, _task: &Task) -> bool {
|
||||
// TODO: at the moment, only filtering by Universe is supported
|
||||
fn match_task(filter: &Filter, task: &Task) -> bool {
|
||||
for cond in &filter.conditions {
|
||||
match cond {
|
||||
Condition::HasTag(ref tag) => {
|
||||
// see #111 for the unwrap
|
||||
let tag: Tag = tag.try_into().unwrap();
|
||||
if !task.has_tag(&tag) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Condition::NoTag(ref tag) => {
|
||||
// see #111 for the unwrap
|
||||
let tag: Tag = tag.try_into().unwrap();
|
||||
if task.has_tag(&tag) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
|
@ -186,6 +204,68 @@ mod test {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tag_filtering() -> Fallible<()> {
|
||||
let mut replica = test_replica();
|
||||
let yes: Tag = "yes".try_into()?;
|
||||
let no: Tag = "no".try_into()?;
|
||||
|
||||
let mut t1 = replica
|
||||
.new_task(Status::Pending, "A".to_owned())?
|
||||
.into_mut(&mut replica);
|
||||
t1.add_tag(&yes)?;
|
||||
let mut t2 = replica
|
||||
.new_task(Status::Pending, "B".to_owned())?
|
||||
.into_mut(&mut replica);
|
||||
t2.add_tag(&yes)?;
|
||||
t2.add_tag(&no)?;
|
||||
let mut t3 = replica
|
||||
.new_task(Status::Pending, "C".to_owned())?
|
||||
.into_mut(&mut replica);
|
||||
t3.add_tag(&no)?;
|
||||
let _t4 = replica.new_task(Status::Pending, "D".to_owned())?;
|
||||
|
||||
// look for just "yes" (A and B)
|
||||
let filter = Filter {
|
||||
universe: Universe::AllTasks,
|
||||
conditions: vec![Condition::HasTag("yes".to_owned())],
|
||||
..Default::default()
|
||||
};
|
||||
let mut filtered: Vec<_> = filtered_tasks(&mut replica, &filter)?
|
||||
.map(|t| t.get_description().to_owned())
|
||||
.collect();
|
||||
filtered.sort();
|
||||
assert_eq!(vec!["A".to_owned(), "B".to_owned()], filtered);
|
||||
|
||||
// look for tags without "no" (A, D)
|
||||
let filter = Filter {
|
||||
universe: Universe::AllTasks,
|
||||
conditions: vec![Condition::NoTag("no".to_owned())],
|
||||
..Default::default()
|
||||
};
|
||||
let mut filtered: Vec<_> = filtered_tasks(&mut replica, &filter)?
|
||||
.map(|t| t.get_description().to_owned())
|
||||
.collect();
|
||||
filtered.sort();
|
||||
assert_eq!(vec!["A".to_owned(), "D".to_owned()], filtered);
|
||||
|
||||
// look for tags with "yes" and "no" (B)
|
||||
let filter = Filter {
|
||||
universe: Universe::AllTasks,
|
||||
conditions: vec![
|
||||
Condition::HasTag("yes".to_owned()),
|
||||
Condition::HasTag("no".to_owned()),
|
||||
],
|
||||
..Default::default()
|
||||
};
|
||||
let filtered: Vec<_> = filtered_tasks(&mut replica, &filter)?
|
||||
.map(|t| t.get_description().to_owned())
|
||||
.collect();
|
||||
assert_eq!(vec!["B".to_owned()], filtered);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pending_tasks() {
|
||||
let mut replica = test_replica();
|
||||
|
|
|
@ -34,14 +34,12 @@ pub(super) fn apply_modification<W: WriteColor>(
|
|||
}
|
||||
|
||||
for tag in modification.add_tags.iter() {
|
||||
// note that the parser should have already ensured that this tag was valid
|
||||
let tag = tag.try_into()?;
|
||||
let tag = tag.try_into()?; // see #111
|
||||
task.add_tag(&tag)?;
|
||||
}
|
||||
|
||||
for tag in modification.remove_tags.iter() {
|
||||
// note that the parser should have already ensured that this tag was valid
|
||||
let tag = tag.try_into()?;
|
||||
let tag = tag.try_into()?; // see #111
|
||||
task.remove_tag(&tag)?;
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue