@@ -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