on master help sign in
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

commit82e7786e747b1fcfac1f963ae6415a22ec9caae1 authorsharkdp <davidpeter@web.de> dateThu 23 Apr 2020, 23:39 (7 years ago) parent0064321
7 files changed+145−36
CHANGELOG.md+5 src/bin/bat/app.rs+18−8 src/bin/bat/clap_app.rs+29 src/config.rs+29−6 src/controller.rs+59−3 src/pretty_printer.rs+2−2 src/printer.rs+3−17
CHANGELOG.md+5
@@ -2,6 +2,11 @@
22
33
44## Features
5+
6+- Add a new `--diff` option that can be used to only show lines surrounding
7+ Git changes, i.e. added, removed or modified lines. The amount of additional
8+ context can be controlled with `--diff-context=N`. See #23 and #940
9+
510## Bugfixes
611## Other
712## `bat` as a library
src/bin/bat/app.rs+18−8
@@ -15,7 +15,7 @@ use console::Term;
1515
1616use bat::{
1717 assets::HighlightingAssets,
18− config::Config,
18+ config::{Config, VisibleLines},
1919 error::*,
2020 input::Input,
2121 line_range::{HighlightedLineRanges, LineRange, LineRanges},
@@ -196,13 +196,23 @@ impl App {
196196 }
197197 })
198198 .unwrap_or_else(|| String::from(HighlightingAssets::default_theme())),
199− line_ranges: self
200− .matches
201− .values_of("line-range")
202− .map(|vs| vs.map(LineRange::from).collect())
203− .transpose()?
204− .map(LineRanges::from)
205− .unwrap_or_default(),
199+ visible_lines: if self.matches.is_present("diff") {
200+ VisibleLines::DiffContext(
201+ self.matches
202+ .value_of("diff-context")
203+ .and_then(|t| t.parse().ok())
204+ .unwrap_or(2),
205+ )
206+ } else {
207+ VisibleLines::Ranges(
208+ self.matches
209+ .values_of("line-range")
210+ .map(|vs| vs.map(LineRange::from).collect())
211+ .transpose()?
212+ .map(LineRanges::from)
213+ .unwrap_or_default(),
214+ )
215+ },
206216 style_components,
207217 syntax_mapping,
208218 pager: self.matches.value_of("pager"),
src/bin/bat/clap_app.rs+29
@@ -105,6 +105,34 @@ pub fn build_app(interactive_output: bool) -> ClapApp<'static, 'static> {
105105 data to bat from STDIN when bat does not otherwise know \
106106 the filename."),
107107 )
108+ .arg(
109+ Arg::with_name("diff")
110+ .long("diff")
111+ .help("Only show lines that have been added/removed/modified.")
112+ .long_help(
113+ "Only show lines that have been added/removed/modified with respect \
114+ to the Git index. Use --diff-context=N to control how much context you want to see.",
115+ ),
116+ )
117+ .arg(
118+ Arg::with_name("diff-context")
119+ .long("diff-context")
120+ .overrides_with("diff-context")
121+ .takes_value(true)
122+ .value_name("N")
123+ .validator(
124+ |n| {
125+ n.parse::<usize>()
126+ .map_err(|_| "must be a number")
127+ .map(|_| ()) // Convert to Result<(), &str>
128+ .map_err(|e| e.to_string())
129+ }, // Convert to Result<(), String>
130+ )
131+ .hidden_short_help(true)
132+ .long_help(
133+ "Include N lines of context around added/removed/modified lines when using '--diff'.",
134+ ),
135+ )
108136 .arg(
109137 Arg::with_name("tabs")
110138 .long("tabs")
@@ -339,6 +367,7 @@ pub fn build_app(interactive_output: bool) -> ClapApp<'static, 'static> {
339367 .takes_value(true)
340368 .number_of_values(1)
341369 .value_name("N:M")
370+ .conflicts_with("diff")
342371 .help("Only print the lines from N to M.")
343372 .long_help(
344373 "Only print the specified range of lines for each file. \
src/config.rs+29−6
@@ -5,6 +5,32 @@ use crate::style::StyleComponents;
55use crate::syntax_mapping::SyntaxMapping;
66use crate::wrapping::WrappingMode;
77
8+#[derive(Debug, Clone)]
9+pub enum VisibleLines {
10+ /// Show all lines which are included in the line ranges
11+ Ranges(LineRanges),
12+
13+ #[cfg(feature = "git")]
14+ /// Only show lines surrounding added/deleted/modified lines
15+ DiffContext(usize),
16+}
17+
18+impl VisibleLines {
19+ pub fn diff_context(&self) -> bool {
20+ match self {
21+ Self::Ranges(_) => false,
22+ #[cfg(feature = "git")]
23+ Self::DiffContext(_) => true,
24+ }
25+ }
26+}
27+
28+impl Default for VisibleLines {
29+ fn default() -> Self {
30+ VisibleLines::Ranges(LineRanges::default())
31+ }
32+}
33+
834#[derive(Debug, Clone, Default)]
935pub struct Config<'a> {
1036 /// The explicitly configured language, if any
@@ -39,8 +65,8 @@ pub struct Config<'a> {
3965 #[cfg(feature = "paging")]
4066 pub paging_mode: PagingMode,
4167
42− /// Specifies the lines that should be printed
43− pub line_ranges: LineRanges,
68+ /// Specifies which lines should be printed
69+ pub visible_lines: VisibleLines,
4470
4571 /// The syntax highlighting theme
4672 pub theme: String,
@@ -62,10 +88,7 @@ pub struct Config<'a> {
6288fn default_config_should_include_all_lines() {
6389 use crate::line_range::RangeCheckResult;
6490
65− assert_eq!(
66− Config::default().line_ranges.check(17),
67− RangeCheckResult::InRange
68− );
91+ assert_eq!(LineRanges::default().check(17), RangeCheckResult::InRange);
6992}
7093
7194#[test]
src/controller.rs+59−3
@@ -1,9 +1,13 @@
11use std::io::{self, Write};
22
33use crate::assets::HighlightingAssets;
4−use crate::config::Config;
4+use crate::config::{Config, VisibleLines};
5+#[cfg(feature = "git")]
6+use crate::diff::{get_git_diff, LineChanges};
57use crate::error::*;
68use crate::input::{Input, InputReader, OpenedInput};
9+#[cfg(feature = "git")]
10+use crate::line_range::LineRange;
711use crate::line_range::{LineRanges, RangeCheckResult};
812use crate::output::OutputType;
913#[cfg(feature = "paging")]
@@ -68,6 +72,32 @@ impl<'b> Controller<'b> {
6872 no_errors = false;
6973 }
7074 Ok(mut opened_input) => {
75+ #[cfg(feature = "git")]
76+ let line_changes = if self.config.visible_lines.diff_context()
77+ || (!self.config.loop_through && self.config.style_components.changes())
78+ {
79+ if let crate::input::OpenedInputKind::OrdinaryFile(ref path) =
80+ opened_input.kind
81+ {
82+ let diff = get_git_diff(path);
83+
84+ if self.config.visible_lines.diff_context()
85+ && diff
86+ .as_ref()
87+ .map(|changes| changes.is_empty())
88+ .unwrap_or(false)
89+ {
90+ continue;
91+ }
92+
93+ diff
94+ } else {
95+ None
96+ }
97+ } else {
98+ None
99+ };
100+
71101 let mut printer: Box<dyn Printer> = if self.config.loop_through {
72102 Box::new(SimplePrinter::new())
73103 } else {
@@ -75,10 +105,18 @@ impl<'b> Controller<'b> {
75105 &self.config,
76106 &self.assets,
77107 &mut opened_input,
108+ #[cfg(feature = "git")]
109+ &line_changes,
78110 ))
79111 };
80112
81− let result = self.print_file(&mut *printer, writer, &mut opened_input);
113+ let result = self.print_file(
114+ &mut *printer,
115+ writer,
116+ &mut opened_input,
117+ #[cfg(feature = "git")]
118+ &line_changes,
119+ );
82120
83121 if let Err(error) = result {
84122 handle_error(&error);
@@ -96,13 +134,31 @@ impl<'b> Controller<'b> {
96134 printer: &mut dyn Printer,
97135 writer: &mut dyn Write,
98136 input: &mut OpenedInput,
137+ #[cfg(feature = "git")] line_changes: &Option<LineChanges>,
99138 ) -> Result<()> {
100139 if !input.reader.first_line.is_empty() || self.config.style_components.header() {
101140 printer.print_header(writer, input)?;
102141 }
103142
104143 if !input.reader.first_line.is_empty() {
105− self.print_file_ranges(printer, writer, &mut input.reader, &self.config.line_ranges)?;
144+ let line_ranges = match self.config.visible_lines {
145+ VisibleLines::Ranges(ref line_ranges) => line_ranges.clone(),
146+ #[cfg(feature = "git")]
147+ VisibleLines::DiffContext(context) => {
148+ let mut line_ranges: Vec<LineRange> = vec![];
149+
150+ if let Some(line_changes) = line_changes {
151+ for line in line_changes.keys() {
152+ let line = *line as usize;
153+ line_ranges.push(LineRange::new(line - context, line + context));
154+ }
155+ }
156+
157+ LineRanges::from(line_ranges)
158+ }
159+ };
160+
161+ self.print_file_ranges(printer, writer, &mut input.reader, &line_ranges)?;
106162 }
107163 printer.print_footer(writer, input)?;
108164
src/pretty_printer.rs+2−2
@@ -6,7 +6,7 @@ use syntect::parsing::SyntaxReference;
66
77use crate::{
88 assets::HighlightingAssets,
9− config::Config,
9+ config::{Config, VisibleLines},
1010 controller::Controller,
1111 error::Result,
1212 input::Input,
@@ -205,7 +205,7 @@ impl<'a> PrettyPrinter<'a> {
205205
206206 /// Specify the lines that should be printed (default: all)
207207 pub fn line_ranges(&mut self, ranges: LineRanges) -> &mut Self {
208− self.config.line_ranges = ranges;
208+ self.config.visible_lines = VisibleLines::Ranges(ranges);
209209 self
210210 }
211211
src/printer.rs+3−17
@@ -24,7 +24,7 @@ use crate::config::Config;
2424use crate::decorations::LineChangesDecoration;
2525use crate::decorations::{Decoration, GridBorderDecoration, LineNumberDecoration};
2626#[cfg(feature = "git")]
27−use crate::diff::{get_git_diff, LineChanges};
27+use crate::diff::LineChanges;
2828use crate::error::*;
2929use crate::input::OpenedInput;
3030use crate::line_range::RangeCheckResult;
@@ -90,7 +90,7 @@ pub(crate) struct InteractivePrinter<'a> {
9090 ansi_prefix_sgr: String,
9191 content_type: Option<ContentType>,
9292 #[cfg(feature = "git")]
93− pub line_changes: Option<LineChanges>,
93+ pub line_changes: &'a Option<LineChanges>,
9494 highlighter: Option<HighlightLines<'a>>,
9595 syntax_set: &'a SyntaxSet,
9696 background_color_highlight: Option<Color>,
@@ -101,6 +101,7 @@ impl<'a> InteractivePrinter<'a> {
101101 config: &'a Config,
102102 assets: &'a HighlightingAssets,
103103 input: &mut OpenedInput,
104+ #[cfg(feature = "git")] line_changes: &'a Option<LineChanges>,
104105 ) -> Self {
105106 let theme = assets.get_theme(&config.theme);
106107
@@ -145,9 +146,6 @@ impl<'a> InteractivePrinter<'a> {
145146 panel_width = 0;
146147 }
147148
148− #[cfg(feature = "git")]
149− let mut line_changes = None;
150−
151149 let highlighter = if input
152150 .reader
153151 .content_type
@@ -155,18 +153,6 @@ impl<'a> InteractivePrinter<'a> {
155153 {
156154 None
157155 } else {
158− // Get the Git modifications
159− #[cfg(feature = "git")]
160− {
161− use crate::input::OpenedInputKind;
162−
163− if config.style_components.changes() {
164− if let OpenedInputKind::OrdinaryFile(ref path) = input.kind {
165− line_changes = get_git_diff(path);
166− }
167− }
168− }
169−
170156 // Determine the type of syntax for highlighting
171157 let syntax = assets.get_syntax(config.language, input, &config.syntax_mapping);
172158 Some(HighlightLines::new(syntax, theme))