Skip to main content

driven/emitter/
copilot.rs

1//! Copilot copilot-instructions.md 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 GitHub Copilot copilot-instructions.md format
8#[derive(Debug, Default)]
9pub struct CopilotEmitter;
10
11impl CopilotEmitter {
12    /// Create a new Copilot emitter
13    pub fn new() -> Self {
14        Self
15    }
16}
17
18impl RuleEmitter for CopilotEmitter {
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        // Copilot format uses clear section headers
30        output.push_str(&format_heading(1, "Copilot Instructions"));
31
32        // Collect all context info
33        let mut focus_areas: Vec<String> = Vec::new();
34        let mut all_standards: Vec<(String, String)> = Vec::new(); // (category, description)
35        let mut raw_content: Vec<String> = Vec::new();
36
37        for rule in rules {
38            match rule {
39                UnifiedRule::Persona {
40                    role,
41                    traits,
42                    principles,
43                    ..
44                } => {
45                    output.push_str(&format_heading(2, "AI Role"));
46                    output.push_str(role);
47                    output.push_str("\n\n");
48
49                    if !traits.is_empty() {
50                        output.push_str(&format_bullet_list(traits));
51                        output.push('\n');
52                    }
53
54                    if !principles.is_empty() {
55                        output.push_str(&format_heading(3, "Core Principles"));
56                        output.push_str(&format_bullet_list(principles));
57                        output.push('\n');
58                    }
59                }
60                UnifiedRule::Context {
61                    focus, includes, ..
62                } => {
63                    focus_areas.extend(focus.clone());
64                    focus_areas.extend(includes.clone());
65                }
66                UnifiedRule::Standard {
67                    category,
68                    description,
69                    ..
70                } => {
71                    all_standards.push((format!("{:?}", category), description.clone()));
72                }
73                UnifiedRule::Workflow { name, steps } => {
74                    output.push_str(&format_heading(2, &format!("Workflow: {}", name)));
75                    for step in steps {
76                        output.push_str(&format!("1. **{}**: {}\n", step.name, step.description));
77                    }
78                    output.push('\n');
79                }
80                UnifiedRule::Raw { content } => {
81                    raw_content.push(content.clone());
82                }
83            }
84        }
85
86        // Emit focus areas
87        if !focus_areas.is_empty() {
88            output.push_str(&format_heading(2, "Project Context"));
89            output.push_str(&format_bullet_list(&focus_areas));
90            output.push('\n');
91        }
92
93        // Emit standards grouped by category
94        if !all_standards.is_empty() {
95            output.push_str(&format_heading(2, "Code Quality"));
96
97            use std::collections::HashMap;
98            let mut by_category: HashMap<String, Vec<String>> = HashMap::new();
99
100            for (category, description) in all_standards {
101                by_category.entry(category).or_default().push(description);
102            }
103
104            for (category, descriptions) in by_category {
105                output.push_str(&format_heading(3, &category));
106                output.push_str(&format_bullet_list(&descriptions));
107                output.push('\n');
108            }
109        }
110
111        // Emit raw content
112        for content in raw_content {
113            output.push_str(&content);
114            output.push_str("\n\n");
115        }
116
117        Ok(output.trim().to_string())
118    }
119
120    fn editor(&self) -> Editor {
121        Editor::Copilot
122    }
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128
129    #[test]
130    fn test_emit_basic() {
131        let rules = vec![UnifiedRule::Context {
132            includes: vec!["src/**".to_string()],
133            excludes: vec!["target/**".to_string()],
134            focus: vec!["Focus on performance".to_string()],
135        }];
136
137        let emitter = CopilotEmitter::new();
138        let output = emitter.emit_string(&rules).unwrap();
139
140        assert!(output.contains("Copilot Instructions"));
141        assert!(output.contains("Project Context"));
142        assert!(output.contains("Focus on performance"));
143    }
144}