quinjet 0.0.50

A fast, live, keyboard-first Git source-control interface for the terminal
use super::refresh_preview::{
    loaded_changes_preview, loaded_two_file_changes_preview, parsed_change_document,
    refresh_with_status,
};
use super::*;
use crate::git::diff::DiffLine;

fn index_entry(path: PathBuf, status: &str) -> crate::git::diff::DiffFileIndexEntry {
    crate::git::diff::DiffFileIndexEntry {
        path,
        old_path: None,
        status: status.to_owned(),
        counts: None,
    }
}

fn assert_prepare_local_diff(effects: &[AppEffect]) {
    assert!(effects.iter().any(|effect| matches!(
        effect,
        AppEffect::Git(command)
            if matches!(command.as_ref(), WorkerCommand::PrepareLocalDiff { .. })
    )));
}

fn assert_indexing_placeholder(app: &App) {
    assert_eq!(app.document.file_count(), 0);
    assert_eq!(
        app.document.lines.first().map(DiffLine::text).as_deref(),
        Some("Indexing changed files…")
    );
}

#[test]
fn a_selected_section_preserves_membership_and_applies_folds_at_the_swap() {
    let (mut app, [main_path, readme_path], mut index, loaded) = loaded_two_file_changes_preview();
    let now = Instant::now();
    app.files_collapsed = false;
    app.collapse_preference_set = false;
    app.expanded_preview_files.clear();
    app.collapsed_preview_files = HashSet::from([readme_path.clone()]);
    let mut status = app.status.clone();
    let new_path = PathBuf::from("src/new.rs");
    status.changes.push(Change {
        path: new_path.clone(),
        original_path: None,
        area: ChangeArea::Unstaged,
        status: ChangeStatus::Added,
    });

    let effects = refresh_with_status(&mut app, status, now);

    assert!(effects.iter().any(|effect| matches!(
        effect,
        AppEffect::Git(command)
            if matches!(command.as_ref(), WorkerCommand::PrepareLocalDiff {
                request,
                ..
            } if matches!(request.as_ref(), LocalDiffRequest::Changes { changes, .. }
                if changes.len() == 3))
    )));
    assert_eq!(app.document, loaded);
    assert_eq!(app.local_diff_change_section, Some(ChangeSection::Unstaged));
    index.files.push(index_entry(new_path.clone(), "added"));

    drop(app.handle_worker_event(
        WorkerEvent::LocalDiffIndex {
            generation: app.diff_generation,
            result: Ok(index),
        },
        now,
    ));

    assert_eq!(app.document, loaded);
    assert_eq!(
        app.collapsed_preview_files,
        HashSet::from([readme_path.clone()])
    );
    assert!(!app.local_diff_pending_paths.contains(&new_path));
    let generation = app.diff_generation;
    let replacement_readme =
        parsed_change_document(&readme_path, "Current diff", "replacement readme");
    let effects = app.handle_worker_event(
        WorkerEvent::LocalDiffFile {
            generation,
            workspace_generation: generation,
            path: readme_path.clone(),
            result: Ok(replacement_readme),
        },
        now,
    );
    assert!(effects.iter().any(|effect| matches!(
        effect,
        AppEffect::Git(command)
            if matches!(command.as_ref(), WorkerCommand::LoadLocalDiffFile { path, .. }
                if path == &main_path)
    )));
    let replacement_main = parsed_change_document(&main_path, "Current diff", "replacement main");
    drop(app.handle_worker_event(
        WorkerEvent::LocalDiffFile {
            generation,
            workspace_generation: generation,
            path: main_path.clone(),
            result: Ok(replacement_main),
        },
        now,
    ));

    assert!(!app.document_loading);
    assert!(!app.collapsed_preview_files.contains(&main_path));
    assert!(app.collapsed_preview_files.contains(&readme_path));
    assert!(app.collapsed_preview_files.contains(&new_path));
    assert!(app.local_diff_preserved_paths.is_empty());
}

#[test]
fn an_individual_change_preserves_its_diff_when_its_status_changes() {
    let (mut app, _, _, loaded) = loaded_changes_preview();
    let now = Instant::now();
    let mut status = app.status.clone();
    for change in &mut status.changes {
        if change.path == Path::new("src/main.rs") {
            change.status = ChangeStatus::TypeChanged;
        }
    }

    let effects = refresh_with_status(&mut app, status, now);

    assert_prepare_local_diff(&effects);
    assert_eq!(app.document, loaded);
    assert!(app.document_loading);
}

#[test]
fn selecting_a_different_change_replaces_the_previous_diff_with_indexing() {
    let (mut app, ..) = loaded_changes_preview();
    let now = Instant::now();
    let mut status = app.status.clone();
    status
        .changes
        .retain(|change| change.path == Path::new("README.md"));

    let effects = refresh_with_status(&mut app, status, now);

    assert_prepare_local_diff(&effects);
    assert_indexing_placeholder(&app);
    assert!(app.document_loading);
    assert!(app.selected_preview_file.is_none());
    assert!(app.collapsed_preview_files.is_empty());
    assert!(app.expanded_preview_files.is_empty());
}

#[test]
fn moving_the_same_path_to_another_area_does_not_preserve_its_diff() {
    let (mut app, ..) = loaded_changes_preview();
    let now = Instant::now();
    let mut status = app.status.clone();
    status
        .changes
        .retain(|change| change.path == Path::new("src/main.rs"));
    for change in &mut status.changes {
        change.area = ChangeArea::Staged;
    }

    let effects = refresh_with_status(&mut app, status, now);

    assert_prepare_local_diff(&effects);
    assert_indexing_placeholder(&app);
}

#[test]
fn moving_a_selected_section_to_another_area_does_not_preserve_its_diff() {
    let (mut app, ..) = loaded_two_file_changes_preview();
    let now = Instant::now();
    let mut status = app.status.clone();
    for change in &mut status.changes {
        change.area = ChangeArea::Staged;
    }

    let effects = refresh_with_status(&mut app, status, now);

    assert_prepare_local_diff(&effects);
    assert_eq!(app.selected_change_section, Some(ChangeSection::Staged));
    assert_indexing_placeholder(&app);
}

#[test]
fn a_superseded_index_cannot_change_the_rendered_selection_or_folds() {
    let (mut app, [main_path, readme_path], index, loaded) = loaded_two_file_changes_preview();
    app.expanded_preview_files.remove(&readme_path);
    let now = Instant::now();
    let status = app.status.clone();
    drop(refresh_with_status(&mut app, status, now));
    let intermediate_generation = app.diff_generation;
    drop(app.handle_worker_event(
        WorkerEvent::LocalDiffIndex {
            generation: intermediate_generation,
            result: Ok(DiffIndex {
                title: "Intermediate diff".to_owned(),
                files: vec![index_entry(main_path.clone(), "modified")],
                truncated: false,
                commit_details: None,
            }),
        },
        now,
    ));

    assert_eq!(app.document, loaded);
    assert_eq!(app.selected_preview_file, Some(readme_path.clone()));
    assert!(!app.expanded_preview_files.contains(&readme_path));
    assert!(app.preview_file_collapsed(&readme_path.to_string_lossy()));
    let mut refresh_effects = Vec::new();
    app.filesystem_changed(&mut refresh_effects);
    assert!(refresh_effects.iter().any(|effect| matches!(
        effect,
        AppEffect::Git(command) if matches!(command.as_ref(), WorkerCommand::Refresh { .. })
    )));
    assert_eq!(app.document, loaded);
    assert_eq!(app.selected_preview_file, Some(readme_path.clone()));
    assert!(app.preview_file_collapsed(&readme_path.to_string_lossy()));
    let effects = app.handle_worker_event(
        WorkerEvent::Status {
            generation: app.status_generation,
            result: Ok(app.status.clone()),
        },
        now,
    );
    assert_prepare_local_diff(&effects);
    let final_generation = app.diff_generation;
    drop(app.handle_worker_event(
        WorkerEvent::LocalDiffIndex {
            generation: final_generation,
            result: Ok(index.clone()),
        },
        now,
    ));

    assert_eq!(app.document, loaded);
    assert_eq!(app.selected_preview_file, Some(readme_path.clone()));
    assert!(!app.expanded_preview_files.contains(&readme_path));
    assert_eq!(
        app.local_diff_preserved_paths,
        HashSet::from([main_path.clone(), readme_path.clone()])
    );
    let replacement_readme =
        parsed_change_document(&readme_path, "Current diff", "replacement readme");
    let effects = app.handle_worker_event(
        WorkerEvent::LocalDiffFile {
            generation: final_generation,
            workspace_generation: final_generation,
            path: readme_path.clone(),
            result: Ok(replacement_readme.clone()),
        },
        now,
    );
    assert!(effects.iter().any(|effect| matches!(
        effect,
        AppEffect::Git(command)
            if matches!(command.as_ref(), WorkerCommand::LoadLocalDiffFile { path, .. }
                if path == &main_path)
    )));
    let replacement_main = parsed_change_document(&main_path, "Current diff", "replacement main");
    drop(app.handle_worker_event(
        WorkerEvent::LocalDiffFile {
            generation: final_generation,
            workspace_generation: final_generation,
            path: main_path.clone(),
            result: Ok(replacement_main.clone()),
        },
        now,
    ));
    let replacement = index.document_with_visibility(
        &HashMap::from([
            (main_path.clone(), replacement_main),
            (readme_path.clone(), replacement_readme),
        ]),
        |path| path == main_path,
    );

    assert_eq!(app.document, replacement);
    assert_eq!(app.selected_preview_file, Some(readme_path.clone()));
    assert!(!app.expanded_preview_files.contains(&readme_path));
    assert!(app.preview_file_collapsed(&readme_path.to_string_lossy()));
}

