Skip to main content

vtcode_config/
output_styles.rs

1use hashbrown::HashMap;
2use serde::{Deserialize, Serialize};
3use std::fs;
4use std::path::Path;
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
7#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
8pub struct OutputStyleConfig {
9    #[serde(default = "default_output_style")]
10    pub active_style: String,
11}
12
13fn default_output_style() -> String {
14    "default".to_string()
15}
16
17impl Default for OutputStyleConfig {
18    fn default() -> Self {
19        Self {
20            active_style: default_output_style(),
21        }
22    }
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
26#[serde(rename_all = "kebab-case")]
27pub struct OutputStyleFileConfig {
28    pub name: String,
29    pub description: Option<String>,
30    #[serde(default)]
31    pub keep_coding_instructions: bool,
32}
33
34#[derive(Debug, Clone)]
35pub struct OutputStyle {
36    pub config: OutputStyleFileConfig,
37    pub content: String,
38}
39
40#[derive(Debug)]
41pub struct OutputStyleManager {
42    styles: HashMap<String, OutputStyle>,
43}
44
45impl Default for OutputStyleManager {
46    fn default() -> Self {
47        Self::new()
48    }
49}
50
51impl OutputStyleManager {
52    pub fn new() -> Self {
53        Self {
54            styles: HashMap::new(),
55        }
56    }
57
58    pub fn load_from_directory<P: AsRef<Path>>(dir: P) -> Result<Self, Box<dyn std::error::Error>> {
59        let mut manager = Self::new();
60        let dir = dir.as_ref();
61
62        if !dir.exists() {
63            return Ok(manager);
64        }
65
66        for entry in fs::read_dir(dir)? {
67            let entry = entry?;
68            let path = entry.path();
69
70            if path.extension().and_then(|s| s.to_str()) == Some("md")
71                && let Ok(output_style) = Self::load_from_file(&path)
72            {
73                manager
74                    .styles
75                    .insert(output_style.config.name.clone(), output_style);
76            }
77        }
78
79        Ok(manager)
80    }
81
82    fn load_from_file<P: AsRef<Path>>(path: P) -> Result<OutputStyle, Box<dyn std::error::Error>> {
83        let content = fs::read_to_string(path)?;
84        Self::parse_output_style(&content)
85    }
86
87    fn parse_output_style(content: &str) -> Result<OutputStyle, Box<dyn std::error::Error>> {
88        // Look for frontmatter (between --- and ---)
89        if let Some(frontmatter_body) = content.strip_prefix("---\n")
90            && let Some(frontmatter_end) = frontmatter_body.find("\n---\n")
91        {
92            let frontmatter_content = &frontmatter_body[..frontmatter_end];
93            let config: OutputStyleFileConfig = serde_yaml::from_str(frontmatter_content)?;
94
95            // Get the content after the frontmatter
96            let content_start = frontmatter_end + 5; // Skip past body + "\n---\n"
97            let actual_content = if content_start < frontmatter_body.len() {
98                &frontmatter_body[content_start..]
99            } else {
100                ""
101            };
102
103            Ok(OutputStyle {
104                config,
105                content: actual_content.to_string(),
106            })
107        } else {
108            // No frontmatter, create default config
109            Ok(OutputStyle {
110                config: OutputStyleFileConfig {
111                    name: "default".to_string(),
112                    description: Some("Default output style".to_string()),
113                    keep_coding_instructions: true,
114                },
115                content: content.to_string(),
116            })
117        }
118    }
119
120    pub fn get_style(&self, name: &str) -> Option<&OutputStyle> {
121        self.styles.get(name)
122    }
123
124    pub fn list_styles(&self) -> Vec<(&String, &str)> {
125        self.styles
126            .iter()
127            .map(|(name, style)| {
128                (
129                    name,
130                    style
131                        .config
132                        .description
133                        .as_deref()
134                        .unwrap_or("No description"),
135                )
136            })
137            .collect()
138    }
139
140    pub fn apply_style(&self, name: &str, base_prompt: &str) -> String {
141        if let Some(style) = self.get_style(name) {
142            if style.config.keep_coding_instructions {
143                // Combine base prompt with style content
144                format!("{}\n\n{}", base_prompt, style.content)
145            } else {
146                // Replace base prompt with style content
147                style.content.clone()
148            }
149        } else {
150            base_prompt.to_string()
151        }
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158    use std::fs;
159    use tempfile::TempDir;
160
161    #[test]
162    fn test_parse_output_style_with_frontmatter() {
163        let content = r#"---
164name: Test Style
165description: A test output style
166keep-coding-instructions: false
167---
168
169# Test Output Style
170
171This is a test output style."#;
172
173        let style = OutputStyleManager::parse_output_style(content).unwrap();
174        assert_eq!(style.config.name, "Test Style");
175        assert_eq!(
176            style.config.description,
177            Some("A test output style".to_string())
178        );
179        assert!(!style.config.keep_coding_instructions);
180        assert!(style.content.contains("This is a test output style"));
181    }
182
183    #[test]
184    fn test_parse_output_style_with_bare_frontmatter_fence() {
185        let style = OutputStyleManager::parse_output_style("---").unwrap();
186
187        assert_eq!(style.config.name, "default");
188        assert_eq!(style.content, "---");
189    }
190
191    #[test]
192    fn test_parse_output_style_without_frontmatter() {
193        let content = r#"This is a plain output style without frontmatter."#;
194
195        let style = OutputStyleManager::parse_output_style(content).unwrap();
196        assert_eq!(style.config.name, "default");
197        assert!(style.content.contains("This is a plain output style"));
198    }
199
200    #[test]
201    fn test_load_from_directory() {
202        let temp_dir = TempDir::new().unwrap();
203        let style_file = temp_dir.path().join("test_style.md");
204
205        fs::write(
206            &style_file,
207            r#"---
208name: Test Style
209description: A test output style
210keep-coding-instructions: true
211---
212
213# Test Output Style
214
215This is a test output style."#,
216        )
217        .unwrap();
218
219        let manager = OutputStyleManager::load_from_directory(temp_dir.path()).unwrap();
220        assert!(manager.get_style("Test Style").is_some());
221    }
222
223    #[test]
224    fn test_apply_style_with_keep_instructions() {
225        let content = r#"---
226name: Test Style
227description: A test output style
228keep-coding-instructions: true
229---
230
231## Custom Instructions
232
233Custom instructions here."#;
234
235        let style = OutputStyleManager::parse_output_style(content).unwrap();
236        let mut manager = OutputStyleManager::new();
237        manager.styles.insert("Test Style".to_string(), style);
238
239        let base_prompt = "Base system prompt";
240        let result = manager.apply_style("Test Style", base_prompt);
241
242        assert!(result.contains("Base system prompt"));
243        assert!(result.contains("Custom instructions here"));
244    }
245
246    #[test]
247    fn test_apply_style_without_keep_instructions() {
248        let content = r#"---
249name: Test Style
250description: A test output style
251keep-coding-instructions: false
252---
253
254## Custom Instructions
255
256Custom instructions here."#;
257
258        let style = OutputStyleManager::parse_output_style(content).unwrap();
259        let mut manager = OutputStyleManager::new();
260        manager.styles.insert("Test Style".to_string(), style);
261
262        let base_prompt = "Base system prompt";
263        let result = manager.apply_style("Test Style", base_prompt);
264
265        assert!(!result.contains("Base system prompt"));
266        assert!(result.contains("Custom instructions here"));
267    }
268}