Simple cursor movement, insertion, and deletion

This commit is contained in:
Joshua Barretto 2025-06-06 12:24:06 +01:00
parent ebc4d97dbc
commit 2476ef2a1a
7 changed files with 175 additions and 45 deletions

View file

@ -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'),

View file

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

View file

@ -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,

View file

@ -27,7 +27,7 @@ impl Default for BorderTheme {
bottom_right: '',
join_left: '',
join_right: '',
fg: Color::DarkGrey,
fg: Color::AnsiValue(244),
}
}
}

View file

@ -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);
});
});
}

View file

@ -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;
}
}
}

View file

@ -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()))
}
};
}
};