ricecoder_lsp/code_actions/
mod.rs

1//! Code Actions Module
2//!
3//! This module provides code action suggestions for fixing issues and refactoring code.
4//!
5//! # Architecture
6//!
7//! The code actions module is organized into:
8//! - `CodeActionsEngine`: Main trait for generating code actions
9//! - `applier`: Module for applying code actions to code
10//! - Language-specific action generators
11//!
12//! # Example
13//!
14//! ```ignore
15//! use ricecoder_lsp::code_actions::CodeActionsEngine;
16//! use ricecoder_lsp::types::{Diagnostic, Language};
17//!
18//! let engine = DefaultCodeActionsEngine::new();
19//! let actions = engine.suggest_code_actions(&diagnostic, code)?;
20//! ```
21
22pub 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/// Error type for code actions operations
34#[derive(Debug, Clone)]
35pub enum CodeActionsError {
36    /// Action generation failed
37    GenerationFailed(String),
38    /// Invalid diagnostic
39    InvalidDiagnostic(String),
40    /// Application failed
41    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
58/// Result type for code actions operations
59pub type CodeActionsResult<T> = Result<T, CodeActionsError>;
60
61/// Trait for generating code actions
62pub trait CodeActionsEngine: Send + Sync {
63    /// Suggest code actions for a diagnostic
64    fn suggest_code_actions(
65        &self,
66        diagnostic: &Diagnostic,
67        code: &str,
68    ) -> CodeActionsResult<Vec<CodeAction>>;
69
70    /// Apply a code action to code
71    fn apply_code_action(&self, code: &str, action: &CodeAction) -> CodeActionsResult<String>;
72}
73
74/// Default code actions engine implementation
75pub struct DefaultCodeActionsEngine;
76
77impl DefaultCodeActionsEngine {
78    /// Create a new code actions engine
79    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        // Generate actions based on diagnostic code
99        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
125/// Suggest removing an unused import
126fn 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    // Get the line to remove
133    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
154/// Suggest removing unreachable code
155fn 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    // Get the line to remove
162    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
183/// Suggest renaming to follow naming conventions
184fn suggest_rename_action(
185    diagnostic: &Diagnostic,
186    _code: &str,
187) -> CodeActionsResult<Vec<CodeAction>> {
188    // Extract the current name from the diagnostic message
189    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            // Suggest a corrected name based on the convention
194            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
227/// Suggest fixing syntax errors
228fn suggest_fix_syntax_action(
229    diagnostic: &Diagnostic,
230    _code: &str,
231) -> CodeActionsResult<Vec<CodeAction>> {
232    if diagnostic.message.contains("mismatched brackets") {
233        // This is a complex fix that would require parsing
234        // For now, we'll just suggest a generic fix
235        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
247/// Convert a name to snake_case
248fn 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
261/// Convert a name to PascalCase
262fn 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
280/// Convert a name to camelCase
281fn 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}