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