зеркало из
https://github.com/glebtv/tui-textarea.git
synced 2026-09-03 22:35:50 +03:00
add EditKind::InsertChunks and EditKind::DeleteChunk edit kinds
Этот коммит содержится в:
414
src/history.rs
414
src/history.rs
@@ -6,12 +6,14 @@ pub enum EditKind {
|
||||
DeleteChar(char, usize),
|
||||
InsertNewline(usize),
|
||||
DeleteNewline(usize),
|
||||
Insert(String, usize),
|
||||
Remove(String, usize),
|
||||
InsertStr(String, usize),
|
||||
DeleteStr(String, usize),
|
||||
InsertChunk(Vec<String>, usize),
|
||||
DeleteChunk(Vec<String>, usize),
|
||||
}
|
||||
|
||||
impl EditKind {
|
||||
fn apply(&self, row: usize, lines: &mut Vec<String>) {
|
||||
pub(crate) fn apply(&self, row: usize, lines: &mut Vec<String>) {
|
||||
match self {
|
||||
EditKind::InsertChar(c, i) => {
|
||||
lines[row].insert(*i, *c);
|
||||
@@ -31,13 +33,36 @@ impl EditKind {
|
||||
lines[row - 1].push_str(&line);
|
||||
}
|
||||
}
|
||||
EditKind::Insert(s, i) => {
|
||||
EditKind::InsertStr(s, i) => {
|
||||
lines[row].insert_str(*i, s.as_str());
|
||||
}
|
||||
EditKind::Remove(s, i) => {
|
||||
EditKind::DeleteStr(s, i) => {
|
||||
let end = *i + s.len();
|
||||
lines[row].replace_range(*i..end, "");
|
||||
}
|
||||
EditKind::InsertChunk(c, i) => {
|
||||
debug_assert!(c.len() > 1, "Chunk size must be > 1: {:?}", c);
|
||||
|
||||
// Handle first line of chunk
|
||||
let first_line = &mut lines[row];
|
||||
let mut last_line = first_line.drain(*i..).as_str().to_string();
|
||||
first_line.push_str(&c[0]);
|
||||
|
||||
// Handle last line of chunk
|
||||
last_line.insert_str(0, c.last().unwrap());
|
||||
lines.insert(row + 1, last_line);
|
||||
|
||||
// Handle last line of chunk
|
||||
lines.splice(row + 1..row + 1, c[1..c.len() - 1].iter().cloned());
|
||||
}
|
||||
EditKind::DeleteChunk(c, i) => {
|
||||
debug_assert!(c.len() > 1, "Chunk size must be > 1: {:?}", c);
|
||||
|
||||
lines[row].truncate(*i);
|
||||
let mut last_line = lines.drain(row + 1..row + c.len()).last().unwrap();
|
||||
last_line.drain(..c[c.len() - 1].len());
|
||||
lines[row].push_str(&last_line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,8 +73,10 @@ impl EditKind {
|
||||
DeleteChar(c, i) => InsertChar(c, i),
|
||||
InsertNewline(i) => DeleteNewline(i),
|
||||
DeleteNewline(i) => InsertNewline(i),
|
||||
Insert(s, i) => Remove(s, i),
|
||||
Remove(s, i) => Insert(s, i),
|
||||
InsertStr(s, i) => DeleteStr(s, i),
|
||||
DeleteStr(s, i) => InsertStr(s, i),
|
||||
InsertChunk(c, i) => DeleteChunk(c, i),
|
||||
DeleteChunk(c, i) => InsertChunk(c, i),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -148,3 +175,376 @@ impl History {
|
||||
self.max_items
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn insert_delete_chunk() {
|
||||
#[rustfmt::skip]
|
||||
let tests = [
|
||||
// Positions
|
||||
(
|
||||
// Text before edit
|
||||
&[
|
||||
"ab",
|
||||
"cd",
|
||||
"ef",
|
||||
][..],
|
||||
// (row, offset) position before edit
|
||||
(0, 0),
|
||||
// Chunk to be inserted
|
||||
&[
|
||||
"x", "y",
|
||||
][..],
|
||||
// Text after edit
|
||||
&[
|
||||
"x",
|
||||
"yab",
|
||||
"cd",
|
||||
"ef",
|
||||
][..],
|
||||
),
|
||||
(
|
||||
&[
|
||||
"ab",
|
||||
"cd",
|
||||
"ef",
|
||||
][..],
|
||||
(0, 1),
|
||||
&[
|
||||
"x", "y",
|
||||
][..],
|
||||
&[
|
||||
"ax",
|
||||
"yb",
|
||||
"cd",
|
||||
"ef",
|
||||
][..],
|
||||
),
|
||||
(
|
||||
&[
|
||||
"ab",
|
||||
"cd",
|
||||
"ef",
|
||||
][..],
|
||||
(0, 2),
|
||||
&[
|
||||
"x", "y",
|
||||
][..],
|
||||
&[
|
||||
"abx",
|
||||
"y",
|
||||
"cd",
|
||||
"ef",
|
||||
][..],
|
||||
),
|
||||
(
|
||||
&[
|
||||
"ab",
|
||||
"cd",
|
||||
"ef",
|
||||
][..],
|
||||
(1, 0),
|
||||
&[
|
||||
"x", "y",
|
||||
][..],
|
||||
&[
|
||||
"ab",
|
||||
"x",
|
||||
"ycd",
|
||||
"ef",
|
||||
][..],
|
||||
),
|
||||
(
|
||||
&[
|
||||
"ab",
|
||||
"cd",
|
||||
"ef",
|
||||
][..],
|
||||
(1, 1),
|
||||
&[
|
||||
"x", "y",
|
||||
][..],
|
||||
&[
|
||||
"ab",
|
||||
"cx",
|
||||
"yd",
|
||||
"ef",
|
||||
][..],
|
||||
),
|
||||
(
|
||||
&[
|
||||
"ab",
|
||||
"cd",
|
||||
"ef",
|
||||
][..],
|
||||
(1, 2),
|
||||
&[
|
||||
"x", "y",
|
||||
][..],
|
||||
&[
|
||||
"ab",
|
||||
"cdx",
|
||||
"y",
|
||||
"ef",
|
||||
][..],
|
||||
),
|
||||
(
|
||||
&[
|
||||
"ab",
|
||||
"cd",
|
||||
"ef",
|
||||
][..],
|
||||
(2, 0),
|
||||
&[
|
||||
"x", "y",
|
||||
][..],
|
||||
&[
|
||||
"ab",
|
||||
"cd",
|
||||
"x",
|
||||
"yef",
|
||||
][..],
|
||||
),
|
||||
(
|
||||
&[
|
||||
"ab",
|
||||
"cd",
|
||||
"ef",
|
||||
][..],
|
||||
(2, 1),
|
||||
&[
|
||||
"x", "y",
|
||||
][..],
|
||||
&[
|
||||
"ab",
|
||||
"cd",
|
||||
"ex",
|
||||
"yf",
|
||||
][..],
|
||||
),
|
||||
(
|
||||
&[
|
||||
"ab",
|
||||
"cd",
|
||||
"ef",
|
||||
][..],
|
||||
(2, 2),
|
||||
&[
|
||||
"x", "y",
|
||||
][..],
|
||||
&[
|
||||
"ab",
|
||||
"cd",
|
||||
"efx",
|
||||
"y",
|
||||
][..],
|
||||
),
|
||||
// More than 2 lines
|
||||
(
|
||||
&[
|
||||
"ab",
|
||||
"cd",
|
||||
"ef",
|
||||
][..],
|
||||
(1, 1),
|
||||
&[
|
||||
"x", "y", "z", "w"
|
||||
][..],
|
||||
&[
|
||||
"ab",
|
||||
"cx",
|
||||
"y",
|
||||
"z",
|
||||
"wd",
|
||||
"ef",
|
||||
][..],
|
||||
),
|
||||
// Empty lines
|
||||
(
|
||||
&[
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
][..],
|
||||
(0, 0),
|
||||
&[
|
||||
"x", "y", "z"
|
||||
][..],
|
||||
&[
|
||||
"x",
|
||||
"y",
|
||||
"z",
|
||||
"",
|
||||
"",
|
||||
][..],
|
||||
),
|
||||
(
|
||||
&[
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
][..],
|
||||
(1, 0),
|
||||
&[
|
||||
"x", "y", "z"
|
||||
][..],
|
||||
&[
|
||||
"",
|
||||
"x",
|
||||
"y",
|
||||
"z",
|
||||
"",
|
||||
][..],
|
||||
),
|
||||
(
|
||||
&[
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
][..],
|
||||
(2, 0),
|
||||
&[
|
||||
"x", "y", "z"
|
||||
][..],
|
||||
&[
|
||||
"",
|
||||
"",
|
||||
"x",
|
||||
"y",
|
||||
"z",
|
||||
][..],
|
||||
),
|
||||
// Empty buffer
|
||||
(
|
||||
&[
|
||||
"",
|
||||
][..],
|
||||
(0, 0),
|
||||
&[
|
||||
"x", "y", "z"
|
||||
][..],
|
||||
&[
|
||||
"x",
|
||||
"y",
|
||||
"z",
|
||||
][..],
|
||||
),
|
||||
// Insert empty lines
|
||||
(
|
||||
&[
|
||||
"ab",
|
||||
"cd",
|
||||
"ef",
|
||||
][..],
|
||||
(0, 0),
|
||||
&[
|
||||
"", "", "",
|
||||
][..],
|
||||
&[
|
||||
"",
|
||||
"",
|
||||
"ab",
|
||||
"cd",
|
||||
"ef",
|
||||
][..],
|
||||
),
|
||||
(
|
||||
&[
|
||||
"ab",
|
||||
"cd",
|
||||
"ef",
|
||||
][..],
|
||||
(1, 0),
|
||||
&[
|
||||
"", "", "",
|
||||
][..],
|
||||
&[
|
||||
"ab",
|
||||
"",
|
||||
"",
|
||||
"cd",
|
||||
"ef",
|
||||
][..],
|
||||
),
|
||||
(
|
||||
&[
|
||||
"ab",
|
||||
"cd",
|
||||
"ef",
|
||||
][..],
|
||||
(1, 1),
|
||||
&[
|
||||
"", "", "",
|
||||
][..],
|
||||
&[
|
||||
"ab",
|
||||
"c",
|
||||
"",
|
||||
"d",
|
||||
"ef",
|
||||
][..],
|
||||
),
|
||||
(
|
||||
&[
|
||||
"ab",
|
||||
"cd",
|
||||
"ef",
|
||||
][..],
|
||||
(1, 2),
|
||||
&[
|
||||
"", "", "",
|
||||
][..],
|
||||
&[
|
||||
"ab",
|
||||
"cd",
|
||||
"",
|
||||
"",
|
||||
"ef",
|
||||
][..],
|
||||
),
|
||||
(
|
||||
&[
|
||||
"ab",
|
||||
"cd",
|
||||
"ef",
|
||||
][..],
|
||||
(2, 2),
|
||||
&[
|
||||
"", "", "",
|
||||
][..],
|
||||
&[
|
||||
"ab",
|
||||
"cd",
|
||||
"ef",
|
||||
"",
|
||||
"",
|
||||
][..],
|
||||
),
|
||||
];
|
||||
|
||||
for (before, pos, input, expected) in tests {
|
||||
let (row, offset) = pos;
|
||||
let mut lines: Vec<_> = before.iter().map(|s| s.to_string()).collect();
|
||||
let chunk: Vec<_> = input.iter().map(|s| s.to_string()).collect();
|
||||
|
||||
let edit = EditKind::InsertChunk(chunk.clone(), offset);
|
||||
edit.apply(row, &mut lines);
|
||||
assert_eq!(
|
||||
&lines, expected,
|
||||
"{:?} at {:?} with {:?}",
|
||||
before, pos, input,
|
||||
);
|
||||
|
||||
let edit = EditKind::DeleteChunk(chunk, offset);
|
||||
edit.apply(row, &mut lines);
|
||||
assert_eq!(
|
||||
&lines, &before,
|
||||
"{:?} at {:?} with {:?}",
|
||||
before, pos, input,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ use crate::widget::{Renderer, Viewport};
|
||||
use crate::word::{find_word_end_forward, find_word_start_backward};
|
||||
#[cfg(feature = "ratatui")]
|
||||
use ratatui::text::Line;
|
||||
use std::mem;
|
||||
#[cfg(feature = "tuirs")]
|
||||
use tui::text::Spans as Line;
|
||||
use unicode_width::UnicodeWidthChar as _;
|
||||
@@ -612,8 +613,7 @@ impl<'a> TextArea<'a> {
|
||||
self.push_history(EditKind::InsertChar(c, i), (row, col));
|
||||
}
|
||||
|
||||
/// Insert a string at current cursor position. Currently the string must not contain any newlines. This method
|
||||
/// returns if some text was inserted or not in the textarea.
|
||||
/// Insert a string at current cursor position. This method returns if some text was inserted or not in the textarea.
|
||||
/// ```
|
||||
/// use tui_textarea::TextArea;
|
||||
///
|
||||
@@ -621,9 +621,43 @@ impl<'a> TextArea<'a> {
|
||||
///
|
||||
/// textarea.insert_str("hello");
|
||||
/// assert_eq!(textarea.lines(), ["hello"]);
|
||||
///
|
||||
/// textarea.insert_str(", world\ngoodbye, world");
|
||||
/// assert_eq!(textarea.lines(), ["hello, world", "goodbye, world"]);
|
||||
/// ```
|
||||
pub fn insert_str<S: Into<String>>(&mut self, s: S) -> bool {
|
||||
let s = s.into();
|
||||
pub fn insert_str(&mut self, s: &str) -> bool {
|
||||
let mut lines: Vec<_> = s.lines().map(ToString::to_string).collect();
|
||||
match lines.len() {
|
||||
0 => false,
|
||||
1 => self.insert_piece(mem::take(&mut lines[0])),
|
||||
_ => self.insert_chunk(lines),
|
||||
}
|
||||
}
|
||||
|
||||
fn insert_chunk(&mut self, chunk: Vec<String>) -> bool {
|
||||
debug_assert!(chunk.len() > 1, "Chunk size must be > 1: {:?}", chunk);
|
||||
|
||||
let (row, col) = self.cursor;
|
||||
let line = &mut self.lines[row];
|
||||
let i = line
|
||||
.char_indices()
|
||||
.nth(col)
|
||||
.map(|(i, _)| i)
|
||||
.unwrap_or(line.len());
|
||||
|
||||
self.cursor = (
|
||||
row + chunk.len() - 1,
|
||||
col + chunk[chunk.len() - 1].chars().count(),
|
||||
);
|
||||
|
||||
let edit = EditKind::InsertChunk(chunk, i);
|
||||
edit.apply(row, &mut self.lines);
|
||||
|
||||
self.push_history(edit, (row, col));
|
||||
true
|
||||
}
|
||||
|
||||
fn insert_piece(&mut self, s: String) -> bool {
|
||||
if s.is_empty() {
|
||||
return false;
|
||||
}
|
||||
@@ -631,7 +665,7 @@ impl<'a> TextArea<'a> {
|
||||
let (row, col) = self.cursor;
|
||||
let line = &mut self.lines[row];
|
||||
debug_assert!(
|
||||
!line.contains('\n'),
|
||||
!s.contains('\n'),
|
||||
"string given to insert_str must not contain newline: {:?}",
|
||||
line,
|
||||
);
|
||||
@@ -644,7 +678,7 @@ impl<'a> TextArea<'a> {
|
||||
line.insert_str(i, &s);
|
||||
|
||||
self.cursor.1 += s.chars().count();
|
||||
self.push_history(EditKind::Insert(s, i), (row, col));
|
||||
self.push_history(EditKind::InsertStr(s, i), (row, col));
|
||||
true
|
||||
}
|
||||
|
||||
@@ -676,7 +710,7 @@ impl<'a> TextArea<'a> {
|
||||
line.replace_range(i..i + bytes, "");
|
||||
|
||||
self.cursor = (row, col);
|
||||
self.push_history(EditKind::Remove(removed.clone(), i), cursor_before);
|
||||
self.push_history(EditKind::DeleteStr(removed.clone(), i), cursor_before);
|
||||
self.yank = removed;
|
||||
true
|
||||
} else {
|
||||
@@ -704,7 +738,7 @@ impl<'a> TextArea<'a> {
|
||||
}
|
||||
|
||||
if self.hard_tab_indent {
|
||||
return self.insert_str("\t");
|
||||
return self.insert_piece("\t".to_string());
|
||||
}
|
||||
|
||||
let (row, col) = self.cursor;
|
||||
@@ -714,7 +748,7 @@ impl<'a> TextArea<'a> {
|
||||
.map(|c| c.width().unwrap_or(0))
|
||||
.sum();
|
||||
let len = self.tab_len - (width % self.tab_len as usize) as u8;
|
||||
self.insert_str(spaces(len))
|
||||
self.insert_piece(spaces(len).to_string())
|
||||
}
|
||||
|
||||
/// Insert a newline at current cursor position.
|
||||
@@ -937,7 +971,7 @@ impl<'a> TextArea<'a> {
|
||||
/// assert_eq!(textarea.lines(), [" bbb cccaaa"]);
|
||||
/// ```
|
||||
pub fn paste(&mut self) -> bool {
|
||||
self.insert_str(self.yank.to_string())
|
||||
self.insert_piece(self.yank.to_string())
|
||||
}
|
||||
|
||||
/// Move the cursor to the position specified by the [`CursorMove`] parameter. For each kind of cursor moves, see
|
||||
|
||||
Ссылка в новой задаче
Block a user