1pub mod abbreviations;
36pub mod check;
37#[cfg(feature = "cli")]
38pub mod cli;
39pub mod code_block;
40pub mod config;
41pub mod diff;
42#[cfg(not(target_arch = "wasm32"))]
43pub mod files;
44pub mod format;
45#[cfg(not(target_arch = "wasm32"))]
46pub mod git_diff;
47#[cfg(feature = "cli")]
48pub mod init;
49#[cfg(feature = "lsp")]
50pub mod lsp;
51#[cfg(feature = "mcp")]
52pub mod mcp;
53pub mod oracle;
54pub mod output;
55pub mod parser;
56pub mod reflow;
57#[cfg(not(target_arch = "wasm32"))]
58pub mod sdiff;
59pub mod sentence;
60#[cfg(feature = "treesitter")]
61mod ts_comments;
62#[cfg(feature = "wasm")]
63pub mod wasm;
64#[cfg(feature = "watch")]
65pub mod watch;
66
67use std::collections::HashMap;
68
69use anyhow::Result;
70
71use crate::config::CodeLang;
72use crate::format::Format;
73use crate::reflow::ReflowConfig;
74use crate::sentence::SentenceSplitter;
75use crate::sentence::unicode::UnicodeSentenceSplitter;
76
77pub struct FormatConfig {
79 pub format: Format,
80 pub max_width: usize,
81 pub use_neural: bool,
82 pub neural_lang: String,
83 pub neural_model_path: Option<std::path::PathBuf>,
84 pub extra_abbreviations: Vec<String>,
85 pub use_pandoc: bool,
86 pub pandoc_format: Option<String>,
88 #[cfg(feature = "pandoc")]
91 pub pandoc_backend: parser::pandoc::PandocBackend,
92 pub code: HashMap<String, CodeLang>,
96 pub format_code: bool,
100 pub clause_breaks: bool,
107 pub fixpoint_backstop: bool,
110 pub render_backstop: bool,
114 pub latex_verbatim_envs: Vec<String>,
117 pub latex_structure_envs: Vec<String>,
120 pub latex_verbatim_commands: Vec<String>,
123}
124
125impl Default for FormatConfig {
126 fn default() -> Self {
127 Self {
128 format: Format::Plaintext,
129 max_width: 0,
130 use_neural: false,
131 neural_lang: "en".to_string(),
132 neural_model_path: None,
133 extra_abbreviations: vec![],
134 use_pandoc: false,
135 pandoc_format: None,
136 #[cfg(feature = "pandoc")]
137 pandoc_backend: parser::pandoc::PandocBackend::default(),
138 code: HashMap::new(),
139 format_code: false,
140 clause_breaks: false,
141 fixpoint_backstop: true,
142 render_backstop: true,
143 latex_verbatim_envs: vec![],
144 latex_structure_envs: vec![],
145 latex_verbatim_commands: vec![],
146 }
147 }
148}
149
150#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
152#[error("input is not valid UTF-8")]
153pub struct InvalidUtf8Error;
154
155#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
158#[error("pandoc backend cannot splice original source bytes")]
159pub struct PandocCannotSplice;
160
161const MAX_FORMAT_PASSES: usize = 4;
164
165impl FormatConfig {
166 pub fn without_safety_backstops(mut self) -> Self {
169 self.fixpoint_backstop = false;
170 self.render_backstop = false;
171 self
172 }
173}
174
175pub fn run_fixpoint<F>(original: &str, enabled: bool, mut step: F) -> Result<String>
180where
181 F: FnMut(&str) -> Result<String>,
182{
183 let once = step(original)?;
184 if !enabled {
185 return Ok(once);
186 }
187 let mut cur = once;
188 let mut seen = std::collections::HashSet::new();
189 seen.insert(original.to_string());
190 seen.insert(cur.clone());
191 for _ in 1..MAX_FORMAT_PASSES {
192 let next = step(&cur)?;
193 if next == cur {
194 return Ok(cur);
195 }
196 if !seen.insert(next.clone()) {
197 return Ok(original.to_string());
198 }
199 cur = next;
200 }
201 Ok(original.to_string())
202}
203
204pub fn build_splitter(config: &FormatConfig) -> Result<Box<dyn SentenceSplitter>> {
206 if config.use_neural {
207 #[cfg(feature = "neural")]
208 {
209 let neural = if let Some(ref path) = config.neural_model_path {
210 sentence::neural::NeuralSentenceSplitter::from_path_with_extras(
211 path,
212 &config.neural_lang,
213 &config.extra_abbreviations,
214 )
215 } else {
216 sentence::neural::NeuralSentenceSplitter::with_extras(
217 &config.neural_lang,
218 &config.extra_abbreviations,
219 )
220 };
221 Ok(Box::new(
222 neural
223 .map_err(|e| anyhow::anyhow!("{e}"))?
224 .with_verbatim_commands(config.latex_verbatim_commands.clone()),
225 ))
226 }
227 #[cfg(not(feature = "neural"))]
228 {
229 Err(anyhow::anyhow!(
230 "neural sentence splitting requires the 'neural' feature"
231 ))
232 }
233 } else {
234 Ok(Box::new(
235 UnicodeSentenceSplitter::for_lang(&config.neural_lang, &config.extra_abbreviations)
236 .with_verbatim_commands(config.latex_verbatim_commands.clone()),
237 ))
238 }
239}
240
241pub fn format_text(input: &str, config: &FormatConfig) -> Result<String> {
243 let splitter = build_splitter(config)?;
244 format_text_with_splitter(input, config, splitter.as_ref())
245}
246
247pub fn format_bytes(input: &[u8], config: &FormatConfig) -> Result<Vec<u8>> {
249 let s = std::str::from_utf8(input).map_err(|_| anyhow::Error::new(InvalidUtf8Error))?;
250 format_text(s, config).map(|s| s.into_bytes())
251}
252
253pub fn format_text_with_splitter(
255 input: &str,
256 config: &FormatConfig,
257 splitter: &dyn SentenceSplitter,
258) -> Result<String> {
259 let had_trailing_newline = input.ends_with('\n');
260 let uses_crlf = input.contains("\r\n");
261
262 let normalized;
264 let work_input = if uses_crlf {
265 normalized = input.replace("\r\n", "\n");
266 &normalized
267 } else {
268 input
269 };
270
271 let once = format_once(work_input, config, splitter, config.format_code)?;
272 let candidate = run_fixpoint(work_input, config.fixpoint_backstop, |cur| {
276 if cur == work_input {
277 Ok(once.clone())
278 } else {
279 format_once(cur, config, splitter, false)
280 }
281 })?;
282
283 let candidate = if config.render_backstop
284 && candidate != work_input
285 && !oracle::matches_ex(
286 config.format,
287 work_input,
288 &candidate,
289 config.format_code,
290 Some(config),
291 ) {
292 work_input.to_string()
293 } else {
294 candidate
295 };
296
297 let mut output = candidate;
298
299 if had_trailing_newline && !output.ends_with('\n') {
301 output.push('\n');
302 } else if !had_trailing_newline {
303 while output.ends_with('\n') {
304 output.pop();
305 }
306 }
307
308 if uses_crlf {
310 output = output.replace('\n', "\r\n");
311 }
312
313 Ok(output)
314}
315
316fn format_once(
319 work_input: &str,
320 config: &FormatConfig,
321 splitter: &dyn SentenceSplitter,
322 format_code: bool,
323) -> Result<String> {
324 use crate::parser::SpannedRegion;
325 use crate::reflow::reflow_spanned;
326
327 let reflow_config = ReflowConfig {
328 max_width: config.max_width,
329 code: Some(&config.code),
330 format_code,
331 clause_breaks: config.clause_breaks,
332 format: config.format,
333 };
334
335 if config.use_pandoc {
339 #[cfg(feature = "pandoc")]
340 {
341 let pandoc_fmt = config
342 .pandoc_format
343 .as_deref()
344 .unwrap_or(match config.format {
345 Format::Org => "org",
346 Format::Latex => "latex",
347 Format::Markdown => "markdown",
348 Format::Rst => "rst",
349 Format::Plaintext => "markdown",
350 });
351 let parser =
352 parser::pandoc::PandocParser::with_backend(pandoc_fmt, config.pandoc_backend);
353 parser
356 .try_parse(work_input)
357 .map_err(|e| anyhow::anyhow!("{e}"))?;
358 return Err(anyhow::Error::new(PandocCannotSplice));
359 }
360 #[cfg(not(feature = "pandoc"))]
361 {
362 return Err(anyhow::anyhow!(
363 "pandoc backend requires the 'pandoc' feature"
364 ));
365 }
366 }
367
368 let spanned: Vec<SpannedRegion> =
369 parser::parser_for_format_config(config.format, Some(config)).parse_full(work_input);
370 match reflow_spanned(work_input, &spanned, splitter, &reflow_config) {
371 Ok(out) => Ok(out),
372 Err(_) => Ok(work_input.to_string()),
373 }
374}
375
376pub fn format_range(
379 input: &str,
380 config: &FormatConfig,
381 start: usize,
382 end: usize,
383) -> Result<String> {
384 let lines: Vec<&str> = input.lines().collect();
385 let total = lines.len();
386
387 let start = start.max(1);
389 let end = end.min(total);
390
391 if start > total {
392 return Ok(input.to_string());
393 }
394
395 let range_text = lines[start - 1..end].join("\n");
397 let formatted = format_text(&range_text, config)?;
398
399 let mut result = String::new();
401 for (i, line) in lines.iter().enumerate() {
402 let line_num = i + 1;
403 if line_num < start {
404 result.push_str(line);
405 result.push('\n');
406 }
407 }
408 result.push_str(&formatted);
409 if !formatted.ends_with('\n') && end < total {
410 result.push('\n');
411 }
412 for (i, line) in lines.iter().enumerate() {
413 let line_num = i + 1;
414 if line_num > end {
415 result.push_str(line);
416 if line_num < total {
417 result.push('\n');
418 }
419 }
420 }
421
422 if input.ends_with('\n') && !result.ends_with('\n') {
424 result.push('\n');
425 } else if !input.ends_with('\n') {
426 while result.ends_with('\n') {
427 result.pop();
428 }
429 }
430
431 Ok(result)
432}