1use unicode_width::UnicodeWidthChar;
10
11pub fn visual_line_count(text: &str, width: usize) -> usize {
19 if text.is_empty() {
20 return 1;
21 }
22 let w = width.max(1);
23 let mut rows = 1usize;
24 let mut col = 0usize;
25 let mut word_start_col = 0usize;
26 let mut in_word = false;
27
28 for ch in text.chars() {
29 let char_w = ch.width().unwrap_or(0);
30 let is_space = ch == ' ' || ch == '\t';
31
32 if is_space {
33 in_word = false;
34 if col + char_w > w {
35 rows += 1;
36 col = char_w;
37 } else {
38 col += char_w;
39 }
40 word_start_col = col;
41 } else {
42 if !in_word {
43 word_start_col = col;
44 in_word = true;
45 }
46 if col + char_w > w {
47 if word_start_col > 0 && word_start_col <= w {
48 rows += 1;
51 let word_len_so_far = col - word_start_col;
52 col = word_len_so_far + char_w;
53 word_start_col = 0;
54 } else {
55 rows += 1;
57 col = char_w;
58 word_start_col = 0;
59 }
60 } else {
61 col += char_w;
62 }
63 }
64 }
65 rows
66}
67
68#[cfg(test)]
69mod tests {
70 use super::*;
71
72 #[test]
73 fn short_line() {
74 assert_eq!(visual_line_count("hello", 80), 1);
75 }
76
77 #[test]
78 fn empty_line() {
79 assert_eq!(visual_line_count("", 80), 1);
80 }
81
82 #[test]
83 fn char_wrap() {
84 assert_eq!(visual_line_count(&"x".repeat(160), 80), 2);
85 }
86
87 #[test]
88 fn word_wrap() {
89 let line = format!("{} {}", "a".repeat(75), "b".repeat(75));
90 assert_eq!(visual_line_count(&line, 80), 2);
91 }
92
93 #[test]
94 fn word_longer_than_width() {
95 assert_eq!(visual_line_count(&"x".repeat(200), 80), 3);
96 }
97
98 #[test]
99 fn exact_width() {
100 assert_eq!(visual_line_count(&"x".repeat(80), 80), 1);
101 }
102
103 #[test]
104 fn exact_width_plus_one() {
105 assert_eq!(visual_line_count(&"x".repeat(81), 80), 2);
106 }
107
108 #[test]
109 fn word_wrap_breaks_before_word() {
110 let text = format!("{} foobar", "a".repeat(76));
111 assert_eq!(visual_line_count(&text, 80), 2);
112 }
113}