vtcode_config/
output_styles.rs

1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
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_end) = content.find("\n---\n") {
90            let frontmatter_start = if content.starts_with("---\n") {
91                0
92            } else {
93                content.find("---\n").unwrap_or(0)
94            };
95            let frontmatter = &content[frontmatter_start..frontmatter_end + 4];
96
97            // Parse the frontmatter
98            let frontmatter_content = &frontmatter[4..frontmatter.len() - 4]; // Remove the ---\n and \n---
99            let config: OutputStyleFileConfig = serde_yaml::from_str(frontmatter_content)?;
100
101            // Get the content after the frontmatter
102            let content_start = frontmatter_end + 5; // Skip past "\n---\n"
103            let actual_content = if content_start < content.len() {
104                &content[content_start..]
105            } else {
106                ""
107            };
108
109            Ok(OutputStyle {
110                config,
111                content: actual_content.to_string(),
112            })
113        } else {
114            // No frontmatter, create default config
115            Ok(OutputStyle {
116                config: OutputStyleFileConfig {
117                    name: "default".to_string(),
118                    description: Some("Default output style".to_string()),
119                    keep_coding_instructions: true,
120                },
121                content: content.to_string(),
122            })
123        }
124    }
125
126    pub fn get_style(&self, name: &str) -> Option<&OutputStyle> {
127        self.styles.get(name)
128    }
129
130    pub fn list_styles(&self) -> Vec<(&String, &str)> {
131        self.styles
132            .iter()
133            .map(|(name, style)| {
134                (
135                    name,
136                    style
137                        .config
138                        .description
139                        .as_deref()
140                        .unwrap_or("No description"),
141                )
142            })
143            .collect()
144    }
145
146    pub fn apply_style(&self, name: &str, base_prompt: &str) -> String {
147        if let Some(style) = self.get_style(name) {
148            if style.config.keep_coding_instructions {
149                // Combine base prompt with style content
150                format!("{}\n\n{}", base_prompt, style.content)
151            } else {
152                // Replace base prompt with style content
153                style.content.clone()
154            }
155        } else {
156            base_prompt.to_string()
157        }
158    }
159}
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164    use std::fs;
165    use tempfile::TempDir;
166
167    #[test]
168    fn test_parse_output_style_with_frontmatter() {
169        let content = r#"---
170name: Test Style
171description: A test output style
172keep-coding-instructions: false
173---
174
175# Test Output Style
176
177This is a test output style."#;
178
179        let style = OutputStyleManager::parse_output_style(content).unwrap();
180        assert_eq!(style.config.name, "Test Style");
181        assert_eq!(
182            style.config.description,
183            Some("A test output style".to_string())
184        );
185        assert_eq!(style.config.keep_coding_instructions, false);
186        assert!(style.content.contains("This is a test output style"));
187    }
188
189    #[test]
190    fn test_parse_output_style_without_frontmatter() {
191        let content = r#"This is a plain output style without frontmatter."#;
192
193        let style = OutputStyleManager::parse_output_style(content).unwrap();
194        assert_eq!(style.config.name, "default");
195        assert!(style.content.contains("This is a plain output style"));
196    }
197
198    #[test]
199    fn test_load_from_directory() {
200        let temp_dir = TempDir::new().unwrap();
201        let style_file = temp_dir.path().join("test_style.md");
202
203        fs::write(
204            &style_file,
205            r#"---
206name: Test Style
207description: A test output style
208keep-coding-instructions: true
209---
210
211# Test Output Style
212
213This is a test output style."#,
214        )
215        .unwrap();
216
217        let manager = OutputStyleManager::load_from_directory(temp_dir.path()).unwrap();
218        assert!(manager.get_style("Test Style").is_some());
219    }
220
221    #[test]
222    fn test_apply_style_with_keep_instructions() {
223        let content = r#"---
224name: Test Style
225description: A test output style
226keep-coding-instructions: true
227---
228
229## Custom Instructions
230
231Custom instructions here."#;
232
233        let style = OutputStyleManager::parse_output_style(content).unwrap();
234        let mut manager = OutputStyleManager::new();
235        manager.styles.insert("Test Style".to_string(), style);
236
237        let base_prompt = "Base system prompt";
238        let result = manager.apply_style("Test Style", base_prompt);
239
240        assert!(result.contains("Base system prompt"));
241        assert!(result.contains("Custom instructions here"));
242    }
243
244    #[test]
245    fn test_apply_style_without_keep_instructions() {
246        let content = r#"---
247name: Test Style
248description: A test output style
249keep-coding-instructions: false
250---
251
252## Custom Instructions
253
254Custom instructions here."#;
255
256        let style = OutputStyleManager::parse_output_style(content).unwrap();
257        let mut manager = OutputStyleManager::new();
258        manager.styles.insert("Test Style".to_string(), style);
259
260        let base_prompt = "Base system prompt";
261        let result = manager.apply_style("Test Style", base_prompt);
262
263        assert!(!result.contains("Base system prompt"));
264        assert!(result.contains("Custom instructions here"));
265    }
266}