зеркало из
https://github.com/glebtv/tui-textarea.git
synced 2026-08-28 11:36:17 +03:00
- 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
256 строки
8.3 KiB
Rust
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));
|
|
}
|