mod async_tasks;
mod ci;
mod construct;
mod dismiss;
mod lint_registration;
mod navigation;
mod phase_state;
mod startup;
pub(super) use phase_state::CountedPhase;
pub(super) use phase_state::KeyedPhase;
mod target_index;
mod types;
use std::collections::HashSet;
use std::path::Path;
use std::rc::Rc;
use std::sync::Arc;
use std::sync::Mutex;
use std::time::Duration;
use std::time::Instant;
use ratatui::layout::Position;
use tui_pane::KeyBind;
use tui_pane::PaneFocusState;
use tui_pane::ThemeRuntime;
use tui_pane::ToastId;
use tui_pane::ToastStyle::Warning;
use tui_pane::TrackedItem;
use super::background::Background;
#[cfg(test)]
use super::columns::LintCell;
use super::columns::StyledSegment;
use super::integration;
use super::integration::AppPaneId;
use super::keymap;
use super::overlays::Overlays;
use super::panes::PaneId;
use super::panes::Panes;
use super::panes::SyncedDescriptionHeight;
use super::project_list::ProjectList;
use super::running_targets::RunningTargets;
use super::state::Config;
use super::state::GitStatusTracker;
use super::state::Inflight;
use super::state::Keymap;
use super::state::Scan;
use super::state::SyncTracker;
use crate::channel::Receiver;
use crate::channel::Sender;
use crate::ci::OwnerRepo;
use crate::constants::SCAN_METADATA_CONCURRENCY;
use crate::constants::TARGET_DIR;
use crate::http::HttpClient;
use crate::lint::LintRuns;
#[cfg(test)]
use crate::lint::LintStatus;
use crate::project::AbsolutePath;
use crate::project::GitStatus;
use crate::project::Package;
use crate::project::ProjectFields;
use crate::project::RustProject;
use crate::project::Workspace;
use crate::project::WorkspaceMetadataStore;
use crate::scan;
use crate::scan::BackgroundMsg;
#[cfg(test)]
#[allow(
clippy::expect_used,
reason = "tests should panic on unexpected values"
)]
#[allow(
clippy::unwrap_used,
reason = "tests should panic on unexpected values"
)]
#[allow(clippy::panic, reason = "tests should panic on unexpected values")]
#[allow(
clippy::unreachable,
reason = "tests should panic on unexpected values"
)]
mod tests;
use anyhow::Error;
use async_tasks::Startup;
pub(crate) use target_index::CleanSelection;
pub(crate) use target_index::TargetDirIndex;
use tui_pane::AppContext;
use tui_pane::ClipboardBackend;
use tui_pane::CopyOutcome;
use tui_pane::FocusedPane;
use tui_pane::Framework;
use tui_pane::FrameworkFocusId;
use tui_pane::GlobalAction;
use tui_pane::Keymap as FrameworkKeymap;
use tui_pane::PaneRegistry;
use tui_pane::SettingsPane;
use tui_pane::SystemClipboard;
use tui_pane::ToastTaskId;
pub(super) use types::CiRunDisplayMode;
pub(crate) use types::ConfirmAction;
pub(super) use types::DirtyState;
pub(super) use types::DiscoveryRowKind;
pub(super) use types::DiscoveryShimmer;
pub(super) use types::FinderState;
pub(super) use types::HoveredPaneRow;
pub(crate) use types::PendingClean;
pub(super) use types::PollBackgroundStats;
#[cfg(test)]
pub(super) use types::RetrySpawnMode;
pub(super) use types::ScanState;
pub(super) use types::SelectionPaths;
pub(super) use types::SelectionSync;
use super::columns;
pub(super) use super::columns::ProjectListWidths;
use super::interaction;
use super::overlays::FinderPane;
use super::pane::PaneRenderCtx;
use super::panes;
use super::panes::BottomRow;
use super::panes::CpuPane;
use super::panes::GitPane;
use super::panes::LangPane;
use super::panes::OutputPane;
use super::panes::PackagePane;
use super::panes::PaneBehavior;
use super::panes::ProjectListPane;
use super::panes::TargetsPane;
pub(super) use super::project_list::ExpandKey;
pub(super) use super::project_list::VisibleRow;
use super::settings;
use super::settings::SettingOption;
use super::settings::SettingsRenderInputs;
use super::settings::StartupSettings;
pub(super) use super::state::AvailabilityStatus;
use super::state::Ci;
use super::state::CiStatusLookup;
use super::state::Lint;
use super::state::Net;
use crate::project;
use crate::project::RootItem;
use crate::scan::MetadataDispatchContext;
pub(super) struct RenderBorrows<'a> {
pub registry: RenderRegistry<'a>,
pub ctx: PaneRenderCtx<'a>,
}
#[derive(Clone, Copy)]
pub(super) struct OverlayRenderInputs<'a> {
settings: Option<&'a SettingsRenderInputs>,
}
impl<'a> OverlayRenderInputs<'a> {
pub(super) const fn none() -> Self { Self { settings: None } }
pub(super) const fn settings(inputs: &'a SettingsRenderInputs) -> Self {
Self {
settings: Some(inputs),
}
}
}
pub(super) struct RenderRegistry<'a> {
pub package: &'a mut PackagePane,
pub lang: &'a mut LangPane,
pub cpu: &'a mut CpuPane,
pub git: &'a mut GitPane,
pub targets: &'a mut TargetsPane,
pub project_list: &'a mut ProjectListPane,
pub output: &'a mut OutputPane,
pub lint: &'a mut Lint,
pub ci: &'a mut Ci,
pub settings_pane: &'a mut SettingsPane,
}
impl PaneRegistry for RenderRegistry<'_> {
type Ctx<'ctx> = PaneRenderCtx<'ctx>;
type PaneId = PaneId;
fn pane_mut(
&mut self,
id: Self::PaneId,
) -> Option<&mut dyn for<'ctx> tui_pane::Renderable<Self::Ctx<'ctx>>> {
let pane: &mut dyn for<'ctx> tui_pane::Renderable<Self::Ctx<'ctx>> = match id {
PaneId::Package => self.package,
PaneId::Lang => self.lang,
PaneId::Cpu => self.cpu,
PaneId::Git => self.git,
PaneId::Targets => self.targets,
PaneId::ProjectList => self.project_list,
PaneId::Output => self.output,
PaneId::Lints => self.lint,
PaneId::CiRuns => self.ci,
PaneId::Settings => self.settings_pane,
PaneId::Keymap | PaneId::Toasts | PaneId::Finder | PaneId::Sccache => return None,
};
Some(pane)
}
}
pub(super) struct FinderSplit<'a> {
pub finder_pane: &'a mut FinderPane,
pub config: &'a Config,
pub project_list: &'a ProjectList,
pub inflight: &'a Inflight,
pub scan: &'a Scan,
pub running_targets: &'a RunningTargets,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(super) enum CargoPortToastAction {
OpenPath(AbsolutePath),
}
impl From<AbsolutePath> for CargoPortToastAction {
fn from(path: AbsolutePath) -> Self { Self::OpenPath(path) }
}
pub(super) struct App {
pub(super) net: Net,
pub(super) panes: Panes,
pub(super) background: Background,
pub(super) inflight: Inflight,
pub(super) lint: Lint,
pub(super) ci: Ci,
pub(super) config: Config,
pub(super) keymap: Keymap,
pub(super) themes: ThemeRuntime,
pub(super) sync_tracker: SyncTracker,
pub(super) git_status_tracker: GitStatusTracker,
pub(super) project_list: ProjectList,
pub(super) scan: Scan,
pub(super) startup: Startup,
pub(super) visited_panes: HashSet<AppPaneId>,
pub(super) overlays: Overlays,
confirm: Option<ConfirmAction>,
pub(super) animation_started: Instant,
pub(super) mouse_pos: Option<Position>,
pub(super) framework: Framework<Self>,
pub(super) framework_keymap: Rc<FrameworkKeymap<Self>>,
pub(super) pending_nav_chord: Vec<KeyBind>,
}
impl App {
pub(super) fn new(
projects: &[RootItem],
background_tx: Sender<BackgroundMsg>,
background_rx: Receiver<BackgroundMsg>,
startup_settings: StartupSettings,
http_client: HttpClient,
scan_started_at: Instant,
metadata_store: Arc<Mutex<WorkspaceMetadataStore>>,
) -> Result<Self, Error> {
construct::AppBuilder::new(
projects,
background_tx,
background_rx,
startup_settings,
http_client,
scan_started_at,
metadata_store,
)
.open_channels()
.run_startup()
.build()
}
pub(super) fn selected_row_owns_lint(&self) -> bool {
match self.project_list.selected_row() {
Some(
VisibleRow::Root { .. }
| VisibleRow::WorktreeEntry { .. }
| VisibleRow::WorktreeGroupHeader { .. },
) => true,
Some(
VisibleRow::GroupHeader { .. }
| VisibleRow::Member { .. }
| VisibleRow::MemberVendored { .. }
| VisibleRow::Vendored { .. }
| VisibleRow::Submodule { .. }
| VisibleRow::WorktreeMember { .. }
| VisibleRow::WorktreeMemberVendored { .. }
| VisibleRow::WorktreeVendored { .. },
)
| None => false,
}
}
#[cfg(test)]
pub(super) fn lint_cell(&self, status: &LintStatus) -> LintCell {
if !self.config.lint_enabled() {
return LintCell::from_parts(
crate::constants::LINT_NO_LOG,
ratatui::style::Style::default(),
);
}
let icon =
integration::lint_icon_for(status.kind()).frame_at(self.animation_started.elapsed());
let style = if matches!(status, LintStatus::Running(_)) {
ratatui::style::Style::default().fg(tui_pane::accent_color())
} else {
ratatui::style::Style::default()
};
LintCell::from_parts(icon, style)
}
pub(super) fn prune_toasts(&mut self) {
let now = Instant::now();
self.framework.toasts.prune_tracked_items(now);
self.framework.toasts.prune(now);
if self.base_focus() == PaneId::Toasts && self.framework.toasts.active_now().is_empty() {
self.set_focus_to_pane(PaneId::ProjectList);
}
}
pub(super) fn animation_timeout(&self) -> Duration {
const IDLE_HEARTBEAT: Duration = Duration::from_secs(1);
const ANIMATION_TICK: Duration = Duration::from_millis(80);
if self.is_animating() {
ANIMATION_TICK
} else {
IDLE_HEARTBEAT
}
}
fn is_animating(&self) -> bool {
self.scan.needs_animation()
|| self.project_list.has_running_lints()
|| self.inflight.needs_animation()
|| self.net.github.has_pr_check_polls()
|| !self.framework.toasts.active_now().is_empty()
}
pub(super) fn show_timed_toast(&mut self, title: impl Into<String>, body: impl Into<String>) {
self.framework.toasts.push_status(title, body);
}
pub(super) fn copy_focused_selection(&mut self) {
let mut backend = SystemClipboard::new();
self.copy_focused_selection_with_backend(&mut backend);
}
pub(super) fn copy_focused_selection_with_backend<B>(&mut self, backend: &mut B)
where
B: ClipboardBackend,
{
let outcome = self.framework.copy_selection(self, backend);
if matches!(outcome, CopyOutcome::Copied { .. }) && self.focus_is(PaneId::Output) {
let live = self.inflight.example_output().to_vec();
let count = self.panes.output.selection_line_count(&live);
self.panes.output.collapse_to_tail();
let lines = if count == 1 { "line" } else { "lines" };
self.show_timed_toast("Copy", format!("Copied {count} {lines}"));
return;
}
self.show_copy_outcome(outcome);
}
fn show_copy_outcome(&mut self, outcome: CopyOutcome) {
match outcome {
CopyOutcome::Copied { label } => {
self.show_timed_toast("Copy", format!("Copied {}", label.noun()));
},
CopyOutcome::NothingToCopy => self.show_timed_toast("Copy", "Nothing to copy"),
CopyOutcome::Unavailable { reason } => {
self.show_timed_toast("Clipboard unavailable", reason.to_string());
},
CopyOutcome::Failed { reason } => {
self.show_timed_toast("Copy failed", reason.to_string());
},
}
}
pub(super) fn show_timed_warning_toast(
&mut self,
title: impl Into<String>,
body: impl Into<String>,
) {
self.framework
.toasts
.push_status_styled(title, body, Warning);
}
pub(super) fn finish_task_toast(&mut self, task_id: ToastTaskId) {
self.framework.toasts.finish_task(task_id);
self.prune_toasts();
}
pub(super) fn finish_body_toast_with_countdown(&mut self, task_id: ToastTaskId) {
let linger = self.framework.toast_settings().finished_task_visible.get();
self.framework.toasts.finish_task_lingering(task_id, linger);
self.prune_toasts();
}
pub(super) fn set_task_tracked_items(&mut self, task_id: ToastTaskId, items: &[TrackedItem]) {
self.framework.toasts.set_tracked_items(task_id, items);
}
pub(super) fn start_clean(&mut self, project_path: &AbsolutePath) -> bool {
let target_dir = self
.scan
.resolve_target_dir(project_path)
.unwrap_or_else(|| AbsolutePath::from(project_path.as_path().join(TARGET_DIR)));
if !target_dir.as_path().exists() {
let name = project::home_relative_path(project_path.as_path());
self.show_timed_toast("Already clean", name);
return false;
}
self.inflight
.clean_mut()
.insert(project_path.clone(), Instant::now());
self.sync_running_clean_toast();
true
}
pub(super) fn clean_spawn_failed(&mut self, project_path: &AbsolutePath) {
self.inflight.clean_mut().remove(project_path.as_path());
self.sync_running_clean_toast();
}
pub(super) fn dismiss_toast(&mut self, id: ToastId) {
self.framework.toasts.dismiss(id);
self.prune_toasts();
}
pub(super) fn register_discovery_shimmer(&mut self, path: &Path) {
if !self.scan.is_complete() || !self.config.discovery_shimmer_enabled() {
return;
}
let shimmer =
types::DiscoveryShimmer::new(Instant::now(), self.config.discovery_shimmer_duration());
self.scan
.discovery_shimmers_mut()
.insert(AbsolutePath::from(path), shimmer);
}
#[cfg(test)]
pub(super) fn discovery_name_segments_for_path(
&self,
row_path: &Path,
name: &str,
git_status: Option<GitStatus>,
row_kind: DiscoveryRowKind,
) -> Option<Vec<StyledSegment>> {
discovery_name_segments_for_path_with_refs(
&self.scan,
&self.config,
&self.project_list,
row_path,
name,
git_status,
row_kind,
)
}
pub(super) fn prune_inactive_project_state(&mut self) {
let mut all_paths: HashSet<AbsolutePath> = HashSet::new();
self.project_list.for_each_leaf_path(|path, _| {
all_paths.insert(AbsolutePath::from(path));
});
self.scan
.pending_git_first_commit_mut()
.retain(|path, _| all_paths.contains(path));
self.ci
.fetch_tracker
.retain(|path| all_paths.contains(path));
}
pub(super) fn lint_at_path(&self, path: &Path) -> Option<&LintRuns> {
self.project_list.lint_at_path(path)
}
pub(super) fn lint_at_path_mut(&mut self, path: &Path) -> Option<&mut LintRuns> {
self.project_list.lint_at_path_mut(path)
}
pub(super) fn clear_all_lint_state(&mut self) {
let mut paths = Vec::new();
self.project_list.for_each_leaf_path(|path, is_rust| {
if is_rust {
paths.push(path.to_path_buf());
}
});
for path in &paths {
if let Some(lr) = self.project_list.lint_at_path_mut(path) {
lr.clear_runs();
}
}
}
pub(super) const fn split_for_render<'a>(
&'a mut self,
selected_project_path: Option<&'a Path>,
animation_elapsed: Duration,
ci_status_lookup: &'a CiStatusLookup,
overlay_inputs: OverlayRenderInputs<'a>,
synced_description_height: SyncedDescriptionHeight,
) -> RenderBorrows<'a> {
let Self {
panes,
lint,
ci,
config,
project_list,
inflight,
scan,
framework,
..
} = self;
let running_targets = panes.running_targets.snapshot();
let registry = RenderRegistry {
package: &mut panes.package,
lang: &mut panes.lang,
cpu: &mut panes.cpu,
git: &mut panes.git,
targets: &mut panes.targets,
project_list: &mut panes.project_list,
output: &mut panes.output,
lint,
ci,
settings_pane: &mut framework.settings_pane,
};
let ctx = PaneRenderCtx {
animation_elapsed,
config,
project_list,
selected_project_path,
inflight,
scan,
ci_status_lookup,
settings_render_inputs: overlay_inputs.settings,
synced_description_height,
running_targets,
};
RenderBorrows { registry, ctx }
}
pub(super) const fn split_finder_for_render(&mut self) -> FinderSplit<'_> {
FinderSplit {
finder_pane: &mut self.overlays.finder_pane,
config: &self.config,
project_list: &self.project_list,
inflight: &self.inflight,
scan: &self.scan,
running_targets: self.panes.running_targets.snapshot(),
}
}
pub(super) fn selected_project_path_for_render(&self) -> Option<&Path> {
self.project_list.selected_project_path()
}
pub(super) fn apply_hovered_pane_row(&mut self) { interaction::apply_hovered_pane_row(self); }
#[cfg(test)]
pub(super) fn set_confirm(&mut self, action: ConfirmAction) { self.confirm = Some(action); }
pub fn request_clean_confirm(&mut self, project_path: AbsolutePath) {
if self.scan.should_verify_before_clean(&project_path) {
let dispatch = self.clean_metadata_dispatch();
scan::spawn_cargo_metadata_refresh(dispatch, project_path.clone());
self.scan.set_confirm_verifying(Some(project_path.clone()));
} else {
self.scan.set_confirm_verifying(None);
}
self.confirm = Some(ConfirmAction::Clean(project_path));
}
pub fn request_clean_group_confirm(
&mut self,
primary: AbsolutePath,
linked: Vec<AbsolutePath>,
) {
if self.scan.should_verify_before_clean(&primary) {
let dispatch = self.clean_metadata_dispatch();
scan::spawn_cargo_metadata_refresh(dispatch, primary.clone());
self.scan.set_confirm_verifying(Some(primary.clone()));
} else {
self.scan.set_confirm_verifying(None);
}
self.confirm = Some(ConfirmAction::CleanGroup { primary, linked });
}
pub fn request_kill_confirm(&mut self, label: String, pid: u32, create_time: u64) {
self.confirm = Some(ConfirmAction::KillTarget {
label,
pid,
create_time,
});
}
pub(super) fn metadata_dispatch(&self) -> MetadataDispatchContext {
MetadataDispatchContext {
handle: self.net.http_client.handle.clone(),
tx: self.background.background_sender(),
metadata_store: Arc::clone(self.scan.metadata_store()),
metadata_limit: Arc::new(tokio::sync::Semaphore::new(SCAN_METADATA_CONCURRENCY)),
}
}
fn clean_metadata_dispatch(&self) -> MetadataDispatchContext { self.metadata_dispatch() }
pub(super) const fn confirm(&self) -> Option<&ConfirmAction> { self.confirm.as_ref() }
pub(super) fn set_example_output(&mut self, output: Vec<String>) {
let was_empty = self.inflight.example_output_is_empty();
self.inflight.set_example_output(output);
if was_empty && !self.inflight.example_output_is_empty() {
self.panes.output.reset_for_open();
self.set_focus_to_pane(PaneId::Output);
}
}
pub(super) const fn mutate_tree(&mut self) -> TreeMutation<'_> {
let include_non_rust = self
.config
.current()
.tui
.include_non_rust
.includes_non_rust();
let Self {
project_list: projects,
panes,
..
} = self;
TreeMutation {
projects,
panes,
include_non_rust,
}
}
pub(super) const fn take_confirm(&mut self) -> Option<ConfirmAction> { self.confirm.take() }
pub(super) fn owner_repo_for_path(&self, path: &Path) -> Option<OwnerRepo> {
self.project_list.owner_repo_for_path_inner(path)
}
pub(super) fn ci_toggle_available_for(&self, path: &Path) -> bool {
self.project_list.ci_toggle_available_for_inner(path)
}
pub(super) fn set_ci_display_mode_for(&mut self, path: &Path, mode: CiRunDisplayMode) {
self.set_ci_display_mode_for_inner(path, mode);
}
pub(super) fn reset_cpu_placeholder(&mut self) {
self.panes.reset_cpu(&self.config.current().cpu);
}
pub(super) fn force_settings_if_unconfigured(&mut self) {
if !self.config.current().tui.include_dirs.is_empty() {
return;
}
self.dispatch_framework_global_action(GlobalAction::OpenSettings);
if let Some(idx) = settings::selection_index_for_setting(self, SettingOption::IncludeDirs) {
self.framework.settings_pane.viewport_mut().set_pos(idx);
}
self.overlays
.set_inline_error("Configure at least one include directory before continuing");
}
fn dispatch_framework_global_action(&mut self, action: GlobalAction) {
let keymap = Rc::clone(&self.framework_keymap);
keymap.dispatch_framework_global(action, self);
}
pub(super) fn rebuild_framework_keymap_from_disk(&mut self) -> Result<(), String> {
let framework_builder = FrameworkKeymap::<Self>::builder().vim_mode(
integration::vim_mode_from_config(self.config.current().tui.navigation_keys),
);
let framework_builder = if let Some(path) = self.keymap.path().map(Path::to_path_buf) {
let display_path = path.display().to_string();
keymap::migrate_removed_action_keys_on_disk(&path).map_err(|err| {
format!("migrating removed keymap actions in {display_path}: {err}")
})?;
framework_builder
.load_toml(path)
.map_err(|err| format!("loading keymap from {display_path}: {err}"))?
} else {
framework_builder
};
let framework_keymap =
integration::build_framework_keymap(framework_builder, &mut self.framework)
.map_err(|err| format!("building framework keymap: {err}"))?;
self.framework_keymap = Rc::new(framework_keymap);
Ok(())
}
pub(super) fn close_framework_overlay_if_open(&mut self) {
if self.framework.overlay().is_some() {
self.dispatch_framework_global_action(GlobalAction::Dismiss);
}
}
pub(super) const fn focused_pane_id(&self) -> PaneId {
Self::pane_id_for_focus(*self.framework.focused())
}
pub(super) fn focus_is(&self, pane: PaneId) -> bool { self.focused_pane_id() == pane }
pub(super) fn reconcile_bottom_row_focus(&mut self) {
let output_active = !self.inflight.example_output_is_empty();
match (output_active, self.focused_pane_id()) {
(true, PaneId::Lints | PaneId::CiRuns) => self.set_focus_to_pane(PaneId::Output),
(false, PaneId::Output) => self.set_focus_to_pane(PaneId::Targets),
_ => {},
}
}
pub(super) fn base_focus(&self) -> PaneId {
if self.overlays.is_finder_open() && self.focus_is(PaneId::Finder) {
return self
.overlays
.finder_return()
.map_or(PaneId::ProjectList, Self::pane_id_for_focus);
}
self.focused_pane_id()
}
pub(super) fn pane_focus_state(&self, pane: PaneId) -> PaneFocusState {
if self.focus_is(pane) {
return PaneFocusState::Active;
}
AppPaneId::from_legacy(pane).map_or(PaneFocusState::Inactive, |id| {
if self.visited_panes.contains(&id) {
PaneFocusState::Remembered
} else {
PaneFocusState::Inactive
}
})
}
pub(super) fn set_focus_to_pane(&mut self, pane: PaneId) {
match AppPaneId::from_legacy(pane) {
Some(id) => self.set_focus(FocusedPane::App(id)),
None if pane == PaneId::Toasts => {
self.set_focus(FocusedPane::Framework(FrameworkFocusId::Toasts));
},
None => {},
}
}
const fn pane_id_for_focus(focus: FocusedPane<AppPaneId>) -> PaneId {
match focus {
FocusedPane::App(id) => id.to_legacy(),
FocusedPane::Framework(FrameworkFocusId::Toasts) => PaneId::Toasts,
}
}
pub(super) fn is_pane_tabbable(&self, pane: PaneId) -> bool {
match panes::behavior(pane) {
PaneBehavior::ProjectList => true,
PaneBehavior::DetailFields => match pane {
PaneId::Package => self.project_list.selected_project_path().is_some(),
PaneId::Lang => self
.project_list
.selected_project_path()
.is_some_and(|path| {
self.project_list
.at_path(path)
.and_then(|p| p.language_stats.as_ref())
.is_some_and(|ls| !ls.entries.is_empty())
}),
PaneId::Git => self.panes.git.content().is_some_and(|g| {
g.head.is_some() || !g.remotes.is_empty() || !g.worktrees.is_empty()
}),
_ => false,
},
PaneBehavior::Cpu => self.panes.cpu.content().is_some(),
PaneBehavior::DetailTargets => {
self.panes
.targets
.content()
.is_some_and(panes::TargetsData::has_targets)
|| self.panes.running_targets.snapshot().has_instances()
},
PaneBehavior::Lints => {
self.inflight.example_output_is_empty()
&& self.lint.content().is_some_and(panes::LintsData::has_runs)
},
PaneBehavior::CiRuns => {
self.inflight.example_output_is_empty()
&& self.ci.content().is_some_and(panes::CiData::has_runs)
},
PaneBehavior::Output => !self.inflight.example_output_is_empty(),
PaneBehavior::Toasts => !self.framework.toasts.active_now().is_empty(),
PaneBehavior::Overlay => false,
}
}
pub(super) fn tabbable_panes(&self) -> Vec<PaneId> {
panes::tab_order(if self.inflight.example_output_is_empty() {
BottomRow::Diagnostics
} else {
BottomRow::Output
})
.into_iter()
.filter(|pane| self.is_pane_tabbable(*pane))
.chain(
self.is_pane_tabbable(PaneId::Toasts)
.then_some(PaneId::Toasts),
)
.collect()
}
pub(super) fn reset_project_panes(&mut self) {
self.panes.package.viewport.home();
self.panes.git.viewport.home();
self.panes.targets.viewport.home();
self.ci.viewport.home();
self.lint.viewport.home();
self.framework.toasts.viewport.home();
self.visited_panes.remove(&AppPaneId::Package);
self.visited_panes.remove(&AppPaneId::Git);
self.visited_panes.remove(&AppPaneId::Targets);
self.visited_panes.remove(&AppPaneId::CiRuns);
}
pub fn sync_selected_project(&mut self) {
self.ensure_visible_rows_cached();
let current = self
.project_list
.selected_project_path()
.map(AbsolutePath::from);
if self
.project_list
.paths
.collapsed_anchor
.as_ref()
.is_some_and(|anchor| current.as_ref() != Some(anchor))
{
self.project_list.paths.collapsed_selected = None;
self.project_list.paths.collapsed_anchor = None;
}
if self.project_list.paths.selected_project == current {
return;
}
self.project_list
.paths
.selected_project
.clone_from(¤t);
self.reset_project_panes();
let panes = self.tabbable_panes();
if !panes.contains(&self.base_focus()) {
self.set_focus_to_pane(PaneId::ProjectList);
}
if let Some(return_target) = self.overlays.finder_return()
&& !panes.contains(&Self::pane_id_for_focus(return_target))
{
self.overlays
.set_finder_return(FocusedPane::App(AppPaneId::ProjectList));
}
if let Some(abs_path) = current
&& self.project_list.paths.last_selected.as_ref() != Some(&abs_path)
{
self.scan.bump_generation();
self.project_list.paths.last_selected = Some(abs_path);
self.project_list.mark_sync_changed();
self.maybe_priority_fetch();
}
}
}
pub(super) struct TreeMutation<'a> {
projects: &'a mut ProjectList,
panes: &'a mut Panes,
include_non_rust: bool,
}
impl TreeMutation<'_> {
pub(super) fn replace_all(&mut self, projects: ProjectList) {
self.projects.replace_roots_from(projects);
}
pub(super) fn insert_into_hierarchy(
&mut self,
item: RootItem,
dispatch: &MetadataDispatchContext,
) -> bool {
let roots = scan::cargo_metadata_roots_for_item(&item);
let changed = self.projects.insert_into_hierarchy(item);
for root in roots {
scan::spawn_cargo_metadata_refresh(dispatch.clone(), root);
}
changed
}
pub(super) fn replace_leaf_by_path(
&mut self,
path: &Path,
item: RootItem,
dispatch: &MetadataDispatchContext,
) -> Option<RootItem> {
let roots = scan::cargo_metadata_roots_for_item(&item);
let previous = self.projects.replace_leaf_by_path(path, item);
for root in roots {
scan::spawn_cargo_metadata_refresh(dispatch.clone(), root);
}
previous
}
pub(super) fn regroup_members(&mut self, inline_dirs: &[String]) {
self.projects.regroup_members(inline_dirs);
}
pub(super) fn regroup_top_level_worktrees(&mut self) {
self.projects.regroup_top_level_worktrees();
}
}
impl Drop for TreeMutation<'_> {
fn drop(&mut self) {
self.panes.clear_for_tree_change();
self.projects.recompute_visibility(self.include_non_rust);
}
}
pub(super) fn discovery_name_segments_for_path_with_refs(
scan: &Scan,
config: &Config,
project_list: &ProjectList,
row_path: &Path,
name: &str,
git_status: Option<GitStatus>,
row_kind: DiscoveryRowKind,
) -> Option<Vec<StyledSegment>> {
if !config.discovery_shimmer_enabled() {
return None;
}
let now = Instant::now();
let (session_path, shimmer) =
discovery_shimmer_session_for_path(scan, project_list, row_path, now, row_kind)?;
let char_count = name.chars().count();
if char_count == 0 {
return None;
}
let base_style = columns::project_name_style(git_status);
let accent_style = columns::project_name_shimmer_style(git_status);
let window = discovery_shimmer_window_len(char_count);
let elapsed_ms =
usize::try_from(now.duration_since(shimmer.started_at).as_millis()).unwrap_or(usize::MAX);
let step = elapsed_ms / discovery_shimmer_step_millis();
let head = (step
+ discovery_shimmer_phase_offset(session_path.as_path(), row_path, row_kind, char_count))
% char_count;
Some(columns::build_shimmer_segments(
name,
base_style,
accent_style,
head,
window,
))
}
fn discovery_shimmer_session_for_path(
scan: &Scan,
project_list: &ProjectList,
row_path: &Path,
now: Instant,
row_kind: DiscoveryRowKind,
) -> Option<(AbsolutePath, DiscoveryShimmer)> {
scan.discovery_shimmers()
.iter()
.filter(|(session_path, shimmer)| {
shimmer.is_active_at(now)
&& discovery_shimmer_session_matches(
project_list,
session_path.as_path(),
row_path,
row_kind,
)
})
.max_by_key(|(_, shimmer)| shimmer.started_at)
.map(|(session_path, shimmer)| (session_path.clone(), *shimmer))
}
fn discovery_shimmer_session_matches(
project_list: &ProjectList,
session_path: &Path,
row_path: &Path,
row_kind: DiscoveryRowKind,
) -> bool {
discovery_scope_contains(project_list, session_path, row_path)
|| discovery_parent_row(project_list, session_path).is_some_and(|parent| {
parent.path.as_path() == row_path && row_kind.allows_parent_kind(parent.kind)
})
}
fn discovery_scope_contains(
project_list: &ProjectList,
session_path: &Path,
row_path: &Path,
) -> bool {
project_list
.iter()
.any(|item| root_item_scope_contains(item, session_path, row_path))
}
fn discovery_parent_row(
project_list: &ProjectList,
session_path: &Path,
) -> Option<DiscoveryParentRow> {
project_list
.iter()
.find_map(|item| root_item_parent_row(item, session_path))
}
const fn discovery_shimmer_window_len(char_count: usize) -> usize {
match char_count {
0 => 0,
1..=2 => 1,
3..=5 => 2,
6..=8 => 3,
_ => 4,
}
}
const fn discovery_shimmer_step_millis() -> usize { 85 }
fn discovery_shimmer_phase_offset(
session_path: &Path,
row_path: &Path,
row_kind: DiscoveryRowKind,
char_count: usize,
) -> usize {
if char_count == 0 {
return 0;
}
let mut hash = 0xcbf2_9ce4_8422_2325_u64;
let key = format!(
"{}|{}|{}",
session_path.to_string_lossy(),
row_path.to_string_lossy(),
row_kind.discriminant()
);
for byte in key.as_bytes() {
hash ^= u64::from(*byte);
hash = hash.wrapping_mul(0x0100_0000_01b3);
}
usize::try_from(hash % u64::try_from(char_count).unwrap_or(1)).unwrap_or(0)
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct DiscoveryParentRow {
path: AbsolutePath,
kind: DiscoveryRowKind,
}
fn package_contains_path(pkg: &Package, row_path: &Path) -> bool {
pkg.path() == row_path
|| pkg
.vendored()
.iter()
.any(|vendored| vendored.path() == row_path)
}
fn workspace_contains_path(ws: &Workspace, row_path: &Path) -> bool {
ws.path() == row_path
|| ws.groups().iter().any(|group| {
group
.members()
.iter()
.any(|member| package_contains_path(member, row_path))
})
|| ws
.vendored()
.iter()
.any(|vendored| vendored.path() == row_path)
}
fn root_item_scope_contains(item: &RootItem, session_path: &Path, row_path: &Path) -> bool {
match item {
RootItem::Rust(RustProject::Workspace(ws)) => {
workspace_scope_contains(ws, session_path, row_path)
},
RootItem::Rust(RustProject::Package(pkg)) => {
package_scope_contains(pkg, session_path, row_path)
},
RootItem::NonRust(project) => project.path() == session_path && project.path() == row_path,
RootItem::Worktrees(group) => group.iter_entries().any(|entry| match entry {
RustProject::Workspace(ws) => workspace_scope_contains(ws, session_path, row_path),
RustProject::Package(pkg) => package_scope_contains(pkg, session_path, row_path),
}),
}
}
fn workspace_scope_contains(ws: &Workspace, session_path: &Path, row_path: &Path) -> bool {
if ws.path() == session_path {
return workspace_contains_path(ws, row_path);
}
if ws
.vendored()
.iter()
.any(|vendored| vendored.path() == session_path && vendored.path() == row_path)
{
return true;
}
ws.groups().iter().any(|group| {
group
.members()
.iter()
.any(|member| package_scope_contains(member, session_path, row_path))
})
}
fn package_scope_contains(pkg: &Package, session_path: &Path, row_path: &Path) -> bool {
if pkg.path() == session_path {
return package_contains_path(pkg, row_path);
}
pkg.vendored()
.iter()
.any(|vendored| vendored.path() == session_path && vendored.path() == row_path)
}
fn root_item_parent_row(item: &RootItem, session_path: &Path) -> Option<DiscoveryParentRow> {
match item {
RootItem::Rust(RustProject::Workspace(ws)) => {
workspace_parent_row(ws, session_path, DiscoveryRowKind::Root)
},
RootItem::Rust(RustProject::Package(pkg)) => {
package_parent_row(pkg, session_path, DiscoveryRowKind::Root)
},
RootItem::NonRust(_) => None,
RootItem::Worktrees(group) => {
if group.primary.path() == session_path {
return None;
}
if group.linked.iter().any(|l| l.path() == session_path) {
return Some(DiscoveryParentRow {
path: group.primary.path().clone(),
kind: DiscoveryRowKind::Root,
});
}
group.iter_entries().find_map(|entry| match entry {
RustProject::Workspace(ws) => {
workspace_parent_row(ws, session_path, DiscoveryRowKind::WorktreeEntry)
},
RustProject::Package(pkg) => {
package_parent_row(pkg, session_path, DiscoveryRowKind::WorktreeEntry)
},
})
},
}
}
fn workspace_parent_row(
ws: &Workspace,
session_path: &Path,
parent_kind: DiscoveryRowKind,
) -> Option<DiscoveryParentRow> {
if ws.path() == session_path {
return None;
}
if ws
.vendored()
.iter()
.any(|vendored| vendored.path() == session_path)
{
return Some(DiscoveryParentRow {
path: ws.path().clone(),
kind: parent_kind,
});
}
for group in ws.groups() {
for member in group.members() {
if member.path() == session_path {
return Some(DiscoveryParentRow {
path: ws.path().clone(),
kind: parent_kind,
});
}
if let Some(parent) =
package_parent_row(member, session_path, DiscoveryRowKind::PathOnly)
{
return Some(parent);
}
}
}
None
}
fn package_parent_row(
pkg: &Package,
session_path: &Path,
parent_kind: DiscoveryRowKind,
) -> Option<DiscoveryParentRow> {
if pkg.path() == session_path {
return None;
}
pkg.vendored()
.iter()
.any(|vendored| vendored.path() == session_path)
.then(|| DiscoveryParentRow {
path: pkg.path().clone(),
kind: parent_kind,
})
}