fix(core): correct tab-stop alignment and width overflow in wrap_line_chars
- Extract TAB_WIDTH constant (4) replacing magic numbers - Calculate tab stop as TAB_WIDTH - (col % TAB_WIDTH) for proper alignment - Split tab expansion across rows when width < TAB_WIDTH - Update test_wrap_with_tab expected value for new behavior - Add tests: narrow width, stop alignment, line boundary, regression Fixes: #27
This commit is contained in:
@@ -3,9 +3,14 @@
|
|||||||
/// to avoid pathological cases on oversized lines.
|
/// to avoid pathological cases on oversized lines.
|
||||||
pub const MAX_WRAP_INPUT_LEN: usize = 10 * 1024 * 1024;
|
pub const MAX_WRAP_INPUT_LEN: usize = 10 * 1024 * 1024;
|
||||||
|
|
||||||
|
/// Column spacing for tab stop alignment.
|
||||||
|
const TAB_WIDTH: usize = 4;
|
||||||
|
|
||||||
/// Split a line into chunks of exactly `width` display columns.
|
/// Split a line into chunks of exactly `width` display columns.
|
||||||
/// For a log viewer, we want character-level wrapping, not word-level.
|
/// For a log viewer, we want character-level wrapping, not word-level.
|
||||||
/// Uses `unicode-width` for correct CJK/emoji/zero-width handling.
|
/// Uses `unicode-width` for correct CJK/emoji/zero-width handling.
|
||||||
|
/// Tab characters expand to the next tab-stop boundary and split across
|
||||||
|
/// rows when the expansion exceeds the remaining width.
|
||||||
pub fn wrap_line_chars(line: &str, width: usize) -> Vec<String> {
|
pub fn wrap_line_chars(line: &str, width: usize) -> Vec<String> {
|
||||||
use unicode_width::UnicodeWidthChar;
|
use unicode_width::UnicodeWidthChar;
|
||||||
|
|
||||||
@@ -19,29 +24,40 @@ pub fn wrap_line_chars(line: &str, width: usize) -> Vec<String> {
|
|||||||
let mut row = String::new();
|
let mut row = String::new();
|
||||||
let mut col = 0;
|
let mut col = 0;
|
||||||
for ch in line.chars() {
|
for ch in line.chars() {
|
||||||
let w = if ch == '\t' {
|
|
||||||
4
|
|
||||||
} else if ch.is_control() {
|
|
||||||
// Control characters (except tab): width 0, still pushed to preserve content.
|
|
||||||
// Visible rendering is the caller's responsibility.
|
|
||||||
0
|
|
||||||
} else {
|
|
||||||
ch.width().unwrap_or(0)
|
|
||||||
};
|
|
||||||
if col + w > width && !row.is_empty() {
|
|
||||||
result.push(std::mem::take(&mut row));
|
|
||||||
col = 0;
|
|
||||||
}
|
|
||||||
if ch == '\t' {
|
if ch == '\t' {
|
||||||
row.push_str(" ");
|
let tab_stop = TAB_WIDTH - (col % TAB_WIDTH);
|
||||||
col += 4;
|
let mut remaining = tab_stop;
|
||||||
|
while remaining > 0 {
|
||||||
|
let avail = width.saturating_sub(col);
|
||||||
|
let fill = remaining.min(avail);
|
||||||
|
for _ in 0..fill {
|
||||||
|
row.push(' ');
|
||||||
|
}
|
||||||
|
col += fill;
|
||||||
|
remaining -= fill;
|
||||||
|
if col >= width {
|
||||||
|
result.push(std::mem::take(&mut row));
|
||||||
|
col = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
|
let w = if ch.is_control() {
|
||||||
|
// Control characters (except tab): width 0, still pushed to preserve content.
|
||||||
|
// Visible rendering is the caller's responsibility.
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
ch.width().unwrap_or(0)
|
||||||
|
};
|
||||||
|
if col + w > width && !row.is_empty() {
|
||||||
|
result.push(std::mem::take(&mut row));
|
||||||
|
col = 0;
|
||||||
|
}
|
||||||
row.push(ch);
|
row.push(ch);
|
||||||
col += w;
|
col += w;
|
||||||
}
|
if col >= width {
|
||||||
if col >= width {
|
result.push(std::mem::take(&mut row));
|
||||||
result.push(std::mem::take(&mut row));
|
col = 0;
|
||||||
col = 0;
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !row.is_empty() {
|
if !row.is_empty() {
|
||||||
@@ -108,7 +124,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_wrap_with_tab() {
|
fn test_wrap_with_tab() {
|
||||||
let result = wrap_line_chars("a\tb", 4);
|
let result = wrap_line_chars("a\tb", 4);
|
||||||
assert_eq!(result, vec!["a", " ", "b"]);
|
assert_eq!(result, vec!["a ", "b"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -188,4 +204,31 @@ mod tests {
|
|||||||
let result = wrap_line_chars("你好", 1);
|
let result = wrap_line_chars("你好", 1);
|
||||||
assert_eq!(result, vec!["你", "好"]);
|
assert_eq!(result, vec!["你", "好"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_tab_narrow_width() {
|
||||||
|
let result = wrap_line_chars("\t", 2);
|
||||||
|
assert_eq!(result, vec![" ", " "]);
|
||||||
|
let result = wrap_line_chars("\t", 1);
|
||||||
|
assert_eq!(result, vec![" ", " ", " ", " "]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_tab_stop_alignment() {
|
||||||
|
assert_eq!(wrap_line_chars("a\tb", 8), vec!["a b"]);
|
||||||
|
assert_eq!(wrap_line_chars("ab\t", 4), vec!["ab "]);
|
||||||
|
assert_eq!(wrap_line_chars("abc\tb", 8), vec!["abc b"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_tab_at_line_boundary() {
|
||||||
|
let result = wrap_line_chars("a\tb", 4);
|
||||||
|
assert_eq!(result, vec!["a ", "b"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_tab_regression_ab_tab() {
|
||||||
|
let result = wrap_line_chars("ab\t", 4);
|
||||||
|
assert_eq!(result, vec!["ab "]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user