use std::collections::HashMap;
use std::collections::HashSet;
use std::path::Path;
use std::thread;
use std::time::Duration;
use std::time::Instant;
use super::App;
use super::ExpandKey::Group;
use super::ExpandKey::Node;
use super::ExpandKey::Worktree;
use super::ExpandKey::WorktreeGroup;
use super::types::ConfigFileStamp;
use super::types::DiskCacheBuildResult;
use super::types::FitWidthsBuildResult;
use super::types::PollBackgroundStats;
use super::types::ScanPhase;
use super::types::StartupPhaseTracker;
use crate::ci::OwnerRepo;
use crate::config::CargoPortConfig;
use crate::constants::SERVICE_RETRY_SECS;
use crate::http::ServiceKind;
use crate::http::ServiceSignal;
use crate::keymap::KeymapError;
use crate::keymap::KeymapErrorReason::ParseError;
use crate::lint;
use crate::lint::LintStatus;
use crate::lint::RegisterProjectRequest;
use crate::project::AbsolutePath;
use crate::project::GitInfo;
use crate::project::GitRepoPresence;
use crate::project::LanguageStats;
use crate::project::LocalGitState;
use crate::project::MemberGroup;
use crate::project::ProjectFields;
use crate::project::RootItem;
use crate::project::Visibility::Deleted;
use crate::project::Visibility::Visible;
use crate::project_list::ProjectList;
use crate::scan;
use crate::scan::BackgroundMsg;
use crate::tui::columns::ResolvedWidths;
use crate::tui::config_reload;
use crate::tui::constants::STARTUP_PHASE_DISK;
use crate::tui::constants::STARTUP_PHASE_GIT;
use crate::tui::constants::STARTUP_PHASE_GITHUB;
use crate::tui::constants::STARTUP_PHASE_LINT;
use crate::tui::terminal::CiFetchMsg;
use crate::tui::terminal::CleanMsg;
use crate::tui::terminal::ExampleMsg;
use crate::tui::toasts;
use crate::tui::toasts::ToastStyle::Error;
use crate::tui::toasts::TrackedItem;
use crate::tui::types::PaneId;
use crate::watcher;
use crate::watcher::WatchRequest;
use crate::watcher::WatcherMsg;
#[derive(Clone)]
struct LegacyRootExpansion {
root_path: AbsolutePath,
old_node_index: usize,
had_children: bool,
named_groups: Vec<usize>,
}
impl App {
#[cfg(test)]
pub(in super::super) fn apply_tree_build(&mut self, projects: ProjectList) {
let selected_path = self
.selected_project_path()
.map(AbsolutePath::from)
.or_else(|| self.selection_paths.last_selected.clone());
let should_focus_project_list = false;
self.projects = projects;
self.dirty.finder.mark_dirty();
self.dirty.rows.mark_dirty();
self.dirty.disk_cache.mark_dirty();
self.dirty.fit_widths.mark_dirty();
self.recompute_cargo_active_paths();
self.prune_inactive_project_state();
self.register_lint_for_root_items();
self.refresh_lint_runs_from_disk();
self.data_generation += 1;
self.detail_generation += 1;
if let Some(path) = selected_path {
self.select_project_in_tree(path.as_path());
} else if !self.projects.is_empty() {
self.pane_manager.pane_mut(PaneId::ProjectList).set_pos(0);
}
if should_focus_project_list {
self.focus_pane(PaneId::ProjectList);
}
self.sync_selected_project();
}
pub(in super::super) fn config_file_stamp(path: &Path) -> Option<ConfigFileStamp> {
let metadata = std::fs::metadata(path).ok()?;
Some(ConfigFileStamp {
modified: metadata.modified().ok(),
len: metadata.len(),
})
}
pub(in super::super) fn sync_config_watch_state(&mut self) {
self.config_last_seen = self
.config_path
.as_deref()
.and_then(Self::config_file_stamp);
}
pub(in super::super) fn record_config_reload_failure(&mut self, err: &str) {
self.status_flash = Some((
"Config reload failed; keeping previous settings".to_string(),
Instant::now(),
));
self.show_timed_toast("Config reload failed", err.to_string());
}
pub(in super::super) fn load_initial_keymap(&mut self) {
let vim_mode = self.current_config.tui.navigation_keys;
let result = crate::keymap::load_keymap(vim_mode);
self.current_keymap = result.keymap;
self.sync_keymap_watch_state();
if !result.errors.is_empty() {
self.show_keymap_diagnostics(&result.errors);
}
if !result.missing_actions.is_empty() {
self.show_timed_toast(
"Keymap updated",
format!(
"Defaults written for missing entries:\n{}",
result.missing_actions.join(", ")
),
);
}
}
pub(in super::super) fn maybe_reload_keymap_from_disk(&mut self) {
let current_stamp = self
.keymap_path
.as_deref()
.and_then(Self::config_file_stamp);
if current_stamp == self.keymap_last_seen {
return;
}
self.keymap_last_seen = current_stamp;
let Some(path) = &self.keymap_path else {
return;
};
let contents = match std::fs::read_to_string(path) {
Ok(c) => c,
Err(e) => {
self.show_keymap_diagnostics(&[crate::keymap::KeymapError {
scope: String::new(),
action: String::new(),
key: String::new(),
reason: ParseError(format!("read error: {e}")),
}]);
return;
},
};
let vim_mode = self.current_config.tui.navigation_keys;
let result = crate::keymap::load_keymap_from_str(&contents, vim_mode);
self.current_keymap = result.keymap;
if result.errors.is_empty() {
self.dismiss_keymap_diagnostics();
} else {
self.show_keymap_diagnostics(&result.errors);
}
if !result.missing_actions.is_empty() {
if let Some(path) = &self.keymap_path {
let content =
crate::keymap::ResolvedKeymap::default_toml_from(&self.current_keymap);
let _ = std::fs::write(path, content);
self.sync_keymap_watch_state();
}
self.show_timed_toast(
"Keymap updated",
format!(
"Defaults written for missing entries:\n{}",
result.missing_actions.join(", ")
),
);
}
}
pub(in super::super) fn sync_keymap_stamp(&mut self) { self.sync_keymap_watch_state(); }
fn sync_keymap_watch_state(&mut self) {
self.keymap_last_seen = self
.keymap_path
.as_deref()
.and_then(Self::config_file_stamp);
}
fn show_keymap_diagnostics(&mut self, errors: &[KeymapError]) {
self.dismiss_keymap_diagnostics();
let body = errors
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join("\n");
let action_path = self.keymap_path.clone();
let id = self.toasts.push_persistent(
"Keymap errors (using defaults)",
body,
Error,
action_path,
1,
);
self.keymap_diagnostics_id = Some(id);
let toast_len = self.active_toasts().len();
self.pane_manager
.pane_mut(PaneId::Toasts)
.set_len(toast_len);
}
fn dismiss_keymap_diagnostics(&mut self) {
if let Some(id) = self.keymap_diagnostics_id.take() {
self.toasts.dismiss(id);
}
}
pub(in super::super) fn maybe_reload_config_from_disk(&mut self) {
let current_stamp = self
.config_path
.as_deref()
.and_then(Self::config_file_stamp);
if current_stamp == self.config_last_seen {
return;
}
self.config_last_seen = current_stamp;
let reload_result = self
.config_path
.as_deref()
.map_or_else(crate::config::try_load, crate::config::try_load_from_path);
match reload_result {
Ok(cfg) => {
self.apply_config(&cfg);
self.sync_config_watch_state();
self.show_timed_toast("Settings", "Reloaded from disk");
},
Err(err) => self.record_config_reload_failure(&err),
}
}
pub(in super::super) fn save_and_apply_config(
&mut self,
cfg: &CargoPortConfig,
) -> Result<(), String> {
crate::config::save(cfg)?;
self.apply_config(cfg);
self.sync_config_watch_state();
Ok(())
}
pub(in super::super) fn apply_config(&mut self, cfg: &CargoPortConfig) {
if self.current_config == *cfg {
return;
}
let actions = config_reload::collect_reload_actions(
&self.current_config,
cfg,
config_reload::ReloadContext {
scan_complete: self.is_scan_complete(),
has_cached_non_rust: self.has_cached_non_rust_projects(),
},
);
crate::config::set_active_config(cfg);
self.current_config = cfg.clone();
if !self.discovery_shimmer_enabled() {
self.discovery_shimmers.clear();
}
if actions.refresh_lint_runtime {
self.refresh_lint_runtime_from_config(cfg);
}
if actions.rescan {
self.rescan();
self.force_settings_if_unconfigured();
} else {
if actions.refresh_lint_runtime {
self.respawn_watcher_and_register_existing_projects();
}
if actions.rebuild_tree {
self.projects
.regroup_members(&self.current_config.tui.inline_dirs);
self.refresh_derived_state();
}
}
}
pub(in super::super) fn refresh_lint_runtime_from_config(&mut self, cfg: &CargoPortConfig) {
let lint_spawn = lint::spawn(cfg, self.bg_tx.clone());
self.lint_runtime = lint_spawn.handle;
self.clear_all_lint_state();
self.running_lint_paths.clear();
self.sync_running_lint_toast();
self.sync_lint_runtime_projects();
self.refresh_lint_runs_from_disk();
self.cached_fit_widths = ResolvedWidths::new(self.lint_enabled());
self.dirty.rows.mark_dirty();
self.dirty.fit_widths.mark_dirty();
self.data_generation += 1;
self.detail_generation += 1;
if let Some(warning) = lint_spawn.warning {
self.status_flash = Some((warning.clone(), Instant::now()));
self.show_timed_toast("Lint runtime", warning);
}
}
pub(in super::super) fn respawn_watcher(&mut self) {
let watch_roots = crate::scan::resolve_include_dirs(&self.current_config.tui.include_dirs);
self.watch_tx = watcher::spawn_watcher(
watch_roots,
self.bg_tx.clone(),
self.ci_run_count(),
self.include_non_rust(),
self.http_client.clone(),
self.lint_runtime.clone(),
);
}
pub(in super::super) fn register_existing_projects(&self) {
self.projects.for_each_leaf(|item| {
self.register_item_background_services(item);
});
}
pub(in super::super) fn finish_watcher_registration_batch(&self) {
let _ = self.watch_tx.send(WatcherMsg::InitialRegistrationComplete);
}
fn respawn_watcher_and_register_existing_projects(&mut self) {
self.respawn_watcher();
self.register_existing_projects();
self.finish_watcher_registration_batch();
}
pub(in super::super) fn refresh_lint_runs_from_disk(&mut self) {
let mut paths = Vec::new();
self.projects.for_each_leaf_path(|path, is_rust| {
if is_rust {
paths.push(path.to_path_buf());
}
});
for path in &paths {
let runs = crate::lint::read_history(path);
if let Some(lr) = self.projects.lint_at_path_mut(path) {
lr.set_runs(runs);
}
}
self.refresh_lint_cache_usage_from_disk();
}
pub(in super::super) fn reload_lint_history(&mut self, project_path: &Path) {
if !self.is_cargo_active_path(project_path) {
return;
}
let runs = crate::lint::read_history(project_path);
if let Some(lr) = self.projects.lint_at_path_mut(project_path) {
lr.set_runs(runs);
}
}
pub(in super::super) fn refresh_lint_cache_usage_from_disk(&mut self) {
let cache_size_bytes = self.current_config.lint.cache_size_bytes().unwrap_or(None);
self.lint_cache_usage = crate::lint::retained_cache_usage(cache_size_bytes);
}
fn register_background_services_for_tree(&self) {
let started = Instant::now();
let mut count = 0usize;
self.projects.for_each_leaf(|item| {
self.register_item_background_services(item);
count += 1;
});
tracing::info!(
elapsed_ms = crate::perf_log::ms(started.elapsed().as_millis()),
count,
"register_background_services_for_tree"
);
}
pub(in super::super) fn register_item_background_services(&self, item: &RootItem) {
let started = Instant::now();
let abs_path = AbsolutePath::from(item.path().to_path_buf());
let repo_root = crate::project::git_repo_root(&abs_path);
let has_repo_root = repo_root.is_some();
let _ = self.watch_tx.send(WatcherMsg::Register(WatchRequest {
project_label: abs_path.to_string_lossy().to_string(),
abs_path: abs_path.clone(),
repo_root,
}));
tracing::info!(
elapsed_ms = crate::perf_log::ms(started.elapsed().as_millis()),
path = %item.display_path(),
has_repo_root,
"app_register_project_background_services"
);
}
fn schedule_startup_project_details(&self) {
let tx = self.bg_tx.clone();
let task_ctx = std::sync::Arc::new(crate::scan::FetchContext {
client: self.http_client.clone(),
});
self.projects.for_each_leaf(|item| {
let abs_path = item.path().to_path_buf();
let display_path = item.display_path().into_string();
let project_name = item
.is_rust()
.then(|| item.name().map(str::to_string))
.flatten()
.filter(|_| {
self.projects
.rust_info_at_path(item.path())
.is_some_and(|r| r.cargo().publishable())
});
let repo_presence = if crate::project::git_repo_root(&abs_path).is_some() {
GitRepoPresence::InRepo
} else {
GitRepoPresence::OutsideRepo
};
let tx = tx.clone();
let task_ctx = std::sync::Arc::clone(&task_ctx);
rayon::spawn(move || {
let request = crate::scan::ProjectDetailRequest {
tx: &tx,
ctx: task_ctx.as_ref(),
_project_path: display_path.as_str(),
abs_path: &abs_path,
project_name: project_name.as_deref(),
repo_presence,
};
crate::scan::fetch_project_details(&request);
});
});
self.schedule_member_crates_io_fetches();
}
fn schedule_member_crates_io_fetches(&self) {
let tx = self.bg_tx.clone();
let client = self.http_client.clone();
let mut members: Vec<(AbsolutePath, String)> = Vec::new();
for item in &self.projects {
let groups: Vec<&MemberGroup> = match item {
crate::project::RootItem::Rust(crate::project::RustProject::Workspace(ws)) => {
ws.groups().iter().collect()
},
crate::project::RootItem::Worktrees(
crate::project::WorktreeGroup::Workspaces {
primary, linked, ..
},
) => std::iter::once(primary)
.chain(linked.iter())
.flat_map(|ws| ws.groups().iter())
.collect(),
_ => continue,
};
for group in groups {
for member in group.members() {
if !member.cargo().publishable() {
continue;
}
if let Some(name) = member.name() {
members.push((member.path().clone(), name.to_string()));
}
}
}
}
if members.is_empty() {
return;
}
rayon::spawn(move || {
for (path, name) in members {
let (info, signal) = client.fetch_crates_io_info(&name);
crate::scan::emit_service_signal(&tx, signal);
if let Some(info) = info {
let _ = tx.send(crate::scan::BackgroundMsg::CratesIoVersion {
path,
version: info.version,
downloads: info.downloads,
});
}
}
});
}
pub(in super::super) fn schedule_git_first_commit_refreshes(&self) {
let tx = self.bg_tx.clone();
let mut projects_by_repo: HashMap<AbsolutePath, Vec<AbsolutePath>> = HashMap::new();
self.projects.for_each_leaf_path(|path, _| {
let abs_path = AbsolutePath::from(path);
let Some(repo_root) = crate::project::git_repo_root(&abs_path) else {
return;
};
projects_by_repo
.entry(repo_root)
.or_default()
.push(abs_path);
});
std::thread::spawn(move || {
for (repo_root, paths) in projects_by_repo {
let started = Instant::now();
let first_commit = crate::project::detect_first_commit(&repo_root);
tracing::info!(
elapsed_ms = crate::perf_log::ms(started.elapsed().as_millis()),
repo_root = %repo_root.display(),
rows = paths.len(),
found = first_commit.is_some(),
"git_first_commit_fetch"
);
for path in paths {
let _ = tx.send(BackgroundMsg::GitFirstCommit {
path,
first_commit: first_commit.clone(),
});
}
}
});
}
fn lint_runtime_root_entries(&self) -> Vec<(AbsolutePath, bool)> {
let mut seen = HashSet::new();
let mut entries = Vec::new();
for item in &self.projects {
let items: Vec<(&AbsolutePath, bool)> = match item {
crate::project::RootItem::Worktrees(
crate::project::WorktreeGroup::Workspaces {
primary, linked, ..
},
) => std::iter::once(primary)
.chain(linked.iter())
.map(|p| (p.path(), true))
.collect(),
crate::project::RootItem::Worktrees(crate::project::WorktreeGroup::Packages {
primary,
linked,
..
}) => std::iter::once(primary)
.chain(linked.iter())
.map(|p| (p.path(), true))
.collect(),
_ => vec![(item.path(), item.is_rust())],
};
for (path, is_rust) in items {
let owned = path.clone();
if seen.insert(owned.clone()) {
entries.push((owned, is_rust));
}
}
}
entries
}
pub(in super::super) fn lint_runtime_projects_snapshot(&self) -> Vec<RegisterProjectRequest> {
if !self.is_scan_complete() {
return Vec::new();
}
self.lint_runtime_root_entries()
.into_iter()
.filter(|(path, _)| !self.is_deleted(path) && self.is_cargo_active_path(path))
.map(|(abs_path, is_rust)| RegisterProjectRequest {
project_label: crate::project::home_relative_path(&abs_path),
abs_path,
is_rust,
})
.collect()
}
pub(in super::super) fn sync_lint_runtime_projects(&self) {
let Some(runtime) = &self.lint_runtime else {
return;
};
runtime.sync_projects(self.lint_runtime_projects_snapshot());
}
fn register_lint_for_root_items(&self) -> usize {
let Some(runtime) = &self.lint_runtime else {
return 0;
};
let mut count = 0;
for item in &self.projects {
match item {
RootItem::Rust(crate::project::RustProject::Workspace(ws)) => {
runtime.register_project(crate::lint::RegisterProjectRequest {
project_label: ws.display_path().into_string(),
abs_path: ws.path().clone(),
is_rust: true,
});
count += 1;
},
RootItem::Rust(crate::project::RustProject::Package(pkg)) => {
runtime.register_project(crate::lint::RegisterProjectRequest {
project_label: pkg.display_path().into_string(),
abs_path: pkg.path().clone(),
is_rust: true,
});
count += 1;
},
RootItem::Worktrees(crate::project::WorktreeGroup::Workspaces {
primary,
linked,
..
}) => {
for ws in std::iter::once(primary).chain(linked.iter()) {
runtime.register_project(crate::lint::RegisterProjectRequest {
project_label: ws.display_path().into_string(),
abs_path: ws.path().clone(),
is_rust: true,
});
count += 1;
}
},
RootItem::Worktrees(crate::project::WorktreeGroup::Packages {
primary,
linked,
..
}) => {
for pkg in std::iter::once(primary).chain(linked.iter()) {
runtime.register_project(crate::lint::RegisterProjectRequest {
project_label: pkg.display_path().into_string(),
abs_path: pkg.path().clone(),
is_rust: true,
});
count += 1;
}
},
RootItem::NonRust(_) => {},
}
}
tracing::info!(count, "lint_register_root_items");
count
}
fn register_lint_project_if_eligible(&self, item: &RootItem) {
if !item.is_rust() {
tracing::info!(reason = "not_rust", path = %item.display_path(), "lint_register_skip");
return;
}
let path = item.path();
let mut is_member = false;
self.projects.for_each_leaf(|existing| {
if matches!(
existing,
RootItem::Rust(crate::project::RustProject::Workspace(_))
) && existing.path() != path
&& path.starts_with(existing.path())
{
is_member = true;
}
});
if is_member {
tracing::info!(reason = "workspace_member", path = %item.display_path(), "lint_register_skip");
return;
}
let Some(runtime) = &self.lint_runtime else {
tracing::info!(reason = "no_runtime", path = %item.display_path(), "lint_register_skip");
return;
};
tracing::info!(path = %item.display_path(), "lint_register");
runtime.register_project(crate::lint::RegisterProjectRequest {
project_label: item.display_path().into_string(),
abs_path: path.clone(),
is_rust: true,
});
}
fn register_lint_for_path(&self, path: &Path) {
if let Some(item) = self.projects.iter().find(|i| i.path() == path) {
self.register_lint_project_if_eligible(item);
}
}
pub(in super::super) fn initialize_startup_phase_tracker(&mut self) {
let disk_expected = super::snapshots::initial_disk_batch_count(&self.projects);
let git_expected = self
.projects
.git_directories()
.into_iter()
.collect::<HashSet<_>>();
let git_seen = self
.projects
.iter()
.filter(|item| item.git_info().is_some())
.filter_map(RootItem::git_directory)
.collect::<HashSet<_>>();
self.scan.startup_phases.disk_complete_at = None;
self.scan.startup_phases.scan_complete_at = Some(Instant::now());
self.scan.startup_phases.disk_expected = Some(disk_expected);
self.scan.startup_phases.git_expected = git_expected;
self.scan.startup_phases.git_seen = git_seen;
self.scan.startup_phases.git_complete_at = None;
self.scan.startup_phases.repo_expected.clear();
self.scan.startup_phases.repo_seen.clear();
self.scan.startup_phases.repo_complete_at = None;
self.scan.startup_phases.git_toast = None;
self.scan.startup_phases.repo_toast = None;
self.scan.startup_phases.startup_toast = None;
self.scan.startup_phases.lint_expected = Some(HashSet::new());
self.scan.startup_phases.lint_seen_terminal.clear();
self.scan.startup_phases.lint_complete_at = None;
self.scan.startup_phases.startup_complete_at = None;
let startup_items = vec![
TrackedItem {
label: STARTUP_PHASE_DISK.to_string(),
key: STARTUP_PHASE_DISK.into(),
started_at: Some(Instant::now()),
completed_at: None,
},
TrackedItem {
label: STARTUP_PHASE_GIT.to_string(),
key: STARTUP_PHASE_GIT.into(),
started_at: Some(Instant::now()),
completed_at: None,
},
TrackedItem {
label: STARTUP_PHASE_LINT.to_string(),
key: STARTUP_PHASE_LINT.into(),
started_at: Some(Instant::now()),
completed_at: None,
},
];
let task_id = self.start_task_toast("Startup", "");
self.set_task_tracked_items(task_id, &startup_items);
self.scan.startup_phases.startup_toast = Some(task_id);
let git_items = Self::tracked_items_for_startup(
&self.scan.startup_phases.git_expected,
&self.scan.startup_phases.git_seen,
);
if !git_items.is_empty() {
let body = self.startup_git_toast_body();
let task_id = self.start_task_toast("Scanning local git repos", &body);
self.set_task_tracked_items(task_id, &git_items);
self.scan.startup_phases.git_toast = Some(task_id);
}
let repo_items = Self::tracked_repo_items_for_startup(
&self.scan.startup_phases.repo_expected,
&self.scan.startup_phases.repo_seen,
);
if !repo_items.is_empty() {
let body = self.startup_repo_toast_body();
let task_id = self.start_task_toast("Retrieving GitHub repo details", &body);
self.set_task_tracked_items(task_id, &repo_items);
self.scan.startup_phases.repo_toast = Some(task_id);
}
tracing::info!(
disk_expected = self.scan.startup_phases.disk_expected.unwrap_or(0),
git_expected = self.scan.startup_phases.git_expected.len(),
repo_expected = self.scan.startup_phases.repo_expected.len(),
lint_expected = self
.scan
.startup_phases
.lint_expected
.as_ref()
.map_or(0, HashSet::len),
"startup_phase_plan"
);
self.maybe_log_startup_phase_completions();
}
pub(in super::super) fn maybe_log_startup_phase_completions(&mut self) {
let Some(scan_complete_at) = self.scan.startup_phases.scan_complete_at else {
return;
};
let now = Instant::now();
self.maybe_complete_startup_disk(now, scan_complete_at);
self.maybe_complete_startup_git(now, scan_complete_at);
self.maybe_complete_startup_repo(now, scan_complete_at);
self.maybe_complete_startup_lints(now, scan_complete_at);
self.maybe_complete_startup_ready(now, scan_complete_at);
}
pub(in super::super) fn maybe_complete_startup_disk(
&mut self,
now: Instant,
scan_complete_at: Instant,
) {
if self.scan.startup_phases.disk_complete_at.is_none()
&& self
.scan
.startup_phases
.disk_expected
.is_some_and(|expected| self.scan.startup_phases.disk_seen.len() >= expected)
{
self.scan.startup_phases.disk_complete_at = Some(now);
if let Some(toast) = self.scan.startup_phases.startup_toast {
self.mark_tracked_item_completed(toast, STARTUP_PHASE_DISK);
}
tracing::info!(
phase = "disk_applied",
since_scan_complete_ms =
crate::perf_log::ms(now.duration_since(scan_complete_at).as_millis()),
seen = self.scan.startup_phases.disk_seen.len(),
expected = self.scan.startup_phases.disk_expected.unwrap_or(0),
"startup_phase_complete"
);
}
}
pub(in super::super) fn maybe_complete_startup_git(
&mut self,
now: Instant,
scan_complete_at: Instant,
) {
if self.scan.startup_phases.git_complete_at.is_none()
&& self.scan.startup_phases.git_seen.len()
>= self.scan.startup_phases.git_expected.len()
{
self.scan.startup_phases.git_complete_at = Some(now);
if let Some(git_toast) = self.scan.startup_phases.git_toast.take() {
self.finish_task_toast(git_toast);
}
if let Some(toast) = self.scan.startup_phases.startup_toast {
self.mark_tracked_item_completed(toast, STARTUP_PHASE_GIT);
}
tracing::info!(
phase = "git_local_applied",
since_scan_complete_ms =
crate::perf_log::ms(now.duration_since(scan_complete_at).as_millis()),
seen = self.scan.startup_phases.git_seen.len(),
expected = self.scan.startup_phases.git_expected.len(),
"startup_phase_complete"
);
}
}
pub(in super::super) fn maybe_complete_startup_repo(
&mut self,
now: Instant,
scan_complete_at: Instant,
) {
if self.scan.startup_phases.repo_complete_at.is_none()
&& self.scan.startup_phases.repo_seen.len()
>= self.scan.startup_phases.repo_expected.len()
{
self.scan.startup_phases.repo_complete_at = Some(now);
if let Some(repo_toast) = self.scan.startup_phases.repo_toast.take() {
self.finish_task_toast(repo_toast);
}
if let Some(toast) = self.scan.startup_phases.startup_toast {
self.mark_tracked_item_completed(toast, STARTUP_PHASE_GITHUB);
}
tracing::info!(
phase = "repo_fetch_applied",
since_scan_complete_ms =
crate::perf_log::ms(now.duration_since(scan_complete_at).as_millis()),
seen = self.scan.startup_phases.repo_seen.len(),
expected = self.scan.startup_phases.repo_expected.len(),
"startup_phase_complete"
);
}
}
pub(in super::super) fn maybe_complete_startup_lints(
&mut self,
now: Instant,
scan_complete_at: Instant,
) {
if self.scan.startup_phases.lint_complete_at.is_none()
&& self
.scan
.startup_phases
.lint_expected
.as_ref()
.is_some_and(|expected| {
!expected.is_empty()
&& self.scan.startup_phases.lint_seen_terminal.len() >= expected.len()
})
{
self.scan.startup_phases.lint_complete_at = Some(now);
tracing::info!(
phase = "lint_terminal_applied",
since_scan_complete_ms =
crate::perf_log::ms(now.duration_since(scan_complete_at).as_millis()),
seen = self.scan.startup_phases.lint_seen_terminal.len(),
expected = self
.scan
.startup_phases
.lint_expected
.as_ref()
.map_or(0, HashSet::len),
"startup_phase_complete"
);
}
}
pub(in super::super) fn maybe_complete_startup_ready(
&mut self,
now: Instant,
scan_complete_at: Instant,
) {
if self.scan.startup_phases.startup_complete_at.is_none() {
let disk_ready = self.scan.startup_phases.disk_complete_at.is_some();
let git_ready = self.scan.startup_phases.git_complete_at.is_some();
let repo_ready = self.scan.startup_phases.repo_complete_at.is_some();
if disk_ready && git_ready && repo_ready {
self.scan.startup_phases.startup_complete_at = Some(now);
if self.scan.startup_phases.lint_startup_complete_at.is_some()
&& let Some(toast) = self.scan.startup_phases.startup_toast.take()
{
self.finish_task_toast(toast);
}
tracing::info!(
since_scan_complete_ms =
crate::perf_log::ms(now.duration_since(scan_complete_at).as_millis()),
disk_seen = self.scan.startup_phases.disk_seen.len(),
disk_expected = self.scan.startup_phases.disk_expected.unwrap_or(0),
git_seen = self.scan.startup_phases.git_seen.len(),
git_expected = self.scan.startup_phases.git_expected.len(),
repo_seen = self.scan.startup_phases.repo_seen.len(),
repo_expected = self.scan.startup_phases.repo_expected.len(),
lint_seen = self.scan.startup_phases.lint_seen_terminal.len(),
lint_expected = self
.scan
.startup_phases
.lint_expected
.as_ref()
.map_or(0, HashSet::len),
"startup_complete"
);
tracing::info!(
since_scan_complete_ms =
crate::perf_log::ms(now.duration_since(scan_complete_at).as_millis()),
"steady_state_begin"
);
}
}
}
pub(in super::super) fn startup_git_toast_body(&self) -> String {
Self::startup_remaining_toast_body(
&self.scan.startup_phases.git_expected,
&self.scan.startup_phases.git_seen,
)
}
pub(in super::super) fn startup_repo_toast_body(&self) -> String {
Self::startup_remaining_repo_toast_body(
&self.scan.startup_phases.repo_expected,
&self.scan.startup_phases.repo_seen,
)
}
fn sync_startup_repo_toast(&mut self) {
let items = Self::tracked_repo_items_for_startup(
&self.scan.startup_phases.repo_expected,
&self.scan.startup_phases.repo_seen,
);
if items.is_empty() {
if let Some(repo_toast) = self.scan.startup_phases.repo_toast.take() {
self.finish_task_toast(repo_toast);
}
return;
}
if let Some(repo_toast) = self.scan.startup_phases.repo_toast {
self.set_task_tracked_items(repo_toast, &items);
} else {
let body = self.startup_repo_toast_body();
let task_id = self.start_task_toast("Retrieving GitHub repo details", &body);
self.set_task_tracked_items(task_id, &items);
self.scan.startup_phases.repo_toast = Some(task_id);
}
}
pub(in super::super) fn tracked_repo_items_for_startup(
expected: &HashSet<OwnerRepo>,
seen: &HashSet<OwnerRepo>,
) -> Vec<TrackedItem> {
expected
.iter()
.map(|repo| TrackedItem {
label: repo.to_string(),
key: repo.into(),
started_at: None,
completed_at: seen.contains(repo).then(Instant::now),
})
.collect()
}
pub(in super::super) fn tracked_items_for_startup(
expected: &HashSet<AbsolutePath>,
seen: &HashSet<AbsolutePath>,
) -> Vec<TrackedItem> {
expected
.iter()
.map(|path| {
let label = crate::project::home_relative_path(path);
let completed_at = if seen.contains(path) {
Some(Instant::now())
} else {
None
};
TrackedItem {
label,
key: path.into(),
started_at: None,
completed_at,
}
})
.collect()
}
pub(in super::super) fn startup_remaining_toast_body(
expected: &HashSet<AbsolutePath>,
seen: &HashSet<AbsolutePath>,
) -> String {
let items: Vec<String> = expected
.iter()
.filter(|path| !seen.contains(*path))
.map(|p| crate::project::home_relative_path(p))
.collect();
let refs: Vec<&str> = items.iter().map(String::as_str).collect();
if refs.is_empty() {
return "Complete".to_string();
}
toasts::format_toast_items(&refs, toasts::toast_body_width())
}
pub(in super::super) fn startup_remaining_repo_toast_body(
expected: &HashSet<OwnerRepo>,
seen: &HashSet<OwnerRepo>,
) -> String {
let items: Vec<String> = expected
.iter()
.filter(|repo| !seen.contains(*repo))
.map(ToString::to_string)
.collect();
let refs: Vec<&str> = items.iter().map(String::as_str).collect();
if refs.is_empty() {
return "Complete".to_string();
}
toasts::format_toast_items(&refs, toasts::toast_body_width())
}
fn startup_git_directory_for_path(&self, path: &Path) -> Option<AbsolutePath> {
self.projects
.iter()
.find(|item| item.at_path(path).is_some())
.and_then(RootItem::git_directory)
}
pub(in super::super) fn startup_lint_toast_body_for(
expected: &HashSet<AbsolutePath>,
seen: &HashSet<AbsolutePath>,
) -> String {
let items: Vec<String> = expected
.iter()
.filter(|path| !seen.contains(*path))
.map(|p| crate::project::home_relative_path(p))
.collect();
let refs: Vec<&str> = items.iter().map(String::as_str).collect();
if refs.is_empty() {
return "Complete".to_string();
}
toasts::format_toast_items(&refs, toasts::toast_body_width())
}
pub(in super::super) fn running_lint_toast_body(&self) -> String {
let paths: HashSet<AbsolutePath> = self.running_lint_paths.keys().cloned().collect();
Self::startup_lint_toast_body_for(&paths, &HashSet::new())
}
pub(in super::super) fn sync_running_clean_toast(&mut self) {
if self.running_clean_paths.is_empty() {
if let Some(task_id) = self.clean_toast.take() {
self.finish_task_toast(task_id);
}
return;
}
let items: Vec<TrackedItem> = self
.running_clean_paths
.iter()
.map(|p| TrackedItem {
label: crate::project::home_relative_path(p.as_path()),
key: p.clone().into(),
started_at: None,
completed_at: None,
})
.collect();
let body = self.running_clean_toast_body();
if let Some(task_id) = self.clean_toast {
self.set_task_tracked_items(task_id, &items);
} else {
let task_id = self.start_task_toast("cargo clean", body);
self.set_task_tracked_items(task_id, &items);
self.clean_toast = Some(task_id);
}
}
fn running_clean_toast_body(&self) -> String {
let items: Vec<String> = self
.running_clean_paths
.iter()
.map(|p| crate::project::home_relative_path(p.as_path()))
.collect();
let refs: Vec<&str> = items.iter().map(String::as_str).collect();
crate::tui::toasts::format_toast_items(&refs, crate::tui::toasts::toast_body_width())
}
pub(in super::super) fn sync_running_lint_toast(&mut self) {
if self.running_lint_paths.is_empty() {
if let Some(task_id) = self.lint_toast {
let empty: HashSet<String> = HashSet::new();
self.toasts.complete_missing_items(task_id, &empty);
if !self.toasts.is_task_finished(task_id) {
let linger = Duration::from_secs_f64(self.current_config.tui.task_linger_secs);
self.toasts.finish_task(task_id, linger);
}
}
return;
}
let running_items: Vec<TrackedItem> = self
.running_lint_paths
.iter()
.map(|(p, &started)| TrackedItem {
label: crate::project::home_relative_path(p),
key: p.clone().into(),
started_at: Some(started),
completed_at: None,
})
.collect();
let running_keys: HashSet<String> = running_items
.iter()
.map(|item| item.key.to_string())
.collect();
if let Some(task_id) = self.lint_toast
&& self.toasts.reactivate_task(task_id)
{
self.toasts.complete_missing_items(task_id, &running_keys);
let linger = Duration::from_secs_f64(self.current_config.tui.task_linger_secs);
self.toasts
.add_new_tracked_items(task_id, &running_items, linger);
for item in &running_items {
if let Some(started) = item.started_at {
self.toasts
.restart_tracked_item(task_id, &item.key, started);
}
}
} else {
let items = running_items;
let body = self.running_lint_toast_body();
let task_id = self.start_task_toast("Lints", body);
self.set_task_tracked_items(task_id, &items);
self.lint_toast = Some(task_id);
}
}
pub(in super::super) fn request_fit_widths_build(&mut self) {
if !self.dirty.fit_widths.is_dirty() {
return;
}
self.builds.fit.latest = self.builds.fit.latest.wrapping_add(1);
if self.builds.fit.active.is_some() {
return;
}
self.spawn_fit_widths_build(self.builds.fit.latest);
}
pub(in super::super) fn spawn_fit_widths_build(&mut self, build_id: u64) {
let tx = self.builds.fit.tx.clone();
let items = self.projects.clone();
let root_labels = self
.projects
.resolved_root_labels(self.include_non_rust().includes_non_rust());
let lint_enabled = self.lint_enabled();
self.builds.fit.active = Some(build_id);
std::thread::spawn(move || {
let started = Instant::now();
let widths = super::snapshots::build_fit_widths_snapshot(
&items,
&root_labels,
lint_enabled,
build_id,
);
let elapsed = started.elapsed();
if elapsed.as_millis() >= crate::perf_log::SLOW_WORKER_MS {
tracing::info!(
elapsed_ms = crate::perf_log::ms(elapsed.as_millis()),
build_id,
items = items.len(),
"fit_widths_build"
);
}
let _ = tx.send(FitWidthsBuildResult { build_id, widths });
});
}
pub(in super::super) fn poll_fit_width_builds(&mut self) -> usize {
let mut applied = 0;
while let Ok(result) = self.builds.fit.rx.try_recv() {
if self.builds.fit.active != Some(result.build_id) {
continue;
}
self.builds.fit.active = None;
self.cached_fit_widths = result.widths;
applied += 1;
if result.build_id == self.builds.fit.latest {
self.dirty.fit_widths.mark_clean();
} else {
self.spawn_fit_widths_build(self.builds.fit.latest);
}
}
applied
}
pub(in super::super) fn request_disk_cache_build(&mut self) {
if !self.dirty.disk_cache.is_dirty() {
return;
}
self.builds.disk.latest = self.builds.disk.latest.wrapping_add(1);
if self.builds.disk.active.is_some() {
return;
}
self.spawn_disk_cache_build(self.builds.disk.latest);
}
pub(in super::super) fn spawn_disk_cache_build(&mut self, build_id: u64) {
let tx = self.builds.disk.tx.clone();
let items = self.projects.clone();
self.builds.disk.active = Some(build_id);
std::thread::spawn(move || {
let started = Instant::now();
let (root_sorted, child_sorted) = super::snapshots::build_disk_cache_snapshot(&items);
let elapsed = started.elapsed();
if elapsed.as_millis() >= crate::perf_log::SLOW_WORKER_MS {
tracing::info!(
elapsed_ms = crate::perf_log::ms(elapsed.as_millis()),
build_id,
items = items.len(),
root_values = root_sorted.len(),
child_sets = child_sorted.len(),
"disk_cache_build"
);
}
let _ = tx.send(DiskCacheBuildResult {
build_id,
root_sorted,
child_sorted,
});
});
}
pub(in super::super) fn poll_disk_cache_builds(&mut self) -> usize {
let mut applied = 0;
while let Ok(result) = self.builds.disk.rx.try_recv() {
if self.builds.disk.active != Some(result.build_id) {
continue;
}
self.builds.disk.active = None;
self.cached_root_sorted = result.root_sorted;
self.cached_child_sorted = result.child_sorted;
applied += 1;
if result.build_id == self.builds.disk.latest {
self.dirty.disk_cache.mark_clean();
} else {
self.spawn_disk_cache_build(self.builds.disk.latest);
}
}
applied
}
pub(in super::super) fn refresh_derived_state(&mut self) {
self.recompute_cargo_active_paths();
self.data_generation += 1;
self.detail_generation += 1;
self.dirty.finder.mark_dirty();
self.dirty.rows.mark_dirty();
self.dirty.disk_cache.mark_dirty();
self.dirty.fit_widths.mark_dirty();
}
fn capture_legacy_root_expansions(&self) -> Vec<LegacyRootExpansion> {
self.projects
.iter()
.enumerate()
.filter_map(|(ni, item)| {
if !self.expanded.contains(&Node(ni)) {
return None;
}
match item {
RootItem::Rust(crate::project::RustProject::Workspace(ws)) => {
Some(LegacyRootExpansion {
root_path: ws.path().clone(),
old_node_index: ni,
had_children: ws.has_members() || !ws.vendored().is_empty(),
named_groups: ws
.groups()
.iter()
.enumerate()
.filter_map(|(gi, group)| {
group
.is_named()
.then(|| self.expanded.contains(&Group(ni, gi)))
.filter(|expanded| *expanded)
.map(|_| gi)
})
.collect(),
})
},
RootItem::Rust(crate::project::RustProject::Package(pkg)) => {
Some(LegacyRootExpansion {
root_path: pkg.path().clone(),
old_node_index: ni,
had_children: !pkg.vendored().is_empty(),
named_groups: Vec::new(),
})
},
_ => None,
}
})
.collect()
}
fn migrate_legacy_root_expansions(&mut self, legacy: &[LegacyRootExpansion]) {
for legacy_root in legacy {
let Some((current_index, current_item)) = self
.projects
.iter()
.enumerate()
.find(|(_, item)| item.path() == legacy_root.root_path.as_path())
else {
continue;
};
match current_item {
RootItem::Worktrees(
group @ crate::project::WorktreeGroup::Workspaces { primary, .. },
) if group.renders_as_group() => {
self.expanded.insert(Node(current_index));
if legacy_root.had_children {
self.expanded.insert(Worktree(current_index, 0));
}
for &group_index in &legacy_root.named_groups {
if primary.groups().get(group_index).is_some() {
self.expanded
.insert(WorktreeGroup(current_index, 0, group_index));
}
self.expanded
.remove(&Group(legacy_root.old_node_index, group_index));
}
},
RootItem::Worktrees(group @ crate::project::WorktreeGroup::Packages { .. })
if group.renders_as_group() =>
{
self.expanded.insert(Node(current_index));
if legacy_root.had_children {
self.expanded.insert(Worktree(current_index, 0));
}
},
_ => {},
}
}
}
fn rebuild_visible_rows_now(&mut self) {
self.cached_visible_rows = super::snapshots::build_visible_rows(
&self.projects,
&self.expanded,
self.include_non_rust().includes_non_rust(),
);
self.dirty.rows.mark_clean();
}
pub(in super::super) fn refresh_async_caches(&mut self) {
self.request_disk_cache_build();
self.request_fit_widths_build();
}
pub(in super::super) fn rescan(&mut self) {
self.projects.clear();
self.ci_fetch_tracker.clear();
self.ci_display_modes.clear();
self.clear_all_lint_state();
self.lint_cache_usage = crate::lint::CacheUsage::default();
self.cargo_active_paths.clear();
self.repo_fetch_cache = crate::scan::new_repo_cache();
self.discovery_shimmers.clear();
self.scan.phase = ScanPhase::Running;
self.scan.started_at = Instant::now();
self.scan.run_count += 1;
self.scan.startup_phases = StartupPhaseTracker::default();
tracing::info!(kind = "rescan", run = self.scan.run_count, "scan_start");
self.priority_fetch_path = None;
self.focus_pane(PaneId::ProjectList);
self.close_settings();
self.close_finder();
self.reset_project_panes();
self.selection_paths.selected_project = None;
self.pending_ci_fetch = None;
self.expanded.clear();
self.pane_manager.pane_mut(PaneId::ProjectList).home();
self.pane_manager
.pane_mut(PaneId::ProjectList)
.set_scroll_offset(0);
self.dirty.rows.mark_dirty();
self.dirty.disk_cache.mark_dirty();
self.dirty.fit_widths.mark_dirty();
self.builds.fit.active = None;
self.builds.fit.latest = 0;
self.builds.disk.active = None;
self.builds.disk.latest = 0;
self.data_generation += 1;
self.detail_generation += 1;
let scan_dirs = scan::resolve_include_dirs(&self.current_config.tui.include_dirs);
let (tx, rx) = scan::spawn_streaming_scan(
scan_dirs,
&self.current_config.tui.inline_dirs,
self.include_non_rust(),
self.http_client.clone(),
);
self.bg_tx = tx;
self.bg_rx = rx;
self.respawn_watcher();
let current_config = self.current_config.clone();
self.refresh_lint_runtime_from_config(¤t_config);
}
pub(in super::super) fn poll_background(&mut self) -> PollBackgroundStats {
const MAX_MSGS_PER_FRAME: usize = 50;
let mut needs_rebuild = false;
let mut msg_count = 0;
let started = Instant::now();
let mut stats = PollBackgroundStats::default();
while msg_count < MAX_MSGS_PER_FRAME {
let Ok(msg) = self.bg_rx.try_recv() else {
break;
};
Self::record_background_msg_kind(&mut stats, &msg);
msg_count += 1;
needs_rebuild |= self.handle_bg_msg(msg);
}
stats.bg_msgs = msg_count;
Self::log_saturated_background_batch(&stats);
stats.ci_msgs = self.poll_ci_fetches();
stats.example_msgs = self.poll_example_msgs();
self.poll_clean_msgs();
stats.tree_results = 0;
stats.fit_results = self.poll_fit_width_builds();
stats.disk_results = self.poll_disk_cache_builds();
if needs_rebuild {
self.refresh_derived_state();
self.maybe_priority_fetch();
}
stats.needs_rebuild = needs_rebuild;
self.refresh_async_caches();
let elapsed = started.elapsed();
if elapsed.as_millis() >= crate::perf_log::SLOW_BG_BATCH_MS {
tracing::info!(
elapsed_ms = crate::perf_log::ms(elapsed.as_millis()),
bg_msgs = stats.bg_msgs,
ci_msgs = stats.ci_msgs,
example_msgs = stats.example_msgs,
tree_results = stats.tree_results,
fit_results = stats.fit_results,
disk_results = stats.disk_results,
needs_rebuild = stats.needs_rebuild,
items = self.projects.len(),
"poll_background"
);
}
stats
}
pub(in super::super) const fn record_background_msg_kind(
stats: &mut PollBackgroundStats,
msg: &BackgroundMsg,
) {
match msg {
BackgroundMsg::DiskUsage { .. } | BackgroundMsg::DiskUsageBatch { .. } => {
stats.disk_usage_msgs += 1;
},
BackgroundMsg::GitInfo { .. } | BackgroundMsg::GitFirstCommit { .. } => {
stats.git_info_msgs += 1;
},
BackgroundMsg::LintStatus { .. } | BackgroundMsg::LintStartupStatus { .. } => {
stats.lint_status_msgs += 1;
},
BackgroundMsg::CiRuns { .. }
| BackgroundMsg::RepoFetchQueued { .. }
| BackgroundMsg::RepoFetchComplete { .. }
| BackgroundMsg::CratesIoVersion { .. }
| BackgroundMsg::RepoMeta { .. }
| BackgroundMsg::Submodules { .. }
| BackgroundMsg::ScanResult { .. }
| BackgroundMsg::ProjectDiscovered { .. }
| BackgroundMsg::ProjectRefreshed { .. }
| BackgroundMsg::LintCachePruned { .. }
| BackgroundMsg::ServiceReachable { .. }
| BackgroundMsg::ServiceRecovered { .. }
| BackgroundMsg::ServiceUnreachable { .. }
| BackgroundMsg::LanguageStatsBatch { .. } => {},
}
}
pub(in super::super) fn log_saturated_background_batch(stats: &PollBackgroundStats) {
const MAX_MSGS_PER_FRAME: usize = 50;
if stats.bg_msgs != MAX_MSGS_PER_FRAME {
return;
}
tracing::info!(
bg_msgs = stats.bg_msgs,
disk_usage_msgs = stats.disk_usage_msgs,
git_info_msgs = stats.git_info_msgs,
lint_status_msgs = stats.lint_status_msgs,
"poll_background_saturated"
);
}
pub(in super::super) fn poll_ci_fetches(&mut self) -> usize {
let mut count = 0;
while let Ok(msg) = self.ci_fetch_rx.try_recv() {
match msg {
CiFetchMsg::Complete { path, result, kind } => {
let before = self
.ci_info_for(Path::new(&path))
.map_or(0, |info| info.runs.len());
self.handle_ci_fetch_complete(&path, result, kind);
let after = self
.ci_info_for(Path::new(&path))
.map_or(0, |info| info.runs.len());
let new_runs = after.saturating_sub(before);
if let Some(task_id) = self.ci_fetch_toast.take() {
let empty: std::collections::HashSet<String> =
std::collections::HashSet::new();
self.toasts.complete_missing_items(task_id, &empty);
let label = if new_runs > 0 {
format!("{new_runs} new runs fetched")
} else {
"no new runs".to_string()
};
let result_item = crate::tui::toasts::TrackedItem {
label,
key: AbsolutePath::from(format!("{path}:result")).into(),
started_at: None,
completed_at: None,
};
let linger = std::time::Duration::from_secs_f64(
self.current_config.tui.task_linger_secs,
);
self.toasts
.add_new_tracked_items(task_id, &[result_item], linger);
self.finish_task_toast(task_id);
}
},
}
count += 1;
}
count
}
pub(in super::super) fn poll_example_msgs(&mut self) -> usize {
let mut count = 0;
while let Ok(msg) = self.example_rx.try_recv() {
match msg {
ExampleMsg::Output(line) => self.example_output.push(line),
ExampleMsg::Progress(line) => self.apply_example_progress(line),
ExampleMsg::Finished => self.finish_example_run(),
}
count += 1;
}
count
}
pub(in super::super) fn apply_example_progress(&mut self, line: String) {
if let Some(last) = self.example_output.last_mut() {
*last = line;
} else {
self.example_output.push(line);
}
}
pub(in super::super) fn finish_example_run(&mut self) {
self.example_running = None;
self.example_output.push("── done ──".to_string());
self.mark_terminal_dirty();
}
pub(in super::super) fn poll_clean_msgs(&mut self) {
while let Ok(msg) = self.clean_rx.try_recv() {
match msg {
CleanMsg::Finished(abs_path) => {
let already_zero = self
.projects
.iter()
.find(|i| i.path() == abs_path.as_path())
.and_then(RootItem::disk_usage_bytes)
.is_none_or(|bytes| bytes == 0);
if already_zero {
self.running_clean_paths.remove(abs_path.as_path());
self.sync_running_clean_toast();
}
},
}
}
}
pub(in super::super) fn handle_disk_usage(&mut self, path: &Path, bytes: u64) {
if self.running_clean_paths.remove(path) {
self.sync_running_clean_toast();
}
self.apply_disk_usage(path, bytes);
}
pub(in super::super) fn handle_disk_usage_batch(&mut self, entries: Vec<(AbsolutePath, u64)>) {
for (path, bytes) in entries {
self.apply_disk_usage(path.as_path(), bytes);
}
}
pub(in super::super) fn apply_disk_usage(&mut self, path: &Path, bytes: u64) {
self.dirty.disk_cache.mark_dirty();
self.dirty.fit_widths.mark_dirty();
let mut lint_runtime_changed = false;
if let Some(project) = self.projects.at_path_mut(path) {
project.disk_usage_bytes = Some(bytes);
if bytes == 0 && !path.exists() && project.visibility != Deleted {
project.visibility = Deleted;
lint_runtime_changed = true;
} else if bytes > 0 && project.visibility != Visible {
project.visibility = Visible;
lint_runtime_changed = true;
}
}
if lint_runtime_changed {
if let Some(runtime) = &self.lint_runtime
&& bytes == 0
{
runtime.unregister_project(AbsolutePath::from(path));
}
if bytes > 0 {
self.register_lint_for_path(path);
}
}
}
fn spawn_repo_fetch_for_git_info(&self, path: &Path, info: &GitInfo) {
let Some(repo_url) = info.url.as_deref() else {
return;
};
let Some(owner_repo) = crate::ci::parse_owner_repo(repo_url) else {
return;
};
let tx = self.bg_tx.clone();
let client = self.http_client.clone();
let repo_cache = self.repo_fetch_cache.clone();
let path: AbsolutePath = AbsolutePath::from(path);
let repo_url = repo_url.to_string();
let ci_run_count = self.ci_run_count();
thread::spawn(move || {
let data =
crate::scan::load_cached_repo_data(&repo_cache, &owner_repo).unwrap_or_else(|| {
let _ = tx.send(BackgroundMsg::RepoFetchQueued {
repo: owner_repo.clone(),
});
let (result, meta, signal) = crate::scan::fetch_ci_runs_cached(
&client,
&repo_url,
owner_repo.owner(),
owner_repo.repo(),
ci_run_count,
);
crate::scan::emit_service_signal(&tx, signal);
let (runs, github_total) = match result {
crate::scan::CiFetchResult::Loaded { runs, github_total } => {
(runs, github_total)
},
crate::scan::CiFetchResult::CacheOnly(runs) => (runs, 0),
};
let data = crate::scan::CachedRepoData {
runs,
meta,
github_total,
};
crate::scan::store_cached_repo_data(&repo_cache, &owner_repo, data.clone());
let _ = tx.send(BackgroundMsg::RepoFetchComplete {
repo: owner_repo.clone(),
});
data
});
let _ = tx.send(BackgroundMsg::CiRuns {
path: path.clone(),
runs: data.runs,
github_total: data.github_total,
});
if let Some(meta) = data.meta {
let _ = tx.send(BackgroundMsg::RepoMeta {
path,
stars: meta.stars,
description: meta.description,
});
}
});
}
pub(in super::super) fn handle_git_info(&mut self, path: &Path, info: GitInfo) {
self.detail_generation += 1;
self.dirty.fit_widths.mark_dirty();
tracing::info!(
path = %path.display(),
path_state = %info.path_state.label(),
"git_info_applied"
);
let preserved_first_commit = self
.git_info_for(path)
.and_then(|existing| existing.first_commit.clone());
let mut info = info;
if info.first_commit.is_none() {
info.first_commit =
preserved_first_commit.or_else(|| self.pending_git_first_commit.remove(path));
}
if let Some(project) = self.projects.at_path_mut(path) {
project.local_git_state = LocalGitState::Detected(Box::new(info.clone()));
}
if self.is_scan_complete() {
let git_dir = self
.startup_git_directory_for_path(path)
.unwrap_or_else(|| AbsolutePath::from(path));
self.scan.startup_phases.git_seen.insert(git_dir.clone());
if let Some(git_toast) = self.scan.startup_phases.git_toast {
self.mark_tracked_item_completed(git_toast, &git_dir.to_string());
}
self.maybe_log_startup_phase_completions();
}
self.spawn_repo_fetch_for_git_info(path, &info);
self.dirty.finder.mark_dirty();
}
pub(in super::super) fn handle_git_first_commit(
&mut self,
path: &Path,
first_commit: Option<&str>,
) {
let first_commit = first_commit.map(String::from);
let mut applied = false;
let Some(project) = self.projects.at_path_mut(path) else {
if let Some(first_commit) = first_commit {
self.pending_git_first_commit
.insert(AbsolutePath::from(path), first_commit);
} else {
self.pending_git_first_commit.remove(path);
}
return;
};
if let Some(info) = project.local_git_state.info_mut() {
info.first_commit.clone_from(&first_commit);
applied = true;
}
if applied {
self.pending_git_first_commit.remove(path);
} else if let Some(first_commit) = first_commit {
self.pending_git_first_commit
.insert(AbsolutePath::from(path), first_commit);
} else {
self.pending_git_first_commit.remove(path);
}
}
fn handle_repo_fetch_queued(&mut self, repo: OwnerRepo) {
let first_repo = self.scan.startup_phases.repo_expected.is_empty();
self.scan.startup_phases.repo_expected.insert(repo);
if first_repo {
self.scan.startup_phases.repo_complete_at = None;
self.scan.startup_phases.startup_complete_at = None;
if let Some(toast) = self.scan.startup_phases.startup_toast {
let linger = Duration::from_secs_f64(self.current_config.tui.task_linger_secs);
self.toasts.add_new_tracked_items(
toast,
&[TrackedItem {
label: STARTUP_PHASE_GITHUB.to_string(),
key: STARTUP_PHASE_GITHUB.into(),
started_at: Some(Instant::now()),
completed_at: None,
}],
linger,
);
let toast_len = self.active_toasts().len();
self.pane_manager
.pane_mut(PaneId::Toasts)
.set_len(toast_len);
}
}
if self.is_scan_complete() {
self.sync_startup_repo_toast();
}
}
pub(in super::super) fn handle_repo_fetch_complete(&mut self, repo: OwnerRepo) {
if let Some(repo_toast) = self.scan.startup_phases.repo_toast {
let label = repo.to_string();
self.mark_tracked_item_completed(repo_toast, &label);
}
self.scan.startup_phases.repo_seen.insert(repo);
self.maybe_log_startup_phase_completions();
}
pub(in super::super) fn handle_repo_meta(
&mut self,
path: &Path,
stars: u64,
description: Option<String>,
) {
if let Some(project) = self.projects.at_path_mut(path) {
project.github_info = Some(crate::project::GitHubInfo { stars, description });
}
}
pub(in super::super) fn handle_project_discovered(&mut self, item: RootItem) -> bool {
let legacy_expansions = self.capture_legacy_root_expansions();
let discovered_path = item.path().to_path_buf();
let mut already_exists = false;
self.projects.for_each_leaf_path(|path, _| {
if path == discovered_path {
already_exists = true;
}
});
if already_exists {
return false;
}
self.register_item_background_services(&item);
let discovered_path = item.path().to_path_buf();
self.projects.insert_into_hierarchy(item);
self.register_discovery_shimmer(discovered_path.as_path());
self.migrate_legacy_root_expansions(&legacy_expansions);
self.rebuild_visible_rows_now();
true
}
pub(in super::super) fn handle_project_refreshed(&mut self, mut item: RootItem) -> bool {
let legacy_expansions = self.capture_legacy_root_expansions();
let path = item.path().to_path_buf();
let Some(old) = self.projects.replace_leaf_by_path(&path, item.clone()) else {
return false;
};
for (project_path, info) in old.collect_project_info() {
if let Some(project) = item.at_path_mut(&project_path) {
*project = info;
}
}
self.projects.replace_leaf_by_path(&path, item);
self.projects.regroup_top_level_worktrees();
self.reload_lint_history(&path);
self.migrate_legacy_root_expansions(&legacy_expansions);
self.rebuild_visible_rows_now();
self.detail_cache_key = None;
self.pane_manager.clear_detail_data();
true
}
pub(in super::super) fn apply_service_signal(&mut self, signal: ServiceSignal) {
match signal {
ServiceSignal::Reachable(service) => {
self.unreachable_services.remove(&service);
},
ServiceSignal::Unreachable(service) => {
self.unreachable_services.insert(service);
if self.service_retry_active.insert(service) {
self.spawn_service_retry(service);
}
},
}
}
pub(in super::super) fn spawn_service_retry(&self, service: ServiceKind) {
#[cfg(test)]
if !self.retry_spawn_mode.is_enabled() {
return;
}
let tx = self.bg_tx.clone();
let client = self.http_client.clone();
thread::spawn(move || {
loop {
if client.probe_service(service) {
crate::scan::emit_service_recovered(&tx, service);
break;
}
thread::sleep(Duration::from_secs(SERVICE_RETRY_SECS));
}
});
}
pub(in super::super) fn mark_service_recovered(&mut self, service: ServiceKind) {
self.unreachable_services.remove(&service);
self.service_retry_active.remove(&service);
}
fn update_generations_for_msg(&mut self, msg: &BackgroundMsg) {
if msg.path().is_some() {
self.data_generation += 1;
}
if let Some(path) = msg.path()
&& self.detail_path_is_affected(path)
{
self.detail_generation += 1;
}
}
fn handle_disk_usage_msg(&mut self, path: &Path, bytes: u64) {
self.scan
.startup_phases
.disk_seen
.insert(AbsolutePath::from(path));
self.handle_disk_usage(path, bytes);
self.maybe_log_startup_phase_completions();
}
fn handle_disk_usage_batch_msg(
&mut self,
root_path: &AbsolutePath,
entries: Vec<(AbsolutePath, u64)>,
) {
self.data_generation += 1;
if entries
.iter()
.any(|(path, _)| self.detail_path_is_affected(path.as_path()))
{
self.detail_generation += 1;
}
self.scan.startup_phases.disk_seen.insert(root_path.clone());
self.handle_disk_usage_batch(entries);
self.maybe_log_startup_phase_completions();
}
fn handle_crates_io_version_msg(&mut self, path: &Path, version: String, downloads: u64) {
if let Some(rust_info) = self.projects.rust_info_at_path_mut(path) {
rust_info.set_crates_io(version, downloads);
}
}
fn handle_lint_startup_status_msg(&mut self, path: &AbsolutePath, status: LintStatus) {
if let Some(lr) = self.projects.lint_at_path_mut(path) {
lr.set_status(status);
}
self.scan.startup_phases.lint_startup_seen += 1;
self.maybe_complete_startup_lint_cache();
}
fn maybe_complete_startup_lint_cache(&mut self) {
if self.scan.startup_phases.lint_startup_complete_at.is_some() {
return;
}
let Some(expected) = self.scan.startup_phases.lint_startup_expected else {
return;
};
if self.scan.startup_phases.lint_startup_seen < expected {
return;
}
let now = Instant::now();
self.scan.startup_phases.lint_startup_complete_at = Some(now);
self.refresh_lint_cache_usage_from_disk();
if let Some(toast) = self.scan.startup_phases.startup_toast {
self.mark_tracked_item_completed(toast, STARTUP_PHASE_LINT);
}
if self.scan.startup_phases.startup_complete_at.is_some()
&& let Some(toast) = self.scan.startup_phases.startup_toast.take()
{
self.finish_task_toast(toast);
}
if let Some(scan_complete_at) = self.scan.startup_phases.scan_complete_at {
tracing::info!(
phase = "lint_startup_applied",
since_scan_complete_ms =
crate::perf_log::ms(now.duration_since(scan_complete_at).as_millis()),
seen = self.scan.startup_phases.lint_startup_seen,
expected,
"startup_phase_complete"
);
}
self.maybe_log_startup_phase_completions();
}
fn handle_lint_status_msg(&mut self, path: &Path, status: LintStatus) {
let abs = AbsolutePath::from(path);
let status_started = matches!(status, LintStatus::Running(_));
let status_is_terminal = matches!(
status,
LintStatus::Passed(_) | LintStatus::Failed(_) | LintStatus::Stale | LintStatus::NoLog
);
if !self.is_cargo_active_path(path) {
if let Some(lr) = self.projects.lint_at_path_mut(path) {
lr.clear_runs();
}
return;
}
let mut is_rust = false;
self.projects.for_each_leaf_path(|p, rust| {
if p == path {
is_rust = rust;
}
});
let eligible = crate::lint::project_is_eligible(
&self.current_config.lint,
&path.to_string_lossy(),
path,
is_rust,
);
if eligible {
if let Some(lr) = self.projects.lint_at_path_mut(path) {
lr.set_status(status);
}
if status_is_terminal {
self.reload_lint_history(path);
}
} else {
if let Some(lr) = self.projects.lint_at_path_mut(path) {
lr.clear_runs();
}
self.running_lint_paths.remove(path);
}
if status_started {
self.running_lint_paths.insert(abs, Instant::now());
}
if status_is_terminal {
self.running_lint_paths.remove(path);
}
self.sync_running_lint_toast();
if !self.is_scan_complete() {
return;
}
if status_started {
let abs_path = AbsolutePath::from(path);
let expected = self
.scan
.startup_phases
.lint_expected
.get_or_insert_with(HashSet::new);
if expected.insert(abs_path) {
self.scan.startup_phases.lint_complete_at = None;
}
}
if status_is_terminal {
let abs_path = AbsolutePath::from(path);
if self
.scan
.startup_phases
.lint_expected
.as_ref()
.is_some_and(|expected| expected.contains(path))
{
self.scan.startup_phases.lint_seen_terminal.insert(abs_path);
}
}
self.maybe_log_startup_phase_completions();
}
fn handle_scan_result(
&mut self,
projects: Vec<RootItem>,
disk_entries: &[(String, AbsolutePath)],
) {
let kind = if self.scan.run_count == 1 {
"initial"
} else {
"rescan"
};
tracing::info!(
elapsed_ms = crate::perf_log::ms(self.scan.started_at.elapsed().as_millis()),
kind,
run = self.scan.run_count,
tree_items = projects.len(),
disk_entries = disk_entries.len(),
"scan_result_applied"
);
let selected_path = self
.selected_project_path()
.map(AbsolutePath::from)
.or_else(|| self.selection_paths.last_selected.clone());
self.projects = ProjectList::new(projects);
self.dirty.finder.mark_dirty();
self.dirty.rows.mark_dirty();
self.dirty.disk_cache.mark_dirty();
self.dirty.fit_widths.mark_dirty();
self.recompute_cargo_active_paths();
self.prune_inactive_project_state();
let lint_registered = self.register_lint_for_root_items();
self.scan.startup_phases.lint_startup_expected = Some(lint_registered);
self.scan.startup_phases.lint_startup_seen = 0;
self.scan.startup_phases.lint_startup_complete_at = None;
self.refresh_lint_runs_from_disk();
self.data_generation += 1;
self.detail_generation += 1;
if let Some(path) = selected_path {
self.select_project_in_tree(path.as_path());
} else if !self.projects.is_empty() {
self.pane_manager.pane_mut(PaneId::ProjectList).set_pos(0);
}
self.sync_selected_project();
self.register_background_services_for_tree();
self.finish_watcher_registration_batch();
self.scan.phase = ScanPhase::Complete;
self.initialize_startup_phase_tracker();
self.schedule_startup_project_details();
self.schedule_git_first_commit_refreshes();
}
pub(in super::super) fn handle_bg_msg(&mut self, msg: BackgroundMsg) -> bool {
self.update_generations_for_msg(&msg);
match msg {
BackgroundMsg::DiskUsage { path, bytes } => {
self.handle_disk_usage_msg(path.as_path(), bytes);
},
BackgroundMsg::DiskUsageBatch { root_path, entries } => {
self.handle_disk_usage_batch_msg(&root_path, entries);
},
BackgroundMsg::CiRuns {
path,
runs,
github_total,
} => {
self.insert_ci_runs(path.as_path(), runs, github_total);
},
BackgroundMsg::RepoFetchQueued { repo } => {
self.handle_repo_fetch_queued(repo);
},
BackgroundMsg::RepoFetchComplete { repo } => self.handle_repo_fetch_complete(repo),
BackgroundMsg::GitInfo { path, info } => {
self.handle_git_info(path.as_path(), info);
},
BackgroundMsg::GitFirstCommit { path, first_commit } => {
self.handle_git_first_commit(path.as_path(), first_commit.as_deref());
},
BackgroundMsg::Submodules { path, submodules } => {
if let Some(info) = self.projects.at_path_mut(path.as_path()) {
info.submodules = submodules;
self.detail_generation += 1;
}
},
BackgroundMsg::CratesIoVersion {
path,
version,
downloads,
} => self.handle_crates_io_version_msg(path.as_path(), version, downloads),
BackgroundMsg::RepoMeta {
path,
stars,
description,
} => self.handle_repo_meta(path.as_path(), stars, description),
BackgroundMsg::ScanResult {
projects,
disk_entries,
} => {
self.handle_scan_result(projects, &disk_entries);
},
BackgroundMsg::ProjectDiscovered { item } => {
if self.handle_project_discovered(item) {
return true;
}
},
BackgroundMsg::ProjectRefreshed { item } => {
if self.handle_project_refreshed(item) {
return true;
}
},
BackgroundMsg::LintCachePruned {
runs_evicted,
bytes_reclaimed,
} => {
self.show_timed_toast(
"Lint cache",
format!(
"Evicted {runs_evicted} {}, reclaimed {}",
if runs_evicted == 1 { "run" } else { "runs" },
crate::tui::render::format_bytes(bytes_reclaimed),
),
);
self.refresh_lint_cache_usage_from_disk();
},
BackgroundMsg::LintStatus { path, status } => {
self.handle_lint_status_msg(path.as_path(), status);
},
BackgroundMsg::LintStartupStatus { path, status } => {
self.handle_lint_startup_status_msg(&path, status);
},
BackgroundMsg::ServiceReachable { service } => {
self.apply_service_signal(ServiceSignal::Reachable(service));
},
BackgroundMsg::ServiceRecovered { service } => {
self.mark_service_recovered(service);
},
BackgroundMsg::ServiceUnreachable { service } => {
self.apply_service_signal(ServiceSignal::Unreachable(service));
},
BackgroundMsg::LanguageStatsBatch { entries } => {
self.handle_language_stats_batch(entries);
},
}
false
}
fn handle_language_stats_batch(&mut self, entries: Vec<(AbsolutePath, LanguageStats)>) {
for (path, stats) in entries {
if let Some(project) = self.projects.at_path_mut(path.as_path()) {
project.language_stats = Some(stats);
}
}
self.detail_generation += 1;
self.dirty.rows.mark_dirty();
}
pub(in super::super) fn detail_path_is_affected(&self, path: &Path) -> bool {
let Some(selected_path) = self.selected_project_path() else {
return false;
};
if selected_path == path {
return true;
}
self.projects
.lint_at_path(selected_path)
.zip(self.projects.lint_at_path(path))
.is_some_and(|(a, b)| std::ptr::eq(a, b))
}
pub(in super::super) fn maybe_priority_fetch(&mut self) {
let Some(abs_path) = self.selected_project_path().map(Path::to_path_buf) else {
return;
};
let abs_key: AbsolutePath = abs_path.clone().into();
let display_path = self
.selected_display_path()
.unwrap_or_else(|| abs_key.display_path());
let name = self
.pane_manager
.package_data
.as_ref()
.map(|d| d.title_name.clone())
.filter(|n| n != "-");
if self
.projects
.at_path(abs_key.as_path())
.is_none_or(|p| p.disk_usage_bytes.is_none())
&& self.priority_fetch_path.as_ref() != Some(&abs_key)
{
self.priority_fetch_path = Some(abs_key);
let abs_str = abs_path.display().to_string();
crate::tui::terminal::spawn_priority_fetch(
self,
display_path.as_str(),
&abs_str,
name.as_ref(),
);
}
}
}