use std::collections::HashSet;
use crate::compare::{BranchCompare, CompareTab};
use crate::git::{BlameLine, FileHistoryEntry, FilePatch, StashEntry};
use crate::i18n::Language;
use crate::insights::{ActionRecommendation, HandoffContext, ReviewPack};
use crate::navigation::ListNavigation;
use crate::pr::PrCreateState;
use crate::related_files::RelatedFiles;
use crate::review_queue::ReviewQueue;
use crate::stats::{
ChangeCouplingAnalysis, CodeOwnership, CommitImpactAnalysis, CommitQualityAnalysis,
FileHeatmap, ProjectHealth, RepoStats,
};
#[derive(Default)]
pub struct StatsViewState {
pub cache: Option<RepoStats>,
pub nav: ListNavigation,
}
#[derive(Default)]
pub struct HeatmapViewState {
pub cache: Option<FileHeatmap>,
pub nav: ListNavigation,
}
#[derive(Default)]
pub struct FileHistoryViewState {
pub cache: Option<Vec<FileHistoryEntry>>,
pub path: Option<String>,
pub nav: ListNavigation,
}
#[derive(Default)]
pub struct BlameViewState {
pub cache: Option<Vec<BlameLine>>,
pub path: Option<String>,
pub nav: ListNavigation,
}
#[derive(Default)]
pub struct OwnershipViewState {
pub cache: Option<CodeOwnership>,
pub nav: ListNavigation,
}
#[derive(Default)]
pub struct StashViewState {
pub cache: Option<Vec<StashEntry>>,
pub nav: ListNavigation,
}
#[derive(Default)]
pub struct PatchViewState {
pub cache: Option<FilePatch>,
pub scroll_offset: usize,
}
#[derive(Default)]
pub struct BranchCompareViewState {
pub cache: Option<BranchCompare>,
pub tab: CompareTab,
pub nav: ListNavigation,
}
#[derive(Default)]
pub struct RelatedFilesViewState {
pub cache: Option<RelatedFiles>,
pub nav: ListNavigation,
}
#[derive(Default)]
pub struct ImpactScoreViewState {
pub cache: Option<CommitImpactAnalysis>,
pub nav: ListNavigation,
}
#[derive(Default)]
pub struct ChangeCouplingViewState {
pub cache: Option<ChangeCouplingAnalysis>,
pub nav: ListNavigation,
}
#[derive(Default)]
pub struct QualityScoreViewState {
pub cache: Option<CommitQualityAnalysis>,
pub nav: ListNavigation,
}
#[derive(Default)]
pub struct HealthViewState {
pub cache: Option<ProjectHealth>,
}
#[derive(Default)]
pub struct ReviewQueueViewState {
pub cache: Option<ReviewQueue>,
pub nav: ListNavigation,
}
#[derive(Default)]
pub struct PrCreateViewState(pub PrCreateState);
#[derive(Default)]
pub struct ReviewPackViewState {
pub cache: Option<ReviewPack>,
pub verdict: Option<serde_json::Value>,
pub nav: ListNavigation,
}
#[derive(Default)]
pub struct NextActionsViewState {
pub cache: Option<Vec<ActionRecommendation>>,
pub nav: ListNavigation,
}
#[derive(Default)]
pub struct HandoffViewState {
pub cache: Option<HandoffContext>,
pub nav: ListNavigation,
}
#[derive(Default)]
pub struct CommitDetailState {
pub scroll: usize,
pub h_scroll: usize,
pub selected_file: usize,
pub expanded_files: HashSet<usize>,
}
#[derive(Default)]
pub struct FileDiffState {
pub cache: Option<FilePatch>,
pub(crate) cache_path: Option<String>,
pub scroll: usize,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum StatusMessageLevel {
Info,
Success,
Error,
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum InputMode {
#[default]
Normal,
Filter,
BranchSelect,
BranchCreate,
StatusView,
CommitInput,
TopologyView,
StatsView,
HeatmapView,
FileHistoryView,
TimelineView,
BlameView,
OwnershipView,
StashView,
PatchView,
PresetSave,
BranchCompareView,
RelatedFilesView,
ImpactScoreView,
ChangeCouplingView,
QualityScoreView,
QuickActionView,
ReviewQueueView,
PrCreate,
ReviewPackView,
NextActionsView,
HandoffView,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum QuickAction {
RiskSummary,
ReviewPack,
NextActions,
Verify,
HandoffClaude,
HandoffCodex,
HandoffCopilot,
Timeline,
Ownership,
ImpactScore,
ChangeCoupling,
QualityScore,
AuthorStats,
Heatmap,
}
impl QuickAction {
pub fn id(&self) -> &'static str {
match self {
Self::RiskSummary => "risk-summary",
Self::ReviewPack => "review-pack",
Self::NextActions => "next-actions",
Self::Verify => "verify",
Self::HandoffClaude => "handoff-claude",
Self::HandoffCodex => "handoff-codex",
Self::HandoffCopilot => "handoff-copilot",
Self::Timeline => "timeline",
Self::Ownership => "ownership",
Self::ImpactScore => "impact-score",
Self::ChangeCoupling => "change-coupling",
Self::QualityScore => "quality-score",
Self::AuthorStats => "author-stats",
Self::Heatmap => "heatmap",
}
}
pub fn title(&self, lang: Language) -> &'static str {
match self {
Self::RiskSummary => lang.quick_risk_summary(),
Self::ReviewPack => lang.quick_review_pack(),
Self::NextActions => lang.quick_next_actions(),
Self::Verify => lang.quick_verify(),
Self::HandoffClaude => lang.quick_handoff_claude(),
Self::HandoffCodex => lang.quick_handoff_codex(),
Self::HandoffCopilot => lang.quick_handoff_copilot(),
Self::Timeline => lang.quick_timeline(),
Self::Ownership => lang.quick_ownership(),
Self::ImpactScore => lang.quick_impact_score(),
Self::ChangeCoupling => lang.quick_change_coupling(),
Self::QualityScore => lang.quick_quality_score(),
Self::AuthorStats => lang.quick_author_stats(),
Self::Heatmap => lang.quick_heatmap(),
}
}
pub fn all() -> &'static [QuickAction] {
&[
Self::RiskSummary,
Self::ReviewPack,
Self::NextActions,
Self::Verify,
Self::HandoffClaude,
Self::HandoffCodex,
Self::HandoffCopilot,
Self::Timeline,
Self::Ownership,
Self::ImpactScore,
Self::ChangeCoupling,
Self::QualityScore,
Self::AuthorStats,
Self::Heatmap,
]
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SidebarPanel {
#[default]
Commits, Status, Branches, Files, Stash, }
impl SidebarPanel {
pub fn from_number(n: u8) -> Option<Self> {
match n {
1 => Some(Self::Status),
2 => Some(Self::Commits),
3 => Some(Self::Branches),
4 => Some(Self::Files),
5 => Some(Self::Stash),
_ => None,
}
}
pub fn number(&self) -> u8 {
match self {
Self::Status => 1,
Self::Commits => 2,
Self::Branches => 3,
Self::Files => 4,
Self::Stash => 5,
}
}
pub fn label(&self, lang: Language) -> &'static str {
match self {
Self::Status => lang.status(),
Self::Commits => lang.commits(),
Self::Branches => lang.branches(),
Self::Files => lang.files(),
Self::Stash => lang.stash(),
}
}
pub fn next(self) -> Self {
match self {
Self::Status => Self::Commits,
Self::Commits => Self::Branches,
Self::Branches => Self::Files,
Self::Files => Self::Stash,
Self::Stash => Self::Status,
}
}
pub fn prev(self) -> Self {
match self {
Self::Status => Self::Stash,
Self::Commits => Self::Status,
Self::Branches => Self::Commits,
Self::Files => Self::Branches,
Self::Stash => Self::Files,
}
}
pub fn all() -> &'static [SidebarPanel] {
&[
Self::Status,
Self::Commits,
Self::Branches,
Self::Files,
Self::Stash,
]
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CommitType {
Feat,
Fix,
Docs,
Style,
Refactor,
Test,
Chore,
Perf,
}
impl CommitType {
pub fn prefix(&self) -> &'static str {
match self {
Self::Feat => "feat: ",
Self::Fix => "fix: ",
Self::Docs => "docs: ",
Self::Style => "style: ",
Self::Refactor => "refactor: ",
Self::Test => "test: ",
Self::Chore => "chore: ",
Self::Perf => "perf: ",
}
}
pub fn key(&self) -> char {
match self {
Self::Feat => 'f',
Self::Fix => 'x',
Self::Docs => 'd',
Self::Style => 's',
Self::Refactor => 'r',
Self::Test => 't',
Self::Chore => 'c',
Self::Perf => 'p',
}
}
pub fn all() -> &'static [CommitType] {
&[
Self::Feat,
Self::Fix,
Self::Docs,
Self::Style,
Self::Refactor,
Self::Test,
Self::Chore,
Self::Perf,
]
}
pub fn name(&self) -> &'static str {
match self {
Self::Feat => "feat",
Self::Fix => "fix",
Self::Docs => "docs",
Self::Style => "style",
Self::Refactor => "refactor",
Self::Test => "test",
Self::Chore => "chore",
Self::Perf => "perf",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_stats_view_state_default() {
let s = StatsViewState::default();
assert!(s.cache.is_none());
assert_eq!(s.nav.selected_index, 0);
}
#[test]
fn test_heatmap_view_state_default() {
let s = HeatmapViewState::default();
assert!(s.cache.is_none());
}
#[test]
fn test_file_history_view_state_default() {
let s = FileHistoryViewState::default();
assert!(s.cache.is_none());
assert!(s.path.is_none());
}
#[test]
fn test_blame_view_state_default() {
let s = BlameViewState::default();
assert!(s.cache.is_none());
assert!(s.path.is_none());
}
#[test]
fn test_ownership_view_state_default() {
let s = OwnershipViewState::default();
assert!(s.cache.is_none());
}
#[test]
fn test_stash_view_state_default() {
let s = StashViewState::default();
assert!(s.cache.is_none());
}
#[test]
fn test_patch_view_state_default() {
let s = PatchViewState::default();
assert!(s.cache.is_none());
assert_eq!(s.scroll_offset, 0);
}
#[test]
fn test_branch_compare_view_state_default() {
let s = BranchCompareViewState::default();
assert!(s.cache.is_none());
assert_eq!(s.tab, CompareTab::default());
}
#[test]
fn test_related_files_view_state_default() {
let s = RelatedFilesViewState::default();
assert!(s.cache.is_none());
}
#[test]
fn test_impact_score_view_state_default() {
let s = ImpactScoreViewState::default();
assert!(s.cache.is_none());
}
#[test]
fn test_change_coupling_view_state_default() {
let s = ChangeCouplingViewState::default();
assert!(s.cache.is_none());
}
#[test]
fn test_quality_score_view_state_default() {
let s = QualityScoreViewState::default();
assert!(s.cache.is_none());
}
#[test]
fn test_health_view_state_default() {
let s = HealthViewState::default();
assert!(s.cache.is_none());
}
#[test]
fn test_commit_detail_state_default() {
let s = CommitDetailState::default();
assert_eq!(s.scroll, 0);
assert_eq!(s.h_scroll, 0);
assert_eq!(s.selected_file, 0);
assert!(s.expanded_files.is_empty());
}
#[test]
fn test_file_diff_state_default() {
let s = FileDiffState::default();
assert!(s.cache.is_none());
assert!(s.cache_path.is_none());
assert_eq!(s.scroll, 0);
}
#[test]
fn test_quick_action_all_returns_14_items() {
assert_eq!(QuickAction::all().len(), 14);
}
#[test]
fn test_quick_action_id_mapping() {
assert_eq!(QuickAction::RiskSummary.id(), "risk-summary");
assert_eq!(QuickAction::ReviewPack.id(), "review-pack");
assert_eq!(QuickAction::NextActions.id(), "next-actions");
assert_eq!(QuickAction::Verify.id(), "verify");
assert_eq!(QuickAction::HandoffClaude.id(), "handoff-claude");
assert_eq!(QuickAction::HandoffCodex.id(), "handoff-codex");
assert_eq!(QuickAction::HandoffCopilot.id(), "handoff-copilot");
assert_eq!(QuickAction::Timeline.id(), "timeline");
assert_eq!(QuickAction::Ownership.id(), "ownership");
assert_eq!(QuickAction::ImpactScore.id(), "impact-score");
assert_eq!(QuickAction::ChangeCoupling.id(), "change-coupling");
assert_eq!(QuickAction::QualityScore.id(), "quality-score");
assert_eq!(QuickAction::AuthorStats.id(), "author-stats");
assert_eq!(QuickAction::Heatmap.id(), "heatmap");
}
#[test]
fn test_quick_action_title_en() {
let lang = Language::En;
for action in QuickAction::all() {
let title = action.title(lang);
assert!(!title.is_empty());
}
}
#[test]
fn test_quick_action_title_ja() {
let lang = Language::Ja;
for action in QuickAction::all() {
let title = action.title(lang);
assert!(!title.is_empty());
}
}
#[test]
fn test_quick_action_all_ids_are_unique() {
let ids: Vec<&str> = QuickAction::all().iter().map(|a| a.id()).collect();
let mut deduped = ids.clone();
deduped.sort();
deduped.dedup();
assert_eq!(ids.len(), deduped.len());
}
#[test]
fn test_sidebar_panel_default_is_commits() {
assert_eq!(SidebarPanel::default(), SidebarPanel::Commits);
}
#[test]
fn test_sidebar_panel_from_number() {
assert_eq!(SidebarPanel::from_number(1), Some(SidebarPanel::Status));
assert_eq!(SidebarPanel::from_number(2), Some(SidebarPanel::Commits));
assert_eq!(SidebarPanel::from_number(3), Some(SidebarPanel::Branches));
assert_eq!(SidebarPanel::from_number(4), Some(SidebarPanel::Files));
assert_eq!(SidebarPanel::from_number(5), Some(SidebarPanel::Stash));
assert_eq!(SidebarPanel::from_number(0), None);
assert_eq!(SidebarPanel::from_number(6), None);
}
#[test]
fn test_sidebar_panel_number_roundtrip() {
for panel in SidebarPanel::all() {
assert_eq!(SidebarPanel::from_number(panel.number()), Some(*panel));
}
}
#[test]
fn test_sidebar_panel_next_cycles() {
let start = SidebarPanel::Status;
let mut current = start;
let mut visited = vec![];
for _ in 0..5 {
visited.push(current);
current = current.next();
}
assert_eq!(visited.len(), 5);
assert_eq!(current, start); }
#[test]
fn test_sidebar_panel_prev_cycles() {
let start = SidebarPanel::Status;
let mut current = start;
let mut visited = vec![];
for _ in 0..5 {
visited.push(current);
current = current.prev();
}
assert_eq!(visited.len(), 5);
assert_eq!(current, start); }
#[test]
fn test_sidebar_panel_next_prev_inverse() {
for panel in SidebarPanel::all() {
assert_eq!(panel.next().prev(), *panel);
assert_eq!(panel.prev().next(), *panel);
}
}
#[test]
fn test_sidebar_panel_label_not_empty() {
for panel in SidebarPanel::all() {
assert!(!panel.label(Language::En).is_empty());
assert!(!panel.label(Language::Ja).is_empty());
}
}
#[test]
fn test_sidebar_panel_all_returns_5() {
assert_eq!(SidebarPanel::all().len(), 5);
}
#[test]
fn test_commit_type_all_returns_8() {
assert_eq!(CommitType::all().len(), 8);
}
#[test]
fn test_commit_type_prefix() {
assert_eq!(CommitType::Feat.prefix(), "feat: ");
assert_eq!(CommitType::Fix.prefix(), "fix: ");
assert_eq!(CommitType::Docs.prefix(), "docs: ");
assert_eq!(CommitType::Style.prefix(), "style: ");
assert_eq!(CommitType::Refactor.prefix(), "refactor: ");
assert_eq!(CommitType::Test.prefix(), "test: ");
assert_eq!(CommitType::Chore.prefix(), "chore: ");
assert_eq!(CommitType::Perf.prefix(), "perf: ");
}
#[test]
fn test_commit_type_key() {
assert_eq!(CommitType::Feat.key(), 'f');
assert_eq!(CommitType::Fix.key(), 'x');
assert_eq!(CommitType::Docs.key(), 'd');
assert_eq!(CommitType::Style.key(), 's');
assert_eq!(CommitType::Refactor.key(), 'r');
assert_eq!(CommitType::Test.key(), 't');
assert_eq!(CommitType::Chore.key(), 'c');
assert_eq!(CommitType::Perf.key(), 'p');
}
#[test]
fn test_commit_type_name() {
for ct in CommitType::all() {
let name = ct.name();
assert!(!name.is_empty());
assert!(ct.prefix().starts_with(name));
}
}
#[test]
fn test_commit_type_keys_are_unique() {
let keys: Vec<char> = CommitType::all().iter().map(|c| c.key()).collect();
let mut deduped = keys.clone();
deduped.sort();
deduped.dedup();
assert_eq!(keys.len(), deduped.len());
}
#[test]
fn test_quick_action_selects_author_stats() {
let actions = QuickAction::all();
assert_eq!(actions[12], QuickAction::AuthorStats);
}
#[test]
fn test_quick_action_selects_heatmap() {
let actions = QuickAction::all();
assert_eq!(actions[13], QuickAction::Heatmap);
}
#[test]
fn test_quick_action_navigate_to_last_item() {
use crate::app::App;
let mut app = App::new();
app.start_quick_action_view();
for _ in 0..13 {
app.quick_action_move_down();
}
assert_eq!(app.selected_quick_action(), Some(QuickAction::Heatmap));
}
#[test]
fn test_input_mode_default_is_normal() {
assert_eq!(InputMode::default(), InputMode::Normal);
}
}