1use std::path::Path;
7
8use crate::config::Config;
9use crate::context::AppContext;
10use crate::error::AftError;
11use crate::format;
12use crate::parser::{detect_language, grammar_for, FileParser};
13
14pub fn line_col_to_byte(source: &str, line: u32, col: u32) -> usize {
22 let bytes = source.as_bytes();
23 let target_line = line as usize;
24 let mut current_line = 0usize;
25 let mut line_start = 0usize;
26
27 loop {
28 let mut line_end = line_start;
29 while line_end < bytes.len() && bytes[line_end] != b'\n' && bytes[line_end] != b'\r' {
30 line_end += 1;
31 }
32
33 if current_line == target_line {
34 return line_start + (col as usize).min(line_end.saturating_sub(line_start));
35 }
36
37 if line_end >= bytes.len() {
38 return source.len();
39 }
40
41 line_start = if bytes[line_end] == b'\r'
42 && line_end + 1 < bytes.len()
43 && bytes[line_end + 1] == b'\n'
44 {
45 line_end + 2
46 } else {
47 line_end + 1
48 };
49 current_line += 1;
50 }
51}
52
53pub fn replace_byte_range(
57 source: &str,
58 start: usize,
59 end: usize,
60 replacement: &str,
61) -> Result<String, AftError> {
62 if start > end {
63 return Err(AftError::InvalidRequest {
64 message: format!(
65 "invalid byte range [{}..{}): start must be <= end",
66 start, end
67 ),
68 });
69 }
70 if end > source.len() {
71 return Err(AftError::InvalidRequest {
72 message: format!(
73 "invalid byte range [{}..{}): end exceeds source length {}",
74 start,
75 end,
76 source.len()
77 ),
78 });
79 }
80 if !source.is_char_boundary(start) {
81 return Err(AftError::InvalidRequest {
82 message: format!(
83 "invalid byte range [{}..{}): start is not a char boundary",
84 start, end
85 ),
86 });
87 }
88 if !source.is_char_boundary(end) {
89 return Err(AftError::InvalidRequest {
90 message: format!(
91 "invalid byte range [{}..{}): end is not a char boundary",
92 start, end
93 ),
94 });
95 }
96
97 let mut result = String::with_capacity(
98 source.len().saturating_sub(end.saturating_sub(start)) + replacement.len(),
99 );
100 result.push_str(&source[..start]);
101 result.push_str(replacement);
102 result.push_str(&source[end..]);
103 Ok(result)
104}
105
106pub fn validate_syntax(path: &Path) -> Result<Option<bool>, AftError> {
111 let mut parser = FileParser::new();
112 match parser.parse(path) {
113 Ok((tree, _lang)) => Ok(Some(!tree.root_node().has_error())),
114 Err(AftError::InvalidRequest { .. }) => {
115 Ok(None)
117 }
118 Err(e) => Err(e),
119 }
120}
121
122pub fn validate_syntax_str(content: &str, path: &Path) -> Option<bool> {
128 let lang = detect_language(path)?;
129 let grammar = grammar_for(lang);
130 let mut parser = tree_sitter::Parser::new();
131 if parser.set_language(&grammar).is_err() {
132 return None;
133 }
134 let tree = parser.parse(content.as_bytes(), None)?;
135 Some(!tree.root_node().has_error())
136}
137
138pub struct DryRunResult {
140 pub diff: String,
142 pub syntax_valid: Option<bool>,
144}
145
146pub fn dry_run_diff(original: &str, proposed: &str, path: &Path) -> DryRunResult {
151 let display_path = path.display().to_string();
152 let text_diff = similar::TextDiff::from_lines(original, proposed);
153 let diff = text_diff
154 .unified_diff()
155 .context_radius(3)
156 .header(
157 &format!("a/{}", display_path),
158 &format!("b/{}", display_path),
159 )
160 .to_string();
161 let syntax_valid = validate_syntax_str(proposed, path);
162 DryRunResult { diff, syntax_valid }
163}
164
165pub fn is_dry_run(params: &serde_json::Value) -> bool {
169 params
170 .get("dry_run")
171 .and_then(|v| v.as_bool())
172 .unwrap_or(false)
173}
174
175pub fn wants_diff(params: &serde_json::Value) -> bool {
177 params
178 .get("include_diff")
179 .and_then(|v| v.as_bool())
180 .unwrap_or(false)
181}
182
183pub fn compute_diff_info(before: &str, after: &str) -> serde_json::Value {
187 use similar::ChangeTag;
188
189 let diff = similar::TextDiff::from_lines(before, after);
190 let mut additions = 0usize;
191 let mut deletions = 0usize;
192 for change in diff.iter_all_changes() {
193 match change.tag() {
194 ChangeTag::Insert => additions += 1,
195 ChangeTag::Delete => deletions += 1,
196 ChangeTag::Equal => {}
197 }
198 }
199
200 let size_limit = 512 * 1024; if before.len() > size_limit || after.len() > size_limit {
203 serde_json::json!({
204 "additions": additions,
205 "deletions": deletions,
206 "truncated": true,
207 })
208 } else {
209 serde_json::json!({
210 "before": before,
211 "after": after,
212 "additions": additions,
213 "deletions": deletions,
214 })
215 }
216}
217pub fn auto_backup(
229 ctx: &AppContext,
230 session: &str,
231 path: &Path,
232 description: &str,
233) -> Result<Option<String>, AftError> {
234 if !path.exists() {
235 return Ok(None);
236 }
237 let backup_id = {
238 let mut store = ctx.backup().borrow_mut();
239 store.snapshot(session, path, description)?
240 }; Ok(Some(backup_id))
242}
243
244pub struct WriteResult {
249 pub syntax_valid: Option<bool>,
251 pub formatted: bool,
253 pub format_skipped_reason: Option<String>,
256 pub validate_requested: bool,
258 pub validation_errors: Vec<format::ValidationError>,
260 pub validate_skipped_reason: Option<String>,
263 pub lsp_outcome: Option<crate::lsp::manager::PostEditWaitOutcome>,
272}
273
274impl WriteResult {
275 pub fn append_lsp_diagnostics_to(&self, result: &mut serde_json::Value) {
290 let Some(outcome) = self.lsp_outcome.as_ref() else {
291 return;
292 };
293
294 result["lsp_diagnostics"] = serde_json::json!(outcome
295 .diagnostics
296 .iter()
297 .map(|d| {
298 serde_json::json!({
299 "file": d.file.display().to_string(),
300 "line": d.line,
301 "column": d.column,
302 "end_line": d.end_line,
303 "end_column": d.end_column,
304 "severity": d.severity.as_str(),
305 "message": d.message,
306 "code": d.code,
307 "source": d.source,
308 })
309 })
310 .collect::<Vec<_>>());
311
312 result["lsp_complete"] = serde_json::Value::Bool(outcome.complete());
313
314 if !outcome.pending_servers.is_empty() {
315 result["lsp_pending_servers"] = serde_json::json!(outcome
316 .pending_servers
317 .iter()
318 .map(|key| key.kind.id_str().to_string())
319 .collect::<Vec<_>>());
320 }
321 if !outcome.exited_servers.is_empty() {
322 result["lsp_exited_servers"] = serde_json::json!(outcome
323 .exited_servers
324 .iter()
325 .map(|key| key.kind.id_str().to_string())
326 .collect::<Vec<_>>());
327 }
328 }
329}
330
331pub fn write_format_validate(
344 path: &Path,
345 content: &str,
346 config: &Config,
347 params: &serde_json::Value,
348) -> Result<WriteResult, AftError> {
349 std::fs::write(path, content).map_err(|e| AftError::InvalidRequest {
351 message: format!("failed to write file: {}", e),
352 })?;
353
354 let (formatted, format_skipped_reason) = format::auto_format(path, config);
356
357 let syntax_valid = match validate_syntax(path) {
359 Ok(sv) => sv,
360 Err(_) => None,
361 };
362
363 let param_validate = params.get("validate").and_then(|v| v.as_str());
365 let config_validate = config.validate_on_edit.as_deref();
366 let validate_mode = param_validate.or(config_validate).unwrap_or("off");
368 let validate_requested = validate_mode == "full";
369 let (validation_errors, validate_skipped_reason) = if validate_requested {
370 format::validate_full(path, config)
371 } else {
372 (Vec::new(), None)
373 };
374
375 Ok(WriteResult {
376 syntax_valid,
377 formatted,
378 format_skipped_reason,
379 validate_requested,
380 validation_errors,
381 validate_skipped_reason,
382 lsp_outcome: None,
383 })
384}
385
386#[cfg(test)]
387mod tests {
388 use super::*;
389
390 #[test]
393 fn line_col_to_byte_empty_string() {
394 assert_eq!(line_col_to_byte("", 0, 0), 0);
395 }
396
397 #[test]
398 fn line_col_to_byte_single_line() {
399 let source = "hello";
400 assert_eq!(line_col_to_byte(source, 0, 0), 0);
401 assert_eq!(line_col_to_byte(source, 0, 3), 3);
402 assert_eq!(line_col_to_byte(source, 0, 5), 5); }
404
405 #[test]
406 fn line_col_to_byte_multi_line() {
407 let source = "abc\ndef\nghi\n";
408 assert_eq!(line_col_to_byte(source, 0, 0), 0);
410 assert_eq!(line_col_to_byte(source, 0, 2), 2);
411 assert_eq!(line_col_to_byte(source, 1, 0), 4);
413 assert_eq!(line_col_to_byte(source, 1, 3), 7);
414 assert_eq!(line_col_to_byte(source, 2, 0), 8);
416 assert_eq!(line_col_to_byte(source, 2, 2), 10);
417 }
418
419 #[test]
420 fn line_col_to_byte_last_line_no_trailing_newline() {
421 let source = "abc\ndef";
422 assert_eq!(line_col_to_byte(source, 1, 0), 4);
424 assert_eq!(line_col_to_byte(source, 1, 3), 7); }
426
427 #[test]
428 fn line_col_to_byte_multi_byte_utf8() {
429 let source = "café\nbar";
431 assert_eq!(line_col_to_byte(source, 0, 0), 0);
433 assert_eq!(line_col_to_byte(source, 0, 5), 5); assert_eq!(line_col_to_byte(source, 1, 0), 6);
436 assert_eq!(line_col_to_byte(source, 1, 2), 8);
437 }
438
439 #[test]
440 fn line_col_to_byte_beyond_end() {
441 let source = "abc";
442 assert_eq!(line_col_to_byte(source, 5, 0), source.len());
444 }
445
446 #[test]
447 fn line_col_to_byte_col_clamped_to_line_length() {
448 let source = "ab\ncd";
449 assert_eq!(line_col_to_byte(source, 0, 10), 2);
451 }
452
453 #[test]
454 fn line_col_to_byte_crlf() {
455 let source = "abc\r\ndef\r\nghi\r\n";
456 assert_eq!(line_col_to_byte(source, 0, 0), 0);
457 assert_eq!(line_col_to_byte(source, 0, 10), 3);
458 assert_eq!(line_col_to_byte(source, 1, 0), 5);
459 assert_eq!(line_col_to_byte(source, 1, 3), 8);
460 assert_eq!(line_col_to_byte(source, 2, 0), 10);
461 }
462
463 #[test]
466 fn replace_byte_range_basic() {
467 let source = "hello world";
468 let result = replace_byte_range(source, 6, 11, "rust").unwrap();
469 assert_eq!(result, "hello rust");
470 }
471
472 #[test]
473 fn replace_byte_range_delete() {
474 let source = "hello world";
475 let result = replace_byte_range(source, 5, 11, "").unwrap();
476 assert_eq!(result, "hello");
477 }
478
479 #[test]
480 fn replace_byte_range_insert_at_same_position() {
481 let source = "helloworld";
482 let result = replace_byte_range(source, 5, 5, " ").unwrap();
483 assert_eq!(result, "hello world");
484 }
485
486 #[test]
487 fn replace_byte_range_replace_entire_string() {
488 let source = "old content";
489 let result = replace_byte_range(source, 0, source.len(), "new content").unwrap();
490 assert_eq!(result, "new content");
491 }
492}
493
494pub fn write_format_only(path: &Path, config: &Config) -> Result<bool, AftError> {
497 use crate::format::detect_formatter;
498 let lang = match crate::parser::detect_language(path) {
499 Some(l) => l,
500 None => return Ok(false),
501 };
502 let formatter = detect_formatter(path, lang, config);
503 if let Some((cmd, args)) = formatter {
504 let status = std::process::Command::new(&cmd)
505 .args(&args)
506 .arg(path)
507 .stdout(std::process::Stdio::null())
508 .stderr(std::process::Stdio::null())
509 .status();
510 match status {
511 Ok(s) if s.success() => Ok(true),
512 _ => Ok(false),
513 }
514 } else {
515 Ok(false)
516 }
517}