Skip to main content

driven/emitter/
windsurf.rs

1//! Windsurf .windsurfrules 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 Windsurf .windsurfrules format
8#[derive(Debug, Default)]
9pub struct WindsurfEmitter;
10
11impl WindsurfEmitter {
12    /// Create a new Windsurf emitter
13    pub fn new() -> Self {
14        Self
15    }
16}
17
18impl RuleEmitter for WindsurfEmitter {
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        // Windsurf format is similar to Cursor
30        output.push_str(&format_heading(1, "Windsurf Rules"));
31
32        let mut standards: Vec<String> = Vec::new();
33        let mut context: Vec<String> = Vec::new();
34
35        for rule in rules {
36            match rule {
37                UnifiedRule::Persona {
38                    name, role, traits, ..
39                } => {
40                    output.push_str(&format_heading(2, &format!("Role: {}", name)));
41                    output.push_str(role);
42                    output.push_str("\n\n");
43
44                    if !traits.is_empty() {
45                        output.push_str(&format_bullet_list(traits));
46                        output.push('\n');
47                    }
48                }
49                UnifiedRule::Standard { description, .. } => {
50                    standards.push(description.clone());
51                }
52                UnifiedRule::Context {
53                    focus, includes, ..
54                } => {
55                    context.extend(focus.clone());
56                    context.extend(includes.clone());
57                }
58                UnifiedRule::Workflow { name, steps } => {
59                    output.push_str(&format_heading(2, name));
60                    for step in steps {
61                        output.push_str(&format!("- {}: {}\n", step.name, step.description));
62                    }
63                    output.push('\n');
64                }
65                UnifiedRule::Raw { content } => {
66                    output.push_str(content);
67                    output.push_str("\n\n");
68                }
69            }
70        }
71
72        if !context.is_empty() {
73            output.push_str(&format_heading(2, "Project Context"));
74            output.push_str(&format_bullet_list(&context));
75            output.push('\n');
76        }
77
78        if !standards.is_empty() {
79            output.push_str(&format_heading(2, "Coding Guidelines"));
80            output.push_str(&format_bullet_list(&standards));
81            output.push('\n');
82        }
83
84        Ok(output.trim().to_string())
85    }
86
87    fn editor(&self) -> Editor {
88        Editor::Windsurf
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95
96    #[test]
97    fn test_emit_windsurf() {
98        let rules = vec![UnifiedRule::Standard {
99            category: crate::format::RuleCategory::Style,
100            priority: 0,
101            description: "Use TypeScript strict mode".to_string(),
102            pattern: None,
103        }];
104
105        let emitter = WindsurfEmitter::new();
106        let output = emitter.emit_string(&rules).unwrap();
107
108        assert!(output.contains("Windsurf Rules"));
109        assert!(output.contains("TypeScript strict mode"));
110    }
111}