Skip to main content

tsu_editor/
unicode.rs

1use unicode_segmentation::UnicodeSegmentation;
2use unicode_width::UnicodeWidthChar;
3use unicode_width::UnicodeWidthStr;
4
5#[allow(dead_code)]
6#[must_use]
7/// Calculates the width of the string when displayed in the terminal
8///
9/// # Arguments
10/// - `s`: Some string of which the display width will be calculated
11///
12/// # Returns
13/// - The width of the input string when it is displayed in the terminal
14pub fn display_width(s: &str) -> usize {
15    s.width()
16}
17
18#[allow(dead_code)]
19#[must_use]
20/// Calculates the width of the char when displayed in the terminal
21///
22/// # Arguments
23/// - `c`: Some character of which the diplay width will be calculated
24///
25/// # Returns
26/// - The width of the input character when it is displayed in the terminal
27pub fn char_display_width(c: char) -> usize {
28    c.width().unwrap_or(0)
29}
30
31#[must_use]
32/// Convert a character to a byte representation
33///
34/// # Arguments
35/// -`line`: A line from which the character will be retrieved
36/// - `char_idx`: The index of the character relative to its position within the provided line
37///
38/// # Returns
39/// - The byte representation of the retrieved character if the given index is within the bounds of
40///   the provided line (i.e., less than the line length)
41pub 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]
48/// Convert a byte to a character representation
49///
50/// # Arguments
51/// - `line`: A line from with the byte will be retrieved
52/// - `byte_offset`: The byte offset of the byte relative to its position within the provided line
53///
54/// # Returns
55/// - The character representation of the retrieved byte if the given byte offset is within the
56///   bounds of the provided line (i.e., less than the line length)
57pub 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]
63/// Query the byte boundary of the next grapheme in a given string
64///
65/// # Arguments
66/// - `s`: A string containing the grapheme in question
67/// - `byte_offset`: The byte offset of the grapheme preceding the grapheme of interest
68///
69/// # Returns
70/// - The byte boundary of the grapheme succeeding the grapheme corresponding to the byte offset
71///   provided if the byte offset is within the bounds of the string, i.e., less than the length of
72///   the string
73pub 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]
93/// Query the byte boundary of the previous grapheme in a given string
94///
95/// # Arguments
96/// - `s`: A string containing the grapheme in question
97/// - `byte_offset`: The byte offset of the grapheme succeeding the grapheme of interest
98///
99/// # Returns
100/// - The byte boundary of the grapheme preceding the grapheme corresponding to the byte offset
101///   provided if the byte offset is within the bounds of the string, i.e., less than the length of
102///   the string
103pub 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]
116/// Convert a character index to a column index within the view
117///
118/// # Arguments
119/// - `line`: The string of the line containing the character in question
120/// - `char_idx`: The index of the character in the provided line
121///
122/// # Returns
123/// - The column index of the character based on its index within the line
124pub 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}