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(source: &str, start: usize, end: usize, replacement: &str) -> String {
36 let mut result = String::with_capacity(source.len() - (end - start) + replacement.len());
37 result.push_str(&source[..start]);
38 result.push_str(replacement);
39 result.push_str(&source[end..]);
40 result
41}
42
43pub fn validate_syntax(path: &Path) -> Result<Option<bool>, AftError> {
48 let mut parser = FileParser::new();
49 match parser.parse(path) {
50 Ok((tree, _lang)) => Ok(Some(!tree.root_node().has_error())),
51 Err(AftError::InvalidRequest { .. }) => {
52 Ok(None)
54 }
55 Err(e) => Err(e),
56 }
57}
58
59pub fn validate_syntax_str(content: &str, path: &Path) -> Option<bool> {
65 let lang = detect_language(path)?;
66 let grammar = grammar_for(lang);
67 let mut parser = tree_sitter::Parser::new();
68 if parser.set_language(&grammar).is_err() {
69 return None;
70 }
71 let tree = parser.parse(content.as_bytes(), None)?;
72 Some(!tree.root_node().has_error())
73}
74
75pub struct DryRunResult {
77 pub diff: String,
79 pub syntax_valid: Option<bool>,
81}
82
83pub fn dry_run_diff(original: &str, proposed: &str, path: &Path) -> DryRunResult {
88 let display_path = path.display().to_string();
89 let text_diff = similar::TextDiff::from_lines(original, proposed);
90 let diff = text_diff
91 .unified_diff()
92 .context_radius(3)
93 .header(
94 &format!("a/{}", display_path),
95 &format!("b/{}", display_path),
96 )
97 .to_string();
98 let syntax_valid = validate_syntax_str(proposed, path);
99 DryRunResult { diff, syntax_valid }
100}
101
102pub fn is_dry_run(params: &serde_json::Value) -> bool {
106 params
107 .get("dry_run")
108 .and_then(|v| v.as_bool())
109 .unwrap_or(false)
110}
111
112pub fn wants_diff(params: &serde_json::Value) -> bool {
114 params
115 .get("include_diff")
116 .and_then(|v| v.as_bool())
117 .unwrap_or(false)
118}
119
120pub fn compute_diff_info(before: &str, after: &str) -> serde_json::Value {
124 use similar::ChangeTag;
125
126 let diff = similar::TextDiff::from_lines(before, after);
127 let mut additions = 0usize;
128 let mut deletions = 0usize;
129 for change in diff.iter_all_changes() {
130 match change.tag() {
131 ChangeTag::Insert => additions += 1,
132 ChangeTag::Delete => deletions += 1,
133 ChangeTag::Equal => {}
134 }
135 }
136
137 let size_limit = 512 * 1024; if before.len() > size_limit || after.len() > size_limit {
140 serde_json::json!({
141 "additions": additions,
142 "deletions": deletions,
143 "truncated": true,
144 })
145 } else {
146 serde_json::json!({
147 "before": before,
148 "after": after,
149 "additions": additions,
150 "deletions": deletions,
151 })
152 }
153}
154pub fn auto_backup(
161 ctx: &AppContext,
162 path: &Path,
163 description: &str,
164) -> Result<Option<String>, AftError> {
165 if !path.exists() {
166 return Ok(None);
167 }
168 let backup_id = {
169 let mut store = ctx.backup().borrow_mut();
170 store.snapshot(path, description)?
171 }; Ok(Some(backup_id))
173}
174
175pub struct WriteResult {
180 pub syntax_valid: Option<bool>,
182 pub formatted: bool,
184 pub format_skipped_reason: Option<String>,
186 pub validate_requested: bool,
188 pub validation_errors: Vec<format::ValidationError>,
190 pub validate_skipped_reason: Option<String>,
192 pub lsp_diagnostics: Vec<crate::lsp::diagnostics::StoredDiagnostic>,
195}
196
197impl WriteResult {
198 pub fn append_lsp_diagnostics_to(&self, result: &mut serde_json::Value) {
201 if !self.lsp_diagnostics.is_empty() {
202 result["lsp_diagnostics"] = serde_json::json!(self
203 .lsp_diagnostics
204 .iter()
205 .map(|d| {
206 serde_json::json!({
207 "file": d.file.display().to_string(),
208 "line": d.line,
209 "column": d.column,
210 "end_line": d.end_line,
211 "end_column": d.end_column,
212 "severity": d.severity.as_str(),
213 "message": d.message,
214 "code": d.code,
215 "source": d.source,
216 })
217 })
218 .collect::<Vec<_>>());
219 }
220 }
221}
222
223pub fn write_format_validate(
235 path: &Path,
236 content: &str,
237 config: &Config,
238 params: &serde_json::Value,
239) -> Result<WriteResult, AftError> {
240 std::fs::write(path, content).map_err(|e| AftError::InvalidRequest {
242 message: format!("failed to write file: {}", e),
243 })?;
244
245 let (formatted, format_skipped_reason) = format::auto_format(path, config);
247
248 let syntax_valid = match validate_syntax(path) {
250 Ok(sv) => sv,
251 Err(_) => None,
252 };
253
254 let validate_requested = params.get("validate").and_then(|v| v.as_str()) == Some("full");
256 let (validation_errors, validate_skipped_reason) = if validate_requested {
257 format::validate_full(path, config)
258 } else {
259 (Vec::new(), None)
260 };
261
262 Ok(WriteResult {
263 syntax_valid,
264 formatted,
265 format_skipped_reason,
266 validate_requested,
267 validation_errors,
268 validate_skipped_reason,
269 lsp_diagnostics: Vec::new(),
270 })
271}
272
273#[cfg(test)]
274mod tests {
275 use super::*;
276
277 #[test]
280 fn line_col_to_byte_empty_string() {
281 assert_eq!(line_col_to_byte("", 0, 0), 0);
282 }
283
284 #[test]
285 fn line_col_to_byte_single_line() {
286 let source = "hello";
287 assert_eq!(line_col_to_byte(source, 0, 0), 0);
288 assert_eq!(line_col_to_byte(source, 0, 3), 3);
289 assert_eq!(line_col_to_byte(source, 0, 5), 5); }
291
292 #[test]
293 fn line_col_to_byte_multi_line() {
294 let source = "abc\ndef\nghi\n";
295 assert_eq!(line_col_to_byte(source, 0, 0), 0);
297 assert_eq!(line_col_to_byte(source, 0, 2), 2);
298 assert_eq!(line_col_to_byte(source, 1, 0), 4);
300 assert_eq!(line_col_to_byte(source, 1, 3), 7);
301 assert_eq!(line_col_to_byte(source, 2, 0), 8);
303 assert_eq!(line_col_to_byte(source, 2, 2), 10);
304 }
305
306 #[test]
307 fn line_col_to_byte_last_line_no_trailing_newline() {
308 let source = "abc\ndef";
309 assert_eq!(line_col_to_byte(source, 1, 0), 4);
311 assert_eq!(line_col_to_byte(source, 1, 3), 7); }
313
314 #[test]
315 fn line_col_to_byte_multi_byte_utf8() {
316 let source = "café\nbar";
318 assert_eq!(line_col_to_byte(source, 0, 0), 0);
320 assert_eq!(line_col_to_byte(source, 0, 5), 5); assert_eq!(line_col_to_byte(source, 1, 0), 6);
323 assert_eq!(line_col_to_byte(source, 1, 2), 8);
324 }
325
326 #[test]
327 fn line_col_to_byte_beyond_end() {
328 let source = "abc";
329 assert_eq!(line_col_to_byte(source, 5, 0), source.len());
331 }
332
333 #[test]
334 fn line_col_to_byte_col_clamped_to_line_length() {
335 let source = "ab\ncd";
336 assert_eq!(line_col_to_byte(source, 0, 10), 2);
338 }
339
340 #[test]
343 fn replace_byte_range_basic() {
344 let source = "hello world";
345 let result = replace_byte_range(source, 6, 11, "rust");
346 assert_eq!(result, "hello rust");
347 }
348
349 #[test]
350 fn replace_byte_range_delete() {
351 let source = "hello world";
352 let result = replace_byte_range(source, 5, 11, "");
353 assert_eq!(result, "hello");
354 }
355
356 #[test]
357 fn replace_byte_range_insert_at_same_position() {
358 let source = "helloworld";
359 let result = replace_byte_range(source, 5, 5, " ");
360 assert_eq!(result, "hello world");
361 }
362
363 #[test]
364 fn replace_byte_range_replace_entire_string() {
365 let source = "old content";
366 let result = replace_byte_range(source, 0, source.len(), "new content");
367 assert_eq!(result, "new content");
368 }
369}
370
371pub fn write_format_only(path: &Path, config: &Config) -> Result<bool, AftError> {
374 use crate::format::detect_formatter;
375 let lang = match crate::parser::detect_language(path) {
376 Some(l) => l,
377 None => return Ok(false),
378 };
379 let formatter = detect_formatter(path, lang, config);
380 if let Some((cmd, args)) = formatter {
381 let status = std::process::Command::new(&cmd)
382 .args(&args)
383 .arg(path)
384 .stdout(std::process::Stdio::null())
385 .stderr(std::process::Stdio::null())
386 .status();
387 match status {
388 Ok(s) if s.success() => Ok(true),
389 _ => Ok(false),
390 }
391 } else {
392 Ok(false)
393 }
394}