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_saphyr::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<(&str, &str)> {
125 self.styles
126 .iter()
127 .map(|(name, style)| {
128 (
129 name.as_str(),
130 style.config.description.as_deref().unwrap_or("No description"),
131 )
132 })
133 .collect()
134 }
135
136 pub fn apply_style(&self, name: &str, base_prompt: &str) -> String {
137 if let Some(style) = self.get_style(name) {
138 if style.config.keep_coding_instructions {
139 format!("{}\n\n{}", base_prompt, style.content)
141 } else {
142 style.content.clone()
144 }
145 } else {
146 base_prompt.to_string()
147 }
148 }
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154 use std::fs;
155 use tempfile::TempDir;
156
157 #[test]
158 fn test_parse_output_style_with_frontmatter() {
159 let content = r#"---
160name: Test Style
161description: A test output style
162keep-coding-instructions: false
163---
164
165# Test Output Style
166
167This is a test output style."#;
168
169 let style = OutputStyleManager::parse_output_style(content).unwrap();
170 assert_eq!(style.config.name, "Test Style");
171 assert_eq!(
172 style.config.description,
173 Some("A test output style".to_string())
174 );
175 assert!(!style.config.keep_coding_instructions);
176 assert!(style.content.contains("This is a test output style"));
177 }
178
179 #[test]
180 fn test_parse_output_style_with_bare_frontmatter_fence() {
181 let style = OutputStyleManager::parse_output_style("---").unwrap();
182
183 assert_eq!(style.config.name, "default");
184 assert_eq!(style.content, "---");
185 }
186
187 #[test]
188 fn test_parse_output_style_without_frontmatter() {
189 let content = r#"This is a plain output style without frontmatter."#;
190
191 let style = OutputStyleManager::parse_output_style(content).unwrap();
192 assert_eq!(style.config.name, "default");
193 assert!(style.content.contains("This is a plain output style"));
194 }
195
196 #[test]
197 fn test_load_from_directory() {
198 let temp_dir = TempDir::new().unwrap();
199 let style_file = temp_dir.path().join("test_style.md");
200
201 fs::write(
202 &style_file,
203 r#"---
204name: Test Style
205description: A test output style
206keep-coding-instructions: true
207---
208
209# Test Output Style
210
211This is a test output style."#,
212 )
213 .unwrap();
214
215 let manager = OutputStyleManager::load_from_directory(temp_dir.path()).unwrap();
216 assert!(manager.get_style("Test Style").is_some());
217 }
218
219 #[test]
220 fn test_apply_style_with_keep_instructions() {
221 let content = r#"---
222name: Test Style
223description: A test output style
224keep-coding-instructions: true
225---
226
227## Custom Instructions
228
229Custom instructions here."#;
230
231 let style = OutputStyleManager::parse_output_style(content).unwrap();
232 let mut manager = OutputStyleManager::new();
233 manager.styles.insert("Test Style".to_string(), style);
234
235 let base_prompt = "Base system prompt";
236 let result = manager.apply_style("Test Style", base_prompt);
237
238 assert!(result.contains("Base system prompt"));
239 assert!(result.contains("Custom instructions here"));
240 }
241
242 #[test]
243 fn test_apply_style_without_keep_instructions() {
244 let content = r#"---
245name: Test Style
246description: A test output style
247keep-coding-instructions: false
248---
249
250## Custom Instructions
251
252Custom instructions here."#;
253
254 let style = OutputStyleManager::parse_output_style(content).unwrap();
255 let mut manager = OutputStyleManager::new();
256 manager.styles.insert("Test Style".to_string(), style);
257
258 let base_prompt = "Base system prompt";
259 let result = manager.apply_style("Test Style", base_prompt);
260
261 assert!(!result.contains("Base system prompt"));
262 assert!(result.contains("Custom instructions here"));
263 }
264}