Skip to content

Commit ade3946

Browse files
authored
Merge pull request #929 from multiplex55/codex/enhance-action-discovery-in-mouse-gestures-dialog
Enhance list-backed action discovery for mouse gesture picker
2 parents 8012abd + 1cd3a35 commit ade3946

1 file changed

Lines changed: 205 additions & 42 deletions

File tree

src/gui/mouse_gestures_dialog.rs

Lines changed: 205 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -161,27 +161,46 @@ fn is_default_generated_label(label: &str) -> bool {
161161
rest.chars().all(|c| c.is_ascii_digit()) || rest == normalize_recorded_tokens_for_label(rest)
162162
}
163163

164-
fn list_query_prefix_for_plugin(plugin_name: &str) -> Option<&'static str> {
164+
fn list_query_prefix_for_plugin(plugin_name: &str) -> Option<(&'static str, Option<&'static str>)> {
165165
match plugin_name {
166-
"folders" => Some("f list"),
167-
"bookmarks" => Some("bm list"),
168-
"clipboard" => Some("cb list"),
169-
"emoji" => Some("emoji list"),
170-
"notes" => Some("note list"),
166+
"folders" => Some(("f list", Some("f"))),
167+
"bookmarks" => Some(("bm list", Some("bm"))),
168+
"clipboard" => Some(("cb list", Some("cb"))),
169+
"emoji" => Some(("emoji list", Some("emoji"))),
170+
"note" | "notes" => Some(("note list", Some("note search"))),
171+
"processes" => Some(("ps", Some("pss"))),
172+
"todo" => Some(("todo list", Some("todo"))),
173+
"snippets" => Some(("cs list", Some("cs"))),
171174
_ => None,
172175
}
173176
}
174177

