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 | 226 ++++++++++++++++--------------------- src/vscreen.rs | 212 ---------------------------------- tests/integration_tests.rs | 19 ---- 6 files changed, 114 insertions(+), 390 deletions(-) delete mode 100644 src/vscreen.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 75f5f3c8..5bc30403 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 37b1cd83..02e1fefc 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 2380e7f9..1fbaad19 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); +pub fn expand_tabs(mut text: &str, width: usize, cursor: &mut usize) -> String { + let mut buffer = String::with_capacity(text.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)); - - // Next. - text = &text[index + 1..text.len()]; - } - - *cursor += text.len(); - buffer.push_str(text); - } + 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)); + + // 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 00e7da96..3f7f1e09 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, + text_trimmed, + true_color, + colored_output, + italics, + background_color + ) + )?; - 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() + }; - 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()..])?; - } - } + 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)?; - } + let text = self.preprocess( + region.trim_end_matches(|c| c == '\r' || c == '\n'), + &mut cursor_total, + ); - // 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; - 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); - // 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; - // 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; - 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); + // 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 ea5d4da6..00000000 --- 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 4d8e83f7..c7d4c507 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()