From 8174e022797924aec735ff440806920767f80496 Mon Sep 17 00:00:00 2001 From: Martin Nordholts Date: Tue, 10 May 2022 08:34:13 +0200 Subject: [PATCH] Remove code that tries to handle ANSI escape inputs Syntax highlighting is broken when input contains ANSI escape characters anyway, so there is not much point in trying to handle ANSI escapes in input. --- CHANGELOG.md | 1 + src/lib.rs | 1 - src/preprocessor.rs | 45 +++---- src/printer.rs | 240 ++++++++++++++++--------------------- src/vscreen.rs | 212 -------------------------------- tests/integration_tests.rs | 19 --- 6 files changed, 121 insertions(+), 397 deletions(-) delete mode 100644 src/vscreen.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 75f5f3c8a8..5bc30403a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ - Relaxed glibc requirements on amd64, see #2106 and #2194 (@sharkdp) - Improved fish completions. See #2275 (@zgracem) +- Stop pre-processing ANSI escape characters. Syntax highlighting on ANSI escaped input is not supported. See #2185 and #2189 (@Enselic) ## Syntaxes diff --git a/src/lib.rs b/src/lib.rs index 37b1cd831f..02e1fefca3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -46,7 +46,6 @@ pub(crate) mod printer; pub mod style; pub(crate) mod syntax_mapping; mod terminal; -mod vscreen; pub(crate) mod wrapping; pub use pretty_printer::{Input, PrettyPrinter}; diff --git a/src/preprocessor.rs b/src/preprocessor.rs index 2380e7f9a5..1fbaad195e 100644 --- a/src/preprocessor.rs +++ b/src/preprocessor.rs @@ -1,35 +1,26 @@ -use console::AnsiCodeIterator; - /// Expand tabs like an ANSI-enabled expand(1). -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) { - match chunk { - (text, true) => buffer.push_str(text), - (mut text, false) => { - while let Some(index) = text.find('\t') { - // Add previous text. - if index > 0 { - *cursor += index; - buffer.push_str(&text[0..index]); - } - - // Add tab. - let spaces = width - (*cursor % width); - *cursor += spaces; - buffer.push_str(&*" ".repeat(spaces)); +pub fn expand_tabs(mut text: &str, width: usize, cursor: &mut usize) -> String { + let mut buffer = String::with_capacity(text.len() * 2); + + while let Some(index) = text.find('\t') { + // Add previous text. + if index > 0 { + *cursor += index; + buffer.push_str(&text[0..index]); + } - // Next. - text = &text[index + 1..text.len()]; - } + // Add tab. + let spaces = width - (*cursor % width); + *cursor += spaces; + buffer.push_str(&*" ".repeat(spaces)); - *cursor += text.len(); - buffer.push_str(text); - } - } + // Next. + text = &text[index + 1..text.len()]; } + *cursor += text.len(); + buffer.push_str(text); + buffer } diff --git a/src/printer.rs b/src/printer.rs index 00e7da96a9..3f7f1e0920 100644 --- a/src/printer.rs +++ b/src/printer.rs @@ -6,8 +6,6 @@ use ansi_term::Style; use bytesize::ByteSize; -use console::AnsiCodeIterator; - use syntect::easy::HighlightLines; use syntect::highlighting::Color; use syntect::highlighting::Theme; @@ -33,7 +31,6 @@ use crate::line_range::RangeCheckResult; use crate::preprocessor::{expand_tabs, replace_nonprintable}; use crate::style::StyleComponent; use crate::terminal::{as_terminal_escaped, to_ansi_color}; -use crate::vscreen::AnsiStyle; use crate::wrapping::WrappingMode; pub(crate) trait Printer { @@ -122,7 +119,6 @@ pub(crate) struct InteractivePrinter<'a> { config: &'a Config<'a>, decorations: Vec>, panel_width: usize, - ansi_style: AnsiStyle, content_type: Option, #[cfg(feature = "git")] pub line_changes: &'a Option, @@ -206,7 +202,6 @@ impl<'a> InteractivePrinter<'a> { config, decorations, content_type: input.reader.content_type, - ansi_style: AnsiStyle::new(), #[cfg(feature = "git")] line_changes, highlighter_from_set, @@ -476,7 +471,7 @@ impl<'a> Printer for InteractivePrinter<'a> { self.config.highlighted_lines.0.check(line_number) == RangeCheckResult::InRange; if highlight_this_line && self.config.theme == "ansi" { - self.ansi_style.update("^[4m"); + write!(handle, "\x1B[4m")?; } let background_color = self @@ -503,51 +498,37 @@ impl<'a> Printer for InteractivePrinter<'a> { let italics = self.config.use_italic_text; for &(style, region) in ®ions { - let ansi_iterator = AnsiCodeIterator::new(region); - for chunk in ansi_iterator { - match chunk { - // ANSI escape passthrough. - (ansi, true) => { - self.ansi_style.update(ansi); - write!(handle, "{}", ansi)?; - } + let text = &*self.preprocess(region, &mut cursor_total); + let text_trimmed = text.trim_end_matches(|c| c == '\r' || c == '\n'); - // Regular text. - (text, false) => { - let text = &*self.preprocess(text, &mut cursor_total); - let text_trimmed = text.trim_end_matches(|c| c == '\r' || c == '\n'); - - write!( - handle, - "{}", - as_terminal_escaped( - style, - &format!("{}{}", self.ansi_style, text_trimmed), - true_color, - colored_output, - italics, - background_color - ) - )?; - - if text.len() != text_trimmed.len() { - if let Some(background_color) = background_color { - let ansi_style = Style { - background: to_ansi_color(background_color, true_color), - ..Default::default() - }; - - let width = if cursor_total <= cursor_max { - cursor_max - cursor_total + 1 - } else { - 0 - }; - write!(handle, "{}", ansi_style.paint(" ".repeat(width)))?; - } - write!(handle, "{}", &text[text_trimmed.len()..])?; - } - } + write!( + handle, + "{}", + as_terminal_escaped( + style, + text_trimmed, + true_color, + colored_output, + italics, + background_color + ) + )?; + + if text.len() != text_trimmed.len() { + if let Some(background_color) = background_color { + let ansi_style = Style { + background: to_ansi_color(background_color, true_color), + ..Default::default() + }; + + let width = if cursor_total <= cursor_max { + cursor_max - cursor_total + 1 + } else { + 0 + }; + write!(handle, "{}", ansi_style.paint(" ".repeat(width)))?; } + write!(handle, "{}", &text[text_trimmed.len()..])?; } } @@ -556,98 +537,82 @@ impl<'a> Printer for InteractivePrinter<'a> { } } else { for &(style, region) in ®ions { - let ansi_iterator = AnsiCodeIterator::new(region); - for chunk in ansi_iterator { - match chunk { - // ANSI escape passthrough. - (ansi, true) => { - self.ansi_style.update(ansi); - write!(handle, "{}", ansi)?; - } - - // Regular text. - (text, false) => { - let text = self.preprocess( - text.trim_end_matches(|c| c == '\r' || c == '\n'), - &mut cursor_total, - ); - - let mut max_width = cursor_max - cursor; - - // line buffer (avoid calling write! for every character) - let mut line_buf = String::with_capacity(max_width * 4); - - // Displayed width of line_buf - let mut current_width = 0; - - for c in text.chars() { - // calculate the displayed width for next character - let cw = c.width().unwrap_or(0); - current_width += cw; - - // if next character cannot be printed on this line, - // flush the buffer. - if current_width > max_width { - // Generate wrap padding if not already generated. - if panel_wrap.is_none() { - panel_wrap = if self.panel_width > 0 { - Some(format!( - "{} ", - self.decorations - .iter() - .map(|d| d - .generate(line_number, true, self) - .text) - .collect::>() - .join(" ") - )) - } else { - Some("".to_string()) - } - } - - // It wraps. - write!( - handle, - "{}\n{}", - as_terminal_escaped( - style, - &*format!("{}{}", self.ansi_style, line_buf), - self.config.true_color, - self.config.colored_output, - self.config.use_italic_text, - background_color - ), - panel_wrap.clone().unwrap() - )?; - - cursor = 0; - max_width = cursor_max; - - line_buf.clear(); - current_width = cw; - } - - line_buf.push(c); + let text = self.preprocess( + region.trim_end_matches(|c| c == '\r' || c == '\n'), + &mut cursor_total, + ); + + let mut max_width = cursor_max - cursor; + + // line buffer (avoid calling write! for every character) + let mut line_buf = String::with_capacity(max_width * 4); + + // Displayed width of line_buf + let mut current_width = 0; + + for c in text.chars() { + // calculate the displayed width for next character + let cw = c.width().unwrap_or(0); + current_width += cw; + + // if next character cannot be printed on this line, + // flush the buffer. + if current_width > max_width { + // Generate wrap padding if not already generated. + if panel_wrap.is_none() { + panel_wrap = if self.panel_width > 0 { + Some(format!( + "{} ", + self.decorations + .iter() + .map(|d| d.generate(line_number, true, self).text) + .collect::>() + .join(" ") + )) + } else { + Some("".to_string()) } - - // flush the buffer - cursor += current_width; - write!( - handle, - "{}", - as_terminal_escaped( - style, - &*format!("{}{}", self.ansi_style, line_buf), - self.config.true_color, - self.config.colored_output, - self.config.use_italic_text, - background_color - ) - )?; } + + // It wraps. + write!( + handle, + "{}\n{}", + as_terminal_escaped( + style, + &line_buf, + self.config.true_color, + self.config.colored_output, + self.config.use_italic_text, + background_color + ), + panel_wrap.clone().unwrap() + )?; + + cursor = 0; + max_width = cursor_max; + + line_buf.clear(); + current_width = cw; } + + line_buf.push(c); } + + // flush the buffer + cursor += current_width; + write!( + handle, + "{}", + as_terminal_escaped( + style, + &line_buf, + self.config.true_color, + self.config.colored_output, + self.config.use_italic_text, + background_color + ) + )?; } if let Some(background_color) = background_color { @@ -666,7 +631,6 @@ impl<'a> Printer for InteractivePrinter<'a> { } if highlight_this_line && self.config.theme == "ansi" { - self.ansi_style.update("^[24m"); write!(handle, "\x1B[24m")?; } diff --git a/src/vscreen.rs b/src/vscreen.rs deleted file mode 100644 index ea5d4da6f0..0000000000 --- a/src/vscreen.rs +++ /dev/null @@ -1,212 +0,0 @@ -use std::fmt::{Display, Formatter}; - -// Wrapper to avoid unnecessary branching when input doesn't have ANSI escape sequences. -pub struct AnsiStyle { - attributes: Option, -} - -impl AnsiStyle { - pub fn new() -> Self { - AnsiStyle { attributes: None } - } - - pub fn update(&mut self, sequence: &str) -> bool { - match &mut self.attributes { - Some(a) => a.update(sequence), - None => { - self.attributes = Some(Attributes::new()); - self.attributes.as_mut().unwrap().update(sequence) - } - } - } -} - -impl Display for AnsiStyle { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self.attributes { - Some(ref a) => a.fmt(f), - None => Ok(()), - } - } -} - -struct Attributes { - foreground: String, - background: String, - underlined: String, - - /// The character set to use. - /// REGEX: `\^[()][AB0-3]` - charset: String, - - /// A buffer for unknown sequences. - unknown_buffer: String, - - /// ON: ^[1m - /// OFF: ^[22m - bold: String, - - /// ON: ^[2m - /// OFF: ^[22m - dim: String, - - /// ON: ^[4m - /// OFF: ^[24m - underline: String, - - /// ON: ^[3m - /// OFF: ^[23m - italic: String, - - /// ON: ^[9m - /// OFF: ^[29m - strike: String, -} - -impl Attributes { - pub fn new() -> Self { - Attributes { - foreground: "".to_owned(), - background: "".to_owned(), - underlined: "".to_owned(), - charset: "".to_owned(), - unknown_buffer: "".to_owned(), - bold: "".to_owned(), - dim: "".to_owned(), - underline: "".to_owned(), - italic: "".to_owned(), - strike: "".to_owned(), - } - } - - /// Update the attributes with an escape sequence. - /// Returns `false` if the sequence is unsupported. - pub fn update(&mut self, sequence: &str) -> bool { - let mut chars = sequence.char_indices().skip(1); - - if let Some((_, t)) = chars.next() { - match t { - '(' => self.update_with_charset('(', chars.map(|(_, c)| c)), - ')' => self.update_with_charset(')', chars.map(|(_, c)| c)), - '[' => { - if let Some((i, last)) = chars.last() { - // SAFETY: Always starts with ^[ and ends with m. - self.update_with_csi(last, &sequence[2..i]) - } else { - false - } - } - _ => self.update_with_unsupported(sequence), - } - } else { - false - } - } - - fn sgr_reset(&mut self) { - self.foreground.clear(); - self.background.clear(); - self.underlined.clear(); - self.bold.clear(); - self.dim.clear(); - self.underline.clear(); - self.italic.clear(); - self.strike.clear(); - } - - fn update_with_sgr(&mut self, parameters: &str) -> bool { - let mut iter = parameters - .split(';') - .map(|p| if p.is_empty() { "0" } else { p }) - .map(|p| p.parse::()) - .map(|p| p.unwrap_or(0)); // Treat errors as 0. - - while let Some(p) = iter.next() { - match p { - 0 => self.sgr_reset(), - 1 => self.bold = format!("\x1B[{}m", parameters), - 2 => self.dim = format!("\x1B[{}m", parameters), - 3 => self.italic = format!("\x1B[{}m", parameters), - 4 => self.underline = format!("\x1B[{}m", parameters), - 23 => self.italic.clear(), - 24 => self.underline.clear(), - 22 => { - self.bold.clear(); - self.dim.clear(); - } - 30..=39 => self.foreground = Self::parse_color(p, &mut iter), - 40..=49 => self.background = Self::parse_color(p, &mut iter), - 58..=59 => self.underlined = Self::parse_color(p, &mut iter), - 90..=97 => self.foreground = Self::parse_color(p, &mut iter), - 100..=107 => self.foreground = Self::parse_color(p, &mut iter), - _ => { - // Unsupported SGR sequence. - // Be compatible and pretend one just wasn't was provided. - } - } - } - - true - } - - fn update_with_csi(&mut self, finalizer: char, sequence: &str) -> bool { - if finalizer == 'm' { - self.update_with_sgr(sequence) - } else { - false - } - } - - fn update_with_unsupported(&mut self, sequence: &str) -> bool { - self.unknown_buffer.push_str(sequence); - false - } - - fn update_with_charset(&mut self, kind: char, set: impl Iterator) -> bool { - self.charset = format!("\x1B{}{}", kind, set.take(1).collect::()); - true - } - - fn parse_color(color: u16, parameters: &mut dyn Iterator) -> String { - match color % 10 { - 8 => match parameters.next() { - Some(5) /* 256-color */ => format!("\x1B[{};5;{}m", color, join(";", 1, parameters)), - Some(2) /* 24-bit color */ => format!("\x1B[{};2;{}m", color, join(";", 3, parameters)), - Some(c) => format!("\x1B[{};{}m", color, c), - _ => "".to_owned(), - }, - 9 => "".to_owned(), - _ => format!("\x1B[{}m", color), - } - } -} - -impl Display for Attributes { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!( - f, - "{}{}{}{}{}{}{}{}{}", - self.foreground, - self.background, - self.underlined, - self.charset, - self.bold, - self.dim, - self.underline, - self.italic, - self.strike, - ) - } -} - -fn join( - delimiter: &str, - limit: usize, - iterator: &mut dyn Iterator, -) -> String { - iterator - .take(limit) - .map(|i| i.to_string()) - .collect::>() - .join(delimiter) -} diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index 4d8e83f76b..c7d4c507c3 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -1419,25 +1419,6 @@ fn ansi_highlight_underline() { .stderr(""); } -// Ensure that ANSI passthrough is emitted properly for both wrapping and non-wrapping printer. -#[test] -fn ansi_passthrough_emit() { - for wrapping in &["never", "character"] { - bat() - .arg("--paging=never") - .arg("--color=never") - .arg("--terminal-width=80") - .arg(format!("--wrap={}", wrapping)) - .arg("--decorations=always") - .arg("--style=plain") - .write_stdin("\x1B[33mColor\nColor \x1B[m\nPlain\n") - .assert() - .success() - .stdout("\x1B[33m\x1B[33mColor\n\x1B[33mColor \x1B[m\nPlain\n") - .stderr(""); - } -} - #[test] fn ignored_suffix_arg() { bat()