From 14639f64ac9b000c2d84a7cc9e1173c31e0aa355 Mon Sep 17 00:00:00 2001 From: sxyazi Date: Sat, 22 Jul 2023 23:06:40 +0800 Subject: [PATCH] feat: undo and redo for input --- src/core/input/input.rs | 272 +++++++++++++++++++--------------------- src/core/input/mod.rs | 4 +- src/core/input/snap.rs | 119 +++++++++++++++--- src/core/input/snaps.rs | 57 +++++++++ src/core/tasks/mod.rs | 4 +- src/ui/dispatcher.rs | 3 + 6 files changed, 294 insertions(+), 165 deletions(-) create mode 100644 src/core/input/snaps.rs diff --git a/src/core/input/input.rs b/src/core/input/input.rs index b6016136..9275d841 100644 --- a/src/core/input/input.rs +++ b/src/core/input/input.rs @@ -1,24 +1,20 @@ +use std::ops::Range; + use anyhow::{anyhow, Result}; use ratatui::layout::Rect; use tokio::sync::oneshot::Sender; -use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; +use unicode_width::UnicodeWidthStr; -use super::InputSnap; +use super::{InputSnap, InputSnaps}; use crate::{core::{external, Position}, misc::CharKind}; #[derive(Default)] pub struct Input { - title: String, - pub(crate) value: String, - position: (u16, u16), + snaps: InputSnaps, - pub(crate) op: InputOp, - pub(crate) start: Option, - - pub(crate) mode: InputMode, - pub(crate) offset: usize, - pub(crate) cursor: usize, - callback: Option>>, + title: String, + position: (u16, u16), + callback: Option>>, pub visible: bool, } @@ -52,102 +48,110 @@ pub enum InputOp { impl Input { pub fn show(&mut self, opt: InputOpt, tx: Sender>) { self.close(false); + self.snaps.reset(opt.value); self.title = opt.title; - self.value = opt.value; self.position = match opt.position { Position::Coords(x, y) => (x, y), _ => unimplemented!(), }; - self.cursor = self.count(); - self.offset = self.cursor.saturating_sub(50 - 2 /* Border width */); self.callback = Some(tx); self.visible = true; } pub fn close(&mut self, submit: bool) -> bool { if let Some(cb) = self.callback.take() { - let _ = cb.send(if submit { Ok(self.value.clone()) } else { Err(anyhow!("canceled")) }); + let _ = + cb.send(if submit { Ok(self.snap_mut().value.clone()) } else { Err(anyhow!("canceled")) }); } - self.op = InputOp::None; - self.start = None; - - self.mode = InputMode::Insert; self.visible = false; true } pub fn escape(&mut self) -> bool { - match self.mode { + let snap = self.snap_mut(); + match snap.mode { InputMode::Normal => { - self.op = InputOp::None; - self.start = None; + snap.op = InputOp::None; + snap.start = None; } InputMode::Insert => { - self.mode = InputMode::Normal; + snap.mode = InputMode::Normal; self.move_(-1); } } + self.snaps.tag(); true } pub fn insert(&mut self, append: bool) -> bool { - if self.mode != InputMode::Normal { + if !self.snap_mut().insert() { return false; } - - self.op = InputOp::None; - self.start = None; - self.mode = InputMode::Insert; if append { self.move_(1); } true } + #[inline] pub fn visual(&mut self) -> bool { - if self.mode != InputMode::Normal { - return false; - } else if self.value.is_empty() { - return false; - } - - self.start = Some(self.cursor); - true + self.snap_mut().visual(); + self.escape() } + #[inline] + pub fn undo(&mut self) -> bool { + self.snaps.undo(); + self.escape() + } + + #[inline] + pub fn redo(&mut self) -> bool { self.snaps.redo() } + pub fn move_(&mut self, step: isize) -> bool { + let snap = self.snap(); let b = self.handle_op( if step <= 0 { - self.cursor.saturating_sub(step.abs() as usize) + snap.cursor.saturating_sub(step.abs() as usize) } else { - self.count().min(self.cursor + step as usize) + snap.count().min(snap.cursor + step as usize) }, false, ); - if self.cursor < self.offset { - self.offset = self.cursor; - } else if self.cursor >= self.offset + 50 - 2 { - self.offset = self.cursor.saturating_sub(50 - 2 - self.mode.delta()); + let snap = self.snap_mut(); + if snap.cursor < snap.offset { + snap.offset = snap.cursor; + } else if snap.value.is_empty() { + snap.offset = 0; + } else { + let delta = snap.mode.delta(); + let s = snap.slice(snap.offset..snap.cursor + delta); + if s.width() >= /*TODO: hardcode*/ 50 - 2 { + let s = s.chars().rev().collect::(); + snap.offset = snap.cursor - InputSnap::find_window(&s, 0).end.saturating_sub(delta); + } } + b } #[inline] pub fn move_in_operating(&mut self, step: isize) -> bool { - if self.op == InputOp::None { false } else { self.move_(step) } + if self.snap_mut().op == InputOp::None { false } else { self.move_(step) } } pub fn backward(&mut self) -> bool { - if self.cursor == 0 { + let snap = self.snap(); + if snap.cursor == 0 { return self.move_(0); } - let idx = self.idx(self.cursor).unwrap_or(self.value.len()); - let mut it = self.value[..idx].chars().rev().enumerate(); + let idx = snap.idx(snap.cursor).unwrap_or(snap.len()); + let mut it = snap.value[..idx].chars().rev().enumerate(); let mut prev = CharKind::new(it.next().unwrap().1); for (i, c) in it { let c = CharKind::new(c); @@ -158,17 +162,18 @@ impl Input { } if prev != CharKind::Space { - return self.move_(-(self.value.len() as isize)); + return self.move_(-(snap.len() as isize)); } false } pub fn forward(&mut self, end: bool) -> bool { - if self.value.is_empty() { + let snap = self.snap(); + if snap.value.is_empty() { return self.move_(0); } - let mut it = self.value.chars().skip(self.cursor).enumerate(); + let mut it = snap.value.chars().skip(snap.cursor).enumerate(); let mut prev = CharKind::new(it.next().unwrap().1); for (i, c) in it { let c = CharKind::new(c); @@ -177,7 +182,7 @@ impl Input { } else { c != CharKind::Space && c != prev }; - if b && self.op != InputOp::None { + if b && snap.op != InputOp::None { return self.move_(i as isize); } else if b { return self.move_(if end { i - 1 } else { i } as isize); @@ -185,46 +190,50 @@ impl Input { prev = c; } - self.move_(self.value.len() as isize) + self.move_(snap.len() as isize) } pub fn type_(&mut self, c: char) -> bool { - if self.cursor < 1 { - self.value.insert(0, c); - } else if self.cursor == self.count() { - self.value.push(c); + let snap = self.snap_mut(); + if snap.cursor < 1 { + snap.value.insert(0, c); + } else if snap.cursor == snap.count() { + snap.value.push(c); } else { - self.value.insert(self.idx(self.cursor).unwrap(), c); + snap.value.insert(snap.idx(snap.cursor).unwrap(), c); } self.move_(1) } pub fn backspace(&mut self) -> bool { - if self.cursor < 1 { + let snap = self.snap_mut(); + if snap.cursor < 1 { return false; - } else if self.cursor == self.count() { - self.value.pop(); + } else if snap.cursor == snap.count() { + snap.value.pop(); } else { - self.value.remove(self.idx(self.cursor - 1).unwrap()); + snap.value.remove(snap.idx(snap.cursor - 1).unwrap()); } self.move_(-1) } pub fn delete(&mut self, insert: bool) -> bool { - match self.op { + match self.snap().op { InputOp::None => { - self.op = InputOp::Delete(insert); - if self.start.is_some() { - return self.handle_op(self.cursor, true).then(|| self.move_(0)).is_some(); + if self.snap().start.is_some() { + self.snap_mut().op = InputOp::Delete(insert); + return self.handle_op(self.snap().cursor, true).then(|| self.move_(0)).is_some(); } - self.start = Some(self.cursor); + let snap = self.snap_mut(); + snap.op = InputOp::Delete(insert); + snap.start = Some(snap.cursor); false } InputOp::Delete(..) => { - self.move_(-(self.value.len() as isize)); - self.value.clear(); - self.mode = if insert { InputMode::Insert } else { InputMode::Normal }; + self.move_(-(self.snap().len() as isize)); + self.snap_mut().value.clear(); + self.snap_mut().mode = if insert { InputMode::Insert } else { InputMode::Normal }; true } _ => false, @@ -232,19 +241,21 @@ impl Input { } pub fn yank(&mut self) -> bool { - match self.op { + match self.snap().op { InputOp::None => { - self.op = InputOp::Yank; - if self.start.is_some() { - return self.handle_op(self.cursor, true).then(|| self.move_(0)).is_some(); + if self.snap().start.is_some() { + self.snap_mut().op = InputOp::Yank; + return self.handle_op(self.snap().cursor, true).then(|| self.move_(0)).is_some(); } - self.start = Some(self.cursor); + let snap = self.snap_mut(); + snap.op = InputOp::Yank; + snap.start = Some(snap.cursor); false } InputOp::Yank => { - self.start = Some(0); - self.move_(self.value.len() as isize); + self.snap_mut().start = Some(0); + self.move_(self.snap().len() as isize); false } _ => false, @@ -252,9 +263,9 @@ impl Input { } pub fn paste(&mut self, before: bool) -> bool { - if self.start.is_some() { - self.op = InputOp::Delete(false); - self.handle_op(self.cursor, true); + if self.snap().start.is_some() { + self.snap_mut().op = InputOp::Delete(false); + self.handle_op(self.snap().cursor, true); } let str = @@ -272,25 +283,26 @@ impl Input { } fn handle_op(&mut self, cursor: usize, include: bool) -> bool { - let snap = self.snap(); - let range = if self.op == InputOp::None { None } else { self.range(cursor, include) }; + let old = self.snap().clone(); + let snap = self.snap_mut(); + let range = if snap.op == InputOp::None { None } else { snap.range(cursor, include) }; - match self.op { + match snap.op { InputOp::None => { - self.cursor = cursor; + snap.cursor = cursor; } InputOp::Delete(insert) => { let range = range.unwrap(); - let (start, end) = (self.idx(range.0), self.idx(range.1)); + let Range { start, end } = snap.idx(range.start)..snap.idx(range.end); - self.value.drain(start.unwrap()..end.unwrap()); - self.mode = if insert { InputMode::Insert } else { InputMode::Normal }; - self.cursor = range.0; + snap.value.drain(start.unwrap()..end.unwrap()); + snap.mode = if insert { InputMode::Insert } else { InputMode::Normal }; + snap.cursor = range.start; } InputOp::Yank => { let range = range.unwrap(); - let (start, end) = (self.idx(range.0), self.idx(range.1)); - let yanked = &self.value[start.unwrap()..end.unwrap()]; + let Range { start, end } = snap.idx(range.start)..snap.idx(range.end); + let yanked = &snap.value[start.unwrap()..end.unwrap()]; futures::executor::block_on(async { external::clipboard_set(yanked).await.ok(); @@ -298,9 +310,16 @@ impl Input { } }; - self.op = InputOp::None; - self.cursor = self.count().saturating_sub(self.mode.delta()).min(self.cursor); - snap != self.snap() + snap.op = InputOp::None; + snap.cursor = snap.count().saturating_sub(snap.mode.delta()).min(snap.cursor); + if *snap == old { + return false; + } + + if old.op != InputOp::None { + self.snaps.tag(); + } + true } } @@ -309,87 +328,50 @@ impl Input { pub fn title(&self) -> String { self.title.clone() } #[inline] - pub fn value(&self) -> &str { - let win = self.window(); - &self.value[self.idx(win.0).unwrap()..self.idx(win.1).unwrap()] - } + pub fn value(&self) -> &str { self.snap().slice(self.snap().window()) } #[inline] - pub fn mode(&self) -> InputMode { self.mode } + pub fn mode(&self) -> InputMode { self.snap().mode } #[inline] pub fn area(&self) -> Rect { + // TODO: hardcode Rect { x: self.position.0, y: self.position.1 + 2, width: 50, height: 3 } } #[inline] pub fn cursor(&self) -> (u16, u16) { - let area = self.area(); - let width = self.value[self.offset..self.idx(self.cursor).unwrap()].width() as u16; + let snap = self.snap(); + let width = snap.slice(snap.offset..snap.cursor).width() as u16; + let area = self.area(); (area.x + width + 1, area.y + 1) } pub fn selected(&self) -> Option { - if self.start.is_none() { + let snap = self.snap(); + if snap.start.is_none() { return None; } - let start = self.start.unwrap(); + let start = snap.start.unwrap(); let (start, end) = - if start < self.cursor { (start, self.cursor) } else { (self.cursor + 1, start + 1) }; + if start < snap.cursor { (start, snap.cursor) } else { (snap.cursor + 1, start + 1) }; - let win = self.window(); - let (start, end) = (start.max(win.0), end.min(win.1)); - let (start, end) = (self.idx(start).unwrap(), self.idx(end).unwrap()); + let win = snap.window(); + let Range { start, end } = start.max(win.start)..end.min(win.end); - let offset = self.idx(self.offset).unwrap(); Some(Rect { - x: self.position.0 + 1 + self.value[offset..start].width() as u16, + x: self.position.0 + 1 + snap.slice(snap.offset..start).width() as u16, y: self.position.1 + 3, - width: self.value[start..end].width() as u16, + width: snap.slice(start..end).width() as u16, height: 1, }) } #[inline] - fn count(&self) -> usize { self.value.chars().count() } + fn snap(&self) -> &InputSnap { self.snaps.current() } #[inline] - fn idx(&self, n: usize) -> Option { - self - .value - .char_indices() - .nth(n) - .map(|(i, _)| i) - .or_else(|| if n == self.count() { Some(self.value.len()) } else { None }) - } - - #[inline] - fn range(&mut self, cursor: usize, include: bool) -> Option<(usize, usize)> { - self - .start - .take() - .map(|s| if s <= cursor { (s, cursor) } else { (cursor, s) }) - .map(|(s, e)| (s, e + include as usize)) - } - - #[inline] - fn window(&self) -> (usize, usize) { - let mut len = 0; - let v = self - .value - .chars() - .enumerate() - .skip(self.offset) - .map_while(|(i, c)| { - len += c.width().unwrap_or(0); - if len > 50 - 2/*Border width*/ { None } else { Some(i) } - }) - .collect::>(); - (v.first().copied().unwrap_or(0), v.last().map(|l| l + 1).unwrap_or(0)) - } - - #[inline] - fn snap(&self) -> InputSnap { InputSnap::from(self) } + fn snap_mut(&mut self) -> &mut InputSnap { self.snaps.current_mut() } } diff --git a/src/core/input/mod.rs b/src/core/input/mod.rs index b7251de8..4aabaa08 100644 --- a/src/core/input/mod.rs +++ b/src/core/input/mod.rs @@ -1,5 +1,7 @@ mod input; mod snap; +mod snaps; pub use input::*; -pub use snap::*; +use snap::*; +use snaps::*; diff --git a/src/core/input/snap.rs b/src/core/input/snap.rs index d9ff2418..ed83b6dc 100644 --- a/src/core/input/snap.rs +++ b/src/core/input/snap.rs @@ -1,28 +1,113 @@ -use super::{Input, InputMode, InputOp}; +use std::ops::Range; -#[derive(Default, PartialEq, Eq)] -pub struct InputSnap { - value: String, +use unicode_width::UnicodeWidthChar; - op: InputOp, - start: Option, +use super::{InputMode, InputOp}; - mode: InputMode, - offset: usize, - cursor: usize, +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub(super) struct InputSnap { + pub(super) value: String, + + pub(super) op: InputOp, + pub(super) start: Option, + + pub(super) mode: InputMode, + pub(super) offset: usize, + pub(super) cursor: usize, } -impl From<&Input> for InputSnap { - fn from(input: &Input) -> Self { +impl InputSnap { + pub(super) fn new(value: String) -> Self { + let cursor = value.chars().count(); + let offset = + cursor.saturating_sub(Self::find_window(&value.chars().rev().collect::(), 0).end); + Self { - value: input.value.clone(), + value, - op: input.op, - start: input.start, + op: Default::default(), + start: Default::default(), - mode: input.mode, - offset: input.offset, - cursor: input.cursor, + mode: Default::default(), + offset, + cursor, } } + + pub(super) fn insert(&mut self) -> bool { + if self.mode != InputMode::Normal { + return false; + } + + self.op = InputOp::None; + self.start = None; + self.mode = InputMode::Insert; + true + } + + pub fn visual(&mut self) -> bool { + if self.mode != InputMode::Normal { + return false; + } else if self.value.is_empty() { + return false; + } + + self.start = Some(self.cursor); + true + } +} + +impl InputSnap { + #[inline] + pub(super) fn len(&self) -> usize { self.value.len() } + + #[inline] + pub(super) fn count(&self) -> usize { self.value.chars().count() } + + #[inline] + pub(super) fn idx(&self, n: usize) -> Option { + self + .value + .char_indices() + .nth(n) + .map(|(i, _)| i) + .or_else(|| if n == self.count() { Some(self.len()) } else { None }) + } + + #[inline] + pub(super) fn slice(&self, range: Range) -> &str { + let (s, e) = (self.idx(range.start), self.idx(range.end)); + &self.value[s.unwrap()..e.unwrap()] + } + + #[inline] + pub(super) fn range(&mut self, cursor: usize, include: bool) -> Option> { + self + .start + .take() + .map(|s| if s <= cursor { (s, cursor) } else { (cursor, s) }) + .map(|(s, e)| s..e + include as usize) + } + + #[inline] + pub(super) fn window(&self) -> Range { Self::find_window(&self.value, self.offset) } + + #[inline] + pub(super) fn find_window(s: &str, offset: usize) -> Range { + let mut width = 0; + let v = s + .chars() + .enumerate() + .skip(offset) + .map_while(|(i, c)| { + width += c.width().unwrap_or(0); + if width < /*TODO: hardcode*/ 50 - 2 { Some(i) } else { None } + }) + .collect::>(); + + if v.is_empty() { + return 0..0; + } + *v.first().unwrap()..v.last().unwrap() + 1 + } } diff --git a/src/core/input/snaps.rs b/src/core/input/snaps.rs new file mode 100644 index 00000000..63520ca0 --- /dev/null +++ b/src/core/input/snaps.rs @@ -0,0 +1,57 @@ +use super::{InputMode, InputSnap}; + +#[derive(Default, PartialEq, Eq)] +pub(super) struct InputSnaps { + idx: usize, + versions: Vec, + current: InputSnap, +} + +impl InputSnaps { + #[inline] + pub(super) fn reset(&mut self, value: String) { + self.idx = 0; + self.versions.clear(); + self.versions.push(InputSnap::new(value)); + self.current = self.versions[0].clone(); + } + + pub(super) fn undo(&mut self) -> bool { + if self.idx == 0 { + return false; + } + + self.idx -= 1; + self.current = self.versions[self.idx].clone(); + true + } + + pub(super) fn redo(&mut self) -> bool { + if self.idx + 1 == self.versions.len() { + return false; + } + + self.idx += 1; + self.current = self.versions[self.idx].clone(); + true + } + + pub(super) fn tag(&mut self) -> bool { + if self.current.value == self.versions[self.idx].value { + return false; + } + + self.versions.truncate(self.idx + 1); + self.versions.push(self.current().clone()); + self.idx += 1; + true + } +} + +impl InputSnaps { + #[inline] + pub(super) fn current(&self) -> &InputSnap { &self.current } + + #[inline] + pub(super) fn current_mut(&mut self) -> &mut InputSnap { &mut self.current } +} diff --git a/src/core/tasks/mod.rs b/src/core/tasks/mod.rs index 7afb79f1..82037064 100644 --- a/src/core/tasks/mod.rs +++ b/src/core/tasks/mod.rs @@ -4,9 +4,9 @@ mod process; mod scheduler; mod tasks; -pub(crate) use file::*; +use file::*; pub use precache::*; -pub(crate) use process::*; +use process::*; pub use scheduler::*; pub use tasks::*; diff --git a/src/ui/dispatcher.rs b/src/ui/dispatcher.rs index 95221887..d90ecb42 100644 --- a/src/ui/dispatcher.rs +++ b/src/ui/dispatcher.rs @@ -210,6 +210,9 @@ impl Executor { "yank" => cx.input.yank(), "paste" => cx.input.paste(exec.named.contains_key("before")), + + "undo" => cx.input.undo(), + "redo" => cx.input.redo(), _ => false, }, InputMode::Insert => match exec.cmd.as_str() {