use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthChar;
use unicode_width::UnicodeWidthStr;
#[allow(dead_code)]
#[must_use]
pub fn display_width(s: &str) -> usize {
s.width()
}
#[allow(dead_code)]
#[must_use]
pub fn char_display_width(c: char) -> usize {
c.width().unwrap_or(0)
}
#[must_use]
pub fn char_to_byte(line: &str, char_idx: usize) -> usize {
line.char_indices()
.nth(char_idx)
.map_or(line.len(), |(idx, _)| idx)
}
#[must_use]
pub fn byte_to_char(line: &str, byte_offset: usize) -> usize {
let byte_offset = byte_offset.min(line.len());
line[..byte_offset].chars().count()
}
#[must_use]
pub fn next_grapheme_boundary(s: &str, byte_offset: usize) -> Option<usize> {
let graphemes: Vec<(usize, &str)> = s.grapheme_indices(true).collect();
for i in 0..graphemes.len() {
let (start, _grapheme) = graphemes[i];
let end = if i + 1 < graphemes.len() {
graphemes[i + 1].0
} else {
s.len()
};
if byte_offset >= start && byte_offset < end {
return Some(end);
}
}
None
}
#[must_use]
pub fn prev_grapheme_boundary(s: &str, byte_offset: usize) -> Option<usize> {
let graphemes: Vec<(usize, &str)> = s.grapheme_indices(true).collect();
for i in (0..graphemes.len()).rev() {
if graphemes[i].0 < byte_offset {
return Some(graphemes[i].0);
}
}
None
}
#[must_use]
pub fn char_to_column(line: &str, char_idx: usize) -> usize {
line.chars().take(char_idx).map(char_display_width).sum()
}
#[must_use]
pub fn column_to_char(line: &str, target_column: usize) -> usize {
let mut current_column = 0;
for (index, c) in line.chars().enumerate() {
let char_width = char_display_width(c);
if current_column + char_width > target_column {
return index;
}
current_column += char_width;
}
line.chars().count()
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_display_width() {
let test_str = "test";
assert_eq!(display_width(test_str), 4);
}
#[test]
fn test_char_display_width() {
let test_char = 't';
assert_eq!(char_display_width(test_char), 1);
}
#[test]
fn test_char_display_width_with_emoji() {
let emoji = '🌊';
assert_eq!(char_display_width(emoji), 2);
}
#[test]
fn test_char_to_byte() {
let test_line = "This is a test line";
let char_idx = 9;
assert_eq!(char_to_byte(test_line, char_idx), 9);
}
#[test]
fn test_char_to_byte_with_emoji() {
let test_line = "This is 🌊 a test line";
let char_idx = 9;
assert_eq!(char_to_byte(test_line, char_idx), 12);
}
#[test]
fn test_byte_to_char() {
let test_line = "This is a test line";
let byte_offset = 9;
assert_eq!(byte_to_char(test_line, byte_offset), 9);
}
#[test]
fn test_byte_to_char_with_emoji() {
let test_line = "This is 🌊 a test line";
let byte_offset = 8;
assert_eq!(byte_to_char(test_line, byte_offset), 8);
}
#[test]
fn test_prev_grapheme_boundary() {
let test_line = "Jesus Christ is the son of God";
let byte_offset = 4;
assert_eq!(prev_grapheme_boundary(test_line, byte_offset).unwrap(), 3);
}
}