1
0
зеркало из https://github.com/glebtv/tui-textarea.git synced 2026-09-03 22:35:50 +03:00

add EditKind::InsertChunks and EditKind::DeleteChunk edit kinds

Этот коммит содержится в:
rhysd
2023-11-01 20:15:42 +09:00
родитель 60a57b3899
Коммит 2a447d45c2
2 изменённых файлов: 451 добавлений и 17 удалений

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

@@ -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