use std::path::Path;
use std::time::Duration;
use std::time::Instant;
use ratatui::Frame;
use ratatui::layout::Position;
use ratatui::layout::Rect;
use tui_pane::Hittable;
use tui_pane::RenderFocus;
use tui_pane::Renderable;
use tui_pane::RunningTracker;
use tui_pane::ToastTaskId;
use tui_pane::TrackedItem;
use tui_pane::Viewport;
use super::Config;
use crate::constants::LINT_NO_LOG;
use crate::lint::CacheUsage;
use crate::lint::LintRunOrigin;
use crate::lint::LintStatus;
use crate::lint::LintStatusKind;
use crate::lint::RuntimeHandle;
use crate::project;
use crate::project::AbsolutePath;
use crate::project::RootItem;
use crate::project::Visibility;
use crate::tui::columns::LintCell;
use crate::tui::integration;
use crate::tui::pane::HoverTarget;
use crate::tui::pane::PaneRenderCtx;
use crate::tui::panes;
use crate::tui::panes::LintsData;
use crate::tui::panes::PaneId;
use crate::tui::project_list::ProjectList;
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub enum LintDisplay {
#[default]
NotRust,
NoRuns,
Runs {
count: usize,
status: LintStatus,
},
}
const NORMAL_LINT_TOAST_TITLE: &str = "Lints";
const CATCH_UP_LINT_TOAST_TITLE: &str = "Catch-up lints";
pub struct Lint {
runtime: Option<RuntimeHandle>,
running: RunningTracker<AbsolutePath>,
catch_up_running: RunningTracker<AbsolutePath>,
pub cache_usage: CacheUsage,
pub viewport: Viewport,
pub focus: RenderFocus,
content: Option<LintsData>,
}
impl Lint {
pub fn new(runtime: Option<RuntimeHandle>) -> Self {
Self {
runtime,
running: RunningTracker::new(),
catch_up_running: RunningTracker::new(),
cache_usage: CacheUsage::default(),
viewport: Viewport::new(),
focus: RenderFocus::inactive(),
content: None,
}
}
pub const fn content(&self) -> Option<&LintsData> { self.content.as_ref() }
pub fn set_content(&mut self, data: LintsData) { self.content = Some(data); }
pub fn clear_content(&mut self) { self.content = None; }
pub const fn runtime(&self) -> Option<&RuntimeHandle> { self.runtime.as_ref() }
pub fn runtime_clone(&self) -> Option<RuntimeHandle> { self.runtime.clone() }
pub fn set_runtime(&mut self, handle: Option<RuntimeHandle>) { self.runtime = handle; }
pub fn apply_lint_status(
&mut self,
path: AbsolutePath,
kind: LintStatusKind,
origin: LintRunOrigin,
) {
match kind {
LintStatusKind::Running => {
self.remove_from_other_running_toast(origin, path.as_path());
let tracker = self.running_tracker_mut(origin);
tracker.running.entry(path).or_insert_with(Instant::now);
},
LintStatusKind::Passed
| LintStatusKind::Failed
| LintStatusKind::Stale
| LintStatusKind::NoLog => self.clear_running_path(path.as_path()),
}
}
pub fn clear_running_path(&mut self, path: &Path) {
self.running.remove(path);
self.catch_up_running.remove(path);
}
pub fn toast_items_for_origin(
&self,
origin: LintRunOrigin,
) -> (Option<ToastTaskId>, Vec<TrackedItem>) {
self.running_tracker(origin).items_for_toast(
|p| project::home_relative_path(p.as_path()),
integration::path_key,
)
}
pub const fn running_toast_title(origin: LintRunOrigin) -> &'static str {
match origin {
LintRunOrigin::CatchUp => CATCH_UP_LINT_TOAST_TITLE,
LintRunOrigin::Normal => NORMAL_LINT_TOAST_TITLE,
}
}
pub const fn set_running_toast_for_origin(
&mut self,
origin: LintRunOrigin,
toast: Option<ToastTaskId>,
) {
match origin {
LintRunOrigin::CatchUp => self.catch_up_running.toast = toast,
LintRunOrigin::Normal => self.running.toast = toast,
}
}
#[cfg(test)]
pub fn running_toast_is_empty(&self) -> bool {
self.running.is_empty() && self.catch_up_running.is_empty()
}
#[cfg(test)]
pub const fn running_toast_id(&self) -> Option<ToastTaskId> { self.running.toast }
pub fn running_toast_path_count(&self) -> usize {
self.running.running.len() + self.catch_up_running.running.len()
}
#[cfg(test)]
pub fn running_toast_contains_path(&self, path: &Path) -> bool {
self.running.running.contains_key(path) || self.catch_up_running.running.contains_key(path)
}
#[cfg(test)]
pub fn normal_running_toast_contains_path(&self, path: &Path) -> bool {
self.running.running.contains_key(path)
}
#[cfg(test)]
pub fn catch_up_running_toast_contains_path(&self, path: &Path) -> bool {
self.catch_up_running.running.contains_key(path)
}
const fn running_tracker(&self, origin: LintRunOrigin) -> &RunningTracker<AbsolutePath> {
match origin {
LintRunOrigin::CatchUp => &self.catch_up_running,
LintRunOrigin::Normal => &self.running,
}
}
const fn running_tracker_mut(
&mut self,
origin: LintRunOrigin,
) -> &mut RunningTracker<AbsolutePath> {
match origin {
LintRunOrigin::CatchUp => &mut self.catch_up_running,
LintRunOrigin::Normal => &mut self.running,
}
}
fn remove_from_other_running_toast(&mut self, origin: LintRunOrigin, path: &Path) {
match origin {
LintRunOrigin::CatchUp => {
self.running.remove(path);
},
LintRunOrigin::Normal => {
self.catch_up_running.remove(path);
},
}
}
pub const fn set_cache_usage(&mut self, usage: CacheUsage) { self.cache_usage = usage; }
pub fn status_for_path(projects: &ProjectList, path: &Path) -> LintStatus {
projects
.lint_at_path(path)
.map_or(LintStatus::NoLog, |lr| lr.status().clone())
}
pub fn status_for_root(item: &RootItem) -> LintStatus { item.lint_rollup_status() }
pub fn status_for_worktree(item: &RootItem, worktree_index: usize) -> LintStatus {
match item {
RootItem::Worktrees(group) => group.lint_status_for_worktree(worktree_index),
_ => LintStatus::NoLog,
}
}
pub fn run_count_at(projects: &ProjectList, path: &Path) -> usize {
projects.lint_at_path(path).map_or(0, |lr| lr.runs().len())
}
pub fn package_display(
projects: &ProjectList,
abs: &AbsolutePath,
is_worktree_group: bool,
is_rust: bool,
) -> LintDisplay {
if !is_rust {
return LintDisplay::NotRust;
}
let path = abs.as_path();
let (status, count) = if is_worktree_group {
let group_item = projects.iter().find(|entry| {
entry.item.path() == abs && matches!(&entry.item, RootItem::Worktrees(_))
});
match group_item.map(|entry| &entry.item) {
Some(item @ RootItem::Worktrees(group)) => {
let status = Self::status_for_root(item);
let count: usize = group
.iter_entries()
.filter(|entry| entry.visibility() == Visibility::Visible)
.map(|entry| Self::run_count_at(projects, entry.path().as_path()))
.sum();
(status, count)
},
_ => (
Self::status_for_path(projects, path),
Self::run_count_at(projects, path),
),
}
} else {
(
Self::status_for_path(projects, path),
Self::run_count_at(projects, path),
)
};
if count == 0 && !matches!(status, LintStatus::Running(_)) {
LintDisplay::NoRuns
} else {
LintDisplay::Runs { count, status }
}
}
}
pub fn lint_cell_for(
status: &LintStatus,
config: &Config,
animation_elapsed: Duration,
) -> LintCell {
if !config.lint_enabled() {
return LintCell::from_parts(LINT_NO_LOG, ratatui::style::Style::default());
}
let icon = integration::lint_icon_for(status.kind()).frame_at(animation_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)
}
impl Renderable<PaneRenderCtx<'_>> for Lint {
fn render(&mut self, frame: &mut Frame<'_>, area: Rect, ctx: &PaneRenderCtx<'_>) {
panes::render_lints_pane_body(frame, area, self, ctx);
}
}
impl Hittable<HoverTarget> for Lint {
fn hit_test_at(&self, pos: Position) -> Option<HoverTarget> {
let row = panes::hit_test_table_row(&self.viewport, pos)?;
Some(HoverTarget::PaneRow {
pane: PaneId::Lints,
row,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_starts_with_no_runtime_and_empty_inflight() {
let lint = Lint::new(None);
assert!(lint.runtime().is_none());
assert!(lint.running_toast_is_empty());
assert!(lint.running_toast_id().is_none());
}
#[test]
fn running_toast_round_trip() {
let mut lint = Lint::new(None);
lint.set_running_toast_for_origin(LintRunOrigin::Normal, Some(ToastTaskId(7)));
assert_eq!(lint.running_toast_id(), Some(tui_pane::ToastTaskId(7)));
lint.set_running_toast_for_origin(LintRunOrigin::Normal, None);
assert!(lint.running_toast_id().is_none());
}
#[test]
fn running_lint_toasts_are_separate_by_origin() {
let mut lint = Lint::new(None);
let path = AbsolutePath::from(Path::new("/abs/a"));
lint.apply_lint_status(
path.clone(),
LintStatusKind::Running,
LintRunOrigin::CatchUp,
);
assert!(lint.catch_up_running_toast_contains_path(path.as_path()));
assert!(!lint.normal_running_toast_contains_path(path.as_path()));
lint.apply_lint_status(path.clone(), LintStatusKind::Running, LintRunOrigin::Normal);
assert!(!lint.catch_up_running_toast_contains_path(path.as_path()));
assert!(lint.normal_running_toast_contains_path(path.as_path()));
lint.apply_lint_status(path.clone(), LintStatusKind::Passed, LintRunOrigin::Normal);
assert!(!lint.running_toast_contains_path(path.as_path()));
}
#[test]
fn package_display_returns_not_rust_when_is_rust_false() {
let projects = ProjectList::default();
let abs = AbsolutePath::from(Path::new("/abs/x"));
assert_eq!(
Lint::package_display(&projects, &abs, false, false),
LintDisplay::NotRust,
);
}
#[test]
fn package_display_returns_no_runs_when_rust_with_zero_runs() {
let projects = ProjectList::default();
let abs = AbsolutePath::from(Path::new("/abs/x"));
assert_eq!(
Lint::package_display(&projects, &abs, false, true),
LintDisplay::NoRuns,
);
}
#[test]
fn run_count_at_returns_zero_for_unknown_path() {
let projects = ProjectList::default();
assert_eq!(Lint::run_count_at(&projects, Path::new("/abs/missing")), 0);
}
#[test]
fn status_for_path_returns_no_log_for_unknown_path() {
let projects = ProjectList::default();
assert_eq!(
Lint::status_for_path(&projects, Path::new("/abs/missing")),
LintStatus::NoLog,
);
}
}