Feature: Highlight non-printable characters

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.
This commit is contained in:
sharkdp 2018-11-01 13:02:29 +01:00 committed by David Peter
parent cbed338c3a
commit ecd862d9ff
6 changed files with 108 additions and 5 deletions

View File

@ -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

View File

@ -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.

View File

@ -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")

View File

@ -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

View File

@ -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<u8>, output: &mut Vec<u8>, 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),
}
}
}

View File

@ -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()
}