Skip to content

Commit 136fa89

Browse files
authored
Merge pull request #886 from multiplex55/codex/extend-plugin-trait-with-query-routing-hints
Codex-generated pull request
2 parents 79be111 + 920d0e7 commit 136fa89

8 files changed

Lines changed: 222 additions & 0 deletions

File tree

src/plugin.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,20 @@ pub trait Plugin: Send + Sync {
7373
Vec::new()
7474
}
7575

76+
/// Optional query head prefixes that should route to this plugin.
77+
///
78+
/// Prefix matching is case-insensitive and uses the first token of the
79+
/// query. Plugins that return an empty slice are considered global and run
80+
/// for all queries.
81+
fn query_prefixes(&self) -> &[&str] {
82+
&[]
83+
}
84+
85+
/// Opt-out of prefix routing and always run this plugin for searches.
86+
fn always_search(&self) -> bool {
87+
false
88+
}
89+
7690
/// Return default settings for this plugin if any.
7791
fn default_settings(&self) -> Option<serde_json::Value> {
7892
None
@@ -295,6 +309,10 @@ impl PluginManager {
295309
enabled_caps: Option<&std::collections::HashMap<String, Vec<String>>>,
296310
) -> Vec<Action> {
297311
let (filtered_query, filters) = split_action_filters(query);
312+
let query_head = filtered_query
313+
.split_whitespace()
314+
.next()
315+
.map(str::to_ascii_lowercase);
298316
let mut actions = Vec::new();
299317
for p in &self.plugins {
300318
let name = p.name();
@@ -310,6 +328,22 @@ impl PluginManager {
310328
}
311329
}
312330
}
331+
332+
if !p.always_search() {
333+
let prefixes = p.query_prefixes();
334+
if !prefixes.is_empty() {
335+
let Some(head) = query_head.as_deref() else {
336+
continue;
337+
};
338+
if !prefixes
339+
.iter()
340+
.any(|prefix| prefix.eq_ignore_ascii_case(head))
341+
{
342+
continue;
343+
}
344+
}
345+
}
346+
313347
actions.extend(p.search(&filtered_query));
314348
}
315349
if filters.include_kinds.is_empty()

src/plugins/bookmarks.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,4 +353,8 @@ impl Plugin for BookmarksPlugin {
353353
},
354354
]
355355
}
356+
357+
fn query_prefixes(&self) -> &[&str] {
358+
&["bm"]
359+
}
356360
}

src/plugins/folders.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,4 +293,8 @@ impl Plugin for FoldersPlugin {
293293
},
294294
]
295295
}
296+
297+
fn query_prefixes(&self) -> &[&str] {
298+
&["f"]
299+
}
296300
}

src/plugins/note.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1440,6 +1440,10 @@ impl Plugin for NotePlugin {
14401440
}
14411441
self.external_open = cfg.external_open;
14421442
}
1443+
1444+
fn query_prefixes(&self) -> &[&str] {
1445+
&["note", "notes"]
1446+
}
14431447
}
14441448

14451449
#[cfg(test)]

src/plugins/omni_search.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,10 @@ impl Plugin for OmniSearchPlugin {
110110
},
111111
]
112112
}
113+
114+
fn query_prefixes(&self) -> &[&str] {
115+
&["o"]
116+
}
113117
}
114118

115119
impl OmniSearchPlugin {

src/plugins/timer.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -869,4 +869,8 @@ impl Plugin for TimerPlugin {
869869
},
870870
]
871871
}
872+
873+
fn query_prefixes(&self) -> &[&str] {
874+
&["timer", "alarm"]
875+
}
872876
}

src/plugins/todo.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1036,6 +1036,10 @@ impl Plugin for TodoPlugin {
10361036
},
10371037
]
10381038
}
1039+
1040+
fn query_prefixes(&self) -> &[&str] {
1041+
&["todo"]
1042+
}
10391043
}
10401044

10411045
#[cfg(test)]

