Skip to main content

driven/emitter/
cursor.rs

1//! Cursor .cursorrules emitter
2
3use super::{RuleEmitter, ensure_parent_dir, format_bullet_list, format_heading};
4use crate::{Editor, Result, parser::UnifiedRule};
5use std::path::Path;
6
7/// Emitter for Cursor .cursorrules format
8#[derive(Debug, Default)]
9pub struct CursorEmitter;
10
11impl CursorEmitter {
12    /// Create a new Cursor emitter
13    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        // Group rules by type
30        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        // Emit personas
47        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        // Emit context
84        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        // Emit standards
115        if !standards.is_empty() {
116            output.push_str(&format_heading(1, "Coding Standards"));
117
118            // Group by category
119            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        // Emit workflows
143        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        // Emit raw content
166        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}