feat(plugin): surface loaded plugin commands in command-line autocomplete
Autocomplete suggestions came only from all_registered_command_names() — the compile-time inventory registry returning &'static str — so runtime-loaded plugin commands could never appear. Typing a plugin's command prefix (e.g. `LS_`) showed nothing even with the plugin loaded and its ribbon tab active; dispatch worked, only discovery was blind. Collect each enabled plugin's ribbon ToolDef command ids plus its manifest command_prefixes into a dynamic candidate pool (OwnedRibbonGroup::command_ids → PluginManager::command_names → plugin_command_names), refreshed in rebuild_ribbon_modules on startup load, settings reload, and every enable/disable toggle. ranked_matches now merges this pool with the static registry and returns owned strings. A plugin.toml `commands = [...]` list for sub-verbs with no ribbon button (e.g. LS_AUTOLABEL) stays a follow-up — it's a plugin-API surface change. Closes #272 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
60872da62a
commit
77c0c5a9d0
8 changed files with 173 additions and 20 deletions
|
|
@ -122,6 +122,24 @@ impl PluginManager {
|
|||
.collect()
|
||||
}
|
||||
|
||||
/// Command names advertised by every alive, non-disabled plugin: each
|
||||
/// ribbon tool's command id plus the manifest's `command_prefixes`. The
|
||||
/// host merges these into command-line autocomplete so plugin commands are
|
||||
/// discoverable by typing (#272).
|
||||
pub fn command_names<F: Fn(&str) -> bool>(&self, is_disabled: F) -> Vec<String> {
|
||||
let mut out = Vec::new();
|
||||
for p in &self.plugins {
|
||||
if is_disabled(p.process.id()) || !p.process.is_alive() {
|
||||
continue;
|
||||
}
|
||||
for group in p.process.ribbon() {
|
||||
out.append(&mut group.command_ids());
|
||||
}
|
||||
out.extend(p.process.manifest().command_prefixes.iter().cloned());
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Begin asynchronous shutdown of every plugin process.
|
||||
///
|
||||
/// Kills every child synchronously on the calling thread and moves the
|
||||
|
|
|
|||
|
|
@ -58,6 +58,49 @@ pub struct OwnedRibbonGroup {
|
|||
pub tools: Vec<OwnedRibbonItem>,
|
||||
}
|
||||
|
||||
impl OwnedRibbonGroup {
|
||||
/// Every command id a user could invoke from this group's tools. The host
|
||||
/// feeds these into the command-line autocomplete so a loaded plugin's
|
||||
/// commands are discoverable by typing, not just via ribbon buttons (#272).
|
||||
pub fn command_ids(&self) -> Vec<String> {
|
||||
let mut out = Vec::new();
|
||||
for item in &self.tools {
|
||||
collect_item_command_ids(item, &mut out);
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
fn push_tool_command(tool: &OwnedToolDef, out: &mut Vec<String>) {
|
||||
if let ModuleEvent::Command(cmd) = &tool.event {
|
||||
out.push(cmd.clone());
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_item_command_ids(item: &OwnedRibbonItem, out: &mut Vec<String>) {
|
||||
match item {
|
||||
OwnedRibbonItem::Tool(t) | OwnedRibbonItem::LargeTool(t) => push_tool_command(t, out),
|
||||
OwnedRibbonItem::Dropdown { items, .. } | OwnedRibbonItem::LargeDropdown { items, .. } => {
|
||||
// Each entry is (command id, label, icon).
|
||||
out.extend(items.iter().map(|(id, _, _)| id.clone()));
|
||||
}
|
||||
OwnedRibbonItem::LayerComboGroup { row2, row3 } => {
|
||||
for t in row2.iter().chain(row3.iter()) {
|
||||
push_tool_command(t, out);
|
||||
}
|
||||
}
|
||||
OwnedRibbonItem::PropertiesGroup { match_prop } => push_tool_command(match_prop, out),
|
||||
OwnedRibbonItem::StyleComboGroup { rows, manager_cmd, .. } => {
|
||||
for t in rows.iter().flatten() {
|
||||
push_tool_command(t, out);
|
||||
}
|
||||
if let Some(cmd) = manager_cmd {
|
||||
out.push(cmd.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct OwnedPluginManifest {
|
||||
pub id: String,
|
||||
|
|
@ -446,4 +489,33 @@ mod tests {
|
|||
assert_eq!(cloned.title(), shared.title());
|
||||
assert_eq!(shared.ribbon_groups().len(), cloned.ribbon_groups().len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn command_ids_covers_tools_and_dropdown_items() {
|
||||
let group = OwnedRibbonGroup {
|
||||
title: "Labels".to_string(),
|
||||
tools: vec![
|
||||
OwnedRibbonItem::Tool(OwnedToolDef {
|
||||
id: "ls_label".to_string(),
|
||||
label: "Label".to_string(),
|
||||
icon: OwnedIconKind::Glyph("L".to_string()),
|
||||
event: ModuleEvent::Command("LS_LABEL".to_string()),
|
||||
}),
|
||||
OwnedRibbonItem::LargeDropdown {
|
||||
id: "ls_menu".to_string(),
|
||||
label: "More".to_string(),
|
||||
icon: OwnedIconKind::Glyph("M".to_string()),
|
||||
items: vec![(
|
||||
"LS_AUTOLABEL".to_string(),
|
||||
"Auto".to_string(),
|
||||
OwnedIconKind::Glyph("A".to_string()),
|
||||
)],
|
||||
default: "LS_AUTOLABEL".to_string(),
|
||||
},
|
||||
],
|
||||
};
|
||||
let ids = group.command_ids();
|
||||
assert!(ids.contains(&"LS_LABEL".to_string()), "got {ids:?}");
|
||||
assert!(ids.contains(&"LS_AUTOLABEL".to_string()), "got {ids:?}");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue