use std::ops::Range;
use unicode_width::UnicodeWidthChar;
pub fn chunk_buffer(buffer: &str, width: usize) -> Vec<Range<usize>> {
if width == 0 || buffer.is_empty() {
let empty: Range<usize> = 0..0;
return vec![empty];
}
let mut chunks = Vec::new();
let mut row_start = 0;
let mut col = 0;
for (i, ch) in buffer.char_indices() {
let ch_width = ch.width().unwrap_or(0);
if ch_width > 0 && col + ch_width > width {
chunks.push(row_start..i);
row_start = i;
col = ch_width;
} else {
col += ch_width;
}
}
chunks.push(row_start..buffer.len());
chunks
}
pub fn cursor_to_visual(buffer: &str, cursor: usize, width: usize) -> (usize, usize) {
let chunks = chunk_buffer(buffer, width);
if cursor == buffer.len() && width > 0 {
let last = chunks.last().unwrap();
let last_width: usize = buffer[last.clone()]
.chars()
.map(|c| c.width().unwrap_or(0))
.sum();
if last_width == width && !buffer.is_empty() {
return (chunks.len(), 0);
}
}
for (row, chunk) in chunks.iter().enumerate() {
let in_chunk = if row == chunks.len() - 1 {
cursor >= chunk.start && cursor <= chunk.end
} else {
cursor >= chunk.start && cursor < chunk.end
};
if in_chunk {
let col: usize = buffer[chunk.start..cursor]
.chars()
.map(|c| c.width().unwrap_or(0))
.sum();
return (row, col);
}
}
let last = chunks.len().saturating_sub(1);
let chunk = &chunks[last];
let col: usize = buffer[chunk.start..cursor.min(chunk.end)]
.chars()
.map(|c| c.width().unwrap_or(0))
.sum();
(last, col)
}
pub fn visual_to_cursor(buffer: &str, row: usize, col: usize, width: usize) -> usize {
let chunks = chunk_buffer(buffer, width);
if row >= chunks.len() {
return buffer.len();
}
let chunk = &chunks[row];
let mut accumulated = 0;
for (i, ch) in buffer[chunk.clone()].char_indices() {
let ch_width = ch.width().unwrap_or(0);
if accumulated + ch_width > col {
if col.saturating_sub(accumulated) > ch_width / 2 {
return chunk.start + i + ch.len_utf8();
}
return chunk.start + i;
}
accumulated += ch_width;
}
chunk.end
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_chunk_empty() {
assert_eq!(chunk_buffer("", 10), vec![0..0]);
}
#[test]
fn test_chunk_zero_width() {
assert_eq!(chunk_buffer("hello", 0), vec![0..0]);
}
#[test]
fn test_chunk_fits() {
assert_eq!(chunk_buffer("hello", 10), vec![0..5]);
}
#[test]
fn test_chunk_exact_fit() {
assert_eq!(chunk_buffer("hello", 5), vec![0..5]);
}
#[test]
fn test_chunk_wraps() {
let chunks = chunk_buffer("hello world", 5);
assert_eq!(chunks.len(), 3);
assert_eq!(&"hello world"[chunks[0].clone()], "hello");
assert_eq!(&"hello world"[chunks[1].clone()], " worl");
assert_eq!(&"hello world"[chunks[2].clone()], "d");
}
#[test]
fn test_chunk_cjk_bump() {
let chunks = chunk_buffer("a世b", 2);
assert_eq!(chunks.len(), 3);
assert_eq!(&"a世b"[chunks[0].clone()], "a");
assert_eq!(&"a世b"[chunks[1].clone()], "世");
assert_eq!(&"a世b"[chunks[2].clone()], "b");
}
#[test]
fn test_chunk_cjk_exact() {
let chunks = chunk_buffer("世界", 4);
assert_eq!(chunks.len(), 1);
assert_eq!(&"世界"[chunks[0].clone()], "世界");
}
#[test]
fn test_chunk_single_char_per_row() {
let chunks = chunk_buffer("abc", 1);
assert_eq!(chunks.len(), 3);
assert_eq!(&"abc"[chunks[0].clone()], "a");
assert_eq!(&"abc"[chunks[1].clone()], "b");
assert_eq!(&"abc"[chunks[2].clone()], "c");
}
#[test]
fn test_cursor_visual_start() {
assert_eq!(cursor_to_visual("hello", 0, 10), (0, 0));
}
#[test]
fn test_cursor_visual_end() {
assert_eq!(cursor_to_visual("hello", 5, 10), (0, 5));
}
#[test]
fn test_cursor_visual_wrapped() {
assert_eq!(cursor_to_visual("hello world", 5, 5), (1, 0));
assert_eq!(cursor_to_visual("hello world", 6, 5), (1, 1));
}
#[test]
fn test_cursor_visual_empty() {
assert_eq!(cursor_to_visual("", 0, 10), (0, 0));
}
#[test]
fn test_cursor_visual_phantom_row() {
assert_eq!(cursor_to_visual("hello", 5, 5), (1, 0));
}
#[test]
fn test_visual_to_cursor_start() {
assert_eq!(visual_to_cursor("hello", 0, 0, 10), 0);
}
#[test]
fn test_visual_to_cursor_end() {
assert_eq!(visual_to_cursor("hello", 0, 5, 10), 5);
}
#[test]
fn test_visual_to_cursor_wrapped() {
assert_eq!(visual_to_cursor("hello world", 1, 0, 5), 5);
}
#[test]
fn test_visual_to_cursor_beyond_rows() {
assert_eq!(visual_to_cursor("hello", 5, 0, 10), 5);
}
#[test]
fn test_partition_is_exact() {
let buffer = "hello world";
let chunks = chunk_buffer(buffer, 5);
let reconstructed: String = chunks.iter().map(|r| &buffer[r.clone()]).collect();
assert_eq!(reconstructed, buffer);
}
#[test]
fn test_partition_is_exact_cjk() {
let buffer = "世界你好";
let chunks = chunk_buffer(buffer, 3);
let reconstructed: String = chunks.iter().map(|r| &buffer[r.clone()]).collect();
assert_eq!(reconstructed, buffer);
}
}