зеркало из
https://github.com/glebtv/tui-textarea.git
synced 2026-08-28 11:36:17 +03:00
refactor undo/redo implementation
Этот коммит содержится в:
@@ -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 {
|
||||
|
||||
174
src/history.rs
174
src/history.rs
@@ -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:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
167
src/textarea.rs
167
src/textarea.rs
@@ -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
|
||||
|
||||
13
src/util.rs
13
src/util.rs
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Ссылка в новой задаче
Block a user