Skip to content

Commit bb8d910

Browse files
authored
Merge pull request #910 from multiplex55/codex/update-exact-mode-post-filtering-in-search_plugins
Preserve plugin-resolved results in exact match mode
2 parents e497f3c + f647d56 commit bb8d910

1 file changed

Lines changed: 128 additions & 0 deletions

File tree

src/gui/mod.rs

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -654,6 +654,45 @@ impl LauncherApp {
654654
haystack_label.to_lowercase().contains(&query_lc)
655655
}
656656

657+
fn should_bypass_exact_post_filter(query: &str, action: &str) -> bool {
658+
// `query:*` actions are command suggestions that should still participate in
659+
// exact display-text filtering when users are browsing command names/options.
660+
if action.starts_with("query:") {
661+
return false;
662+
}
663+
664+
let mut parts = query.split_whitespace();
665+
let Some(head) = parts.next().map(str::to_ascii_lowercase) else {
666+
return false;
667+
};
668+
let Some(subcommand) = parts.next().map(str::to_ascii_lowercase) else {
669+
return false;
670+
};
671+
672+
// Only bypass launcher-side exact display filtering when the query is an
673+
// explicit plugin command whose plugin already returned resolved outputs.
674+
// Example: `note today` / `note search <term>` yielding `note:new:*` or
675+
// `note:open:*` actions; re-filtering those by label text can hide valid results.
676+
matches!(head.as_str(), "note" | "notes")
677+
&& matches!(
678+
subcommand.as_str(),
679+
"today"
680+
| "search"
681+
| "links"
682+
| "link"
683+
| "list"
684+
| "open"
685+
| "new"
686+
| "add"
687+
| "create"
688+
| "graph"
689+
| "templates"
690+
| "tag"
691+
| "rm"
692+
)
693+
&& action.starts_with("note:")
694+
}
695+
657696
fn has_diagnostics_widget(&self) -> bool {
658697
self.dashboard
659698
.slots
@@ -1744,6 +1783,14 @@ impl LauncherApp {
17441783
for a in plugin_results {
17451784
let desc_lc = a.desc.to_lowercase();
17461785
if self.is_exact_match_mode() {
1786+
if Self::should_bypass_exact_post_filter(trimmed, &a.action) {
1787+
// Plugin commands like `note today`/`note search <term>` already
1788+
// returned concrete results (e.g. `note:new:*`, `note:open:*`).
1789+
// Re-filtering by label/desc text can hide valid plugin-resolved
1790+
// outputs, so keep them as-is in exact mode.
1791+
res.push((a, 0.0));
1792+
continue;
1793+
}
17471794
if query_term.is_empty() {
17481795
res.push((a, 0.0));
17491796
} else {
@@ -1814,6 +1861,13 @@ impl LauncherApp {
18141861
for a in plugin_results {
18151862
let desc_lc = a.desc.to_lowercase();
18161863
if self.is_exact_match_mode() {
1864+
if Self::should_bypass_exact_post_filter(trimmed, &a.action) {
1865+
// Explicit plugin commands can resolve into result lists/artifacts.
1866+
// Preserve those resolved actions in exact mode instead of applying
1867+
// a second label/description exact filter in the launcher layer.
1868+
res.push((a, 0.0));
1869+
continue;
1870+
}
18171871
if query_term_lc.is_empty() {
18181872
res.push((a, 0.0));
18191873
} else {
@@ -5395,6 +5449,55 @@ mod tests {
53955449
}
53965450
}
53975451

5452+
struct ExactFilterPlugin;
5453+
5454+
impl crate::plugin::Plugin for ExactFilterPlugin {
5455+
fn search(&self, query: &str) -> Vec<Action> {
5456+
let query = query.trim().to_ascii_lowercase();
5457+
if query == "note today" {
5458+
return vec![Action {
5459+
label: "Create 2025 02 23".into(),
5460+
desc: "Note".into(),
5461+
action: "note:new:2025-02-23".into(),
5462+
args: None,
5463+
}];
5464+
}
5465+
if query.starts_with("note search ") {
5466+
return vec![Action {
5467+
label: "Alpha note".into(),
5468+
desc: "Note".into(),
5469+
action: "note:open:alpha".into(),
5470+
args: None,
5471+
}];
5472+
}
5473+
if query.starts_with("note ") {
5474+
return vec![Action {
5475+
label: "note search".into(),
5476+
desc: "Note".into(),
5477+
action: "query:note search ".into(),
5478+
args: None,
5479+
}];
5480+
}
5481+
Vec::new()
5482+
}
5483+
5484+
fn name(&self) -> &str {
5485+
"exact-filter-plugin"
5486+
}
5487+
5488+
fn description(&self) -> &str {
5489+
"Exact filter test plugin"
5490+
}
5491+
5492+
fn capabilities(&self) -> &[&str] {
5493+
&[]
5494+
}
5495+
5496+
fn query_prefixes(&self) -> &[&str] {
5497+
&["note"]
5498+
}
5499+
}
5500+
53985501
#[test]
53995502
fn inline_error_visibility_respects_setting() {
54005503
let ctx = egui::Context::default();
@@ -5550,6 +5653,31 @@ mod tests {
55505653
assert!(!app.results.iter().any(|a| a.action == "demo:action"));
55515654
}
55525655

5656+
#[test]
5657+
fn exact_mode_keeps_plugin_resolved_results_but_filters_query_suggestions() {
5658+
let ctx = egui::Context::default();
5659+
let mut app = new_app(&ctx);
5660+
app.match_exact = true;
5661+
app.plugins.register(Box::new(ExactFilterPlugin));
5662+
5663+
app.query = "note today".into();
5664+
app.search();
5665+
assert!(app
5666+
.results
5667+
.iter()
5668+
.any(|a| a.action == "note:new:2025-02-23"));
5669+
5670+
app.query = "note search alpha".into();
5671+
app.last_results_valid = false;
5672+
app.search();
5673+
assert!(app.results.iter().any(|a| a.action == "note:open:alpha"));
5674+
5675+
app.query = "note zz".into();
5676+
app.last_results_valid = false;
5677+
app.search();
5678+
assert!(app.results.is_empty());
5679+
}
5680+
55535681
#[test]
55545682
fn watch_events_refresh_alias_and_lowercase_alias_caches() {
55555683
let _lock = TEST_MUTEX.lock().unwrap();

0 commit comments

Comments
 (0)