jk 0.2.7

A jj-native terminal UI for Jujutsu
use jk_tui::operation_log_view::{OperationLogRow, OperationLogSnapshot};

pub fn operation_log_snapshot(title: &str, rendered: &str) -> OperationLogSnapshot {
    let rows = operation_log_rows(rendered);
    OperationLogSnapshot::new(rows).with_title(title)
}

fn operation_log_rows(rendered: &str) -> Vec<OperationLogRow> {
    let mut rows = Vec::new();
    for line in rendered.lines().map(strip_ansi) {
        if let Some(row) = parse_operation_row(&line) {
            rows.push(row);
        } else if let Some(title) = parse_operation_title_line(&line)
            && let Some(row) = rows.last_mut()
            && row.title.trim().is_empty()
        {
            row.title = title.to_owned();
        }
    }
    rows
}

fn parse_operation_row(line: &str) -> Option<OperationLogRow> {
    let mut fields = line.split_whitespace();
    let marker = fields.next()?;
    let operation_id = fields.next()?;
    if !is_operation_marker(marker) || !looks_like_operation_id(operation_id) {
        return None;
    }
    Some(OperationLogRow::new(
        operation_id,
        operation_id.chars().take(12).collect::<String>(),
        String::new(),
        marker == "@",
    ))
}

fn parse_operation_title_line(line: &str) -> Option<&str> {
    line.strip_prefix("")
        .map(str::trim)
        .filter(|line| !line.is_empty())
}

fn is_operation_marker(marker: &str) -> bool {
    matches!(marker, "@" | "" | "" | "×" | "")
}

fn looks_like_operation_id(value: &str) -> bool {
    value.len() >= 8 && value.chars().all(|character| character.is_ascii_hexdigit())
}

fn strip_ansi(text: &str) -> String {
    let mut output = String::with_capacity(text.len());
    let mut chars = text.chars().peekable();
    while let Some(character) = chars.next() {
        if character == '\u{1b}' && chars.peek() == Some(&'[') {
            let _ = chars.next();
            for code in chars.by_ref() {
                if code.is_ascii_alphabetic() {
                    break;
                }
            }
        } else {
            output.push(character);
        }
    }
    output
}

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

    #[test]
    fn snapshot_extracts_operation_rows_and_titles() {
        let rendered = "\
@ abcdef1234567890 user@example.test now
│  latest operation
○ 0123456789abcdef user@example.test earlier
│  previous operation
";

        let snapshot = operation_log_snapshot("jj op log", rendered);

        assert_eq!(snapshot.title(), "jj op log");
        assert_eq!(snapshot.rows().len(), 2);
        assert_eq!(snapshot.rows()[0].operation_id, "abcdef1234567890");
        assert_eq!(snapshot.rows()[0].display_id, "abcdef123456");
        assert_eq!(snapshot.rows()[0].title, "latest operation");
        assert!(snapshot.rows()[0].current);
        assert_eq!(snapshot.rows()[1].operation_id, "0123456789abcdef");
        assert_eq!(snapshot.rows()[1].title, "previous operation");
        assert!(!snapshot.rows()[1].current);
    }

    #[test]
    fn snapshot_ignores_ansi_sequences_before_parsing() {
        let rendered = "\u{1b}[32m@ abcdef1234567890 user@example.test now\u{1b}[0m\n\
│  colored current operation\n";

        let snapshot = operation_log_snapshot("jj op log", rendered);

        assert_eq!(snapshot.rows().len(), 1);
        assert_eq!(snapshot.rows()[0].operation_id, "abcdef1234567890");
        assert_eq!(snapshot.rows()[0].title, "colored current operation");
        assert!(snapshot.rows()[0].current);
    }
}