tests/plugin_routing.rs

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
use multi_launcher::actions::Action;
2+
use multi_launcher::plugin::{Plugin, PluginManager};
3+
use multi_launcher::plugins::todo::TodoPlugin;
4+
use std::sync::{
5+
atomic::{AtomicUsize, Ordering},
6+
Arc,
7+
};
8+
9+
struct CountingPlugin {
10+
name: &'static str,
11+
prefixes: &'static [&'static str],
12+
always_search: bool,
13+
calls: Arc<AtomicUsize>,
14+
}
15+
16+
impl CountingPlugin {
17+
fn new(
18+
name: &'static str,
19+
prefixes: &'static [&'static str],
20+
always_search: bool,
21+
calls: Arc<AtomicUsize>,
22+
) -> Self {
23+
Self {
24+
name,
25+
prefixes,
26+
always_search,
27+
calls,
28+
}
29+
}
30+
}
31+
32+
impl Plugin for CountingPlugin {
33+
fn search(&self, query: &str) -> Vec<Action> {
34+
self.calls.fetch_add(1, Ordering::SeqCst);
35+
vec![Action {
36+
label: format!("{}:{query}", self.name),
37+
desc: "test".into(),
38+
action: self.name.into(),
39+
args: None,
40+
}]
41+
}
42+
43+
fn name(&self) -> &str {
44+
self.name
45+
}
46+
47+
fn description(&self) -> &str {
48+
"test"
49+
}
50+
51+
fn capabilities(&self) -> &[&str] {
52+
&["search"]
53+
}
54+
55+
fn query_prefixes(&self) -> &[&str] {
56+
self.prefixes
57+
}
58+
59+
fn always_search(&self) -> bool {
60+
self.always_search
61+
}
62+
}
63+
64+
#[test]
65+
fn routing_selects_expected_plugins() {
66+
let todo_calls = Arc::new(AtomicUsize::new(0));
67+
let timer_calls = Arc::new(AtomicUsize::new(0));
68+
let global_calls = Arc::new(AtomicUsize::new(0));
69+
70+
let mut pm = PluginManager::new();
71+
pm.register(Box::new(CountingPlugin::new(
72+
"todo_plugin",
73+
&["todo"],
74+
false,
75+
todo_calls.clone(),
76+
)));
77+
pm.register(Box::new(CountingPlugin::new(
78+
"timer_plugin",
79+
&["timer"],
80+
false,
81+
timer_calls.clone(),
82+
)));
83+
pm.register(Box::new(CountingPlugin::new(
84+
"global_plugin",
85+
&[],
86+
false,
87+
global_calls.clone(),
88+
)));
89+
90+
let out = pm.search_filtered("todo list", None, None);
91+
assert_eq!(todo_calls.load(Ordering::SeqCst), 1);
92+
assert_eq!(timer_calls.load(Ordering::SeqCst), 0);
93+
assert_eq!(global_calls.load(Ordering::SeqCst), 1);
94+
assert!(out.iter().any(|a| a.action == "todo_plugin"));
95+
assert!(out.iter().any(|a| a.action == "global_plugin"));
96+
assert!(!out.iter().any(|a| a.action == "timer_plugin"));
97+
}
98+
99+
#[test]
100+
fn global_plugins_and_opt_out_plugins_still_run() {
101+
let global_calls = Arc::new(AtomicUsize::new(0));
102+
let opt_out_calls = Arc::new(AtomicUsize::new(0));
103+
let prefixed_calls = Arc::new(AtomicUsize::new(0));
104+
105+
let mut pm = PluginManager::new();
106+
pm.register(Box::new(CountingPlugin::new(
107+
"global",
108+
&[],
109+
false,
110+
global_calls.clone(),
111+
)));
112+
pm.register(Box::new(CountingPlugin::new(
113+
"always",
114+
&["timer"],
115+
true,
116+
opt_out_calls.clone(),
117+
)));
118+
pm.register(Box::new(CountingPlugin::new(
119+
"prefixed",
120+
&["todo"],
121+
false,
122+
prefixed_calls.clone(),
123+
)));
124+
125+
pm.search_filtered("plain query", None, None);
126+
assert_eq!(global_calls.load(Ordering::SeqCst), 1);
127+
assert_eq!(opt_out_calls.load(Ordering::SeqCst), 1);
128+
assert_eq!(prefixed_calls.load(Ordering::SeqCst), 0);
129+
}
130+
131+
#[test]
132+
fn existing_prefix_commands_remain_equivalent() {
133+
let plugin = TodoPlugin::default();
134+
let direct = plugin.search("todo list");
135+
136+
let mut pm = PluginManager::new();
137+
pm.register(Box::new(TodoPlugin::default()));
138+
let routed = pm.search_filtered("todo list", None, None);
139+
140+
let routed_view: Vec<_> = routed
141+
.iter()
142+
.map(|a| {
143+
(
144+
a.label.as_str(),
145+
a.desc.as_str(),
146+
a.action.as_str(),
147+
a.args.as_ref(),
148+
)
149+
})
150+
.collect();
151+
let direct_view: Vec<_> = direct
152+
.iter()
153+
.map(|a| {
154+
(
155+
a.label.as_str(),
156+
a.desc.as_str(),
157+
a.action.as_str(),
158+
a.args.as_ref(),
159+
)
160+
})
161+
.collect();
162+
163+
assert_eq!(routed_view, direct_view);
164+
}

0 commit comments

Comments
 (0)