diff --git a/src/help_window.rs b/src/help_window.rs index 7c4b9df9..daf3cc7b 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 869554cd..2e4942ef 100644 --- a/src/plugins/todo.rs +++ b/src/plugins/todo.rs @@ -403,26 +403,69 @@ 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; - if let Some(stripped) = filter.strip_prefix('!') { - negative = true; - filter = stripped.trim(); + for token in filter.split_whitespace() { + let mut token = token; + let mut token_negated = false; + if let Some(stripped) = token.strip_prefix('!') { + token_negated = true; + token = stripped; + } + + if let Some(tag) = token.strip_prefix('@').or_else(|| token.strip_prefix('#')) { + if !tag.is_empty() { + if token_negated { + excluded_tags.push(tag); + } else { + requested_tags.push(tag); + } + } + continue; + } + + if token_negated + && !negative + && !token.is_empty() + && text_tokens.is_empty() + { + negative = true; + } + + if !token.is_empty() { + 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() || !excluded_tags.is_empty(); + + // Tag filters run first, then text filters apply fuzzy matching against remaining text. + if !requested_tags.is_empty() { 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 !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, filter).is_some(); + let text_match = self.matcher.fuzzy_match(&t.text, &text_filter).is_some(); if negative { !text_match } else { @@ -431,7 +474,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)); }