Skip to main content

vtcode_tui/utils/
cached_style_parser.rs

1//! Cached Style Parser for Performance Optimization
2//!
3//! This module provides caching for frequently parsed anstyle strings to avoid
4//! repeated parsing overhead, especially useful for theme parsing and
5//! frequently used style configurations.
6//!
7//! Uses [`LrMap`] (left-right concurrent map) for lock-free reads on the hot
8//! path while still allowing safe concurrent inserts on cache miss.
9
10use anstyle::Style as AnsiStyle;
11use anyhow::{Context, Result};
12use vtcode_commons::lr_map::LrMap;
13
14/// Thread-safe cached parser for Git and LS_COLORS style strings
15pub struct CachedStyleParser {
16    git_cache: LrMap<String, AnsiStyle>,
17    ls_colors_cache: LrMap<String, AnsiStyle>,
18}
19
20impl CachedStyleParser {
21    /// Create a new cached style parser
22    pub fn new() -> Self {
23        Self {
24            git_cache: LrMap::new(),
25            ls_colors_cache: LrMap::new(),
26        }
27    }
28
29    /// Parse and cache a Git-style color string (e.g., "bold red blue")
30    pub fn parse_git_style(&self, input: &str) -> Result<AnsiStyle> {
31        if let Some(cached) = self.git_cache.get(input) {
32            return Ok(cached);
33        }
34
35        let result = anstyle_git::parse(input)
36            .map_err(|e| anyhow::anyhow!("Failed to parse Git style '{}': {:?}", input, e))?;
37
38        self.git_cache.insert(input.to_string(), result);
39        Ok(result)
40    }
41
42    /// Parse and cache an LS_COLORS-style string (e.g., "01;34")
43    pub fn parse_ls_colors(&self, input: &str) -> Result<AnsiStyle> {
44        if let Some(cached) = self.ls_colors_cache.get(input) {
45            return Ok(cached);
46        }
47
48        let result = anstyle_ls::parse(input)
49            .ok_or_else(|| anyhow::anyhow!("Failed to parse LS_COLORS '{}'", input))?;
50
51        self.ls_colors_cache.insert(input.to_string(), result);
52        Ok(result)
53    }
54
55    /// Parse using Git syntax first, then LS_COLORS as fallback, with caching
56    pub fn parse_flexible(&self, input: &str) -> Result<AnsiStyle> {
57        match self.parse_git_style(input) {
58            Ok(style) => Ok(style),
59            Err(_) => self
60                .parse_ls_colors(input)
61                .with_context(|| format!("Could not parse style string: '{}'", input)),
62        }
63    }
64
65    /// Clear all cached styles
66    pub fn clear_cache(&self) {
67        self.git_cache.clear();
68        self.ls_colors_cache.clear();
69    }
70
71    /// Get cache statistics
72    pub fn cache_stats(&self) -> (usize, usize) {
73        (self.git_cache.len(), self.ls_colors_cache.len())
74    }
75}
76
77impl Default for CachedStyleParser {
78    fn default() -> Self {
79        Self::new()
80    }
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86
87    #[test]
88    fn test_parse_git_style() {
89        let parser = CachedStyleParser::new();
90        let result = parser.parse_git_style("bold red").unwrap();
91
92        assert!(result.get_effects().contains(anstyle::Effects::BOLD));
93    }
94
95    #[test]
96    fn test_parse_ls_colors() {
97        let parser = CachedStyleParser::new();
98        let result = parser.parse_ls_colors("34").unwrap(); // Blue
99
100        assert!(result.get_fg_color().is_some());
101    }
102
103    #[test]
104    fn test_parse_flexible_git_first() {
105        let parser = CachedStyleParser::new();
106        let result = parser.parse_flexible("bold green").unwrap();
107
108        assert!(result.get_effects().contains(anstyle::Effects::BOLD));
109    }
110
111    #[test]
112    fn test_parse_flexible_ls_fallback() {
113        let parser = CachedStyleParser::new();
114        let result = parser.parse_flexible("01;34").unwrap(); // Bold blue in ANSI codes
115
116        assert!(result.get_effects().contains(anstyle::Effects::BOLD));
117    }
118
119    #[test]
120    fn test_caching_behavior() {
121        let parser = CachedStyleParser::new();
122
123        // Parse same string twice - should use cache on second call
124        let _result1 = parser.parse_git_style("red").unwrap();
125        let _result2 = parser.parse_git_style("red").unwrap();
126
127        let (git_count, _) = parser.cache_stats();
128        assert_eq!(git_count, 1); // Only one cached entry for "red"
129    }
130
131    #[test]
132    fn test_cache_clear() {
133        let parser = CachedStyleParser::new();
134        let _result = parser.parse_git_style("blue").unwrap();
135
136        assert_eq!(parser.cache_stats().0, 1); // One cached entry
137
138        parser.clear_cache();
139
140        assert_eq!(parser.cache_stats().0, 0); // Cache cleared
141    }
142
143    #[test]
144    fn test_multiple_cache_entries() {
145        let parser = CachedStyleParser::new();
146        let _result1 = parser.parse_git_style("bold red").unwrap();
147        let _result2 = parser.parse_git_style("italic green").unwrap();
148        let _result3 = parser.parse_ls_colors("34").unwrap();
149
150        let (git_count, ls_count) = parser.cache_stats();
151        assert_eq!(git_count, 2); // Two Git style entries
152        assert_eq!(ls_count, 1); // One LS_COLORS entry
153    }
154}