Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,7 @@ harness = false
[[bench]]
name = "omni_search"
harness = false
[[bench]]
name = "todo_widget_filtering"
harness = false

110 changes: 110 additions & 0 deletions benches/todo_widget_filtering.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use multi_launcher::plugins::todo::TodoEntry;

fn build_entries(count: usize) -> Vec<TodoEntry> {
(0..count)
.map(|i| TodoEntry {
text: format!("Todo item {i:05} with mixed CASE"),
done: i % 3 == 0,
priority: (i % 10) as u8,
tags: vec![
format!("team{}", i % 8),
format!("Feature{}", i % 16),
"Urgent".into(),
],
})
.collect()
}

fn old_tags_match(filter_tags: &[String], entry: &TodoEntry) -> bool {
if filter_tags.is_empty() {
return true;
}
filter_tags.iter().any(|tag| {
let filter = tag.to_lowercase();
entry
.tags
.iter()
.any(|t| t.to_lowercase().contains(&filter))
})
}

fn new_tags_match(normalized_filter_tags: &[String], entry: &TodoEntry) -> bool {
if normalized_filter_tags.is_empty() {
return true;
}
normalized_filter_tags.iter().any(|filter| {
entry
.tags
.iter()
.any(|tag| tag.eq_ignore_ascii_case(filter) || tag.to_lowercase().contains(filter))
})
}

fn old_sort_entries(entries: &mut Vec<(usize, TodoEntry)>) {
entries.sort_by(|a, b| {
a.1.text
.to_lowercase()
.cmp(&b.1.text.to_lowercase())
.then_with(|| a.0.cmp(&b.0))
});
}

fn new_sort_entries(entries: &mut Vec<(usize, TodoEntry)>) {
let mut keyed: Vec<(String, usize, TodoEntry)> = entries
.drain(..)
.map(|(idx, entry)| (entry.text.to_lowercase(), idx, entry))
.collect();
keyed.sort_by(|a, b| a.0.cmp(&b.0).then_with(|| a.1.cmp(&b.1)));
entries.extend(keyed.into_iter().map(|(_, idx, entry)| (idx, entry)));
}

fn bench_todo_filter_and_sort(c: &mut Criterion) {
let todos = build_entries(25_000);
let filter_tags = vec![
"urgent".to_string(),
"feature1".to_string(),
"team7".to_string(),
];
let normalized_filter_tags: Vec<String> =
filter_tags.iter().map(|t| t.to_lowercase()).collect();

c.bench_function("todo_filter_old", |b| {
b.iter(|| {
let count = todos
.iter()
.filter(|entry| old_tags_match(black_box(&filter_tags), entry))
.count();
black_box(count)
})
});

c.bench_function("todo_filter_new", |b| {
b.iter(|| {
let count = todos
.iter()
.filter(|entry| new_tags_match(black_box(&normalized_filter_tags), entry))
.count();
black_box(count)
})
});

c.bench_function("todo_sort_old", |b| {
b.iter(|| {
let mut entries: Vec<(usize, TodoEntry)> = todos.iter().cloned().enumerate().collect();
old_sort_entries(&mut entries);
black_box(entries.len())
})
});

c.bench_function("todo_sort_new", |b| {
b.iter(|| {
let mut entries: Vec<(usize, TodoEntry)> = todos.iter().cloned().enumerate().collect();
new_sort_entries(&mut entries);
black_box(entries.len())
})
});
}

criterion_group!(benches, bench_todo_filter_and_sort);
criterion_main!(benches);
58 changes: 40 additions & 18 deletions src/dashboard/widgets/todo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,16 +194,23 @@ impl TodoWidget {
})
}

