ricecoder_lsp/code_actions/
applier.rs

1//! Code Action Applier
2//!
3//! This module provides functionality for applying code actions to code.
4
5use super::{CodeActionsError, CodeActionsResult};
6use crate::types::{CodeAction, TextEdit, WorkspaceEdit};
7
8/// Apply a code action to code
9pub fn apply_code_action(code: &str, action: &CodeAction) -> CodeActionsResult<String> {
10    apply_workspace_edit(code, &action.edit)
11}
12
13/// Apply a workspace edit to code
14pub fn apply_workspace_edit(code: &str, edit: &WorkspaceEdit) -> CodeActionsResult<String> {
15    // For now, we'll handle single-file edits
16    // In a real implementation, this would handle multi-file edits
17
18    if edit.changes.is_empty() {
19        return Ok(code.to_string());
20    }
21
22    // Get the first (and usually only) file's edits
23    if let Some((_, edits)) = edit.changes.iter().next() {
24        apply_text_edits(code, edits)
25    } else {
26        Ok(code.to_string())
27    }
28}
29
30/// Apply text edits to code
31pub fn apply_text_edits(code: &str, edits: &[TextEdit]) -> CodeActionsResult<String> {
32    // Sort edits in reverse order to apply from end to start
33    // This prevents range shifts when applying multiple edits
34    let mut sorted_edits = edits.to_vec();
35    sorted_edits.sort_by(|a, b| {
36        // Sort by line descending, then by character descending
37        match b.range.start.line.cmp(&a.range.start.line) {
38            std::cmp::Ordering::Equal => b.range.start.character.cmp(&a.range.start.character),
39            other => other,
40        }
41    });
42
43    let mut result = code.to_string();
44
45    for edit in sorted_edits {
46        result = apply_single_text_edit(&result, &edit)?;
47    }
48
49    Ok(result)
50}
51
52/// Apply a single text edit to code
53fn apply_single_text_edit(code: &str, edit: &TextEdit) -> CodeActionsResult<String> {
54    // Validate the range
55    let lines: Vec<&str> = code.lines().collect();
56
57    let start_line = edit.range.start.line as usize;
58    let start_char = edit.range.start.character as usize;
59    let end_line = edit.range.end.line as usize;
60    let end_char = edit.range.end.character as usize;
61
62    // Check bounds
63    if start_line >= lines.len() {
64        return Err(CodeActionsError::ApplicationFailed(format!(
65            "Start line {} out of bounds",
66            start_line
67        )));
68    }
69
70    if end_line >= lines.len() {
71        return Err(CodeActionsError::ApplicationFailed(format!(
72            "End line {} out of bounds",
73            end_line
74        )));
75    }
76
77    // Calculate byte positions
78    let mut byte_start = 0;
79    for (i, line) in lines.iter().enumerate() {
80        if i == start_line {
81            byte_start += start_char;
82            break;
83        }
84        byte_start += line.len() + 1; // +1 for newline
85    }
86
87    let mut byte_end = 0;
88    for (i, line) in lines.iter().enumerate() {
89        if i == end_line {
90            byte_end += end_char;
91            break;
92        }
93        byte_end += line.len() + 1; // +1 for newline
94    }
95
96    // Ensure byte positions are valid
97    if byte_start > code.len() || byte_end > code.len() || byte_start > byte_end {
98        return Err(CodeActionsError::ApplicationFailed(
99            "Invalid byte positions for edit".to_string(),
100        ));
101    }
102
103    // Apply the edit
104    let mut result = String::new();
105    result.push_str(&code[..byte_start]);
106    result.push_str(&edit.new_text);
107    result.push_str(&code[byte_end..]);
108
109    Ok(result)
110}
111
112/// Validate that an edit range is valid for the given code
113pub fn validate_edit_range(code: &str, edit: &TextEdit) -> CodeActionsResult<()> {
114    let lines: Vec<&str> = code.lines().collect();
115
116    let start_line = edit.range.start.line as usize;
117    let start_char = edit.range.start.character as usize;
118    let end_line = edit.range.end.line as usize;
119    let end_char = edit.range.end.character as usize;
120
121    // Check line bounds
122    if start_line >= lines.len() {
123        return Err(CodeActionsError::ApplicationFailed(format!(
124            "Start line {} out of bounds (total lines: {})",
125            start_line,
126            lines.len()
127        )));
128    }
129
130    if end_line >= lines.len() {
131        return Err(CodeActionsError::ApplicationFailed(format!(
132            "End line {} out of bounds (total lines: {})",
133            end_line,
134            lines.len()
135        )));
136    }
137
138    // Check character bounds
139    if start_char > lines[start_line].len() {
140        return Err(CodeActionsError::ApplicationFailed(format!(
141            "Start character {} out of bounds (line length: {})",
142            start_char,
143            lines[start_line].len()
144        )));
145    }
146
147    if end_char > lines[end_line].len() {
148        return Err(CodeActionsError::ApplicationFailed(format!(
149            "End character {} out of bounds (line length: {})",
150            end_char,
151            lines[end_line].len()
152        )));
153    }
154
155    // Check that start <= end
156    if start_line > end_line || (start_line == end_line && start_char > end_char) {
157        return Err(CodeActionsError::ApplicationFailed(
158            "Start position is after end position".to_string(),
159        ));
160    }
161
162    Ok(())
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168    use crate::types::{Position, Range};
169
170    #[test]
171    fn test_apply_single_text_edit_replace() {
172        let code = "hello world";
173        let edit = TextEdit {
174            range: Range::new(Position::new(0, 0), Position::new(0, 5)),
175            new_text: "goodbye".to_string(),
176        };
177
178        let result = apply_single_text_edit(code, &edit);
179        assert!(result.is_ok());
180        assert_eq!(result.unwrap(), "goodbye world");
181    }
182
183    #[test]
184    fn test_apply_single_text_edit_insert() {
185        let code = "hello world";
186        let edit = TextEdit {
187            range: Range::new(Position::new(0, 5), Position::new(0, 5)),
188            new_text: " there".to_string(),
189        };
190
191        let result = apply_single_text_edit(code, &edit);
192        assert!(result.is_ok());
193        assert_eq!(result.unwrap(), "hello there world");
194    }
195
196    #[test]
197    fn test_apply_single_text_edit_delete() {
198        let code = "hello world";
199        let edit = TextEdit {
200            range: Range::new(Position::new(0, 5), Position::new(0, 11)),
201            new_text: String::new(),
202        };
203
204        let result = apply_single_text_edit(code, &edit);
205        assert!(result.is_ok());
206        assert_eq!(result.unwrap(), "hello");
207    }
208
209    #[test]
210    fn test_validate_edit_range_valid() {
211        let code = "hello\nworld";
212        let edit = TextEdit {
213            range: Range::new(Position::new(0, 0), Position::new(0, 5)),
214            new_text: "goodbye".to_string(),
215        };
216
217        let result = validate_edit_range(code, &edit);
218        assert!(result.is_ok());
219    }
220
221    #[test]
222    fn test_validate_edit_range_out_of_bounds() {
223        let code = "hello\nworld";
224        let edit = TextEdit {
225            range: Range::new(Position::new(5, 0), Position::new(5, 5)),
226            new_text: "test".to_string(),
227        };
228
229        let result = validate_edit_range(code, &edit);
230        assert!(result.is_err());
231    }
232
233    #[test]
234    fn test_apply_text_edits_multiple() {
235        let code = "hello world";
236        let edits = vec![
237            TextEdit {
238                range: Range::new(Position::new(0, 0), Position::new(0, 5)),
239                new_text: "goodbye".to_string(),
240            },
241            TextEdit {
242                range: Range::new(Position::new(0, 6), Position::new(0, 11)),
243                new_text: "universe".to_string(),
244            },
245        ];
246
247        let result = apply_text_edits(code, &edits);
248        assert!(result.is_ok());
249        assert_eq!(result.unwrap(), "goodbye universe");
250    }
251}