use unicode_width::UnicodeWidthChar;
pub fn char_count(input: &str) -> usize {
input.chars().count()
}
fn byte_index(input: &str, cursor: usize) -> usize {
input
.char_indices()
.nth(cursor)
.map(|(index, _)| index)
.unwrap_or(input.len())
}
pub fn insert(input: &mut String, cursor: &mut usize, character: char) {
input.insert(byte_index(input, *cursor), character);
*cursor += 1;
}
pub fn insert_text(input: &mut String, cursor: &mut usize, text: &str) {
let byte_index = input
.char_indices()
.nth(*cursor)
.map(|(index, _)| index)
.unwrap_or(input.len());
input.insert_str(byte_index, text);
*cursor += text.chars().count();
}
pub fn backspace(input: &mut String, cursor: &mut usize) {
if *cursor == 0 {
return;
}
*cursor -= 1;
input.remove(byte_index(input, *cursor));
}
pub fn delete(input: &mut String, cursor: usize) {
if cursor < char_count(input) {
input.remove(byte_index(input, cursor));
}
}
pub fn display_width_before(input: &str, cursor: usize) -> usize {
input
.chars()
.take(cursor)
.map(|character| character.width().unwrap_or(0))
.sum()
}
pub struct VisualLayout {
pub lines: Vec<String>,
pub cursor_row: usize,
pub cursor_col: usize,
}
pub fn visual_layout(input: &str, cursor: usize, width: usize) -> VisualLayout {
let width = width.max(1);
let mut lines = vec![String::new()];
let mut row = 0;
let mut col = 0;
let mut cursor_position = None;
for (index, character) in input.chars().enumerate() {
if index == cursor {
cursor_position = Some(normalize_cursor(row, col, width));
}
if character == '\n' {
lines.push(String::new());
row += 1;
col = 0;
continue;
}
let character_width = character.width().unwrap_or(0);
if character_width > 0 && col + character_width > width {
lines.push(String::new());
row += 1;
col = 0;
}
lines[row].push(character);
col += character_width;
}
let (cursor_row, cursor_col) =
cursor_position.unwrap_or_else(|| normalize_cursor(row, col, width));
while lines.len() <= cursor_row {
lines.push(String::new());
}
VisualLayout {
lines,
cursor_row,
cursor_col,
}
}
fn normalize_cursor(row: usize, col: usize, width: usize) -> (usize, usize) {
if col >= width {
(row + col / width, col % width)
} else {
(row, col)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn edits_multibyte_characters_at_character_boundaries() {
let mut input = "aé界".to_string();
let mut cursor = 2;
insert(&mut input, &mut cursor, '🙂');
assert_eq!(input, "aé🙂界");
assert_eq!(cursor, 3);
backspace(&mut input, &mut cursor);
assert_eq!(input, "aé界");
assert_eq!(cursor, 2);
delete(&mut input, cursor);
assert_eq!(input, "aé");
}
#[test]
fn display_width_handles_wide_and_combining_characters() {
assert_eq!(display_width_before("a界e\u{301}", 4), 4);
}
#[test]
fn insert_text_inserts_at_character_cursor() {
let mut input = "a界c".to_string();
let mut cursor = 2;
insert_text(&mut input, &mut cursor, "é\n");
assert_eq!(input, "a界é\nc");
assert_eq!(cursor, 4);
}
#[test]
fn visually_wraps_without_changing_the_input() {
let input = "abcdefghij";
let layout = visual_layout(input, input.chars().count(), 4);
assert_eq!(layout.lines, ["abcd", "efgh", "ij"]);
assert_eq!((layout.cursor_row, layout.cursor_col), (2, 2));
assert_eq!(input, "abcdefghij");
}
#[test]
fn cursor_tracks_wrapped_rows_when_moving_through_input() {
let at_end = visual_layout("abcdefghij", 10, 4);
assert_eq!((at_end.cursor_row, at_end.cursor_col), (2, 2));
let near_start = visual_layout("abcdefghij", 2, 4);
assert_eq!((near_start.cursor_row, near_start.cursor_col), (0, 2));
}
#[test]
fn explicit_newlines_are_hard_breaks() {
let layout = visual_layout("abc\ndefgh", 9, 3);
assert_eq!(layout.lines, ["abc", "def", "gh"]);
assert_eq!((layout.cursor_row, layout.cursor_col), (2, 2));
}
#[test]
fn wide_characters_wrap_by_terminal_width() {
let layout = visual_layout("a界bc", 4, 3);
assert_eq!(layout.lines, ["a界", "bc"]);
assert_eq!((layout.cursor_row, layout.cursor_col), (1, 2));
}
}