fn tags_match(&self, entry: &TodoEntry) -> bool {
if self.cfg.filter_tags.is_empty() {
fn normalized_filter_tags(&self) -> Vec<String> {
self.cfg
.filter_tags
.iter()
.map(|tag| tag.to_lowercase())
.collect()
}

fn tags_match(&self, entry: &TodoEntry, normalized_filter_tags: &[String]) -> bool {
if normalized_filter_tags.is_empty() {
return true;
}
self.cfg.filter_tags.iter().any(|tag| {
let filter = tag.to_lowercase();
normalized_filter_tags.iter().any(|filter| {
entry
.tags
.iter()
.any(|t| t.to_lowercase().contains(&filter))
.any(|tag| tag.eq_ignore_ascii_case(filter) || tag.to_lowercase().contains(filter))
})
}

Expand All @@ -219,16 +226,22 @@ impl TodoWidget {
entry.priority >= self.cfg.min_priority
}

fn entry_matches_filters(&self, entry: &TodoEntry) -> bool {
self.status_match(entry) && self.priority_match(entry) && self.tags_match(entry)
fn entry_matches_filters(&self, entry: &TodoEntry, normalized_filter_tags: &[String]) -> bool {
self.status_match(entry)
&& self.priority_match(entry)
&& self.tags_match(entry, normalized_filter_tags)
}

fn filter_entries(&self, todos: &[TodoEntry]) -> Vec<(usize, TodoEntry)> {
fn filter_entries(
&self,
todos: &[TodoEntry],
normalized_filter_tags: &[String],
) -> Vec<(usize, TodoEntry)> {
todos
.iter()
.cloned()
.enumerate()
.filter(|(_, t)| self.entry_matches_filters(t))
.filter(|(_, t)| self.entry_matches_filters(t, normalized_filter_tags))
.collect()
}

Expand All @@ -250,19 +263,26 @@ impl TodoWidget {
entries.sort_by(|a, b| b.1.priority.cmp(&a.1.priority).then_with(|| a.0.cmp(&b.0)))
}
TodoSort::Created => entries.sort_by_key(|(idx, _)| *idx),
TodoSort::Alphabetical => entries.sort_by(|a, b| {
a.1.text
.to_lowercase()
.cmp(&b.1.text.to_lowercase())
.then_with(|| a.0.cmp(&b.0))
}),
TodoSort::Alphabetical => {
let mut keyed_entries: Vec<(String, usize, TodoEntry)> = entries
.drain(..)
.map(|(idx, entry)| (entry.text.to_lowercase(), idx, entry))
.collect();
keyed_entries.sort_by(|a, b| a.0.cmp(&b.0).then_with(|| a.1.cmp(&b.1)));
entries.extend(
keyed_entries
.into_iter()
.map(|(_, idx, entry)| (idx, entry)),
);
}
}
}

fn render_summary(&mut self, ui: &mut egui::Ui, todos: &[TodoEntry]) -> Option<WidgetAction> {
let normalized_filter_tags = self.normalized_filter_tags();
let filtered: Vec<&TodoEntry> = todos
.iter()
.filter(|t| self.priority_match(t) && self.tags_match(t))
.filter(|t| self.priority_match(t) && self.tags_match(t, &normalized_filter_tags))
.collect();
let done = filtered.iter().filter(|t| t.done).count();
let total = filtered.len();
Expand Down Expand Up @@ -353,7 +373,8 @@ impl TodoWidget {
ctx: &DashboardContext<'_>,
todos: &[TodoEntry],
) -> Option<WidgetAction> {
let mut entries = self.filter_entries(todos);
let normalized_filter_tags = self.normalized_filter_tags();
let mut entries = self.filter_entries(todos, &normalized_filter_tags);
Self::sort_entries(&mut entries, self.cfg.sort);
entries.truncate(self.cfg.count);

Expand Down Expand Up @@ -505,7 +526,8 @@ mod tests {
cfg.sort = TodoSort::Priority;
let widget = TodoWidget::new(cfg);

let mut filtered = widget.filter_entries(&sample_entries());
let normalized_filter_tags = widget.normalized_filter_tags();
let mut filtered = widget.filter_entries(&sample_entries(), &normalized_filter_tags);
TodoWidget::sort_entries(&mut filtered, TodoSort::Priority);
let texts: Vec<String> = filtered.into_iter().map(|(_, entry)| entry.text).collect();
assert_eq!(texts, vec!["gamma"]);
Expand Down