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)
|
Some(c)
|
||||||
}
|
}
|
||||||
KeyCode::Backspace if modifiers == KeyModifiers::NONE => Some('\x08'),
|
KeyCode::Backspace if modifiers == KeyModifiers::NONE => Some('\x08'),
|
||||||
|
KeyCode::Delete if modifiers == KeyModifiers::NONE => Some('\x7F'),
|
||||||
KeyCode::Enter if modifiers == KeyModifiers::NONE => Some('\n'),
|
KeyCode::Enter if modifiers == KeyModifiers::NONE => Some('\n'),
|
||||||
_ => None,
|
_ => None,
|
||||||
},
|
},
|
||||||
|
|
@ -109,19 +110,7 @@ impl RawEvent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_go(&self) -> bool {
|
pub fn to_open_prompt(&self) -> Option<Action> {
|
||||||
matches!(
|
|
||||||
&self.0,
|
|
||||||
TerminalEvent::Key(KeyEvent {
|
|
||||||
code: KeyCode::Enter,
|
|
||||||
modifiers: KeyModifiers::NONE,
|
|
||||||
kind: KeyEventKind::Press,
|
|
||||||
..
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn to_open(&self) -> Option<Action> {
|
|
||||||
if matches!(
|
if matches!(
|
||||||
&self.0,
|
&self.0,
|
||||||
TerminalEvent::Key(KeyEvent {
|
TerminalEvent::Key(KeyEvent {
|
||||||
|
|
@ -132,7 +121,13 @@ impl RawEvent {
|
||||||
})
|
})
|
||||||
) {
|
) {
|
||||||
Some(Action::OpenPrompt)
|
Some(Action::OpenPrompt)
|
||||||
} else if matches!(
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_open_switcher(&self) -> Option<Action> {
|
||||||
|
if matches!(
|
||||||
&self.0,
|
&self.0,
|
||||||
TerminalEvent::Key(KeyEvent {
|
TerminalEvent::Key(KeyEvent {
|
||||||
code: KeyCode::Char('b'),
|
code: KeyCode::Char('b'),
|
||||||
|
|
|
||||||
111
src/state.rs
111
src/state.rs
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
Action, Args, Color, Error, Event, theme,
|
Action, Args, Color, Dir, Error, Event, theme,
|
||||||
ui::{self, Element as _, Resp},
|
ui::{self, Element as _, Resp},
|
||||||
};
|
};
|
||||||
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
|
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
|
||||||
|
|
@ -14,11 +14,58 @@ new_key_type! {
|
||||||
#[derive(Copy, Clone, Default)]
|
#[derive(Copy, Clone, Default)]
|
||||||
pub struct Cursor {
|
pub struct Cursor {
|
||||||
pub pos: usize,
|
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 struct Buffer {
|
||||||
pub path: PathBuf,
|
pub path: PathBuf,
|
||||||
pub chars: Vec<char>,
|
pub text: Text,
|
||||||
pub cursors: HopSlotMap<CursorId, Cursor>,
|
pub cursors: HopSlotMap<CursorId, Cursor>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -32,21 +79,69 @@ impl Buffer {
|
||||||
};
|
};
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
path,
|
path,
|
||||||
chars,
|
text: Text { chars },
|
||||||
cursors: HopSlotMap::default(),
|
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) {
|
pub fn insert(&mut self, pos: usize, c: char) {
|
||||||
self.chars.insert(pos, c);
|
self.text.chars.insert(pos.min(self.text.chars.len()), c);
|
||||||
self.cursors.values_mut().for_each(|c| {
|
self.cursors.values_mut().for_each(|cursor| {
|
||||||
if c.pos >= pos {
|
if cursor.pos >= pos {
|
||||||
c.pos += 1
|
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())
|
self.cursors.insert(Cursor::default())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -207,8 +207,11 @@ impl<'a> Rect<'a> {
|
||||||
self.rect([0, 0], self.size())
|
self.rect([0, 0], self.size())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_cursor(&mut self, cursor: [usize; 2], style: CursorStyle) -> Rect {
|
pub fn set_cursor(&mut self, cursor: [isize; 2], style: CursorStyle) -> Rect {
|
||||||
if self.has_focus {
|
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.fb.cursor = Some((
|
||||||
[
|
[
|
||||||
self.origin[0] + cursor[0] as u16,
|
self.origin[0] + cursor[0] as u16,
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ impl Default for BorderTheme {
|
||||||
bottom_right: '╯',
|
bottom_right: '╯',
|
||||||
join_left: '├',
|
join_left: '├',
|
||||||
join_right: '┤',
|
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())
|
.rect([self.preamble.chars().count(), 0], frame.size())
|
||||||
.with(|frame| {
|
.with(|frame| {
|
||||||
frame.text([0, 0], &self.text);
|
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 super::*;
|
||||||
use crate::{
|
use crate::{
|
||||||
state::{BufferId, Cursor, CursorId},
|
state::{Buffer, BufferId, Cursor, CursorId},
|
||||||
terminal::CursorStyle,
|
terminal::CursorStyle,
|
||||||
};
|
};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Doc {
|
pub struct Doc {
|
||||||
buffer: BufferId,
|
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 {
|
impl Doc {
|
||||||
|
|
@ -15,7 +19,26 @@ impl Doc {
|
||||||
Self {
|
Self {
|
||||||
buffer,
|
buffer,
|
||||||
// TODO: Don't index directly
|
// 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 {
|
let Some(buffer) = state.buffers.get_mut(self.buffer) else {
|
||||||
return Err(event);
|
return Err(event);
|
||||||
};
|
};
|
||||||
let Some(cursor) = buffer.cursors.get(self.cursor) else {
|
|
||||||
return Err(event);
|
|
||||||
};
|
|
||||||
|
|
||||||
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().map(Action::Move))
|
.or_else(|| e.to_move().map(Action::Move))
|
||||||
.or_else(|| e.to_pane_move().map(Action::PaneMove))
|
.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)) => {
|
Some(Action::SwitchBuffer(new_buffer)) => {
|
||||||
buffer.end_session(self.cursor);
|
|
||||||
self.buffer = new_buffer;
|
self.buffer = new_buffer;
|
||||||
let Some(buffer) = state.buffers.get_mut(self.buffer) else {
|
let Some(buffer) = state.buffers.get_mut(self.buffer) else {
|
||||||
return Err(event);
|
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))
|
Ok(Resp::handled(None))
|
||||||
}
|
}
|
||||||
Some(Action::Char(c)) => {
|
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))
|
Ok(Resp::handled(None))
|
||||||
}
|
}
|
||||||
_ => Err(event),
|
_ => Err(event),
|
||||||
|
|
@ -58,19 +96,16 @@ impl Visual for Doc {
|
||||||
let Some(buffer) = state.buffers.get(self.buffer) else {
|
let Some(buffer) = state.buffers.get(self.buffer) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let Some(cursor) = buffer.cursors.get(self.cursor) else {
|
let Some(cursor) = buffer.cursors.get(self.cursors[&self.buffer]) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut n = 0;
|
// Set cursor position
|
||||||
for (i, line) in buffer.chars.split(|c| *c == '\n').enumerate() {
|
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);
|
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 => {
|
None => {
|
||||||
break match self.panes.handle(state, event) {
|
break match self.panes.handle(state, event) {
|
||||||
Ok(resp) => resp.action,
|
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