1
0
зеркало из https://github.com/glebtv/tui-textarea.git synced 2026-08-28 11:36:17 +03:00
Files
tui-textarea/tests/atomic.rs
srothgan 9c4db800ef feat(textarea): add atomic range editing support (#19)
- add public AtomicRange API with validation and diagnostic errors
- make cursor movement, deletion, insertion, selection, undo/redo, and set_lines atom-aware
- clear caller-derived atomic ranges after successful content mutations
- add focused atomic range tests plus serde coverage
- document atomic ranges and add a caller-owned parsing example
2026-07-01 04:17:20 +10:00

256 строки
8.3 KiB
Rust

use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::widgets::Widget as _;
use tui_textarea::{
AtomicDeleteDirection, AtomicRange, AtomicRangeRejectReason, CursorMove, TextArea, WrapMode,
};
fn atom(start_col: usize, end_col: usize) -> AtomicRange {
AtomicRange {
row: 0,
start_col,
end_col,
}
}
fn render(textarea: &TextArea<'_>, width: u16, height: u16) {
let area = Rect {
x: 0,
y: 0,
width,
height,
};
let mut buf = Buffer::empty(area);
textarea.render(area, &mut buf);
}
#[test]
fn atomic_ranges_validate_sort_and_clear() {
let mut textarea = TextArea::from(["abcdef"]);
textarea
.try_set_atomic_ranges([atom(4, 6), atom(1, 3)])
.unwrap();
assert_eq!(textarea.atomic_ranges(), &[atom(1, 3), atom(4, 6)]);
textarea.clear_atomic_ranges();
assert!(textarea.atomic_ranges().is_empty());
textarea.set_atomic_ranges([atom(1, 3)]);
let err = textarea
.try_set_atomic_ranges([
atom(2, 2),
AtomicRange {
row: 9,
start_col: 0,
end_col: 1,
},
atom(5, 7),
atom(1, 4),
atom(3, 5),
])
.unwrap_err();
let reasons: Vec<_> = err
.rejected
.iter()
.map(|rejected| rejected.reason)
.collect();
assert_eq!(
reasons,
[
AtomicRangeRejectReason::Empty,
AtomicRangeRejectReason::OverlapsPrevious,
AtomicRangeRejectReason::ColumnOutOfBounds,
AtomicRangeRejectReason::RowOutOfBounds,
]
);
assert_eq!(textarea.atomic_ranges(), &[atom(1, 3)]);
textarea.set_atomic_ranges([]);
assert!(textarea.atomic_ranges().is_empty());
}
#[test]
#[should_panic(expected = "invalid atomic ranges")]
fn set_atomic_ranges_panics_on_invalid_ranges() {
let mut textarea = TextArea::from(["abc"]);
textarea.set_atomic_ranges([atom(1, 1)]);
}
#[test]
fn setting_atomic_ranges_normalizes_cursor_and_selection_start() {
let mut textarea = TextArea::from(["abcdef"]);
textarea.move_cursor(CursorMove::Jump(0, 2));
textarea.set_atomic_ranges([atom(1, 4)]);
assert_eq!(textarea.cursor(), (0, 4));
let mut textarea = TextArea::from(["abcdef"]);
textarea.move_cursor(CursorMove::Jump(0, 2));
textarea.start_selection();
textarea.move_cursor(CursorMove::Jump(0, 6));
textarea.set_atomic_ranges([atom(1, 4)]);
assert_eq!(textarea.selection_range(), Some(((0, 1), (0, 6))));
let mut textarea = TextArea::from(["abcdef"]);
textarea.move_cursor(CursorMove::Jump(0, 2));
textarea.start_selection();
textarea.move_cursor(CursorMove::Jump(0, 1));
textarea.set_atomic_ranges([atom(1, 4)]);
assert!(!textarea.is_selecting());
assert_eq!(textarea.cursor(), (0, 1));
}
#[test]
fn cursor_movement_skips_atom_interiors() {
let mut textarea = TextArea::from(["abXYZcd"]);
textarea.set_atomic_ranges([atom(2, 5)]);
textarea.move_cursor(CursorMove::Jump(0, 2));
textarea.move_cursor(CursorMove::Forward);
assert_eq!(textarea.cursor(), (0, 5));
textarea.move_cursor(CursorMove::Back);
assert_eq!(textarea.cursor(), (0, 2));
textarea.move_cursor(CursorMove::Jump(0, 3));
assert_eq!(textarea.cursor(), (0, 5));
let mut textarea = TextArea::from(["aa XYZ bb"]);
textarea.set_atomic_ranges([atom(3, 6)]);
textarea.move_cursor(CursorMove::Jump(0, 3));
textarea.move_cursor(CursorMove::WordEnd);
assert_eq!(textarea.cursor(), (0, 6));
}
#[test]
fn directional_atomic_deletion_respects_boundaries() {
let mut textarea = TextArea::from(["abXYZcd"]);
textarea.set_atomic_ranges([atom(2, 5)]);
textarea.move_cursor(CursorMove::Jump(0, 5));
assert_eq!(
textarea.atomic_range_at_cursor(AtomicDeleteDirection::Backward),
Some(atom(2, 5))
);
assert!(textarea.delete_char());
assert_eq!(textarea.lines(), ["abcd"]);
assert_eq!(textarea.cursor(), (0, 2));
assert!(textarea.atomic_ranges().is_empty());
assert!(textarea.undo());
assert_eq!(textarea.lines(), ["abXYZcd"]);
assert!(textarea.atomic_ranges().is_empty());
assert!(textarea.redo());
assert_eq!(textarea.lines(), ["abcd"]);
let mut textarea = TextArea::from(["abXYZcd"]);
textarea.set_atomic_ranges([atom(2, 5)]);
textarea.move_cursor(CursorMove::Jump(0, 2));
assert!(textarea.delete_next_char());
assert_eq!(textarea.lines(), ["abcd"]);
let mut textarea = TextArea::from(["abXYZcd"]);
textarea.set_atomic_ranges([atom(2, 5)]);
textarea.move_cursor(CursorMove::Jump(0, 2));
assert!(textarea.delete_char());
assert_eq!(textarea.lines(), ["aXYZcd"]);
let mut textarea = TextArea::from(["abXYZcd"]);
textarea.set_atomic_ranges([atom(2, 5)]);
textarea.move_cursor(CursorMove::Jump(0, 5));
assert!(textarea.delete_next_char());
assert_eq!(textarea.lines(), ["abXYZd"]);
}
#[test]
fn range_deletion_expands_partial_atom_coverage() {
let mut textarea = TextArea::from(["abXYZcd"]);
textarea.set_atomic_ranges([atom(2, 5)]);
textarea.move_cursor(CursorMove::Jump(0, 2));
assert!(textarea.delete_str(2));
assert_eq!(textarea.lines(), ["abcd"]);
assert_eq!(textarea.yank_text(), "XYZ");
assert_eq!(textarea.cursor(), (0, 2));
let mut textarea = TextArea::from(["abXYZcd"]);
textarea.set_atomic_ranges([atom(2, 5)]);
textarea.move_cursor(CursorMove::Jump(0, 1));
textarea.start_selection();
textarea.move_cursor(CursorMove::Jump(0, 3));
textarea.set_atomic_ranges([atom(2, 5)]);
assert!(textarea.cut());
assert_eq!(textarea.lines(), ["acd"]);
assert_eq!(textarea.yank_text(), "bXYZ");
assert_eq!(textarea.cursor(), (0, 1));
}
#[test]
fn word_and_line_deletions_use_atomic_boundaries() {
let mut textarea = TextArea::from(["abXYZ cd"]);
textarea.set_atomic_ranges([atom(2, 5)]);
textarea.move_cursor(CursorMove::Jump(0, 5));
assert!(textarea.delete_word());
assert_eq!(textarea.lines(), ["ab cd"]);
assert_eq!(textarea.cursor(), (0, 2));
let mut textarea = TextArea::from(["abXYZ cd"]);
textarea.set_atomic_ranges([atom(2, 5)]);
textarea.move_cursor(CursorMove::Jump(0, 2));
assert!(textarea.delete_next_word());
assert_eq!(textarea.lines(), ["ab cd"]);
let mut textarea = TextArea::from(["abXYZcd"]);
textarea.set_atomic_ranges([atom(2, 5)]);
textarea.move_cursor(CursorMove::Jump(0, 5));
assert!(textarea.delete_line_by_head());
assert_eq!(textarea.lines(), ["cd"]);
assert_eq!(textarea.yank_text(), "abXYZ");
}
#[test]
fn insertion_after_atom_normalized_jump_clears_ranges() {
let mut textarea = TextArea::from(["abXYZcd"]);
textarea.set_atomic_ranges([atom(2, 5)]);
textarea.move_cursor(CursorMove::Jump(0, 3));
assert_eq!(textarea.cursor(), (0, 5));
textarea.insert_char('!');
assert_eq!(textarea.lines(), ["abXYZ!cd"]);
assert!(textarea.atomic_ranges().is_empty());
let mut textarea = TextArea::from(["abXYZcd"]);
textarea.set_atomic_ranges([atom(2, 5)]);
textarea.move_cursor(CursorMove::Jump(0, 3));
assert!(textarea.insert_str("!!"));
assert_eq!(textarea.lines(), ["abXYZ!!cd"]);
}
#[test]
fn set_lines_and_clear_content_mutations_clear_atomic_ranges() {
let mut textarea = TextArea::from(["abXYZcd"]);
textarea.set_atomic_ranges([atom(2, 5)]);
textarea.set_lines(vec!["abc".to_string()], (0, 0));
assert!(textarea.atomic_ranges().is_empty());
textarea.set_atomic_ranges([atom(0, 2)]);
assert!(textarea.clear());
assert!(textarea.atomic_ranges().is_empty());
}
#[test]
fn wrapped_vertical_movement_normalizes_atom_interiors() {
let mut textarea = TextArea::from(["abcdefghij"]);
textarea.set_wrap_mode(WrapMode::WordOrGlyph);
render(&textarea, 5, 4);
textarea.set_atomic_ranges([atom(6, 8)]);
textarea.move_cursor(CursorMove::Jump(0, 2));
textarea.move_cursor(CursorMove::Down);
assert_eq!(textarea.cursor(), (0, 8));
let mut textarea = TextArea::from(["abcdefghij"]);
textarea.set_wrap_mode(WrapMode::WordOrGlyph);
render(&textarea, 5, 4);
textarea.set_atomic_ranges([atom(2, 4)]);
textarea.move_cursor(CursorMove::Jump(0, 7));
textarea.move_cursor(CursorMove::Up);
assert_eq!(textarea.cursor(), (0, 2));
}