diff --git a/tests/syntax-tests/highlighted/Diff/82e7786e747b1fcfac1f963ae6415a22ec9caae1.diff b/tests/syntax-tests/highlighted/Diff/82e7786e747b1fcfac1f963ae6415a22ec9caae1.diff new file mode 100644 index 00000000..39f95b74 --- /dev/null +++ b/tests/syntax-tests/highlighted/Diff/82e7786e747b1fcfac1f963ae6415a22ec9caae1.diff @@ -0,0 +1,353 @@ +diff --git a/CHANGELOG.md b/CHANGELOG.md +index ced88213..973eba9a 100644 +--- a/CHANGELOG.md ++++ b/CHANGELOG.md +@@ -2,6 +2,11 @@ +  +  + ## Features ++ ++- Add a new `--diff` option that can be used to only show lines surrounding ++ Git changes, i.e. added, removed or modified lines. The amount of additional ++ context can be controlled with `--diff-context=N`. See #23 and #940 ++ + ## Bugfixes + ## Other + ## `bat` as a library +diff --git a/src/bin/bat/app.rs b/src/bin/bat/app.rs +index e5221455..f0f519ea 100644 +--- a/src/bin/bat/app.rs ++++ b/src/bin/bat/app.rs +@@ -15,7 +15,7 @@ use console::Term; +  + use bat::{ + assets::HighlightingAssets, +- config::Config, ++ config::{Config, VisibleLines}, + error::*, + input::Input, + line_range::{HighlightedLineRanges, LineRange, LineRanges}, +@@ -196,13 +196,23 @@ impl App { + } + }) + .unwrap_or_else(|| String::from(HighlightingAssets::default_theme())), +- line_ranges: self +- .matches +- .values_of("line-range") +- .map(|vs| vs.map(LineRange::from).collect()) +- .transpose()? +- .map(LineRanges::from) +- .unwrap_or_default(), ++ visible_lines: if self.matches.is_present("diff") { ++ VisibleLines::DiffContext( ++ self.matches ++ .value_of("diff-context") ++ .and_then(|t| t.parse().ok()) ++ .unwrap_or(2), ++ ) ++ } else { ++ VisibleLines::Ranges( ++ self.matches ++ .values_of("line-range") ++ .map(|vs| vs.map(LineRange::from).collect()) ++ .transpose()? ++ .map(LineRanges::from) ++ .unwrap_or_default(), ++ ) ++ }, + style_components, + syntax_mapping, + pager: self.matches.value_of("pager"), +diff --git a/src/bin/bat/clap_app.rs b/src/bin/bat/clap_app.rs +index c7344991..85edefde 100644 +--- a/src/bin/bat/clap_app.rs ++++ b/src/bin/bat/clap_app.rs +@@ -105,6 +105,34 @@ pub fn build_app(interactive_output: bool) -> ClapApp<'static, 'static> { + data to bat from STDIN when bat does not otherwise know \ + the filename."), + ) ++ .arg( ++ Arg::with_name("diff") ++ .long("diff") ++ .help("Only show lines that have been added/removed/modified.") ++ .long_help( ++ "Only show lines that have been added/removed/modified with respect \ ++ to the Git index. Use --diff-context=N to control how much context you want to see.", ++ ), ++ ) ++ .arg( ++ Arg::with_name("diff-context") ++ .long("diff-context") ++ .overrides_with("diff-context") ++ .takes_value(true) ++ .value_name("N") ++ .validator( ++ |n| { ++ n.parse::() ++ .map_err(|_| "must be a number") ++ .map(|_| ()) // Convert to Result<(), &str> ++ .map_err(|e| e.to_string()) ++ }, // Convert to Result<(), String> ++ ) ++ .hidden_short_help(true) ++ .long_help( ++ "Include N lines of context around added/removed/modified lines when using '--diff'.", ++ ), ++ ) + .arg( + Arg::with_name("tabs") + .long("tabs") +@@ -339,6 +367,7 @@ pub fn build_app(interactive_output: bool) -> ClapApp<'static, 'static> { + .takes_value(true) + .number_of_values(1) + .value_name("N:M") ++ .conflicts_with("diff") + .help("Only print the lines from N to M.") + .long_help( + "Only print the specified range of lines for each file. \ +diff --git a/src/config.rs b/src/config.rs +index d5df9910..3c24b77f 100644 +--- a/src/config.rs ++++ b/src/config.rs +@@ -5,6 +5,32 @@ use crate::style::StyleComponents; + use crate::syntax_mapping::SyntaxMapping; + use crate::wrapping::WrappingMode; +  ++#[derive(Debug, Clone)] ++pub enum VisibleLines { ++ /// Show all lines which are included in the line ranges ++ Ranges(LineRanges), ++ ++ #[cfg(feature = "git")] ++ /// Only show lines surrounding added/deleted/modified lines ++ DiffContext(usize), ++} ++ ++impl VisibleLines { ++ pub fn diff_context(&self) -> bool { ++ match self { ++ Self::Ranges(_) => false, ++ #[cfg(feature = "git")] ++ Self::DiffContext(_) => true, ++ } ++ } ++} ++ ++impl Default for VisibleLines { ++ fn default() -> Self { ++ VisibleLines::Ranges(LineRanges::default()) ++ } ++} ++ + #[derive(Debug, Clone, Default)] + pub struct Config<'a> { + /// The explicitly configured language, if any +@@ -39,8 +65,8 @@ pub struct Config<'a> { + #[cfg(feature = "paging")] + pub paging_mode: PagingMode, +  +- /// Specifies the lines that should be printed +- pub line_ranges: LineRanges, ++ /// Specifies which lines should be printed ++ pub visible_lines: VisibleLines, +  + /// The syntax highlighting theme + pub theme: String, +@@ -62,10 +88,7 @@ pub struct Config<'a> { + fn default_config_should_include_all_lines() { + use crate::line_range::RangeCheckResult; +  +- assert_eq!( +- Config::default().line_ranges.check(17), +- RangeCheckResult::InRange +- ); ++ assert_eq!(LineRanges::default().check(17), RangeCheckResult::InRange); + } +  + #[test] +diff --git a/src/controller.rs b/src/controller.rs +index 09c6ec2a..f636d5fd 100644 +--- a/src/controller.rs ++++ b/src/controller.rs +@@ -1,9 +1,13 @@ + use std::io::{self, Write}; +  + use crate::assets::HighlightingAssets; +-use crate::config::Config; ++use crate::config::{Config, VisibleLines}; ++#[cfg(feature = "git")] ++use crate::diff::{get_git_diff, LineChanges}; + use crate::error::*; + use crate::input::{Input, InputReader, OpenedInput}; ++#[cfg(feature = "git")] ++use crate::line_range::LineRange; + use crate::line_range::{LineRanges, RangeCheckResult}; + use crate::output::OutputType; + #[cfg(feature = "paging")] +@@ -68,6 +72,32 @@ impl<'b> Controller<'b> { + no_errors = false; + } + Ok(mut opened_input) => { ++ #[cfg(feature = "git")] ++ let line_changes = if self.config.visible_lines.diff_context() ++ || (!self.config.loop_through && self.config.style_components.changes()) ++ { ++ if let crate::input::OpenedInputKind::OrdinaryFile(ref path) = ++ opened_input.kind ++ { ++ let diff = get_git_diff(path); ++ ++ if self.config.visible_lines.diff_context() ++ && diff ++ .as_ref() ++ .map(|changes| changes.is_empty()) ++ .unwrap_or(false) ++ { ++ continue; ++ } ++ ++ diff ++ } else { ++ None ++ } ++ } else { ++ None ++ }; ++ + let mut printer: Box = if self.config.loop_through { + Box::new(SimplePrinter::new()) + } else { +@@ -75,10 +105,18 @@ impl<'b> Controller<'b> { + &self.config, + &self.assets, + &mut opened_input, ++ #[cfg(feature = "git")] ++ &line_changes, + )) + }; +  +- let result = self.print_file(&mut *printer, writer, &mut opened_input); ++ let result = self.print_file( ++ &mut *printer, ++ writer, ++ &mut opened_input, ++ #[cfg(feature = "git")] ++ &line_changes, ++ ); +  + if let Err(error) = result { + handle_error(&error); +@@ -96,13 +134,31 @@ impl<'b> Controller<'b> { + printer: &mut dyn Printer, + writer: &mut dyn Write, + input: &mut OpenedInput, ++ #[cfg(feature = "git")] line_changes: &Option, + ) -> Result<()> { + if !input.reader.first_line.is_empty() || self.config.style_components.header() { + printer.print_header(writer, input)?; + } +  + if !input.reader.first_line.is_empty() { +- self.print_file_ranges(printer, writer, &mut input.reader, &self.config.line_ranges)?; ++ let line_ranges = match self.config.visible_lines { ++ VisibleLines::Ranges(ref line_ranges) => line_ranges.clone(), ++ #[cfg(feature = "git")] ++ VisibleLines::DiffContext(context) => { ++ let mut line_ranges: Vec = vec![]; ++ ++ if let Some(line_changes) = line_changes { ++ for line in line_changes.keys() { ++ let line = *line as usize; ++ line_ranges.push(LineRange::new(line - context, line + context)); ++ } ++ } ++ ++ LineRanges::from(line_ranges) ++ } ++ }; ++ ++ self.print_file_ranges(printer, writer, &mut input.reader, &line_ranges)?; + } + printer.print_footer(writer, input)?; +  +diff --git a/src/pretty_printer.rs b/src/pretty_printer.rs +index 0c78ea90..13bd5dbc 100644 +--- a/src/pretty_printer.rs ++++ b/src/pretty_printer.rs +@@ -6,7 +6,7 @@ use syntect::parsing::SyntaxReference; +  + use crate::{ + assets::HighlightingAssets, +- config::Config, ++ config::{Config, VisibleLines}, + controller::Controller, + error::Result, + input::Input, +@@ -205,7 +205,7 @@ impl<'a> PrettyPrinter<'a> { +  + /// Specify the lines that should be printed (default: all) + pub fn line_ranges(&mut self, ranges: LineRanges) -> &mut Self { +- self.config.line_ranges = ranges; ++ self.config.visible_lines = VisibleLines::Ranges(ranges); + self + } +  +diff --git a/src/printer.rs b/src/printer.rs +index 5eed437e..2b245cfd 100644 +--- a/src/printer.rs ++++ b/src/printer.rs +@@ -24,7 +24,7 @@ use crate::config::Config; + use crate::decorations::LineChangesDecoration; + use crate::decorations::{Decoration, GridBorderDecoration, LineNumberDecoration}; + #[cfg(feature = "git")] +-use crate::diff::{get_git_diff, LineChanges}; ++use crate::diff::LineChanges; + use crate::error::*; + use crate::input::OpenedInput; + use crate::line_range::RangeCheckResult; +@@ -90,7 +90,7 @@ pub(crate) struct InteractivePrinter<'a> { + ansi_prefix_sgr: String, + content_type: Option, + #[cfg(feature = "git")] +- pub line_changes: Option, ++ pub line_changes: &'a Option, + highlighter: Option>, + syntax_set: &'a SyntaxSet, + background_color_highlight: Option, +@@ -101,6 +101,7 @@ impl<'a> InteractivePrinter<'a> { + config: &'a Config, + assets: &'a HighlightingAssets, + input: &mut OpenedInput, ++ #[cfg(feature = "git")] line_changes: &'a Option, + ) -> Self { + let theme = assets.get_theme(&config.theme); +  +@@ -145,9 +146,6 @@ impl<'a> InteractivePrinter<'a> { + panel_width = 0; + } +  +- #[cfg(feature = "git")] +- let mut line_changes = None; +- + let highlighter = if input + .reader + .content_type +@@ -155,18 +153,6 @@ impl<'a> InteractivePrinter<'a> { + { + None + } else { +- // Get the Git modifications +- #[cfg(feature = "git")] +- { +- use crate::input::OpenedInputKind; +- +- if config.style_components.changes() { +- if let OpenedInputKind::OrdinaryFile(ref path) = input.kind { +- line_changes = get_git_diff(path); +- } +- } +- } +- + // Determine the type of syntax for highlighting + let syntax = assets.get_syntax(config.language, input, &config.syntax_mapping); + Some(HighlightLines::new(syntax, theme)) diff --git a/tests/syntax-tests/source/Diff/82e7786e747b1fcfac1f963ae6415a22ec9caae1.diff b/tests/syntax-tests/source/Diff/82e7786e747b1fcfac1f963ae6415a22ec9caae1.diff new file mode 100644 index 00000000..ebbee60c --- /dev/null +++ b/tests/syntax-tests/source/Diff/82e7786e747b1fcfac1f963ae6415a22ec9caae1.diff @@ -0,0 +1,353 @@ +diff --git a/CHANGELOG.md b/CHANGELOG.md +index ced88213..973eba9a 100644 +--- a/CHANGELOG.md ++++ b/CHANGELOG.md +@@ -2,6 +2,11 @@ + + + ## Features ++ ++- Add a new `--diff` option that can be used to only show lines surrounding ++ Git changes, i.e. added, removed or modified lines. The amount of additional ++ context can be controlled with `--diff-context=N`. See #23 and #940 ++ + ## Bugfixes + ## Other + ## `bat` as a library +diff --git a/src/bin/bat/app.rs b/src/bin/bat/app.rs +index e5221455..f0f519ea 100644 +--- a/src/bin/bat/app.rs ++++ b/src/bin/bat/app.rs +@@ -15,7 +15,7 @@ use console::Term; + + use bat::{ + assets::HighlightingAssets, +- config::Config, ++ config::{Config, VisibleLines}, + error::*, + input::Input, + line_range::{HighlightedLineRanges, LineRange, LineRanges}, +@@ -196,13 +196,23 @@ impl App { + } + }) + .unwrap_or_else(|| String::from(HighlightingAssets::default_theme())), +- line_ranges: self +- .matches +- .values_of("line-range") +- .map(|vs| vs.map(LineRange::from).collect()) +- .transpose()? +- .map(LineRanges::from) +- .unwrap_or_default(), ++ visible_lines: if self.matches.is_present("diff") { ++ VisibleLines::DiffContext( ++ self.matches ++ .value_of("diff-context") ++ .and_then(|t| t.parse().ok()) ++ .unwrap_or(2), ++ ) ++ } else { ++ VisibleLines::Ranges( ++ self.matches ++ .values_of("line-range") ++ .map(|vs| vs.map(LineRange::from).collect()) ++ .transpose()? ++ .map(LineRanges::from) ++ .unwrap_or_default(), ++ ) ++ }, + style_components, + syntax_mapping, + pager: self.matches.value_of("pager"), +diff --git a/src/bin/bat/clap_app.rs b/src/bin/bat/clap_app.rs +index c7344991..85edefde 100644 +--- a/src/bin/bat/clap_app.rs ++++ b/src/bin/bat/clap_app.rs +@@ -105,6 +105,34 @@ pub fn build_app(interactive_output: bool) -> ClapApp<'static, 'static> { + data to bat from STDIN when bat does not otherwise know \ + the filename."), + ) ++ .arg( ++ Arg::with_name("diff") ++ .long("diff") ++ .help("Only show lines that have been added/removed/modified.") ++ .long_help( ++ "Only show lines that have been added/removed/modified with respect \ ++ to the Git index. Use --diff-context=N to control how much context you want to see.", ++ ), ++ ) ++ .arg( ++ Arg::with_name("diff-context") ++ .long("diff-context") ++ .overrides_with("diff-context") ++ .takes_value(true) ++ .value_name("N") ++ .validator( ++ |n| { ++ n.parse::() ++ .map_err(|_| "must be a number") ++ .map(|_| ()) // Convert to Result<(), &str> ++ .map_err(|e| e.to_string()) ++ }, // Convert to Result<(), String> ++ ) ++ .hidden_short_help(true) ++ .long_help( ++ "Include N lines of context around added/removed/modified lines when using '--diff'.", ++ ), ++ ) + .arg( + Arg::with_name("tabs") + .long("tabs") +@@ -339,6 +367,7 @@ pub fn build_app(interactive_output: bool) -> ClapApp<'static, 'static> { + .takes_value(true) + .number_of_values(1) + .value_name("N:M") ++ .conflicts_with("diff") + .help("Only print the lines from N to M.") + .long_help( + "Only print the specified range of lines for each file. \ +diff --git a/src/config.rs b/src/config.rs +index d5df9910..3c24b77f 100644 +--- a/src/config.rs ++++ b/src/config.rs +@@ -5,6 +5,32 @@ use crate::style::StyleComponents; + use crate::syntax_mapping::SyntaxMapping; + use crate::wrapping::WrappingMode; + ++#[derive(Debug, Clone)] ++pub enum VisibleLines { ++ /// Show all lines which are included in the line ranges ++ Ranges(LineRanges), ++ ++ #[cfg(feature = "git")] ++ /// Only show lines surrounding added/deleted/modified lines ++ DiffContext(usize), ++} ++ ++impl VisibleLines { ++ pub fn diff_context(&self) -> bool { ++ match self { ++ Self::Ranges(_) => false, ++ #[cfg(feature = "git")] ++ Self::DiffContext(_) => true, ++ } ++ } ++} ++ ++impl Default for VisibleLines { ++ fn default() -> Self { ++ VisibleLines::Ranges(LineRanges::default()) ++ } ++} ++ + #[derive(Debug, Clone, Default)] + pub struct Config<'a> { + /// The explicitly configured language, if any +@@ -39,8 +65,8 @@ pub struct Config<'a> { + #[cfg(feature = "paging")] + pub paging_mode: PagingMode, + +- /// Specifies the lines that should be printed +- pub line_ranges: LineRanges, ++ /// Specifies which lines should be printed ++ pub visible_lines: VisibleLines, + + /// The syntax highlighting theme + pub theme: String, +@@ -62,10 +88,7 @@ pub struct Config<'a> { + fn default_config_should_include_all_lines() { + use crate::line_range::RangeCheckResult; + +- assert_eq!( +- Config::default().line_ranges.check(17), +- RangeCheckResult::InRange +- ); ++ assert_eq!(LineRanges::default().check(17), RangeCheckResult::InRange); + } + + #[test] +diff --git a/src/controller.rs b/src/controller.rs +index 09c6ec2a..f636d5fd 100644 +--- a/src/controller.rs ++++ b/src/controller.rs +@@ -1,9 +1,13 @@ + use std::io::{self, Write}; + + use crate::assets::HighlightingAssets; +-use crate::config::Config; ++use crate::config::{Config, VisibleLines}; ++#[cfg(feature = "git")] ++use crate::diff::{get_git_diff, LineChanges}; + use crate::error::*; + use crate::input::{Input, InputReader, OpenedInput}; ++#[cfg(feature = "git")] ++use crate::line_range::LineRange; + use crate::line_range::{LineRanges, RangeCheckResult}; + use crate::output::OutputType; + #[cfg(feature = "paging")] +@@ -68,6 +72,32 @@ impl<'b> Controller<'b> { + no_errors = false; + } + Ok(mut opened_input) => { ++ #[cfg(feature = "git")] ++ let line_changes = if self.config.visible_lines.diff_context() ++ || (!self.config.loop_through && self.config.style_components.changes()) ++ { ++ if let crate::input::OpenedInputKind::OrdinaryFile(ref path) = ++ opened_input.kind ++ { ++ let diff = get_git_diff(path); ++ ++ if self.config.visible_lines.diff_context() ++ && diff ++ .as_ref() ++ .map(|changes| changes.is_empty()) ++ .unwrap_or(false) ++ { ++ continue; ++ } ++ ++ diff ++ } else { ++ None ++ } ++ } else { ++ None ++ }; ++ + let mut printer: Box = if self.config.loop_through { + Box::new(SimplePrinter::new()) + } else { +@@ -75,10 +105,18 @@ impl<'b> Controller<'b> { + &self.config, + &self.assets, + &mut opened_input, ++ #[cfg(feature = "git")] ++ &line_changes, + )) + }; + +- let result = self.print_file(&mut *printer, writer, &mut opened_input); ++ let result = self.print_file( ++ &mut *printer, ++ writer, ++ &mut opened_input, ++ #[cfg(feature = "git")] ++ &line_changes, ++ ); + + if let Err(error) = result { + handle_error(&error); +@@ -96,13 +134,31 @@ impl<'b> Controller<'b> { + printer: &mut dyn Printer, + writer: &mut dyn Write, + input: &mut OpenedInput, ++ #[cfg(feature = "git")] line_changes: &Option, + ) -> Result<()> { + if !input.reader.first_line.is_empty() || self.config.style_components.header() { + printer.print_header(writer, input)?; + } + + if !input.reader.first_line.is_empty() { +- self.print_file_ranges(printer, writer, &mut input.reader, &self.config.line_ranges)?; ++ let line_ranges = match self.config.visible_lines { ++ VisibleLines::Ranges(ref line_ranges) => line_ranges.clone(), ++ #[cfg(feature = "git")] ++ VisibleLines::DiffContext(context) => { ++ let mut line_ranges: Vec = vec![]; ++ ++ if let Some(line_changes) = line_changes { ++ for line in line_changes.keys() { ++ let line = *line as usize; ++ line_ranges.push(LineRange::new(line - context, line + context)); ++ } ++ } ++ ++ LineRanges::from(line_ranges) ++ } ++ }; ++ ++ self.print_file_ranges(printer, writer, &mut input.reader, &line_ranges)?; + } + printer.print_footer(writer, input)?; + +diff --git a/src/pretty_printer.rs b/src/pretty_printer.rs +index 0c78ea90..13bd5dbc 100644 +--- a/src/pretty_printer.rs ++++ b/src/pretty_printer.rs +@@ -6,7 +6,7 @@ use syntect::parsing::SyntaxReference; + + use crate::{ + assets::HighlightingAssets, +- config::Config, ++ config::{Config, VisibleLines}, + controller::Controller, + error::Result, + input::Input, +@@ -205,7 +205,7 @@ impl<'a> PrettyPrinter<'a> { + + /// Specify the lines that should be printed (default: all) + pub fn line_ranges(&mut self, ranges: LineRanges) -> &mut Self { +- self.config.line_ranges = ranges; ++ self.config.visible_lines = VisibleLines::Ranges(ranges); + self + } + +diff --git a/src/printer.rs b/src/printer.rs +index 5eed437e..2b245cfd 100644 +--- a/src/printer.rs ++++ b/src/printer.rs +@@ -24,7 +24,7 @@ use crate::config::Config; + use crate::decorations::LineChangesDecoration; + use crate::decorations::{Decoration, GridBorderDecoration, LineNumberDecoration}; + #[cfg(feature = "git")] +-use crate::diff::{get_git_diff, LineChanges}; ++use crate::diff::LineChanges; + use crate::error::*; + use crate::input::OpenedInput; + use crate::line_range::RangeCheckResult; +@@ -90,7 +90,7 @@ pub(crate) struct InteractivePrinter<'a> { + ansi_prefix_sgr: String, + content_type: Option, + #[cfg(feature = "git")] +- pub line_changes: Option, ++ pub line_changes: &'a Option, + highlighter: Option>, + syntax_set: &'a SyntaxSet, + background_color_highlight: Option, +@@ -101,6 +101,7 @@ impl<'a> InteractivePrinter<'a> { + config: &'a Config, + assets: &'a HighlightingAssets, + input: &mut OpenedInput, ++ #[cfg(feature = "git")] line_changes: &'a Option, + ) -> Self { + let theme = assets.get_theme(&config.theme); + +@@ -145,9 +146,6 @@ impl<'a> InteractivePrinter<'a> { + panel_width = 0; + } + +- #[cfg(feature = "git")] +- let mut line_changes = None; +- + let highlighter = if input + .reader + .content_type +@@ -155,18 +153,6 @@ impl<'a> InteractivePrinter<'a> { + { + None + } else { +- // Get the Git modifications +- #[cfg(feature = "git")] +- { +- use crate::input::OpenedInputKind; +- +- if config.style_components.changes() { +- if let OpenedInputKind::OrdinaryFile(ref path) = input.kind { +- line_changes = get_git_diff(path); +- } +- } +- } +- + // Determine the type of syntax for highlighting + let syntax = assets.get_syntax(config.language, input, &config.syntax_mapping); + Some(HighlightLines::new(syntax, theme))