ricecoder_lsp/code_actions/
applier.rs1use super::{CodeActionsError, CodeActionsResult};
6use crate::types::{CodeAction, TextEdit, WorkspaceEdit};
7
8pub fn apply_code_action(code: &str, action: &CodeAction) -> CodeActionsResult<String> {
10 apply_workspace_edit(code, &action.edit)
11}
12
13pub fn apply_workspace_edit(code: &str, edit: &WorkspaceEdit) -> CodeActionsResult<String> {
15 if edit.changes.is_empty() {
19 return Ok(code.to_string());
20 }
21
22 if let Some((_, edits)) = edit.changes.iter().next() {
24 apply_text_edits(code, edits)
25 } else {
26 Ok(code.to_string())
27 }
28}
29
30pub fn apply_text_edits(code: &str, edits: &[TextEdit]) -> CodeActionsResult<String> {
32 let mut sorted_edits = edits.to_vec();
35 sorted_edits.sort_by(|a, b| {
36 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
52fn apply_single_text_edit(code: &str, edit: &TextEdit) -> CodeActionsResult<String> {
54 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 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 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; }
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; }
95
96 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 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
112pub 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 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 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 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}