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(
224 ctx: &AppContext,
225 path: &Path,
226 description: &str,
227) -> Result<Option<String>, AftError> {
228 if !path.exists() {
229 return Ok(None);
230 }
231 let backup_id = {
232 let mut store = ctx.backup().borrow_mut();
233 store.snapshot(path, description)?
234 }; Ok(Some(backup_id))
236}
237
238pub struct WriteResult {
243 pub syntax_valid: Option<bool>,
245 pub formatted: bool,
247 pub format_skipped_reason: Option<String>,
249 pub validate_requested: bool,
251 pub validation_errors: Vec<format::ValidationError>,
253 pub validate_skipped_reason: Option<String>,
255 pub lsp_diagnostics: Vec<crate::lsp::diagnostics::StoredDiagnostic>,
258}
259
260impl WriteResult {
261 pub fn append_lsp_diagnostics_to(&self, result: &mut serde_json::Value) {
264 if !self.lsp_diagnostics.is_empty() {
265 result["lsp_diagnostics"] = serde_json::json!(self
266 .lsp_diagnostics
267 .iter()
268 .map(|d| {
269 serde_json::json!({
270 "file": d.file.display().to_string(),
271 "line": d.line,
272 "column": d.column,
273 "end_line": d.end_line,
274 "end_column": d.end_column,
275 "severity": d.severity.as_str(),
276 "message": d.message,
277 "code": d.code,
278 "source": d.source,
279 })
280 })
281 .collect::<Vec<_>>());
282 }
283 }
284}
285
286pub fn write_format_validate(
298 path: &Path,
299 content: &str,
300 config: &Config,
301 params: &serde_json::Value,
302) -> Result<WriteResult, AftError> {
303 std::fs::write(path, content).map_err(|e| AftError::InvalidRequest {
305 message: format!("failed to write file: {}", e),
306 })?;
307
308 let (formatted, format_skipped_reason) = format::auto_format(path, config);
310
311 let syntax_valid = match validate_syntax(path) {
313 Ok(sv) => sv,
314 Err(_) => None,
315 };
316
317 let validate_requested = params.get("validate").and_then(|v| v.as_str()) == Some("full");
319 let (validation_errors, validate_skipped_reason) = if validate_requested {
320 format::validate_full(path, config)
321 } else {
322 (Vec::new(), None)
323 };
324
325 Ok(WriteResult {
326 syntax_valid,
327 formatted,
328 format_skipped_reason,
329 validate_requested,
330 validation_errors,
331 validate_skipped_reason,
332 lsp_diagnostics: Vec::new(),
333 })
334}
335
336#[cfg(test)]
337mod tests {
338 use super::*;
339
340 #[test]
343 fn line_col_to_byte_empty_string() {
344 assert_eq!(line_col_to_byte("", 0, 0), 0);
345 }
346
347 #[test]
348 fn line_col_to_byte_single_line() {
349 let source = "hello";
350 assert_eq!(line_col_to_byte(source, 0, 0), 0);
351 assert_eq!(line_col_to_byte(source, 0, 3), 3);
352 assert_eq!(line_col_to_byte(source, 0, 5), 5); }
354
355 #[test]
356 fn line_col_to_byte_multi_line() {
357 let source = "abc\ndef\nghi\n";
358 assert_eq!(line_col_to_byte(source, 0, 0), 0);
360 assert_eq!(line_col_to_byte(source, 0, 2), 2);
361 assert_eq!(line_col_to_byte(source, 1, 0), 4);
363 assert_eq!(line_col_to_byte(source, 1, 3), 7);
364 assert_eq!(line_col_to_byte(source, 2, 0), 8);
366 assert_eq!(line_col_to_byte(source, 2, 2), 10);
367 }
368
369 #[test]
370 fn line_col_to_byte_last_line_no_trailing_newline() {
371 let source = "abc\ndef";
372 assert_eq!(line_col_to_byte(source, 1, 0), 4);
374 assert_eq!(line_col_to_byte(source, 1, 3), 7); }
376
377 #[test]
378 fn line_col_to_byte_multi_byte_utf8() {
379 let source = "café\nbar";
381 assert_eq!(line_col_to_byte(source, 0, 0), 0);
383 assert_eq!(line_col_to_byte(source, 0, 5), 5); assert_eq!(line_col_to_byte(source, 1, 0), 6);
386 assert_eq!(line_col_to_byte(source, 1, 2), 8);
387 }
388
389 #[test]
390 fn line_col_to_byte_beyond_end() {
391 let source = "abc";
392 assert_eq!(line_col_to_byte(source, 5, 0), source.len());
394 }
395
396 #[test]
397 fn line_col_to_byte_col_clamped_to_line_length() {
398 let source = "ab\ncd";
399 assert_eq!(line_col_to_byte(source, 0, 10), 2);
401 }
402
403 #[test]
404 fn line_col_to_byte_crlf() {
405 let source = "abc\r\ndef\r\nghi\r\n";
406 assert_eq!(line_col_to_byte(source, 0, 0), 0);
407 assert_eq!(line_col_to_byte(source, 0, 10), 3);
408 assert_eq!(line_col_to_byte(source, 1, 0), 5);
409 assert_eq!(line_col_to_byte(source, 1, 3), 8);
410 assert_eq!(line_col_to_byte(source, 2, 0), 10);
411 }
412
413 #[test]
416 fn replace_byte_range_basic() {
417 let source = "hello world";
418 let result = replace_byte_range(source, 6, 11, "rust").unwrap();
419 assert_eq!(result, "hello rust");
420 }
421
422 #[test]
423 fn replace_byte_range_delete() {
424 let source = "hello world";
425 let result = replace_byte_range(source, 5, 11, "").unwrap();
426 assert_eq!(result, "hello");
427 }
428
429 #[test]
430 fn replace_byte_range_insert_at_same_position() {
431 let source = "helloworld";
432 let result = replace_byte_range(source, 5, 5, " ").unwrap();
433 assert_eq!(result, "hello world");
434 }
435
436 #[test]
437 fn replace_byte_range_replace_entire_string() {
438 let source = "old content";
439 let result = replace_byte_range(source, 0, source.len(), "new content").unwrap();
440 assert_eq!(result, "new content");
441 }
442}
443
444pub fn write_format_only(path: &Path, config: &Config) -> Result<bool, AftError> {
447 use crate::format::detect_formatter;
448 let lang = match crate::parser::detect_language(path) {
449 Some(l) => l,
450 None => return Ok(false),
451 };
452 let formatter = detect_formatter(path, lang, config);
453 if let Some((cmd, args)) = formatter {
454 let status = std::process::Command::new(&cmd)
455 .args(&args)
456 .arg(path)
457 .stdout(std::process::Stdio::null())
458 .stderr(std::process::Stdio::null())
459 .status();
460 match status {
461 Ok(s) if s.success() => Ok(true),
462 _ => Ok(false),
463 }
464 } else {
465 Ok(false)
466 }
467}