feat: undo and redo for input

This commit is contained in:
sxyazi 2023-07-22 23:06:40 +08:00
parent f6547dd051
commit 14639f64ac
No known key found for this signature in database
6 changed files with 294 additions and 165 deletions

View file

@ -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<usize>,
pub(crate) mode: InputMode,
pub(crate) offset: usize,
pub(crate) cursor: usize,
callback: Option<Sender<Result<String>>>,
title: String,
position: (u16, u16),
callback: Option<Sender<Result<String>>>,
pub visible: bool,
}
@ -52,102 +48,110 @@ pub enum InputOp {
impl Input {
pub fn show(&mut self, opt: InputOpt, tx: Sender<Result<String>>) {
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::<String>();
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<Rect> {
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<usize> {
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::<Vec<_>>();
(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() }
}

View file

@ -1,5 +1,7 @@
mod input;
mod snap;
mod snaps;
pub use input::*;
pub use snap::*;
use snap::*;
use snaps::*;

View file

@ -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<usize>,
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<usize>,
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::<String>(), 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<usize> {
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<usize>) -> &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<Range<usize>> {
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<usize> { Self::find_window(&self.value, self.offset) }
#[inline]
pub(super) fn find_window(s: &str, offset: usize) -> Range<usize> {
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::<Vec<_>>();
if v.is_empty() {
return 0..0;
}
*v.first().unwrap()..v.last().unwrap() + 1
}
}

57
src/core/input/snaps.rs Normal file
View file

@ -0,0 +1,57 @@
use super::{InputMode, InputSnap};
#[derive(Default, PartialEq, Eq)]
pub(super) struct InputSnaps {
idx: usize,
versions: Vec<InputSnap>,
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 }
}

View file

@ -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::*;

View file

@ -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() {