lib: Add text-based patch item filter

This commit is contained in:
Erik Kundt
2024-03-20 09:51:05 +01:00
parent 0f86553566
commit e20dd69949
3 changed files with 182 additions and 7 deletions
Generated
+37
View File
@@ -695,6 +695,15 @@ version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
[[package]]
name = "fuzzy-matcher"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94"
dependencies = [
"thread_local",
]
[[package]]
name = "generic-array"
version = "0.14.7"
@@ -1087,6 +1096,12 @@ version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
[[package]]
name = "minimal-lexical"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "miniz_oxide"
version = "0.7.1"
@@ -1127,6 +1142,16 @@ dependencies = [
"unicode-segmentation",
]
[[package]]
name = "nom"
version = "7.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
dependencies = [
"memchr",
"minimal-lexical",
]
[[package]]
name = "nonempty"
version = "0.5.0"
@@ -1615,10 +1640,12 @@ name = "radicle-tui"
version = "0.3.0"
dependencies = [
"anyhow",
"fuzzy-matcher",
"inquire",
"lexopt",
"libc",
"log",
"nom",
"radicle",
"radicle-surf",
"radicle-term",
@@ -2282,6 +2309,16 @@ dependencies = [
"winapi",
]
[[package]]
name = "thread_local"
version = "1.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c"
dependencies = [
"cfg-if",
"once_cell",
]
[[package]]
name = "time"
version = "0.1.45"
+2
View File
@@ -19,8 +19,10 @@ flux = ["dep:tokio", "dep:tokio-stream"]
anyhow = { version = "1" }
inquire = { version = "0.6.2", default-features = false, features = ["termion", "editor"] }
lexopt = { version = "0.3.0" }
fuzzy-matcher = "*"
libc = { version = "^0.2" }
log = { version = "0.4.19" }
nom = { version = "^7.1.0" }
radicle = { git = "https://seed.radicle.xyz/z3gqcJUoA1n9HaHKufZs5FCSGazv5" }
radicle-term = { git = "https://seed.radicle.xyz/z3gqcJUoA1n9HaHKufZs5FCSGazv5", package = "radicle-term" }
radicle-surf = { version = "0.18.0" }
+143 -7
View File
@@ -1,18 +1,26 @@
use std::str::FromStr;
use nom::bytes::complete::{tag, take};
use nom::multi::separated_list0;
use nom::sequence::{delimited, preceded};
use nom::{IResult, Parser};
use radicle::cob::{Label, ObjectId, Timestamp, TypedId};
use radicle::crypto::PublicKey;
use radicle::git::Oid;
use radicle::identity::{Did, Identity};
use radicle::node::{Alias, NodeId};
use radicle::{issue, patch, Profile};
use ratatui::style::{Color, Style, Stylize};
use ratatui::widgets::Cell;
use radicle::cob::{Label, ObjectId, Timestamp, TypedId};
use radicle::issue;
use radicle::issue::{Issue, IssueId, Issues};
use radicle::node::notifications::{Notification, NotificationId, NotificationKind};
use radicle::node::AliasStore;
use radicle::node::{Alias, AliasStore, NodeId};
use radicle::patch;
use radicle::patch::{Patch, PatchId, Patches};
use radicle::storage::git::Repository;
use radicle::storage::{ReadRepository, ReadStorage, RefUpdate, WriteRepository};
use radicle::Profile;
use ratatui::style::{Color, Style, Stylize};
use ratatui::widgets::Cell;
use super::super::git;
use super::theme::style;
@@ -490,6 +498,107 @@ impl ToRow<9> for PatchItem {
}
}
#[derive(Clone, Default, Debug, Eq, PartialEq)]
pub struct PatchItemFilter {
status: Option<patch::Status>,
authored: bool,
authors: Vec<Did>,
search: Option<String>,
}
impl PatchItemFilter {
pub fn status(&self) -> Option<patch::Status> {
self.status
}
pub fn matches(&self, patch: &PatchItem) -> bool {
use fuzzy_matcher::skim::SkimMatcherV2;
use fuzzy_matcher::FuzzyMatcher;
let matcher = SkimMatcherV2::default();
let matches_state = match self.status {
Some(patch::Status::Draft) => matches!(patch.state, patch::State::Draft),
Some(patch::Status::Open) => matches!(patch.state, patch::State::Open { .. }),
Some(patch::Status::Merged) => matches!(patch.state, patch::State::Merged { .. }),
Some(patch::Status::Archived) => matches!(patch.state, patch::State::Archived),
None => true,
};
let matches_authored = if self.authored {
patch.author.you
} else {
true
};
let matches_authors = (!self.authors.is_empty())
.then(|| {
self.authors
.iter()
.any(|other| patch.author.nid.unwrap() == **other)
})
.unwrap_or(true);
let matches_search = match &self.search {
Some(search) => match matcher.fuzzy_match(&patch.title, search) {
Some(score) => score == 0 || score > 60,
_ => false,
},
None => true,
};
matches_state && matches_authored && matches_authors && matches_search
}
}
impl FromStr for PatchItemFilter {
type Err = anyhow::Error;
fn from_str(value: &str) -> Result<Self, Self::Err> {
let mut status = None;
let mut search = String::new();
let mut authored = false;
let mut authors = vec![];
let mut authors_parser = |input| -> IResult<&str, Vec<&str>> {
preceded(
tag("authors:"),
delimited(
tag("["),
separated_list0(tag(","), take(56_usize)),
tag("]"),
),
)(input)
};
let parts = value.split(' ');
for part in parts {
match part {
"is:open" => status = Some(patch::Status::Open),
"is:merged" => status = Some(patch::Status::Merged),
"is:archived" => status = Some(patch::Status::Archived),
"is:draft" => status = Some(patch::Status::Draft),
"is:authored" => authored = true,
other => match authors_parser.parse(other) {
Ok((_, dids)) => {
for did in dids {
authors.push(Did::from_str(did)?);
}
}
_ => search.push_str(other),
},
}
}
Ok(Self {
status,
authored,
authors,
search: Some(search),
})
}
}
pub fn format_issue_state(state: &issue::State) -> (String, Color) {
match state {
issue::State::Open => (" ● ".into(), Color::Green),
@@ -551,3 +660,30 @@ pub fn format_assignees(assignees: &[(Option<PublicKey>, Option<Alias>, bool)])
}
output
}
#[cfg(test)]
mod tests {
use anyhow::Result;
use super::*;
#[test]
fn patch_item_filter_from_str_should_succeed() -> Result<()> {
let search = r#"is:open is:authored authors:[did:key:z6MkkpTPzcq1ybmjQyQpyre15JUeMvZY6toxoZVpLZ8YarsB,did:key:z6Mku8hpprWTmCv3BqkssCYDfr2feUdyLSUnycVajFo9XVAx] cli"#;
let actual = PatchItemFilter::from_str(search)?;
let expected = PatchItemFilter {
status: Some(patch::Status::Open),
authored: true,
authors: vec![
Did::from_str("did:key:z6MkkpTPzcq1ybmjQyQpyre15JUeMvZY6toxoZVpLZ8YarsB")?,
Did::from_str("did:key:z6Mku8hpprWTmCv3BqkssCYDfr2feUdyLSUnycVajFo9XVAx")?,
],
search: Some("cli".to_string()),
};
assert_eq!(expected, actual);
Ok(())
}
}