oyo 0.1.36

CLI TUI for oyo - Step-through diff viewer
use super::types::SyntaxScopeCache;
use super::{display_metrics, AnimationPhase, App, ViewMode};
use crate::syntax::{SyntaxCache, SyntaxEngine, SyntaxSide};
use oyo_core::{LineKind, ViewLine};
use ratatui::text::Span;
use std::time::Instant;

impl App {
    pub fn syntax_enabled(&self) -> bool {
        if self.multi_diff.current_file_is_binary() {
            return false;
        }
        match self.syntax_mode {
            crate::config::SyntaxMode::On => true,
            crate::config::SyntaxMode::Off => false,
        }
    }

    pub(crate) fn syntax_cache_epoch(&self) -> u64 {
        if !self.syntax_enabled() {
            return 0;
        }
        self.syntax_caches
            .get(self.multi_diff.selected_index)
            .and_then(|cache| cache.as_ref())
            .map(|cache| cache.epoch())
            .unwrap_or(0)
    }

    pub(crate) fn syntax_warmup_pending(&self) -> bool {
        if !self.syntax_enabled() {
            return false;
        }
        self.syntax_caches
            .get(self.multi_diff.selected_index)
            .and_then(|cache| cache.as_ref())
            .map(|cache| cache.warm_pending())
            .unwrap_or(false)
    }

