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

refactor undo/redo implementation

Этот коммит содержится в:
rhysd
2023-11-13 19:29:43 +09:00
родитель cb868a19b6
Коммит bae9d4cee3
6 изменённых файлов: 203 добавлений и 164 удалений

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

@@ -158,22 +158,22 @@ impl<'a> LineHighlighter<'a> {
pub fn selection(
&mut self,
row: usize,
current_row: usize,
start_row: usize,
start_off: usize,
end_row: usize,
end_off: usize,
) {
let (start, end) = if row == start_row {
let (start, end) = if current_row == start_row {
if start_row == end_row {
(start_off, end_off)
} else {
self.select_at_end = true;
(start_off, self.line.len())
}
} else if row == end_row {
} else if current_row == end_row {
(0, end_off)
} else if start_row < row && row < end_row {
} else if start_row < current_row && current_row < end_row {
self.select_at_end = true;
(0, self.line.len())
} else {

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

@@ -1,69 +1,75 @@
use crate::util::Pos;
use std::collections::VecDeque;
#[derive(Clone, Debug)]
pub enum EditKind {
InsertChar(char, usize),
DeleteChar(char, usize),
InsertNewline(usize),
DeleteNewline(usize),
InsertStr(String, usize),
DeleteStr(String, usize),
InsertChunk(Vec<String>, usize, usize),
DeleteChunk(Vec<String>, usize, usize),
InsertChar(char),
DeleteChar(char),
InsertNewline,
DeleteNewline,
InsertStr(String),
DeleteStr(String),
InsertChunk(Vec<String>),
DeleteChunk(Vec<String>),
}
impl EditKind {
pub(crate) fn apply(&self, row: usize, lines: &mut Vec<String>) {
pub(crate) fn apply(&self, lines: &mut Vec<String>, before: &Pos, after: &Pos) {
match self {
EditKind::InsertChar(c, i) => {
lines[row].insert(*i, *c);
EditKind::InsertChar(c) => {
lines[before.row].insert(before.offset, *c);
}
EditKind::DeleteChar(_, i) => {
lines[row].remove(*i);
EditKind::DeleteChar(_) => {
lines[before.row].remove(after.offset);
}
EditKind::InsertNewline(i) => {
let line = &mut lines[row];
let next_line = line[*i..].to_string();
line.truncate(*i);
lines.insert(row + 1, next_line);
EditKind::InsertNewline => {
let line = &mut lines[before.row];
let next_line = line[before.offset..].to_string();
line.truncate(before.offset);
lines.insert(before.row + 1, next_line);
}
EditKind::DeleteNewline(_) => {
if row > 0 {
let line = lines.remove(row);
lines[row - 1].push_str(&line);
}
EditKind::DeleteNewline => {
debug_assert!(before.row > 0, "invalid pos: {:?}", before);
let line = lines.remove(before.row);
lines[before.row - 1].push_str(&line);
}
EditKind::InsertStr(s, i) => {
lines[row].insert_str(*i, s.as_str());
EditKind::InsertStr(s) => {
lines[before.row].insert_str(before.offset, s.as_str());
}
EditKind::DeleteStr(s, i) => {
let end = *i + s.len();
lines[row].replace_range(*i..end, "");
EditKind::DeleteStr(s) => {
lines[after.row].drain(after.offset..after.offset + s.len());
}
EditKind::InsertChunk(c, row, i) => {
EditKind::InsertChunk(c) => {
debug_assert!(c.len() > 1, "Chunk size must be > 1: {:?}", c);
let row = *row;
// Handle first line of chunk
let first_line = &mut lines[row];
let mut last_line = first_line.drain(*i..).as_str().to_string();
let first_line = &mut lines[before.row];
let mut last_line = first_line.drain(before.offset..).as_str().to_string();
first_line.push_str(&c[0]);
// Handle last line of chunk
let next_row = before.row + 1;
last_line.insert_str(0, c.last().unwrap());
lines.insert(row + 1, last_line);
lines.insert(next_row, last_line);
// Handle last line of chunk
lines.splice(row + 1..row + 1, c[1..c.len() - 1].iter().cloned());
// Handle middle lines of chunk
lines.splice(next_row..next_row, c[1..c.len() - 1].iter().cloned());
}
EditKind::DeleteChunk(c, row, i) => {
EditKind::DeleteChunk(c) => {
debug_assert!(c.len() > 1, "Chunk size must be > 1: {:?}", c);
let row = *row;
lines[row].truncate(*i);
let mut last_line = lines.drain(row + 1..row + c.len()).last().unwrap();
// Remove middle lines of chunk
let mut last_line = lines
.drain(after.row + 1..after.row + c.len())
.last()
.unwrap();
// Remove last line of chunk
last_line.drain(..c[c.len() - 1].len());
lines[row].push_str(&last_line);
// Remove first line of chunk and concat remaining
let first_line = &mut lines[after.row];
first_line.truncate(after.offset);
first_line.push_str(&last_line);
}
}
}
@@ -71,14 +77,14 @@ impl EditKind {
fn invert(&self) -> Self {
use EditKind::*;
match self.clone() {
InsertChar(c, i) => DeleteChar(c, i),
DeleteChar(c, i) => InsertChar(c, i),
InsertNewline(i) => DeleteNewline(i),
DeleteNewline(i) => InsertNewline(i),
InsertStr(s, i) => DeleteStr(s, i),
DeleteStr(s, i) => InsertStr(s, i),
InsertChunk(c, r, i) => DeleteChunk(c, r, i),
DeleteChunk(c, r, i) => InsertChunk(c, r, i),
InsertChar(c) => DeleteChar(c),
DeleteChar(c) => InsertChar(c),
InsertNewline => DeleteNewline,
DeleteNewline => InsertNewline,
InsertStr(s) => DeleteStr(s),
DeleteStr(s) => InsertStr(s),
InsertChunk(c) => DeleteChunk(c),
DeleteChunk(c) => InsertChunk(c),
}
}
}
@@ -86,39 +92,33 @@ impl EditKind {
#[derive(Clone, Debug)]
pub struct Edit {
kind: EditKind,
cursor_before: (usize, usize),
cursor_after: (usize, usize),
before: Pos,
after: Pos,
}
impl Edit {
pub fn new(
kind: EditKind,
cursor_before: (usize, usize),
cursor_after: (usize, usize),
) -> Self {
pub fn new(kind: EditKind, before: Pos, after: Pos) -> Self {
Self {
kind,
cursor_before,
cursor_after,
before,
after,
}
}
pub fn redo(&self, lines: &mut Vec<String>) {
let (row, _) = self.cursor_before;
self.kind.apply(row, lines);
self.kind.apply(lines, &self.before, &self.after);
}
pub fn undo(&self, lines: &mut Vec<String>) {
let (row, _) = self.cursor_after;
self.kind.invert().apply(row, lines); // Undo is redo of inverted edit
self.kind.invert().apply(lines, &self.after, &self.before); // Undo is redo of inverted edit
}
pub fn cursor_before(&self) -> (usize, usize) {
self.cursor_before
(self.before.row, self.before.col)
}
pub fn cursor_after(&self) -> (usize, usize) {
self.cursor_after
(self.after.row, self.after.col)
}
}
@@ -185,7 +185,6 @@ mod tests {
#[test]
fn insert_delete_chunk() {
#[rustfmt::skip]
#[allow(clippy::identity_op)]
let tests = [
// Positions
(
@@ -195,7 +194,7 @@ mod tests {
"cd",
"ef",
][..],
// (row, offset) position before edit
// (row, col) position before edit
(0, 0),
// Chunk to be inserted
&[
@@ -551,7 +550,7 @@ mod tests {
"🐮🐰",
"🐧🐭",
][..],
(0, 4 * 2),
(0, 2),
&[
"🐷", "🐼", "🐴",
][..],
@@ -587,7 +586,7 @@ mod tests {
"🐮🐰",
"🐧🐭",
][..],
(1, 4 * 1),
(1, 1),
&[
"🐷", "🐼", "🐴",
][..],
@@ -605,7 +604,7 @@ mod tests {
"🐮🐰",
"🐧🐭",
][..],
(2, 4 * 2),
(2, 2),
&[
"🐷", "🐼", "🐴",
][..],
@@ -619,26 +618,33 @@ mod tests {
),
];
for (before, pos, input, expected) in tests {
let (row, offset) = pos;
for test in tests {
let (before, pos, input, expected) = test;
let (row, col) = pos;
let before_pos = {
let offset = before[row]
.char_indices()
.map(|(i, _)| i)
.nth(col)
.unwrap_or(before[row].len());
Pos::new(row, col, offset)
};
let mut lines: Vec<_> = before.iter().map(|s| s.to_string()).collect();
let chunk: Vec<_> = input.iter().map(|s| s.to_string()).collect();
let after_pos = {
let row = row + input.len() - 1;
let last = input.last().unwrap();
let col = last.chars().count();
Pos::new(row, col, last.len())
};
let edit = EditKind::InsertChunk(chunk.clone(), row, offset);
edit.apply(row, &mut lines);
assert_eq!(
&lines, expected,
"{:?} at {:?} with {:?}",
before, pos, input,
);
let edit = EditKind::InsertChunk(chunk.clone());
edit.apply(&mut lines, &before_pos, &after_pos);
assert_eq!(&lines, expected, "{test:?}");
let edit = EditKind::DeleteChunk(chunk, row, offset);
edit.apply(row, &mut lines);
assert_eq!(
&lines, &before,
"{:?} at {:?} with {:?}",
before, pos, input,
);
let edit = EditKind::DeleteChunk(chunk);
edit.apply(&mut lines, &after_pos, &before_pos);
assert_eq!(&lines, &before, "{test:?}");
}
}
}

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

@@ -8,7 +8,7 @@ use crate::ratatui::widgets::{Block, Widget};
use crate::scroll::Scrolling;
#[cfg(feature = "search")]
use crate::search::Search;
use crate::util::spaces;
use crate::util::{spaces, Pos};
use crate::widget::{Renderer, Viewport};
use crate::word::{find_word_end_forward, find_word_start_backward};
#[cfg(feature = "ratatui")]
@@ -54,18 +54,6 @@ impl ToString for YankText {
}
}
struct Pos {
row: usize,
col: usize,
offset: usize,
}
impl Pos {
fn new(row: usize, col: usize, offset: usize) -> Self {
Self { row, col, offset }
}
}
/// A type to manage state of textarea.
///
/// [`TextArea::default`] creates an empty textarea. [`TextArea::new`] creates a textarea with given text lines.
@@ -715,8 +703,10 @@ impl<'a> TextArea<'a> {
}
}
fn push_history(&mut self, kind: EditKind, cursor_before: (usize, usize)) {
let edit = Edit::new(kind, cursor_before, self.cursor);
fn push_history(&mut self, kind: EditKind, before: Pos, after_offset: usize) {
let (row, col) = self.cursor;
let after = Pos::new(row, col, after_offset);
let edit = Edit::new(kind, before, after);
self.history.push(edit);
}
@@ -745,7 +735,11 @@ impl<'a> TextArea<'a> {
.unwrap_or(line.len());
line.insert(i, c);
self.cursor.1 += 1;
self.push_history(EditKind::InsertChar(c, i), (row, col));
self.push_history(
EditKind::InsertChar(c),
Pos::new(row, col, i),
i + c.len_utf8(),
);
}
/// Insert a string at current cursor position. This method returns if some text was inserted or not in the textarea.
@@ -780,16 +774,20 @@ impl<'a> TextArea<'a> {
.nth(col)
.map(|(i, _)| i)
.unwrap_or(line.len());
let before = Pos::new(row, col, i);
self.cursor = (
let (row, col) = (
row + chunk.len() - 1,
chunk[chunk.len() - 1].chars().count(),
);
self.cursor = (row, col);
let edit = EditKind::InsertChunk(chunk, row, i);
edit.apply(row, &mut self.lines);
let end_offset = chunk.last().unwrap().len();
self.push_history(edit, (row, col));
let edit = EditKind::InsertChunk(chunk);
edit.apply(&mut self.lines, &before, &Pos::new(row, col, end_offset));
self.push_history(edit, before, end_offset);
true
}
@@ -812,45 +810,37 @@ impl<'a> TextArea<'a> {
.map(|(i, _)| i)
.unwrap_or(line.len());
line.insert_str(i, &s);
let end_offset = i + s.len();
self.cursor.1 += s.chars().count();
self.push_history(EditKind::InsertStr(s, i), (row, col));
self.push_history(EditKind::InsertStr(s), Pos::new(row, col, i), end_offset);
true
}
fn delete_offset_range(
&mut self,
start_row: usize,
start_col: usize,
start_offset: usize,
end_row: usize,
end_offset: usize,
should_yank: bool,
) {
let cursor_before = self.cursor;
self.cursor = (start_row, start_col);
fn delete_range(&mut self, start: Pos, end: Pos, should_yank: bool) {
self.cursor = (start.row, start.col);
if start_row == end_row {
let removed = self.lines[start_row]
.drain(start_offset..end_offset)
if start.row == end.row {
let removed = self.lines[start.row]
.drain(start.offset..end.offset)
.as_str()
.to_string();
if should_yank {
self.yank = removed.clone().into();
}
self.push_history(EditKind::DeleteStr(removed, start_offset), cursor_before);
self.push_history(EditKind::DeleteStr(removed), end, start.offset);
return;
}
let mut deleted = vec![self.lines[start_row]
.drain(start_offset..)
let mut deleted = vec![self.lines[start.row]
.drain(start.offset..)
.as_str()
.to_string()];
deleted.extend(self.lines.drain(start_row + 1..end_row));
if start_row + 1 < self.lines.len() {
let mut last_line = self.lines.remove(start_row + 1);
self.lines[start_row].push_str(&last_line[end_offset..]);
last_line.truncate(end_offset);
deleted.extend(self.lines.drain(start.row + 1..end.row));
if start.row + 1 < self.lines.len() {
let mut last_line = self.lines.remove(start.row + 1);
self.lines[start.row].push_str(&last_line[end.offset..]);
last_line.truncate(end.offset);
deleted.push(last_line);
}
@@ -859,11 +849,11 @@ impl<'a> TextArea<'a> {
}
let edit = if deleted.len() == 1 {
EditKind::DeleteStr(deleted.remove(0), start_offset)
EditKind::DeleteStr(deleted.remove(0))
} else {
EditKind::DeleteChunk(deleted, start_row, start_offset)
EditKind::DeleteChunk(deleted)
};
self.push_history(edit, cursor_before);
self.push_history(edit, end, start.offset);
}
/// Delete a string from the current cursor position. The `chars` parameter means number of characters, not a byte
@@ -896,14 +886,16 @@ impl<'a> TextArea<'a> {
let mut remaining = chars;
let mut find_end = move |line: &str| {
let mut col = 0usize;
for (i, _) in line.char_indices() {
if remaining == 0 {
return Some(i);
return Some((i, col));
}
col += 1;
remaining -= 1;
}
if remaining == 0 {
Some(line.len())
Some((line.len(), col))
} else {
remaining -= 1;
None
@@ -919,29 +911,39 @@ impl<'a> TextArea<'a> {
};
// First line
if let Some(offset) = find_end(&line[start_offset..]) {
if let Some((offset_delta, col_delta)) = find_end(&line[start_offset..]) {
let end_offset = start_offset + offset_delta;
let end_col = start_col + col_delta;
let removed = self.lines[start_row]
.drain(start_offset..start_offset + offset)
.drain(start_offset..end_offset)
.as_str()
.to_string();
self.yank = removed.clone().into();
self.push_history(EditKind::DeleteStr(removed, start_offset), self.cursor);
self.push_history(
EditKind::DeleteStr(removed),
Pos::new(start_row, end_col, end_offset),
start_offset,
);
return true;
}
let mut r = start_row + 1;
let mut i = 0;
let mut offset = 0;
let mut col = 0;
while r < self.lines.len() {
let line = &self.lines[r];
if let Some(offset) = find_end(line) {
i = offset;
if let Some((o, c)) = find_end(line) {
offset = o;
col = c;
break;
}
r += 1;
}
self.delete_offset_range(start_row, start_col, start_offset, r, i, true);
let start = Pos::new(start_row, start_col, start_offset);
let end = Pos::new(r, col, offset);
self.delete_range(start, end, true);
true
}
@@ -950,20 +952,31 @@ impl<'a> TextArea<'a> {
return false;
}
let cursor_before = self.cursor;
let row = cursor_before.0;
#[inline]
fn bytes_and_chars(claimed: usize, s: &str) -> (usize, usize) {
// Note: `claimed` may be larger than characters in `s` (e.g. usize::MAX)
let mut last_col = 0;
for (col, (bytes, _)) in s.char_indices().enumerate() {
if col == claimed {
return (bytes, claimed);
}
last_col = col;
}
(s.len(), last_col + 1)
}
let (row, _) = self.cursor;
let line = &mut self.lines[row];
if let Some((i, _)) = line.char_indices().nth(col) {
let bytes = line[i..]
.char_indices()
.nth(chars)
.map(|(i, _)| i)
.unwrap_or_else(|| line[i..].len());
let removed = line[i..i + bytes].to_string();
line.replace_range(i..i + bytes, "");
let (bytes, chars) = bytes_and_chars(chars, &line[i..]);
let removed = line.drain(i..i + bytes).as_str().to_string();
self.cursor = (row, col);
self.push_history(EditKind::DeleteStr(removed.clone(), i), cursor_before);
self.push_history(
EditKind::DeleteStr(removed.clone()),
Pos::new(row, col + chars, i + bytes),
i,
);
self.yank = removed.into();
true
} else {
@@ -1021,17 +1034,17 @@ impl<'a> TextArea<'a> {
let (row, col) = self.cursor;
let line = &mut self.lines[row];
let idx = line
let offset = line
.char_indices()
.nth(col)
.map(|(i, _)| i)
.unwrap_or(line.len());
let next_line = line[idx..].to_string();
line.truncate(idx);
let next_line = line[offset..].to_string();
line.truncate(offset);
self.lines.insert(row + 1, next_line);
self.cursor = (row + 1, 0);
self.push_history(EditKind::InsertNewline(idx), (row, col));
self.push_history(EditKind::InsertNewline, Pos::new(row, col, offset), 0);
}
/// Delete a newline from **head** of current cursor line. This method returns if a newline was deleted or not in
@@ -1050,7 +1063,7 @@ impl<'a> TextArea<'a> {
return true;
}
let (row, col) = self.cursor;
let (row, _) = self.cursor;
if row == 0 {
return false;
}
@@ -1061,7 +1074,7 @@ impl<'a> TextArea<'a> {
self.cursor = (row - 1, prev_line.chars().count());
prev_line.push_str(&line);
self.push_history(EditKind::DeleteNewline(prev_line_end), (row, col));
self.push_history(EditKind::DeleteNewline, Pos::new(row, 0, 0), prev_line_end);
true
}
@@ -1088,10 +1101,14 @@ impl<'a> TextArea<'a> {
}
let line = &mut self.lines[row];
if let Some((i, c)) = line.char_indices().nth(col - 1) {
line.remove(i);
if let Some((offset, c)) = line.char_indices().nth(col - 1) {
line.remove(offset);
self.cursor.1 -= 1;
self.push_history(EditKind::DeleteChar(c, i), (row, col));
self.push_history(
EditKind::DeleteChar(c),
Pos::new(row, col, offset + c.len_utf8()),
offset,
);
true
} else {
false
@@ -1427,7 +1444,7 @@ impl<'a> TextArea<'a> {
fn delete_selection(&mut self, should_yank: bool) -> bool {
if let Some((s, e)) = self.take_selection_range() {
self.delete_offset_range(s.row, s.col, s.offset, e.row, e.offset, should_yank);
self.delete_range(s, e, should_yank);
return true;
}
false

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

@@ -6,3 +6,16 @@ pub fn spaces(size: u8) -> &'static str {
pub fn num_digits(i: usize) -> u8 {
f64::log10(i as f64) as u8 + 1
}
#[derive(Debug, Clone)]
pub struct Pos {
pub row: usize,
pub col: usize,
pub offset: usize,
}
impl Pos {
pub fn new(row: usize, col: usize, offset: usize) -> Self {
Self { row, col, offset }
}
}

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

@@ -55,7 +55,11 @@ fn test_input_all_combinations_sanity() {
for input in inputs {
t.input(input.clone());
t.undo();
t.redo();
t.input_without_shortcuts(input);
t.undo();
t.redo();
}
}

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

@@ -830,7 +830,6 @@ fn test_delete_str_multiple_lines() {
assert!(t.undo(), "undo: {:?}", test);
assert_eq!(t.lines(), before, "content after undo: {:?}", test);
assert_eq!(t.cursor(), (row, col), "cursor after undo: {:?}", test);
}
}