More sensible undo/redo, better mouse controls

This commit is contained in:
Joshua Barretto 2025-09-22 21:59:49 +01:00
parent 5169d5ae92
commit 923039813e
4 changed files with 135 additions and 50 deletions

View file

@ -17,19 +17,20 @@ pub enum Dir {
#[derive(Clone, Debug)]
pub enum Action {
Char(char), // Insert a character
Indent(bool), // Indent (indent vs deindent)
Move(Dir, Dist, bool, bool), // Move the cursor (dir, dist, retain_base, word)
PaneMove(Dir), // Move panes
PaneOpen(Dir), // Create a new pane
PaneClose, // Close the current pane
Cancel, // Cancels the current action
Continue, // Continue past an info-only element (like a help screen)
Go, // Search, accept, or select the current option
Yes, // A binary confirmation is answered 'yes'
No, // A binary confirmation is answered 'no'
Quit, // Quit the application
OpenPrompt, // Open the command prompt
Char(char), // Insert a character
Indent(bool), // Indent (indent vs deindent)
Move(Dir, Dist, bool, bool), // Move the cursor (dir, dist, retain_base, word)
Pan(Dir, Dist), // Pan the view window
PaneMove(Dir), // Move panes
PaneOpen(Dir), // Create a new pane
PaneClose, // Close the current pane
Cancel, // Cancels the current action
Continue, // Continue past an info-only element (like a help screen)
Go, // Search, accept, or select the current option
Yes, // A binary confirmation is answered 'yes'
No, // A binary confirmation is answered 'no'
Quit, // Quit the application
OpenPrompt, // Open the command prompt
Show(Option<String>, String), // Display an optionally titled informational text box to the user
OpenSwitcher, // Open the buffer switcher
OpenOpener(PathBuf), // Open the file opener
@ -41,7 +42,7 @@ pub enum Action {
SelectToken, // Fully select the token under the cursor
SelectAll, // Fully select the entire input
Save, // Save the current buffer
Mouse(MouseAction, [isize; 2]),
Mouse(MouseAction, [isize; 2], bool), // (action, pos, is_ctrl)
Undo,
Redo,
}
@ -57,6 +58,7 @@ pub enum Dist {
#[derive(Clone, Debug)]
pub enum MouseAction {
Click,
Drag,
ScrollDown,
ScrollUp,
}
@ -176,11 +178,11 @@ impl RawEvent {
pub fn to_move(&self) -> Option<Action> {
let (dir, dist, retain_base, word) = match &self.0 {
TerminalEvent::Mouse(ev) => match ev.kind {
MouseEventKind::ScrollUp => (Dir::Up, Dist::Char, false, false),
MouseEventKind::ScrollDown => (Dir::Down, Dist::Char, false, false),
_ => return None,
},
// TerminalEvent::Mouse(ev) => match ev.kind {
// MouseEventKind::ScrollUp => (Dir::Up, Dist::Char, false, false),
// MouseEventKind::ScrollDown => (Dir::Down, Dist::Char, false, false),
// _ => return None,
// },
TerminalEvent::Key(KeyEvent {
code,
modifiers,
@ -208,6 +210,19 @@ impl RawEvent {
Some(Action::Move(dir, dist, retain_base, word))
}
pub fn to_pan(&self) -> Option<Action> {
let (dir, dist) = match &self.0 {
TerminalEvent::Mouse(ev) => match ev.kind {
MouseEventKind::ScrollUp => (Dir::Up, Dist::Char),
MouseEventKind::ScrollDown => (Dir::Down, Dist::Char),
_ => return None,
},
_ => return None,
};
Some(Action::Pan(dir, dist))
}
pub fn to_select_token(&self) -> Option<Action> {
if matches!(
&self.0,
@ -438,14 +453,15 @@ impl RawEvent {
};
if let Some(pos) = area.contains([ev.column as isize, ev.row as isize]) {
match ev.kind {
MouseEventKind::ScrollUp => Some(Action::Mouse(MouseAction::ScrollUp, pos)),
MouseEventKind::ScrollDown => Some(Action::Mouse(MouseAction::ScrollDown, pos)),
MouseEventKind::Down(MouseButton::Left) => {
Some(Action::Mouse(MouseAction::Click, pos))
}
_ => None,
}
let action = match ev.kind {
MouseEventKind::ScrollUp => MouseAction::ScrollUp,
MouseEventKind::ScrollDown => MouseAction::ScrollDown,
MouseEventKind::Down(MouseButton::Left) => MouseAction::Click,
MouseEventKind::Drag(MouseButton::Left) => MouseAction::Drag,
_ => return None,
};
let is_ctrl = ev.modifiers == KeyModifiers::CONTROL;
Some(Action::Mouse(action, pos, is_ctrl))
} else {
None
}

View file

@ -150,10 +150,12 @@ pub struct Buffer {
pub path: Option<PathBuf>,
pub undo: Vec<Change>,
pub redo: Vec<Change>,
action_counter: usize,
}
pub struct Change {
kind: ChangeKind,
action_id: usize,
cursors: HashMap<CursorId, (Cursor, Cursor)>,
}
@ -203,6 +205,7 @@ impl Buffer {
path: Some(path),
undo: Vec::new(),
redo: Vec::new(),
action_counter: 0,
})
}
@ -245,13 +248,15 @@ impl Buffer {
self.undo = Vec::new();
}
pub fn goto_cursor(&mut self, cursor_id: CursorId, pos: [isize; 2]) {
pub fn goto_cursor(&mut self, cursor_id: CursorId, pos: [isize; 2], set_base: bool) {
let Some(cursor) = self.cursors.get_mut(cursor_id) else {
return;
};
cursor.pos = self.text.to_pos(pos);
cursor.reset_desired_col(&self.text);
cursor.base = cursor.pos;
if set_base {
cursor.base = cursor.pos;
}
}
pub fn select_token_cursor(&mut self, cursor_id: CursorId) {
@ -427,6 +432,10 @@ impl Buffer {
}
}
pub fn begin_action(&mut self) {
self.action_counter += 1;
}
fn push_undo(&mut self, mut change: Change) {
self.redo.clear(); // TODO: Maybe add tree undos?
@ -475,26 +484,45 @@ impl Buffer {
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
fn undo_or_redo(&mut self, is_undo: bool) -> bool {
if let Some(mut change) = if is_undo {
self.undo.pop()
} else {
self.redo.pop()
} {
let action_id = change.action_id;
// Keep applying previous changes provided they were part of the same action
loop {
let inv_change = change.invert();
self.apply_change(&inv_change);
if is_undo {
self.redo.push(inv_change)
} else {
self.undo.push(inv_change)
}
change = if let Some(c) = (if is_undo {
&mut self.undo
} else {
&mut self.redo
})
.pop_if(|c| c.action_id == action_id)
{
c
} else {
break true;
};
}
} else {
false
}
}
pub fn undo(&mut self) -> bool {
self.undo_or_redo(true)
}
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
}
self.undo_or_redo(false)
}
fn insert_inner(&mut self, pos: usize, chars: impl IntoIterator<Item = char>) -> Change {
@ -508,6 +536,7 @@ impl Buffer {
self.update_highlights();
Change {
kind: ChangeKind::Insert(base, chars),
action_id: self.action_counter,
cursors: self
.cursors
.iter_mut()
@ -541,6 +570,7 @@ impl Buffer {
self.update_highlights();
Change {
kind: ChangeKind::Remove(range.start, removed),
action_id: self.action_counter,
cursors: self
.cursors
.iter_mut()
@ -657,7 +687,7 @@ impl Buffer {
.iter()
.find(|(l, _)| l == last_char)
&& let next_pos = cursor.selection().map_or(cursor.pos, |s| s.end)
&& let next_char = self
&& let next_tok = self
.text
.chars()
.get(next_pos..)
@ -665,11 +695,16 @@ impl Buffer {
.iter()
.filter(|c| !c.is_ascii_whitespace())
.next()
&& let next_char = self.text.chars().get(next_pos)
{
let close_block = next_char != Some(r)
let close_block = (next_tok != Some(r)
&& next_indent
.strip_prefix(&*prev_indent)
.map_or(true, |i| i.is_empty());
.map_or(false, |i| i.is_empty()))
|| (next_char != Some(r)
&& prev_indent
.strip_prefix(&*next_indent)
.map_or(false, |i| !i.is_empty()));
(
if close_block { Some(*r) } else { None },
true,

View file

@ -57,10 +57,12 @@ impl Input {
cursor_id: CursorId,
event: Event,
) -> Result<Resp, Event> {
buffer.begin_action();
match event.to_action(|e| {
e.to_char()
.map(Action::Char)
.or_else(|| e.to_move())
.or_else(|| e.to_pan())
.or_else(|| e.to_select_token())
.or_else(|| e.to_select_all())
.or_else(|| e.to_indent())
@ -91,13 +93,30 @@ impl Input {
self.refocus(buffer, cursor_id);
Ok(Resp::handled(None))
}
Some(Action::Pan(dir, dist)) => {
let dist = match dist {
Dist::Char => [1, 1],
Dist::Page => self.last_area.size().map(|s| s.saturating_sub(3).max(1)),
// TODO: Don't just use an arbitrary very large number
Dist::Doc => [1_000_000_000; 2],
};
let dfocus = match dir {
Dir::Up => [0, -1],
Dir::Down => [0, 1],
Dir::Left => [-1, 0],
Dir::Right => [1, 0],
};
self.focus[0] += dfocus[0] * dist[0] as isize;
self.focus[1] += dfocus[1] * dist[1] as isize;
Ok(Resp::handled(None))
}
Some(Action::Indent(forward)) => {
buffer.indent(cursor_id, forward);
self.refocus(buffer, cursor_id);
Ok(Resp::handled(None))
}
Some(Action::GotoLine(line)) => {
buffer.goto_cursor(cursor_id, [0, line]);
buffer.goto_cursor(cursor_id, [0, line], true);
self.refocus(buffer, cursor_id);
Ok(Resp::handled(None))
}
@ -110,8 +129,23 @@ impl Input {
buffer.select_all_cursor(cursor_id);
Ok(Resp::handled(None))
}
Some(Action::Mouse(MouseAction::Click, pos)) => {
buffer.goto_cursor(cursor_id, [self.focus[0] + pos[0], self.focus[1] + pos[1]]);
Some(Action::Mouse(MouseAction::Click, pos, false)) => {
buffer.goto_cursor(
cursor_id,
[self.focus[0] + pos[0], self.focus[1] + pos[1]],
true,
);
Ok(Resp::handled(None))
}
Some(
Action::Mouse(MouseAction::Drag, pos, false)
| Action::Mouse(MouseAction::Click, pos, true),
) => {
buffer.goto_cursor(
cursor_id,
[self.focus[0] + pos[0], self.focus[1] + pos[1]],
false,
);
Ok(Resp::handled(None))
}
Some(Action::Undo) => {

View file

@ -86,7 +86,7 @@ impl Element for Panes {
self.selected = new_idx;
Ok(Resp::handled(None))
}
Some(Action::Mouse(_, pos)) => {
Some(Action::Mouse(_, pos, _)) => {
for (i, pane) in self.panes.iter_mut().enumerate() {
if pane.last_area.contains(pos).is_some() {
self.selected = i;