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 {
21 let mut byte = 0;
22 for (i, l) in source.lines().enumerate() {
23 if i == line as usize {
24 return byte + (col as usize).min(l.len());
25 }
26 byte += l.len() + 1; }
28 source.len()
30}
31
32pub fn replace_byte_range(
36 source: &str,
37 start: usize,
38 end: usize,
39 replacement: &str,
40) -> Result<String, AftError> {
41 if start > end {
42 return Err(AftError::InvalidRequest {
43 message: format!(
44 "invalid byte range [{}..{}): start must be <= end",
45 start, end
46 ),
47 });
48 }
49 if end > source.len() {
50 return Err(AftError::InvalidRequest {
51 message: format!(
52 "invalid byte range [{}..{}): end exceeds source length {}",
53 start,
54 end,
55 source.len()
56 ),
57 });
58 }
59 if !source.is_char_boundary(start) {
60 return Err(AftError::InvalidRequest {
61 message: format!(
62 "invalid byte range [{}..{}): start is not a char boundary",
63 start, end
64 ),
65 });
66 }
67 if !source.is_char_boundary(end) {
68 return Err(AftError::InvalidRequest {
69 message: format!(
70 "invalid byte range [{}..{}): end is not a char boundary",
71 start, end
72 ),
73 });
74 }
75
76 let mut result = String::with_capacity(
77 source.len().saturating_sub(end.saturating_sub(start)) + replacement.len(),
78 );
79 result.push_str(&source[..start]);
80 result.push_str(replacement);
81 result.push_str(&source[end..]);
82 Ok(result)
83}
84
85pub fn validate_syntax(path: &Path) -> Result<Option<bool>, AftError> {
90 let mut parser = FileParser::new();
91 match parser.parse(path) {
92 Ok((tree, _lang)) => Ok(Some(!tree.root_node().has_error())),
93 Err(AftError::InvalidRequest { .. }) => {
94 Ok(None)
96 }
97 Err(e) => Err(e),
98 }
99}
100
101pub fn validate_syntax_str(content: &str, path: &Path) -> Option<bool> {
107 let lang = detect_language(path)?;
108 let grammar = grammar_for(lang);
109 let mut parser = tree_sitter::Parser::new();
110 if parser.set_language(&grammar).is_err() {
111 return None;
112 }
113 let tree = parser.parse(content.as_bytes(), None)?;
114 Some(!tree.root_node().has_error())
115}
116
117pub struct DryRunResult {
119 pub diff: String,
121 pub syntax_valid: Option<bool>,
123}
124
125pub fn dry_run_diff(original: &str, proposed: &str, path: &Path) -> DryRunResult {
130 let display_path = path.display().to_string();
131 let text_diff = similar::TextDiff::from_lines(original, proposed);
132 let diff = text_diff
133 .unified_diff()
134 .context_radius(3)
135 .header(
136 &format!("a/{}", display_path),
137 &format!("b/{}", display_path),
138 )
139 .to_string();
140 let syntax_valid = validate_syntax_str(proposed, path);
141 DryRunResult { diff, syntax_valid }
142}
143
144pub fn is_dry_run(params: &serde_json::Value) -> bool {
148 params
149 .get("dry_run")
150 .and_then(|v| v.as_bool())
151 .unwrap_or(false)
152}
153
154pub fn wants_diff(params: &serde_json::Value) -> bool {
156 params
157 .get("include_diff")
158 .and_then(|v| v.as_bool())
159 .unwrap_or(false)
160}
161
162pub fn compute_diff_info(before: &str, after: &str) -> serde_json::Value {
166 use similar::ChangeTag;
167
168 let diff = similar::TextDiff::from_lines(before, after);
169 let mut additions = 0usize;
170 let mut deletions = 0usize;
171 for change in diff.iter_all_changes() {
172 match change.tag() {
173 ChangeTag::Insert => additions += 1,
174 ChangeTag::Delete => deletions += 1,
175 ChangeTag::Equal => {}
176 }
177 }
178
179 let size_limit = 512 * 1024; if before.len() > size_limit || after.len() > size_limit {
182 serde_json::json!({
183 "additions": additions,
184 "deletions": deletions,
185 "truncated": true,
186 })
187 } else {
188 serde_json::json!({
189 "before": before,
190 "after": after,
191 "additions": additions,
192 "deletions": deletions,
193 })
194 }
195}
196pub fn auto_backup(
203 ctx: &AppContext,
204 path: &Path,
205 description: &str,
206) -> Result<Option<String>, AftError> {
207 if !path.exists() {
208 return Ok(None);
209 }
210 let backup_id = {
211 let mut store = ctx.backup().borrow_mut();
212 store.snapshot(path, description)?
213 }; Ok(Some(backup_id))
215}
216
217pub struct WriteResult {
222 pub syntax_valid: Option<bool>,
224 pub formatted: bool,
226 pub format_skipped_reason: Option<String>,
228 pub validate_requested: bool,
230 pub validation_errors: Vec<format::ValidationError>,
232 pub validate_skipped_reason: Option<String>,
234 pub lsp_diagnostics: Vec<crate::lsp::diagnostics::StoredDiagnostic>,
237}
238
239impl WriteResult {
240 pub fn append_lsp_diagnostics_to(&self, result: &mut serde_json::Value) {
243 if !self.lsp_diagnostics.is_empty() {
244 result["lsp_diagnostics"] = serde_json::json!(self
245 .lsp_diagnostics
246 .iter()
247 .map(|d| {
248 serde_json::json!({
249 "file": d.file.display().to_string(),
250 "line": d.line,
251 "column": d.column,
252 "end_line": d.end_line,
253 "end_column": d.end_column,
254 "severity": d.severity.as_str(),
255 "message": d.message,
256 "code": d.code,
257 "source": d.source,
258 })
259 })
260 .collect::<Vec<_>>());
261 }
262 }
263}
264
265pub fn write_format_validate(
277 path: &Path,
278 content: &str,
279 config: &Config,
280 params: &serde_json::Value,
281) -> Result<WriteResult, AftError> {
282 std::fs::write(path, content).map_err(|e| AftError::InvalidRequest {
284 message: format!("failed to write file: {}", e),
285 })?;
286
287 let (formatted, format_skipped_reason) = format::auto_format(path, config);
289
290 let syntax_valid = match validate_syntax(path) {
292 Ok(sv) => sv,
293 Err(_) => None,
294 };
295
296 let validate_requested = params.get("validate").and_then(|v| v.as_str()) == Some("full");
298 let (validation_errors, validate_skipped_reason) = if validate_requested {
299 format::validate_full(path, config)
300 } else {
301 (Vec::new(), None)
302 };
303
304 Ok(WriteResult {
305 syntax_valid,
306 formatted,
307 format_skipped_reason,
308 validate_requested,
309 validation_errors,
310 validate_skipped_reason,
311 lsp_diagnostics: Vec::new(),
312 })
313}
314
315#[cfg(test)]
316mod tests {
317 use super::*;
318
319 #[test]
322 fn line_col_to_byte_empty_string() {
323 assert_eq!(line_col_to_byte("", 0, 0), 0);
324 }
325
326 #[test]
327 fn line_col_to_byte_single_line() {
328 let source = "hello";
329 assert_eq!(line_col_to_byte(source, 0, 0), 0);
330 assert_eq!(line_col_to_byte(source, 0, 3), 3);
331 assert_eq!(line_col_to_byte(source, 0, 5), 5); }
333
334 #[test]
335 fn line_col_to_byte_multi_line() {
336 let source = "abc\ndef\nghi\n";
337 assert_eq!(line_col_to_byte(source, 0, 0), 0);
339 assert_eq!(line_col_to_byte(source, 0, 2), 2);
340 assert_eq!(line_col_to_byte(source, 1, 0), 4);
342 assert_eq!(line_col_to_byte(source, 1, 3), 7);
343 assert_eq!(line_col_to_byte(source, 2, 0), 8);
345 assert_eq!(line_col_to_byte(source, 2, 2), 10);
346 }
347
348 #[test]
349 fn line_col_to_byte_last_line_no_trailing_newline() {
350 let source = "abc\ndef";
351 assert_eq!(line_col_to_byte(source, 1, 0), 4);
353 assert_eq!(line_col_to_byte(source, 1, 3), 7); }
355
356 #[test]
357 fn line_col_to_byte_multi_byte_utf8() {
358 let source = "café\nbar";
360 assert_eq!(line_col_to_byte(source, 0, 0), 0);
362 assert_eq!(line_col_to_byte(source, 0, 5), 5); assert_eq!(line_col_to_byte(source, 1, 0), 6);
365 assert_eq!(line_col_to_byte(source, 1, 2), 8);
366 }
367
368 #[test]
369 fn line_col_to_byte_beyond_end() {
370 let source = "abc";
371 assert_eq!(line_col_to_byte(source, 5, 0), source.len());
373 }
374
375 #[test]
376 fn line_col_to_byte_col_clamped_to_line_length() {
377 let source = "ab\ncd";
378 assert_eq!(line_col_to_byte(source, 0, 10), 2);
380 }
381
382 #[test]
385 fn replace_byte_range_basic() {
386 let source = "hello world";
387 let result = replace_byte_range(source, 6, 11, "rust").unwrap();
388 assert_eq!(result, "hello rust");
389 }
390
391 #[test]
392 fn replace_byte_range_delete() {
393 let source = "hello world";
394 let result = replace_byte_range(source, 5, 11, "").unwrap();
395 assert_eq!(result, "hello");
396 }
397
398 #[test]
399 fn replace_byte_range_insert_at_same_position() {
400 let source = "helloworld";
401 let result = replace_byte_range(source, 5, 5, " ").unwrap();
402 assert_eq!(result, "hello world");
403 }
404
405 #[test]
406 fn replace_byte_range_replace_entire_string() {
407 let source = "old content";
408 let result = replace_byte_range(source, 0, source.len(), "new content").unwrap();
409 assert_eq!(result, "new content");
410 }
411}
412
413pub fn write_format_only(path: &Path, config: &Config) -> Result<bool, AftError> {
416 use crate::format::detect_formatter;
417 let lang = match crate::parser::detect_language(path) {
418 Some(l) => l,
419 None => return Ok(false),
420 };
421 let formatter = detect_formatter(path, lang, config);
422 if let Some((cmd, args)) = formatter {
423 let status = std::process::Command::new(&cmd)
424 .args(&args)
425 .arg(path)
426 .stdout(std::process::Stdio::null())
427 .stderr(std::process::Stdio::null())
428 .status();
429 match status {
430 Ok(s) if s.success() => Ok(true),
431 _ => Ok(false),
432 }
433 } else {
434 Ok(false)
435 }
436}