1use unicode_segmentation::UnicodeSegmentation;
2use unicode_width::UnicodeWidthChar;
3use unicode_width::UnicodeWidthStr;
4
5#[allow(dead_code)]
6#[must_use]
7pub fn display_width(s: &str) -> usize {
15 s.width()
16}
17
18#[allow(dead_code)]
19#[must_use]
20pub fn char_display_width(c: char) -> usize {
28 c.width().unwrap_or(0)
29}
30
31#[must_use]
32pub fn char_to_byte(line: &str, char_idx: usize) -> usize {
42 line.char_indices()
43 .nth(char_idx)
44 .map_or(line.len(), |(idx, _)| idx)
45}
46
47#[must_use]
48pub fn byte_to_char(line: &str, byte_offset: usize) -> usize {
58 let byte_offset = byte_offset.min(line.len());
59 line[..byte_offset].chars().count()
60}
61
62#[must_use]
63pub fn next_grapheme_boundary(s: &str, byte_offset: usize) -> Option<usize> {
74 let graphemes: Vec<(usize, &str)> = s.grapheme_indices(true).collect();
75
76 for i in 0..graphemes.len() {
77 let (start, _grapheme) = graphemes[i];
78 let end = if i + 1 < graphemes.len() {
79 graphemes[i + 1].0
80 } else {
81 s.len()
82 };
83
84 if byte_offset >= start && byte_offset < end {
85 return Some(end);
86 }
87 }
88
89 None
90}
91
92#[must_use]
93pub fn prev_grapheme_boundary(s: &str, byte_offset: usize) -> Option<usize> {
104 let graphemes: Vec<(usize, &str)> = s.grapheme_indices(true).collect();
105
106 for i in (0..graphemes.len()).rev() {
107 if graphemes[i].0 < byte_offset {
108 return Some(graphemes[i].0);
109 }
110 }
111
112 None
113}
114
115#[must_use]
116pub fn char_to_column(line: &str, char_idx: usize) -> usize {
125 line.chars().take(char_idx).map(char_display_width).sum()
126}
127
128#[must_use]
129pub fn column_to_char(line: &str, target_column: usize) -> usize {
130 let mut current_column = 0;
131
132 for (index, c) in line.chars().enumerate() {
133 let char_width = char_display_width(c);
134 if current_column + char_width > target_column {
135 return index;
136 }
137 current_column += char_width;
138 }
139
140 line.chars().count()
141}
142
143#[cfg(test)]
144mod test {
145 use super::*;
146
147 #[test]
148 fn test_display_width() {
149 let test_str = "test";
150 assert_eq!(display_width(test_str), 4);
151 }
152
153 #[test]
154 fn test_char_display_width() {
155 let test_char = 't';
156 assert_eq!(char_display_width(test_char), 1);
157 }
158
159 #[test]
160 fn test_char_display_width_with_emoji() {
161 let emoji = '🌊';
162 assert_eq!(char_display_width(emoji), 2);
163 }
164
165 #[test]
166 fn test_char_to_byte() {
167 let test_line = "This is a test line";
168 let char_idx = 9;
169 assert_eq!(char_to_byte(test_line, char_idx), 9);
170 }
171
172 #[test]
173 fn test_char_to_byte_with_emoji() {
174 let test_line = "This is 🌊 a test line";
175 let char_idx = 9;
176 assert_eq!(char_to_byte(test_line, char_idx), 12);
177 }
178
179 #[test]
180 fn test_byte_to_char() {
181 let test_line = "This is a test line";
182 let byte_offset = 9;
183 assert_eq!(byte_to_char(test_line, byte_offset), 9);
184 }
185
186 #[test]
187 fn test_byte_to_char_with_emoji() {
188 let test_line = "This is 🌊 a test line";
189 let byte_offset = 8;
190 assert_eq!(byte_to_char(test_line, byte_offset), 8);
191 }
192
193 #[test]
194 fn test_prev_grapheme_boundary() {
195 let test_line = "Jesus Christ is the son of God";
196 let byte_offset = 4;
197 assert_eq!(prev_grapheme_boundary(test_line, byte_offset).unwrap(), 3);
198 }
199}