jk 0.2.6

A jj-native terminal UI for Jujutsu
use jk_cli::{WorkspaceListSnapshot, WorkspaceSummary};
use jk_tui::log_view::LogAction;
use jk_tui::rendered_view::RenderedAction;
use jk_tui::workspaces_view::{WorkspaceViewRow, WorkspaceViewSnapshot, WorkspacesAction};

pub fn workspace_view_snapshot(snapshot: WorkspaceListSnapshot) -> WorkspaceViewSnapshot {
    let rows = snapshot
        .workspaces
        .into_iter()
        .map(workspace_view_row)
        .collect();
    WorkspaceViewSnapshot::new(rows).with_title(snapshot.title)
}

fn workspace_view_row(workspace: WorkspaceSummary) -> WorkspaceViewRow {
    let root_display = workspace
        .root
        .as_ref()
        .map_or_else(|| "(no root)".to_owned(), |root| root.display().to_string());
    let mut row = WorkspaceViewRow::new(workspace.name, root_display, workspace.current);
    if let Some(root) = workspace.root {
        row = row.with_root(root);
    }
    if let Some(change_id) = workspace.change_id {
        row = row.with_change_id(change_id);
    }
    if let Some(commit_id) = workspace.commit_id {
        row = row.with_commit_id(commit_id);
    }
    row
}

pub fn update_stale_success_message(
    workspace_name: &str,
    command: &str,
    stderr: &str,
    stdout: &str,
) -> String {
    let command = compact_update_stale_command(command);
    let output = compact_command_output(stderr).or_else(|| compact_command_output(stdout));
    match output {
        Some(output) => format!("updated {workspace_name} via {command}: {output}"),
        None => format!("updated {workspace_name} via {command}"),
    }
}

pub const fn workspace_action_for_log_action(action: LogAction) -> WorkspacesAction {
    match action {
        LogAction::Previous => WorkspacesAction::Previous,
        LogAction::Next => WorkspacesAction::Next,
        LogAction::ScrollPreviousLine => WorkspacesAction::ScrollPreviousLine,
        LogAction::ScrollNextLine => WorkspacesAction::ScrollNextLine,
        LogAction::PagePrevious => WorkspacesAction::Previous,
        LogAction::PageNext => WorkspacesAction::Next,
        LogAction::First => WorkspacesAction::First,
        LogAction::Last => WorkspacesAction::Last,
        LogAction::Refresh => WorkspacesAction::Refresh,
        LogAction::OpenDiff => WorkspacesAction::OpenDiff,
        LogAction::ToggleExpanded => WorkspacesAction::OpenLog,
        LogAction::ToggleHelp => WorkspacesAction::ToggleHelp,
        LogAction::Quit => WorkspacesAction::Quit,
        LogAction::Home | LogAction::Log | LogAction::CollapseExpanded => {
            WorkspacesAction::ReturnBack
        }
        _ => WorkspacesAction::ReturnBack,
    }
}

pub const fn workspace_inspection_action_for_log_action(action: LogAction) -> RenderedAction {
    match action {
        LogAction::Previous => RenderedAction::ScrollPrevious,
        LogAction::Next => RenderedAction::ScrollNext,
        LogAction::ScrollPreviousLine => RenderedAction::ScrollPrevious,
        LogAction::ScrollNextLine => RenderedAction::ScrollNext,
        LogAction::PagePrevious => RenderedAction::PagePrevious,
        LogAction::PageNext => RenderedAction::PageNext,
        LogAction::ToggleMark => RenderedAction::PageNext,
        LogAction::ClearMarks => RenderedAction::Ignore,
        LogAction::First => RenderedAction::First,
        LogAction::Last => RenderedAction::Last,
        LogAction::ToggleHelp => RenderedAction::ToggleHelp,
        LogAction::Refresh => RenderedAction::Refresh,
        LogAction::Quit => RenderedAction::Quit,
        _ => RenderedAction::ReturnToLog,
    }
}

