Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions src/plugin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,20 @@ pub trait Plugin: Send + Sync {
Vec::new()
}

/// Optional query head prefixes that should route to this plugin.
///
/// Prefix matching is case-insensitive and uses the first token of the
/// query. Plugins that return an empty slice are considered global and run
/// for all queries.
fn query_prefixes(&self) -> &[&str] {
&[]
}

/// Opt-out of prefix routing and always run this plugin for searches.
fn always_search(&self) -> bool {
false
}

/// Return default settings for this plugin if any.
fn default_settings(&self) -> Option<serde_json::Value> {
None
Expand Down Expand Up @@ -295,6 +309,10 @@ impl PluginManager {
enabled_caps: Option<&std::collections::HashMap<String, Vec<String>>>,
) -> Vec<Action> {
let (filtered_query, filters) = split_action_filters(query);
let query_head = filtered_query
.split_whitespace()
.next()
.map(str::to_ascii_lowercase);
let mut actions = Vec::new();
for p in &self.plugins {
let name = p.name();
Expand All @@ -310,6 +328,22 @@ impl PluginManager {
}
}
}

if !p.always_search() {
let prefixes = p.query_prefixes();
if !prefixes.is_empty() {
let Some(head) = query_head.as_deref() else {
continue;
};
if !prefixes
.iter()
.any(|prefix| prefix.eq_ignore_ascii_case(head))
{
continue;
}
}
}

actions.extend(p.search(&filtered_query));
}
if filters.include_kinds.is_empty()
Expand Down
4 changes: 4 additions & 0 deletions src/plugins/bookmarks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -353,4 +353,8 @@ impl Plugin for BookmarksPlugin {
},
]
}

fn query_prefixes(&self) -> &[&str] {
&["bm"]
}
}
4 changes: 4 additions & 0 deletions src/plugins/folders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -293,4 +293,8 @@ impl Plugin for FoldersPlugin {
},
]
}

fn query_prefixes(&self) -> &[&str] {
&["f"]
}
}
4 changes: 4 additions & 0 deletions src/plugins/note.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1440,6 +1440,10 @@ impl Plugin for NotePlugin {
}
self.external_open = cfg.external_open;
}

fn query_prefixes(&self) -> &[&str] {
&["note", "notes"]
}
}

#[cfg(test)]
Expand Down
4 changes: 4 additions & 0 deletions src/plugins/omni_search.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ impl Plugin for OmniSearchPlugin {
},
]
}

fn query_prefixes(&self) -> &[&str] {
&["o"]
}
}

impl OmniSearchPlugin {
Expand Down
4 changes: 4 additions & 0 deletions src/plugins/timer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -869,4 +869,8 @@ impl Plugin for TimerPlugin {
},
]
}

fn query_prefixes(&self) -> &[&str] {
&["timer", "alarm"]
}
}
4 changes: 4 additions & 0 deletions src/plugins/todo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1036,6 +1036,10 @@ impl Plugin for TodoPlugin {
},
]
}

fn query_prefixes(&self) -> &[&str] {
&["todo"]
}
}

#[cfg(test)]
Expand Down
164 changes: 164 additions & 0 deletions tests/plugin_routing.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
use multi_launcher::actions::Action;
use multi_launcher::plugin::{Plugin, PluginManager};
use multi_launcher::plugins::todo::TodoPlugin;
use std::sync::{
atomic::{AtomicUsize, Ordering},
Arc,
};

struct CountingPlugin {
name: &'static str,
prefixes: &'static [&'static str],
always_search: bool,
calls: Arc<AtomicUsize>,
}

impl CountingPlugin {
fn new(
name: &'static str,
prefixes: &'static [&'static str],
always_search: bool,
calls: Arc<AtomicUsize>,
) -> Self {
Self {
name,
prefixes,
always_search,
calls,
}
}
}

impl Plugin for CountingPlugin {
fn search(&self, query: &str) -> Vec<Action> {
self.calls.fetch_add(1, Ordering::SeqCst);
vec![Action {
label: format!("{}:{query}", self.name),
desc: "test".into(),
action: self.name.into(),
args: None,
}]
}

fn name(&self) -> &str {
self.name
}

fn description(&self) -> &str {
"test"
}

fn capabilities(&self) -> &[&str] {
&["search"]
}

fn query_prefixes(&self) -> &[&str] {
self.prefixes
}

fn always_search(&self) -> bool {
self.always_search
}
}

#[test]
fn routing_selects_expected_plugins() {
let todo_calls = Arc::new(AtomicUsize::new(0));
let timer_calls = Arc::new(AtomicUsize::new(0));
let global_calls = Arc::new(AtomicUsize::new(0));

let mut pm = PluginManager::new();
pm.register(Box::new(CountingPlugin::new(
"todo_plugin",
&["todo"],
false,
todo_calls.clone(),
)));
pm.register(Box::new(CountingPlugin::new(
"timer_plugin",
&["timer"],
false,
timer_calls.clone(),
)));
pm.register(Box::new(CountingPlugin::new(
"global_plugin",
&[],
false,
global_calls.clone(),
)));

let out = pm.search_filtered("todo list", None, None);
assert_eq!(todo_calls.load(Ordering::SeqCst), 1);
assert_eq!(timer_calls.load(Ordering::SeqCst), 0);
assert_eq!(global_calls.load(Ordering::SeqCst), 1);
assert!(out.iter().any(|a| a.action == "todo_plugin"));
assert!(out.iter().any(|a| a.action == "global_plugin"));
assert!(!out.iter().any(|a| a.action == "timer_plugin"));
}

#[test]
fn global_plugins_and_opt_out_plugins_still_run() {
let global_calls = Arc::new(AtomicUsize::new(0));
let opt_out_calls = Arc::new(AtomicUsize::new(0));
let prefixed_calls = Arc::new(AtomicUsize::new(0));

let mut pm = PluginManager::new();
pm.register(Box::new(CountingPlugin::new(
"global",
&[],
false,
global_calls.clone(),
)));
pm.register(Box::new(CountingPlugin::new(
"always",
&["timer"],
true,
opt_out_calls.clone(),
)));
pm.register(Box::new(CountingPlugin::new(
"prefixed",
&["todo"],
false,
prefixed_calls.clone(),
)));

pm.search_filtered("plain query", None, None);
assert_eq!(global_calls.load(Ordering::SeqCst), 1);
assert_eq!(opt_out_calls.load(Ordering::SeqCst), 1);
assert_eq!(prefixed_calls.load(Ordering::SeqCst), 0);
}

#[test]
fn existing_prefix_commands_remain_equivalent() {
let plugin = TodoPlugin::default();
let direct = plugin.search("todo list");

let mut pm = PluginManager::new();
pm.register(Box::new(TodoPlugin::default()));
let routed = pm.search_filtered("todo list", None, None);

let routed_view: Vec<_> = routed
.iter()
.map(|a| {
(
a.label.as_str(),
a.desc.as_str(),
a.action.as_str(),
a.args.as_ref(),
)
})
.collect();
let direct_view: Vec<_> = direct
.iter()
.map(|a| {
(
a.label.as_str(),
a.desc.as_str(),
a.action.as_str(),
a.args.as_ref(),
)
})
.collect();

assert_eq!(routed_view, direct_view);
}