diff --git a/rust/hg-core/src/operations/annotate.rs b/rust/hg-core/src/operations/annotate.rs index d81714a1c88d5143d3999afaa6e4eab4f32f590e_cnVzdC9oZy1jb3JlL3NyYy9vcGVyYXRpb25zL2Fubm90YXRlLnJz..cc30bead2d6153a7477785aa1495e5f6f155d62e_cnVzdC9oZy1jb3JlL3NyYy9vcGVyYXRpb25zL2Fubm90YXRlLnJz 100644 --- a/rust/hg-core/src/operations/annotate.rs +++ b/rust/hg-core/src/operations/annotate.rs @@ -1,5 +1,3 @@ -use std::borrow::Cow; - use crate::{ bdiff::{self, Lines}, errors::HgError, @@ -26,7 +24,7 @@ pub struct AnnotateOptions { pub treat_binary_as_text: bool, pub follow_copies: bool, - pub whitespace: CleanWhitespace, + pub whitespace: Option<CleanWhitespace>, } /// The final result of annotating a file. @@ -71,6 +69,6 @@ impl OwnedLines { /// Cleans `data` based on `whitespace` and then splits into lines. fn split( - data: Vec<u8>, - whitespace: CleanWhitespace, + mut data: Vec<u8>, + whitespace: Option<CleanWhitespace>, ) -> Result<Self, HgError> { @@ -76,8 +74,7 @@ ) -> Result<Self, HgError> { - let data = match clean_whitespace(&data, whitespace) { - Cow::Borrowed(_) => data, - Cow::Owned(data) => data, - }; + if let Some(ws) = whitespace { + clean_whitespace(&mut data, ws); + } Self::try_new(data, |data| bdiff::split_lines(data)) } @@ -323,11 +320,8 @@ // Step 3: Read files and split lines. Do the base file with and without // whitespace cleaning. Do the rest of the files in parallel with rayon. let base_file_original_lines = match options.whitespace { - CleanWhitespace::None => None, - _ => Some(OwnedLines::split( - base_file_data.clone(), - CleanWhitespace::None, - )?), + None => None, + _ => Some(OwnedLines::split(base_file_data.clone(), None)?), }; graph[base_id].file = AnnotatedFileState::Read(OwnedLines::split( base_file_data, diff --git a/rust/hg-core/src/utils/strings.rs b/rust/hg-core/src/utils/strings.rs index d81714a1c88d5143d3999afaa6e4eab4f32f590e_cnVzdC9oZy1jb3JlL3NyYy91dGlscy9zdHJpbmdzLnJz..cc30bead2d6153a7477785aa1495e5f6f155d62e_cnVzdC9oZy1jb3JlL3NyYy91dGlscy9zdHJpbmdzLnJz 100644 --- a/rust/hg-core/src/utils/strings.rs +++ b/rust/hg-core/src/utils/strings.rs @@ -1,9 +1,7 @@ //! Contains string-related utilities. use crate::utils::hg_path::HgPath; -use lazy_static::lazy_static; -use regex::bytes::Regex; -use std::{borrow::Cow, cell::Cell, fmt, io::Write as _, ops::Deref as _}; +use std::{cell::Cell, fmt, io::Write as _, ops::Deref as _}; /// Useful until rust/issues/56345 is stable /// @@ -304,8 +302,6 @@ /// Options for [`clean_whitespace`]. #[derive(Copy, Clone)] pub enum CleanWhitespace { - /// Do nothing. - None, /// Remove whitespace at ends of lines. AtEol, /// Collapse consecutive whitespace characters into a single space. @@ -315,35 +311,58 @@ } /// Normalizes whitespace in text so that it won't apppear in diffs. -/// Returns `Cow::Borrowed(text)` if the result is unchanged. -pub fn clean_whitespace(text: &[u8], how: CleanWhitespace) -> Cow<[u8]> { - lazy_static! { - // To match wsclean in mdiff.py, this includes "\f". - static ref AT_EOL: Regex = - Regex::new(r"(?m)[ \t\r\f]+$").expect("valid regex"); - // To match fixws in cext/bdiff.c, this does *not* include "\f". - static ref MULTIPLE: Regex = - Regex::new(r"[ \t\r]+").expect("valid regex"); - } - let replacement: &[u8] = match how { - CleanWhitespace::None => return Cow::Borrowed(text), - CleanWhitespace::AtEol => return AT_EOL.replace_all(text, b""), - CleanWhitespace::Collapse => b" ", - CleanWhitespace::All => b"", - }; - let text = MULTIPLE.replace_all(text, replacement); - replace_all_cow(&AT_EOL, text, b"") -} - -/// Helper to call [`Regex::replace_all`] with `Cow` as input and output. -fn replace_all_cow<'a>( - regex: &Regex, - haystack: Cow<'a, [u8]>, - replacement: &[u8], -) -> Cow<'a, [u8]> { - match haystack { - Cow::Borrowed(haystack) => regex.replace_all(haystack, replacement), - Cow::Owned(haystack) => { - Cow::Owned(regex.replace_all(&haystack, replacement).into_owned()) +pub fn clean_whitespace(text: &mut Vec<u8>, how: CleanWhitespace) { + // We copy text[i] to text[w], where w advances more slowly than i. + let mut w = 0; + match how { + // To match wsclean in mdiff.py, this removes "\f" (0xC). + CleanWhitespace::AtEol => { + let mut newline_dest = 0; + for i in 0..text.len() { + let char = text[i]; + match char { + b' ' | b'\t' | b'\r' | 0xC => {} + _ => { + if char == b'\n' { + w = newline_dest; + } + newline_dest = w + 1; + } + } + text[w] = char; + w += 1; + } + } + // To match fixws in cext/bdiff.c, CleanWhitespace::Collapse and + // CleanWhitespace::All do *not* remove "\f". + CleanWhitespace::Collapse => { + for i in 0..text.len() { + match text[i] { + b' ' | b'\t' | b'\r' => { + if w == 0 || text[w - 1] != b' ' { + text[w] = b' '; + w += 1; + } + } + b'\n' if w > 0 && text[w - 1] == b' ' => { + text[w - 1] = b'\n'; + } + char => { + text[w] = char; + w += 1; + } + } + } + } + CleanWhitespace::All => { + for i in 0..text.len() { + match text[i] { + b' ' | b'\t' | b'\r' => {} + char => { + text[w] = char; + w += 1; + } + } + } } } @@ -348,5 +367,6 @@ } } + text.truncate(w); } #[cfg(test)] @@ -378,4 +398,57 @@ assert_eq!(short_user(b"First Last <user@example.com>"), b"user"); assert_eq!(short_user(b"First Last <user.name@example.com>"), b"user"); } + + fn clean_ws(text: &[u8], how: CleanWhitespace) -> Vec<u8> { + let mut vec = text.to_vec(); + clean_whitespace(&mut vec, how); + vec + } + + #[test] + fn test_clean_whitespace_at_eol() { + // To match wsclean in mdiff.py, CleanWhitespace::AtEol only removes + // the final line's trailing whitespace if it ends in \n. + use CleanWhitespace::AtEol; + assert_eq!(clean_ws(b"", AtEol), b""); + assert_eq!(clean_ws(b" ", AtEol), b" "); + assert_eq!(clean_ws(b" ", AtEol), b" "); + assert_eq!(clean_ws(b"A", AtEol), b"A"); + assert_eq!(clean_ws(b"\n\n\n", AtEol), b"\n\n\n"); + assert_eq!(clean_ws(b" \n", AtEol), b"\n"); + assert_eq!(clean_ws(b"A \n", AtEol), b"A\n"); + assert_eq!(clean_ws(b"A B C\t\r\n", AtEol), b"A B C\n"); + assert_eq!(clean_ws(b"A \tB C\r\nD ", AtEol), b"A \tB C\nD "); + assert_eq!(clean_ws(b"A\x0CB\x0C\n", AtEol), b"A\x0CB\n"); + } + + #[test] + fn test_clean_whitespace_collapse() { + use CleanWhitespace::Collapse; + assert_eq!(clean_ws(b"", Collapse), b""); + assert_eq!(clean_ws(b" ", Collapse), b" "); + assert_eq!(clean_ws(b" ", Collapse), b" "); + assert_eq!(clean_ws(b"A", Collapse), b"A"); + assert_eq!(clean_ws(b"\n\n\n", Collapse), b"\n\n\n"); + assert_eq!(clean_ws(b" \n", Collapse), b"\n"); + assert_eq!(clean_ws(b"A \n", Collapse), b"A\n"); + assert_eq!(clean_ws(b"A B C\t\r\n", Collapse), b"A B C\n"); + assert_eq!(clean_ws(b"A \tB C\r\nD ", Collapse), b"A B C\nD "); + assert_eq!(clean_ws(b"A\x0CB\x0C\n", Collapse), b"A\x0CB\x0C\n"); + } + + #[test] + fn test_clean_whitespace_all() { + use CleanWhitespace::All; + assert_eq!(clean_ws(b"", All), b""); + assert_eq!(clean_ws(b" ", All), b""); + assert_eq!(clean_ws(b" ", All), b""); + assert_eq!(clean_ws(b"A", All), b"A"); + assert_eq!(clean_ws(b"\n\n\n", All), b"\n\n\n"); + assert_eq!(clean_ws(b" \n", All), b"\n"); + assert_eq!(clean_ws(b"A \n", All), b"A\n"); + assert_eq!(clean_ws(b"A B C\t\r\n", All), b"ABC\n"); + assert_eq!(clean_ws(b"A \tB C\r\nD ", All), b"ABC\nD"); + assert_eq!(clean_ws(b"A\x0CB\x0C\n", All), b"A\x0CB\x0C\n"); + } } diff --git a/rust/rhg/src/commands/annotate.rs b/rust/rhg/src/commands/annotate.rs index d81714a1c88d5143d3999afaa6e4eab4f32f590e_cnVzdC9yaGcvc3JjL2NvbW1hbmRzL2Fubm90YXRlLnJz..cc30bead2d6153a7477785aa1495e5f6f155d62e_cnVzdC9yaGcvc3JjL2NvbW1hbmRzL2Fubm90YXRlLnJz 100644 --- a/rust/rhg/src/commands/annotate.rs +++ b/rust/rhg/src/commands/annotate.rs @@ -161,5 +161,5 @@ treat_binary_as_text: args.get_flag("text"), follow_copies: !args.get_flag("no-follow"), whitespace: if args.get_flag("ignore-all-space") { - CleanWhitespace::All + Some(CleanWhitespace::All) } else if args.get_flag("ignore-space-change") { @@ -165,3 +165,3 @@ } else if args.get_flag("ignore-space-change") { - CleanWhitespace::Collapse + Some(CleanWhitespace::Collapse) } else if args.get_flag("ignore-space-at-eol") { @@ -167,5 +167,5 @@ } else if args.get_flag("ignore-space-at-eol") { - CleanWhitespace::AtEol + Some(CleanWhitespace::AtEol) } else { // We ignore the --ignore-blank-lines flag (present for consistency // with other commands) since it has no effect on annotate. @@ -169,7 +169,7 @@ } else { // We ignore the --ignore-blank-lines flag (present for consistency // with other commands) since it has no effect on annotate. - CleanWhitespace::None + None }, };