Added undo/redo support
This commit is contained in:
parent
5925e37fba
commit
5169d5ae92
7 changed files with 244 additions and 55 deletions
|
|
@ -42,6 +42,8 @@ pub enum Action {
|
|||
SelectAll, // Fully select the entire input
|
||||
Save, // Save the current buffer
|
||||
Mouse(MouseAction, [isize; 2]),
|
||||
Undo,
|
||||
Redo,
|
||||
}
|
||||
|
||||
/// How far should movement go?
|
||||
|
|
@ -65,6 +67,8 @@ pub enum Event {
|
|||
Action(Action),
|
||||
// The incoming event is a raw user input.
|
||||
Raw(RawEvent),
|
||||
// A terminal bell ring
|
||||
Bell,
|
||||
}
|
||||
|
||||
impl From<Action> for Event {
|
||||
|
|
@ -85,6 +89,7 @@ impl Event {
|
|||
match self {
|
||||
Self::Action(a) => Some(a.clone()),
|
||||
Self::Raw(te) => translate(te),
|
||||
Self::Bell => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -409,6 +414,24 @@ impl RawEvent {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn to_undo_redo(&self) -> Option<Action> {
|
||||
match &self.0 {
|
||||
TerminalEvent::Key(KeyEvent {
|
||||
code: KeyCode::Char('z'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
kind: KeyEventKind::Press,
|
||||
..
|
||||
}) => Some(Action::Undo),
|
||||
TerminalEvent::Key(KeyEvent {
|
||||
code: KeyCode::Char('y'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
kind: KeyEventKind::Press,
|
||||
..
|
||||
}) => Some(Action::Redo),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_mouse(&self, area: Area) -> Option<Action> {
|
||||
let TerminalEvent::Mouse(ev) = self.0 else {
|
||||
return None;
|
||||
|
|
|
|||
11
src/main.rs
11
src/main.rs
|
|
@ -48,11 +48,12 @@ fn main() -> Result<(), Error> {
|
|||
}
|
||||
|
||||
// Have the UI handle events
|
||||
if ui
|
||||
.handle(&mut state, Event::from_raw(ev))
|
||||
.map_or(false, |r| r.into_ended().is_some())
|
||||
{
|
||||
return Ok(());
|
||||
match ui.handle(&mut state, Event::from_raw(ev)) {
|
||||
Ok(r) if r.is_end() => return Ok(()),
|
||||
Ok(_) => {}
|
||||
Err(Event::Bell) => term.ring_bell(),
|
||||
// Unhandled event!
|
||||
Err(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
224
src/state.rs
224
src/state.rs
|
|
@ -5,6 +5,7 @@ use crate::{
|
|||
};
|
||||
use slotmap::{HopSlotMap, new_key_type};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
io,
|
||||
ops::Range,
|
||||
path::{Path, PathBuf},
|
||||
|
|
@ -15,7 +16,7 @@ new_key_type! {
|
|||
pub struct CursorId;
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Default)]
|
||||
#[derive(Copy, Clone, Debug, Default)]
|
||||
pub struct Cursor {
|
||||
pub base: usize,
|
||||
pub pos: usize,
|
||||
|
|
@ -147,6 +148,31 @@ pub struct Buffer {
|
|||
pub cursors: HopSlotMap<CursorId, Cursor>,
|
||||
pub dir: Option<PathBuf>,
|
||||
pub path: Option<PathBuf>,
|
||||
pub undo: Vec<Change>,
|
||||
pub redo: Vec<Change>,
|
||||
}
|
||||
|
||||
pub struct Change {
|
||||
kind: ChangeKind,
|
||||
cursors: HashMap<CursorId, (Cursor, Cursor)>,
|
||||
}
|
||||
|
||||
pub enum ChangeKind {
|
||||
Insert(usize, Vec<char>),
|
||||
Remove(usize, Vec<char>),
|
||||
}
|
||||
|
||||
impl Change {
|
||||
fn invert(mut self) -> Self {
|
||||
self.kind = match self.kind {
|
||||
ChangeKind::Insert(at, s) => ChangeKind::Remove(at, s),
|
||||
ChangeKind::Remove(at, s) => ChangeKind::Insert(at, s),
|
||||
};
|
||||
for (from, to) in self.cursors.values_mut() {
|
||||
core::mem::swap(from, to);
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Buffer {
|
||||
|
|
@ -175,6 +201,8 @@ impl Buffer {
|
|||
cursors: HopSlotMap::default(),
|
||||
dir,
|
||||
path: Some(path),
|
||||
undo: Vec::new(),
|
||||
redo: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -205,7 +233,7 @@ impl Buffer {
|
|||
.map(|hl| hl.highlighter.highlight(self.text.chars()));
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
pub fn reset(&mut self) {
|
||||
self.unsaved = true;
|
||||
|
||||
self.text.chars.clear();
|
||||
|
|
@ -214,6 +242,7 @@ impl Buffer {
|
|||
self.cursors.values_mut().for_each(|cursor| {
|
||||
*cursor = Cursor::default();
|
||||
});
|
||||
self.undo = Vec::new();
|
||||
}
|
||||
|
||||
pub fn goto_cursor(&mut self, cursor_id: CursorId, pos: [isize; 2]) {
|
||||
|
|
@ -398,26 +427,149 @@ impl Buffer {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, pos: usize, chars: impl IntoIterator<Item = char>) {
|
||||
self.unsaved = true;
|
||||
fn push_undo(&mut self, mut change: Change) {
|
||||
self.redo.clear(); // TODO: Maybe add tree undos?
|
||||
|
||||
let Some(last) = self.undo.last_mut() else {
|
||||
return self.undo.push(change);
|
||||
};
|
||||
|
||||
// Attempt to merge changes together
|
||||
match (&mut last.kind, &mut change.kind) {
|
||||
(ChangeKind::Insert(at, s), ChangeKind::Insert(at2, s2)) if *at + s.len() == *at2 => {
|
||||
s.append(s2);
|
||||
}
|
||||
(ChangeKind::Remove(at, s), ChangeKind::Remove(at2, s2)) if *at == *at2 + s2.len() => {
|
||||
s2.append(s);
|
||||
*s = core::mem::take(s2);
|
||||
*at = *at2;
|
||||
}
|
||||
_ => return self.undo.push(change),
|
||||
}
|
||||
|
||||
for (id, (from2, to2)) in change.cursors {
|
||||
last.cursors
|
||||
.entry(id)
|
||||
.and_modify(|(_, to)| *to = to2)
|
||||
.or_insert((from2, to2));
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_change(&mut self, change: &Change) {
|
||||
match &change.kind {
|
||||
ChangeKind::Insert(at, s) => {
|
||||
for (i, c) in s.iter().enumerate() {
|
||||
self.text.chars.insert(at + i, *c);
|
||||
}
|
||||
}
|
||||
ChangeKind::Remove(at, s) => {
|
||||
self.text.chars.drain(*at..*at + s.len());
|
||||
}
|
||||
}
|
||||
for (id, (_, to)) in change.cursors.iter() {
|
||||
if let Some(c) = self.cursors.get_mut(*id) {
|
||||
// panic!("Changing {c:?} to {to:?}");
|
||||
*c = *to;
|
||||
}
|
||||
}
|
||||
self.update_highlights();
|
||||
}
|
||||
|
||||
pub fn undo(&mut self) -> bool {
|
||||
if let Some(change) = self.undo.pop() {
|
||||
let change = change.invert();
|
||||
self.apply_change(&change);
|
||||
self.redo.push(change);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn redo(&mut self) -> bool {
|
||||
if let Some(change) = self.redo.pop() {
|
||||
let change = change.invert();
|
||||
self.apply_change(&change);
|
||||
self.undo.push(change);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn insert_inner(&mut self, pos: usize, chars: impl IntoIterator<Item = char>) -> Change {
|
||||
let chars = chars.into_iter().collect::<Vec<_>>();
|
||||
let mut n = 0;
|
||||
for c in chars {
|
||||
self.text
|
||||
.chars
|
||||
.insert((pos + n).min(self.text.chars.len()), c);
|
||||
let base = pos.min(self.text.chars.len());
|
||||
for c in &chars {
|
||||
self.text.chars.insert(base + n, *c);
|
||||
n += 1;
|
||||
}
|
||||
self.update_highlights();
|
||||
self.cursors.values_mut().for_each(|cursor| {
|
||||
if cursor.base >= pos {
|
||||
cursor.base += n;
|
||||
}
|
||||
if cursor.pos >= pos {
|
||||
cursor.pos += n;
|
||||
cursor.reset_desired_col(&self.text);
|
||||
}
|
||||
});
|
||||
Change {
|
||||
kind: ChangeKind::Insert(base, chars),
|
||||
cursors: self
|
||||
.cursors
|
||||
.iter_mut()
|
||||
.map(|(id, cursor)| {
|
||||
let old = *cursor;
|
||||
if cursor.base >= pos {
|
||||
cursor.base += n;
|
||||
}
|
||||
if cursor.pos >= pos {
|
||||
cursor.pos += n;
|
||||
cursor.reset_desired_col(&self.text);
|
||||
}
|
||||
(id, (old, *cursor))
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, pos: usize, chars: impl IntoIterator<Item = char>) {
|
||||
self.unsaved = true;
|
||||
let change = self.insert_inner(pos, chars);
|
||||
self.push_undo(change);
|
||||
}
|
||||
|
||||
// Assumes range is well-formed
|
||||
fn remove_inner(&mut self, range: Range<usize>) -> Change {
|
||||
self.unsaved = true;
|
||||
|
||||
// TODO: Bell if false?
|
||||
let removed = self.text.chars.drain(range.clone()).collect();
|
||||
self.update_highlights();
|
||||
Change {
|
||||
kind: ChangeKind::Remove(range.start, removed),
|
||||
cursors: self
|
||||
.cursors
|
||||
.iter_mut()
|
||||
.map(|(id, cursor)| {
|
||||
let old = *cursor;
|
||||
if cursor.base >= range.start {
|
||||
cursor.base = cursor
|
||||
.base
|
||||
.saturating_sub(range.end - range.start)
|
||||
.max(range.start);
|
||||
}
|
||||
if cursor.pos >= range.start {
|
||||
cursor.pos = cursor
|
||||
.pos
|
||||
.saturating_sub(range.end - range.start)
|
||||
.max(range.start);
|
||||
cursor.reset_desired_col(&self.text);
|
||||
}
|
||||
(id, (old, *cursor))
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
// Assumes range is well-formed
|
||||
pub fn remove(&mut self, range: Range<usize>) {
|
||||
self.unsaved = true;
|
||||
let change = self.remove_inner(range);
|
||||
self.push_undo(change);
|
||||
}
|
||||
|
||||
pub fn insert_after(&mut self, cursor_id: CursorId, chars: impl IntoIterator<Item = char>) {
|
||||
|
|
@ -444,30 +596,6 @@ impl Buffer {
|
|||
}
|
||||
}
|
||||
|
||||
// Assumes range is well-formed
|
||||
pub fn remove(&mut self, range: Range<usize>) {
|
||||
self.unsaved = true;
|
||||
|
||||
// TODO: Bell if false?
|
||||
self.text.chars.drain(range.clone());
|
||||
self.update_highlights();
|
||||
self.cursors.values_mut().for_each(|cursor| {
|
||||
if cursor.base >= range.start {
|
||||
cursor.base = cursor
|
||||
.base
|
||||
.saturating_sub(range.end - range.start)
|
||||
.max(range.start);
|
||||
}
|
||||
if cursor.pos >= range.start {
|
||||
cursor.pos = cursor
|
||||
.pos
|
||||
.saturating_sub(range.end - range.start)
|
||||
.max(range.start);
|
||||
cursor.reset_desired_col(&self.text);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn backspace(&mut self, cursor_id: CursorId) {
|
||||
let Some(cursor) = self.cursors.get(cursor_id) else {
|
||||
return;
|
||||
|
|
@ -507,11 +635,17 @@ impl Buffer {
|
|||
let Some(cursor) = self.cursors.get(cursor_id) else {
|
||||
return;
|
||||
};
|
||||
let line = self.text.to_coord(cursor.pos)[1];
|
||||
let line_start = self.text.to_pos([0, line]);
|
||||
let coord = self.text.to_coord(cursor.pos);
|
||||
let line_start = self.text.to_pos([0, coord[1]]);
|
||||
|
||||
let prev_indent = self.text.indent_of_line(line).to_vec();
|
||||
let next_indent = self.text.indent_of_line(line + 1).to_vec();
|
||||
let prev_indent = self
|
||||
.text
|
||||
.indent_of_line(coord[1])
|
||||
.iter()
|
||||
.copied()
|
||||
.take(coord[0] as usize)
|
||||
.collect::<Vec<_>>();
|
||||
let next_indent = self.text.indent_of_line(coord[1] + 1).to_vec();
|
||||
|
||||
let (close_block, extra_indent, trailing_indent, base_indent) = if let Some(last_pos) =
|
||||
cursor
|
||||
|
|
|
|||
|
|
@ -277,6 +277,7 @@ pub struct Terminal<'a> {
|
|||
stdout: StdoutLock<'a>,
|
||||
size: [u16; 2],
|
||||
fb: [Framebuffer; 2],
|
||||
bell: bool,
|
||||
}
|
||||
|
||||
impl<'a> Terminal<'a> {
|
||||
|
|
@ -304,6 +305,7 @@ impl<'a> Terminal<'a> {
|
|||
stdout: io::stdout().lock(),
|
||||
size: [size.columns, size.rows],
|
||||
fb: [Framebuffer::default(), Framebuffer::default()],
|
||||
bell: false,
|
||||
};
|
||||
|
||||
let hook = panic::take_hook();
|
||||
|
|
@ -322,6 +324,10 @@ impl<'a> Terminal<'a> {
|
|||
self.size = size;
|
||||
}
|
||||
|
||||
pub fn ring_bell(&mut self) {
|
||||
self.bell = true;
|
||||
}
|
||||
|
||||
pub fn update(&mut self, render: impl FnOnce(&mut Rect)) {
|
||||
// Reset framebuffer
|
||||
if self.fb[0].size != self.size {
|
||||
|
|
@ -337,6 +343,11 @@ impl<'a> Terminal<'a> {
|
|||
|
||||
self.stdout
|
||||
.sync_update(|stdout| {
|
||||
if self.bell {
|
||||
self.bell = false;
|
||||
stdout.queue(style::Print('\x07')).unwrap();
|
||||
}
|
||||
|
||||
let mut cursor_pos = [0, 0];
|
||||
let mut fg = Color::Reset;
|
||||
let mut bg = Color::Reset;
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ impl Input {
|
|||
.or_else(|| e.to_select_all())
|
||||
.or_else(|| e.to_indent())
|
||||
.or_else(|| e.to_mouse(self.last_area))
|
||||
.or_else(|| e.to_undo_redo())
|
||||
}) {
|
||||
Some(Action::Char(c)) => {
|
||||
if c == '\x08' {
|
||||
|
|
@ -92,6 +93,7 @@ impl Input {
|
|||
}
|
||||
Some(Action::Indent(forward)) => {
|
||||
buffer.indent(cursor_id, forward);
|
||||
self.refocus(buffer, cursor_id);
|
||||
Ok(Resp::handled(None))
|
||||
}
|
||||
Some(Action::GotoLine(line)) => {
|
||||
|
|
@ -101,6 +103,7 @@ impl Input {
|
|||
}
|
||||
Some(Action::SelectToken) => {
|
||||
buffer.select_token_cursor(cursor_id);
|
||||
self.refocus(buffer, cursor_id);
|
||||
Ok(Resp::handled(None))
|
||||
}
|
||||
Some(Action::SelectAll) => {
|
||||
|
|
@ -109,9 +112,24 @@ impl Input {
|
|||
}
|
||||
Some(Action::Mouse(MouseAction::Click, pos)) => {
|
||||
buffer.goto_cursor(cursor_id, [self.focus[0] + pos[0], self.focus[1] + pos[1]]);
|
||||
self.refocus(buffer, cursor_id);
|
||||
Ok(Resp::handled(None))
|
||||
}
|
||||
Some(Action::Undo) => {
|
||||
if buffer.undo() {
|
||||
self.refocus(buffer, cursor_id);
|
||||
Ok(Resp::handled(None))
|
||||
} else {
|
||||
Ok(Resp::handled(Some(Event::Bell)))
|
||||
}
|
||||
}
|
||||
Some(Action::Redo) => {
|
||||
if buffer.redo() {
|
||||
self.refocus(buffer, cursor_id);
|
||||
Ok(Resp::handled(None))
|
||||
} else {
|
||||
Ok(Resp::handled(Some(Event::Bell)))
|
||||
}
|
||||
}
|
||||
_ => Err(event),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ impl Element<()> for Prompt {
|
|||
Some(Action::Cancel) => Ok(Resp::end(None)),
|
||||
Some(Action::Go) => match self.parse_action() {
|
||||
Ok(action) => {
|
||||
self.buffer.clear();
|
||||
self.buffer.reset();
|
||||
Ok(Resp::end(Some(action.into())))
|
||||
}
|
||||
Err(err) => Ok(Resp::handled(Some(
|
||||
|
|
@ -303,7 +303,7 @@ impl Opener {
|
|||
}
|
||||
|
||||
fn set_string(&mut self, s: &str) {
|
||||
self.buffer.clear();
|
||||
self.buffer.reset();
|
||||
self.buffer.enter(self.cursor_id, s.chars());
|
||||
self.update_completions();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ impl Element<()> for Root {
|
|||
fn handle(&mut self, state: &mut State, mut event: Event) -> Result<Resp<()>, Event> {
|
||||
// Pass the event down through the list of tasks until we meet one that can handle it
|
||||
let mut task_idx = self.tasks.len();
|
||||
let action = loop {
|
||||
let event = loop {
|
||||
task_idx = match task_idx.checked_sub(1) {
|
||||
Some(task_idx) => task_idx,
|
||||
None => {
|
||||
|
|
@ -77,7 +77,7 @@ impl Element<()> for Root {
|
|||
};
|
||||
|
||||
// Handle 'top-level' actions
|
||||
if let Some(action) = action.and_then(|e| {
|
||||
if let Some(action) = event.as_ref().and_then(|e| {
|
||||
e.to_action(|e| {
|
||||
e.to_open_prompt()
|
||||
.or_else(|| e.to_cancel())
|
||||
|
|
@ -126,6 +126,8 @@ impl Element<()> for Root {
|
|||
.map(|r| r.into_can_end());
|
||||
}
|
||||
}
|
||||
} else if let Some(event) = event {
|
||||
return Err(event);
|
||||
}
|
||||
|
||||
// Root element swallows all other events
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue