oyo 0.1.56

CLI TUI for oyo - Step-through diff viewer
use super::{App, ContentResponse};
use oyo_core::multi::{ContentSource, PendingFileContent};
use oyo_core::MultiFileDiff;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::mpsc;
use std::thread;

const MAX_CONTENT_BYTES: u64 = 32 * 1024 * 1024;

fn read_file(path: &Path) -> Option<Vec<u8>> {
    let metadata = path.metadata().ok()?;
    if metadata.len() > MAX_CONTENT_BYTES {
        return None;
    }
    std::fs::read(path).ok()
}

fn read_jj_file(repo_root: &Path, revision: &str, path: &Path) -> Option<Vec<u8>> {
    let output = Command::new("jj")
        .current_dir(repo_root)
        .arg("-R")
        .arg(repo_root)
        .arg("--no-pager")
        .arg("--config")
        .arg("signing.behavior=\"drop\"")
        .args(["file", "show", "-r", revision])
        .arg(path)
        .output()
        .ok()?;
    output.status.success().then_some(output.stdout)
}

fn load_direct(source: &ContentSource) -> Option<Option<Vec<u8>>> {
    match source {
        ContentSource::Empty => Some(Some(Vec::new())),
        ContentSource::File(path) => Some(read_file(path)),
        ContentSource::JjFile {
            repo_root,
            revision,
            path,
        } => Some(read_jj_file(repo_root, revision, path)),
        ContentSource::GitObject { .. } => None,
    }
}

fn load_requests(
    generation: u64,
    requests: Vec<PendingFileContent>,
    tx: &mpsc::Sender<ContentResponse>,
) {
    let mut bytes = vec![[None, None]; requests.len()];
    let mut git_groups: HashMap<PathBuf, Vec<(usize, usize, String)>> = HashMap::new();
    for (request_idx, request) in requests.iter().enumerate() {
        for (side_idx, source) in [&request.old, &request.new].into_iter().enumerate() {
            if let Some(value) = load_direct(source) {
                bytes[request_idx][side_idx] = Some(value);
            } else if let ContentSource::GitObject { repo_root, spec } = source {
                git_groups.entry(repo_root.clone()).or_default().push((
                    request_idx,
                    side_idx,
                    spec.clone(),
                ));
            }
        }
    }

    for (repo_root, group) in git_groups {
        let specs = group
            .iter()
            .map(|(_, _, spec)| spec.clone())
            .collect::<Vec<_>>();
        let objects = oyo_core::git::get_objects_batch(&repo_root, &specs, MAX_CONTENT_BYTES)
            .unwrap_or_else(|_| vec![None; specs.len()]);
        for ((request_idx, side_idx, _), object) in group.into_iter().zip(objects) {
            bytes[request_idx][side_idx] = Some(object);
        }
    }

    for (request, mut sides) in requests.into_iter().zip(bytes) {
        let old = sides[0].take().flatten();
        let new = sides[1].take().flatten();
        let response = ContentResponse {
            generation,
            file_index: request.file_index,
            identity: request.clone(),
            content: MultiFileDiff::prepare_file_content(old, new),
        };
        if tx.send(response).is_err() {
            break;
        }
    }
}

pub(crate) fn load_content_request_sync(
    request: PendingFileContent,
) -> oyo_core::multi::PreparedFileContent {
    let (tx, rx) = mpsc::channel();
    load_requests(0, vec![request], &tx);
    rx.recv()
        .expect("content loader must return one response")
        .content
}

impl App {
    pub(crate) fn start_content_loading(&mut self) -> bool {
        let mut requests = self.multi_diff.take_pending_content();
        if requests.is_empty() {
            return false;
        }
        self.content_generation = self.content_generation.wrapping_add(1);
        let generation = self.content_generation;
        self.content_loading.clear();
        for request in &requests {
            self.content_loading
                .insert(request.file_index, request.clone());
        }
        let selected = self.multi_diff.selected_index;
        requests.sort_by_key(|request| usize::from(request.file_index != selected));
        let first = requests.remove(0);
        let (tx, rx) = mpsc::channel();
        thread::spawn(move || {
            load_requests(generation, vec![first], &tx);
            if !requests.is_empty() {
                load_requests(generation, requests, &tx);
            }
        });
        self.content_worker_rx = Some(rx);
        true
    }

    pub(crate) fn content_loading_count(&self) -> usize {
        self.content_loading.len()
    }

    pub(crate) fn poll_content_responses(&mut self) -> bool {
        let Some(rx) = self.content_worker_rx.as_ref() else {
            return false;
        };
        let mut responses = Vec::new();
        while let Ok(response) = rx.try_recv() {
            responses.push(response);
        }
        if responses.is_empty() {
            return false;
        }
        for response in responses {
            if response.generation != self.content_generation
                || self.content_loading.get(&response.file_index) != Some(&response.identity)
            {
                continue;
            }
            self.content_loading.remove(&response.file_index);
            if !self
                .multi_diff
                .apply_prepared_content(response.file_index, response.content)
            {
                continue;
            }
            if response.file_index == self.multi_diff.selected_index {
                self.multi_diff.ensure_full_navigator(response.file_index);
                self.clear_fold_context_caches();
                self.reset_current_max_line_width();
                if let Some(cache) = self.syntax_caches.get_mut(response.file_index) {
                    *cache = None;
                }
                if let Some(cache) = self.fold_scope_caches.get_mut(response.file_index) {
                    *cache = None;
                }
                self.syntax_scope_cache = None;
                let _ = self.queue_current_file_diff();
            }
            self.mark_diff_changed();
        }
        if self.content_loading.is_empty() {
            self.content_worker_rx = None;
        }
        true
    }
}