diff --git a/src/utils.rs b/src/utils.rs index 29ab93d..ae18673 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -182,7 +182,7 @@ pub struct FunctionLineFinder<'a> { } impl<'a> FunctionLineFinder<'a> { - /// Maximum number of characters of a function line that are shown. + /// Maximum number of bytes of a function line that are shown. const MAX_WIDTH: usize = 40; /// Returns `None` when there are no patterns, so no function lines. @@ -209,15 +209,20 @@ impl<'a> FunctionLineFinder<'a> { } } self.next_line = self.next_line.max(end); - let line = self.last_match?.trim_ascii_start(); - let end = match std::str::from_utf8(line) { - Ok(text) => text - .char_indices() - .nth(Self::MAX_WIDTH) - .map_or(line.len(), |(end, _)| end), - Err(_) => line.len().min(Self::MAX_WIDTH), - }; - Some(line[..end].trim_ascii_end().to_vec()) + // C's isspace(): unlike `is_ascii_whitespace`, it includes vertical tab. + let is_space = |byte: &u8| byte.is_ascii_whitespace() || *byte == b'\x0b'; + let line = self.last_match?; + let start = line + .iter() + .position(|byte| !is_space(byte)) + .unwrap_or(line.len()); + let line = &line[start..]; + let line = &line[..line.len().min(Self::MAX_WIDTH)]; + let end = line + .iter() + .rposition(|byte| !is_space(byte)) + .map_or(0, |last| last + 1); + Some(line[..end].to_vec()) } } diff --git a/tests/by-util/test_diff.rs b/tests/by-util/test_diff.rs index c6d4140..20c6677 100644 --- a/tests/by-util/test_diff.rs +++ b/tests/by-util/test_diff.rs @@ -873,6 +873,47 @@ fn function_lines() { .stderr_is("'\\(': Unmatched ( or \\(\n"); } +// A function line is cut at 40 bytes, even inside a multi-byte character. +#[test] +fn function_line_is_cut_at_40_bytes() { + let (at, ts) = at_and_ts!(); + let function_line = format!("func_{}()", "é".repeat(40)); + at.write("old", &format!("{function_line}\n1\n2\n3\n4\n5\n6\n")); + at.write("new", &format!("{function_line}\n1\n2\n3\n4\nX\n6\n")); + + let mut expected = b"@@ -3,5 +3,5 @@ ".to_vec(); + expected.extend(&function_line.as_bytes()[..40]); + expected.push(b'\n'); + + let result = ts + .ucmd() + .args(&["-u", "-p", "old", "new"]) + .fails_with_code(1); + let header = result + .stdout() + .split_inclusive(|&byte| byte == b'\n') + .nth(2); + assert_eq!(header, Some(expected.as_slice())); +} + +// Vertical tabs count as white space around a function line, as they do for +// C's isspace(). +#[test] +fn function_line_vertical_tabs_are_trimmed() { + let (at, ts) = at_and_ts!(); + at.write("old", "\x0b\x0c fg() \t\x0c\r\x0b\n1\n2\n3\n4\n5\n6\n"); + at.write("new", "\x0b\x0c fg() \t\x0c\r\x0b\n1\n2\n3\n4\nX\n6\n"); + + ts.ucmd() + .args(&["-u", "-F", "fg", "old", "new"]) + .fails_with_code(1) + .stdout_contains("\n@@ -3,5 +3,5 @@ fg()\n"); + ts.ucmd() + .args(&["-c", "-F", "fg", "old", "new"]) + .fails_with_code(1) + .stdout_contains("\n*************** fg()\n"); +} + #[cfg(unix)] #[test] fn quoted_file_names() {