From 899fdbb347b6444eb06d8a26375ed10e35c612e2 Mon Sep 17 00:00:00 2001 From: Martin Nordholts Date: Mon, 29 Aug 2022 18:49:29 +0200 Subject: [PATCH] Remove code that tries to handle ANSI escape inputs (#2189) 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()