Implementation of 'bat --diff'

This adds a new `--diff` option that can be used to only show lines
close to Git changes (added/removed/modified lines). The amount of
additional context can be controlled with `--diff-context=N`.

closes #23
This commit is contained in:
sharkdp 2020-04-23 23:39:30 +02:00 committed by David Peter
parent 0064321323
commit 82e7786e74
7 changed files with 145 additions and 36 deletions

View File

@ -2,6 +2,11 @@
## Features ## 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 ## Bugfixes
## Other ## Other
## `bat` as a library ## `bat` as a library

View File

@ -15,7 +15,7 @@ use console::Term;
use bat::{ use bat::{
assets::HighlightingAssets, assets::HighlightingAssets,
config::Config, config::{Config, VisibleLines},
error::*, error::*,
input::Input, input::Input,
line_range::{HighlightedLineRanges, LineRange, LineRanges}, line_range::{HighlightedLineRanges, LineRange, LineRanges},
@ -196,13 +196,23 @@ impl App {
} }
}) })
.unwrap_or_else(|| String::from(HighlightingAssets::default_theme())), .unwrap_or_else(|| String::from(HighlightingAssets::default_theme())),
line_ranges: self visible_lines: if self.matches.is_present("diff") {
.matches VisibleLines::DiffContext(
.values_of("line-range") self.matches
.map(|vs| vs.map(LineRange::from).collect()) .value_of("diff-context")
.transpose()? .and_then(|t| t.parse().ok())
.map(LineRanges::from) .unwrap_or(2),
.unwrap_or_default(), )
} 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, style_components,
syntax_mapping, syntax_mapping,
pager: self.matches.value_of("pager"), pager: self.matches.value_of("pager"),

View File

