vtcode_config/
output_styles.rs1use 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 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 let frontmatter_content = &frontmatter[4..frontmatter.len() - 4]; let config: OutputStyleFileConfig = serde_yaml::from_str(frontmatter_content)?;
100
101 let content_start = frontmatter_end + 5; 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 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 format!("{}\n\n{}", base_prompt, style.content)
151 } else {
152 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}