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
304pub fn format_validation_errors(errors: &[format::ValidationError]) -> String {
307 errors
308 .iter()
309 .map(|e| format!("line {}: {}", e.line, e.message))
310 .collect::<Vec<_>>()
311 .join("; ")
312}
313
314impl WriteResult {
315 pub fn append_lsp_diagnostics_to(&self, result: &mut serde_json::Value) {
330 result["rolled_back"] = serde_json::json!(self.rolled_back);
331
332 let Some(outcome) = self.lsp_outcome.as_ref() else {
333 return;
334 };
335
336 result["lsp_diagnostics"] = serde_json::json!(outcome
337 .diagnostics
338 .iter()
339 .map(|d| {
340 serde_json::json!({
341 "file": d.file.display().to_string(),
342 "line": d.line,
343 "column": d.column,
344 "end_line": d.end_line,
345 "end_column": d.end_column,
346 "severity": d.severity.as_str(),
347 "message": d.message,
348 "code": d.code,
349 "source": d.source,
350 })
351 })
352 .collect::<Vec<_>>());
353
354 result["lsp_complete"] = serde_json::Value::Bool(outcome.complete());
355
356 if !outcome.pending_servers.is_empty() {
357 result["lsp_pending_servers"] = serde_json::json!(outcome
358 .pending_servers
359 .iter()
360 .map(|key| key.kind.id_str().to_string())
361 .collect::<Vec<_>>());
362 }
363 if !outcome.exited_servers.is_empty() {
364 result["lsp_exited_servers"] = serde_json::json!(outcome
365 .exited_servers
366 .iter()
367 .map(|key| key.kind.id_str().to_string())
368 .collect::<Vec<_>>());
369 }
370 }
371}
372
373pub fn write_format_validate(
386 path: &Path,
387 content: &str,
388 config: &Config,
389 params: &serde_json::Value,
390) -> Result<WriteResult, AftError> {
391 let pre_write_content = if path.exists() {
392 std::fs::read_to_string(path).ok()
393 } else {
394 None
395 };
396 let was_syntax_valid = if pre_write_content.is_some() {
400 match validate_syntax(path) {
401 Ok(valid) => valid,
402 Err(_) => None,
403 }
404 } else {
405 None
406 };
407
408 std::fs::write(path, content).map_err(|e| AftError::InvalidRequest {
410 message: format!("failed to write file: {}", e),
411 })?;
412
413 let (formatted, format_skipped_reason) = format::auto_format(path, config);
415
416 let syntax_valid = match validate_syntax(path) {
418 Ok(sv) => sv,
419 Err(_) => None,
420 };
421 let rolled_back = if was_syntax_valid == Some(true) && syntax_valid == Some(false) {
422 if let Some(original) = pre_write_content.as_ref() {
423 std::fs::write(path, original).map_err(|e| AftError::InvalidRequest {
424 message: format!("failed to roll back invalid edit: {}", e),
425 })?;
426 true
427 } else {
428 false
429 }
430 } else {
431 false
432 };
433
434 let param_validate = params.get("validate").and_then(|v| v.as_str());
436 let config_validate = config.validate_on_edit.as_deref();
437 let validate_mode = param_validate.or(config_validate).unwrap_or("off");
439 let validate_requested = validate_mode == "full";
440 let (validation_errors, validate_skipped_reason) = if validate_requested {
441 format::validate_full(path, config)
442 } else {
443 (Vec::new(), None)
444 };
445
446 Ok(WriteResult {
447 syntax_valid,
448 formatted,
449 format_skipped_reason,
450 validate_requested,
451 validation_errors,
452 validate_skipped_reason,
453 rolled_back,
454 lsp_outcome: None,
455 })
456}
457
458#[cfg(test)]
459mod tests {
460 use super::*;
461
462 #[test]
465 fn line_col_to_byte_empty_string() {
466 assert_eq!(line_col_to_byte("", 0, 0), 0);
467 }
468
469 #[test]
470 fn line_col_to_byte_single_line() {
471 let source = "hello";
472 assert_eq!(line_col_to_byte(source, 0, 0), 0);
473 assert_eq!(line_col_to_byte(source, 0, 3), 3);
474 assert_eq!(line_col_to_byte(source, 0, 5), 5); }
476
477 #[test]
478 fn line_col_to_byte_multi_line() {
479 let source = "abc\ndef\nghi\n";
480 assert_eq!(line_col_to_byte(source, 0, 0), 0);
482 assert_eq!(line_col_to_byte(source, 0, 2), 2);
483 assert_eq!(line_col_to_byte(source, 1, 0), 4);
485 assert_eq!(line_col_to_byte(source, 1, 3), 7);
486 assert_eq!(line_col_to_byte(source, 2, 0), 8);
488 assert_eq!(line_col_to_byte(source, 2, 2), 10);
489 }
490
491 #[test]
492 fn line_col_to_byte_last_line_no_trailing_newline() {
493 let source = "abc\ndef";
494 assert_eq!(line_col_to_byte(source, 1, 0), 4);
496 assert_eq!(line_col_to_byte(source, 1, 3), 7); }
498
499 #[test]
500 fn line_col_to_byte_multi_byte_utf8() {
501 let source = "café\nbar";
503 assert_eq!(line_col_to_byte(source, 0, 0), 0);
505 assert_eq!(line_col_to_byte(source, 0, 5), 5); assert_eq!(line_col_to_byte(source, 1, 0), 6);
508 assert_eq!(line_col_to_byte(source, 1, 2), 8);
509 }
510
511 #[test]
512 fn line_col_to_byte_beyond_end() {
513 let source = "abc";
514 assert_eq!(line_col_to_byte(source, 5, 0), source.len());
516 }
517
518 #[test]
519 fn line_col_to_byte_col_clamped_to_line_length() {
520 let source = "ab\ncd";
521 assert_eq!(line_col_to_byte(source, 0, 10), 2);
523 }
524
525 #[test]
526 fn line_col_to_byte_crlf() {
527 let source = "abc\r\ndef\r\nghi\r\n";
528 assert_eq!(line_col_to_byte(source, 0, 0), 0);
529 assert_eq!(line_col_to_byte(source, 0, 10), 3);
530 assert_eq!(line_col_to_byte(source, 1, 0), 5);
531 assert_eq!(line_col_to_byte(source, 1, 3), 8);
532 assert_eq!(line_col_to_byte(source, 2, 0), 10);
533 }
534
535 #[test]
538 fn replace_byte_range_basic() {
539 let source = "hello world";
540 let result = replace_byte_range(source, 6, 11, "rust").unwrap();
541 assert_eq!(result, "hello rust");
542 }
543
544 #[test]
545 fn replace_byte_range_delete() {
546 let source = "hello world";
547 let result = replace_byte_range(source, 5, 11, "").unwrap();
548 assert_eq!(result, "hello");
549 }
550
551 #[test]
552 fn replace_byte_range_insert_at_same_position() {
553 let source = "helloworld";
554 let result = replace_byte_range(source, 5, 5, " ").unwrap();
555 assert_eq!(result, "hello world");
556 }
557
558 #[test]
559 fn replace_byte_range_replace_entire_string() {
560 let source = "old content";
561 let result = replace_byte_range(source, 0, source.len(), "new content").unwrap();
562 assert_eq!(result, "new content");
563 }
564}