diff options
Diffstat (limited to 'src/main.rs')
-rw-r--r-- | src/main.rs | 593 |
1 files changed, 492 insertions, 101 deletions
diff --git a/src/main.rs b/src/main.rs index a1f12622..36239b67 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,115 +1,70 @@ +extern crate bitflags; + +#[macro_use] +extern crate error_chain; + mod align; mod ansi; +#[cfg(not(tarpaulin_include))] +mod bat_utils; mod cli; mod color; -mod colors; mod config; mod delta; +mod draw; mod edits; mod env; mod features; mod format; mod git_config; -mod handlers; -mod minusplus; +mod git_config_entry; mod options; mod paint; +mod parse; mod parse_style; -mod parse_styles; mod style; -mod utils; -mod wrapping; - -mod subcommands; - +mod syntect_color; mod tests; -use std::io::{self, ErrorKind, IsTerminal}; +use std::io::{self, ErrorKind, Read, Write}; +use std::path::PathBuf; use std::process; use bytelines::ByteLinesReader; +use itertools::Itertools; +use structopt::StructOpt; +use crate::bat_utils::assets::{list_languages, HighlightingAssets}; +use crate::bat_utils::output::{OutputType, PagingMode}; +use crate::config::delta_unreachable; use crate::delta::delta; -use crate::utils::bat::assets::list_languages; -use crate::utils::bat::output::OutputType; - -pub fn fatal<T>(errmsg: T) -> ! -where - T: AsRef<str> + std::fmt::Display, -{ - #[cfg(not(test))] - { - eprintln!("{errmsg}"); - // As in Config::error_exit_code: use 2 for error - // because diff uses 0 and 1 for non-error. - process::exit(2); - } - #[cfg(test)] - panic!("{}\n", errmsg); -} +use crate::options::theme::is_light_syntax_theme; pub mod errors { - pub use anyhow::{anyhow, Context, Error, Result}; + error_chain! { + foreign_links { + Io(::std::io::Error); + SyntectError(::syntect::LoadingError); + ParseIntError(::std::num::ParseIntError); + } + } } #[cfg(not(tarpaulin_include))] fn main() -> std::io::Result<()> { - // Do this first because both parsing all the input in `run_app()` and - // listing all processes takes about 50ms on Linux. - // It also improves the chance that the calling process is still around when - // input is piped into delta (e.g. `git show --word-diff=color | delta`). - utils::process::start_determining_calling_process_in_thread(); - - // Ignore ctrl-c (SIGINT) to avoid leaving an orphaned pager process. - // See https://github.com/dandavison/delta/issues/681 - ctrlc::set_handler(|| {}) - .unwrap_or_else(|err| eprintln!("Failed to set ctrl-c handler: {err}")); - let exit_code = run_app()?; - // when you call process::exit, no destructors are called, so we want to do it only once, here - process::exit(exit_code); -} + let assets = HighlightingAssets::new(); + let opt = cli::Opt::from_args_and_git_config(&mut git_config::GitConfig::try_create(), assets); -#[cfg(not(tarpaulin_include))] -// An Ok result contains the desired process exit code. Note that 1 is used to -// report that two files differ when delta is called with two positional -// arguments and without standard input; 2 is used to report a real problem. -fn run_app() -> std::io::Result<i32> { - let assets = utils::bat::assets::load_highlighting_assets(); - let env = env::DeltaEnv::init(); - let opt = cli::Opt::from_args_and_git_config( - env.clone(), - git_config::GitConfig::try_create(&env), - assets, - ); - - let subcommand_result = if opt.list_languages { - Some(list_languages()) + if opt.list_languages { + list_languages()?; + process::exit(0); } else if opt.list_syntax_themes { - Some(subcommands::list_syntax_themes::list_syntax_themes()) + list_syntax_themes()?; + process::exit(0); } else if opt.show_syntax_themes { - Some(subcommands::show_syntax_themes::show_syntax_themes()) - } else if opt.show_themes { - Some(subcommands::show_themes::show_themes( - opt.dark, - opt.light, - opt.computed.is_light_mode, - )) - } else if opt.show_colors { - Some(subcommands::show_colors::show_colors()) - } else if opt.parse_ansi { - Some(subcommands::parse_ansi::parse_ansi()) - } else { - None - }; - if let Some(result) = subcommand_result { - if let Err(error) = result { - match error.kind() { - ErrorKind::BrokenPipe => {} - _ => fatal(format!("{error}")), - } - } - return Ok(0); - }; + show_syntax_themes()?; + process::exit(0); + } let _show_config = opt.show_config; let config = config::Config::from(opt); @@ -117,34 +72,470 @@ fn run_app() -> std::io::Result<i32> { if _show_config { let stdout = io::stdout(); let mut stdout = stdout.lock(); - subcommands::show_config::show_config(&config, &mut stdout)?; - return Ok(0); + show_config(&config, &mut stdout)?; + process::exit(0); } - let mut output_type = - OutputType::from_mode(&env, config.paging_mode, config.pager.clone(), &config).unwrap(); + let mut output_type = OutputType::from_mode(config.paging_mode, None, &config).unwrap(); let mut writer = output_type.handle().unwrap(); - if let (Some(minus_file), Some(plus_file)) = (&config.minus_file, &config.plus_file) { - let exit_code = subcommands::diff::diff(minus_file, plus_file, &config, &mut writer); - return Ok(exit_code); - } - - if io::stdin().is_terminal() { - eprintln!( - "\ - The main way to use delta is to configure it as the pager for git: \ - see https://github.com/dandavison/delta#get-started. \ - You can also use delta to diff two files: `delta file_A file_B`." - ); - return Ok(config.error_exit_code); + if atty::is(atty::Stream::Stdin) { + process::exit(diff( + config.minus_file.as_ref(), + config.plus_file.as_ref(), + &config, + &mut writer, + )); } if let Err(error) = delta(io::stdin().lock().byte_lines(), &mut writer, &config) { match error.kind() { - ErrorKind::BrokenPipe => return Ok(0), - _ => eprintln!("{error}"), + ErrorKind::BrokenPipe => process::exit(0), + _ => eprintln!("{}", error), + } + }; + Ok(()) +} + +/// Run `diff -u` on the files provided on the command line and display the output. +fn diff( + minus_file: Option<&PathBuf>, + plus_file: Option<&PathBuf>, + config: &config::Config, + writer: &mut dyn Write, +) -> i32 { + use std::io::BufReader; + let die = || { + eprintln!("Usage: delta minus_file plus_file"); + process::exit(config.error_exit_code); + }; + let diff_command = "diff"; + let mut diff_process = process::Command::new(PathBuf::from(diff_command)) + .arg("-u") + .args(&[ + minus_file.unwrap_or_else(die), + plus_file.unwrap_or_else(die), + ]) + .stdout(process::Stdio::piped()) + .spawn() + .unwrap_or_else(|err| { + eprintln!("Failed to execute the command '{}': {}", diff_command, err); + process::exit(config.error_exit_code); + }); + let exit_code = diff_process + .wait() + .unwrap_or_else(|_| { + delta_unreachable(&format!("'{}' process not running.", diff_command)); + }) + .code() + .unwrap_or_else(|| { + eprintln!("'{}' process terminated without exit status.", diff_command); + process::exit(config.error_exit_code); + }); + + if let Err(error) = delta( + BufReader::new(diff_process.stdout.unwrap()).byte_lines(), + writer, + &config, + ) { + match error.kind() { + ErrorKind::BrokenPipe => process::exit(0), + _ => { + eprintln!("{}", error); + process::exit(config.error_exit_code); + } + } + }; + exit_code +} + +fn show_config(config: &config::Config, writer: &mut dyn Write) -> std::io::Result<()> { + // styles first + writeln!( + writer, + " commit-style = {commit_style} + file-style = {file_style} + hunk-header-style = {hunk_header_style} + minus-style = {minus_style} + minus-non-emph-style = {minus_non_emph_style} + minus-emph-style = {minus_emph_style} + minus-empty-line-marker-style = {minus_empty_line_marker_style} + zero-style = {zero_style} + plus-style = {plus_style} + plus-non-emph-style = {plus_non_emph_style} + plus-emph-style = {plus_emph_style} + plus-empty-line-marker-style = {plus_empty_line_marker_style} + whitespace-error-style = {whitespace_error_style}", + commit_style = config.commit_style.to_painted_string(), + file_style = config.file_style.to_painted_string(), + hunk_header_style = config.hunk_header_style.to_painted_string(), + minus_emph_style = config.minus_emph_style.to_painted_string(), + minus_empty_line_marker_style = config.minus_empty_line_marker_style.to_painted_string(), + minus_non_emph_style = config.minus_non_emph_style.to_painted_string(), + minus_style = config.minus_style.to_painted_string(), + plus_emph_style = config.plus_emph_style.to_painted_string(), + plus_empty_line_marker_style = config.plus_empty_line_marker_style.to_painted_string(), + plus_non_emph_style = config.plus_non_emph_style.to_painted_string(), + plus_style = config.plus_style.to_painted_string(), + whitespace_error_style = config.whitespace_error_style.to_painted_string(), + zero_style = config.zero_style.to_painted_string(), + )?; + // Everything else + writeln!( + writer, + " 24-bit-color = {true_color} + file-added-label = {file_added_label} + file-modified-label = {file_modified_label} + file-removed-label = {file_removed_label} + file-renamed-label = {file_renamed_label}", + true_color = config.true_color, + file_added_label = format_option_value(&config.file_added_label), + file_modified_label = format_option_value(&config.file_modified_label), + file_removed_label = format_option_value(&config.file_removed_label), + file_renamed_label = format_option_value(&config.file_renamed_label), + )?; + writeln!( + writer, + " hyperlinks = {hyperlinks}", + hyperlinks = config.hyperlinks + )?; + if config.hyperlinks { + writeln!( + writer, + " hyperlinks-file-link-format = {hyperlinks_file_link_format}", + hyperlinks_file_link_format = format_option_value(&config.hyperlinks_file_link_format), + )? + } + writeln!( + writer, + " inspect-raw-lines = {inspect_raw_lines} + keep-plus-minus-markers = {keep_plus_minus_markers}", + inspect_raw_lines = match config.inspect_raw_lines { + cli::InspectRawLines::True => "true", + cli::InspectRawLines::False => "false", + }, + keep_plus_minus_markers = config.keep_plus_minus_markers, + )?; + writeln!( + writer, + " line-numbers = {line_numbers}", + line_numbers = config.line_numbers + )?; + if config.line_numbers { + writeln!( + writer, + " line-numbers-minus-style = {line_numbers_minus_style} + line-numbers-zero-style = {line_numbers_zero_style} + line-numbers-plus-style = {line_numbers_plus_style} + line-numbers-left-style = {line_numbers_left_style} + line-numbers-right-style = {line_numbers_right_style} + line-numbers-left-format = {line_numbers_left_format} + line-numbers-right-format = {line_numbers_right_format}", + line_numbers_minus_style = config.line_numbers_minus_style.to_painted_string(), + line_numbers_zero_style = config.line_numbers_zero_style.to_painted_string(), + line_numbers_plus_style = config.line_numbers_plus_style.to_painted_string(), + line_numbers_left_style = config.line_numbers_left_style.to_painted_string(), + line_numbers_right_style = config.line_numbers_right_style.to_painted_string(), + line_numbers_left_format = format_option_value(&config.line_numbers_left_format), + line_numbers_right_format = format_option_value(&config.line_numbers_right_format), + )? + } + writeln!( + writer, + " max-line-distance = {max_line_distance} + max-line-length = {max_line_length} + navigate = {navigate} + paging = {paging_mode} + side-by-side = {side_by_side} + syntax-theme = {syntax_theme} + width = {width} + tabs = {tab_width} + word-diff-regex = {tokenization_regex}", + max_line_distance = config.max_line_distance, + max_line_length = config.max_line_length, + navigate = config.navigate, + paging_mode = match config.paging_mode { + PagingMode::Always => "always", + PagingMode::Never => "never", + PagingMode::QuitIfOneScreen => "auto", + }, + side_by_side = config.side_by_side, + syntax_theme = config + .syntax_theme + .clone() + .map(|t| t.name.unwrap_or_else(|| "none".to_string())) + .unwrap_or_else(|| "none".to_string()), + width = match config.decorations_width { + cli::Width::Fixed(width) => width.to_string(), + cli::Width::Variable => "variable".to_string(), + }, + tab_width = config.tab_width, + tokenization_regex = format_option_value(&config.tokenization_regex.to_string()), + )?; + Ok(()) +} + +// Heuristics determining whether to quote string option values when printing values intended for +// git config. +fn format_option_value<S>(s: S) -> String +where + S: AsRef<str>, +{ + let s = s.as_ref(); + if s.ends_with(' ') + || s.starts_with(' ') + || s.contains(&['\\', '{', '}', ':'][..]) + || s.is_empty() + { + format!("'{}'", s) + } else { + s.to_string() + } +} + +#[cfg(not(tarpaulin_include))] +fn show_syntax_themes() -> std::io::Result<()> { + let mut opt = cli::Opt::from_args(); + let assets = HighlightingAssets::new(); + let mut output_type = OutputType::from_mode( + PagingMode::QuitIfOneScreen, + None, + &config::Config::from(cli::Opt::default()), + ) + .unwrap(); + let mut writer = output_type.handle().unwrap(); + opt.computed.syntax_set = assets.syntax_set; + + if !(opt.dark || opt.light) { + _show_syntax_themes(opt.clone(), false, &mut writer)?; + _show_syntax_themes(opt, true, &mut writer)?; + } else if opt.light { + _show_syntax_themes(opt, true, &mut writer)?; + } else { + _show_syntax_themes(opt, false, &mut writer)? + }; + Ok(()) +} + +fn _show_syntax_themes( + mut opt: cli::Opt, + is_light_mode: bool, + writer: &mut dyn Write, +) -> std::io::Result<()> { + use bytelines::ByteLines; + use std::io::BufReader; + let mut input = b"\ +diff --git a/example.rs b/example.rs +index f38589a..0f1bb83 100644 +--- a/example.rs ++++ b/example.rs +@@ -1,5 +1,5 @@ +-// Output the square of a number. +-fn print_square(num: f64) { +- let result = f64::powf(num, 2.0); +- println!(\"The square of {:.2} is {:.2}.\", num, result); ++// Output the cube of a number. ++fn print_cube(num: f64) { ++ let result = f64::powf(num, 3.0); ++ println!(\"The cube of {:.2} is {:.2}.\", num, result); +" + .to_vec(); + if !atty::is(atty::Stream::Stdin) { + let mut buf = Vec::new(); + io::stdin().lock().read_to_end(&mut buf)?; + if !buf.is_empty() { + input = buf; } }; - Ok(0) + + opt.computed.is_light_mode = is_light_mode; + let mut config = config::Config::from(opt); + let title_style = ansi_term::Style::new().bold(); + let assets = HighlightingAssets::new(); + + for syntax_theme in assets + .theme_set + .themes + .iter() + .filter(|(t, _)| is_light_syntax_theme(t) == is_light_mode) + .map(|(t, _)| t) + { + writeln!(writer, "\n\nTheme: {}\n", title_style.paint(syntax_theme))?; + config.syntax_theme = Some(assets.theme_set.themes[syntax_theme.as_str()].clone()); + if let Err(error) = delta(ByteLines::new(BufReader::new(&input[0..])), writer, &config) { + match error.kind() { + ErrorKind::BrokenPipe => process::exit(0), + _ => eprintln!("{}", error), + } + }; + } + Ok(()) +} + +#[cfg(not(tarpaulin_include))] +pub fn list_syntax_themes() -> std::io::Result<()> { + let stdout = io::stdout(); + let mut stdout = stdout.lock(); + if atty::is(atty::Stream::Stdout) { + _list_syntax_themes_for_humans(&mut stdout) + } else { + _list_syntax_themes_for_machines(&mut stdout) + } +} + +pub fn _list_syntax_themes_for_humans(writer: &mut dyn Write) -> std::io::Result<()> { + let assets = HighlightingAssets::new(); + let themes = &assets.theme_set.themes; + + writeln!(writer, "Light themes:")?; + for (theme, _) in themes.iter().filter(|(t, _)| is_light_syntax_theme(*t)) { + writeln!(writer, " {}", theme)?; + } + writeln!(writer, "\nDark themes:")?; + for (theme, _) in themes.iter().filter(|(t, _)| !is_light_syntax_theme(*t)) { + writeln!(writer, " {}", theme)?; + } + writeln!( + writer, + "\nUse delta --show-syntax-themes to demo the themes." + )?; + Ok(()) +} + +pub fn _list_syntax_themes_for_machines(writer: &mut dyn Write) -> std::io::Result<()> { + let assets = HighlightingAssets::new(); + let themes = &assets.theme_set.themes; + for (theme, _) in themes + .iter() + .sorted_by_key(|(t, _)| is_light_syntax_theme(*t)) + { + writeln!( + writer, + "{}\t{}", + if is_light_syntax_theme(theme) { + "light" + } else { + "dark" + }, + theme + )?; + } + Ok(()) +} + +#[cfg(test)] +mod main_tests { + use super::*; + use std::io::{Cursor, Seek, SeekFrom}; + + use crate::ansi; + use crate::tests::integration_test_utils::integration_test_utils; + + #[test] + fn test_show_config() { + let config = integration_test_utils::make_config_from_args(&[]); + let mut writer = Cursor::new(vec![0; 1024]); + show_config(&config, &mut writer).unwrap(); + let mut s = String::new(); + writer.seek(SeekFrom::Start(0)).unwrap(); + writer.read_to_string(&mut s).unwrap(); + let s = ansi::strip_ansi_codes(&s); + assert!(s.contains(" commit-style = raw\n")); + assert!(s.contains(r" word-diff-regex = '\w+'")); + } + + #[test] + #[ignore] // Not working (timing out) when run by tarpaulin, presumably due to stdin detection. + fn test_show_syntax_themes() { + let opt = integration_test_utils::make_options_from_args(&[]); + + let mut writer = Cursor::new(vec![0; 1024]); + _show_syntax_themes(opt, true, &mut writer).unwrap(); + let mut s = String::new(); + writer.seek(SeekFrom::Start(0)).unwrap(); + writer.read_to_string(&mut s).unwrap(); + let s = ansi::strip_ansi_codes(&s); + assert!(s.contains("\nTheme: gruvbox-white\n")); + println!("{}", s); + assert!(s.contains("\nfn print_cube(num: f64) {\n")); + } + + #[test] + fn test_list_syntax_themes_for_humans() { + let mut writer = Cursor::new(vec![0; 512]); + _list_syntax_themes_for_humans(&mut writer).unwrap(); + let mut s = String::new(); + writer.seek(SeekFrom::Start(0)).unwrap(); + writer.read_to_string(&mut s).unwrap(); + assert!(s.contains("Light themes:\n")); + assert!(s.contains(" GitHub\n")); + assert!(s.contains("Dark themes:\n")); + assert!(s.contains(" Dracula\n")); + } + + #[test] + fn test_list_syntax_themes_for_machines() { + let mut writer = Cursor::new(vec![0; 512]); + _list_syntax_themes_for_machines(&mut writer).unwrap(); + let mut s = String::new(); + writer.seek(SeekFrom::Start(0)).unwrap(); + writer.read_to_string(&mut s).unwrap(); + assert!(s.contains("light GitHub\n")); + assert!(s.contains("dark Dracula\n")); + } + + #[test] + #[cfg_attr(target_os = "windows", ignore)] + fn test_diff_same_empty_file() { + let config = integration_test_utils::make_config_from_args(&[]); + let mut writer = Cursor::new(vec![]); + let exit_code = diff( + Some(&PathBuf::from("/dev/null")), + Some(&PathBuf::from("/dev/null")), + &config, + &mut writer, + ); + assert_eq!(exit_code, 0); + let mut s = String::new(); + writer.seek(SeekFrom::Start(0)).unwrap(); + writer.read_to_string(&mut s).unwrap(); + assert!(s.is_empty()); + } + + #[test] + #[cfg_attr(target_os = "windows", ignore)] + fn test_diff_same_non_empty_file() { + let config = integration_test_utils::make_config_from_args(&[]); + let mut writer = Cursor::new(vec![]); + let exit_code = diff( + Some(&PathBuf::from("/etc/passwd")), + Some(&PathBuf::from("/etc/passwd")), + &config, + &mut writer, + ); + assert_eq!(exit_code, 0); + let mut s = String::new(); + writer.seek(SeekFrom::Start(0)).unwrap(); + writer.read_to_string(&mut s).unwrap(); + assert!(s.is_empty()); + } + + #[test] + #[cfg_attr(target_os = "windows", ignore)] + fn test_diff_differing_files() { + let config = integration_test_utils::make_config_from_args(&[]); + let mut writer = Cursor::new(vec![]); + let exit_code = diff( + Some(&PathBuf::from("/dev/null")), + Some(&PathBuf::from("/etc/passwd")), + &config, + &mut writer, + ); + assert_eq!(exit_code, 1); + let mut s = String::new(); + writer.seek(SeekFrom::Start(0)).unwrap(); + writer.read_to_string(&mut s).unwrap(); + let s = ansi::strip_ansi_codes(&s); + assert!(s.contains("comparing: /dev/null ⟶ /etc/passwd\n")); + } } |