driven/emitter/
copilot.rs1use 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 CopilotEmitter;
10
11impl CopilotEmitter {
12 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 output.push_str(&format_heading(1, "Copilot Instructions"));
31
32 let mut focus_areas: Vec<String> = Vec::new();
34 let mut all_standards: Vec<(String, String)> = Vec::new(); 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 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 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 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}