More sensible undo/redo, better mouse controls
This commit is contained in:
parent
5169d5ae92
commit
923039813e
4 changed files with 135 additions and 50 deletions
|
|
@ -17,19 +17,20 @@ pub enum Dir {
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum Action {
|
pub enum Action {
|
||||||
Char(char), // Insert a character
|
Char(char), // Insert a character
|
||||||
Indent(bool), // Indent (indent vs deindent)
|
Indent(bool), // Indent (indent vs deindent)
|
||||||
Move(Dir, Dist, bool, bool), // Move the cursor (dir, dist, retain_base, word)
|
Move(Dir, Dist, bool, bool), // Move the cursor (dir, dist, retain_base, word)
|
||||||
PaneMove(Dir), // Move panes
|
Pan(Dir, Dist), // Pan the view window
|
||||||
PaneOpen(Dir), // Create a new pane
|
PaneMove(Dir), // Move panes
|
||||||
PaneClose, // Close the current pane
|
PaneOpen(Dir), // Create a new pane
|
||||||
Cancel, // Cancels the current action
|
PaneClose, // Close the current pane
|
||||||
Continue, // Continue past an info-only element (like a help screen)
|
Cancel, // Cancels the current action
|
||||||
Go, // Search, accept, or select the current option
|
Continue, // Continue past an info-only element (like a help screen)
|
||||||
Yes, // A binary confirmation is answered 'yes'
|
Go, // Search, accept, or select the current option
|
||||||
No, // A binary confirmation is answered 'no'
|
Yes, // A binary confirmation is answered 'yes'
|
||||||
Quit, // Quit the application
|
No, // A binary confirmation is answered 'no'
|
||||||
OpenPrompt, // Open the command prompt
|
Quit, // Quit the application
|
||||||
|
OpenPrompt, // Open the command prompt
|
||||||
Show(Option<String>, String), // Display an optionally titled informational text box to the user
|
Show(Option<String>, String), // Display an optionally titled informational text box to the user
|
||||||
OpenSwitcher, // Open the buffer switcher
|
OpenSwitcher, // Open the buffer switcher
|
||||||
OpenOpener(PathBuf), // Open the file opener
|
OpenOpener(PathBuf), // Open the file opener
|
||||||
|
|
@ -41,7 +42,7 @@ pub enum Action {
|
||||||
SelectToken, // Fully select the token under the cursor
|
SelectToken, // Fully select the token under the cursor
|
||||||
SelectAll, // Fully select the entire input
|
SelectAll, // Fully select the entire input
|
||||||
Save, // Save the current buffer
|
Save, // Save the current buffer
|
||||||
Mouse(MouseAction, [isize; 2]),
|
Mouse(MouseAction, [isize; 2], bool), // (action, pos, is_ctrl)
|
||||||
Undo,
|
Undo,
|
||||||
Redo,
|
Redo,
|
||||||
}
|
}
|
||||||
|
|
@ -57,6 +58,7 @@ pub enum Dist {
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum MouseAction {
|
pub enum MouseAction {
|
||||||
Click,
|
Click,
|
||||||
|
Drag,
|
||||||
ScrollDown,
|
ScrollDown,
|
||||||
ScrollUp,
|
ScrollUp,
|
||||||
}
|
}
|
||||||
|
|
@ -176,11 +178,11 @@ impl RawEvent {
|
||||||
|
|
||||||
pub fn to_move(&self) -> Option<Action> {
|
pub fn to_move(&self) -> Option<Action> {
|
||||||
let (dir, dist, retain_base, word) = match &self.0 {
|
let (dir, dist, retain_base, word) = match &self.0 {
|
||||||
TerminalEvent::Mouse(ev) => match ev.kind {
|
// TerminalEvent::Mouse(ev) => match ev.kind {
|
||||||
MouseEventKind::ScrollUp => (Dir::Up, Dist::Char, false, false),
|
// MouseEventKind::ScrollUp => (Dir::Up, Dist::Char, false, false),
|
||||||
MouseEventKind::ScrollDown => (Dir::Down, Dist::Char, false, false),
|
// MouseEventKind::ScrollDown => (Dir::Down, Dist::Char, false, false),
|
||||||
_ => return None,
|
// _ => return None,
|
||||||
},
|
// },
|
||||||
TerminalEvent::Key(KeyEvent {
|
TerminalEvent::Key(KeyEvent {
|
||||||
code,
|
code,
|
||||||
modifiers,
|
modifiers,
|
||||||
|
|
@ -208,6 +210,19 @@ impl RawEvent {
|
||||||
Some(Action::Move(dir, dist, retain_base, word))
|
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> {
|
pub fn to_select_token(&self) -> Option<Action> {
|
||||||
if matches!(
|
if matches!(
|
||||||
&self.0,
|
&self.0,
|
||||||
|
|
@ -438,14 +453,15 @@ impl RawEvent {
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(pos) = area.contains([ev.column as isize, ev.row as isize]) {
|
if let Some(pos) = area.contains([ev.column as isize, ev.row as isize]) {
|
||||||
match ev.kind {
|
let action = match ev.kind {
|
||||||
MouseEventKind::ScrollUp => Some(Action::Mouse(MouseAction::ScrollUp, pos)),
|
MouseEventKind::ScrollUp => MouseAction::ScrollUp,
|
||||||
MouseEventKind::ScrollDown => Some(Action::Mouse(MouseAction::ScrollDown, pos)),
|
MouseEventKind::ScrollDown => MouseAction::ScrollDown,
|
||||||
MouseEventKind::Down(MouseButton::Left) => {
|
MouseEventKind::Down(MouseButton::Left) => MouseAction::Click,
|
||||||
Some(Action::Mouse(MouseAction::Click, pos))
|
MouseEventKind::Drag(MouseButton::Left) => MouseAction::Drag,
|
||||||
}
|
_ => return None,
|
||||||
_ => None,
|
};
|
||||||
}
|
let is_ctrl = ev.modifiers == KeyModifiers::CONTROL;
|
||||||
|
Some(Action::Mouse(action, pos, is_ctrl))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
|
||||||
73
src/state.rs
73
src/state.rs
|
|
@ -150,10 +150,12 @@ pub struct Buffer {
|
||||||
pub path: Option<PathBuf>,
|
pub path: Option<PathBuf>,
|
||||||
pub undo: Vec<Change>,
|
pub undo: Vec<Change>,
|
||||||
pub redo: Vec<Change>,
|
pub redo: Vec<Change>,
|
||||||
|
action_counter: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Change {
|
pub struct Change {
|
||||||
kind: ChangeKind,
|
kind: ChangeKind,
|
||||||
|
action_id: usize,
|
||||||
cursors: HashMap<CursorId, (Cursor, Cursor)>,
|
cursors: HashMap<CursorId, (Cursor, Cursor)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -203,6 +205,7 @@ impl Buffer {
|
||||||
path: Some(path),
|
path: Some(path),
|
||||||
undo: Vec::new(),
|
undo: Vec::new(),
|
||||||
redo: Vec::new(),
|
redo: Vec::new(),
|
||||||
|
action_counter: 0,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -245,13 +248,15 @@ impl Buffer {
|
||||||
self.undo = Vec::new();
|
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 {
|
let Some(cursor) = self.cursors.get_mut(cursor_id) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
cursor.pos = self.text.to_pos(pos);
|
cursor.pos = self.text.to_pos(pos);
|
||||||
cursor.reset_desired_col(&self.text);
|
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) {
|
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) {
|
fn push_undo(&mut self, mut change: Change) {
|
||||||
self.redo.clear(); // TODO: Maybe add tree undos?
|
self.redo.clear(); // TODO: Maybe add tree undos?
|
||||||
|
|
||||||
|
|
@ -475,26 +484,45 @@ impl Buffer {
|
||||||
self.update_highlights();
|
self.update_highlights();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn undo(&mut self) -> bool {
|
fn undo_or_redo(&mut self, is_undo: bool) -> bool {
|
||||||
if let Some(change) = self.undo.pop() {
|
if let Some(mut change) = if is_undo {
|
||||||
let change = change.invert();
|
self.undo.pop()
|
||||||
self.apply_change(&change);
|
} else {
|
||||||
self.redo.push(change);
|
self.redo.pop()
|
||||||
true
|
} {
|
||||||
|
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 {
|
} else {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn undo(&mut self) -> bool {
|
||||||
|
self.undo_or_redo(true)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn redo(&mut self) -> bool {
|
pub fn redo(&mut self) -> bool {
|
||||||
if let Some(change) = self.redo.pop() {
|
self.undo_or_redo(false)
|
||||||
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 {
|
fn insert_inner(&mut self, pos: usize, chars: impl IntoIterator<Item = char>) -> Change {
|
||||||
|
|
@ -508,6 +536,7 @@ impl Buffer {
|
||||||
self.update_highlights();
|
self.update_highlights();
|
||||||
Change {
|
Change {
|
||||||
kind: ChangeKind::Insert(base, chars),
|
kind: ChangeKind::Insert(base, chars),
|
||||||
|
action_id: self.action_counter,
|
||||||
cursors: self
|
cursors: self
|
||||||
.cursors
|
.cursors
|
||||||
.iter_mut()
|
.iter_mut()
|
||||||
|
|
@ -541,6 +570,7 @@ impl Buffer {
|
||||||
self.update_highlights();
|
self.update_highlights();
|
||||||
Change {
|
Change {
|
||||||
kind: ChangeKind::Remove(range.start, removed),
|
kind: ChangeKind::Remove(range.start, removed),
|
||||||
|
action_id: self.action_counter,
|
||||||
cursors: self
|
cursors: self
|
||||||
.cursors
|
.cursors
|
||||||
.iter_mut()
|
.iter_mut()
|
||||||
|
|
@ -657,7 +687,7 @@ impl Buffer {
|
||||||
.iter()
|
.iter()
|
||||||
.find(|(l, _)| l == last_char)
|
.find(|(l, _)| l == last_char)
|
||||||
&& let next_pos = cursor.selection().map_or(cursor.pos, |s| s.end)
|
&& let next_pos = cursor.selection().map_or(cursor.pos, |s| s.end)
|
||||||
&& let next_char = self
|
&& let next_tok = self
|
||||||
.text
|
.text
|
||||||
.chars()
|
.chars()
|
||||||
.get(next_pos..)
|
.get(next_pos..)
|
||||||
|
|
@ -665,11 +695,16 @@ impl Buffer {
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|c| !c.is_ascii_whitespace())
|
.filter(|c| !c.is_ascii_whitespace())
|
||||||
.next()
|
.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
|
&& next_indent
|
||||||
.strip_prefix(&*prev_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 },
|
if close_block { Some(*r) } else { None },
|
||||||
true,
|
true,
|
||||||
|
|
|
||||||
|
|
@ -57,10 +57,12 @@ impl Input {
|
||||||
cursor_id: CursorId,
|
cursor_id: CursorId,
|
||||||
event: Event,
|
event: Event,
|
||||||
) -> Result<Resp, Event> {
|
) -> Result<Resp, Event> {
|
||||||
|
buffer.begin_action();
|
||||||
match event.to_action(|e| {
|
match event.to_action(|e| {
|
||||||
e.to_char()
|
e.to_char()
|
||||||
.map(Action::Char)
|
.map(Action::Char)
|
||||||
.or_else(|| e.to_move())
|
.or_else(|| e.to_move())
|
||||||
|
.or_else(|| e.to_pan())
|
||||||
.or_else(|| e.to_select_token())
|
.or_else(|| e.to_select_token())
|
||||||
.or_else(|| e.to_select_all())
|
.or_else(|| e.to_select_all())
|
||||||
.or_else(|| e.to_indent())
|
.or_else(|| e.to_indent())
|
||||||
|
|
@ -91,13 +93,30 @@ impl Input {
|
||||||
self.refocus(buffer, cursor_id);
|
self.refocus(buffer, cursor_id);
|
||||||
Ok(Resp::handled(None))
|
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)) => {
|
Some(Action::Indent(forward)) => {
|
||||||
buffer.indent(cursor_id, forward);
|
buffer.indent(cursor_id, forward);
|
||||||
self.refocus(buffer, cursor_id);
|
self.refocus(buffer, cursor_id);
|
||||||
Ok(Resp::handled(None))
|
Ok(Resp::handled(None))
|
||||||
}
|
}
|
||||||
Some(Action::GotoLine(line)) => {
|
Some(Action::GotoLine(line)) => {
|
||||||
buffer.goto_cursor(cursor_id, [0, line]);
|
buffer.goto_cursor(cursor_id, [0, line], true);
|
||||||
self.refocus(buffer, cursor_id);
|
self.refocus(buffer, cursor_id);
|
||||||
Ok(Resp::handled(None))
|
Ok(Resp::handled(None))
|
||||||
}
|
}
|
||||||
|
|
@ -110,8 +129,23 @@ impl Input {
|
||||||
buffer.select_all_cursor(cursor_id);
|
buffer.select_all_cursor(cursor_id);
|
||||||
Ok(Resp::handled(None))
|
Ok(Resp::handled(None))
|
||||||
}
|
}
|
||||||
Some(Action::Mouse(MouseAction::Click, pos)) => {
|
Some(Action::Mouse(MouseAction::Click, pos, false)) => {
|
||||||
buffer.goto_cursor(cursor_id, [self.focus[0] + pos[0], self.focus[1] + pos[1]]);
|
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))
|
Ok(Resp::handled(None))
|
||||||
}
|
}
|
||||||
Some(Action::Undo) => {
|
Some(Action::Undo) => {
|
||||||
|
|
|
||||||
|
|
@ -86,7 +86,7 @@ impl Element for Panes {
|
||||||
self.selected = new_idx;
|
self.selected = new_idx;
|
||||||
Ok(Resp::handled(None))
|
Ok(Resp::handled(None))
|
||||||
}
|
}
|
||||||
Some(Action::Mouse(_, pos)) => {
|
Some(Action::Mouse(_, pos, _)) => {
|
||||||
for (i, pane) in self.panes.iter_mut().enumerate() {
|
for (i, pane) in self.panes.iter_mut().enumerate() {
|
||||||
if pane.last_area.contains(pos).is_some() {
|
if pane.last_area.contains(pos).is_some() {
|
||||||
self.selected = i;
|
self.selected = i;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue