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 {
147 params
148 .get("include_diff")
149 .and_then(|v| v.as_bool())
150 .unwrap_or(false)
151 || wants_diff_content(params)
152}
153
154pub fn wants_diff_content(params: &serde_json::Value) -> bool {
161 params
162 .get("include_diff_content")
163 .and_then(|v| v.as_bool())
164 .unwrap_or(false)
165}
166
167pub fn compute_diff_counts(before: &str, after: &str) -> serde_json::Value {
171 use similar::ChangeTag;
172
173 let diff = similar::TextDiff::from_lines(before, after);
174 let mut additions = 0usize;
175 let mut deletions = 0usize;
176 for change in diff.iter_all_changes() {
177 match change.tag() {
178 ChangeTag::Insert => additions += 1,
179 ChangeTag::Delete => deletions += 1,
180 ChangeTag::Equal => {}
181 }
182 }
183 serde_json::json!({
184 "additions": additions,
185 "deletions": deletions,
186 })
187}
188
189pub fn compute_diff_for_response(
195 params: &serde_json::Value,
196 before: &str,
197 after: &str,
198) -> serde_json::Value {
199 if wants_diff_content(params) {
200 compute_diff_info(before, after)
201 } else {
202 compute_diff_counts(before, after)
203 }
204}
205
206pub fn compute_diff_info(before: &str, after: &str) -> serde_json::Value {
210 use similar::ChangeTag;
211
212 let diff = similar::TextDiff::from_lines(before, after);
213 let mut additions = 0usize;
214 let mut deletions = 0usize;
215 for change in diff.iter_all_changes() {
216 match change.tag() {
217 ChangeTag::Insert => additions += 1,
218 ChangeTag::Delete => deletions += 1,
219 ChangeTag::Equal => {}
220 }
221 }
222
223 let size_limit = 512 * 1024; if before.len() > size_limit || after.len() > size_limit {
226 serde_json::json!({
227 "additions": additions,
228 "deletions": deletions,
229 "truncated": true,
230 })
231 } else {
232 serde_json::json!({
233 "before": before,
234 "after": after,
235 "additions": additions,
236 "deletions": deletions,
237 })
238 }
239}
240pub fn auto_backup(
252 ctx: &AppContext,
253 session: &str,
254 path: &Path,
255 description: &str,
256 op_id: Option<&str>,
257) -> Result<Option<String>, AftError> {
258 if std::fs::symlink_metadata(path).is_err() {
259 return Ok(None);
260 }
261 let backup_id = {
262 let mut store = ctx.backup().borrow_mut();
263 store.snapshot_with_op(session, path, description, op_id)?
264 }; Ok(Some(backup_id))
266}
267
268pub struct WriteResult {
273 pub syntax_valid: Option<bool>,
275 pub formatted: bool,
277 pub format_skipped_reason: Option<String>,
281 pub validate_requested: bool,
283 pub validation_errors: Vec<format::ValidationError>,
285 pub validate_skipped_reason: Option<String>,
288 pub rolled_back: bool,
293 pub lsp_outcome: Option<crate::lsp::manager::PostEditWaitOutcome>,
302}
303
304impl WriteResult {
305 pub fn append_lsp_diagnostics_to(&self, result: &mut serde_json::Value) {
320 result["rolled_back"] = serde_json::json!(self.rolled_back);
321
322 let Some(outcome) = self.lsp_outcome.as_ref() else {
323 return;
324 };
325
326 result["lsp_diagnostics"] = serde_json::json!(outcome
327 .diagnostics
328 .iter()
329 .map(|d| {
330 serde_json::json!({
331 "file": d.file.display().to_string(),
332 "line": d.line,
333 "column": d.column,
334 "end_line": d.end_line,
335 "end_column": d.end_column,
336 "severity": d.severity.as_str(),
337 "message": d.message,
338 "code": d.code,
339 "source": d.source,
340 })
341 })
342 .collect::<Vec<_>>());
343
344 result["lsp_complete"] = serde_json::Value::Bool(outcome.complete());
345
346 if !outcome.pending_servers.is_empty() {
347 result["lsp_pending_servers"] = serde_json::json!(outcome
348 .pending_servers
349 .iter()
350 .map(|key| key.kind.id_str().to_string())
351 .collect::<Vec<_>>());
352 }
353 if !outcome.exited_servers.is_empty() {
354 result["lsp_exited_servers"] = serde_json::json!(outcome
355 .exited_servers
356 .iter()
357 .map(|key| key.kind.id_str().to_string())
358 .collect::<Vec<_>>());
359 }
360 }
361}
362
363pub fn write_format_validate(
376 path: &Path,
377 content: &str,
378 config: &Config,
379 params: &serde_json::Value,
380) -> Result<WriteResult, AftError> {
381 let pre_write_content = if path.exists() {
382 std::fs::read_to_string(path).ok()
383 } else {
384 None
385 };
386 let was_syntax_valid = if pre_write_content.is_some() {
390 match validate_syntax(path) {
391 Ok(valid) => valid,
392 Err(_) => None,
393 }
394 } else {
395 None
396 };
397
398 std::fs::write(path, content).map_err(|e| AftError::InvalidRequest {
400 message: format!("failed to write file: {}", e),
401 })?;
402
403 let (formatted, format_skipped_reason) = format::auto_format(path, config);
405
406 let syntax_valid = match validate_syntax(path) {
408 Ok(sv) => sv,
409 Err(_) => None,
410 };
411 let rolled_back = if was_syntax_valid == Some(true) && syntax_valid == Some(false) {
412 if let Some(original) = pre_write_content.as_ref() {
413 std::fs::write(path, original).map_err(|e| AftError::InvalidRequest {
414 message: format!("failed to roll back invalid edit: {}", e),
415 })?;
416 true
417 } else {
418 false
419 }
420 } else {
421 false
422 };
423
424 let param_validate = params.get("validate").and_then(|v| v.as_str());
426 let config_validate = config.validate_on_edit.as_deref();
427 let validate_mode = param_validate.or(config_validate).unwrap_or("off");
429 let validate_requested = validate_mode == "full";
430 let (validation_errors, validate_skipped_reason) = if validate_requested {
431 format::validate_full(path, config)
432 } else {
433 (Vec::new(), None)
434 };
435
436 Ok(WriteResult {
437 syntax_valid,
438 formatted,
439 format_skipped_reason,
440 validate_requested,
441 validation_errors,
442 validate_skipped_reason,
443 rolled_back,
444 lsp_outcome: None,
445 })
446}
447
448#[cfg(test)]
449mod tests {
450 use super::*;
451
452 #[test]
455 fn line_col_to_byte_empty_string() {
456 assert_eq!(line_col_to_byte("", 0, 0), 0);
457 }
458
459 #[test]
460 fn line_col_to_byte_single_line() {
461 let source = "hello";
462 assert_eq!(line_col_to_byte(source, 0, 0), 0);
463 assert_eq!(line_col_to_byte(source, 0, 3), 3);
464 assert_eq!(line_col_to_byte(source, 0, 5), 5); }
466
467 #[test]
468 fn line_col_to_byte_multi_line() {
469 let source = "abc\ndef\nghi\n";
470 assert_eq!(line_col_to_byte(source, 0, 0), 0);
472 assert_eq!(line_col_to_byte(source, 0, 2), 2);
473 assert_eq!(line_col_to_byte(source, 1, 0), 4);
475 assert_eq!(line_col_to_byte(source, 1, 3), 7);
476 assert_eq!(line_col_to_byte(source, 2, 0), 8);
478 assert_eq!(line_col_to_byte(source, 2, 2), 10);
479 }
480
481 #[test]
482 fn line_col_to_byte_last_line_no_trailing_newline() {
483 let source = "abc\ndef";
484 assert_eq!(line_col_to_byte(source, 1, 0), 4);
486 assert_eq!(line_col_to_byte(source, 1, 3), 7); }
488
489 #[test]
490 fn line_col_to_byte_multi_byte_utf8() {
491 let source = "café\nbar";
493 assert_eq!(line_col_to_byte(source, 0, 0), 0);
495 assert_eq!(line_col_to_byte(source, 0, 5), 5); assert_eq!(line_col_to_byte(source, 1, 0), 6);
498 assert_eq!(line_col_to_byte(source, 1, 2), 8);
499 }
500
501 #[test]
502 fn line_col_to_byte_beyond_end() {
503 let source = "abc";
504 assert_eq!(line_col_to_byte(source, 5, 0), source.len());
506 }
507
508 #[test]
509 fn line_col_to_byte_col_clamped_to_line_length() {
510 let source = "ab\ncd";
511 assert_eq!(line_col_to_byte(source, 0, 10), 2);
513 }
514
515 #[test]
516 fn line_col_to_byte_crlf() {
517 let source = "abc\r\ndef\r\nghi\r\n";
518 assert_eq!(line_col_to_byte(source, 0, 0), 0);
519 assert_eq!(line_col_to_byte(source, 0, 10), 3);
520 assert_eq!(line_col_to_byte(source, 1, 0), 5);
521 assert_eq!(line_col_to_byte(source, 1, 3), 8);
522 assert_eq!(line_col_to_byte(source, 2, 0), 10);
523 }
524
525 #[test]
528 fn replace_byte_range_basic() {
529 let source = "hello world";
530 let result = replace_byte_range(source, 6, 11, "rust").unwrap();
531 assert_eq!(result, "hello rust");
532 }
533
534 #[test]
535 fn replace_byte_range_delete() {
536 let source = "hello world";
537 let result = replace_byte_range(source, 5, 11, "").unwrap();
538 assert_eq!(result, "hello");
539 }
540
541 #[test]
542 fn replace_byte_range_insert_at_same_position() {
543 let source = "helloworld";
544 let result = replace_byte_range(source, 5, 5, " ").unwrap();
545 assert_eq!(result, "hello world");
546 }
547
548 #[test]
549 fn replace_byte_range_replace_entire_string() {
550 let source = "old content";
551 let result = replace_byte_range(source, 0, source.len(), "new content").unwrap();
552 assert_eq!(result, "new content");
553 }
554}