178+
fn has_concrete_actions(actions: &[crate::actions::Action]) -> bool {
179+
actions
180+
.iter()
181+
.any(|action| !action.action.trim_start().starts_with("query:"))
182+
}
183+
175184
fn resolve_action_source(plugin: &dyn Plugin, filter: &str) -> Vec<crate::actions::Action> {
176-
if let Some(prefix) = list_query_prefix_for_plugin(plugin.name()) {
177-
let query = if filter.trim().is_empty() {
178-
prefix.to_string()
179-
} else {
180-
format!("{prefix} {}", filter.trim())
181-
};
182-
let actions = plugin.search(&query);
183-
if !actions.is_empty() {
184-
return actions;
185+
if let Some((prefix, alternate_prefix)) = list_query_prefix_for_plugin(plugin.name()) {
186+
let filter = filter.trim();
187+
for candidate in [Some(prefix), alternate_prefix] {
188+
let Some(candidate) = candidate else {
189+
continue;
190+
};
191+
192+
let query = if filter.is_empty() {
193+
candidate.to_string()
194+
} else {
195+
format!("{candidate} {filter}")
196+
};
197+
let actions = plugin.search(&query);
198+
if actions.is_empty() {
199+
continue;
200+
}
201+
if has_concrete_actions(&actions) {
202+
return actions;
203+
}
185204
}
186205
}
187206
plugin.commands()
@@ -1281,16 +1300,48 @@ mod tests {
12811300
assert_eq!(dlg.rename_idx, Some(1));
12821301
}
12831302

1284-
#[derive(Clone)]
1303+
use std::collections::HashMap;
1304+
use std::sync::Mutex;
1305+
12851306
struct MockPlugin {
12861307
name: String,
1287-
search_results: Vec<crate::actions::Action>,
1308+
search_results_by_query: HashMap<String, Vec<crate::actions::Action>>,
12881309
command_results: Vec<crate::actions::Action>,
1310+
seen_queries: Mutex<Vec<String>>,
1311+
}
1312+
1313+
impl MockPlugin {
1314+
fn with_query_results(
1315+
name: &str,
1316+
search_results_by_query: impl IntoIterator<Item = (String, Vec<crate::actions::Action>)>,
1317+
command_results: Vec<crate::actions::Action>,
1318+
) -> Self {
1319+
Self {
1320+
name: name.into(),
1321+
search_results_by_query: search_results_by_query.into_iter().collect(),
1322+
command_results,
1323+
seen_queries: Mutex::new(Vec::new()),
1324+
}
1325+
}
1326+
1327+
fn seen_queries(&self) -> Vec<String> {
1328+
self.seen_queries
1329+
.lock()
1330+
.expect("seen_queries lock poisoned")
1331+
.clone()
1332+
}
12891333
}
12901334

12911335
impl crate::plugin::Plugin for MockPlugin {
1292-
fn search(&self, _query: &str) -> Vec<crate::actions::Action> {
1293-
self.search_results.clone()
1336+
fn search(&self, query: &str) -> Vec<crate::actions::Action> {
1337+
self.seen_queries
1338+
.lock()
1339+
.expect("seen_queries lock poisoned")
1340+
.push(query.to_string());
1341+
self.search_results_by_query
1342+
.get(query)
1343+
.cloned()
1344+
.unwrap_or_default()
12941345
}
12951346

12961347
fn name(&self) -> &str {
@@ -1332,37 +1383,149 @@ mod tests {
13321383
assert_eq!(entry.label, "My Favorite Gesture");
13331384
}
13341385

1335-
#[test]
1336-
fn resolve_action_source_prefers_mapped_list_search_and_falls_back_to_commands() {
1337-
let list_action = crate::actions::Action {
1338-
label: "Clipboard Entry".into(),
1339-
desc: "Clipboard".into(),
1340-
action: "clipboard:copy:1".into(),
1386+
fn query_result(
1387+
query: &str,
1388+
actions: Vec<crate::actions::Action>,
1389+
) -> (String, Vec<crate::actions::Action>) {
1390+
(query.to_string(), actions)
1391+
}
1392+
fn concrete_action(action: &str) -> crate::actions::Action {
1393+
crate::actions::Action {
1394+
label: "Concrete".into(),
1395+
desc: "Mock".into(),
1396+
action: action.into(),
13411397
args: None,
1342-
};
1343-
let command_action = crate::actions::Action {
1344-
label: "cb list".into(),
1345-
desc: "Clipboard".into(),
1346-
action: "query:cb list".into(),
1398+
}
1399+
}
1400+
1401+
fn query_action(action: &str) -> crate::actions::Action {
1402+
crate::actions::Action {
1403+
label: "Query".into(),
1404+
desc: "Mock".into(),
1405+
action: action.into(),
13471406
args: None,
1348-
};
1407+
}
1408+
}
1409+
1410+
#[test]
1411+
fn resolve_action_source_prefers_mapped_list_search_and_falls_back_to_commands() {
1412+
let command_action = query_action("query:cb list");
1413+
let mapped_plugin = MockPlugin::with_query_results(
1414+
"clipboard",
1415+
[query_result(
1416+
"cb list abc",
1417+
vec![concrete_action("clipboard:copy:1")],
1418+
)],
1419+
vec![command_action.clone()],
1420+
);
13491421

1350-
let mapped_plugin = MockPlugin {
1351-
name: "clipboard".into(),
1352-
search_results: vec![list_action.clone()],
1353-
command_results: vec![command_action.clone()],
1354-
};
13551422
let mapped_actions = resolve_action_source(&mapped_plugin, "abc");
13561423
assert_eq!(mapped_actions.len(), 1);
13571424
assert_eq!(mapped_actions[0].action, "clipboard:copy:1");
1358-
1359-
let fallback_plugin = MockPlugin {
1360-
name: "custom".into(),
1361-
search_results: vec![list_action],
1362-
command_results: vec![command_action.clone()],
1363-
};
1425+
assert_eq!(mapped_plugin.seen_queries(), vec!["cb list abc"]);
1426+
1427+
let fallback_plugin = MockPlugin::with_query_results(
1428+
"custom",
1429+
[query_result(
1430+
"ignored",
1431+
vec![concrete_action("clipboard:copy:2")],
1432+
)],
1433+
vec![command_action.clone()],
1434+
);
13641435
let fallback_actions = resolve_action_source(&fallback_plugin, "abc");
13651436
assert_eq!(fallback_actions.len(), 1);
13661437
assert_eq!(fallback_actions[0].action, command_action.action);
1438+
assert!(fallback_plugin.seen_queries().is_empty());
1439+
}
1440+
1441+
#[test]
1442+
fn resolve_action_source_uses_mapped_search_for_each_supported_list_plugin() {
1443+
let cases = [
1444+
("clipboard", "cb list", "clipboard:copy:0"),
1445+
("emoji", "emoji list", "clipboard:😀"),
1446+
("processes", "ps", "process:switch:42"),
1447+
("note", "note list", "note:open:alpha"),
1448+
("notes", "note list", "note:open:beta"),
1449+
("folders", "f list", "/tmp"),
1450+
("bookmarks", "bm list", "https://example.com"),
1451+
("todo", "todo list", "todo:toggle:1"),
1452+
("snippets", "cs list", "snippet:copy:1"),
1453+
];
1454+
1455+
for (plugin_name, expected_query_prefix, expected_action) in cases {
1456+
let plugin = MockPlugin::with_query_results(
1457+
plugin_name,
1458+
[query_result(
1459+
&format!("{expected_query_prefix} abc"),
1460+
vec![concrete_action(expected_action)],
1461+
)],
1462+
vec![query_action("query:fallback")],
1463+
);
1464+
1465+
let actions = resolve_action_source(&plugin, "abc");
1466+
assert_eq!(actions.len(), 1, "plugin={plugin_name}");
1467+
assert_eq!(actions[0].action, expected_action, "plugin={plugin_name}");
1468+
assert_eq!(
1469+
plugin.seen_queries(),
1470+
vec![format!("{expected_query_prefix} abc")],
1471+
"plugin={plugin_name}"
1472+
);
1473+
}
1474+
}
1475+
1476+
#[test]
1477+
fn resolve_action_source_falls_back_to_commands_when_mapped_search_is_empty() {
1478+
let command_action = query_action("query:note list");
1479+
let plugin = MockPlugin::with_query_results(
1480+
"note",
1481+
Vec::<(String, Vec<crate::actions::Action>)>::new(),
1482+
vec![command_action.clone()],
1483+
);
1484+
1485+
let actions = resolve_action_source(&plugin, "abc");
1486+
assert_eq!(actions.len(), 1);
1487+
assert_eq!(actions[0].action, command_action.action);
1488+
assert_eq!(
1489+
plugin.seen_queries(),
1490+
vec!["note list abc", "note search abc"]
1491+
);
1492+
}
1493+
1494+
#[test]
1495+
fn resolve_action_source_retries_alternate_prefix_when_primary_only_returns_query_actions() {
1496+
let plugin = MockPlugin::with_query_results(
1497+
"clipboard",
1498+
[
1499+
query_result(
1500+
"cb list abc",
1501+
vec![query_action("query:cb list"), query_action("query:cb")],
1502+
),
1503+
query_result("cb abc", vec![concrete_action("clipboard:copy:7")]),
1504+
],
1505+
vec![query_action("query:cb list")],
1506+
);
1507+
1508+
let actions = resolve_action_source(&plugin, "abc");
1509+
assert_eq!(actions.len(), 1);
1510+
assert_eq!(actions[0].action, "clipboard:copy:7");
1511+
assert_eq!(plugin.seen_queries(), vec!["cb list abc", "cb abc"]);
1512+
}
1513+
1514+
#[test]
1515+
fn resolve_action_source_uses_commands_for_unmapped_plugins() {
1516+
let command_action = query_action("query:custom");
1517+
let plugin = MockPlugin::with_query_results(
1518+
"custom",
1519+
[query_result(
1520+
"custom abc",
1521+
vec![concrete_action("custom:run")],
1522+
)],
1523+
vec![command_action.clone()],
1524+
);
1525+
1526+
let actions = resolve_action_source(&plugin, "abc");
1527+
assert_eq!(actions.len(), 1);
1528+
assert_eq!(actions[0].action, command_action.action);
1529+
assert!(plugin.seen_queries().is_empty());
13671530
}
13681531
}

0 commit comments

Comments
 (0)