ricecoder_lsp/code_actions/
mod.rs1pub mod adapters;
23pub mod applier;
24pub mod generic_engine;
25
26pub use adapters::{PythonCodeActionAdapter, RustCodeActionAdapter, TypeScriptCodeActionAdapter};
27pub use generic_engine::GenericCodeActionsEngine;
28
29use crate::types::{CodeAction, CodeActionKind, Diagnostic, TextEdit, WorkspaceEdit};
30use std::error::Error;
31use std::fmt;
32
33#[derive(Debug, Clone)]
35pub enum CodeActionsError {
36 GenerationFailed(String),
38 InvalidDiagnostic(String),
40 ApplicationFailed(String),
42}
43
44impl fmt::Display for CodeActionsError {
45 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46 match self {
47 CodeActionsError::GenerationFailed(msg) => {
48 write!(f, "Action generation failed: {}", msg)
49 }
50 CodeActionsError::InvalidDiagnostic(msg) => write!(f, "Invalid diagnostic: {}", msg),
51 CodeActionsError::ApplicationFailed(msg) => write!(f, "Application failed: {}", msg),
52 }
53 }
54}
55
56impl Error for CodeActionsError {}
57
58pub type CodeActionsResult<T> = Result<T, CodeActionsError>;
60
61pub trait CodeActionsEngine: Send + Sync {
63 fn suggest_code_actions(
65 &self,
66 diagnostic: &Diagnostic,
67 code: &str,
68 ) -> CodeActionsResult<Vec<CodeAction>>;
69
70 fn apply_code_action(&self, code: &str, action: &CodeAction) -> CodeActionsResult<String>;
72}
73
74pub struct DefaultCodeActionsEngine;
76
77impl DefaultCodeActionsEngine {
78 pub fn new() -> Self {
80 Self
81 }
82}
83
84impl Default for DefaultCodeActionsEngine {
85 fn default() -> Self {
86 Self::new()
87 }
88}
89
90impl CodeActionsEngine for DefaultCodeActionsEngine {
91 fn suggest_code_actions(
92 &self,
93 diagnostic: &Diagnostic,
94 code: &str,
95 ) -> CodeActionsResult<Vec<CodeAction>> {
96 let mut actions = Vec::new();
97
98 if let Some(code_str) = &diagnostic.code {
100 match code_str.as_str() {
101 "unused-import" => {
102 actions.extend(suggest_remove_import_action(diagnostic, code)?);
103 }
104 "unreachable-code" => {
105 actions.extend(suggest_remove_unreachable_action(diagnostic, code)?);
106 }
107 "naming-convention" => {
108 actions.extend(suggest_rename_action(diagnostic, code)?);
109 }
110 "syntax-error" => {
111 actions.extend(suggest_fix_syntax_action(diagnostic, code)?);
112 }
113 _ => {}
114 }
115 }
116
117 Ok(actions)
118 }
119
120 fn apply_code_action(&self, code: &str, action: &CodeAction) -> CodeActionsResult<String> {
121 applier::apply_code_action(code, action)
122 }
123}
124
125fn suggest_remove_import_action(
127 diagnostic: &Diagnostic,
128 code: &str,
129) -> CodeActionsResult<Vec<CodeAction>> {
130 let line_num = diagnostic.range.start.line as usize;
131
132 if let Some(_line) = code.lines().nth(line_num) {
134 let mut edit = WorkspaceEdit::new();
135 let text_edit = TextEdit {
136 range: diagnostic.range,
137 new_text: String::new(),
138 };
139
140 edit.add_edit("file://unknown".to_string(), text_edit);
141
142 let action = CodeAction::new(
143 "Remove unused import".to_string(),
144 CodeActionKind::QuickFix,
145 edit,
146 );
147
148 Ok(vec![action])
149 } else {
150 Ok(Vec::new())
151 }
152}
153
154fn suggest_remove_unreachable_action(
156 diagnostic: &Diagnostic,
157 code: &str,
158) -> CodeActionsResult<Vec<CodeAction>> {
159 let line_num = diagnostic.range.start.line as usize;
160
161 if let Some(_line) = code.lines().nth(line_num) {
163 let mut edit = WorkspaceEdit::new();
164 let text_edit = TextEdit {
165 range: diagnostic.range,
166 new_text: String::new(),
167 };
168
169 edit.add_edit("file://unknown".to_string(), text_edit);
170
171 let action = CodeAction::new(
172 "Remove unreachable code".to_string(),
173 CodeActionKind::QuickFix,
174 edit,
175 );
176
177 Ok(vec![action])
178 } else {
179 Ok(Vec::new())
180 }
181}
182
183fn suggest_rename_action(
185 diagnostic: &Diagnostic,
186 _code: &str,
187) -> CodeActionsResult<Vec<CodeAction>> {
188 if let Some(start) = diagnostic.message.find('`') {
190 if let Some(end) = diagnostic.message[start + 1..].find('`') {
191 let current_name = &diagnostic.message[start + 1..start + 1 + end];
192
193 let suggested_name = if diagnostic.message.contains("snake_case") {
195 to_snake_case(current_name)
196 } else if diagnostic.message.contains("PascalCase") {
197 to_pascal_case(current_name)
198 } else if diagnostic.message.contains("camelCase") {
199 to_camel_case(current_name)
200 } else {
201 return Ok(Vec::new());
202 };
203
204 if suggested_name != current_name {
205 let mut edit = WorkspaceEdit::new();
206 let text_edit = TextEdit {
207 range: diagnostic.range,
208 new_text: suggested_name.clone(),
209 };
210
211 edit.add_edit("file://unknown".to_string(), text_edit);
212
213 let action = CodeAction::new(
214 format!("Rename to `{}`", suggested_name),
215 CodeActionKind::QuickFix,
216 edit,
217 );
218
219 return Ok(vec![action]);
220 }
221 }
222 }
223
224 Ok(Vec::new())
225}
226
227fn suggest_fix_syntax_action(
229 diagnostic: &Diagnostic,
230 _code: &str,
231) -> CodeActionsResult<Vec<CodeAction>> {
232 if diagnostic.message.contains("mismatched brackets") {
233 let action = CodeAction::new(
236 "Check bracket matching".to_string(),
237 CodeActionKind::QuickFix,
238 WorkspaceEdit::new(),
239 );
240
241 Ok(vec![action])
242 } else {
243 Ok(Vec::new())
244 }
245}
246
247fn to_snake_case(name: &str) -> String {
249 let mut result = String::new();
250
251 for (i, ch) in name.chars().enumerate() {
252 if i > 0 && ch.is_uppercase() {
253 result.push('_');
254 }
255 result.push(ch.to_lowercase().next().unwrap_or(ch));
256 }
257
258 result
259}
260
261fn to_pascal_case(name: &str) -> String {
263 let mut result = String::new();
264 let mut capitalize_next = true;
265
266 for ch in name.chars() {
267 if ch == '_' {
268 capitalize_next = true;
269 } else if capitalize_next {
270 result.push(ch.to_uppercase().next().unwrap_or(ch));
271 capitalize_next = false;
272 } else {
273 result.push(ch);
274 }
275 }
276
277 result
278}
279
280fn to_camel_case(name: &str) -> String {
282 let mut result = String::new();
283 let mut capitalize_next = false;
284
285 for ch in name.chars() {
286 if ch == '_' {
287 capitalize_next = true;
288 } else if capitalize_next {
289 result.push(ch.to_uppercase().next().unwrap_or(ch));
290 capitalize_next = false;
291 } else {
292 result.push(ch.to_lowercase().next().unwrap_or(ch));
293 }
294 }
295
296 result
297}
298
299#[cfg(test)]
300mod tests {
301 use super::*;
302
303 #[test]
304 fn test_to_snake_case() {
305 assert_eq!(to_snake_case("MyFunction"), "my_function");
306 assert_eq!(to_snake_case("myFunction"), "my_function");
307 assert_eq!(to_snake_case("CONSTANT"), "c_o_n_s_t_a_n_t");
308 }
309
310 #[test]
311 fn test_to_pascal_case() {
312 assert_eq!(to_pascal_case("my_function"), "MyFunction");
313 assert_eq!(to_pascal_case("myFunction"), "MyFunction");
314 assert_eq!(to_pascal_case("my_const"), "MyConst");
315 }
316
317 #[test]
318 fn test_to_camel_case() {
319 assert_eq!(to_camel_case("my_function"), "myFunction");
320 assert_eq!(to_camel_case("MyFunction"), "myfunction");
321 assert_eq!(to_camel_case("my_const"), "myConst");
322 }
323
324 #[test]
325 fn test_code_actions_engine_creation() {
326 let engine = DefaultCodeActionsEngine::new();
327 let _ = engine;
328 }
329}