1use anyhow::{Context, Result};
2use std::path::Path;
3
4use crate::FormatConfig;
5use crate::diff;
6use crate::format::Format;
7
8pub fn format_file(path: &str, max_width: usize) -> Result<String> {
11 let content =
12 std::fs::read_to_string(path).with_context(|| format!("failed to read {}", path))?;
13 let fmt = Format::recognized_from_path(Path::new(path)).ok_or_else(|| {
14 anyhow::anyhow!("{path}: not a prose format; pass --format or use .org/.tex/.md/.rst/.txt")
15 })?;
16 let config = FormatConfig {
17 format: fmt,
18 max_width,
19 ..Default::default()
20 };
21 crate::format_text(&content, &config)
22}
23
24pub fn format_file_with(
26 path: &str,
27 max_width: usize,
28 extra_abbreviations: Vec<String>,
29) -> Result<String> {
30 let content =
31 std::fs::read_to_string(path).with_context(|| format!("failed to read {}", path))?;
32 let fmt = Format::recognized_from_path(Path::new(path)).ok_or_else(|| {
33 anyhow::anyhow!("{path}: not a prose format; pass --format or use .org/.tex/.md/.rst/.txt")
34 })?;
35 let config = FormatConfig {
36 format: fmt,
37 max_width,
38 extra_abbreviations,
39 ..Default::default()
40 };
41 crate::format_text(&content, &config)
42}
43
44pub fn check_file(path: &str, max_width: usize) -> Result<Option<String>> {
47 let original =
48 std::fs::read_to_string(path).with_context(|| format!("failed to read {}", path))?;
49 let formatted = format_file(path, max_width)?;
50 if original == formatted {
51 return Ok(None);
52 }
53 Ok(Some(diff::unified_diff(path, &original, &formatted)))
54}
55
56pub fn format_in_place(path: &str, max_width: usize) -> Result<bool> {
58 let original =
59 std::fs::read_to_string(path).with_context(|| format!("failed to read {}", path))?;
60 let formatted = format_file(path, max_width)?;
61 if original == formatted {
62 return Ok(false);
63 }
64 std::fs::write(path, &formatted).with_context(|| format!("failed to write {}", path))?;
65 Ok(true)
66}
67
68#[cfg(test)]
69mod tests {
70 use super::*;
71
72 #[test]
73 fn already_formatted() {
74 let tmp = std::env::temp_dir().join("snap_files_ok.md");
75 std::fs::write(&tmp, "Hello world.\nThis is a test.\n").unwrap();
76 let result = check_file(tmp.to_str().unwrap(), 0).unwrap();
77 assert!(result.is_none());
78 std::fs::remove_file(&tmp).ok();
79 }
80
81 #[test]
82 fn needs_formatting() {
83 let tmp = std::env::temp_dir().join("snap_files_needs.md");
84 std::fs::write(&tmp, "Hello world. This is a test. Another sentence.\n").unwrap();
85 let result = check_file(tmp.to_str().unwrap(), 0).unwrap();
86 assert!(result.is_some());
87 let diff = result.unwrap();
88 assert!(diff.contains("--- a/"));
89 assert!(diff.contains("+Hello world."));
90 std::fs::remove_file(&tmp).ok();
91 }
92
93 #[test]
94 fn format_in_place_changes_file() {
95 let tmp = std::env::temp_dir().join("snap_files_inplace.md");
96 std::fs::write(&tmp, "Hello world. This is a test.\n").unwrap();
97 let changed = format_in_place(tmp.to_str().unwrap(), 0).unwrap();
98 assert!(changed);
99 let content = std::fs::read_to_string(&tmp).unwrap();
100 assert!(content.contains("Hello world.\n"));
101 assert!(content.contains("This is a test.\n"));
102 std::fs::remove_file(&tmp).ok();
103 }
104
105 #[test]
106 fn format_in_place_noop_when_formatted() {
107 let tmp = std::env::temp_dir().join("snap_files_noop.md");
108 std::fs::write(&tmp, "Hello world.\nThis is a test.\n").unwrap();
109 let changed = format_in_place(tmp.to_str().unwrap(), 0).unwrap();
110 assert!(!changed);
111 std::fs::remove_file(&tmp).ok();
112 }
113
114 #[test]
115 fn format_file_refuses_source_extension() {
116 let tmp = std::env::temp_dir().join("snap_files_refuse.rs");
117 std::fs::write(&tmp, "fn main() { let x = 1.0; }\n").unwrap();
118 let err = format_file(tmp.to_str().unwrap(), 0).unwrap_err();
119 assert!(err.to_string().contains("not a prose format"), "got: {err}");
120 std::fs::remove_file(&tmp).ok();
121 }
122}