Skip to main content

vtcode_ui/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(crate) 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(crate) 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 =
36            anstyle_git::parse(input).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(crate) 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).ok_or_else(|| anyhow::anyhow!("Failed to parse LS_COLORS '{input}'"))?;
49
50        self.ls_colors_cache.insert(input.to_string(), result);
51        Ok(result)
52    }
53
54    /// Parse using Git syntax first, then LS_COLORS as fallback, with caching
55    pub(crate) fn parse_flexible(&self, input: &str) -> Result<AnsiStyle> {
56        match self.parse_git_style(input) {
57            Ok(style) => Ok(style),
58            Err(_) => self
59                .parse_ls_colors(input)
60                .with_context(|| format!("Could not parse style string: '{input}'")),
61        }
62    }
63
64    /// Clear all cached styles
65    fn clear_cache(&self) {
66        self.git_cache.clear();
67        self.ls_colors_cache.clear();
68    }
69
70    /// Get cache statistics
71    fn cache_stats(&self) -> (usize, usize) {
72        (self.git_cache.len(), self.ls_colors_cache.len())
73    }
74}
75
76impl Default for CachedStyleParser {
77    fn default() -> Self {
78        Self::new()
79    }
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85
86    #[test]
87    fn test_parse_git_style() {
88        let parser = CachedStyleParser::new();
89        let result = parser.parse_git_style("bold red").unwrap();
90
91        assert!(result.get_effects().contains(anstyle::Effects::BOLD));
92    }
93
94    #[test]
95    fn test_parse_ls_colors() {
96        let parser = CachedStyleParser::new();
97        let result = parser.parse_ls_colors("34").unwrap(); // Blue
98
99        assert!(result.get_fg_color().is_some());
100    }
101
102    #[test]
103    fn test_parse_flexible_git_first() {
104        let parser = CachedStyleParser::new();
105        let result = parser.parse_flexible("bold green").unwrap();
106
107        assert!(result.get_effects().contains(anstyle::Effects::BOLD));
108    }
109
110    #[test]
111    fn test_parse_flexible_ls_fallback() {
112        let parser = CachedStyleParser::new();
113        let result = parser.parse_flexible("01;34").unwrap(); // Bold blue in ANSI codes
114
115        assert!(result.get_effects().contains(anstyle::Effects::BOLD));
116    }
117
118    #[test]
119    fn test_caching_behavior() {
120        let parser = CachedStyleParser::new();
121
122        // Parse same string twice - should use cache on second call
123        let _result1 = parser.parse_git_style("red").unwrap();
124        let _result2 = parser.parse_git_style("red").unwrap();
125
126        let (git_count, _) = parser.cache_stats();
127        assert_eq!(git_count, 1); // Only one cached entry for "red"
128    }
129
130    #[test]
131    fn test_cache_clear() {
132        let parser = CachedStyleParser::new();
133        let _result = parser.parse_git_style("blue").unwrap();
134
135        assert_eq!(parser.cache_stats().0, 1); // One cached entry
136
137        parser.clear_cache();
138
139        assert_eq!(parser.cache_stats().0, 0); // Cache cleared
140    }
141
142    #[test]
143    fn test_multiple_cache_entries() {
144        let parser = CachedStyleParser::new();
145        let _result1 = parser.parse_git_style("bold red").unwrap();
146        let _result2 = parser.parse_git_style("italic green").unwrap();
147        let _result3 = parser.parse_ls_colors("34").unwrap();
148
149        let (git_count, ls_count) = parser.cache_stats();
150        assert_eq!(git_count, 2); // Two Git style entries
151        assert_eq!(ls_count, 1); // One LS_COLORS entry
152    }
153}