fn compact_update_stale_command(command: &str) -> &str {
    command
        .strip_prefix("jj -R ")
        .and_then(|command| command.split_once(" workspace update-stale"))
        .map_or(command, |_| "workspace update-stale")
}

fn compact_command_output(output: &str) -> Option<String> {
    let first_line = output.lines().find(|line| !line.trim().is_empty())?.trim();
    const MAX_STATUS_CHARS: usize = 120;
    if first_line.chars().count() <= MAX_STATUS_CHARS {
        Some(first_line.to_owned())
    } else {
        let mut summary = first_line
            .chars()
            .take(MAX_STATUS_CHARS.saturating_sub(3))
            .collect::<String>();
        summary.push_str("...");
        Some(summary)
    }
}

#[cfg(test)]
mod tests {
    use std::path::PathBuf;

    use super::*;

    #[test]
    fn snapshot_mapping_preserves_row_fields() {
        let snapshot = WorkspaceListSnapshot {
            title: "jj workspace list".to_owned(),
            workspaces: vec![WorkspaceSummary {
                name: "dogfood".to_owned(),
                root: Some(PathBuf::from("/repo/dogfood")),
                current: true,
                change_id: Some("abc123".to_owned()),
                commit_id: Some("def456".to_owned()),
            }],
        };

        let snapshot = workspace_view_snapshot(snapshot);

        assert_eq!(snapshot.title(), "jj workspace list");
        assert_eq!(
            snapshot.rows(),
            &[WorkspaceViewRow::new("dogfood", "/repo/dogfood", true)
                .with_root("/repo/dogfood")
                .with_change_id("abc123")
                .with_commit_id("def456")]
        );
    }

    #[test]
    fn update_stale_success_message_prefers_stderr_then_stdout() {
        assert_eq!(
            update_stale_success_message(
                "dogfood",
                "jj -R /repo/dogfood workspace update-stale",
                "warning line",
                "stdout line",
            ),
            "updated dogfood via workspace update-stale: warning line"
        );

        assert_eq!(
            update_stale_success_message(
                "dogfood",
                "jj -R /repo/dogfood workspace update-stale",
                "",
                "stdout line",
            ),
            "updated dogfood via workspace update-stale: stdout line"
        );

        assert_eq!(
            update_stale_success_message("dogfood", "jj workspace update-stale", "", ""),
            "updated dogfood via jj workspace update-stale"
        );
    }

    #[test]
    fn workspace_action_mapping_preserves_workspace_policy() {
        assert_eq!(
            workspace_action_for_log_action(LogAction::Next),
            WorkspacesAction::Next
        );
        assert_eq!(
            workspace_action_for_log_action(LogAction::PageNext),
            WorkspacesAction::Next
        );
        assert_eq!(
            workspace_action_for_log_action(LogAction::OpenDiff),
            WorkspacesAction::OpenDiff
        );
        assert_eq!(
            workspace_action_for_log_action(LogAction::ToggleExpanded),
            WorkspacesAction::OpenLog
        );
        assert_eq!(
            workspace_action_for_log_action(LogAction::Home),
            WorkspacesAction::ReturnBack
        );
    }

    #[test]
    fn workspace_inspection_action_mapping_preserves_rendered_policy() {
        assert_eq!(
            workspace_inspection_action_for_log_action(LogAction::Next),
            RenderedAction::ScrollNext
        );
        assert_eq!(
            workspace_inspection_action_for_log_action(LogAction::ToggleMark),
            RenderedAction::PageNext
        );
        assert_eq!(
            workspace_inspection_action_for_log_action(LogAction::ClearMarks),
            RenderedAction::Ignore
        );
        assert_eq!(
            workspace_inspection_action_for_log_action(LogAction::Refresh),
            RenderedAction::Refresh
        );
        assert_eq!(
            workspace_inspection_action_for_log_action(LogAction::OpenDiff),
            RenderedAction::ReturnToLog
        );
    }
}