Skip to content

Commit 986e394

Browse files
authored
Merge pull request #896 from multiplex55/codex/add-dirty-flags-and-optimize-caches
Debounce completion index rebuilds for action/command cache updates
2 parents e6b369e + 0093071 commit 986e394

1 file changed

Lines changed: 143 additions & 2 deletions

File tree

src/gui/mod.rs

Lines changed: 143 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ const SUBCOMMANDS: &[&str] = &[
125125
/// Prefix used to search user saved applications.
126126
pub const APP_PREFIX: &str = "app";
127127
const NOTE_SEARCH_DEBOUNCE: Duration = Duration::from_secs(1);
128+
const COMPLETION_REBUILD_DEBOUNCE: Duration = Duration::from_millis(120);
128129

129130
fn scale_ui<R>(ui: &mut egui::Ui, scale: f32, add_contents: impl FnOnce(&mut egui::Ui) -> R) -> R {
130131
ui.scope(|ui| {
@@ -407,6 +408,9 @@ pub struct LauncherApp {
407408
actions_by_id: HashMap<String, Action>,
408409
command_cache: Vec<Action>,
409410
completion_index: Option<Map<Vec<u8>>>,
411+
action_completion_dirty: bool,
412+
command_completion_dirty: bool,
413+
completion_rebuild_after: Option<Instant>,
410414
suggestions: Vec<String>,
411415
autocomplete_index: usize,
412416
pub query: String,
@@ -618,7 +622,8 @@ impl LauncherApp {
618622
.iter()
619623
.map(|a| (a.action.clone(), a.clone()))
620624
.collect();
621-
self.update_completion_index();
625+
self.action_completion_dirty = true;
626+
self.schedule_completion_rebuild();
622627
}
623628

624629
pub fn update_command_cache(&mut self) {
@@ -627,7 +632,37 @@ impl LauncherApp {
627632
.commands_filtered(self.enabled_plugins.as_ref());
628633
cmds.sort_by_cached_key(|a| a.label.to_lowercase());
629634
self.command_cache = cmds;
630-
self.update_completion_index();
635+
self.command_completion_dirty = true;
636+
self.schedule_completion_rebuild();
637+
}
638+
639+
fn schedule_completion_rebuild(&mut self) {
640+
self.completion_rebuild_after = Some(Instant::now() + COMPLETION_REBUILD_DEBOUNCE);
641+
self.completion_index = None;
642+
self.autocomplete_index = 0;
643+
self.suggestions.clear();
644+
}
645+
646+
fn maybe_rebuild_completion_index(&mut self, now: Instant) {
647+
let should_rebuild = self
648+
.completion_rebuild_after
649+
.is_some_and(|scheduled| now >= scheduled)
650+
&& (self.action_completion_dirty || self.command_completion_dirty);
651+
if should_rebuild {
652+
self.update_completion_index();
653+
self.action_completion_dirty = false;
654+
self.command_completion_dirty = false;
655+
self.completion_rebuild_after = None;
656+
}
657+
}
658+
659+
fn rebuild_completion_index_now(&mut self) {
660+
if self.action_completion_dirty || self.command_completion_dirty {
661+
self.update_completion_index();
662+
self.action_completion_dirty = false;
663+
self.command_completion_dirty = false;
664+
}
665+
self.completion_rebuild_after = None;
631666
}
632667

633668
pub fn process_watch_events(&mut self) {
@@ -735,6 +770,7 @@ impl LauncherApp {
735770
}
736771
}
737772
}
773+
self.maybe_rebuild_completion_index(Instant::now());
738774
}
739775

740776
fn update_completion_index(&mut self) {
@@ -1440,6 +1476,9 @@ impl LauncherApp {
14401476
actions_by_id,
14411477
command_cache: Vec::new(),
14421478
completion_index: None,
1479+
action_completion_dirty: false,
1480+
command_completion_dirty: false,
1481+
completion_rebuild_after: None,
14431482
suggestions: Vec::new(),
14441483
autocomplete_index: 0,
14451484
vim_mode: false,
@@ -1476,6 +1515,7 @@ impl LauncherApp {
14761515

14771516
app.update_action_cache();
14781517
app.update_command_cache();
1518+
app.rebuild_completion_index_now();
14791519
app.search();
14801520
crate::plugins::mouse_gestures::sync_enabled_plugins(app.enabled_plugins.as_ref());
14811521
app
@@ -5170,6 +5210,107 @@ mod tests {
51705210
std::env::set_current_dir(original_dir).unwrap();
51715211
}
51725212

5213+
#[test]
5214+
fn watch_event_bursts_delay_completion_rebuild_until_debounce_window() {
5215+
let _lock = TEST_MUTEX.lock().unwrap();
5216+
let dir = tempdir().unwrap();
5217+
let original_dir = std::env::current_dir().unwrap();
5218+
std::env::set_current_dir(dir.path()).unwrap();
5219+
5220+
std::fs::write(
5221+
"actions.json",
5222+
serde_json::to_string_pretty(&serde_json::json!([
5223+
{
5224+
"label": "Initial App",
5225+
"desc": "demo",
5226+
"action": "initial:app",
5227+
"args": null
5228+
}
5229+
]))
5230+
.unwrap(),
5231+
)
5232+
.unwrap();
5233+
5234+
let ctx = egui::Context::default();
5235+
let mut app = new_app(&ctx);
5236+
app.rebuild_completion_index_now();
5237+
assert!(app.completion_index.is_some());
5238+
5239+
std::fs::write(
5240+
"actions.json",
5241+
serde_json::to_string_pretty(&serde_json::json!([
5242+
{
5243+
"label": "Updated App",
5244+
"desc": "demo",
5245+
"action": "updated:app",
5246+
"args": null
5247+
}
5248+
]))
5249+
.unwrap(),
5250+
)
5251+
.unwrap();
5252+
5253+
send_event(WatchEvent::Actions);
5254+
send_event(WatchEvent::Actions);
5255+
app.process_watch_events();
5256+
5257+
assert!(app.completion_index.is_none());
5258+
assert!(app.action_completion_dirty);
5259+
5260+
let scheduled = app
5261+
.completion_rebuild_after
5262+
.expect("rebuild should be scheduled");
5263+
app.maybe_rebuild_completion_index(scheduled - Duration::from_millis(1));
5264+
assert!(app.completion_index.is_none());
5265+
5266+
app.maybe_rebuild_completion_index(scheduled + Duration::from_millis(1));
5267+
assert!(app.completion_index.is_some());
5268+
assert!(!app.action_completion_dirty);
5269+
assert!(!app.command_completion_dirty);
5270+
assert!(app.completion_rebuild_after.is_none());
5271+
5272+
std::env::set_current_dir(original_dir).unwrap();
5273+
}
5274+
5275+
#[test]
5276+
fn completion_suggestions_clear_until_rebuild_and_match_latest_entries() {
5277+
let _lock = TEST_MUTEX.lock().unwrap();
5278+
let ctx = egui::Context::default();
5279+
let mut app = new_app(&ctx);
5280+
app.query_autocomplete = true;
5281+
5282+
app.actions = Arc::new(vec![Action {
5283+
label: "Old App".into(),
5284+
desc: "demo".into(),
5285+
action: "old:app".into(),
5286+
args: None,
5287+
}]);
5288+
app.update_action_cache();
5289+
app.rebuild_completion_index_now();
5290+
5291+
app.query = "app ".into();
5292+
app.update_suggestions();
5293+
assert!(app.suggestions.iter().any(|s| s == "app old app"));
5294+
5295+
app.actions = Arc::new(vec![Action {
5296+
label: "New App".into(),
5297+
desc: "demo".into(),
5298+
action: "new:app".into(),
5299+
args: None,
5300+
}]);
5301+
app.update_action_cache();
5302+
5303+
assert!(app.completion_index.is_none());
5304+
assert!(app.suggestions.is_empty());
5305+
5306+
app.maybe_rebuild_completion_index(
5307+
Instant::now() + COMPLETION_REBUILD_DEBOUNCE + Duration::from_millis(1),
5308+
);
5309+
5310+
assert!(app.suggestions.iter().all(|s| s != "app old app"));
5311+
assert!(app.suggestions.iter().any(|s| s == "app new app"));
5312+
}
5313+
51735314
#[test]
51745315
fn open_note_panel_reuses_existing_panel_for_same_slug() {
51755316
let _lock = TEST_MUTEX.lock().unwrap();

0 commit comments

Comments
 (0)