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
7use anstyle::Style as AnsiStyle;
8use anyhow::{Context, Result};
9use std::collections::HashMap;
10use std::sync::RwLock;
11
12/// Thread-safe cached parser for Git and LS_COLORS style strings
13pub struct CachedStyleParser {
14    git_cache: RwLock<HashMap<String, AnsiStyle>>,
15    ls_colors_cache: RwLock<HashMap<String, AnsiStyle>>,
16}
17
18impl CachedStyleParser {
19    /// Create a new cached style parser
20    pub fn new() -> Self {
21        Self {
22            git_cache: RwLock::new(HashMap::new()),
23            ls_colors_cache: RwLock::new(HashMap::new()),
24        }
25    }
26
27    /// Parse and cache a Git-style color string (e.g., "bold red blue")
28    pub fn parse_git_style(&self, input: &str) -> Result<AnsiStyle> {
29        // Check cache first
30        if let Ok(cache) = self.git_cache.read()
31            && let Some(cached) = cache.get(input)
32        {
33            return Ok(*cached);
34        }
35
36        // Parse and cache result
37        let result = anstyle_git::parse(input)
38            .map_err(|e| anyhow::anyhow!("Failed to parse Git style '{}': {:?}", input, e))?;
39
40        if let Ok(mut cache) = self.git_cache.write() {
41            cache.insert(input.to_string(), result);
42        }
43
44        Ok(result)
45    }
46
47    /// Parse and cache an LS_COLORS-style string (e.g., "01;34")
48    pub fn parse_ls_colors(&self, input: &str) -> Result<AnsiStyle> {
49        // Check cache first
50        if let Ok(cache) = self.ls_colors_cache.read()
51            && let Some(cached) = cache.get(input)
52        {
53            return Ok(*cached);
54        }
55
56        // Parse and cache result
57        let result = anstyle_ls::parse(input)
58            .ok_or_else(|| anyhow::anyhow!("Failed to parse LS_COLORS '{}'", input))?;
59
60        if let Ok(mut cache) = self.ls_colors_cache.write() {
61            cache.insert(input.to_string(), result);
62        }
63
64        Ok(result)
65    }
66
67    /// Parse using Git syntax first, then LS_COLORS as fallback, with caching
68    pub fn parse_flexible(&self, input: &str) -> Result<AnsiStyle> {
69        // Try Git syntax first
70        match self.parse_git_style(input) {
71            Ok(style) => Ok(style),
72            Err(_) => {
73                // Fall back to LS_COLORS if Git parsing fails
74                self.parse_ls_colors(input)
75                    .with_context(|| format!("Could not parse style string: '{}'", input))
76            }
77        }
78    }
79
80    /// Clear all cached styles
81    pub fn clear_cache(&self) {
82        if let Ok(mut git_cache) = self.git_cache.write() {
83            git_cache.clear();
84        }
85        if let Ok(mut ls_colors_cache) = self.ls_colors_cache.write() {
86            ls_colors_cache.clear();
87        }
88    }
89
90    /// Get cache statistics
91    pub fn cache_stats(&self) -> (usize, usize) {
92        let git_count = self.git_cache.read().ok().map(|c| c.len()).unwrap_or(0);
93        let ls_colors_count = self
94            .ls_colors_cache
95            .read()
96            .ok()
97            .map(|c| c.len())
98            .unwrap_or(0);
99        (git_count, ls_colors_count)
100    }
101}
102
103impl Default for CachedStyleParser {
104    fn default() -> Self {
105        Self::new()
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112
113    #[test]
114    fn test_parse_git_style() {
115        let parser = CachedStyleParser::new();
116        let result = parser.parse_git_style("bold red").unwrap();
117
118        assert!(result.get_effects().contains(anstyle::Effects::BOLD));
119    }
120
121    #[test]
122    fn test_parse_ls_colors() {
123        let parser = CachedStyleParser::new();
124        let result = parser.parse_ls_colors("34").unwrap(); // Blue
125
126        assert!(result.get_fg_color().is_some());
127    }
128
129    #[test]
130    fn test_parse_flexible_git_first() {
131        let parser = CachedStyleParser::new();
132        let result = parser.parse_flexible("bold green").unwrap();
133
134        assert!(result.get_effects().contains(anstyle::Effects::BOLD));
135    }
136
137    #[test]
138    fn test_parse_flexible_ls_fallback() {
139        let parser = CachedStyleParser::new();
140        let result = parser.parse_flexible("01;34").unwrap(); // Bold blue in ANSI codes
141
142        assert!(result.get_effects().contains(anstyle::Effects::BOLD));
143    }
144
145    #[test]
146    fn test_caching_behavior() {
147        let parser = CachedStyleParser::new();
148
149        // Parse same string twice - should use cache on second call
150        let _result1 = parser.parse_git_style("red").unwrap();
151        let _result2 = parser.parse_git_style("red").unwrap();
152
153        let (git_count, _) = parser.cache_stats();
154        assert_eq!(git_count, 1); // Only one cached entry for "red"
155    }
156
157    #[test]
158    fn test_cache_clear() {
159        let parser = CachedStyleParser::new();
160        let _result = parser.parse_git_style("blue").unwrap();
161
162        assert_eq!(parser.cache_stats().0, 1); // One cached entry
163
164        parser.clear_cache();
165
166        assert_eq!(parser.cache_stats().0, 0); // Cache cleared
167    }
168
169    #[test]
170    fn test_multiple_cache_entries() {
171        let parser = CachedStyleParser::new();
172        let _result1 = parser.parse_git_style("bold red").unwrap();
173        let _result2 = parser.parse_git_style("italic green").unwrap();
174        let _result3 = parser.parse_ls_colors("34").unwrap();
175
176        let (git_count, ls_count) = parser.cache_stats();
177        assert_eq!(git_count, 2); // Two Git style entries
178        assert_eq!(ls_count, 1); // One LS_COLORS entry
179    }
180}