@ -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 \ data to bat from STDIN when bat does not otherwise know \
the filename."), 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::<usize>()
.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(
Arg::with_name("tabs") Arg::with_name("tabs")
.long("tabs") .long("tabs")
@ -339,6 +367,7 @@ pub fn build_app(interactive_output: bool) -> ClapApp<'static, 'static> {
.takes_value(true) .takes_value(true)
.number_of_values(1) .number_of_values(1)
.value_name("N:M") .value_name("N:M")
.conflicts_with("diff")
.help("Only print the lines from N to M.") .help("Only print the lines from N to M.")
.long_help( .long_help(
"Only print the specified range of lines for each file. \ "Only print the specified range of lines for each file. \

View File

@ -5,6 +5,32 @@ use crate::style::StyleComponents;
use crate::syntax_mapping::SyntaxMapping; use crate::syntax_mapping::SyntaxMapping;
use crate::wrapping::WrappingMode; 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)] #[derive(Debug, Clone, Default)]
pub struct Config<'a> { pub struct Config<'a> {
/// The explicitly configured language, if any /// The explicitly configured language, if any
@ -39,8 +65,8 @@ pub struct Config<'a> {
#[cfg(feature = "paging")] #[cfg(feature = "paging")]
pub paging_mode: PagingMode, pub paging_mode: PagingMode,
/// Specifies the lines that should be printed /// Specifies which lines should be printed
pub line_ranges: LineRanges, pub visible_lines: VisibleLines,
/// The syntax highlighting theme /// The syntax highlighting theme
pub theme: String, pub theme: String,
@ -62,10 +88,7 @@ pub struct Config<'a> {
fn default_config_should_include_all_lines() { fn default_config_should_include_all_lines() {
use crate::line_range::RangeCheckResult; use crate::line_range::RangeCheckResult;
assert_eq!( assert_eq!(LineRanges::default().check(17), RangeCheckResult::InRange);
Config::default().line_ranges.check(17),
RangeCheckResult::InRange
);
} }
#[test] #[test]

View File

@ -1,9 +1,13 @@
use std::io::{self, Write}; use std::io::{self, Write};
use crate::assets::HighlightingAssets; 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::error::*;
use crate::input::{Input, InputReader, OpenedInput}; use crate::input::{Input, InputReader, OpenedInput};
#[cfg(feature = "git")]
use crate::line_range::LineRange;
use crate::line_range::{LineRanges, RangeCheckResult}; use crate::line_range::{LineRanges, RangeCheckResult};
use crate::output::OutputType; use crate::output::OutputType;
#[cfg(feature = "paging")] #[cfg(feature = "paging")]
@ -68,6 +72,32 @@ impl<'b> Controller<'b> {
no_errors = false; no_errors = false;
} }
Ok(mut opened_input) => { 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<dyn Printer> = if self.config.loop_through { let mut printer: Box<dyn Printer> = if self.config.loop_through {
Box::new(SimplePrinter::new()) Box::new(SimplePrinter::new())
} else { } else {
@ -75,10 +105,18 @@ impl<'b> Controller<'b> {
&self.config, &self.config,
&self.assets, &self.assets,
&mut opened_input, &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 { if let Err(error) = result {
handle_error(&error); handle_error(&error);
@ -96,13 +134,31 @@ impl<'b> Controller<'b> {
printer: &mut dyn Printer, printer: &mut dyn Printer,
writer: &mut dyn Write, writer: &mut dyn Write,
input: &mut OpenedInput, input: &mut OpenedInput,
#[cfg(feature = "git")] line_changes: &Option<LineChanges>,
) -> Result<()> { ) -> Result<()> {
if !input.reader.first_line.is_empty() || self.config.style_components.header() { if !input.reader.first_line.is_empty() || self.config.style_components.header() {
printer.print_header(writer, input)?; printer.print_header(writer, input)?;
} }
if !input.reader.first_line.is_empty() { 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<LineRange> = 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)?; printer.print_footer(writer, input)?;

View File

@ -6,7 +6,7 @@ use syntect::parsing::SyntaxReference;
use crate::{ use crate::{
assets::HighlightingAssets, assets::HighlightingAssets,
config::Config, config::{Config, VisibleLines},
controller::Controller, controller::Controller,
error::Result, error::Result,
input::Input, input::Input,
@ -205,7 +205,7 @@ impl<'a> PrettyPrinter<'a> {
/// Specify the lines that should be printed (default: all) /// Specify the lines that should be printed (default: all)
pub fn line_ranges(&mut self, ranges: LineRanges) -> &mut Self { pub fn line_ranges(&mut self, ranges: LineRanges) -> &mut Self {
self.config.line_ranges = ranges; self.config.visible_lines = VisibleLines::Ranges(ranges);
self self
} }

View File

@ -24,7 +24,7 @@ use crate::config::Config;
use crate::decorations::LineChangesDecoration; use crate::decorations::LineChangesDecoration;
use crate::decorations::{Decoration, GridBorderDecoration, LineNumberDecoration}; use crate::decorations::{Decoration, GridBorderDecoration, LineNumberDecoration};
#[cfg(feature = "git")] #[cfg(feature = "git")]
use crate::diff::{get_git_diff, LineChanges}; use crate::diff::LineChanges;
use crate::error::*; use crate::error::*;
use crate::input::OpenedInput; use crate::input::OpenedInput;
use crate::line_range::RangeCheckResult; use crate::line_range::RangeCheckResult;
@ -90,7 +90,7 @@ pub(crate) struct InteractivePrinter<'a> {
ansi_prefix_sgr: String, ansi_prefix_sgr: String,
content_type: Option<ContentType>, content_type: Option<ContentType>,
#[cfg(feature = "git")] #[cfg(feature = "git")]
pub line_changes: Option<LineChanges>, pub line_changes: &'a Option<LineChanges>,
highlighter: Option<HighlightLines<'a>>, highlighter: Option<HighlightLines<'a>>,
syntax_set: &'a SyntaxSet, syntax_set: &'a SyntaxSet,
background_color_highlight: Option<Color>, background_color_highlight: Option<Color>,
@ -101,6 +101,7 @@ impl<'a> InteractivePrinter<'a> {
config: &'a Config, config: &'a Config,
assets: &'a HighlightingAssets, assets: &'a HighlightingAssets,
input: &mut OpenedInput, input: &mut OpenedInput,
#[cfg(feature = "git")] line_changes: &'a Option<LineChanges>,
) -> Self { ) -> Self {
let theme = assets.get_theme(&config.theme); let theme = assets.get_theme(&config.theme);
@ -145,9 +146,6 @@ impl<'a> InteractivePrinter<'a> {
panel_width = 0; panel_width = 0;
} }
#[cfg(feature = "git")]
let mut line_changes = None;
let highlighter = if input let highlighter = if input
.reader .reader
.content_type .content_type
@ -155,18 +153,6 @@ impl<'a> InteractivePrinter<'a> {
{ {
None None
} else { } 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 // Determine the type of syntax for highlighting
let syntax = assets.get_syntax(config.language, input, &config.syntax_mapping); let syntax = assets.get_syntax(config.language, input, &config.syntax_mapping);
Some(HighlightLines::new(syntax, theme)) Some(HighlightLines::new(syntax, theme))