From ecd862d9ff729a5de5e7b2771be02155567b72cf Mon Sep 17 00:00:00 2001 From: sharkdp Date: Thu, 1 Nov 2018 13:02:29 +0100 Subject: [PATCH] Feature: Highlight non-printable characters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a new `-A`/`--show-all` option (in analogy to GNU Linux `cat`s option) that highlights non-printable characters like space, tab or newline. This works in two steps: - **Preprocessing**: replace space by `•`, replace tab by `├──┤`, replace newline by `␤`, etc. - **Highlighting**: Use a newly written Sublime syntax to highlight these special symbols. Note: This feature is not technically a drop-in replacement for GNU `cat`s `--show-all` but it has the same purpose. --- .../syntaxes/show-nonprintable.sublime-syntax | 25 +++++++++++ src/app.rs | 12 +++++- src/clap_app.rs | 12 ++++++ src/controller.rs | 19 ++++++++- src/preprocessor.rs | 41 ++++++++++++++++++- src/printer.rs | 4 +- 6 files changed, 108 insertions(+), 5 deletions(-) create mode 100644 assets/syntaxes/show-nonprintable.sublime-syntax diff --git a/assets/syntaxes/show-nonprintable.sublime-syntax b/assets/syntaxes/show-nonprintable.sublime-syntax new file mode 100644 index 00000000..6e90e19e --- /dev/null +++ b/assets/syntaxes/show-nonprintable.sublime-syntax @@ -0,0 +1,25 @@ +%YAML 1.2 +--- +# http://www.sublimetext.com/docs/3/syntax.html +name: Highlight non-printables +file_extensions: + - show-nonprintable +scope: whitespace +contexts: + main: + - match: "•" + scope: support.function.show-nonprintable.space + - match: "├─*┤" + scope: constant.character.escape.show-nonprintable.tab + - match: "␤" + scope: keyword.operator.show-nonprintable.newline + - match: "␍" + scope: string.show-nonprintable.carriage-return + - match: "␀" + scope: entity.other.attribute-name.show-nonprintable.null + - match: "␇" + scope: entity.other.attribute-name.show-nonprintable.bell + - match: "␛" + scope: entity.other.attribute-name.show-nonprintable.escape + - match: "␈" + scope: entity.other.attribute-name.show-nonprintable.backspace diff --git a/src/app.rs b/src/app.rs index b1f967c1..0a69195c 100644 --- a/src/app.rs +++ b/src/app.rs @@ -37,6 +37,9 @@ pub struct Config<'a> { /// The explicitly configured language, if any pub language: Option<&'a str>, + /// Whether or not to show/replace non-printable characters like space, tab and newline. + pub show_nonprintable: bool, + /// The character width of the terminal pub term_width: usize, @@ -169,7 +172,14 @@ impl App { Ok(Config { true_color: is_truecolor_terminal(), - language: self.matches.value_of("language"), + language: self.matches.value_of("language").or_else(|| { + if self.matches.is_present("show-all") { + Some("show-nonprintable") + } else { + None + } + }), + show_nonprintable: self.matches.is_present("show-all"), output_wrap: if !self.interactive_output { // We don't have the tty width when piping to another program. // There's no point in wrapping when this is the case. diff --git a/src/clap_app.rs b/src/clap_app.rs index b9b38393..fff618e1 100644 --- a/src/clap_app.rs +++ b/src/clap_app.rs @@ -158,6 +158,18 @@ pub fn build_app(interactive_output: bool) -> ClapApp<'static, 'static> { '--style=numbers'", ), ) + .arg( + Arg::with_name("show-all") + .long("show-all") + .alias("show-nonprintable") + .short("A") + .conflicts_with("language") + .help("Show non-printable characters (space, tab, newline, ..).") + .long_help( + "Show non-printable characters like space, tab or newline. \ + Use '--tabs' to control the width of the tab-placeholders.", + ), + ) .arg( Arg::with_name("line-range") .long("line-range") diff --git a/src/controller.rs b/src/controller.rs index 371ec484..ae606e31 100644 --- a/src/controller.rs +++ b/src/controller.rs @@ -1,4 +1,5 @@ use std::io::{self, Write}; +use std::mem::swap; use app::Config; use assets::HighlightingAssets; @@ -6,6 +7,7 @@ use errors::*; use inputfile::{InputFile, InputFileReader}; use line_range::{LineRanges, RangeCheckResult}; use output::OutputType; +use preprocessor::replace_nonprintable; use printer::{InteractivePrinter, Printer, SimplePrinter}; pub struct Controller<'a> { @@ -64,7 +66,14 @@ impl<'b> Controller<'b> { input_file: InputFile<'a>, ) -> Result<()> { printer.print_header(writer, input_file)?; - self.print_file_ranges(printer, writer, reader, &self.config.line_ranges)?; + self.print_file_ranges( + printer, + writer, + reader, + &self.config.line_ranges, + self.config.show_nonprintable, + self.config.tab_width, + )?; printer.print_footer(writer)?; Ok(()) @@ -76,12 +85,20 @@ impl<'b> Controller<'b> { writer: &mut Write, mut reader: InputFileReader, line_ranges: &LineRanges, + show_nonprintable: bool, + tab_width: usize, ) -> Result<()> { let mut line_buffer = Vec::new(); + let mut line_buffer_processed = Vec::new(); let mut line_number: usize = 1; while reader.read_line(&mut line_buffer)? { + if show_nonprintable { + replace_nonprintable(&mut line_buffer, &mut line_buffer_processed, tab_width); + swap(&mut line_buffer, &mut line_buffer_processed); + } + match line_ranges.check(line_number) { RangeCheckResult::OutsideRange => { // Call the printer in case we need to call the syntax highlighter diff --git a/src/preprocessor.rs b/src/preprocessor.rs index 0a99e07d..a69f453e 100644 --- a/src/preprocessor.rs +++ b/src/preprocessor.rs @@ -1,7 +1,7 @@ use console::AnsiCodeIterator; /// Expand tabs like an ANSI-enabled expand(1). -pub fn expand(line: &str, width: usize, cursor: &mut usize) -> String { +pub fn expand_tabs(line: &str, width: usize, cursor: &mut usize) -> String { let mut buffer = String::with_capacity(line.len() * 2); for chunk in AnsiCodeIterator::new(line) { @@ -32,3 +32,42 @@ pub fn expand(line: &str, width: usize, cursor: &mut usize) -> String { buffer } + +pub fn replace_nonprintable(input: &mut Vec, output: &mut Vec, tab_width: usize) { + output.clear(); + + let tab_width = if tab_width == 0 { + 4 + } else if tab_width == 1 { + 2 + } else { + tab_width + }; + + for chr in input { + match *chr { + // space + b' ' => output.extend_from_slice("•".as_bytes()), + // tab + b'\t' => { + output.extend_from_slice("├".as_bytes()); + output.extend_from_slice("─".repeat(tab_width - 2).as_bytes()); + output.extend_from_slice("┤".as_bytes()); + } + // new line + b'\n' => output.extend_from_slice("␤".as_bytes()), + // carriage return + b'\r' => output.extend_from_slice("␍".as_bytes()), + // null + 0x00 => output.extend_from_slice("␀".as_bytes()), + // bell + 0x07 => output.extend_from_slice("␇".as_bytes()), + // backspace + 0x08 => output.extend_from_slice("␈".as_bytes()), + // escape + 0x1B => output.extend_from_slice("␛".as_bytes()), + // anything else + _ => output.push(*chr), + } + } +} diff --git a/src/printer.rs b/src/printer.rs index fbe14120..593853cf 100644 --- a/src/printer.rs +++ b/src/printer.rs @@ -22,7 +22,7 @@ use diff::get_git_diff; use diff::LineChanges; use errors::*; use inputfile::{InputFile, InputFileReader}; -use preprocessor::expand; +use preprocessor::expand_tabs; use style::OutputWrap; use terminal::{as_terminal_escaped, to_ansi_color}; @@ -177,7 +177,7 @@ impl<'a> InteractivePrinter<'a> { fn preprocess(&self, text: &str, cursor: &mut usize) -> String { if self.config.tab_width > 0 { - expand(text, self.config.tab_width, cursor) + expand_tabs(text, self.config.tab_width, cursor) } else { text.to_string() }