ricecoder_storage/industry/
cursor.rs

1//! Cursor IDE configuration adapter
2//!
3//! Reads and converts Cursor IDE configuration files (.cursorrules and .cursor/ directory)
4//! into RiceCoder's internal configuration format.
5
6use crate::config::{Config, SteeringRule};
7use crate::error::StorageResult;
8use crate::types::DocumentFormat;
9use std::path::Path;
10use tracing::{debug, warn};
11
12use super::adapter::IndustryFileAdapter;
13
14/// Cursor IDE adapter
15pub struct CursorAdapter;
16
17impl CursorAdapter {
18    /// Create a new Cursor adapter
19    pub fn new() -> Self {
20        CursorAdapter
21    }
22
23    /// Read .cursorrules file
24    fn read_cursorrules(&self, project_root: &Path) -> StorageResult<Option<String>> {
25        let cursorrules_path = project_root.join(".cursorrules");
26
27        if !cursorrules_path.exists() {
28            debug!("No .cursorrules file found at {:?}", cursorrules_path);
29            return Ok(None);
30        }
31
32        debug!("Reading .cursorrules from {:?}", cursorrules_path);
33        let content = std::fs::read_to_string(&cursorrules_path).map_err(|e| {
34            crate::error::StorageError::io_error(
35                cursorrules_path.clone(),
36                crate::error::IoOperation::Read,
37                e,
38            )
39        })?;
40
41        Ok(Some(content))
42    }
43
44    /// Read .cursor/ directory settings
45    fn read_cursor_settings(&self, project_root: &Path) -> StorageResult<Option<String>> {
46        let cursor_dir = project_root.join(".cursor");
47
48        if !cursor_dir.exists() {
49            debug!("No .cursor directory found at {:?}", cursor_dir);
50            return Ok(None);
51        }
52
53        debug!("Reading .cursor settings from {:?}", cursor_dir);
54
55        // Try to read settings.json or other config files in .cursor/
56        let settings_path = cursor_dir.join("settings.json");
57        if settings_path.exists() {
58            let content = std::fs::read_to_string(&settings_path).map_err(|e| {
59                crate::error::StorageError::io_error(
60                    settings_path.clone(),
61                    crate::error::IoOperation::Read,
62                    e,
63                )
64            })?;
65            return Ok(Some(content));
66        }
67
68        // If no settings.json, try to read all files in the directory
69        let mut combined_content = String::new();
70        if let Ok(entries) = std::fs::read_dir(&cursor_dir) {
71            for entry in entries.flatten() {
72                if let Ok(metadata) = entry.metadata() {
73                    if metadata.is_file() {
74                        if let Ok(content) = std::fs::read_to_string(entry.path()) {
75                            combined_content.push_str(&format!(
76                                "# {}\n{}\n\n",
77                                entry.path().display(),
78                                content
79                            ));
80                        }
81                    }
82                }
83            }
84        }
85
86        if combined_content.is_empty() {
87            Ok(None)
88        } else {
89            Ok(Some(combined_content))
90        }
91    }
92}
93
94impl Default for CursorAdapter {
95    fn default() -> Self {
96        Self::new()
97    }
98}
99
100impl IndustryFileAdapter for CursorAdapter {
101    fn name(&self) -> &'static str {
102        "cursor"
103    }
104
105    fn can_handle(&self, project_root: &Path) -> bool {
106        let cursorrules_exists = project_root.join(".cursorrules").exists();
107        let cursor_dir_exists = project_root.join(".cursor").exists();
108
109        cursorrules_exists || cursor_dir_exists
110    }
111
112    fn read_config(&self, project_root: &Path) -> StorageResult<Config> {
113        let mut config = Config::default();
114
115        // Read .cursorrules
116        if let Ok(Some(cursorrules_content)) = self.read_cursorrules(project_root) {
117            debug!("Adding Cursor rules as steering rule");
118            config.steering.push(SteeringRule {
119                name: "cursor-rules".to_string(),
120                content: cursorrules_content,
121                format: DocumentFormat::Markdown,
122            });
123        }
124
125        // Read .cursor/ settings
126        if let Ok(Some(cursor_settings)) = self.read_cursor_settings(project_root) {
127            debug!("Adding Cursor settings as steering rule");
128            config.steering.push(SteeringRule {
129                name: "cursor-settings".to_string(),
130                content: cursor_settings,
131                format: DocumentFormat::Markdown,
132            });
133        }
134
135        if config.steering.is_empty() {
136            warn!("Cursor adapter found files but no content was read");
137        }
138
139        Ok(config)
140    }
141
142    fn priority(&self) -> u32 {
143        // Cursor has medium priority (after project-specific, before generic)
144        50
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151    use std::fs;
152    use tempfile::TempDir;
153
154    #[test]
155    fn test_cursor_adapter_detects_cursorrules() {
156        let temp_dir = TempDir::new().unwrap();
157        let cursorrules_path = temp_dir.path().join(".cursorrules");
158        fs::write(&cursorrules_path, "# Cursor rules").unwrap();
159
160        let adapter = CursorAdapter::new();
161        assert!(adapter.can_handle(temp_dir.path()));
162    }
163
164    #[test]
165    fn test_cursor_adapter_detects_cursor_dir() {
166        let temp_dir = TempDir::new().unwrap();
167        let cursor_dir = temp_dir.path().join(".cursor");
168        fs::create_dir(&cursor_dir).unwrap();
169
170        let adapter = CursorAdapter::new();
171        assert!(adapter.can_handle(temp_dir.path()));
172    }
173
174    #[test]
175    fn test_cursor_adapter_no_files() {
176        let temp_dir = TempDir::new().unwrap();
177
178        let adapter = CursorAdapter::new();
179        assert!(!adapter.can_handle(temp_dir.path()));
180    }
181
182    #[test]
183    fn test_cursor_adapter_reads_cursorrules() {
184        let temp_dir = TempDir::new().unwrap();
185        let cursorrules_path = temp_dir.path().join(".cursorrules");
186        let rules_content = "# Cursor rules\nBe helpful";
187        fs::write(&cursorrules_path, rules_content).unwrap();
188
189        let adapter = CursorAdapter::new();
190        let config = adapter.read_config(temp_dir.path()).unwrap();
191
192        assert_eq!(config.steering.len(), 1);
193        assert_eq!(config.steering[0].name, "cursor-rules");
194        assert_eq!(config.steering[0].content, rules_content);
195        assert_eq!(config.steering[0].format, DocumentFormat::Markdown);
196    }
197
198    #[test]
199    fn test_cursor_adapter_reads_cursor_settings() {
200        let temp_dir = TempDir::new().unwrap();
201        let cursor_dir = temp_dir.path().join(".cursor");
202        fs::create_dir(&cursor_dir).unwrap();
203        let settings_path = cursor_dir.join("settings.json");
204        let settings_content = r#"{"key": "value"}"#;
205        fs::write(&settings_path, settings_content).unwrap();
206
207        let adapter = CursorAdapter::new();
208        let config = adapter.read_config(temp_dir.path()).unwrap();
209
210        assert_eq!(config.steering.len(), 1);
211        assert_eq!(config.steering[0].name, "cursor-settings");
212        assert_eq!(config.steering[0].content, settings_content);
213    }
214
215    #[test]
216    fn test_cursor_adapter_reads_both() {
217        let temp_dir = TempDir::new().unwrap();
218        let cursorrules_path = temp_dir.path().join(".cursorrules");
219        fs::write(&cursorrules_path, "# Rules").unwrap();
220
221        let cursor_dir = temp_dir.path().join(".cursor");
222        fs::create_dir(&cursor_dir).unwrap();
223        let settings_path = cursor_dir.join("settings.json");
224        fs::write(&settings_path, r#"{"key": "value"}"#).unwrap();
225
226        let adapter = CursorAdapter::new();
227        let config = adapter.read_config(temp_dir.path()).unwrap();
228
229        assert_eq!(config.steering.len(), 2);
230        assert_eq!(config.steering[0].name, "cursor-rules");
231        assert_eq!(config.steering[1].name, "cursor-settings");
232    }
233
234    #[test]
235    fn test_cursor_adapter_priority() {
236        let adapter = CursorAdapter::new();
237        assert_eq!(adapter.priority(), 50);
238    }
239
240    #[test]
241    fn test_cursor_adapter_name() {
242        let adapter = CursorAdapter::new();
243        assert_eq!(adapter.name(), "cursor");
244    }
245}