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).
This commit is contained in:
2 files changed
+191
-48
No files matched your search
@@ -0,0 +1,121 @@
|
||||
//! 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user