vtcode_config/
output_styles.rs1use 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 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 let content_start = frontmatter_end + 5; 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 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 format!("{}\n\n{}", base_prompt, style.content)
145 } else {
146 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}