    pub fn syntax_spans_for_line(
        &mut self,
        side: SyntaxSide,
        line_num: Option<usize>,
    ) -> Option<Vec<Span<'static>>> {
        if !self.syntax_enabled() {
            return None;
        }
        let line_num = line_num?;
        if line_num == 0 {
            return None;
        }
        let cache = self.ensure_syntax_cache()?;
        cache.rendered_spans(side, line_num - 1)
    }

    pub(crate) fn maybe_warm_syntax_cache(&mut self) -> bool {
        if !self.syntax_enabled() {
            return false;
        }
        if self.animation_phase != AnimationPhase::Idle || self.snap_frame.is_some() {
            return false;
        }
        let idle = self.diff_last_input.elapsed().as_millis();
        let idle_threshold = self.diff_idle_ms as u128;
        let debounce = self.syntax_warmup_debounce_ms;
        let active_lines = self.syntax_warmup_active_lines;
        let pending_lines = self.syntax_warmup_pending_lines;
        let idle_lines = self.syntax_warmup_idle_lines;

        let now = Instant::now();
        let target_ready = self
            .syntax_warmup_target_at
            .map(|at| now.duration_since(at).as_millis() >= debounce as u128)
            .unwrap_or(true);
        if self.syntax_warmup_target.is_some() && !target_ready {
            return false;
        }
        let target = if target_ready {
            self.syntax_warmup_target
        } else {
            None
        };
        let target_applied = self.syntax_warmup_target_applied;
        let current_index = self.multi_diff.selected_index;

        let (apply_target, clear_target, warmed) = {
            let Some(cache) = self.ensure_syntax_cache() else {
                return false;
            };

            let mut apply_target = None;
            let mut clear_target = false;
            if let Some(target) = target {
                if target.file_index == current_index {
                    if target_applied != Some(target) {
                        cache.set_warmup_targets(target.old, target.new);
                        apply_target = Some(target);
                    }
                } else {
                    clear_target = true;
                }
            }

            let warm_pending = cache.warm_pending();
            if idle < idle_threshold && !warm_pending {
                return false;
            }
            let budget = if idle >= idle_threshold {
                idle_lines
            } else if warm_pending {
                pending_lines
            } else {
                active_lines
            };
            let warmed = cache.warm_checkpoints(budget) > 0;
            (apply_target, clear_target, warmed)
        };

        let mut changed = warmed;
        if clear_target {
            self.syntax_warmup_target = None;
            self.syntax_warmup_target_applied = None;
            changed = true;
        }
        if let Some(target) = apply_target {
            self.syntax_warmup_target_applied = Some(target);
            changed = true;
        }
        if target_ready && self.syntax_warmup_target.is_some() {
            self.syntax_warmup_target_at = None;
            changed = true;
        }
        changed
    }

    pub(crate) fn begin_syntax_warmup_frame(&mut self) {
        self.syntax_warmup_frame_old = None;
        self.syntax_warmup_frame_new = None;
    }

    pub(crate) fn record_syntax_warmup_line(&mut self, side: SyntaxSide, line_num: usize) {
        if line_num == 0 {
            return;
        }
        let line_idx = line_num.saturating_sub(1);
        match side {
            SyntaxSide::Old => update_warmup_range(&mut self.syntax_warmup_frame_old, line_idx),
            SyntaxSide::New => update_warmup_range(&mut self.syntax_warmup_frame_new, line_idx),
        }
    }

    pub(crate) fn commit_syntax_warmup_frame(&mut self) {
        if !self.syntax_enabled() {
            return;
        }
        let old = self.syntax_warmup_frame_old;
        let new = self.syntax_warmup_frame_new;
        if old.is_none() && new.is_none() {
            return;
        }
        let target = super::SyntaxWarmupTarget {
            file_index: self.multi_diff.selected_index,
            old,
            new,
        };
        if self.syntax_warmup_target != Some(target) {
            self.syntax_warmup_target = Some(target);
            self.syntax_warmup_target_at = Some(Instant::now());
        }
    }

    pub fn syntax_scope_target(&mut self, view: &[ViewLine]) -> Option<(usize, String)> {
        if !self.show_syntax_scopes {
            return None;
        }
        let step_direction = self.multi_diff.current_step_direction();
        let (display_len, _) = display_metrics(
            view,
            self.view_mode,
            self.animation_phase,
            self.scroll_offset,
            step_direction,
            self.split_align_lines,
        );
        if display_len == 0 {
            return None;
        }
        let viewport_height = self.last_viewport_height.max(1);
        let target_idx = self.scroll_offset.saturating_add(viewport_height / 2);
        let display_idx = target_idx.min(display_len.saturating_sub(1));

        let (side, line_num) = self.syntax_line_for_display(view, display_idx)?;
        let file_index = self.multi_diff.selected_index;
        if let Some(cache) = &self.syntax_scope_cache {
            if cache.file_index == file_index && cache.side == side && cache.line_num == line_num {
                return Some((display_idx, cache.label.clone()));
            }
        }
        let file_name = self.current_file_path();
        let nav = self.multi_diff.current_navigator();
        let content = match side {
            SyntaxSide::Old => nav.old_content(),
            SyntaxSide::New => nav.new_content(),
        };
        if self.syntax_engine.is_none() {
            self.syntax_engine = Some(SyntaxEngine::new(&self.syntax_theme, self.theme_is_light));
        }
        let engine = self.syntax_engine.as_ref()?;
        let scopes = engine.scopes_for_line(content, &file_name, line_num - 1);
        let label = if scopes.is_empty() {
            "scopes: (none)".to_string()
        } else {
            format!("scopes: {}", scopes.join(" | "))
        };
        self.syntax_scope_cache = Some(SyntaxScopeCache {
            file_index,
            side,
            line_num,
            label: label.clone(),
        });
        Some((display_idx, label))
    }

    pub(crate) fn ensure_syntax_cache(&mut self) -> Option<&mut SyntaxCache> {
        if !self.syntax_enabled() {
            return None;
        }
        let idx = self.multi_diff.selected_index;
        if idx >= self.syntax_caches.len() {
            self.syntax_caches = vec![None; self.multi_diff.file_count()];
        }
        if self.syntax_caches[idx].is_none() {
            let file_name = self.current_file_path();
            let (old_content, new_content) = self.multi_diff.file_contents_arc(idx)?;
            let force_lazy = self.multi_diff.current_file_diff_disabled()
                || self.multi_diff.current_file_is_large();
            if self.syntax_engine.is_none() {
                self.syntax_engine =
                    Some(SyntaxEngine::new(&self.syntax_theme, self.theme_is_light));
            }
            let engine = self.syntax_engine.as_ref()?;
            self.syntax_caches[idx] = Some(SyntaxCache::new(
                engine,
                old_content.as_ref(),
                new_content.as_ref(),
                &file_name,
                force_lazy,
            ));
        }
        self.syntax_caches[idx].as_mut()
    }

    fn syntax_line_for_display(
        &self,
        view: &[ViewLine],
        display_idx: usize,
    ) -> Option<(SyntaxSide, usize)> {
        match self.view_mode {
            ViewMode::UnifiedPane | ViewMode::Blame => view.get(display_idx).and_then(|line| {
                line.new_line.or(line.old_line).map(|line_num| {
                    let side = if line.new_line.is_some() {
                        SyntaxSide::New
                    } else {
                        SyntaxSide::Old
                    };
                    (side, line_num)
                })
            }),
            ViewMode::Evolution => {
                let mut display_count = 0usize;
                for line in view {
                    let visible = match line.kind {
                        LineKind::Deleted => false,
                        LineKind::PendingDelete => {
                            line.is_active && self.animation_phase != AnimationPhase::Idle
                        }
                        _ => true,
                    };
                    if visible {
                        if display_count == display_idx {
                            let line_num = line.new_line.or(line.old_line)?;
                            let side = if line.new_line.is_some() {
                                SyntaxSide::New
                            } else {
                                SyntaxSide::Old
                            };
                            return Some((side, line_num));
                        }
                        display_count += 1;
                    }
                }
                None
            }
            ViewMode::Split => {
                let align_lines = self.split_align_lines;
                let mut old_count = 0usize;
                let mut new_count = 0usize;
                let mut old_line = None;
                let mut new_line = None;

                for line in view {
                    let old_present = line.old_line.is_some();
                    let new_present = line.new_line.is_some()
                        && !matches!(line.kind, LineKind::Deleted | LineKind::PendingDelete);
                    if old_present || (align_lines && new_present) {
                        if old_present && old_count == display_idx {
                            old_line = line.old_line;
                        }
                        old_count += 1;
                    }
                    if new_present || (align_lines && old_present) {
                        if new_present && new_count == display_idx {
                            new_line = line.new_line;
                        }
                        new_count += 1;
                    }
                    if new_line.is_some() || (old_count > display_idx && new_count > display_idx) {
                        break;
                    }
                }

                if let Some(line_num) = new_line {
                    Some((SyntaxSide::New, line_num))
                } else {
                    old_line.map(|line_num| (SyntaxSide::Old, line_num))
                }
            }
        }
    }
}

fn update_warmup_range(range: &mut Option<super::WarmupRange>, line_idx: usize) {
    match range {
        Some(existing) => {
            existing.start = existing.start.min(line_idx);
            existing.end = existing.end.max(line_idx);
        }
        None => {
            *range = Some(super::WarmupRange {
                start: line_idx,
                end: line_idx,
            });
        }
    }
}