From 15cd2480ec4f3ad8e44bf5e126c573c3869ed01d Mon Sep 17 00:00:00 2001 From: multiplex55 <6619098+multiplex55@users.noreply.github.com> Date: Thu, 8 Jan 2026 18:24:49 -0500 Subject: [PATCH 1/2] Improve todo list tag filtering --- src/help_window.rs | 6 ++++- src/plugins/todo.rs | 54 ++++++++++++++++++++++++++++++++------------- 2 files changed, 44 insertions(+), 16 deletions(-) diff --git a/src/help_window.rs b/src/help_window.rs index 7c4b9df..daf3cc7 100644 --- a/src/help_window.rs +++ b/src/help_window.rs @@ -206,7 +206,11 @@ fn example_queries(name: &str) -> Option<&'static [&'static str]> { "stopwatch" => Some(&["sw start", "sw list"]), "task_manager" => Some(&["tm"]), "text_case" => Some(&["case snake Hello World"]), - "todo" => Some(&["todo add buy milk @home", "todo list"]), + "todo" => Some(&[ + "todo add buy milk @home", + "todo list", + "todo list @testing @ui", + ]), "wikipedia" => Some(&["wiki rust"]), "help" => Some(&["help"]), _ => None, diff --git a/src/plugins/todo.rs b/src/plugins/todo.rs index 869554c..9867ea9 100644 --- a/src/plugins/todo.rs +++ b/src/plugins/todo.rs @@ -403,26 +403,50 @@ impl TodoPlugin { }; let mut entries: Vec<(usize, &TodoEntry)> = guard.iter().enumerate().collect(); + let mut requested_tags: Vec<&str> = Vec::new(); + let mut text_tokens: Vec<&str> = Vec::new(); let mut negative = false; - if let Some(stripped) = filter.strip_prefix('!') { - negative = true; - filter = stripped.trim(); + for token in filter.split_whitespace() { + if let Some(stripped) = token.strip_prefix('!') { + if !negative + && !stripped.starts_with('@') + && !stripped.starts_with('#') + && text_tokens.is_empty() + { + negative = true; + if !stripped.is_empty() { + text_tokens.push(stripped); + } + continue; + } + } + + if let Some(tag) = token.strip_prefix('@').or_else(|| token.strip_prefix('#')) { + if !tag.is_empty() { + requested_tags.push(tag); + } + } else { + text_tokens.push(token); + } } - let tag_filter = filter.starts_with('#'); - if tag_filter { - let tag = filter.trim_start_matches('#'); + let text_filter = text_tokens.join(" "); + let has_tag_filter = !requested_tags.is_empty(); + + // Tag filters run first, then text filters apply fuzzy matching against remaining text. + if has_tag_filter { entries.retain(|(_, t)| { - let has_tag = t.tags.iter().any(|tg| tg.eq_ignore_ascii_case(tag)); - if negative { - !has_tag - } else { - has_tag - } + requested_tags.iter().all(|requested| { + t.tags + .iter() + .any(|tag| tag.eq_ignore_ascii_case(requested)) + }) }); - } else if !filter.is_empty() { + } + + if !text_filter.is_empty() { entries.retain(|(_, t)| { - let text_match = self.matcher.fuzzy_match(&t.text, filter).is_some(); + let text_match = self.matcher.fuzzy_match(&t.text, &text_filter).is_some(); if negative { !text_match } else { @@ -431,7 +455,7 @@ impl TodoPlugin { }); } - if filter.is_empty() || tag_filter { + if text_filter.is_empty() || has_tag_filter { entries.sort_by(|a, b| b.1.priority.cmp(&a.1.priority)); } From f3371cf1911f97101e47bb707944e97ae5c1d9d5 Mon Sep 17 00:00:00 2001 From: multiplex55 <6619098+multiplex55@users.noreply.github.com> Date: Thu, 8 Jan 2026 18:35:43 -0500 Subject: [PATCH 2/2] Handle negated tag filters in todo list --- src/plugins/todo.rs | 49 +++++++++++++++++++++++++++++++-------------- 1 file changed, 34 insertions(+), 15 deletions(-) diff --git a/src/plugins/todo.rs b/src/plugins/todo.rs index 9867ea9..2e4942e 100644 --- a/src/plugins/todo.rs +++ b/src/plugins/todo.rs @@ -404,37 +404,46 @@ impl TodoPlugin { let mut entries: Vec<(usize, &TodoEntry)> = guard.iter().enumerate().collect(); let mut requested_tags: Vec<&str> = Vec::new(); + let mut excluded_tags: Vec<&str> = Vec::new(); let mut text_tokens: Vec<&str> = Vec::new(); let mut negative = false; for token in filter.split_whitespace() { + let mut token = token; + let mut token_negated = false; if let Some(stripped) = token.strip_prefix('!') { - if !negative - && !stripped.starts_with('@') - && !stripped.starts_with('#') - && text_tokens.is_empty() - { - negative = true; - if !stripped.is_empty() { - text_tokens.push(stripped); - } - continue; - } + token_negated = true; + token = stripped; } if let Some(tag) = token.strip_prefix('@').or_else(|| token.strip_prefix('#')) { if !tag.is_empty() { - requested_tags.push(tag); + if token_negated { + excluded_tags.push(tag); + } else { + requested_tags.push(tag); + } } - } else { + continue; + } + + if token_negated + && !negative + && !token.is_empty() + && text_tokens.is_empty() + { + negative = true; + } + + if !token.is_empty() { text_tokens.push(token); } } let text_filter = text_tokens.join(" "); - let has_tag_filter = !requested_tags.is_empty(); + let has_tag_filter = !requested_tags.is_empty() || !excluded_tags.is_empty(); // Tag filters run first, then text filters apply fuzzy matching against remaining text. - if has_tag_filter { + if !requested_tags.is_empty() { entries.retain(|(_, t)| { requested_tags.iter().all(|requested| { t.tags @@ -444,6 +453,16 @@ impl TodoPlugin { }); } + if !excluded_tags.is_empty() { + entries.retain(|(_, t)| { + !excluded_tags.iter().any(|excluded| { + t.tags + .iter() + .any(|tag| tag.eq_ignore_ascii_case(excluded)) + }) + }); + } + if !text_filter.is_empty() { entries.retain(|(_, t)| { let text_match = self.matcher.fuzzy_match(&t.text, &text_filter).is_some();