Simple cursor movement, insertion, and deletion
This commit is contained in:
parent
ebc4d97dbc
commit
2476ef2a1a
7 changed files with 175 additions and 45 deletions
|
|
@ -66,6 +66,7 @@ impl RawEvent {
|
|||
Some(c)
|
||||
}
|
||||
KeyCode::Backspace if modifiers == KeyModifiers::NONE => Some('\x08'),
|
||||
KeyCode::Delete if modifiers == KeyModifiers::NONE => Some('\x7F'),
|
||||
KeyCode::Enter if modifiers == KeyModifiers::NONE => Some('\n'),
|
||||
_ => None,
|
||||
},
|
||||
|
|
@ -109,19 +110,7 @@ impl RawEvent {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn is_go(&self) -> bool {
|
||||
matches!(
|
||||
&self.0,
|
||||
TerminalEvent::Key(KeyEvent {
|
||||
code: KeyCode::Enter,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
kind: KeyEventKind::Press,
|
||||
..
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
pub fn to_open(&self) -> Option<Action> {
|
||||
pub fn to_open_prompt(&self) -> Option<Action> {
|
||||
if matches!(
|
||||
&self.0,
|
||||
TerminalEvent::Key(KeyEvent {
|
||||
|
|
@ -132,7 +121,13 @@ impl RawEvent {
|
|||
})
|
||||
) {
|
||||
Some(Action::OpenPrompt)
|
||||
} else if matches!(
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_open_switcher(&self) -> Option<Action> {
|
||||
if matches!(
|
||||
&self.0,
|
||||
TerminalEvent::Key(KeyEvent {
|
||||
code: KeyCode::Char('b'),
|
||||
|
|
|
|||
111
src/state.rs
111
src/state.rs
|
|
@ -1,5 +1,5 @@
|
|||
use crate::{
|
||||
Action, Args, Color, Error, Event, theme,
|
||||
Action, Args, Color, Dir, Error, Event, theme,
|
||||
ui::{self, Element as _, Resp},
|
||||
};
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
|
||||
|
|
@ -14,11 +14,58 @@ new_key_type! {
|
|||
#[derive(Copy, Clone, Default)]
|
||||
pub struct Cursor {
|
||||
pub pos: usize,
|
||||
// Used to 'remember' the desired column when skipping over shorter lines
|
||||
desired_col: isize,
|
||||
}
|
||||
|
||||
impl Cursor {
|
||||
fn reset_desired_col(&mut self, text: &Text) {
|
||||
self.desired_col = text.to_coord(self.pos)[0];
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Text {
|
||||
chars: Vec<char>,
|
||||
}
|
||||
|
||||
impl Text {
|
||||
pub fn to_coord(&self, pos: usize) -> [isize; 2] {
|
||||
let mut n = 0;
|
||||
let mut i = 0;
|
||||
for line in self.lines() {
|
||||
if (n..n + line.len() + 1).contains(&pos) {
|
||||
return [(pos - n) as isize, i as isize];
|
||||
} else {
|
||||
n += line.len() + 1;
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
[0, i as isize]
|
||||
}
|
||||
|
||||
pub fn to_pos(&self, mut coord: [isize; 2]) -> usize {
|
||||
if coord[1] < 0 {
|
||||
return 0;
|
||||
}
|
||||
let mut pos = 0;
|
||||
for (i, line) in self.lines().enumerate() {
|
||||
if i as isize == coord[1] {
|
||||
return pos + coord[0].clamp(0, line.len() as isize) as usize;
|
||||
} else {
|
||||
pos += line.len() + 1;
|
||||
}
|
||||
}
|
||||
pos.min(self.chars.len())
|
||||
}
|
||||
|
||||
pub fn lines(&self) -> impl Iterator<Item = &[char]> {
|
||||
self.chars.split(|c| *c == '\n')
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Buffer {
|
||||
pub path: PathBuf,
|
||||
pub chars: Vec<char>,
|
||||
pub text: Text,
|
||||
pub cursors: HopSlotMap<CursorId, Cursor>,
|
||||
}
|
||||
|
||||
|
|
@ -32,21 +79,69 @@ impl Buffer {
|
|||
};
|
||||
Ok(Self {
|
||||
path,
|
||||
chars,
|
||||
text: Text { chars },
|
||||
cursors: HopSlotMap::default(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn move_cursor(&mut self, cursor_id: CursorId, dir: Dir) {
|
||||
let Some(cursor) = self.cursors.get_mut(cursor_id) else {
|
||||
return;
|
||||
};
|
||||
match dir {
|
||||
Dir::Left => {
|
||||
cursor.pos = cursor.pos.saturating_sub(1);
|
||||
cursor.reset_desired_col(&self.text);
|
||||
}
|
||||
Dir::Right => {
|
||||
cursor.pos = (cursor.pos + 1).min(self.text.chars.len());
|
||||
cursor.reset_desired_col(&self.text);
|
||||
}
|
||||
Dir::Up => {
|
||||
let mut coord = self.text.to_coord(cursor.pos);
|
||||
cursor.pos = self.text.to_pos([cursor.desired_col, coord[1] - 1]);
|
||||
}
|
||||
Dir::Down => {
|
||||
let mut coord = self.text.to_coord(cursor.pos);
|
||||
cursor.pos = self.text.to_pos([cursor.desired_col, coord[1] + 1]);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, pos: usize, c: char) {
|
||||
self.chars.insert(pos, c);
|
||||
self.cursors.values_mut().for_each(|c| {
|
||||
if c.pos >= pos {
|
||||
c.pos += 1
|
||||
self.text.chars.insert(pos.min(self.text.chars.len()), c);
|
||||
self.cursors.values_mut().for_each(|cursor| {
|
||||
if cursor.pos >= pos {
|
||||
cursor.pos += 1;
|
||||
cursor.reset_desired_col(&self.text);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn begin_session(&mut self) -> CursorId {
|
||||
pub fn remove(&mut self, pos: usize) {
|
||||
// TODO: Bell if false?
|
||||
if self.text.chars.len() > pos {
|
||||
self.text.chars.remove(pos);
|
||||
self.cursors.values_mut().for_each(|cursor| {
|
||||
if cursor.pos >= pos {
|
||||
cursor.pos = cursor.pos.saturating_sub(1);
|
||||
cursor.reset_desired_col(&self.text);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn backspace(&mut self, pos: usize) {
|
||||
if let Some(pos) = pos.checked_sub(1) {
|
||||
self.remove(pos);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn delete(&mut self, pos: usize) {
|
||||
self.remove(pos);
|
||||
}
|
||||
|
||||
pub fn start_session(&mut self) -> CursorId {
|
||||
self.cursors.insert(Cursor::default())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -207,8 +207,11 @@ impl<'a> Rect<'a> {
|
|||
self.rect([0, 0], self.size())
|
||||
}
|
||||
|
||||
pub fn set_cursor(&mut self, cursor: [usize; 2], style: CursorStyle) -> Rect {
|
||||
if self.has_focus {
|
||||
pub fn set_cursor(&mut self, cursor: [isize; 2], style: CursorStyle) -> Rect {
|
||||
if self.has_focus
|
||||
&& (0..=self.size()[0] as isize).contains(&cursor[0])
|
||||
&& (0..self.size()[1] as isize).contains(&cursor[1])
|
||||
{
|
||||
self.fb.cursor = Some((
|
||||
[
|
||||
self.origin[0] + cursor[0] as u16,
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ impl Default for BorderTheme {
|
|||
bottom_right: '╯',
|
||||
join_left: '├',
|
||||
join_right: '┤',
|
||||
fg: Color::DarkGrey,
|
||||
fg: Color::AnsiValue(244),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ impl Visual for Input {
|
|||
.rect([self.preamble.chars().count(), 0], frame.size())
|
||||
.with(|frame| {
|
||||
frame.text([0, 0], &self.text);
|
||||
frame.set_cursor([self.cursor, 0], CursorStyle::BlinkingBar);
|
||||
frame.set_cursor([self.cursor as isize, 0], CursorStyle::BlinkingBar);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,17 @@
|
|||
use super::*;
|
||||
use crate::{
|
||||
state::{BufferId, Cursor, CursorId},
|
||||
state::{Buffer, BufferId, Cursor, CursorId},
|
||||
terminal::CursorStyle,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Doc {
|
||||
buffer: BufferId,
|
||||
cursor: CursorId,
|
||||
// Remember the cursor we use for each buffer
|
||||
cursors: HashMap<BufferId, CursorId>,
|
||||
// x/y location in the buffer that the centre of pane is trying to focus on
|
||||
focus: [isize; 2],
|
||||
}
|
||||
|
||||
impl Doc {
|
||||
|
|
@ -15,7 +19,26 @@ impl Doc {
|
|||
Self {
|
||||
buffer,
|
||||
// TODO: Don't index directly
|
||||
cursor: state.buffers[buffer].begin_session(),
|
||||
cursors: [(buffer, state.buffers[buffer].start_session())]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
focus: [0, 0],
|
||||
}
|
||||
}
|
||||
|
||||
fn refocus(&mut self, state: &mut State) {
|
||||
let Some(buffer) = state.buffers.get_mut(self.buffer) else {
|
||||
return;
|
||||
};
|
||||
let Some(cursor) = buffer.cursors.get(self.cursors[&self.buffer]) else {
|
||||
return;
|
||||
};
|
||||
self.focus = buffer.text.to_coord(cursor.pos);
|
||||
}
|
||||
|
||||
pub fn close(self, state: &mut State) {
|
||||
for (buffer, cursor) in self.cursors {
|
||||
state.buffers[buffer].end_session(cursor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -25,27 +48,42 @@ impl Element for Doc {
|
|||
let Some(buffer) = state.buffers.get_mut(self.buffer) else {
|
||||
return Err(event);
|
||||
};
|
||||
let Some(cursor) = buffer.cursors.get(self.cursor) else {
|
||||
return Err(event);
|
||||
};
|
||||
|
||||
match event.to_action(|e| {
|
||||
e.to_char()
|
||||
.map(Action::Char)
|
||||
.or_else(|| e.to_move().map(Action::Move))
|
||||
.or_else(|| e.to_pane_move().map(Action::PaneMove))
|
||||
.or_else(|| e.to_open_switcher())
|
||||
}) {
|
||||
action @ Some(Action::OpenSwitcher) => Ok(Resp::handled(action)),
|
||||
Some(Action::SwitchBuffer(new_buffer)) => {
|
||||
buffer.end_session(self.cursor);
|
||||
self.buffer = new_buffer;
|
||||
let Some(buffer) = state.buffers.get_mut(self.buffer) else {
|
||||
return Err(event);
|
||||
};
|
||||
self.cursor = buffer.begin_session();
|
||||
// Start a new cursor session for this buffer if one doesn't exist
|
||||
self.cursors
|
||||
.entry(self.buffer)
|
||||
.or_insert_with(|| buffer.start_session());
|
||||
self.refocus(state);
|
||||
Ok(Resp::handled(None))
|
||||
}
|
||||
Some(Action::Char(c)) => {
|
||||
buffer.insert(cursor.pos, c);
|
||||
let Some(cursor) = buffer.cursors.get(self.cursors[&self.buffer]) else {
|
||||
return Err(event);
|
||||
};
|
||||
if c == '\x08' {
|
||||
buffer.backspace(cursor.pos);
|
||||
} else if c == '\x7F' {
|
||||
buffer.delete(cursor.pos);
|
||||
} else {
|
||||
buffer.insert(cursor.pos, c);
|
||||
}
|
||||
Ok(Resp::handled(None))
|
||||
}
|
||||
Some(Action::Move(dir)) => {
|
||||
buffer.move_cursor(self.cursors[&self.buffer], dir);
|
||||
Ok(Resp::handled(None))
|
||||
}
|
||||
_ => Err(event),
|
||||
|
|
@ -58,19 +96,16 @@ impl Visual for Doc {
|
|||
let Some(buffer) = state.buffers.get(self.buffer) else {
|
||||
return;
|
||||
};
|
||||
let Some(cursor) = buffer.cursors.get(self.cursor) else {
|
||||
let Some(cursor) = buffer.cursors.get(self.cursors[&self.buffer]) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut n = 0;
|
||||
for (i, line) in buffer.chars.split(|c| *c == '\n').enumerate() {
|
||||
// Set cursor position
|
||||
let cursor_coord = buffer.text.to_coord(cursor.pos);
|
||||
frame.set_cursor(cursor_coord, CursorStyle::BlinkingBar);
|
||||
|
||||
for (i, line) in buffer.text.lines().enumerate() {
|
||||
frame.text([0, i], line);
|
||||
|
||||
if (n..=n + line.len()).contains(&cursor.pos) {
|
||||
frame.set_cursor([cursor.pos - n, i], CursorStyle::BlinkingBar);
|
||||
}
|
||||
|
||||
n += line.len() + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,7 +34,9 @@ impl Element<CanEnd> for Root {
|
|||
None => {
|
||||
break match self.panes.handle(state, event) {
|
||||
Ok(resp) => resp.action,
|
||||
Err(event) => event.to_action(|e| e.to_open().or_else(|| e.to_cancel())),
|
||||
Err(event) => {
|
||||
event.to_action(|e| e.to_open_prompt().or_else(|| e.to_cancel()))
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue