pub mod abbreviations;
pub mod cli;
pub mod config;
pub mod diff;
pub mod files;
pub mod format;
pub mod git_diff;
pub mod init;
pub mod lsp;
pub mod output;
pub mod parser;
pub mod reflow;
pub mod sdiff;
pub mod sentence;
pub mod watch;
use anyhow::Result;
use crate::format::Format;
use crate::parser::FormatParser;
use crate::parser::latex::LatexParser;
use crate::parser::markdown::MarkdownParser;
use crate::parser::org::OrgParser;
use crate::parser::plaintext::PlaintextParser;
use crate::reflow::{ReflowConfig, reflow};
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 fn build_splitter(config: &FormatConfig) -> Result<Box<dyn SentenceSplitter>> {
if config.use_neural {
let neural = if let Some(ref path) = config.neural_model_path {
sentence::neural::NeuralSentenceSplitter::from_path(path)
} else {
sentence::neural::NeuralSentenceSplitter::new(&config.neural_lang)
};
Ok(Box::new(neural.map_err(|e| anyhow::anyhow!("{e}"))?))
} else {
Ok(Box::new(UnicodeSentenceSplitter::for_lang(
&config.neural_lang,
&config.extra_abbreviations,
)))
}
}
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_text_with_splitter(
input: &str,
config: &FormatConfig,
splitter: &dyn SentenceSplitter,
) -> Result<String> {
let parser: Box<dyn FormatParser> = match config.format {
Format::Org => Box::new(OrgParser),
Format::Latex => Box::new(LatexParser),
Format::Markdown => Box::new(MarkdownParser),
Format::Rst => Box::new(parser::rst::RstParser),
Format::Plaintext => Box::new(PlaintextParser),
};
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 regions = parser.parse(work_input);
let reflow_config = ReflowConfig {
max_width: config.max_width,
};
let mut output = reflow(®ions, splitter, &reflow_config);
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)
}
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)
}