1use super::{RuleEmitter, ensure_parent_dir, format_bullet_list, format_heading};
4use crate::{Editor, Result, parser::UnifiedRule};
5use std::path::Path;
6
7#[derive(Debug, Default)]
9pub struct CursorEmitter;
10
11impl CursorEmitter {
12 pub fn new() -> Self {
14 Self
15 }
16}
17
18impl RuleEmitter for CursorEmitter {
19 fn emit_file(&self, rules: &[UnifiedRule], path: &Path) -> Result<()> {
20 ensure_parent_dir(path)?;
21 let content = self.emit_string(rules)?;
22 std::fs::write(path, content)?;
23 Ok(())
24 }
25
26 fn emit_string(&self, rules: &[UnifiedRule]) -> Result<String> {
27 let mut output = String::new();
28
29 let mut personas: Vec<&UnifiedRule> = Vec::new();
31 let mut standards: Vec<&UnifiedRule> = Vec::new();
32 let mut contexts: Vec<&UnifiedRule> = Vec::new();
33 let mut workflows: Vec<&UnifiedRule> = Vec::new();
34 let mut raw: Vec<&UnifiedRule> = Vec::new();
35
36 for rule in rules {
37 match rule {
38 UnifiedRule::Persona { .. } => personas.push(rule),
39 UnifiedRule::Standard { .. } => standards.push(rule),
40 UnifiedRule::Context { .. } => contexts.push(rule),
41 UnifiedRule::Workflow { .. } => workflows.push(rule),
42 UnifiedRule::Raw { .. } => raw.push(rule),
43 }
44 }
45
46 for rule in personas {
48 if let UnifiedRule::Persona {
49 name,
50 role,
51 identity,
52 style,
53 traits,
54 principles,
55 } = rule
56 {
57 output.push_str(&format_heading(1, &format!("AI Persona: {}", name)));
58 output.push_str(role);
59 output.push_str("\n\n");
60
61 if let Some(id) = identity {
62 output.push_str(&format!("**Identity:** {}\n\n", id));
63 }
64
65 if let Some(s) = style {
66 output.push_str(&format!("**Communication Style:** {}\n\n", s));
67 }
68
69 if !traits.is_empty() {
70 output.push_str(&format_heading(2, "Traits"));
71 output.push_str(&format_bullet_list(traits));
72 output.push('\n');
73 }
74
75 if !principles.is_empty() {
76 output.push_str(&format_heading(2, "Principles"));
77 output.push_str(&format_bullet_list(principles));
78 output.push('\n');
79 }
80 }
81 }
82
83 if !contexts.is_empty() {
85 output.push_str(&format_heading(1, "Project Context"));
86 for rule in contexts {
87 if let UnifiedRule::Context {
88 includes,
89 excludes,
90 focus,
91 } = rule
92 {
93 if !includes.is_empty() {
94 output.push_str(&format_heading(2, "Include"));
95 output.push_str(&format_bullet_list(includes));
96 output.push('\n');
97 }
98
99 if !excludes.is_empty() {
100 output.push_str(&format_heading(2, "Exclude"));
101 output.push_str(&format_bullet_list(excludes));
102 output.push('\n');
103 }
104
105 if !focus.is_empty() {
106 output.push_str(&format_heading(2, "Focus Areas"));
107 output.push_str(&format_bullet_list(focus));
108 output.push('\n');
109 }
110 }
111 }
112 }
113
114 if !standards.is_empty() {
116 output.push_str(&format_heading(1, "Coding Standards"));
117
118 use std::collections::HashMap;
120 let mut by_category: HashMap<String, Vec<&str>> = HashMap::new();
121
122 for rule in &standards {
123 if let UnifiedRule::Standard {
124 category,
125 description,
126 ..
127 } = rule
128 {
129 let cat_name = format!("{:?}", category);
130 by_category.entry(cat_name).or_default().push(description);
131 }
132 }
133
134 for (category, descriptions) in by_category {
135 output.push_str(&format_heading(2, &category));
136 let items: Vec<String> = descriptions.iter().map(|s| s.to_string()).collect();
137 output.push_str(&format_bullet_list(&items));
138 output.push('\n');
139 }
140 }
141
142 for rule in workflows {
144 if let UnifiedRule::Workflow { name, steps } = rule {
145 output.push_str(&format_heading(1, &format!("Workflow: {}", name)));
146
147 for (i, step) in steps.iter().enumerate() {
148 output.push_str(&format!("### Step {}: {}\n\n", i + 1, step.name));
149 output.push_str(&step.description);
150 output.push_str("\n\n");
151
152 if let Some(condition) = &step.condition {
153 output.push_str(&format!("**Condition:** {}\n\n", condition));
154 }
155
156 if !step.actions.is_empty() {
157 output.push_str("**Actions:**\n");
158 output.push_str(&format_bullet_list(&step.actions));
159 output.push('\n');
160 }
161 }
162 }
163 }
164
165 for rule in raw {
167 if let UnifiedRule::Raw { content } = rule {
168 output.push_str(content);
169 output.push_str("\n\n");
170 }
171 }
172
173 Ok(output.trim().to_string())
174 }
175
176 fn editor(&self) -> Editor {
177 Editor::Cursor
178 }
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184 use crate::format::RuleCategory;
185
186 #[test]
187 fn test_emit_standards() {
188 let rules = vec![
189 UnifiedRule::Standard {
190 category: RuleCategory::Style,
191 priority: 0,
192 description: "Use snake_case for functions".to_string(),
193 pattern: None,
194 },
195 UnifiedRule::Standard {
196 category: RuleCategory::Style,
197 priority: 1,
198 description: "Use PascalCase for types".to_string(),
199 pattern: None,
200 },
201 ];
202
203 let emitter = CursorEmitter::new();
204 let output = emitter.emit_string(&rules).unwrap();
205
206 assert!(output.contains("Coding Standards"));
207 assert!(output.contains("snake_case"));
208 assert!(output.contains("PascalCase"));
209 }
210
211 #[test]
212 fn test_emit_persona() {
213 let rules = vec![UnifiedRule::Persona {
214 name: "Architect".to_string(),
215 role: "Senior system architect".to_string(),
216 identity: Some("Expert in distributed systems".to_string()),
217 style: Some("Direct and technical".to_string()),
218 traits: vec!["Analytical".to_string(), "Detail-oriented".to_string()],
219 principles: vec!["Simplicity first".to_string()],
220 }];
221
222 let emitter = CursorEmitter::new();
223 let output = emitter.emit_string(&rules).unwrap();
224
225 assert!(output.contains("AI Persona: Architect"));
226 assert!(output.contains("Senior system architect"));
227 assert!(output.contains("Analytical"));
228 }
229}