use std::time::Instant;
use crate::ui::app::{
ActiveFilter, App, ShellAction, queue_task, refresh_results, select_first_change,
sync_contextual_view,
};
use crate::ui::async_task::TaskSpec;
use crate::ui::perf;
use crate::ui::store::navigation::{NavigationAction, navigation_primary_view};
use crate::ui::store::navigation_tree::{build_change_navigator, build_navigator};
use crate::ui::workspace_read;
use code_moniker_workspace::live::{WorkspaceLiveEvent, WorkspaceLiveRefreshPlan};
use code_moniker_workspace::registry::WorkspaceStaleness;
#[derive(Clone, Copy)]
enum StoreChangeKind {
FileCatalog,
Reloaded,
Notes,
}
pub(in crate::ui) fn handle_store_event(app: &mut App, event: WorkspaceLiveEvent) {
handle_store_event_sync(app, event);
}
pub(in crate::ui) fn handle_store_event_sync(app: &mut App, event: WorkspaceLiveEvent) {
let plan = WorkspaceLiveRefreshPlan::from_event(event);
if should_queue_workspace_live_plan(&plan) {
if should_defer_workspace_live_plan(app) {
mark_workspace_stale(app, plan);
} else {
queue_workspace_live_plan(app, plan);
}
return;
}
if plan.includes_notes() {
refresh_workspace_notes(app);
}
}
fn should_queue_workspace_live_plan(plan: &WorkspaceLiveRefreshPlan) -> bool {
plan.requires_rescan() || !plan.source_paths().is_empty() || plan.includes_git_base()
}
fn should_defer_workspace_live_plan(app: &App) -> bool {
crate::ui::app::live_refresh_on_demand(app)
&& crate::ui::app::store(app).queries().snapshot().is_some()
}
fn mark_workspace_stale(app: &mut App, plan: WorkspaceLiveRefreshPlan) {
if plan.includes_notes() {
refresh_workspace_notes(app);
}
let pending = app
.runtime
.pending_live_plan
.take()
.unwrap_or_default()
.coalesce(plan.without_notes());
let summary = WorkspaceStaleness::plan_summary(&pending);
app.runtime.pending_live_plan = Some(pending);
crate::ui::app::set_status(app, format!("workspace stale ({summary}) — R refreshes"));
}
pub(in crate::ui) fn refresh_workspace_on_demand(app: &mut App) {
let Some(plan) = app.runtime.pending_live_plan.take() else {
crate::ui::app::set_status(app, "workspace index is fresh");
return;
};
if !queue_workspace_live_plan(app, plan.clone()) {
app.runtime.pending_live_plan = Some(plan);
}
}
fn refresh_workspace_notes(app: &mut App) {
match crate::ui::app::reload_notes(app) {
Ok(()) => {
refresh_ui_after_store_change(
app,
StoreChangeKind::Notes,
"notes refreshed".to_string(),
);
}
Err(error) => {
crate::ui::app::set_status(app, format!("notes reload failed: {error:#}"));
}
}
}
fn queue_workspace_live_plan(app: &mut App, plan: WorkspaceLiveRefreshPlan) -> bool {
let Some(snapshot) = crate::ui::app::store(app).queries().snapshot_arc() else {
return queue_workspace_rescan(app);
};
let task = TaskSpec::refresh_workspace_live_plan(
crate::ui::app::store_options(app),
app.workspace.cache().clone(),
snapshot,
plan,
);
if queue_task(app, task) {
crate::ui::app::set_status(app, "live workspace refresh queued in background");
return true;
}
false
}
fn queue_workspace_rescan(app: &mut App) -> bool {
if queue_task(
app,
TaskSpec::load_symbol_index(crate::ui::app::store_options(app)),
) {
crate::ui::app::set_status(app, "workspace rescan queued in background");
return true;
}
false
}
pub(in crate::ui) fn apply_file_catalog_store(app: &mut App, status: String) {
refresh_ui_after_store_change(app, StoreChangeKind::FileCatalog, status);
}
pub(in crate::ui) fn apply_reloaded_store(app: &mut App, status: String) {
refresh_ui_after_store_change(app, StoreChangeKind::Reloaded, status);
}
fn refresh_ui_after_store_change(app: &mut App, kind: StoreChangeKind, status: String) {
let total_started = Instant::now();
if matches!(
kind,
StoreChangeKind::FileCatalog | StoreChangeKind::Reloaded
) {
let started = Instant::now();
app.runtime.watch_roots_update = Some(crate::ui::app::store_watch_roots(app));
app.refresh_header_search_options();
perf::record(
"store_refresh.watch_search",
started.elapsed(),
status.as_str(),
);
}
let reset = matches!(crate::ui::app::active_filter(app), ActiveFilter::Change)
&& navigation_primary_view(app.app_store.navigation())
.rows
.is_empty();
if matches!(kind, StoreChangeKind::Reloaded) {
let started = Instant::now();
refresh_active_filter_after_store_reload(app);
perf::record(
"store_refresh.active_filter",
started.elapsed(),
status.as_str(),
);
}
let started = Instant::now();
let explorer = build_navigator(
crate::ui::app::store(app),
&crate::ui::app::store_options(app).paths,
);
perf::record(
"store_refresh.build_navigator",
started.elapsed(),
status.as_str(),
);
let started = Instant::now();
let change = build_change_navigator(crate::ui::app::store(app));
perf::record(
"store_refresh.build_change_navigator",
started.elapsed(),
status.as_str(),
);
let started = Instant::now();
app.app_store
.dispatch_navigation(NavigationAction::ReplaceModels { explorer, change });
perf::record(
"store_refresh.replace_models",
started.elapsed(),
status.as_str(),
);
let started = Instant::now();
refresh_results(app, matches!(kind, StoreChangeKind::FileCatalog) || reset);
perf::record(
"store_refresh.refresh_results",
started.elapsed(),
status.as_str(),
);
if reset && !matches!(kind, StoreChangeKind::FileCatalog) {
let started = Instant::now();
select_first_change(app);
perf::record(
"store_refresh.select_first_change",
started.elapsed(),
status.as_str(),
);
}
let started = Instant::now();
sync_contextual_view(app);
perf::record(
"store_refresh.sync_contextual_view",
started.elapsed(),
status.as_str(),
);
crate::ui::app::set_status(app, status);
perf::record(
"store_refresh.total",
total_started.elapsed(),
crate::ui::app::status(app),
);
}
fn refresh_active_filter_after_store_reload(app: &mut App) {
let active_filter = match crate::ui::app::active_filter(app) {
ActiveFilter::HeaderSearch(results) => ActiveFilter::HeaderSearch(
app.header_search_results(&results.text, &results.langs, &results.kind_filters),
),
ActiveFilter::None => ActiveFilter::None,
ActiveFilter::Change => ActiveFilter::Change,
};
crate::ui::app::dispatch_shell(app, ShellAction::ReplaceActiveFilter(active_filter));
refresh_usage_lens_after_store_reload(app);
}
fn refresh_usage_lens_after_store_reload(app: &mut App) {
let Some(focus) = crate::ui::app::usage_lens(app).cloned() else {
return;
};
let Some(focus) = workspace_read::usage_focus_for_target(
crate::ui::app::store(app),
focus.target,
focus.label,
) else {
return;
};
let visible_defs = focus.contexts.clone();
let expand_symbols = visible_defs.len() <= 200;
crate::ui::app::dispatch_shell(app, ShellAction::SetUsageLens(Some(focus)));
app.app_store
.dispatch_navigation(NavigationAction::SetUsageLens {
visible_defs,
reset_expansion: false,
expand_symbols,
});
}