Skip to content

Commit 894c375

Browse files
authored
Merge pull request #930 from multiplex55/codex/add-cached-entry-type-for-searchable-text
Optimize action/plugin search caching with normalized CachedSearchEntry
2 parents ade3946 + 1c10038 commit 894c375

1 file changed

Lines changed: 175 additions & 38 deletions

File tree

src/gui/mod.rs

Lines changed: 175 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -435,9 +435,10 @@ pub struct LauncherApp {
435435
/// duplicates the pointer, keeping the action data itself shared. When
436436
/// actions are edited the entire `Arc` is replaced with a new one.
437437
pub actions: Arc<Vec<Action>>,
438-
action_cache: Vec<(String, String)>,
438+
action_cache: Vec<CachedSearchEntry>,
439439
actions_by_id: HashMap<String, Action>,
440440
command_cache: Vec<Action>,
441+
command_search_cache: Vec<CachedSearchEntry>,
441442
completion_index: Option<Map<Vec<u8>>>,
442443
action_completion_dirty: bool,
443444
command_completion_dirty: bool,
@@ -593,6 +594,23 @@ pub struct LauncherApp {
593594
pub vim_mode: bool,
594595
}
595596

597+
#[derive(Clone, Debug, Default, PartialEq, Eq)]
598+
struct CachedSearchEntry {
599+
label_lc: String,
600+
desc_lc: String,
601+
action_lc: String,
602+
}
603+
604+
impl CachedSearchEntry {
605+
fn from_action(action: &Action) -> Self {
606+
Self {
607+
label_lc: action.label.to_lowercase(),
608+
desc_lc: action.desc.to_lowercase(),
609+
action_lc: action.action.to_lowercase(),
610+
}
611+
}
612+
}
613+
596614
impl LauncherApp {
597615
fn normalize_alias(alias: Option<String>) -> (Option<String>, Option<String>) {
598616
let alias_lc = alias.as_ref().map(|text| text.to_lowercase());
@@ -646,12 +664,12 @@ impl LauncherApp {
646664
self.match_exact || self.fuzzy_weight <= 0.0
647665
}
648666

649-
fn matches_exact_display_text(haystack_label: &str, query: &str) -> bool {
650-
let query_lc = query.trim().to_lowercase();
667+
fn matches_exact_display_text(cached: &CachedSearchEntry, query_lc: &str) -> bool {
668+
let query_lc = query_lc.trim();
651669
if query_lc.is_empty() {
652670
return true;
653671
}
654-
haystack_label.to_lowercase().contains(&query_lc)
672+
cached.label_lc.contains(query_lc)
655673
}
656674

657675
fn should_bypass_exact_post_filter(query: &str, action: &str) -> bool {
@@ -704,7 +722,7 @@ impl LauncherApp {
704722
self.action_cache = self
705723
.actions
706724
.iter()
707-
.map(|a| (a.label.to_lowercase(), a.desc.to_lowercase()))
725+
.map(CachedSearchEntry::from_action)
708726
.collect();
709727
self.actions_by_id = self
710728
.actions
@@ -720,6 +738,7 @@ impl LauncherApp {
720738
.plugins
721739
.commands_filtered(self.enabled_plugins.as_ref());
722740
cmds.sort_by_cached_key(|a| a.label.to_lowercase());
741+
self.command_search_cache = cmds.iter().map(CachedSearchEntry::from_action).collect();
723742
self.command_cache = cmds;
724743
self.command_completion_dirty = true;
725744
self.schedule_completion_rebuild();
@@ -1072,9 +1091,7 @@ impl LauncherApp {
10721091

10731092
// Keep MG hook in lockstep with whether the plugin is enabled in the UI/settings.
10741093
crate::plugins::mouse_gestures::sync_enabled_plugins(self.enabled_plugins.as_ref());
1075-
if self.enabled_plugins.is_some() {
1076-
self.update_command_cache();
1077-
}
1094+
self.update_command_cache();
10781095
self.enabled_capabilities = enabled_capabilities;
10791096
if let Some((x, y)) = offscreen_pos {
10801097
self.offscreen_pos = (x as f32, y as f32);
@@ -1629,6 +1646,7 @@ impl LauncherApp {
16291646
action_cache: Vec::new(),
16301647
actions_by_id,
16311648
command_cache: Vec::new(),
1649+
command_search_cache: Vec::new(),
16321650
completion_index: None,
16331651
action_completion_dirty: false,
16341652
command_completion_dirty: false,
@@ -1747,14 +1765,15 @@ impl LauncherApp {
17471765
res.extend(self.actions.iter().cloned().map(|a| (a, 0.0)));
17481766
} else {
17491767
for (i, a) in self.actions.iter().enumerate() {
1750-
let (_, ref desc_lc) = self.action_cache[i];
1768+
let cached = &self.action_cache[i];
17511769
if self.is_exact_match_mode() {
17521770
let alias_match = self.alias_matches_lc(&a.action, query_lc);
1753-
let label_match = Self::matches_exact_display_text(&a.label, query);
1771+
let label_match = Self::matches_exact_display_text(cached, query_lc);
17541772
// Prefer displayed label text, but keep `desc`/aliases as supplemental
17551773
// filters for compatibility with existing query behavior.
1756-
let desc_match = desc_lc.contains(query_lc);
1757-
if label_match || desc_match || alias_match {
1774+
let desc_match = cached.desc_lc.contains(query_lc);
1775+
let action_match = cached.action_lc.contains(query_lc);
1776+
if label_match || desc_match || action_match || alias_match {
17581777
let score = if alias_match { 1.0 } else { 0.0 };
17591778
res.push((a.clone(), score));
17601779
}
@@ -1781,7 +1800,7 @@ impl LauncherApp {
17811800
);
17821801
let query_term = trimmed_lc.splitn(2, ' ').nth(1).unwrap_or("");
17831802
for a in plugin_results {
1784-
let desc_lc = a.desc.to_lowercase();
1803+
let cached = CachedSearchEntry::from_action(&a);
17851804
if self.is_exact_match_mode() {
17861805
if Self::should_bypass_exact_post_filter(trimmed, &a.action) {
17871806
// Plugin commands like `note today`/`note search <term>` already
@@ -1795,9 +1814,10 @@ impl LauncherApp {
17951814
res.push((a, 0.0));
17961815
} else {
17971816
let alias_match = self.alias_matches_lc(&a.action, query_term);
1798-
let label_match = Self::matches_exact_display_text(&a.label, query_term);
1799-
let desc_match = desc_lc.contains(query_term);
1800-
if label_match || desc_match || alias_match {
1817+
let label_match = Self::matches_exact_display_text(&cached, query_term);
1818+
let desc_match = cached.desc_lc.contains(query_term);
1819+
let action_match = cached.action_lc.contains(query_term);
1820+
if label_match || desc_match || action_match || alias_match {
18011821
let score = if alias_match { 1.0 } else { 0.0 };
18021822
res.push((a, score));
18031823
}
@@ -1825,24 +1845,25 @@ impl LauncherApp {
18251845
);
18261846

18271847
if plugin_results.is_empty() && !trimmed.is_empty() {
1828-
for a in self
1829-
.plugins
1830-
.commands_filtered(self.enabled_plugins.as_ref())
1848+
for (a, cached) in self
1849+
.command_cache
1850+
.iter()
1851+
.zip(self.command_search_cache.iter())
18311852
{
1832-
let desc_lc = a.desc.to_lowercase();
18331853
if self.is_exact_match_mode() {
18341854
let alias_match = self.alias_matches_lc(&a.action, trimmed_lc);
1835-
let label_match = Self::matches_exact_display_text(&a.label, trimmed);
1836-
let desc_match = desc_lc.contains(trimmed_lc);
1837-
if label_match || desc_match || alias_match {
1855+
let label_match = Self::matches_exact_display_text(cached, trimmed_lc);
1856+
let desc_match = cached.desc_lc.contains(trimmed_lc);
1857+
let action_match = cached.action_lc.contains(trimmed_lc);
1858+
if label_match || desc_match || action_match || alias_match {
18381859
let score = if alias_match { 1.0 } else { 0.0 };
1839-
res.push((a, score));
1860+
res.push((a.clone(), score));
18401861
}
18411862
} else {
18421863
let s1 = self.matcher.fuzzy_match(&a.label, trimmed);
18431864
let s2 = self.matcher.fuzzy_match(&a.desc, trimmed);
18441865
if let Some(score) = s1.max(s2) {
1845-
res.push((a, score as f32 * self.fuzzy_weight));
1866+
res.push((a.clone(), score as f32 * self.fuzzy_weight));
18461867
}
18471868
}
18481869
}
@@ -1859,7 +1880,7 @@ impl LauncherApp {
18591880
}
18601881
let query_term_lc = query_term.to_lowercase();
18611882
for a in plugin_results {
1862-
let desc_lc = a.desc.to_lowercase();
1883+
let cached = CachedSearchEntry::from_action(&a);
18631884
if self.is_exact_match_mode() {
18641885
if Self::should_bypass_exact_post_filter(trimmed, &a.action) {
18651886
// Explicit plugin commands can resolve into result lists/artifacts.
@@ -1872,9 +1893,10 @@ impl LauncherApp {
18721893
res.push((a, 0.0));
18731894
} else {
18741895
let alias_match = self.alias_matches_lc(&a.action, &query_term_lc);
1875-
let label_match = Self::matches_exact_display_text(&a.label, &query_term);
1876-
let desc_match = desc_lc.contains(&query_term_lc);
1877-
if label_match || desc_match || alias_match {
1896+
let label_match = Self::matches_exact_display_text(&cached, &query_term_lc);
1897+
let desc_match = cached.desc_lc.contains(&query_term_lc);
1898+
let action_match = cached.action_lc.contains(&query_term_lc);
1899+
if label_match || desc_match || action_match || alias_match {
18781900
let score = if alias_match { 1.0 } else { 0.0 };
18791901
res.push((a, score));
18801902
}
@@ -5667,17 +5689,49 @@ mod tests {
56675689
}
56685690

56695691
#[test]
5670-
fn exact_display_match_is_case_insensitive_substring() {
5671-
assert!(LauncherApp::matches_exact_display_text("eve", "Eve"));
5672-
assert!(LauncherApp::matches_exact_display_text("EVENING", "Eve"));
5692+
fn exact_display_match_uses_pre_normalized_query_substring() {
5693+
let cached = CachedSearchEntry {
5694+
label_lc: "testingeve123".into(),
5695+
desc_lc: String::new(),
5696+
action_lc: String::new(),
5697+
};
56735698
assert!(LauncherApp::matches_exact_display_text(
5674-
"testingEve123",
5675-
"eve"
5676-
));
5677-
assert!(!LauncherApp::matches_exact_display_text(
5678-
"testing123",
5679-
"Eve"
5699+
&cached,
5700+
&"Eve".to_lowercase()
56805701
));
5702+
assert!(LauncherApp::matches_exact_display_text(&cached, "eve"));
5703+
assert!(!LauncherApp::matches_exact_display_text(&cached, "night"));
5704+
}
5705+
5706+
#[test]
5707+
fn action_and_command_search_cache_is_normalized() {
5708+
let ctx = egui::Context::default();
5709+
let mut app = new_app(&ctx);
5710+
app.actions = Arc::new(vec![Action {
5711+
label: "MiXeD Label".into(),
5712+
desc: "MiXeD Desc".into(),
5713+
action: "Action:ID".into(),
5714+
args: None,
5715+
}]);
5716+
app.update_action_cache();
5717+
5718+
assert_eq!(app.action_cache.len(), 1);
5719+
assert_eq!(app.action_cache[0].label_lc, "mixed label");
5720+
assert_eq!(app.action_cache[0].desc_lc, "mixed desc");
5721+
assert_eq!(app.action_cache[0].action_lc, "action:id");
5722+
5723+
app.plugins.register(Box::new(ExactFilterPlugin));
5724+
app.update_command_cache();
5725+
assert_eq!(app.command_cache.len(), app.command_search_cache.len());
5726+
for (action, cached) in app
5727+
.command_cache
5728+
.iter()
5729+
.zip(app.command_search_cache.iter())
5730+
{
5731+
assert_eq!(cached.label_lc, action.label.to_lowercase());
5732+
assert_eq!(cached.desc_lc, action.desc.to_lowercase());
5733+
assert_eq!(cached.action_lc, action.action.to_lowercase());
5734+
}
56815735
}
56825736

56835737
#[test]
@@ -5730,6 +5784,89 @@ mod tests {
57305784
assert!(app.results.is_empty());
57315785
}
57325786

5787+
#[test]
5788+
fn update_paths_refreshes_command_search_cache_for_plugin_reload() {
5789+
struct CommandPlugin;
5790+
5791+
impl crate::plugin::Plugin for CommandPlugin {
5792+
fn search(&self, _query: &str) -> Vec<Action> {
5793+
Vec::new()
5794+
}
5795+
5796+
fn name(&self) -> &str {
5797+
"command-plugin"
5798+
}
5799+
5800+
fn description(&self) -> &str {
5801+
"command plugin"
5802+
}
5803+
5804+
fn capabilities(&self) -> &[&str] {
5805+
&[]
5806+
}
5807+
5808+
fn commands(&self) -> Vec<Action> {
5809+
vec![Action {
5810+
label: "PlUgIn Command".into(),
5811+
desc: "PlUgIn Desc".into(),
5812+
action: "Plugin:Command".into(),
5813+
args: None,
5814+
}]
5815+
}
5816+
}
5817+
5818+
let ctx = egui::Context::default();
5819+
let mut app = new_app(&ctx);
5820+
app.plugins.register(Box::new(CommandPlugin));
5821+
5822+
app.update_paths(
5823+
None, // plugin_dirs
5824+
None, // index_paths
5825+
None, // enabled_plugins
5826+
None, // enabled_capabilities
5827+
None, // offscreen_pos
5828+
None, // enable_toasts
5829+
None, // show_inline_errors
5830+
None, // show_error_toasts
5831+
None, // toast_duration
5832+
None, // fuzzy_weight
5833+
None, // usage_weight
5834+
None, // match_exact
5835+
None, // follow_mouse
5836+
None, // static_enabled
5837+
None, // static_pos
5838+
None, // static_size
5839+
None, // hide_after_run
5840+
None, // clear_query_after_run
5841+
None, // require_confirm_destructive
5842+
None, // timer_refresh
5843+
None, // disable_timer_updates
5844+
None, // preserve_command
5845+
None, // query_autocomplete
5846+
None, // net_refresh
5847+
None, // net_unit
5848+
None, // screenshot_dir
5849+
None, // screenshot_save_file
5850+
None, // screenshot_use_editor
5851+
None, // screenshot_auto_save
5852+
None, // always_on_top
5853+
None, // page_jump
5854+
None, // note_panel_default_size
5855+
None, // note_save_on_close
5856+
None, // note_always_overwrite
5857+
None, // note_images_as_links
5858+
None, // note_show_details
5859+
None, // note_more_limit
5860+
None, // show_dashboard_diagnostics
5861+
);
5862+
5863+
assert_eq!(app.command_cache.len(), 1);
5864+
assert_eq!(app.command_search_cache.len(), 1);
5865+
assert_eq!(app.command_search_cache[0].label_lc, "plugin command");
5866+
assert_eq!(app.command_search_cache[0].desc_lc, "plugin desc");
5867+
assert_eq!(app.command_search_cache[0].action_lc, "plugin:command");
5868+
}
5869+
57335870
#[test]
57345871
fn malformed_note_new_action_reports_error_and_search_recovers() {
57355872
let ctx = egui::Context::default();

0 commit comments

Comments
 (0)