From 6e1e8f919ee77ae89e3fc3ec7ef3ce8008e5a5e1 Mon Sep 17 00:00:00 2001 From: C4 Patino Date: Sat, 11 Jan 2025 01:13:51 -0600 Subject: [PATCH] Added buffer insertion and deletion --- rustfmt.toml | 1 + src/editor/editor.rs | 166 +++++++++++++++------------------ src/editor/keybinds.rs | 13 --- src/editor/mod.rs | 6 +- src/macros.rs | 130 ++++++++++++++++++++++++++ src/main.rs | 13 ++- src/util/display.rs | 151 ++++++++++++++++++++++++++++++ src/{editor => util}/keymap.rs | 27 ++---- src/util/mod.rs | 6 +- 9 files changed, 378 insertions(+), 135 deletions(-) delete mode 100644 src/editor/keybinds.rs create mode 100644 src/macros.rs create mode 100644 src/util/display.rs rename src/{editor => util}/keymap.rs (83%) diff --git a/rustfmt.toml b/rustfmt.toml index 7530651..c775577 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1 +1,2 @@ max_width = 120 +use_small_heuristics = "Max" diff --git a/src/editor/editor.rs b/src/editor/editor.rs index 9b55995..1dd0d5f 100644 --- a/src/editor/editor.rs +++ b/src/editor/editor.rs @@ -1,21 +1,16 @@ use color_eyre::Report; -use crossterm::{ - cursor, - event::{poll, read, Event, KeyEvent}, - execute, queue, style, - terminal::{self, ClearType}, -}; +use crossterm::event::{poll, read, Event, KeyCode, KeyEvent, KeyModifiers}; use std::{ fs::File, - io::{self, BufRead, BufReader, Write}, + io::{BufRead, BufReader}, sync::mpsc, time::{Duration, Instant}, }; use tokio::runtime::Runtime; -use super::{default_keybinds, Keymap}; +use crate::util::{Display, Keymap}; -#[derive(Debug, Eq, PartialEq, Hash)] +#[derive(Eq, PartialEq, Hash)] pub enum Mode { NORMAL, COMMAND, @@ -23,54 +18,35 @@ pub enum Mode { VISUAL, } -#[derive(Debug)] pub struct Editor { pub(crate) buffer: Vec, + pub(crate) command: String, pub(crate) dirty: bool, pub(crate) stop: bool, pub(crate) mode: Mode, - pub(crate) size: (u16, u16), - pub(crate) cursor: (u16, u16), - pub(crate) offset: (u16, u16), + pub(crate) display: Display, pub(crate) keymap: Keymap, pub(crate) last_key_time: Instant, - - pub(crate) out: io::Stdout, -} - -impl Drop for Editor { - fn drop(&mut self) { - let _ = execute!(self.out, style::ResetColor, terminal::LeaveAlternateScreen); - } } impl Editor { pub fn new() -> Self { - let mut keymap = Keymap::new(); - default_keybinds(&mut keymap); - - let mut editor = Self { + Self { buffer: vec![String::new()], + command: String::new(), dirty: true, stop: false, mode: Mode::NORMAL, - size: terminal::size().unwrap(), - cursor: (0, 0), - offset: (0, 0), + display: Display::new(), - keymap, + keymap: Keymap::new(), last_key_time: Instant::now(), - - out: io::stdout(), - }; - - let _ = execute!(editor.out, terminal::EnterAlternateScreen); - editor + } } pub fn load_file(&mut self, filename: &str) { @@ -100,23 +76,22 @@ impl Editor { tokio::spawn(async move { Editor::key_event_listener(tx).await; }); + }); - while !self.stop { - self.handle_key_event(&mut rx).await?; - self.render().await?; + while !self.stop { + self.handle_key_event(&mut rx)?; + + if self.dirty { + self.display.render(&self.buffer, &self.command, &self.mode)?; + self.dirty = false; } + } - self.buffer.push("This is outside main loop".to_string()); - self.dirty = true; - self.render().await?; - - Ok(()) - }) + Ok(()) } - async fn handle_key_event(&mut self, rx: &mut mpsc::Receiver) -> Result<(), Report> { + fn handle_key_event(&mut self, rx: &mut mpsc::Receiver) -> Result<(), Report> { if self.last_key_time.elapsed().as_millis() > 1000 && !self.keymap.is_empty() { - self.buffer.push("TIMEOUT".to_string()); self.execute_keymap_action()?; self.dirty = true; } @@ -132,22 +107,69 @@ impl Editor { unresolved = self.keymap.traverse(&self.mode, event)?; } - if let Some(unresolved) = unresolved { - self.buffer.push(format!( - "{} {:?} {:?}", - unresolved.code, unresolved.modifiers, self.last_key_time - )); - } - if self.keymap.is_leaf() { self.execute_keymap_action()?; } + if let Some(unresolved) = unresolved { + if !unresolved.modifiers.intersects(KeyModifiers::ALT | KeyModifiers::CONTROL) { + self.handle_unresolved_key_event(unresolved); + } + } + self.last_key_time = Instant::now(); self.dirty = true; Ok(()) } + fn handle_unresolved_key_event(&mut self, unresolved: KeyEvent) { + match self.mode { + Mode::COMMAND => { + if let KeyCode::Char(c) = unresolved.code { + self.command.push(c); + } else if unresolved.code == KeyCode::Backspace { + self.command.pop(); + } + } + Mode::INSERT => match unresolved.code { + KeyCode::Char(c) => { + let (x, y) = self.display.cursor.position; + self.buffer[y as usize].insert(x as usize, c); + self.display.move_cursor((1, 0), &self.buffer); + } + KeyCode::Enter => { + let (x, y) = self.display.cursor.position; + let remaining = self.buffer[y as usize].split_off(x as usize); + self.buffer.insert((y + 1) as usize, remaining); + self.display.move_cursor((-(x as i16), 1), &self.buffer) + } + KeyCode::Delete => { + let (x, y) = self.display.cursor.position; + if x < self.buffer[y as usize].len() as u16 { + self.buffer[y as usize].remove(x as usize); + } else if y + 1 < self.buffer.len() as u16 { + let next_line = self.buffer.remove((y + 1) as usize); + self.buffer[y as usize].push_str(&next_line); + } + } + KeyCode::Backspace => { + let (x, y) = self.display.cursor.position; + if x > 0 { + self.buffer[y as usize].remove((x - 1) as usize); + self.display.move_cursor((-1, 0), &self.buffer); + } else if y > 0 { + let prev_line_len = self.buffer[(y - 1) as usize].len() as u16; + let current_line = self.buffer.remove(y as usize); + self.buffer[(y - 1) as usize].push_str(¤t_line); + self.display.move_cursor((prev_line_len as i16, -1), &self.buffer); + } + } + _ => {} + }, + _ => {} + } + } + fn execute_keymap_action(&mut self) -> Result<(), Report> { if let Some(action) = self.keymap.get_action() { action.borrow_mut()(self)?; @@ -157,44 +179,6 @@ impl Editor { Ok(()) } - async fn render(&mut self) -> Result<(), Report> { - if !self.dirty { - return Ok(()); - } - - queue!( - self.out, - style::ResetColor, - terminal::Clear(ClearType::All), - cursor::MoveTo(0, 0) - )?; - - let max_lines = self.size.1 as usize; - - let render = &self.buffer[self.offset.1 as usize..] - .iter() - .take(max_lines) - .collect::>(); - - for line in render { - queue!(self.out, style::Print(line), cursor::MoveToNextLine(1))?; - } - - let rendered_lines = render.len(); - if rendered_lines < max_lines { - let empty_lines = max_lines - rendered_lines; - for _ in 0..empty_lines { - queue!(self.out, style::Print("~"), cursor::MoveToNextLine(1))?; - } - } - - queue!(self.out, cursor::MoveTo(self.cursor.0, self.cursor.1))?; - self.out.flush()?; - - self.dirty = false; - Ok(()) - } - async fn key_event_listener(tx: mpsc::Sender) { loop { if !poll(Duration::from_millis(10)).unwrap() { diff --git a/src/editor/keybinds.rs b/src/editor/keybinds.rs deleted file mode 100644 index f5a9b43..0000000 --- a/src/editor/keybinds.rs +++ /dev/null @@ -1,13 +0,0 @@ -use super::{Keymap, Mode}; -use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; - -pub fn default_keybinds(keymap: &mut Keymap) { - keymap.add_keybind( - vec![Mode::NORMAL], - vec![ - KeyEvent::new(KeyCode::Char('w'), KeyModifiers::CONTROL), - KeyEvent::new(KeyCode::Char('q'), KeyModifiers::CONTROL), - ], - |editor| Ok(editor.stop = true), - ); -} diff --git a/src/editor/mod.rs b/src/editor/mod.rs index 1a66457..28fa1a8 100644 --- a/src/editor/mod.rs +++ b/src/editor/mod.rs @@ -1,9 +1,5 @@ mod editor; -mod keybinds; -mod keymap; -pub(crate) use self::editor::{ControlCode, Mode}; -pub(crate) use self::keybinds::default_keybinds; -pub(crate) use self::keymap::Keymap; +pub(crate) use self::editor::Mode; pub use self::editor::Editor; diff --git a/src/macros.rs b/src/macros.rs new file mode 100644 index 0000000..0aff3fe --- /dev/null +++ b/src/macros.rs @@ -0,0 +1,130 @@ +use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; + +use crate::editor::{Editor, Mode}; + +macro_rules! add_keybind { + ($editor:expr, $modes:expr, $sequence:expr, $action:expr) => { + $editor.keymap.add_keybind( + add_keybind!(@parse_modes $modes), + add_keybind!(@parse_keys $sequence), + $action, + ); + }; + + (@parse_keys $keyseq:expr) => {{ + let mut keys = Vec::new(); + let mut chars = $keyseq.chars().peekable(); + + while let Some(c) = chars.next() { + if c == '<' { + let mut key_name = String::new(); + while let Some(&peek) = chars.peek() { + if peek == '>' { + chars.next(); + break; + } + key_name.push(chars.next().unwrap()); + } + + let (modifiers, key) = if let Some(split_idx) = key_name.find('-') { + let (modifier, key) = key_name.split_at(split_idx); + (modifier, &key[1..].to_string()) + } else { + ("", &key_name) + }; + + let modifiers = match modifiers { + "C" => KeyModifiers::CONTROL, + "S" => KeyModifiers::SHIFT, + "A" => KeyModifiers::ALT, + _ => KeyModifiers::NONE, + }; + + let key_event = match key.as_str() { + "BS" => KeyEvent::new(KeyCode::Backspace, modifiers), + "Tab" => KeyEvent::new(KeyCode::Tab, modifiers), + "CR" | "Enter" | "Return" => KeyEvent::new(KeyCode::Enter, modifiers), + "Esc" => KeyEvent::new(KeyCode::Esc, modifiers), + "Space" => KeyEvent::new(KeyCode::Char(' '), modifiers), + "Up" => KeyEvent::new(KeyCode::Up, modifiers), + "Down" => KeyEvent::new(KeyCode::Down, modifiers), + "Left" => KeyEvent::new(KeyCode::Left, modifiers), + "Right" => KeyEvent::new(KeyCode::Right, modifiers), + "Insert" => KeyEvent::new(KeyCode::Insert, modifiers), + "Del" => KeyEvent::new(KeyCode::Delete, modifiers), + "Home" => KeyEvent::new(KeyCode::Home, modifiers), + "End" => KeyEvent::new(KeyCode::End, modifiers), + "PageUp" => KeyEvent::new(KeyCode::PageUp, modifiers), + "PageDown" => KeyEvent::new(KeyCode::PageDown, modifiers), + key if key.starts_with('F') && key[1..].parse::().is_ok() => { + let f_num = key[1..].parse::().unwrap(); + KeyEvent::new(KeyCode::F(f_num), modifiers) + } + key if key.len() == 1 => { + KeyEvent::new(KeyCode::Char(key.chars().next().unwrap()), modifiers) + } + _ => panic!("Unknown key: {}", key), + }; + + keys.push(key_event); + } else { + keys.push(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE)); + } + } + + keys + }}; + + (@parse_modes $modes:expr) => {{ + $modes + .chars() + .filter_map(|c| match c { + 'n' => Some(Mode::NORMAL), + 'v' => Some(Mode::VISUAL), + 'c' => Some(Mode::COMMAND), + 'i' => Some(Mode::INSERT), + _ => None, + }) + .collect::>() + }}; +} + +pub fn default_keybinds(editor: &mut Editor) { + add_keybind!(editor, "ni", "", |e| Ok(e.stop = true)); + + add_keybind!(editor, "n", "k", |e| { + e.display.move_cursor((0, -1), &e.buffer); + Ok(()) + }); + + add_keybind!(editor, "n", "j", |e| { + e.display.move_cursor((0, 1), &e.buffer); + Ok(()) + }); + + add_keybind!(editor, "n", "h", |e| { + e.display.move_cursor((-1, 0), &e.buffer); + Ok(()) + }); + + add_keybind!(editor, "n", "l", |e| { + e.display.move_cursor((1, 0), &e.buffer); + Ok(()) + }); + + add_keybind!(editor, "n", "i", |e| { + e.mode = Mode::INSERT; + Ok(()) + }); + + add_keybind!(editor, "n", ":", |e| { + e.mode = Mode::COMMAND; + Ok(()) + }); + + add_keybind!(editor, "ic", "", |e| { + e.mode = Mode::NORMAL; + e.command.clear(); + Ok(()) + }); +} diff --git a/src/main.rs b/src/main.rs index a56b084..d447f33 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,13 +1,16 @@ mod editor; +mod util; -use std::time::Instant; +#[macro_use] +mod macros; use clap::Parser; use color_eyre::Report; use crossterm::terminal; +use tracing_subscriber::EnvFilter; use editor::Editor; -use tracing_subscriber::EnvFilter; +use macros::default_keybinds; struct RawModeGuard; impl Drop for RawModeGuard { @@ -25,9 +28,7 @@ fn setup() -> Result<(), Report> { if std::env::var("RUST_LOG").is_err() { std::env::set_var("RUST_LOG", "info") } - tracing_subscriber::fmt::fmt() - .with_env_filter(EnvFilter::from_default_env()) - .init(); + tracing_subscriber::fmt::fmt().with_env_filter(EnvFilter::from_default_env()).init(); Ok(()) } @@ -49,6 +50,8 @@ fn main() -> Result<(), Report> { let _raw_mode_guard = RawModeGuard; let mut editor = Editor::new(); + default_keybinds(&mut editor); + if let Some(filename) = &args.filename { editor.load_file(filename); }; diff --git a/src/util/display.rs b/src/util/display.rs new file mode 100644 index 0000000..d8fa0a5 --- /dev/null +++ b/src/util/display.rs @@ -0,0 +1,151 @@ +use color_eyre::Report; +use crossterm::{ + cursor::{self, SetCursorStyle}, + execute, queue, + style::{self, Stylize}, + terminal::{self, ClearType}, +}; +use std::io::{self, Write}; + +use crate::editor::Mode; + +pub struct Display { + size: (u16, u16), + offset: (u16, u16), + + pub(crate) cursor: Cursor, + + pub(crate) out: io::Stdout, +} + +pub struct Cursor { + pub(crate) position: (u16, u16), + pub(crate) max_column: u16, +} + +impl Cursor { + pub fn new() -> Self { + Self { position: (0, 0), max_column: 0 } + } + + fn move_by(&mut self, delta: (i16, i16), buffer: &Vec) { + let saturate = |pos: u16, delta: i16| { + if delta.is_negative() { + pos.saturating_sub(delta.abs() as u16) + } else { + pos.saturating_add(delta as u16) + } + }; + + let (mut x, mut y) = self.position; + let (dx, dy) = delta; + + if dx != 0 { + x = saturate(x, dx); + self.max_column = x; + } + + if dy != 0 { + y = saturate(y, dy); + } + + self.position = (x, y); + self.validate_cursor(buffer); + } + + fn move_x(&mut self, new_x: u16) { + self.position.0 = new_x; + } + + fn move_y(&mut self, new_y: u16) { + self.position.1 = new_y; + } + + fn validate_cursor(&mut self, buffer: &Vec) { + let (_x, y) = self.position; + + if y >= buffer.len() as u16 { + self.move_y(buffer.len().saturating_sub(1) as u16); + } + + let line_len = buffer[self.position.1 as usize].len() as u16; + self.move_x(self.max_column.min(line_len)); + } +} + +impl Drop for Display { + fn drop(&mut self) { + let _ = execute!(self.out, style::ResetColor, terminal::LeaveAlternateScreen); + } +} + +impl Display { + pub fn new() -> Self { + let mut display = + Self { size: terminal::size().unwrap(), offset: (0, 0), cursor: Cursor::new(), out: io::stdout() }; + + let _ = execute!(display.out, terminal::EnterAlternateScreen); + + display + } + + pub fn move_cursor(&mut self, delta: (i16, i16), buffer: &Vec) { + self.cursor.move_by(delta, buffer); + } + + pub fn render(&mut self, buffer: &Vec, command: &String, mode: &Mode) -> Result<(), Report> { + queue!(self.out, style::ResetColor, terminal::Clear(ClearType::All), cursor::MoveTo(0, 0))?; + + let mut max_lines = self.size.1 as usize; + if !command.is_empty() { + max_lines -= 1; + } + + let max_columns = self.size.0 as usize; + + let render = buffer[self.offset.1 as usize..] + .iter() + .take(max_lines) + .map(|line| { + if line.len() > self.offset.0 as usize { + let start = self.offset.0 as usize; + let end = (start + max_columns).min(line.len()); + &line[start..end] + } else { + "" + } + }) + .collect::>(); + + for line in &render { + queue!(self.out, style::Print(line), cursor::MoveToNextLine(1))?; + } + + let rendered_lines = render.len(); + if rendered_lines < max_lines { + let empty_lines = max_lines - rendered_lines; + for _ in 0..empty_lines { + queue!(self.out, style::Print("~"), cursor::MoveToNextLine(1))?; + } + } + + if !command.is_empty() { + queue!( + self.out, + style::SetAttribute(style::Attribute::Bold), + style::Print(command), + style::SetAttribute(style::Attribute::Reset), + cursor::MoveToNextLine(1) + )?; + } + + match mode { + Mode::INSERT => queue!(self.out, cursor::SetCursorStyle::BlinkingBar)?, + _ => queue!(self.out, cursor::SetCursorStyle::DefaultUserShape)?, + } + + queue!(self.out, cursor::MoveTo(self.cursor.position.0, self.cursor.position.1))?; + self.out.flush()?; + Ok(()) + } +} diff --git a/src/editor/keymap.rs b/src/util/keymap.rs similarity index 83% rename from src/editor/keymap.rs rename to src/util/keymap.rs index 03e17a8..c046879 100644 --- a/src/editor/keymap.rs +++ b/src/util/keymap.rs @@ -1,8 +1,8 @@ use color_eyre::Report; use crossterm::event::KeyEvent; -use std::{cell::RefCell, collections::HashMap, fmt, io, rc::Rc}; +use std::{cell::RefCell, collections::HashMap, rc::Rc}; -use crate::editor::{ControlCode, Editor, Mode}; +use crate::editor::{Editor, Mode}; type ActionFn = dyn FnMut(&mut Editor) -> Result<(), Report>; @@ -12,18 +12,14 @@ struct KeyNode { action: Option>>, } -impl fmt::Debug for KeyNode { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "KeyNode {{ children: {:?} }}", self.children) - } +pub struct Keymap { + root: HashMap>>, + current: Option>>, } impl KeyNode { pub fn new() -> Rc> { - Rc::new(RefCell::new(Self { - children: HashMap::new(), - action: None, - })) + Rc::new(RefCell::new(Self { children: HashMap::new(), action: None })) } pub fn is_leaf(&self) -> bool { @@ -43,18 +39,9 @@ impl KeyNode { } } -#[derive(Debug)] -pub struct Keymap { - root: HashMap>>, - current: Option>>, -} - impl Keymap { pub fn new() -> Self { - Self { - root: HashMap::new(), - current: None, - } + Self { root: HashMap::new(), current: None } } pub fn add_keybind(&mut self, modes: Vec, sequence: Vec, action: F) diff --git a/src/util/mod.rs b/src/util/mod.rs index aae1fd1..313b1c5 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -1 +1,5 @@ -pub(crate) mod rope; +pub(crate) mod display; +pub(crate) mod keymap; + +pub(crate) use self::display::Display; +pub(crate) use self::keymap::Keymap;