use crate::model::buffer::Buffer;
use crate::primitives::highlighter::HighlightSpan;
use crate::primitives::reference_highlighter::ReferenceHighlighter;
use ratatui::style::Color;
use std::time::{Duration, Instant};
pub const DEFAULT_DEBOUNCE_MS: u64 = 150;
pub struct ReferenceHighlightCache {
cached_spans: Vec<HighlightSpan>,
cached_cursor: Option<usize>,
cached_viewport: Option<(usize, usize)>,
cursor_changed_at: Option<Instant>,
debounce_delay: Duration,
}
impl ReferenceHighlightCache {
pub fn new() -> Self {
Self {
cached_spans: Vec::new(),
cached_cursor: None,
cached_viewport: None,
cursor_changed_at: None,
debounce_delay: Duration::from_millis(DEFAULT_DEBOUNCE_MS),
}
}
pub fn with_debounce(delay_ms: u64) -> Self {
Self {
debounce_delay: Duration::from_millis(delay_ms),
..Self::new()
}
}
pub fn get_highlights(
&mut self,
highlighter: &mut ReferenceHighlighter,
buffer: &Buffer,
cursor_position: usize,
viewport_start: usize,
viewport_end: usize,
context_bytes: usize,
highlight_color: Color,
) -> &[HighlightSpan] {
let now = Instant::now();
let viewport = (viewport_start, viewport_end);
if self.cached_cursor.is_none() {
self.cached_cursor = Some(cursor_position);
self.cached_viewport = Some(viewport);
highlighter.highlight_color = highlight_color;
self.cached_spans = highlighter.highlight_occurrences(
buffer,
cursor_position,
viewport_start,
viewport_end,
context_bytes,
);
return &self.cached_spans;
}
let cursor_changed = self.cached_cursor != Some(cursor_position);
let viewport_changed = self.cached_viewport != Some(viewport);
if cursor_changed {
self.cursor_changed_at = Some(now);
self.cached_cursor = Some(cursor_position);
if viewport_changed {
self.cached_viewport = Some(viewport);
}
return &self.cached_spans;
}
if let Some(changed_at) = self.cursor_changed_at {
if now.duration_since(changed_at) >= self.debounce_delay {
highlighter.highlight_color = highlight_color;
self.cached_spans = highlighter.highlight_occurrences(
buffer,
cursor_position,
viewport_start,
viewport_end,
context_bytes,
);
self.cached_viewport = Some(viewport);
self.cursor_changed_at = None;
}
} else if viewport_changed {
highlighter.highlight_color = highlight_color;
self.cached_spans = highlighter.highlight_occurrences(
buffer,
cursor_position,
viewport_start,
viewport_end,
context_bytes,
);
self.cached_viewport = Some(viewport);
}
&self.cached_spans
}
pub fn needs_redraw(&self) -> Option<Duration> {
self.cursor_changed_at.map(|changed_at| {
let elapsed = changed_at.elapsed();
if elapsed >= self.debounce_delay {
Duration::ZERO
} else {
self.debounce_delay - elapsed
}
})
}
pub fn invalidate(&mut self) {
self.cached_spans.clear();
self.cached_cursor = None;
self.cached_viewport = None;
self.cursor_changed_at = None;
}
pub fn is_debouncing(&self) -> bool {
self.cursor_changed_at.is_some()
}
pub fn debounce_delay(&self) -> Duration {
self.debounce_delay
}
pub fn set_debounce_delay(&mut self, delay: Duration) {
self.debounce_delay = delay;
}
}
impl Default for ReferenceHighlightCache {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::buffer::Buffer;
use crate::primitives::reference_highlighter::ReferenceHighlighter;
use std::thread::sleep;
#[test]
fn test_raw_highlighter_works() {
let mut highlighter = ReferenceHighlighter::new();
let buffer = Buffer::from_str_test("hello world hello");
let spans = highlighter.highlight_occurrences(&buffer, 0, 0, 17, 1000);
assert!(
!spans.is_empty(),
"Raw highlighter should find 'hello' occurrences"
);
}
#[test]
fn test_cache_computes_on_first_call() {
let mut cache = ReferenceHighlightCache::with_debounce(100);
let mut highlighter = ReferenceHighlighter::new();
let buffer = Buffer::from_str_test("hello world hello");
let color = Color::Rgb(60, 60, 80);
let spans = cache.get_highlights(&mut highlighter, &buffer, 0, 0, 17, 1000, color);
assert!(!spans.is_empty(), "Should compute on first call");
}
#[test]
fn test_cache_returns_stale_during_debounce() {
let mut cache = ReferenceHighlightCache::with_debounce(100);
let mut highlighter = ReferenceHighlighter::new();
let buffer = Buffer::from_str_test("hello world hello");
let color = Color::Rgb(60, 60, 80);
let spans = cache.get_highlights(&mut highlighter, &buffer, 0, 0, 17, 1000, color);
assert!(!spans.is_empty(), "Should compute on first call");
let first_len = spans.len();
let spans = cache.get_highlights(&mut highlighter, &buffer, 6, 0, 17, 1000, color);
assert_eq!(
spans.len(),
first_len,
"Should return stale cache during debounce"
);
assert!(
cache.needs_redraw().is_some(),
"Should signal need for redraw"
);
}
#[test]
fn test_cache_computes_after_debounce() {
let mut cache = ReferenceHighlightCache::with_debounce(10); let mut highlighter = ReferenceHighlighter::new();
let buffer = Buffer::from_str_test("hello world hello");
let color = Color::Rgb(60, 60, 80);
let spans = cache.get_highlights(&mut highlighter, &buffer, 0, 0, 17, 1000, color);
let first_count = spans.len();
assert!(!spans.is_empty(), "Should compute on first call");
let _ = cache.get_highlights(&mut highlighter, &buffer, 6, 0, 17, 1000, color);
sleep(Duration::from_millis(20));
let spans = cache.get_highlights(&mut highlighter, &buffer, 6, 0, 17, 1000, color);
assert!(!spans.is_empty(), "Should have highlights after debounce");
assert!(
spans.len() != first_count || spans.len() == 1,
"Should have recomputed for new word"
);
}
#[test]
fn test_cache_invalidation() {
let mut cache = ReferenceHighlightCache::with_debounce(10);
let mut highlighter = ReferenceHighlighter::new();
let buffer = Buffer::from_str_test("hello world hello");
let color = Color::Rgb(60, 60, 80);
let spans = cache.get_highlights(&mut highlighter, &buffer, 0, 0, 17, 1000, color);
assert!(!spans.is_empty());
cache.invalidate();
let spans = cache.get_highlights(&mut highlighter, &buffer, 0, 0, 17, 1000, color);
assert!(!spans.is_empty(), "Should recompute after invalidation");
}
#[test]
fn test_needs_redraw() {
let mut cache = ReferenceHighlightCache::with_debounce(50);
let mut highlighter = ReferenceHighlighter::new();
let buffer = Buffer::from_str_test("hello world");
let color = Color::Rgb(60, 60, 80);
assert!(cache.needs_redraw().is_none());
let _ = cache.get_highlights(&mut highlighter, &buffer, 0, 0, 11, 1000, color);
assert!(cache.needs_redraw().is_none());
let _ = cache.get_highlights(&mut highlighter, &buffer, 6, 0, 11, 1000, color);
let remaining = cache.needs_redraw();
assert!(remaining.is_some());
assert!(remaining.unwrap() <= Duration::from_millis(50));
}
}