Files
Kigi-CLI/crates/codegen/kigi-ratatui-textarea/src/textarea/soft_wrap_nav_tests.rs
T
ZacharyZhang-NY 443de79e2f fix(textarea): Up and Down land on the soft-wrapped row they target
A soft-wrapped row and the next row share a byte: the row's exclusive end
is the next row's start. Up or Down with a goal column past the target
row's width put the cursor on that byte, so it drew on the wrong row. Up
made no progress and Down skipped a row.

`row_with_end` tells a soft wrap from a hard break (byte-adjacent rows
are soft). `move_to_display_col_on_line` takes the row and its `RowEnd`
and clamps a soft row to its last grapheme, or to the start of an element
that closes the row. A hard-break row still reaches its newline.
`clamp_to_line` now cuts on graphemes, not chars, so a ZWJ cluster stays
whole.

Kigi differs from upstream's premise that elements wrap whole: with no
display override in the buffer, `wrapped_lines` uses plain textwrap and
an element can span rows. Snapping to such an element's start would
leave the target row (Down from byte 0 stuck at 0), so an element that
began on an earlier row keeps the boundary `display_col_to_buffer_pos`
chose, as before this change.

`wrapped_navigation_with_zwj_graphemes` asserted the bug: Up from the
end stayed on the last row (byte 22). It now expects row 1's cluster
start (byte 11), as upstream does.

Tests: upstream's four cases, the row-spanning element, and a seeded
property test (500 cases): each Up or Down moves exactly one row. The
property test fails with the clamp disabled.

Ported from upstream (grok-build ratatui-textarea/textarea.rs, 1.0.28).
2026-09-19 02:17:19 -04:00

122 lines
3.3 KiB
Rust

//! Up/Down across soft-wrapped rows: a soft row's exclusive end is the next row's start.
use rand::prelude::*;
use ratatui::layout::Rect;
use ratatui::text::Line;
use super::{ElementKind, TextArea};
fn ta_with(text: &str) -> TextArea {
let mut t = TextArea::new();
t.insert_str(text);
t
}
fn cursor_row(t: &TextArea, width: u16) -> usize {
TextArea::wrapped_line_index_by_start(&t.wrapped_lines(width), t.cursor()).unwrap()
}
#[test]
fn up_from_wide_row_lands_on_last_char_of_narrower_soft_row() {
let width = 8;
let mut t = ta_with("aaa bb ccccccc");
assert_eq!(vec![0..7, 7..14], *t.wrapped_lines(width));
t.move_cursor_up();
assert_eq!(6, t.cursor());
assert_eq!(0, cursor_row(&t, width));
assert_eq!(Some((6, 0)), t.cursor_pos(Rect::new(0, 0, width, 5)));
t.move_cursor_down();
assert_eq!(14, t.cursor());
t.move_cursor_up();
assert_eq!(6, t.cursor());
t.move_cursor_up();
assert_eq!(0, t.cursor());
}
#[test]
fn up_onto_newline_terminated_row_lands_on_the_newline() {
let mut t = ta_with("ab\ncdef");
assert_eq!(vec![0..2, 3..7], *t.wrapped_lines(8));
t.move_cursor_up();
assert_eq!(2, t.cursor());
assert_eq!(0, cursor_row(&t, 8));
}
#[test]
fn down_from_wide_row_lands_on_last_char_of_narrower_soft_row() {
let mut t = ta_with("ccccccc aaa bb ddddddd");
assert_eq!(vec![0..8, 8..15, 15..22], *t.wrapped_lines(8));
t.set_cursor(7);
t.move_cursor_down();
assert_eq!(14, t.cursor());
assert_eq!(1, cursor_row(&t, 8));
t.move_cursor_down();
assert_eq!(22, t.cursor());
}
#[test]
fn up_onto_soft_row_closed_by_element_lands_before_the_element() {
let mut t = TextArea::new();
t.insert_element("xyz", ElementKind(0), Some(Line::from("[ELEM]")));
t.insert_str("longword");
assert_eq!(vec![0..3, 3..11], *t.wrapped_lines(8));
t.move_cursor_up();
assert_eq!(0, t.cursor());
assert_eq!(0, cursor_row(&t, 8));
}
#[test]
fn down_through_element_spanning_rows_reaches_the_end() {
let mut t = TextArea::new();
t.insert_element("abcdefgh", ElementKind(0), None);
t.insert_str("z");
assert_eq!(vec![0..3, 3..6, 6..9], *t.wrapped_lines(3));
t.set_cursor(0);
t.move_cursor_down();
assert_eq!(8, t.cursor());
}
#[test]
fn up_and_down_move_one_row_at_a_time() {
let mut rng = StdRng::seed_from_u64(7);
for _ in 0..500 {
let words = rng.random_range(1..12);
let text = (0..words)
.map(|_| "x".repeat(rng.random_range(1..9)))
.collect::<Vec<_>>()
.join(" ");
let width = rng.random_range(3..12);
let mut t = ta_with(&text);
let rows = t.wrapped_lines(width).len();
t.set_cursor(rng.random_range(0..=text.len()));
let mut row = cursor_row(&t, width);
while row + 1 < rows {
t.move_cursor_down();
assert_eq!(
row + 1,
cursor_row(&t, width),
"down in {text:?} at width {width}"
);
row += 1;
}
while row > 0 {
t.move_cursor_up();
assert_eq!(
row - 1,
cursor_row(&t, width),
"up in {text:?} at width {width}"
);
row -= 1;
}
}
}