ricecoder_storage/industry/
cursor.rs1use 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
14pub struct CursorAdapter;
16
17impl CursorAdapter {
18 pub fn new() -> Self {
20 CursorAdapter
21 }
22
23 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 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 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 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 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 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 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}