pub mod abbreviations;
pub mod check;
#[cfg(feature = "cli")]
pub mod cli;
pub mod code_block;
pub mod config;
pub mod diff;
#[cfg(not(target_arch = "wasm32"))]
pub mod files;
pub mod format;
#[cfg(not(target_arch = "wasm32"))]
pub mod git_diff;
#[cfg(feature = "cli")]
pub mod init;
#[cfg(feature = "lsp")]
pub mod lsp;
#[cfg(feature = "mcp")]
pub mod mcp;
pub mod oracle;
pub mod output;
pub mod parser;
pub mod reflow;
#[cfg(not(target_arch = "wasm32"))]
pub mod sdiff;
pub mod sentence;
#[cfg(feature = "treesitter")]
mod ts_comments;
#[cfg(feature = "wasm")]
pub mod wasm;
#[cfg(feature = "watch")]
pub mod watch;
use std::collections::HashMap;
use anyhow::Result;
use crate::config::CodeLang;
use crate::format::Format;
use crate::reflow::ReflowConfig;
use crate::sentence::SentenceSplitter;
use crate::sentence::unicode::UnicodeSentenceSplitter;
pub struct FormatConfig {
pub format: Format,
pub max_width: usize,
pub use_neural: bool,
pub neural_lang: String,
pub neural_model_path: Option<std::path::PathBuf>,
pub extra_abbreviations: Vec<String>,
pub use_pandoc: bool,
pub pandoc_format: Option<String>,
#[cfg(feature = "pandoc")]
pub pandoc_backend: parser::pandoc::PandocBackend,
pub code: HashMap<String, CodeLang>,
pub format_code: bool,
pub clause_breaks: bool,
pub fixpoint_backstop: bool,
pub render_backstop: bool,
pub latex_verbatim_envs: Vec<String>,
pub latex_structure_envs: Vec<String>,
pub latex_verbatim_commands: Vec<String>,
}
impl Default for FormatConfig {
fn default() -> Self {
Self {
format: Format::Plaintext,
max_width: 0,
use_neural: false,
neural_lang: "en".to_string(),
neural_model_path: None,
extra_abbreviations: vec![],
use_pandoc: false,
pandoc_format: None,
#[cfg(feature = "pandoc")]
pandoc_backend: parser::pandoc::PandocBackend::default(),
code: HashMap::new(),
format_code: false,
clause_breaks: false,
fixpoint_backstop: true,
render_backstop: true,
latex_verbatim_envs: vec![],
latex_structure_envs: vec![],
latex_verbatim_commands: vec![],
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[error("input is not valid UTF-8")]
pub struct InvalidUtf8Error;
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[error("pandoc backend cannot splice original source bytes")]
pub struct PandocCannotSplice;
const MAX_FORMAT_PASSES: usize = 4;
impl FormatConfig {
pub fn without_safety_backstops(mut self) -> Self {
self.fixpoint_backstop = false;
self.render_backstop = false;
self
}
}
pub fn run_fixpoint<F>(original: &str, enabled: bool, mut step: F) -> Result<String>
where
F: FnMut(&str) -> Result<String>,
{
let once = step(original)?;
if !enabled {
return Ok(once);
}
let mut cur = once;
let mut seen = std::collections::HashSet::new();
seen.insert(original.to_string());
seen.insert(cur.clone());
for _ in 1..MAX_FORMAT_PASSES {
let next = step(&cur)?;
if next == cur {
return Ok(cur);
}
if !seen.insert(next.clone()) {
return Ok(original.to_string());
}
cur = next;
}
Ok(original.to_string())
}
pub fn build_splitter(config: &FormatConfig) -> Result<Box<dyn SentenceSplitter>> {
if config.use_neural {
#[cfg(feature = "neural")]
{
let neural = if let Some(ref path) = config.neural_model_path {
sentence::neural::NeuralSentenceSplitter::from_path_with_extras(
path,
&config.neural_lang,
&config.extra_abbreviations,
)
} else {
sentence::neural::NeuralSentenceSplitter::with_extras(
&config.neural_lang,
&config.extra_abbreviations,
)
};
Ok(Box::new(
neural
.map_err(|e| anyhow::anyhow!("{e}"))?
.with_verbatim_commands(config.latex_verbatim_commands.clone()),
))
}
#[cfg(not(feature = "neural"))]
{
Err(anyhow::anyhow!(
"neural sentence splitting requires the 'neural' feature"
))
}
} else {
Ok(Box::new(
UnicodeSentenceSplitter::for_lang(&config.neural_lang, &config.extra_abbreviations)
.with_verbatim_commands(config.latex_verbatim_commands.clone()),
))
}
}
pub fn format_text(input: &str, config: &FormatConfig) -> Result<String> {
let splitter = build_splitter(config)?;
format_text_with_splitter(input, config, splitter.as_ref())
}
pub fn format_bytes(input: &[u8], config: &FormatConfig) -> Result<Vec<u8>> {
let s = std::str::from_utf8(input).map_err(|_| anyhow::Error::new(InvalidUtf8Error))?;
format_text(s, config).map(|s| s.into_bytes())
}
pub fn format_text_with_splitter(
input: &str,
config: &FormatConfig,
splitter: &dyn SentenceSplitter,
) -> Result<String> {
let had_trailing_newline = input.ends_with('\n');
let uses_crlf = input.contains("\r\n");
let normalized;
let work_input = if uses_crlf {
normalized = input.replace("\r\n", "\n");
&normalized
} else {
input
};
let once = format_once(work_input, config, splitter, config.format_code)?;
let candidate = run_fixpoint(work_input, config.fixpoint_backstop, |cur| {
if cur == work_input {
Ok(once.clone())
} else {
format_once(cur, config, splitter, false)
}
})?;
let candidate = if config.render_backstop
&& candidate != work_input
&& !oracle::matches_ex(
config.format,
work_input,
&candidate,
config.format_code,
Some(config),
) {
work_input.to_string()
} else {
candidate
};
let mut output = candidate;
if had_trailing_newline && !output.ends_with('\n') {
output.push('\n');
} else if !had_trailing_newline {
while output.ends_with('\n') {
output.pop();
}
}
if uses_crlf {
output = output.replace('\n', "\r\n");
}
Ok(output)
}
fn format_once(
work_input: &str,
config: &FormatConfig,
splitter: &dyn SentenceSplitter,
format_code: bool,
) -> Result<String> {
use crate::parser::SpannedRegion;
use crate::reflow::reflow_spanned;
let reflow_config = ReflowConfig {
max_width: config.max_width,
code: Some(&config.code),
format_code,
clause_breaks: config.clause_breaks,
format: config.format,
};
if config.use_pandoc {
#[cfg(feature = "pandoc")]
{
let pandoc_fmt = config
.pandoc_format
.as_deref()
.unwrap_or(match config.format {
Format::Org => "org",
Format::Latex => "latex",
Format::Markdown => "markdown",
Format::Rst => "rst",
Format::Plaintext => "markdown",
});
let parser =
parser::pandoc::PandocParser::with_backend(pandoc_fmt, config.pandoc_backend);
parser
.try_parse(work_input)
.map_err(|e| anyhow::anyhow!("{e}"))?;
return Err(anyhow::Error::new(PandocCannotSplice));
}
#[cfg(not(feature = "pandoc"))]
{
return Err(anyhow::anyhow!(
"pandoc backend requires the 'pandoc' feature"
));
}
}
let spanned: Vec<SpannedRegion> =
parser::parser_for_format_config(config.format, Some(config)).parse_full(work_input);
match reflow_spanned(work_input, &spanned, splitter, &reflow_config) {
Ok(out) => Ok(out),
Err(_) => Ok(work_input.to_string()),
}
}
pub fn format_range(
input: &str,
config: &FormatConfig,
start: usize,
end: usize,
) -> Result<String> {
let lines: Vec<&str> = input.lines().collect();
let total = lines.len();
let start = start.max(1);
let end = end.min(total);
if start > total {
return Ok(input.to_string());
}
let range_text = lines[start - 1..end].join("\n");
let formatted = format_text(&range_text, config)?;
let mut result = String::new();
for (i, line) in lines.iter().enumerate() {
let line_num = i + 1;
if line_num < start {
result.push_str(line);
result.push('\n');
}
}
result.push_str(&formatted);
if !formatted.ends_with('\n') && end < total {
result.push('\n');
}
for (i, line) in lines.iter().enumerate() {
let line_num = i + 1;
if line_num > end {
result.push_str(line);
if line_num < total {
result.push('\n');
}
}
}
if input.ends_with('\n') && !result.ends_with('\n') {
result.push('\n');
} else if !input.ends_with('\n') {
while result.ends_with('\n') {
result.pop();
}
}
Ok(result)
}