clankerdiff-core 0.1.1

Renderer-independent diff and code review domain model
Documentation
use crate::{DiffScope, DiffSide, RepositoryAction, RevealAmount, ViewMode};
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FocusPane {
    #[default]
    Files,
    Diff,
}

#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum InteractionPhase {
    #[default]
    Browse,
    Draft,
    Help,
    ThemePicker,
    RepositoryPrompt,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[expect(
    clippy::struct_excessive_bools,
    reason = "Capabilities are independent flags"
)]
pub struct ReviewCapabilities {
    pub repository: bool,
    pub refresh: bool,
    pub scope: bool,
    pub submit: bool,
    pub clipboard: bool,
}

impl Default for ReviewCapabilities {
    fn default() -> Self {
        Self {
            repository: true,
            refresh: true,
            scope: true,
            submit: true,
            clipboard: true,
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[expect(
    clippy::struct_excessive_bools,
    reason = "Command conditions are independent flags"
)]
pub struct CommandContext {
    pub phase: InteractionPhase,
    pub capabilities: ReviewCapabilities,
    pub repository_pending: bool,
    pub document_ready: bool,
    pub navigation_available: bool,
    pub themes_available: bool,
}

impl Default for CommandContext {
    fn default() -> Self {
        Self {
            phase: InteractionPhase::Browse,
            capabilities: ReviewCapabilities::default(),
            repository_pending: false,
            document_ready: true,
            navigation_available: true,
            themes_available: false,
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ReviewCommand {
    BeginComment,
    EditComment,
    DeleteComment,
    UndoComment,
    SubmitComment,
    Cancel,
    ShowHelp,
    ScrollHelp(isize),
    OpenThemePicker,
    SelectTheme(usize),
    MoveTheme(isize),
    CommitTheme,
}

impl ReviewCommand {
    #[must_use]
    pub fn enabled(self, context: &CommandContext) -> bool {
        let phase = context.phase;
        match self {
            Self::Cancel => true,
            Self::ShowHelp => phase == InteractionPhase::Browse,
            Self::ScrollHelp(_) => phase == InteractionPhase::Help,
            Self::OpenThemePicker => phase == InteractionPhase::Browse && context.themes_available,
            Self::SelectTheme(_) | Self::MoveTheme(_) | Self::CommitTheme => {
                phase == InteractionPhase::ThemePicker
            }
            Self::SubmitComment => phase == InteractionPhase::Draft && context.document_ready,
            Self::BeginComment | Self::EditComment | Self::DeleteComment | Self::UndoComment => {
                phase == InteractionPhase::Browse && context.document_ready
            }
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DiffReviewCommand {
    Review(ReviewCommand),
    Focus(FocusPane),
    ToggleFocus,
    SelectFile(usize),
    SelectRow(usize),
    SelectSide(DiffSide),
    MoveSelection(isize),
    Scroll { pane: FocusPane, lines: isize },
    Page(isize),
    First,
    Last,
    OpenSelected,
    CollapseSelected,
    RevealGap(RevealAmount),
    ToggleFullFile,
    SetViewMode(ViewMode),
    CycleViewMode,
    SubmitReview,
    CopyReview,
    SetScope(DiffScope),
    CycleScope,
    Refresh,
    ToggleStage,
    StageAll,
    UnstageAll,
    BeginCommit,
    BeginDiscard,
    RepositoryAction(RepositoryAction),
}

impl From<ReviewCommand> for DiffReviewCommand {
    fn from(command: ReviewCommand) -> Self {
        Self::Review(command)
    }
}

impl DiffReviewCommand {
    #[must_use]
    pub fn enabled(&self, context: &CommandContext) -> bool {
        if let Self::Review(command) = self {
            return command.enabled(context);
        }
        if context.phase != InteractionPhase::Browse {
            return false;
        }
        let capability = match self {
            Self::SetScope(_) | Self::CycleScope => {
                context.capabilities.scope && !context.repository_pending
            }
            Self::Refresh => context.capabilities.refresh && !context.repository_pending,
            Self::ToggleStage
            | Self::StageAll
            | Self::UnstageAll
            | Self::BeginCommit
            | Self::BeginDiscard
            | Self::RepositoryAction(_) => {
                context.capabilities.repository && !context.repository_pending
            }
            Self::SubmitReview => context.capabilities.submit,
            Self::CopyReview => context.capabilities.clipboard,
            Self::Focus(FocusPane::Files) | Self::ToggleFocus => context.navigation_available,
            _ => true,
        };
        capability
            && (context.document_ready
                || matches!(self, Self::SetScope(_) | Self::CycleScope | Self::Refresh))
    }
}