1
0
зеркало из https://github.com/glebtv/tui-textarea.git synced 2026-08-28 11:36:17 +03:00

fix(textarea): clamp undo/redo cursor after oversized delete_str (#9)

Этот коммит содержится в:
srothgan
2026-02-18 22:37:00 +01:00
коммит произвёл GitHub
родитель 1740facd24
Коммит f03a7b9ec1
4 изменённых файлов: 98 добавлений и 3 удалений

Просмотреть файл

@@ -1,3 +1,13 @@
<a id="v0.9.1"></a>
# [v0.9.1](https://github.com/srothgan/tui-textarea/releases/tag/v0.9.1) - 2026-02-18
- Fix panic on `Ctrl+U`/`undo()` after oversized `delete_str()` (including `select_all() + delete_str(usize::MAX)` on empty buffers).
- Fix root cause by clamping exhausted `delete_str()` ranges to EOF so history never stores out-of-bounds rows.
- Add defensive cursor clamping in `undo()`/`redo()` when restoring from history.
- Add regression tests for the reported panic paths and invalid-history cursor restore edge cases.
[Changes][v0.9.1]
<a id="v0.9.0"></a> <a id="v0.9.0"></a>
# [v0.9.0](https://github.com/srothgan/tui-textarea/releases/tag/v0.9.0) - 2026-02-18 # [v0.9.0](https://github.com/srothgan/tui-textarea/releases/tag/v0.9.0) - 2026-02-18
@@ -474,6 +484,7 @@ First release :tada:
[Changes][v0.1.0] [Changes][v0.1.0]
[v0.9.1]: https://github.com/srothgan/tui-textarea/compare/v0.9.0...v0.9.1
[v0.9.0]: https://github.com/srothgan/tui-textarea/compare/v0.8.0...v0.9.0 [v0.9.0]: https://github.com/srothgan/tui-textarea/compare/v0.8.0...v0.9.0
[v0.8.0]: https://github.com/srothgan/tui-textarea/compare/v0.7.1...v0.8.0 [v0.8.0]: https://github.com/srothgan/tui-textarea/compare/v0.7.1...v0.8.0
[v0.7.1]: https://github.com/srothgan/tui-textarea/compare/v0.7.0...v0.7.1 [v0.7.1]: https://github.com/srothgan/tui-textarea/compare/v0.7.0...v0.7.1

Просмотреть файл

@@ -3,7 +3,7 @@ name = "tui-textarea-2"
homepage = "https://github.com/srothgan/tui-textarea#readme" homepage = "https://github.com/srothgan/tui-textarea#readme"
repository = "https://github.com/srothgan/tui-textarea" repository = "https://github.com/srothgan/tui-textarea"
documentation = "https://docs.rs/tui-textarea-2/latest/tui_textarea/" documentation = "https://docs.rs/tui-textarea-2/latest/tui_textarea/"
version = "0.9.0" version = "0.9.1"
edition = "2021" edition = "2021"
rust-version = "1.56.1" # for `tui` crate support rust-version = "1.56.1" # for `tui` crate support
authors = ["Simon Peter Rothgang <simonrothgang@icloud.com>", "rhysd <lin90162@yahoo.co.jp>"] authors = ["Simon Peter Rothgang <simonrothgang@icloud.com>", "rhysd <lin90162@yahoo.co.jp>"]

Просмотреть файл

@@ -998,6 +998,14 @@ impl<'a> TextArea<'a> {
r += 1; r += 1;
} }
if r == self.lines.len() {
// Clamp exhausted deletions to EOF on the last line so history keeps a valid cursor.
r = self.lines.len() - 1;
let line = &self.lines[r];
offset = line.len();
col = line.chars().count();
}
let start = Pos::new(start_row, start_col, start_offset); let start = Pos::new(start_row, start_col, start_offset);
let end = Pos::new(r, col, offset); let end = Pos::new(r, col, offset);
self.delete_range(start, end, true); self.delete_range(start, end, true);
@@ -1465,6 +1473,18 @@ impl<'a> TextArea<'a> {
.unwrap_or(line.len()) .unwrap_or(line.len())
} }
fn clamp_cursor_to_buffer(&mut self, cursor: (usize, usize)) -> (usize, usize) {
if self.lines.is_empty() {
self.lines.push(String::new());
return (0, 0);
}
let (row, col) = cursor;
let row = row.min(self.lines.len() - 1);
let col = col.min(self.lines[row].chars().count());
(row, col)
}
/// Set the style used for text selection. The default style is light blue. /// Set the style used for text selection. The default style is light blue.
/// ``` /// ```
/// use tui_textarea::TextArea; /// use tui_textarea::TextArea;
@@ -1651,7 +1671,7 @@ impl<'a> TextArea<'a> {
pub fn undo(&mut self) -> bool { pub fn undo(&mut self) -> bool {
if let Some(cursor) = self.history.undo(&mut self.lines) { if let Some(cursor) = self.history.undo(&mut self.lines) {
self.cancel_selection(); self.cancel_selection();
self.cursor = cursor; self.cursor = self.clamp_cursor_to_buffer(cursor);
true true
} else { } else {
false false
@@ -1674,7 +1694,7 @@ impl<'a> TextArea<'a> {
pub fn redo(&mut self) -> bool { pub fn redo(&mut self) -> bool {
if let Some(cursor) = self.history.redo(&mut self.lines) { if let Some(cursor) = self.history.redo(&mut self.lines) {
self.cancel_selection(); self.cancel_selection();
self.cursor = cursor; self.cursor = self.clamp_cursor_to_buffer(cursor);
true true
} else { } else {
false false
@@ -2639,4 +2659,33 @@ mod tests {
textarea.scroll((-5, 0)); textarea.scroll((-5, 0));
assert_eq!(textarea.cursor(), (12, 0)); assert_eq!(textarea.cursor(), (12, 0));
} }
#[test]
fn undo_clamps_invalid_cursor_from_history() {
let mut textarea = TextArea::default();
textarea.history.push(Edit::new(
EditKind::DeleteStr(String::new()),
Pos::new(1, 10, 0),
Pos::new(0, 0, 0),
));
assert!(textarea.undo());
assert_eq!(textarea.lines(), [""]);
assert_eq!(textarea.cursor(), (0, 0));
}
#[test]
fn redo_clamps_invalid_cursor_from_history() {
let mut textarea = TextArea::default();
textarea.history.push(Edit::new(
EditKind::InsertStr(String::new()),
Pos::new(0, 0, 0),
Pos::new(1, 10, 0),
));
assert!(textarea.undo());
assert!(textarea.redo());
assert_eq!(textarea.lines(), [""]);
assert_eq!(textarea.cursor(), (0, 0));
}
} }

Просмотреть файл

@@ -80,3 +80,38 @@ fn test_insert_multi_code_unit_emoji() {
} }
assert_eq!(t.lines(), ["👨‍👩‍👧‍👦"]); assert_eq!(t.lines(), ["👨‍👩‍👧‍👦"]);
} }
#[test]
fn test_ctrl_u_after_delete_str_max_on_empty_buffer() {
let mut t = TextArea::default();
assert!(t.delete_str(usize::MAX));
assert_eq!(t.lines(), [""]);
assert_eq!(t.cursor(), (0, 0));
assert!(t.input(Input {
key: Key::Char('u'),
ctrl: true,
alt: false,
shift: false,
}));
assert_eq!(t.lines(), [""]);
assert_eq!(t.cursor(), (0, 0));
}
#[test]
fn test_ctrl_u_after_select_all_then_delete_str_max_on_empty_buffer() {
let mut t = TextArea::default();
t.select_all();
assert!(t.delete_str(usize::MAX));
assert_eq!(t.lines(), [""]);
assert_eq!(t.cursor(), (0, 0));
assert!(t.input(Input {
key: Key::Char('u'),
ctrl: true,
alt: false,
shift: false,
}));
assert_eq!(t.lines(), [""]);
assert_eq!(t.cursor(), (0, 0));
}