#[test]
fn a_completed_refresh_prunes_removed_file_state() {
    let (mut app, [main_path, readme_path], mut index, loaded) = loaded_two_file_changes_preview();
    let now = Instant::now();
    app.collapsed_preview_files.insert(readme_path.clone());
    let mut status = app.status.clone();
    status.changes.retain(|change| change.path == main_path);
    drop(refresh_with_status(&mut app, status, now));
    index.files.retain(|file| file.path == main_path);
    drop(app.handle_worker_event(
        WorkerEvent::LocalDiffIndex {
            generation: app.diff_generation,
            result: Ok(index),
        },
        now,
    ));

    assert_eq!(app.document, loaded);
    assert_eq!(app.selected_preview_file, Some(readme_path.clone()));
    assert!(app.collapsed_preview_files.contains(&readme_path));
    let generation = app.diff_generation;
    let replacement = parsed_change_document(&main_path, "Current diff", "replacement main");
    drop(app.handle_worker_event(
        WorkerEvent::LocalDiffFile {
            generation,
            workspace_generation: generation,
            path: main_path.clone(),
            result: Ok(replacement.clone()),
        },
        now,
    ));

    assert_eq!(app.document, replacement);
    assert_eq!(app.selected_preview_file, Some(main_path.clone()));
    assert!(app.expanded_preview_files.contains(&main_path));
    assert!(!app.expanded_preview_files.contains(&readme_path));
    assert!(!app.collapsed_preview_files.contains(&readme_path));
}

#[test]
fn growing_a_single_file_section_auto_collapses_every_file() {
    let (mut app, path, mut index, loaded) = loaded_changes_preview();
    app.reset_local_diff_runtime();
    app.selected_change_section = Some(ChangeSection::Unstaged);
    app.changes_diff_version = 3;
    if let Some(request) = app.local_diff_request_for_view() {
        app.prepare_local_diff(request, &mut Vec::new());
    }
    app.document = loaded;
    app.document_loading = false;
    app.diff_generation = 5;
    app.local_diff_workspace_generation = Some(5);
    app.local_diff_index = Some(index.clone());
    app.local_diff_single_loaded = true;
    app.selected_preview_file = Some(path.clone());
    let now = Instant::now();
    let new_path = PathBuf::from("src/new.rs");
    let mut status = app.status.clone();
    status.changes.push(Change {
        path: new_path.clone(),
        original_path: None,
        area: ChangeArea::Unstaged,
        status: ChangeStatus::Added,
    });
    drop(refresh_with_status(&mut app, status, now));
    index.files.push(index_entry(new_path.clone(), "added"));
    drop(app.handle_worker_event(
        WorkerEvent::LocalDiffIndex {
            generation: app.diff_generation,
            result: Ok(index),
        },
        now,
    ));

    assert!(!app.local_diff_pending_paths.contains(&new_path));
    let generation = app.diff_generation;
    let replacement = parsed_change_document(&path, "Current diff", "replacement main");
    drop(app.handle_worker_event(
        WorkerEvent::LocalDiffFile {
            generation,
            workspace_generation: generation,
            path: path.clone(),
            result: Ok(replacement),
        },
        now,
    ));

    assert!(!app.document_loading);
    assert_eq!(app.collapsed_preview_files, HashSet::from([path, new_path]));
}