jk 0.2.7

A jj-native terminal UI for Jujutsu
use jk_cli::{DiffFormat, LogTemplateSelection};
use jk_tui::command_discovery::{BindingContext, filtered_discovery_len};
use jk_tui::diff_view::DiffView;

#[derive(Clone, Copy)]
pub enum MenuDirection {
    Previous,
    Next,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ViewOptionRow {
    LogTemplate,
    DiffFormat(DiffFormat),
    Placeholder,
}

const DIFF_VIEW_OPTION_ROWS: &[ViewOptionRow] = &[
    ViewOptionRow::DiffFormat(DiffFormat::Patch),
    ViewOptionRow::DiffFormat(DiffFormat::Summary),
    ViewOptionRow::DiffFormat(DiffFormat::Stat),
    ViewOptionRow::DiffFormat(DiffFormat::Types),
    ViewOptionRow::DiffFormat(DiffFormat::NameOnly),
    ViewOptionRow::DiffFormat(DiffFormat::Git),
    ViewOptionRow::DiffFormat(DiffFormat::ColorWords),
];

pub fn wrapped_selection(selected: usize, row_count: usize, direction: MenuDirection) -> usize {
    if row_count == 0 {
        return 0;
    }

    let selected = selected.min(row_count - 1);
    match direction {
        MenuDirection::Previous => selected.checked_sub(1).unwrap_or(row_count - 1),
        MenuDirection::Next => (selected + 1) % row_count,
    }
}

pub fn clamp_command_discovery_selection(
    context: BindingContext,
    query: &str,
    selected: &mut usize,
) {
    let row_count = filtered_discovery_len(context, query);
    if row_count == 0 {
        *selected = 0;
    } else {
        *selected = (*selected).min(row_count - 1);
    }
}

pub const fn view_option_rows(context: BindingContext) -> &'static [ViewOptionRow] {
    match context {
        BindingContext::Log => &[ViewOptionRow::LogTemplate],
        BindingContext::Diff => DIFF_VIEW_OPTION_ROWS,
        BindingContext::Inspection
        | BindingContext::Workspaces
        | BindingContext::CommandHistory
        | BindingContext::OperationLog => &[ViewOptionRow::Placeholder],
    }
}

pub fn view_options_lines(
    context: BindingContext,
    selected: usize,
    template: &LogTemplateSelection,
    active_diff_format: Option<DiffFormat>,
) -> Vec<String> {
    match context {
        BindingContext::Log => {
            let marker = if selected == 0 { ">" } else { " " };
            vec![
                format!("{marker} {:<18} {}", "Template", template.label()),
                String::new(),
                "j/k or arrows move   enter open   esc close".to_owned(),
            ]
        }
        BindingContext::Diff => {
            let active = active_diff_format.unwrap_or(DiffFormat::Patch);
            let mut lines = DIFF_VIEW_OPTION_ROWS
                .iter()
                .enumerate()
                .map(|(index, row)| {
                    let marker = if index == selected { ">" } else { " " };
                    let ViewOptionRow::DiffFormat(format) = row else {
                        unreachable!("diff view rows are all formats");
                    };
                    let active_marker = if *format == active { "*" } else { " " };
                    format!("{marker} {active_marker} {:<14}", format.label())
                })
                .collect::<Vec<_>>();
            lines.push(String::new());
            lines.push("j/k or arrows move   enter apply   esc close".to_owned());
            lines
        }
        BindingContext::Inspection => vec![
            "No view options in this slice.".to_owned(),
            String::new(),
            "esc close".to_owned(),
        ],
        BindingContext::Workspaces => vec![
            "No workspace view options in this slice.".to_owned(),
            String::new(),
            "esc close".to_owned(),
        ],
        BindingContext::CommandHistory => vec![
            "No command history options in this slice.".to_owned(),
            String::new(),
            "esc close".to_owned(),
        ],
        BindingContext::OperationLog => vec![
            "No operation log options in this slice.".to_owned(),
            String::new(),
            "esc close".to_owned(),
        ],
    }
}

pub fn diff_file_list_lines(view: &DiffView, selected: usize) -> Vec<String> {
    let paths = view.file_paths();
    if paths.is_empty() {
        return vec![
            "No files in this diff.".to_owned(),
            String::new(),
            "esc close".to_owned(),
        ];
    }

    paths
        .iter()
        .enumerate()
        .map(|(index, path)| {
            let marker = if index == selected { ">" } else { " " };
            format!("{marker} {:>2}/{} {path}", index + 1, paths.len())
        })
        .chain(std::iter::once(String::new()))
        .chain(std::iter::once(
            "j/k or arrows move   enter jump   esc close".to_owned(),
        ))
        .collect()
}

pub fn template_selector_lines(options: &[LogTemplateSelection], selected: usize) -> Vec<String> {
    options
        .iter()
        .enumerate()
        .map(|(index, template)| {
            let marker = if index == selected { ">" } else { " " };
            let name = template.template_name().unwrap_or("jj configured template");
            format!("{marker} {:<18} {name}", template.label())
        })
        .chain(std::iter::once(String::new()))
        .chain(std::iter::once(
            "j/k or arrows move   enter apply   esc cancel".to_owned(),
        ))
        .collect()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn wrapped_selection_wraps_and_clamps() {
        assert_eq!(wrapped_selection(0, 3, MenuDirection::Previous), 2);
        assert_eq!(wrapped_selection(2, 3, MenuDirection::Next), 0);
        assert_eq!(wrapped_selection(99, 3, MenuDirection::Previous), 1);
        assert_eq!(wrapped_selection(99, 3, MenuDirection::Next), 0);
        assert_eq!(wrapped_selection(4, 0, MenuDirection::Next), 0);
    }

    #[test]
    fn view_options_lines_show_template_or_placeholder() {
        assert_eq!(
            view_options_lines(BindingContext::Log, 0, &LogTemplateSelection::Oneline, None),
            vec![
                "> Template           oneline".to_owned(),
                String::new(),
                "j/k or arrows move   enter open   esc close".to_owned(),
            ]
        );
        assert_eq!(
            view_options_lines(
                BindingContext::Inspection,
                0,
                &LogTemplateSelection::Configured,
                None
            ),
            vec![
                "No view options in this slice.".to_owned(),
                String::new(),
                "esc close".to_owned(),
            ]
        );
    }

    #[test]
    fn template_selector_lines_show_template_names() {
        assert_eq!(
            template_selector_lines(
                &[
                    LogTemplateSelection::Configured,
                    LogTemplateSelection::Compact,
                ],
                1
            ),
            vec![
                "  configured         jj configured template".to_owned(),
                "> compact            builtin_log_compact".to_owned(),
                String::new(),
                "j/k or arrows move   enter apply   esc cancel".to_owned(),
            ]
        );
    }
}