cairo_lang_formatter/
cairo_formatter.rsuse std::fmt::{Debug, Display};
use std::fs;
use std::io::{Read, stdin};
use std::path::{Path, PathBuf};
use anyhow::{Context, Result, anyhow};
use cairo_lang_diagnostics::FormattedDiagnosticEntry;
use cairo_lang_filesystem::db::FilesGroup;
use cairo_lang_filesystem::ids::{CAIRO_FILE_EXTENSION, FileId, FileKind, FileLongId, VirtualFile};
use cairo_lang_parser::utils::{SimpleParserDatabase, get_syntax_root_and_diagnostics};
use cairo_lang_utils::Intern;
use diffy::{PatchFormatter, create_patch};
use ignore::WalkBuilder;
use ignore::types::TypesBuilder;
use thiserror::Error;
use crate::{CAIRO_FMT_IGNORE, FormatterConfig, get_formatted_file};
#[derive(Clone)]
pub struct FileDiff {
pub original: String,
pub formatted: String,
}
impl FileDiff {
pub fn display_colored(&self) -> FileDiffColoredDisplay<'_> {
FileDiffColoredDisplay { diff: self }
}
}
impl Display for FileDiff {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", create_patch(&self.original, &self.formatted))
}
}
impl Debug for FileDiff {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "FileDiff({self})")
}
}
pub struct FileDiffColoredDisplay<'a> {
diff: &'a FileDiff,
}
impl Display for FileDiffColoredDisplay<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let patch = create_patch(&self.diff.original, &self.diff.formatted);
let patch_formatter = PatchFormatter::new().with_color();
let formatted_patch = patch_formatter.fmt_patch(&patch);
formatted_patch.fmt(f)
}
}
impl Debug for FileDiffColoredDisplay<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "FileDiffColoredDisplay({:?})", self.diff)
}
}
#[derive(Debug)]
pub enum FormatOutcome {
Identical(String),
DiffFound(FileDiff),
}
impl FormatOutcome {
pub fn into_output_text(self) -> String {
match self {
FormatOutcome::Identical(original) => original,
FormatOutcome::DiffFound(diff) => diff.formatted,
}
}
}
#[derive(Debug, Error)]
pub enum FormattingError {
#[error(transparent)]
ParsingError(ParsingError),
#[error(transparent)]
Error(#[from] anyhow::Error),
}
#[derive(Debug, Error)]
pub struct ParsingError(Vec<FormattedDiagnosticEntry>);
impl ParsingError {
pub fn iter(&self) -> impl Iterator<Item = &FormattedDiagnosticEntry> {
self.0.iter()
}
}
impl From<ParsingError> for Vec<FormattedDiagnosticEntry> {
fn from(error: ParsingError) -> Self {
error.0
}
}
impl From<Vec<FormattedDiagnosticEntry>> for ParsingError {
fn from(diagnostics: Vec<FormattedDiagnosticEntry>) -> Self {
Self(diagnostics)
}
}
impl Display for ParsingError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for entry in &self.0 {
writeln!(f, "{entry}")?;
}
Ok(())
}
}
pub struct StdinFmt;
pub trait FormattableInput {
fn to_file_id(&self, db: &dyn FilesGroup) -> Result<FileId>;
fn overwrite_content(&self, _content: String) -> Result<()>;
}
impl FormattableInput for &Path {
fn to_file_id(&self, db: &dyn FilesGroup) -> Result<FileId> {
Ok(FileId::new(db, PathBuf::from(self)))
}
fn overwrite_content(&self, content: String) -> Result<()> {
fs::write(self, content)?;
Ok(())
}
}
impl FormattableInput for String {
fn to_file_id(&self, db: &dyn FilesGroup) -> Result<FileId> {
Ok(FileLongId::Virtual(VirtualFile {
parent: None,
name: "string_to_format".into(),
content: self.clone().into(),
code_mappings: [].into(),
kind: FileKind::Module,
})
.intern(db))
}
fn overwrite_content(&self, _content: String) -> Result<()> {
Ok(())
}
}
impl FormattableInput for StdinFmt {
fn to_file_id(&self, db: &dyn FilesGroup) -> Result<FileId> {
let mut buffer = String::new();
stdin().read_to_string(&mut buffer)?;
Ok(FileLongId::Virtual(VirtualFile {
parent: None,
name: "<stdin>".into(),
content: buffer.into(),
code_mappings: [].into(),
kind: FileKind::Module,
})
.intern(db))
}
fn overwrite_content(&self, content: String) -> Result<()> {
print!("{content}");
Ok(())
}
}
fn format_input(
input: &dyn FormattableInput,
config: &FormatterConfig,
) -> Result<FormatOutcome, FormattingError> {
let db = SimpleParserDatabase::default();
let file_id = input.to_file_id(&db).context("Unable to create virtual file.")?;
let original_text =
db.file_content(file_id).ok_or_else(|| anyhow!("Unable to read from input."))?;
let (syntax_root, diagnostics) = get_syntax_root_and_diagnostics(&db, file_id, &original_text);
if diagnostics.check_error_free().is_err() {
return Err(FormattingError::ParsingError(
diagnostics.format_with_severity(&db, &Default::default()).into(),
));
}
let formatted_text = get_formatted_file(&db, &syntax_root, config.clone());
if formatted_text == original_text.as_ref() {
Ok(FormatOutcome::Identical(original_text.to_string()))
} else {
let diff = FileDiff { original: original_text.to_string(), formatted: formatted_text };
Ok(FormatOutcome::DiffFound(diff))
}
}
#[derive(Debug)]
pub struct CairoFormatter {
formatter_config: FormatterConfig,
}
impl CairoFormatter {
pub fn new(formatter_config: FormatterConfig) -> Self {
Self { formatter_config }
}
pub fn walk(&self, path: &Path) -> WalkBuilder {
let mut builder = WalkBuilder::new(path);
builder.add_custom_ignore_filename(CAIRO_FMT_IGNORE);
builder.follow_links(false);
builder.skip_stdout(true);
let mut types_builder = TypesBuilder::new();
types_builder.add(CAIRO_FILE_EXTENSION, &format!("*.{CAIRO_FILE_EXTENSION}")).unwrap();
types_builder.select(CAIRO_FILE_EXTENSION);
builder.types(types_builder.build().unwrap());
builder
}
pub fn format_in_place(
&self,
input: &dyn FormattableInput,
) -> Result<FormatOutcome, FormattingError> {
match format_input(input, &self.formatter_config)? {
FormatOutcome::DiffFound(diff) => {
input.overwrite_content(diff.formatted.clone())?;
Ok(FormatOutcome::DiffFound(diff))
}
FormatOutcome::Identical(original) => Ok(FormatOutcome::Identical(original)),
}
}
pub fn format_to_string(
&self,
input: &dyn FormattableInput,
) -> Result<FormatOutcome, FormattingError> {
format_input(input, &self.formatter_config)
}
}