// src/textarea/highlight/engine.rs #![allow(dead_code)] use std::collections::hash_map::DefaultHasher; use std::hash::{Hash, Hasher}; use ratatui::style::{Modifier, Style}; use syntect::{ highlighting::{ HighlightIterator, HighlightState, Highlighter, Style as SynStyle, Theme, ThemeSet, }, parsing::{ParseState, ScopeStack, SyntaxReference, SyntaxSet}, }; use crate::data_provider::DataProvider; use super::chunks::StyledChunk; #[derive(Debug)] pub struct SyntectEngine { ps: SyntaxSet, ts: ThemeSet, theme_name: String, syntax_name: Option, // Cached parser state (after line i) parse_after: Vec, // Cached scope stack (after line i) stack_after: Vec, // Hash of line contents to detect edits line_hashes: Vec, } impl Default for SyntectEngine { fn default() -> Self { Self::new() } } impl SyntectEngine { pub fn new() -> Self { let ps = SyntaxSet::load_defaults_newlines(); let ts = ThemeSet::load_defaults(); Self { ps, ts, theme_name: "InspiredGitHub".to_string(), syntax_name: None, parse_after: Vec::new(), stack_after: Vec::new(), line_hashes: Vec::new(), } } pub fn clear(&mut self) { self.parse_after.clear(); self.stack_after.clear(); self.line_hashes.clear(); } pub fn set_theme(&mut self, theme_name: &str) -> bool { if self.ts.themes.contains_key(theme_name) { self.theme_name = theme_name.to_string(); true } else { false } } pub fn set_syntax_by_name(&mut self, name: &str) -> bool { if self.ps.find_syntax_by_name(name).is_some() { self.syntax_name = Some(name.to_string()); self.clear(); true } else { false } } pub fn set_syntax_by_extension(&mut self, ext: &str) -> bool { if let Some(s) = self.ps.find_syntax_by_extension(ext) { self.syntax_name = Some(s.name.clone()); self.clear(); true } else { false } } pub fn invalidate_from(&mut self, line_idx: usize) { if line_idx < self.parse_after.len() { self.parse_after.truncate(line_idx); } if line_idx < self.stack_after.len() { self.stack_after.truncate(line_idx); } if line_idx < self.line_hashes.len() { self.line_hashes.truncate(line_idx); } } pub fn on_insert_line(&mut self, at: usize) { self.invalidate_from(at); } pub fn on_delete_line(&mut self, at: usize) { self.invalidate_from(at); } fn theme(&self) -> &Theme { self.ts .themes .get(&self.theme_name) .expect("theme exists") } fn syntax_ref(&self) -> &SyntaxReference { if let Some(name) = &self.syntax_name { if let Some(s) = self.ps.find_syntax_by_name(name) { return s; } } self.ps.find_syntax_plain_text() } fn map_syntect_style(s: SynStyle) -> Style { let fg = ratatui::style::Color::Rgb(s.foreground.r, s.foreground.g, s.foreground.b); let mut st = Style::default().fg(fg); use syntect::highlighting::FontStyle; if s.font_style.contains(FontStyle::BOLD) { st = st.add_modifier(Modifier::BOLD); } if s.font_style.contains(FontStyle::UNDERLINE) { st = st.add_modifier(Modifier::UNDERLINED); } if s.font_style.contains(FontStyle::ITALIC) { st = st.add_modifier(Modifier::ITALIC); } st } fn hash_line(s: &str) -> u64 { let mut h = DefaultHasher::new(); s.hash(&mut h); h.finish() } // Verify cached chain up to the nearest trusted predecessor of line_idx, // using the provider to fetch the current lines. fn verify_and_truncate_before(&mut self, line_idx: usize, provider: &dyn DataProvider) { let mut k = std::cmp::min(line_idx, self.parse_after.len()); while k > 0 { let j = k - 1; let curr = Self::hash_line(provider.field_value(j)); if self.line_hashes.get(j) == Some(&curr) { break; } self.invalidate_from(j); k = j; } } // Ensure we have parser + stack for lines [0..line_idx) fn ensure_state_before(&mut self, line_idx: usize, provider: &dyn DataProvider) { if line_idx == 0 || self.parse_after.len() >= line_idx { return; } let syntax = self.syntax_ref(); let theme = self.theme().clone(); // Clone to avoid borrow conflicts let highlighter = Highlighter::new(&theme); let mut ps = if self.parse_after.is_empty() { ParseState::new(syntax) } else { self.parse_after[self.parse_after.len() - 1].clone() }; let mut stack = if self.stack_after.is_empty() { ScopeStack::new() } else { self.stack_after[self.stack_after.len() - 1].clone() }; let start = self.parse_after.len(); for i in start..line_idx { let s = provider.field_value(i); // Fix: parse_line takes 2 arguments: line and &SyntaxSet let ops = ps.parse_line(s, &self.ps).unwrap_or_default(); // Fix: HighlightState::new requires &Highlighter and ScopeStack let mut highlight_state = HighlightState::new(&highlighter, stack.clone()); // Fix: HighlightIterator::new expects &mut HighlightState as first parameter let it = HighlightIterator::new(&mut highlight_state, &ops[..], s, &highlighter); for (_style, _text) in it { // Iterate to apply ops; we don't need the tokens here. } // Update the stack from the highlight state stack = highlight_state.path.clone(); let h = Self::hash_line(s); self.parse_after.push(ps.clone()); self.stack_after.push(stack.clone()); if i >= self.line_hashes.len() { self.line_hashes.push(h); } else { self.line_hashes[i] = h; } } } // Highlight a single line using cached state; update caches for this line. pub fn highlight_line_cached( &mut self, line_idx: usize, line: &str, provider: &dyn DataProvider, ) -> Vec { // Auto-detect prior changes and truncate cache if needed self.verify_and_truncate_before(line_idx, provider); // Precompute states up to line_idx self.ensure_state_before(line_idx, provider); let syntax = self.syntax_ref(); let theme = self.theme().clone(); // Clone to avoid borrow conflicts let highlighter = Highlighter::new(&theme); let mut ps = if line_idx == 0 { ParseState::new(syntax) } else if self.parse_after.len() >= line_idx { self.parse_after[line_idx - 1].clone() } else { ParseState::new(syntax) }; let stack = if line_idx == 0 { ScopeStack::new() } else if self.stack_after.len() >= line_idx { self.stack_after[line_idx - 1].clone() } else { ScopeStack::new() }; // Fix: parse_line takes 2 arguments: line and &SyntaxSet let ops = ps.parse_line(line, &self.ps).unwrap_or_default(); // Fix: HighlightState::new requires &Highlighter and ScopeStack let mut highlight_state = HighlightState::new(&highlighter, stack); // Fix: HighlightIterator::new expects &mut HighlightState as first parameter let iter = HighlightIterator::new(&mut highlight_state, &ops[..], line, &highlighter); let mut out: Vec = Vec::new(); for (syn_style, slice) in iter { if slice.is_empty() { continue; } let text = slice.trim_end_matches('\n').to_string(); if text.is_empty() { continue; } out.push(StyledChunk { text, style: Self::map_syntect_style(syn_style), }); } // Update caches for this line (state after this line) let h = Self::hash_line(line); if line_idx >= self.parse_after.len() { self.parse_after.push(ps); } else { self.parse_after[line_idx] = ps; } // Update stack from highlight state let final_stack = highlight_state.path.clone(); if line_idx >= self.stack_after.len() { self.stack_after.push(final_stack); } else { self.stack_after[line_idx] = final_stack; } if line_idx >= self.line_hashes.len() { self.line_hashes.push(h); } else { self.line_hashes[line_idx] = h; } out } }