1#![cfg_attr(test, allow(clippy::items_after_test_module))]
7
8use std::path::Path;
9
10use crate::config::Config;
11use crate::context::AppContext;
12use crate::error::AftError;
13use crate::format;
14use crate::parser::{detect_language, grammar_for, FileParser};
15
16pub fn line_col_to_byte(source: &str, line: u32, col: u32) -> usize {
24 let bytes = source.as_bytes();
25 let target_line = line as usize;
26 let mut current_line = 0usize;
27 let mut line_start = 0usize;
28
29 loop {
30 let mut line_end = line_start;
31 while line_end < bytes.len() && bytes[line_end] != b'\n' && bytes[line_end] != b'\r' {
32 line_end += 1;
33 }
34
35 if current_line == target_line {
36 return line_start + (col as usize).min(line_end.saturating_sub(line_start));
37 }
38
39 if line_end >= bytes.len() {
40 return source.len();
41 }
42
43 line_start = if bytes[line_end] == b'\r'
44 && line_end + 1 < bytes.len()
45 && bytes[line_end + 1] == b'\n'
46 {
47 line_end + 2
48 } else {
49 line_end + 1
50 };
51 current_line += 1;
52 }
53}
54
55pub fn replace_byte_range(
59 source: &str,
60 start: usize,
61 end: usize,
62 replacement: &str,
63) -> Result<String, AftError> {
64 if start > end {
65 return Err(AftError::InvalidRequest {
66 message: format!(
67 "invalid byte range [{}..{}): start must be <= end",
68 start, end
69 ),
70 });
71 }
72 if end > source.len() {
73 return Err(AftError::InvalidRequest {
74 message: format!(
75 "invalid byte range [{}..{}): end exceeds source length {}",
76 start,
77 end,
78 source.len()
79 ),
80 });
81 }
82 if !source.is_char_boundary(start) {
83 return Err(AftError::InvalidRequest {
84 message: format!(
85 "invalid byte range [{}..{}): start is not a char boundary",
86 start, end
87 ),
88 });
89 }
90 if !source.is_char_boundary(end) {
91 return Err(AftError::InvalidRequest {
92 message: format!(
93 "invalid byte range [{}..{}): end is not a char boundary",
94 start, end
95 ),
96 });
97 }
98
99 let mut result = String::with_capacity(
100 source.len().saturating_sub(end.saturating_sub(start)) + replacement.len(),
101 );
102 result.push_str(&source[..start]);
103 result.push_str(replacement);
104 result.push_str(&source[end..]);
105 Ok(result)
106}
107
108pub fn validate_syntax(path: &Path) -> Result<Option<bool>, AftError> {
113 let mut parser = FileParser::new();
114 match parser.parse(path) {
115 Ok((tree, _lang)) => Ok(Some(!tree.root_node().has_error())),
116 Err(AftError::InvalidRequest { .. }) => {
117 Ok(None)
119 }
120 Err(e) => Err(e),
121 }
122}
123
124pub fn validate_syntax_str(content: &str, path: &Path) -> Option<bool> {
130 let lang = detect_language(path)?;
131 let grammar = grammar_for(lang);
132 let mut parser = tree_sitter::Parser::new();
133 if parser.set_language(&grammar).is_err() {
134 return None;
135 }
136 let tree = parser.parse(content.as_bytes(), None)?;
137 Some(!tree.root_node().has_error())
138}
139
140pub fn wants_diff(params: &serde_json::Value) -> bool {
142 params
143 .get("include_diff")
144 .and_then(|v| v.as_bool())
145 .unwrap_or(false)
146}
147
148pub fn compute_diff_info(before: &str, after: &str) -> serde_json::Value {
152 use similar::ChangeTag;
153
154 let diff = similar::TextDiff::from_lines(before, after);
155 let mut additions = 0usize;
156 let mut deletions = 0usize;
157 for change in diff.iter_all_changes() {
158 match change.tag() {
159 ChangeTag::Insert => additions += 1,
160 ChangeTag::Delete => deletions += 1,
161 ChangeTag::Equal => {}
162 }
163 }
164
165 let size_limit = 512 * 1024; if before.len() > size_limit || after.len() > size_limit {
168 serde_json::json!({
169 "additions": additions,
170 "deletions": deletions,
171 "truncated": true,
172 })
173 } else {
174 serde_json::json!({
175 "before": before,
176 "after": after,
177 "additions": additions,
178 "deletions": deletions,
179 })
180 }
181}
182pub fn auto_backup(
194 ctx: &AppContext,
195 session: &str,
196 path: &Path,
197 description: &str,
198 op_id: Option<&str>,
199) -> Result<Option<String>, AftError> {
200 if !path.exists() {
201 return Ok(None);
202 }
203 let backup_id = {
204 let mut store = ctx.backup().borrow_mut();
205 store.snapshot_with_op(session, path, description, op_id)?
206 }; Ok(Some(backup_id))
208}
209
210pub struct WriteResult {
215 pub syntax_valid: Option<bool>,
217 pub formatted: bool,
219 pub format_skipped_reason: Option<String>,
223 pub validate_requested: bool,
225 pub validation_errors: Vec<format::ValidationError>,
227 pub validate_skipped_reason: Option<String>,
230 pub rolled_back: bool,
235 pub lsp_outcome: Option<crate::lsp::manager::PostEditWaitOutcome>,
244}
245
246impl WriteResult {
247 pub fn append_lsp_diagnostics_to(&self, result: &mut serde_json::Value) {
262 result["rolled_back"] = serde_json::json!(self.rolled_back);
263
264 let Some(outcome) = self.lsp_outcome.as_ref() else {
265 return;
266 };
267
268 result["lsp_diagnostics"] = serde_json::json!(outcome
269 .diagnostics
270 .iter()
271 .map(|d| {
272 serde_json::json!({
273 "file": d.file.display().to_string(),
274 "line": d.line,
275 "column": d.column,
276 "end_line": d.end_line,
277 "end_column": d.end_column,
278 "severity": d.severity.as_str(),
279 "message": d.message,
280 "code": d.code,
281 "source": d.source,
282 })
283 })
284 .collect::<Vec<_>>());
285
286 result["lsp_complete"] = serde_json::Value::Bool(outcome.complete());
287
288 if !outcome.pending_servers.is_empty() {
289 result["lsp_pending_servers"] = serde_json::json!(outcome
290 .pending_servers
291 .iter()
292 .map(|key| key.kind.id_str().to_string())
293 .collect::<Vec<_>>());
294 }
295 if !outcome.exited_servers.is_empty() {
296 result["lsp_exited_servers"] = serde_json::json!(outcome
297 .exited_servers
298 .iter()
299 .map(|key| key.kind.id_str().to_string())
300 .collect::<Vec<_>>());
301 }
302 }
303}
304
305pub fn write_format_validate(
318 path: &Path,
319 content: &str,
320 config: &Config,
321 params: &serde_json::Value,
322) -> Result<WriteResult, AftError> {
323 let pre_write_content = if path.exists() {
324 std::fs::read_to_string(path).ok()
325 } else {
326 None
327 };
328 let was_syntax_valid = if pre_write_content.is_some() {
332 match validate_syntax(path) {
333 Ok(valid) => valid,
334 Err(_) => None,
335 }
336 } else {
337 None
338 };
339
340 std::fs::write(path, content).map_err(|e| AftError::InvalidRequest {
342 message: format!("failed to write file: {}", e),
343 })?;
344
345 let (formatted, format_skipped_reason) = format::auto_format(path, config);
347
348 let syntax_valid = match validate_syntax(path) {
350 Ok(sv) => sv,
351 Err(_) => None,
352 };
353 let rolled_back = if was_syntax_valid == Some(true) && syntax_valid == Some(false) {
354 if let Some(original) = pre_write_content.as_ref() {
355 std::fs::write(path, original).map_err(|e| AftError::InvalidRequest {
356 message: format!("failed to roll back invalid edit: {}", e),
357 })?;
358 true
359 } else {
360 false
361 }
362 } else {
363 false
364 };
365
366 let param_validate = params.get("validate").and_then(|v| v.as_str());
368 let config_validate = config.validate_on_edit.as_deref();
369 let validate_mode = param_validate.or(config_validate).unwrap_or("off");
371 let validate_requested = validate_mode == "full";
372 let (validation_errors, validate_skipped_reason) = if validate_requested {
373 format::validate_full(path, config)
374 } else {
375 (Vec::new(), None)
376 };
377
378 Ok(WriteResult {
379 syntax_valid,
380 formatted,
381 format_skipped_reason,
382 validate_requested,
383 validation_errors,
384 validate_skipped_reason,
385 rolled_back,
386 lsp_outcome: None,
387 })
388}
389
390#[cfg(test)]
391mod tests {
392 use super::*;
393
394 #[test]
397 fn line_col_to_byte_empty_string() {
398 assert_eq!(line_col_to_byte("", 0, 0), 0);
399 }
400
401 #[test]
402 fn line_col_to_byte_single_line() {
403 let source = "hello";
404 assert_eq!(line_col_to_byte(source, 0, 0), 0);
405 assert_eq!(line_col_to_byte(source, 0, 3), 3);
406 assert_eq!(line_col_to_byte(source, 0, 5), 5); }
408
409 #[test]
410 fn line_col_to_byte_multi_line() {
411 let source = "abc\ndef\nghi\n";
412 assert_eq!(line_col_to_byte(source, 0, 0), 0);
414 assert_eq!(line_col_to_byte(source, 0, 2), 2);
415 assert_eq!(line_col_to_byte(source, 1, 0), 4);
417 assert_eq!(line_col_to_byte(source, 1, 3), 7);
418 assert_eq!(line_col_to_byte(source, 2, 0), 8);
420 assert_eq!(line_col_to_byte(source, 2, 2), 10);
421 }
422
423 #[test]
424 fn line_col_to_byte_last_line_no_trailing_newline() {
425 let source = "abc\ndef";
426 assert_eq!(line_col_to_byte(source, 1, 0), 4);
428 assert_eq!(line_col_to_byte(source, 1, 3), 7); }
430
431 #[test]
432 fn line_col_to_byte_multi_byte_utf8() {
433 let source = "café\nbar";
435 assert_eq!(line_col_to_byte(source, 0, 0), 0);
437 assert_eq!(line_col_to_byte(source, 0, 5), 5); assert_eq!(line_col_to_byte(source, 1, 0), 6);
440 assert_eq!(line_col_to_byte(source, 1, 2), 8);
441 }
442
443 #[test]
444 fn line_col_to_byte_beyond_end() {
445 let source = "abc";
446 assert_eq!(line_col_to_byte(source, 5, 0), source.len());
448 }
449
450 #[test]
451 fn line_col_to_byte_col_clamped_to_line_length() {
452 let source = "ab\ncd";
453 assert_eq!(line_col_to_byte(source, 0, 10), 2);
455 }
456
457 #[test]
458 fn line_col_to_byte_crlf() {
459 let source = "abc\r\ndef\r\nghi\r\n";
460 assert_eq!(line_col_to_byte(source, 0, 0), 0);
461 assert_eq!(line_col_to_byte(source, 0, 10), 3);
462 assert_eq!(line_col_to_byte(source, 1, 0), 5);
463 assert_eq!(line_col_to_byte(source, 1, 3), 8);
464 assert_eq!(line_col_to_byte(source, 2, 0), 10);
465 }
466
467 #[test]
470 fn replace_byte_range_basic() {
471 let source = "hello world";
472 let result = replace_byte_range(source, 6, 11, "rust").unwrap();
473 assert_eq!(result, "hello rust");
474 }
475
476 #[test]
477 fn replace_byte_range_delete() {
478 let source = "hello world";
479 let result = replace_byte_range(source, 5, 11, "").unwrap();
480 assert_eq!(result, "hello");
481 }
482
483 #[test]
484 fn replace_byte_range_insert_at_same_position() {
485 let source = "helloworld";
486 let result = replace_byte_range(source, 5, 5, " ").unwrap();
487 assert_eq!(result, "hello world");
488 }
489
490 #[test]
491 fn replace_byte_range_replace_entire_string() {
492 let source = "old content";
493 let result = replace_byte_range(source, 0, source.len(), "new content").unwrap();
494 assert_eq!(result, "new content");
495 }
496}