diff --git a/src/actions.rs b/src/actions.rs index 7caf118..a03b47f 100644 --- a/src/actions.rs +++ b/src/actions.rs @@ -19,7 +19,10 @@ use std::collections::HashMap; -use crate::{Cursor, Direction, InsertState, Mode, State}; +use crate::{ + state::{InsertState, Mode, State}, + Cursor, Direction, +}; pub type Action = fn(&mut State); @@ -29,6 +32,12 @@ pub fn load_actions() -> HashMap { ("move_line_down", move_line_down), ("move_line_up", move_line_up), ("move_char_right", move_char_right), + ("move_next_word_start", move_next_word_start), + ("move_prev_word_start", move_prev_word_start), + ("move_next_word_end", move_next_word_end), + ("move_next_long_word_start", move_next_long_word_start), + ("move_prev_long_word_start", move_prev_long_word_start), + ("move_next_long_word_end", move_next_long_word_end), ("page_up", page_up), ("page_down", page_down), ("goto_line_start", goto_line_start), @@ -75,6 +84,30 @@ pub fn move_line_up(state: &mut State) { state.move_cursor(Direction::Up); } +pub fn move_next_word_start(state: &mut State) { + state.set_error("move_next_word_start is unimplemented"); +} + +pub fn move_prev_word_start(state: &mut State) { + state.set_error("move_prev_word_start is unimplemented"); +} + +pub fn move_next_word_end(state: &mut State) { + state.set_error("move_next_word_end is unimplemented"); +} + +pub fn move_next_long_word_start(state: &mut State) { + state.set_error("move_next_long_word_start is unimplemented"); +} + +pub fn move_prev_long_word_start(state: &mut State) { + state.set_error("move_prev_long_word_start is unimplemented"); +} + +pub fn move_next_long_word_end(state: &mut State) { + state.set_error("move_next_long_word_end is unimplemented"); +} + pub fn page_up(state: &mut State) { state.set_error("page_up is unimplemented"); } @@ -127,10 +160,12 @@ pub fn visual_mode(state: &mut State) { } pub fn insert_mode(state: &mut State) { + state.save_buffer(); state.mode = Mode::Insert(InsertState { append: false }); } pub fn append_mode(state: &mut State) { + state.save_buffer(); state.move_cursor(Direction::Right); state.mode = Mode::Insert(InsertState { append: true }); } @@ -146,24 +181,24 @@ pub fn insert_at_line_end(state: &mut State) { } pub fn open_below(state: &mut State) { + insert_mode(state); state.cursor.line += 1; state.cursor.column = 0; state.buffer.insert_char(state.cursor, '\n'); - state.mode = Mode::Insert(InsertState { append: false }); } pub fn open_above(state: &mut State) { + insert_mode(state); state.cursor.column = 0; state.buffer.insert_char(state.cursor, '\n'); - state.mode = Mode::Insert(InsertState { append: false }); } pub fn undo(state: &mut State) { - state.set_error("undo is unimplemented"); + state.undo(); } pub fn redo(state: &mut State) { - state.set_error("redo is unimplemented"); + state.redo(); } pub fn delete_char_backward(state: &mut State) { diff --git a/src/keybinds.rs b/src/keybinds.rs index c9f993b..69af716 100644 --- a/src/keybinds.rs +++ b/src/keybinds.rs @@ -64,6 +64,12 @@ impl Default for Keybinds { (Char('j'), move_line_down), (Char('k'), move_line_up), (Char('l'), move_char_right), + (Char('w'), move_next_word_start), + (Char('b'), move_prev_word_start), + (Char('e'), move_next_word_end), + (Char('W'), move_next_long_word_start), + (Char('B'), move_prev_long_word_start), + (Char('E'), move_next_long_word_end), (Char('i'), insert_mode), (Char('a'), append_mode), (Char('I'), insert_at_line_start), diff --git a/src/main.rs b/src/main.rs index 06860f7..f43d89c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,72 +20,28 @@ use std::{ env::args, ffi::OsString, - fs::{File, OpenOptions}, + fs::File, io::{Read, Stdout, Write}, os::fd::FromRawFd, - path::{Path, PathBuf}, - sync::Arc, + path::Path, }; -use crossbeam_channel::Sender; -use crossterm::{ - cursor, - event::{Event, KeyCode, KeyEvent}, - terminal, QueueableCommand, Result, -}; -use parking_lot::Mutex; -use ropey::Rope; +use crossterm::{terminal, QueueableCommand, Result}; use yacexits::{exit, EX_DATAERR, EX_UNAVAILABLE}; mod actions; mod buffer; mod config; mod keybinds; +mod state; mod theme; -use buffer::Buffer; -use config::Config; -use keybinds::{Key, Keybind}; -use theme::StyleStore; - #[derive(Copy, Clone, Debug, Default)] pub struct Cursor { pub column: usize, pub line: usize, } -#[derive(Clone, Debug, Default)] -pub struct CommandState { - pub buf: String, - pub cursor: usize, -} - -#[derive(Copy, Clone, Debug)] -pub struct InsertState { - append: bool, -} - -#[derive(Clone, Debug, Default)] -pub enum Mode { - #[default] - Normal, - Command(CommandState), - Visual, - Insert(InsertState), -} - -impl Mode { - pub fn cursor_style(&self) -> cursor::SetCursorStyle { - use cursor::SetCursorStyle as Style; - match self { - Mode::Normal => Style::SteadyBlock, - Mode::Visual => Style::BlinkingBlock, - Mode::Insert(_) => Style::BlinkingBar, - Mode::Command(_) => Style::SteadyUnderScore, - } - } -} - #[derive(Copy, Clone, Debug)] pub enum Direction { Left, @@ -94,317 +50,7 @@ pub enum Direction { Right, } -pub struct State { - pub styles: Arc>, - pub config: Config, - pub buffer: Buffer, - pub cursor: Cursor, - pub file: Option, - pub mode: Mode, - pub submode: Option, - pub last_saved: Rope, - pub scroll: Cursor, - pub size: (usize, usize), - pub quit: bool, - pub error: Option, - pub theme_tx: Sender, -} - -impl State { - pub fn from_str(file_name: Option, text: &str) -> Result { - let styles = Arc::new(Mutex::new(StyleStore::default())); - let buffer = Buffer::from_str(styles.clone(), text); - let last_saved = buffer.as_ref().clone(); - let (cols, rows) = terminal::size()?; - let theme_tx = config::ThemeWatcher::spawn(styles.clone()); - - Ok(Self { - styles, - config: Default::default(), - buffer, - cursor: Cursor::default(), - file: file_name, - mode: Mode::default(), - submode: None, - last_saved, - scroll: Cursor::default(), - size: (cols as usize, rows as usize), - quit: false, - error: None, - theme_tx, - }) - } - - pub fn draw(&mut self, out: &mut impl Write) -> Result<()> { - // begin update - let (cols, rows) = terminal::size()?; - out.queue(terminal::BeginSynchronizedUpdate)?; - out.queue(terminal::Clear(terminal::ClearType::All))?; - let mut styles = self.styles.lock(); - - // draw status line - let mut set_cursor_pos = None; - let mut show_status_bar = false; - - if let Mode::Command(CommandState { buf, cursor }) = &self.mode { - let col = *cursor as u16 + 1; - let row = rows - 1; - out.queue(cursor::MoveTo(0, row))?; - write!(out, ":{}", buf)?; - set_cursor_pos = Some((col, row)); - show_status_bar = true; - } else if let Some(error) = self.error.as_ref() { - let error_style = styles.get_scope("error"); - out.queue(cursor::MoveTo(0, rows - 1))?; - error_style.print_styled(out, error)?; - show_status_bar = true; - } - - // done with styles - drop(styles); - - // draw buffer - let buffer_rows = if show_status_bar { rows - 1 } else { rows }; - let lr_width = self - .buffer - .draw(cols, buffer_rows, self.scroll, self.cursor, out)?; - - // draw submode - if let Some(submode) = self.submode { - out.queue(cursor::MoveTo(cols - 10, rows - 1))?; - write!(out, "{:?}", submode)?; - } - - // draw cursor - let cursor_pos = set_cursor_pos.unwrap_or_else(|| { - // calculate cursor position on buffer - let cursor = self.buffer.clamped_cursor(self.cursor); - let col = cursor.column.saturating_sub(self.scroll.column) as u16; - let row = cursor.line.saturating_sub(self.scroll.line) as u16; - let col = col + lr_width as u16; - (col, row) - }); - - out.queue(cursor::MoveTo(cursor_pos.0, cursor_pos.1))?; - out.queue(self.mode.cursor_style())?; - - // finish update - out.queue(terminal::EndSynchronizedUpdate)?; - out.flush()?; - - Ok(()) - } - - /// Sets the current error message. - pub fn set_error(&mut self, error: impl ToString) { - self.error = Some(error.to_string()); - } - - pub fn on_event(&mut self, event: Event) { - // reset the error from the last event - self.error = None; - - match event { - Event::Resize(cols, rows) => { - self.size = (cols as usize, rows as usize); - } - Event::Key(KeyEvent { code, .. }) => match &self.mode { - Mode::Normal | Mode::Visual => { - self.on_key(code); - } - Mode::Command(state) => self.on_command_key(code, state.clone()), - Mode::Insert(_) => { - if !self.on_key(code) { - if let KeyCode::Char(c) = code { - self.buffer.insert_char(self.cursor, c); - self.move_cursor(Direction::Right); - } - } - } - }, - _ => {} - } - } - - fn on_command_key(&mut self, code: KeyCode, mut state: CommandState) { - match code { - KeyCode::Char(c) => { - state.buf.insert(state.cursor, c); - state.cursor += 1; - } - KeyCode::Backspace => { - if state.cursor > 0 { - state.cursor -= 1; - state.buf.remove(state.cursor); - } - } - KeyCode::Delete if state.cursor < state.buf.len() => { - state.buf.remove(state.cursor); - } - KeyCode::Left if state.cursor > 0 => { - state.cursor -= 1; - } - KeyCode::Right if state.cursor < state.buf.len() => { - state.cursor += 1; - } - KeyCode::Enter => { - // TODO add to command history - let _ = self - .execute_command(&state.buf) - .map_err(|err| self.set_error(err)); - self.mode = Mode::Normal; - return; - } - KeyCode::Esc => { - self.mode = Mode::default(); - return; - } - _ => {} - } - - self.mode = Mode::Command(state); - } - - /// Processes a key press event. - /// - /// Returns `true` if the key was handled by a keybind, `false` otherwise. - fn on_key(&mut self, key: Key) -> bool { - let keybinds = match &self.mode { - Mode::Normal => &self.config.keybinds.normal, - Mode::Insert(_) => &self.config.keybinds.insert, - Mode::Visual => &self.config.keybinds.visual, - Mode::Command(_) => return false, // command mode is handled in [on_command_event] - }; - - if let Some(submode) = self.submode { - // if we're currently in a submode, try to run this submode's keybind - keybinds - .submodes - .get(&submode) - .and_then(|submode| submode.get(&key)) - .cloned() - .map(|keybind| self.execute_keybind(keybind)); - // whether or not there is a keybind we exit the submode - self.submode = None; - } else if keybinds.submodes.contains_key(&key) { - // enter a submode if available - self.submode = Some(key); - } else if let Some(keybind) = keybinds.map.get(&key) { - // run the keybind if available - self.execute_keybind(keybind.clone()); - } else { - // key is not bound; don't handle - return false; - } - - true - } - - fn write_buffer(&mut self, file: OsString) -> Result<()> { - self.last_saved = self.buffer.as_ref().clone(); - let out = self.buffer.as_ref().bytes().collect::>(); - let mut handle = OpenOptions::new() - .create(true) - .truncate(true) - .write(true) - .open(file)?; - - handle.write_all(out.as_slice())?; - Ok(()) - } - - fn write_command(&mut self, command: &str, args: &[&str]) -> std::result::Result<(), String> { - let handle = match self.file.clone() { - Some(handle) => handle, - None => match args.get(0) { - Some(part) => { - let file = OsString::from(part); - self.file = Some(file.clone()); - file - } - None => { - return Err(format!("{}: No file name.", command)); - } - }, - }; - - self.write_buffer(handle).map_err(|err| format!("{}", err)) - } - - fn execute_keybind(&mut self, keybind: Keybind) { - match keybind { - Keybind::Action(action) => action(self), - Keybind::Command(command) => { - let _ = self - .execute_command(&command) - .map_err(|err| self.set_error(err)); - self.mode = Mode::Normal; - return; - } - } - } - - fn execute_command(&mut self, command: &str) -> std::result::Result<(), String> { - let command_parts = command.split(' ').collect::>(); - - let command = match command_parts.get(0) { - Some(command) => command, - None => return Ok(()), - }; - - let args = &command_parts[1..]; - - match *command { - "q!" => self.quit = true, - "q" => { - if self.last_saved == *self.buffer.as_ref() { - self.quit = true; - } else { - return Err("Buffer is unsaved (add ! to override)".into()); - } - } - "w" => return self.write_command(command, args), - "wq" => { - self.write_command(command, args)?; - self.quit = true; - } - command => match command.parse::() { - Err(_) => return Err(format!("{}: Unrecognized command.", command)), - Ok(line) if line >= self.buffer.as_ref().len_lines() => { - return Err(format!("Line {} is out-of-bounds", line)) - } - Ok(line) => { - self.cursor.line = line; - self.scroll_to_cursor(); - } - }, - } - - Ok(()) - } - - fn move_cursor(&mut self, direction: Direction) { - let wrap = self.config.move_linewrap; - self.buffer.move_cursor(&mut self.cursor, direction, wrap); - self.scroll_to_cursor(); - } - - fn scroll_to_cursor(&mut self) { - if self.cursor.column < self.scroll.column + 3 { - self.scroll.column = self.cursor.column.saturating_sub(3); - } else if self.cursor.column + 6 >= self.scroll.column + self.size.0 { - self.scroll.column = self.cursor.column.saturating_sub(self.size.0 - 6); - } - - if self.cursor.line < self.scroll.line { - self.scroll.line = self.cursor.line; - } else if self.cursor.line + 3 >= self.scroll.line + self.size.1 { - self.scroll.line = self.cursor.line + 3 - self.size.1; - } - } -} - -fn screen_main(stdout: &mut Stdout, mut state: State) -> Result<()> { +fn screen_main(stdout: &mut Stdout, mut state: state::State) -> Result<()> { let poll_timeout = std::time::Duration::from_millis(100); while !state.quit { state.draw(stdout)?; @@ -458,7 +104,7 @@ fn main() -> Result<()> { exit(EX_DATAERR); }); - let state = State::from_str(file_name, &text)?; + let state = state::State::from_str(file_name, &text)?; // begin to enter alternate screen let mut stdout = std::io::stdout(); diff --git a/src/state.rs b/src/state.rs new file mode 100644 index 0000000..f659b55 --- /dev/null +++ b/src/state.rs @@ -0,0 +1,417 @@ +/* + * Copyright (c) 2023 Marceline Cramer + * SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under + * the terms of the GNU Affero General Public License as published by the Free + * Software Foundation, either version 3 of the License, or (at your option) any + * later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more + * details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +use std::{ffi::OsString, fs::OpenOptions, io::Write, path::PathBuf, sync::Arc}; + +use crossbeam_channel::Sender; +use crossterm::{ + cursor, + event::{Event, KeyCode, KeyEvent}, + terminal, QueueableCommand, Result, +}; +use parking_lot::Mutex; +use ropey::Rope; + +use crate::{ + buffer::Buffer, + config::{Config, ThemeWatcher}, + keybinds::{Key, Keybind}, + theme::StyleStore, + Cursor, Direction, +}; + +#[derive(Clone, Debug, Default)] +pub struct CommandState { + pub buf: String, + pub cursor: usize, +} + +#[derive(Copy, Clone, Debug)] +pub struct InsertState { + pub append: bool, +} + +#[derive(Clone, Debug, Default)] +pub enum Mode { + #[default] + Normal, + Command(CommandState), + Visual, + Insert(InsertState), +} + +impl Mode { + pub fn cursor_style(&self) -> cursor::SetCursorStyle { + use cursor::SetCursorStyle as Style; + match self { + Mode::Normal => Style::SteadyBlock, + Mode::Visual => Style::BlinkingBlock, + Mode::Insert(_) => Style::BlinkingBar, + Mode::Command(_) => Style::SteadyUnderScore, + } + } +} + +pub struct State { + pub styles: Arc>, + pub config: Config, + pub buffer: Buffer, + pub undo_buffers: Vec, + pub redo_buffers: Vec, + pub cursor: Cursor, + pub file: Option, + pub mode: Mode, + pub submode: Option, + pub last_saved: Rope, + pub scroll: Cursor, + pub size: (usize, usize), + pub quit: bool, + pub error: Option, + pub theme_tx: Sender, +} + +impl State { + pub fn from_str(file_name: Option, text: &str) -> Result { + let styles = Arc::new(Mutex::new(StyleStore::default())); + let buffer = Buffer::from_str(styles.clone(), text); + let last_saved = buffer.as_ref().clone(); + let (cols, rows) = terminal::size()?; + let theme_tx = ThemeWatcher::spawn(styles.clone()); + + Ok(Self { + styles, + config: Default::default(), + buffer, + undo_buffers: Vec::new(), + redo_buffers: Vec::new(), + cursor: Cursor::default(), + file: file_name, + mode: Mode::default(), + submode: None, + last_saved, + scroll: Cursor::default(), + size: (cols as usize, rows as usize), + quit: false, + error: None, + theme_tx, + }) + } + + pub fn draw(&mut self, out: &mut impl Write) -> Result<()> { + // begin update + let (cols, rows) = terminal::size()?; + out.queue(terminal::BeginSynchronizedUpdate)?; + out.queue(terminal::Clear(terminal::ClearType::All))?; + let mut styles = self.styles.lock(); + + // draw status line + let mut set_cursor_pos = None; + let mut show_status_bar = false; + + if let Mode::Command(CommandState { buf, cursor }) = &self.mode { + let col = *cursor as u16 + 1; + let row = rows - 1; + out.queue(cursor::MoveTo(0, row))?; + write!(out, ":{}", buf)?; + set_cursor_pos = Some((col, row)); + show_status_bar = true; + } else if let Some(error) = self.error.as_ref() { + let error_style = styles.get_scope("error"); + out.queue(cursor::MoveTo(0, rows - 1))?; + error_style.print_styled(out, error)?; + show_status_bar = true; + } + + // done with styles + drop(styles); + + // draw buffer + let buffer_rows = if show_status_bar { rows - 1 } else { rows }; + let lr_width = self + .buffer + .draw(cols, buffer_rows, self.scroll, self.cursor, out)?; + + // draw submode + if let Some(submode) = self.submode { + out.queue(cursor::MoveTo(cols - 10, rows - 1))?; + write!(out, "{:?}", submode)?; + } + + // draw cursor + let cursor_pos = set_cursor_pos.unwrap_or_else(|| { + // calculate cursor position on buffer + let cursor = self.buffer.clamped_cursor(self.cursor); + let col = cursor.column.saturating_sub(self.scroll.column) as u16; + let row = cursor.line.saturating_sub(self.scroll.line) as u16; + let col = col + lr_width as u16; + (col, row) + }); + + out.queue(cursor::MoveTo(cursor_pos.0, cursor_pos.1))?; + out.queue(self.mode.cursor_style())?; + + // finish update + out.queue(terminal::EndSynchronizedUpdate)?; + out.flush()?; + + Ok(()) + } + + /// Sets the current error message. + pub fn set_error(&mut self, error: impl ToString) { + self.error = Some(error.to_string()); + } + + pub fn on_event(&mut self, event: Event) { + // reset the error from the last event + self.error = None; + + match event { + Event::Resize(cols, rows) => { + self.size = (cols as usize, rows as usize); + } + Event::Key(KeyEvent { code, .. }) => match &self.mode { + Mode::Normal | Mode::Visual => { + self.on_key(code); + } + Mode::Command(state) => self.on_command_key(code, state.clone()), + Mode::Insert(_) => { + if !self.on_key(code) { + if let KeyCode::Char(c) = code { + self.buffer.insert_char(self.cursor, c); + self.move_cursor(Direction::Right); + } + } + } + }, + _ => {} + } + } + + fn on_command_key(&mut self, code: KeyCode, mut state: CommandState) { + match code { + KeyCode::Char(c) => { + state.buf.insert(state.cursor, c); + state.cursor += 1; + } + KeyCode::Backspace => { + if state.cursor > 0 { + state.cursor -= 1; + state.buf.remove(state.cursor); + } + } + KeyCode::Delete if state.cursor < state.buf.len() => { + state.buf.remove(state.cursor); + } + KeyCode::Left if state.cursor > 0 => { + state.cursor -= 1; + } + KeyCode::Right if state.cursor < state.buf.len() => { + state.cursor += 1; + } + KeyCode::Enter => { + // TODO add to command history + let _ = self + .execute_command(&state.buf) + .map_err(|err| self.set_error(err)); + self.mode = Mode::Normal; + return; + } + KeyCode::Esc => { + self.mode = Mode::default(); + return; + } + _ => {} + } + + self.mode = Mode::Command(state); + } + + /// Processes a key press event. + /// + /// Returns `true` if the key was handled by a keybind, `false` otherwise. + fn on_key(&mut self, key: Key) -> bool { + let keybinds = match &self.mode { + Mode::Normal => &self.config.keybinds.normal, + Mode::Insert(_) => &self.config.keybinds.insert, + Mode::Visual => &self.config.keybinds.visual, + Mode::Command(_) => return false, // command mode is handled in [on_command_event] + }; + + if let Some(submode) = self.submode { + // if we're currently in a submode, try to run this submode's keybind + keybinds + .submodes + .get(&submode) + .and_then(|submode| submode.get(&key)) + .cloned() + .map(|keybind| self.execute_keybind(keybind)); + // whether or not there is a keybind we exit the submode + self.submode = None; + } else if keybinds.submodes.contains_key(&key) { + // enter a submode if available + self.submode = Some(key); + } else if let Some(keybind) = keybinds.map.get(&key) { + // run the keybind if available + self.execute_keybind(keybind.clone()); + } else { + // key is not bound; don't handle + return false; + } + + true + } + + fn write_buffer(&mut self, file: OsString) -> Result<()> { + self.last_saved = self.buffer.as_ref().clone(); + let out = self.buffer.as_ref().bytes().collect::>(); + let mut handle = OpenOptions::new() + .create(true) + .truncate(true) + .write(true) + .open(file)?; + + handle.write_all(out.as_slice())?; + Ok(()) + } + + fn write_command(&mut self, command: &str, args: &[&str]) -> std::result::Result<(), String> { + let handle = match self.file.clone() { + Some(handle) => handle, + None => match args.get(0) { + Some(part) => { + let file = OsString::from(part); + self.file = Some(file.clone()); + file + } + None => { + return Err(format!("{}: No file name.", command)); + } + }, + }; + + self.write_buffer(handle).map_err(|err| format!("{}", err)) + } + + fn execute_keybind(&mut self, keybind: Keybind) { + match keybind { + Keybind::Action(action) => action(self), + Keybind::Command(command) => { + let _ = self + .execute_command(&command) + .map_err(|err| self.set_error(err)); + self.mode = Mode::Normal; + return; + } + } + } + + fn execute_command(&mut self, command: &str) -> std::result::Result<(), String> { + let command_parts = command.split(' ').collect::>(); + + let command = match command_parts.get(0) { + Some(command) => command, + None => return Ok(()), + }; + + let args = &command_parts[1..]; + + match *command { + "q!" => self.quit = true, + "q" => { + if self.last_saved == *self.buffer.as_ref() { + self.quit = true; + } else { + return Err("Buffer is unsaved (add ! to override)".into()); + } + } + "w" => return self.write_command(command, args), + "wq" => { + self.write_command(command, args)?; + self.quit = true; + } + command => match command.parse::() { + Err(_) => return Err(format!("{}: Unrecognized command.", command)), + Ok(line) if line >= self.buffer.as_ref().len_lines() => { + return Err(format!("Line {} is out-of-bounds", line)) + } + Ok(line) => { + self.cursor.line = line; + self.scroll_to_cursor(); + } + }, + } + + Ok(()) + } + + pub fn move_cursor(&mut self, direction: Direction) { + let wrap = self.config.move_linewrap; + self.buffer.move_cursor(&mut self.cursor, direction, wrap); + self.scroll_to_cursor(); + } + + pub fn scroll_to_cursor(&mut self) { + if self.cursor.column < self.scroll.column + 3 { + self.scroll.column = self.cursor.column.saturating_sub(3); + } else if self.cursor.column + 6 >= self.scroll.column + self.size.0 { + self.scroll.column = self.cursor.column.saturating_sub(self.size.0 - 6); + } + + if self.cursor.line < self.scroll.line { + self.scroll.line = self.cursor.line; + } else if self.cursor.line + 3 >= self.scroll.line + self.size.1 { + self.scroll.line = self.cursor.line + 3 - self.size.1; + } + } + + /// If modified, saves the current buffer in undo history and clears the redo history. + pub fn save_buffer(&mut self) { + /*if let Some(last) = self.undo_buffers.last() { + if *last.as_ref() == *self.buffer.as_ref() { + return; + } + }*/ + + let current = self.buffer.clone(); + self.undo_buffers.push(current); + self.redo_buffers.clear(); + } + + /// Pops the last undo state from the history and pushes the current state to the redo buffers. + pub fn undo(&mut self) { + match self.undo_buffers.pop() { + None => self.set_error("Already at oldest change"), + Some(last) => { + let current = std::mem::replace(&mut self.buffer, last); + self.redo_buffers.push(current); + } + } + } + + /// Pops the next redo state from the history and pushes the current state to the undo buffers. + pub fn redo(&mut self) { + match self.redo_buffers.pop() { + None => self.set_error("Already at newest change"), + Some(next) => { + let current = std::mem::replace(&mut self.buffer, next); + self.undo_buffers.push(current); + } + } + } +}