use ::console::Term;
use anyhow::Result;
use clap::CommandFactory;
use clap::FromArgMatches;
#[cfg(panic = "unwind")]
use human_panic::setup_panic;
use libdiffsitter::cli;
use libdiffsitter::cli::Args;
use libdiffsitter::config::{Config, ReadError};
use libdiffsitter::console_utils;
use libdiffsitter::diff;
use libdiffsitter::generate_ast_vector_data;
use libdiffsitter::parse::generate_language;
use libdiffsitter::parse::lang_name_from_file_ext;
#[cfg(feature = "static-grammar-libs")]
use libdiffsitter::parse::SUPPORTED_LANGUAGES;
use libdiffsitter::render::{DisplayData, DocumentDiffData, Renderer};
use log::{debug, error, info, warn, LevelFilter};
use serde_json as json;
use std::{
io,
path::Path,
process::{Child, Command},
};
#[cfg(feature = "better-build-info")]
use shadow_rs::shadow;
#[cfg(feature = "jemallocator")]
use jemallocator::Jemalloc;
#[cfg(feature = "jemallocator")]
#[global_allocator]
static GLOBAL: Jemalloc = Jemalloc;
fn derive_config(args: &Args) -> Result<Config> {
if args.no_config {
info!("`no_config` specified, falling back to default config");
return Ok(Config::default());
}
match Config::try_from_file(args.config.as_ref()) {
Ok(config) => Ok(config),
Err(e) => match e {
ReadError::ReadFileFailure(_) | ReadError::NoDefault => {
warn!("{} - falling back to default config", e);
Ok(Config::default())
}
ReadError::DeserializationFailure(e) => {
error!("Failed to deserialize config file: {}", e);
Err(anyhow::anyhow!(e))
}
},
}
}
fn are_input_files_supported(args: &Args, config: &Config) -> bool {
let paths = [&args.old, &args.new];
if let Some(file_type) = &args.file_type {
return generate_language(file_type, &config.grammar).is_ok();
}
paths.into_iter().all(|path| match path {
None => {
warn!("Missing a file. You need two files to make a diff.");
false
}
Some(path) => {
debug!("Checking if {} can be parsed", path.display());
match path.extension() {
None => {
warn!("No filetype deduced for {}", path.display());
false
}
Some(ext) => {
let ext = ext.to_string_lossy();
let lang_name = lang_name_from_file_ext(&ext, &config.grammar);
match lang_name {
Ok(lang_name) => {
debug!("Deduced language {} for path {}", lang_name, path.display());
true
}
Err(e) => {
warn!("Extension {} not supported: {}", ext, e);
false
}
}
}
}
}
})
}
fn run_diff(args: Args, config: Config) -> Result<()> {
let render_config = config.formatting;
let render_param = args.renderer;
let renderer = render_config.get_renderer(render_param)?;
let file_type = args.file_type.as_deref();
let path_a = args.old.as_ref().unwrap();
let path_b = args.new.as_ref().unwrap();
let ast_data_a = generate_ast_vector_data(path_a.clone(), file_type, &config.grammar)?;
let ast_data_b = generate_ast_vector_data(path_b.clone(), file_type, &config.grammar)?;
let diff_vec_a = config
.input_processing
.process(&ast_data_a.tree, &ast_data_a.text);
let diff_vec_b = config
.input_processing
.process(&ast_data_b.tree, &ast_data_b.text);
let hunks = diff::compute_edit_script(&diff_vec_a, &diff_vec_b)?;
let params = DisplayData {
hunks,
old: DocumentDiffData {
filename: &ast_data_a.path.to_string_lossy(),
text: &ast_data_a.text,
},
new: DocumentDiffData {
filename: &ast_data_b.path.to_string_lossy(),
text: &ast_data_b.text,
},
};
let mut buf_writer = Term::buffered_stdout();
let term_info = buf_writer.clone();
renderer.render(&mut buf_writer, ¶ms, Some(&term_info))?;
buf_writer.flush()?;
Ok(())
}
fn dump_default_config() -> Result<()> {
let config = Config::default();
println!("{}", json::to_string_pretty(&config)?);
Ok(())
}
fn diff_fallback(cmd: &str, old: &Path, new: &Path) -> io::Result<Child> {
debug!("Spawning diff fallback process");
Command::new(cmd).args([old, new]).spawn()
}
pub fn list_supported_languages() {
#[cfg(feature = "static-grammar-libs")]
{
println!("This program was compiled with support for:");
for language in SUPPORTED_LANGUAGES.as_slice() {
println!("* {language}");
}
}
#[cfg(feature = "dynamic-grammar-libs")]
{
println!("This program will dynamically load grammars from shared libraries");
}
}
fn print_shell_completion(shell: clap_complete::Shell) {
let mut app = cli::Args::command();
clap_complete::generate(shell, &mut app, "diffsitter", &mut io::stdout());
}
fn main() -> Result<()> {
#[cfg(panic = "unwind")]
setup_panic!();
#[cfg(feature = "better-build-info")]
shadow!(build);
use cli::Command;
#[cfg(feature = "better-build-info")]
let command = Args::command().version(build::CLAP_LONG_VERSION);
#[cfg(not(feature = "better-build-info"))]
let command = Args::command();
let matches = command.get_matches();
let args = Args::from_arg_matches(&matches)?;
let config = derive_config(&args)?;
if let Some(cmd) = args.cmd {
match cmd {
Command::List => list_supported_languages(),
Command::DumpDefaultConfig => dump_default_config()?,
Command::GenCompletion { shell } => {
print_shell_completion(shell.into());
}
}
} else {
let log_level = if args.debug {
LevelFilter::Trace
} else {
LevelFilter::Off
};
pretty_env_logger::formatted_timed_builder()
.filter_level(log_level)
.init();
console_utils::set_term_colors(args.color_output);
let files_supported = are_input_files_supported(&args, &config);
if files_supported {
run_diff(args, config)?;
} else if let Some(cmd) = config.fallback_cmd {
info!("Input files are not supported but user has configured diff fallback");
diff_fallback(&cmd, &args.old.unwrap(), &args.new.unwrap())?;
} else {
anyhow::bail!("Unsupported file type with no fallback command specified.");
}
}
Ok(())
}