jjj 0.2.1

A modal interface for Jujutsu.
use bevy::prelude::*;
use ratatui::{
    prelude::{Rect, *},
    widgets::Block,
};

use crate::backend::log::LogRevsetEvent;

use super::change_buffer::{ChangeBufferSelectionEvent, RevisionSelection};
use super::prelude::*;

#[mutants::skip]
#[tracing::instrument(skip_all)]
pub fn plugin(app: &mut App) {
    trace!("Initializing plugin...");

    app.add_systems(
        Update,
        (monitor_logged_revset, monitor_selected_revset).in_set(AppSet::Update),
    );

    trace!("Plugin initialized.");
}

#[derive(Component, Default)]
pub struct StatusLine {
    pub logged_revset: Option<String>,
    pub selected_revset: Option<RevisionSelection>,
}

fn monitor_logged_revset(
    mut ev_log_request: EventReader<LogRevsetEvent>,
    mut status_line: Query<&mut StatusLine>,
) {
    let Ok(mut status_line) = status_line.get_single_mut() else {
        return;
    };

    for LogRevsetEvent(revset) in ev_log_request.read() {
        status_line.logged_revset = Some(revset.clone());
    }
}

fn monitor_selected_revset(
    mut ev_selection: EventReader<ChangeBufferSelectionEvent>,
    mut status_line: Query<&mut StatusLine>,
) {
    let Ok(mut status_line) = status_line.get_single_mut() else {
        return;
    };

    for ChangeBufferSelectionEvent(selection) in ev_selection.read() {
        status_line.selected_revset = Some(selection.clone());
    }
}

impl Widget for &StatusLine {
    fn render(self, status_line_area: Rect, buf: &mut Buffer)
    where
        Self: Sized,
    {
        let selected_revset = match &self.selected_revset {
            Some(RevisionSelection::Single(rev)) => {
                let change_id = rev.change_id.into_parts();
                Line::from(vec![
                    Span::styled(change_id.head, Style::new().magenta()),
                    Span::styled(change_id.tail, Style::new().black()),
                ])
            }
            Some(RevisionSelection::Range(start, end)) => {
                let start_change_id = start.change_id.into_parts();
                let end_change_id = end.change_id.into_parts();
                Line::from(vec![
                    Span::styled(start_change_id.head, Style::new().magenta()),
                    Span::styled(start_change_id.tail, Style::new().black()),
                    Span::styled("..", Style::new().black()),
                    Span::styled(end_change_id.head, Style::new().magenta()),
                    Span::styled(end_change_id.tail, Style::new().black()),
                ])
            }
            None => "-".into(),
        };

        let revset = self.logged_revset.clone().unwrap_or("-".into());

        let logged_revset = Span::styled(&revset, Style::new().black());

        let [_, selected_revset_area, logged_revset_area] = Layout::default()
            .direction(Direction::Horizontal)
            .horizontal_margin(1)
            .spacing(2)
            .constraints([
                Constraint::Fill(1),
                Constraint::Length(selected_revset.width() as u16),
                Constraint::Length(revset.len().try_into().unwrap()),
            ])
            .areas(status_line_area);

        Block::default().on_white().render(status_line_area, buf);
        selected_revset.render(selected_revset_area, buf);
        logged_revset.render(logged_revset_area, buf);
    }
}

#[cfg(test)]
mod tests {
    use insta::assert_snapshot;
    use ratatui::backend::TestBackend;

    use crate::backend::revisions::{ChangeId, Revision};

    use super::*;

    #[test]
    fn snapshot_selected_revset_single() {
        let line = StatusLine {
            selected_revset: Some(RevisionSelection::Single(Revision {
                change_id: ChangeId("foo".into(), 3),
                ..default()
            })),
            ..default()
        };

        let mut terminal = Terminal::new(TestBackend::new(80, 20)).unwrap();
        terminal
            .draw(|frame| frame.render_widget(&line, frame.area()))
            .unwrap();
        assert_snapshot!(terminal.backend());
    }

    #[test]
    fn snapshot_selected_revset_range() {
        let line = StatusLine {
            selected_revset: Some(RevisionSelection::Range(
                Revision {
                    change_id: ChangeId("foo".into(), 3),
                    ..default()
                },
                Revision {
                    change_id: ChangeId("bar".into(), 3),
                    ..default()
                },
            )),
            ..default()
        };

        let mut terminal = Terminal::new(TestBackend::new(80, 20)).unwrap();
        terminal
            .draw(|frame| frame.render_widget(&line, frame.area()))
            .unwrap();
        assert_snapshot!(terminal.backend());
    }

    #[test]
    fn snapshot_logged_revset() {
        let line = StatusLine {
            logged_revset: Some("lel".into()),
            ..default()
        };

        let mut terminal = Terminal::new(TestBackend::new(80, 20)).unwrap();
        terminal
            .draw(|frame| frame.render_widget(&line, frame.area()))
            .unwrap();
        assert_snapshot!(terminal.backend());
    }
}