indigo_kernel/
display_width.rs1use ropey::{Rope, RopeSlice};
6use std::cmp::max;
7use unicode_segmentation::UnicodeSegmentation as _;
8use unicode_width::UnicodeWidthStr;
9
10pub trait DisplayWidth {
11 fn display_width(&self) -> usize;
12}
13
14impl DisplayWidth for char {
15 fn display_width(&self) -> usize {
16 str_width(self.encode_utf8(&mut [0; 4]))
17 }
18}
19
20impl DisplayWidth for &str {
21 fn display_width(&self) -> usize {
22 str_width(self)
23 }
24}
25
26impl DisplayWidth for RopeSlice<'_> {
27 fn display_width(&self) -> usize {
28 let (chunk, _) = self.chunk(0);
30 if chunk.len() == self.len() {
31 return str_width(chunk);
32 }
33 str_width(&String::from(*self))
34 }
35}
36
37impl DisplayWidth for Rope {
38 fn display_width(&self) -> usize {
39 self.slice(..).display_width()
40 }
41}
42
43const TAB_WIDTH: usize = 8;
44
45fn str_width(str: &str) -> usize {
46 let bytes = str.as_bytes();
47 if bytes.is_ascii() {
48 return ascii_width(bytes);
49 }
50 let joined = |&a: &u8, &b: &u8| !(a.is_ascii() && b.is_ascii()) || (a == b'\r' && b == b'\n');
54 let mut width = 0;
55 let mut start = 0;
56 for run in bytes.chunk_by(joined) {
57 let end = start + run.len();
58 width += if run.is_ascii() {
59 ascii_width(run)
60 } else {
61 str[start..end].graphemes(true).map(grapheme_width).sum()
62 };
63 start = end;
64 }
65 width
66}
67
68fn ascii_width(bytes: &[u8]) -> usize {
70 let tabs = bytes
71 .iter()
72 .map(|&byte| usize::from(byte == b'\t'))
73 .sum::<usize>();
74 let crlfs = bytes.windows(2).filter(|pair| pair == b"\r\n").count();
75 bytes.len() + (TAB_WIDTH - 1) * tabs - crlfs
76}
77
78fn grapheme_width(grapheme: &str) -> usize {
79 if let Some('\t') = grapheme.chars().next() {
80 return TAB_WIDTH;
81 }
82 max(1, UnicodeWidthStr::width(grapheme))
83}
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88 use hegel::{TestCase, generators as gs};
89 use std::str;
90
91 fn oracle(str: &str) -> usize {
92 str.graphemes(true).map(grapheme_width).sum()
93 }
94
95 fn text_gen() -> gs::TextGenerator {
96 const ALPHABET: &str = "ab1 \t\r\n\x00\x7f\u{0301}\u{200D}\u{FE0F}\u{20E3}\u{0600}\u{0602}π―π΅π©β€π\u{1F3FB}δΈΓ©\u{1100}\u{1161}\u{0915}\u{094D}\u{0937}";
98 gs::text().alphabet(ALPHABET).max_size(64)
99 }
100
101 #[hegel::test(test_cases = 20_000)]
102 fn str_width_matches_oracle(tc: TestCase) {
103 let text: String = tc.draw(text_gen());
104 assert_eq!(text.as_str().display_width(), oracle(&text));
105 for char in text.chars() {
106 assert_eq!(char.display_width(), oracle(char.encode_utf8(&mut [0; 4])));
107 }
108 }
109
110 #[hegel::test(test_cases = 2_000)]
111 fn rope_slice_width_matches_oracle(tc: TestCase) {
112 let text: String = tc.draw(text_gen().min_size(1));
115 let repeats = tc.draw(
116 gs::integers::<usize>()
117 .min_value(1)
118 .max_value(16 * 1024 / text.len()),
119 );
120 let string = text.repeat(repeats);
121 let rope = Rope::from_str(&string);
122 let start = tc.draw(gs::integers::<usize>().max_value(string.len()));
123 let end = tc.draw(
124 gs::integers::<usize>()
125 .min_value(start)
126 .max_value(string.len()),
127 );
128 let start = str::floor_char_boundary(&string, start);
129 let end = str::floor_char_boundary(&string, end);
130 assert_eq!(
131 rope.slice(start..end).display_width(),
132 oracle(&string[start..end])
133 );
134 }
135}