diff --git a/src/app/app.rs b/src/app/app.rs index 670cba7..9c5fd46 100644 --- a/src/app/app.rs +++ b/src/app/app.rs @@ -18,7 +18,10 @@ use ratatui::{ use crate::app::{ clipboard::Clipboard, error_msg::StatusMessage, - logic::{calc::{Grid, get_header_size}, cell::CellType}, + logic::{ + calc::{Grid, get_header_size}, + cell::CellType, + }, mode::Mode, screen::ScreenSpace, }; @@ -42,7 +45,7 @@ impl Widget for &App { let (x_max, y_max) = self.screen.how_many_cells_fit_in(&area, &self.vars); let is_selected = |x: usize, y: usize| -> bool { - if let Mode::Visual((mut x1, mut y1)) | Mode::VisualCmd((mut x1, mut y1), _)= self.mode { + if let Mode::Visual((mut x1, mut y1)) | Mode::VisualCmd((mut x1, mut y1), _) = self.mode { let (mut x2, mut y2) = self.grid.cursor(); x1 += 1; y1 += 1; @@ -78,7 +81,7 @@ impl Widget for &App { let mut x_idx: usize = 0; let mut y_idx: usize = 0; if x != 0 { - x_idx = x as usize -1 + self.screen.scroll_x(); + x_idx = x as usize - 1 + self.screen.scroll_x(); } if y != 0 { y_idx = y as usize - 1 + self.screen.scroll_y(); @@ -93,9 +96,9 @@ impl Widget for &App { /// Center the text "99 " -> " 99 " fn center_text(text: &str, avaliable_space: i32) -> String { let margin = avaliable_space - text.len() as i32; - let margin = margin/2; - let l_margin = (0..margin).into_iter().map(|_| ' ').collect::(); - let r_margin = (0..(margin-(l_margin.len() as i32))).into_iter().map(|_| ' ').collect::(); + let margin = margin / 2; + let l_margin = (0..margin).into_iter().map(|_| ' ').collect::(); + let r_margin = (0..(margin - (l_margin.len() as i32))).into_iter().map(|_| ' ').collect::(); format!("{l_margin}{text}{r_margin}") } @@ -214,7 +217,7 @@ impl Widget for &App { } // If this is the row header column - let area = if x==0 && y != 0 { + let area = if x == 0 && y != 0 { Rect::new(x_off, y_off, row_header_width, cell_height) } else if let Some(suggestion) = suggest_upper_bound { let max_available_width = area.width - x_off; @@ -387,17 +390,20 @@ impl App { event::KeyCode::Enter => { let v = editor.as_string(); - // try to insert as a float - if let Ok(v) = v.parse::() { - self.grid.set_cell_raw(self.grid.cursor(), Some(v)); - } else { - // if you can't, then insert as a string - if !v.is_empty() { - self.grid.set_cell_raw(self.grid.cursor(), Some(v)); + let cursor = self.grid.cursor(); + self.grid.transact_on_grid(|grid| { + // try to insert as a float + if let Ok(v) = v.parse::() { + grid.set_cell_raw(cursor, Some(v)); } else { - self.grid.set_cell_raw::(self.grid.cursor(), None); + // if you can't, then insert as a string + if !v.is_empty() { + grid.set_cell_raw(cursor, Some(v.to_owned())); + } else { + grid.set_cell_raw::(cursor, None); + } } - } + }); self.mode = Mode::Normal; } @@ -413,10 +419,23 @@ impl App { }, Mode::Normal => match event::read()? { event::Event::Key(key_event) => match key_event.code { - event::KeyCode::F(_) => todo!(), - event::KeyCode::Char(c) => { - Mode::process_key(self, c); - } + event::KeyCode::F(n) => {}, + event::KeyCode::Char(c) => Mode::process_key(self, c), + // Pretend that the arrow keys are vim movement keys + event::KeyCode::Left => Mode::process_key(self, 'h'), + event::KeyCode::Right => Mode::process_key(self, 'l'), + event::KeyCode::Up => Mode::process_key(self, 'k'), + event::KeyCode::Down => Mode::process_key(self, 'j'), + // Getting ctrl to work isn't going will right now. Use page keys for the time being. + event::KeyCode::PageUp => self.grid.redo(), + event::KeyCode::PageDown => self.grid.undo(), + event::KeyCode::Modifier(modifier_key_code) => { + if let event::ModifierKeyCode::LeftControl | event::ModifierKeyCode::RightControl = modifier_key_code { + // TODO my terminal (alacritty) isn't showing me ctrl presses. I know + // that they work tho, since ctrl+r works here in neovim. + // panic!("heard ctrl"); + } + }, _ => {} }, _ => {} diff --git a/src/app/clipboard.rs b/src/app/clipboard.rs index 777ec40..adb33e6 100644 --- a/src/app/clipboard.rs +++ b/src/app/clipboard.rs @@ -18,12 +18,7 @@ pub struct Clipboard { impl Clipboard { pub fn new() -> Self { - Self { - clipboard: Vec::new(), - last_paste_cell: (0, 0), - momentum: (0, 1), - source_cell: (0, 0), - } + Self { clipboard: Vec::new(), last_paste_cell: (0, 0), momentum: (0, 1), source_cell: (0, 0) } } /// Panics if clipboard is 0 length (if you call after you @@ -49,25 +44,28 @@ impl Clipboard { // cursor let (cx, cy) = into.cursor(); - // iterate thru the clipbaord's cells - for (x, row) in self.clipboard.iter().enumerate() { - for (y, cell) in row.iter().enumerate() { - let idx = (x + cx, y + cy); + let cursor = into.cursor(); + into.transact_on_grid(|grid| { + // iterate thru the clipbaord's cells + for (x, row) in self.clipboard.iter().enumerate() { + for (y, cell) in row.iter().enumerate() { + let idx = (x + cx, y + cy); - if translate { - if let Some(cell) = cell { - let trans = cell.translate_cell(self.source_cell, into.cursor()); - into.set_cell_raw(idx, Some(trans)); + if translate { + if let Some(cell) = cell { + let trans = cell.translate_cell(self.source_cell, cursor); + grid.set_cell_raw(idx, Some(trans)); + } else { + // The cell at this location doesn't exist (empty) + grid.set_cell_raw::(idx, None); + } } else { - // The cell at this location doesn't exist (empty) - into.set_cell_raw::(idx, None); + // translate = false + grid.set_cell_raw::(idx, cell.clone()); } - } else { - // translate = false - into.set_cell_raw::(idx, cell.clone()); } } - } + }); let (lx, ly) = self.last_paste_cell; self.momentum = (cx as i32 - lx as i32, cy as i32 - ly as i32); @@ -109,16 +107,19 @@ impl Clipboard { // size the clipboard appropriately self.clipboard.clear(); - // clone data into clipboard - for x in low_x..=hi_x { - let mut col = Vec::new(); - for y in low_y..=hi_y { - let a = from.get_cell_raw(x, y); - col.push(a.clone()); - from.set_cell_raw::((x, y), None); + + from.transact_on_grid(|grid| { + // clone data into clipboard + for x in low_x..=hi_x { + let mut col = Vec::new(); + for y in low_y..=hi_y { + let a = grid.get_cell_raw(x, y); + col.push(a.clone()); + grid.set_cell_raw::((x, y), None); + } + self.clipboard.push(col); } - self.clipboard.push(col); - } + }); self.last_paste_cell = (low_x, low_y); } } @@ -394,5 +395,4 @@ fn copy_paste_range_in_function() { let a = app.grid.get_cell("B1").as_ref().expect("Should've been set by paste"); assert_eq!(a.to_string(), "=sum(A:A)"); - } diff --git a/src/app/logic/calc.rs b/src/app/logic/calc.rs index b3bca0d..fdca585 100644 --- a/src/app/logic/calc.rs +++ b/src/app/logic/calc.rs @@ -2,20 +2,21 @@ use std::{ cmp::{max, min}, fs::{self, File}, io::{Read, Write}, - path::PathBuf + path::PathBuf, }; use evalexpr::*; -use crate::app::{ - logic::{ - cell::{CSV_DELIMITER, CellType}, - ctx, - }, mode::Mode, +use crate::app::logic::{ + calc::internal::CellGrid, + cell::{CSV_DELIMITER, CellType}, + ctx, }; #[cfg(test)] use crate::app::app::App; +#[cfg(test)] +use crate::app::mode::Mode; pub fn get_header_size() -> usize { let row_header_width = LEN.to_string().len(); @@ -26,13 +27,109 @@ pub const LEN: usize = 1001; pub const CSV_EXT: &str = "csv"; pub const CUSTOM_EXT: &str = "nscim"; +mod internal { + use crate::app::logic::{calc::LEN, cell::CellType}; + + #[derive(Clone)] + pub struct CellGrid { + // a b c ... + // 0 + // 1 + // 2 + // ... + cells: Vec>>, + } + + impl CellGrid { + pub fn new() -> Self { + let mut a = Vec::with_capacity(LEN); + for _ in 0..LEN { + let mut b = Vec::with_capacity(LEN); + for _ in 0..LEN { + b.push(None) + } + a.push(b) + } + Self { cells: a } + } + + pub fn insert_row(&mut self, y: usize) { + for x in 0..LEN { + self.cells[x].insert(y, None); + self.cells[x].pop(); + } + } + + pub fn insert_column(&mut self, x: usize) { + let mut v = Vec::with_capacity(LEN); + for _ in 0..LEN { + v.push(None); + } + // let clone = self.grid_history[self.current_grid].clone(); + self.cells.insert(x, v); + // keep the grid LEN + self.cells.pop(); + } + + pub fn get_cell_raw(&self, x: usize, y: usize) -> &Option { + if x >= LEN || y >= LEN { + return &None; + } + &self.cells[x][y] + } + pub fn set_cell_raw>(&mut self, (x, y): (usize, usize), val: Option) { + // TODO check oob + self.cells[x][y] = val.map(|v| v.into()); + } + /// Iterate over the entire grid and see where + /// the farthest modified cell is. + #[must_use] + pub fn max(&self) -> (usize, usize) { + let mut max_x = 0; + let mut max_y = 0; + + for (xi, x) in self.cells.iter().enumerate() { + for (yi, cell) in x.iter().enumerate() { + if cell.is_some() { + if yi > max_y { + max_y = yi + } + if xi > max_x { + max_x = xi + } + } + } + } + + (max_x, max_y) + } + + #[must_use] + pub fn max_y_at_x(&self, x: usize) -> usize { + let mut max_y = 0; + if let Some(col) = self.cells.get(x) { + // we could do fancy things like .take_while(not null) but then + // we would have to deal with empty cells and stuff, which sounds + // boring. This will be fast "enough", considering the grid is + // probably only like 1k cells + for (yi, cell) in col.iter().enumerate() { + if cell.is_some() { + if yi > max_y { + max_y = yi + } + } + } + } + max_y + } + } +} + pub struct Grid { - // a b c ... - // 0 - // 1 - // 2 - // ... - cells: Vec>>, + /// Which grid in history are we currently on + current_grid: usize, + /// An array of grids, thru history + grid_history: Vec, /// (X, Y) selected_cell: (usize, usize), /// Have unsaved modifications been made? @@ -47,20 +144,9 @@ impl std::fmt::Debug for Grid { impl Grid { pub fn new() -> Self { - let mut a = Vec::with_capacity(LEN); - for _ in 0..LEN { - let mut b = Vec::with_capacity(LEN); - for _ in 0..LEN { - b.push(None) - } - a.push(b) - } + let x = CellGrid::new(); - Self { - cells: a, - selected_cell: (0, 0), - dirty: false, - } + Self { current_grid: 0, grid_history: vec![x], selected_cell: (0, 0), dirty: false } } pub fn new_from_file(file: &mut File) -> std::io::Result { @@ -68,14 +154,17 @@ impl Grid { let mut buf = String::new(); file.read_to_string(&mut buf)?; - for (yi, line) in buf.lines().enumerate() { - let cells = Self::parse_csv_line(line); - for (xi, cell) in cells.into_iter().enumerate() { - // This gets automatically duck-typed - grid.set_cell_raw((xi, yi), cell); + grid.transact_on_grid(|grid| { + for (yi, line) in buf.lines().enumerate() { + let cells = Self::parse_csv_line(line); + + for (xi, cell) in cells.into_iter().enumerate() { + // This gets automatically duck-typed + grid.set_cell_raw((xi, yi), cell); + } } - } + }); // force dirty back off, we just read the data so it's gtg grid.dirty = false; @@ -112,19 +201,15 @@ impl Grid { } let mut f = fs::OpenOptions::new().write(true).append(false).truncate(true).create(true).open(path)?; - let (mx, my) = self.max(); + let (mx, my) = self.get_grid().max(); for y in 0..=my { for x in 0..=mx { - let cell = &self.cells[x][y]; + let cell = &self.get_grid().get_cell_raw(x, y); // newline after the cell, because it's end of line. // else, just put a comma after the cell. - let is_last = x==mx; - let delim = if is_last { - '\n' - } else { - CSV_DELIMITER - }; + let is_last = x == mx; + let delim = if is_last { '\n' } else { CSV_DELIMITER }; let data = if let Some(cell) = cell { if let Ok(val) = self.evaluate(&cell.to_string()) @@ -146,6 +231,40 @@ impl Grid { Ok(()) } + pub fn get_grid<'a>(&'a self) -> &'a CellGrid { + &self.grid_history[self.current_grid] + } + + fn get_grid_mut<'a>(&'a mut self) -> &'a mut CellGrid { + &mut self.grid_history[self.current_grid] + } + + pub fn undo(&mut self) { + self.current_grid = self.current_grid.saturating_sub(1); + } + pub fn redo(&mut self) { + self.current_grid = min(self.grid_history.len() - 1, self.current_grid + 1); + } + + pub fn transact_on_grid(&mut self, mut action: F) + where + F: FnMut(&mut CellGrid) -> (), + { + // push on a new reality + let new = self.get_grid().clone(); + self.grid_history.push(new); + self.current_grid += 1; + + // delete the other fork of the history + for i in self.current_grid + 1..self.grid_history.len() { + self.grid_history.remove(i); + } + + action(&mut self.grid_history[self.current_grid]); + + self.dirty = true; + } + pub fn needs_to_be_saved(&self) -> bool { self.dirty } @@ -252,101 +371,58 @@ impl Grid { } pub fn insert_row_above(&mut self, (_x, insertion_y): (usize, usize)) { - for x in 0..LEN { - self.cells[x].insert(insertion_y, None); - self.cells[x].pop(); - for y in 0..LEN { - if let Some(cell) = self.get_cell_raw(x, y).as_ref().map(|f| { - f.custom_translate_cell((0, 0), (0, 1), |rolling, old, new| { - if let Some((_, arg_y)) = Grid::parse_to_idx(old) { - if arg_y < insertion_y { rolling.to_owned() } else { rolling.replace(old, new) } - } else { - unimplemented!("Invalid variable wanted to be translated") - } + self.transact_on_grid(|grid: &mut CellGrid| { + grid.insert_row(insertion_y); + for x in 0..LEN { + for y in 0..LEN { + if let Some(cell) = grid.get_cell_raw(x, y).as_ref().map(|f| { + f.custom_translate_cell((0, 0), (0, 1), |rolling, old, new| { + if let Some((_, arg_y)) = Grid::parse_to_idx(old) { + if arg_y < insertion_y { rolling.to_owned() } else { rolling.replace(old, new) } + } else { + unimplemented!("Invalid variable wanted to be translated") + } + }) + }) { + grid.set_cell_raw((x, y), Some(cell)); } - )}) { - self.set_cell_raw((x,y), Some(cell)); - } - } - } - } - - pub fn insert_row_below(&mut self, (x, y): (usize, usize)) { - self.insert_row_above((x,y+1)); - } - - pub fn insert_column_before(&mut self, (insertion_x, _y): (usize, usize)) { - let mut v = Vec::with_capacity(LEN); - for _ in 0..LEN { - v.push(None); - } - self.cells.insert(insertion_x, v); - // keep the grid LEN - self.cells.pop(); - for x in 0..LEN { - for y in 0..LEN { - if let Some(cell) = self.get_cell_raw(x, y).as_ref().map(|f| { - f.custom_translate_cell((0, 0), (1, 0), |rolling, old, new| { - if let Some((arg_x, _)) = Grid::parse_to_idx(old) { - // add 1 because of the insertion - if arg_x < insertion_x { rolling.to_owned() } else { rolling.replace(old, new) } - } else { - // FIXME could be a range - unimplemented!("Invalid variable wanted to be translated") - } - }) - }) { - self.set_cell_raw((x, y), Some(cell)); } } - } + }); } + + pub fn insert_row_below(&mut self, (x, y): (usize, usize)) { + self.insert_row_above((x, y + 1)); + } + + + pub fn insert_column_before(&mut self, (insertion_x, _y): (usize, usize)) { + self.transact_on_grid(|grid| { + grid.insert_column(insertion_x); + for x in 0..LEN { + for y in 0..LEN { + if let Some(cell) = grid.get_cell_raw(x, y).as_ref().map(|f| { + f.custom_translate_cell((0, 0), (1, 0), |rolling, old, new| { + if let Some((arg_x, _)) = Grid::parse_to_idx(old) { + // add 1 because of the insertion + if arg_x < insertion_x { rolling.to_owned() } else { rolling.replace(old, new) } + } else { + unimplemented!("Invalid variable wanted to be translated") + } + }) + }) { + grid.set_cell_raw((x, y), Some(cell)); + } + } + } + }); + } + + pub fn insert_column_after(&mut self, (x, y): (usize, usize)) { self.insert_column_before((x + 1, y)); } - /// Iterate over the entire grid and see where - /// the farthest modified cell is. - #[must_use] - fn max(&self) -> (usize, usize) { - let mut max_x = 0; - let mut max_y = 0; - - for (xi, x) in self.cells.iter().enumerate() { - for (yi, cell) in x.iter().enumerate() { - if cell.is_some() { - if yi > max_y { - max_y = yi - } - if xi > max_x { - max_x = xi - } - } - } - } - - (max_x, max_y) - } - - #[must_use] - pub fn max_y_at_x(&self, x: usize) -> usize { - let mut max_y = 0; - if let Some(col) = self.cells.get(x) { - // we could do fancy things like .take_while(not null) but then - // we would have to deal with empty cells and stuff, which sounds - // boring. This will be fast "enough", considering the grid is - // probably only like 1k cells - for (yi, cell) in col.iter().enumerate() { - if cell.is_some() { - if yi > max_y { - max_y = yi - } - } - } - } - - max_y - } /// Only evaluates equations, such as `=10` or `=A1/C2`, not /// strings or numbers. @@ -385,10 +461,7 @@ impl Grid { EvalexprError::VariableIdentifierNotFound(var_not_found) => { return Err(format!("\"{var_not_found}\" is not a variable")); } - EvalexprError::TypeError { - expected: e, - actual: a, - } => { + EvalexprError::TypeError { expected: e, actual: a } => { // IE: You put a string into a function that wants a float return Err(format!("Wanted {e:?}, got {a}")); } @@ -412,7 +485,7 @@ impl Grid { } pub fn char_to_idx(i: &str) -> usize { - let x_idx = i + let x_idx = i .chars() .filter(|f| f.is_alphabetic()) .enumerate() @@ -447,15 +520,13 @@ impl Grid { /// Helper for tests #[cfg(test)] + /// Don't ever remove this from being just a test-helper. + /// This function doesn't correctly use the undo/redo api, which would require doing + /// transactions on the grid instead of direct access. pub fn set_cell>(&mut self, cell_id: &str, val: T) { if let Some(loc) = Self::parse_to_idx(cell_id) { - self.set_cell_raw(loc, Some(val)); + self.get_grid_mut().set_cell_raw(loc, Some(val)) } - } - - pub fn set_cell_raw>(&mut self, (x, y): (usize, usize), val: Option) { - // TODO check oob - self.cells[x][y] = val.map(|v| v.into()); self.dirty = true; } @@ -474,7 +545,7 @@ impl Grid { if x >= LEN || y >= LEN { return &None; } - &self.cells[x][y] + &self.get_grid().get_cell_raw(x, y) } pub fn num_to_char(idx: usize) -> String { @@ -592,7 +663,7 @@ fn saving_neoscim() { fn cell_strings() { let mut grid = Grid::new(); - assert!(&grid.cells[0][0].is_none()); + assert!(&grid.get_grid().get_cell_raw(0, 0).is_none()); grid.set_cell("A0", "Hello".to_string()); assert!(grid.get_cell("A0").is_some()); @@ -729,17 +800,17 @@ fn grid_max() { let mut grid = Grid::new(); grid.set_cell("A0", 1.); - let (mx, my) = grid.max(); + let (mx, my) = grid.get_grid().max(); assert_eq!(mx, 0); assert_eq!(my, 0); grid.set_cell("B0", 1.); - let (mx, my) = grid.max(); + let (mx, my) = grid.get_grid().max(); assert_eq!(mx, 1); assert_eq!(my, 0); grid.set_cell("B5", 1.); - let (mx, my) = grid.max(); + let (mx, my) = grid.get_grid().max(); assert_eq!(mx, 1); assert_eq!(my, 5); } @@ -1120,7 +1191,7 @@ fn insert_row_above_3() { #[test] fn cell_eval_depth() { use crate::app::mode::*; - let mut app= App::new(); + let mut app = App::new(); app.grid.set_cell("A0", 1.); app.grid.set_cell("A1", "=A0+$A$0".to_string()); @@ -1170,4 +1241,3 @@ fn return_string_from_fn() { } } } - diff --git a/src/app/mode.rs b/src/app/mode.rs index 0307761..fc99aa9 100644 --- a/src/app/mode.rs +++ b/src/app/mode.rs @@ -2,7 +2,8 @@ use std::{ cmp::{max, min}, fmt::Display, fs, - path::PathBuf, process::Command, + path::PathBuf, + process::Command, }; use ratatui::{ @@ -153,11 +154,13 @@ impl Mode { let mut save_range = |to: &str| { let mut g = Grid::new(); - for (i, x) in (low_x..=hi_x).enumerate() { - for (j, y) in (low_y..=hi_y).enumerate() { - g.set_cell_raw((i, j), app.grid.get_cell_raw(x, y).clone()); + g.transact_on_grid(|grid| { + for (i, x) in (low_x..=hi_x).enumerate() { + for (j, y) in (low_y..=hi_y).enumerate() { + grid.set_cell_raw((i, j), grid.get_cell_raw(x, y).clone()); + } } - } + }); if let Err(_e) = g.save_to(to) { app.msg = StatusMessage::error("Failed to save file"); } @@ -171,22 +174,24 @@ impl Mode { } } } - return "unknown" + return "unknown"; }; match args[0] { "f" | "fill" => { - for (i, x) in (low_x..=hi_x).enumerate() { - for (j, y) in (low_y..=hi_y).enumerate() { - let arg = args.get(1) - .map(|s| s.replace("xi", &i.to_string())) - .map(|s| s.replace("yi", &j.to_string())) - .map(|s| s.replace("x", &x.to_string())) - .map(|s| s.replace("y", &y.to_string())) - ; - app.grid.set_cell_raw((x,y), arg); + app.grid.transact_on_grid(|grid| { + for (i, x) in (low_x..=hi_x).enumerate() { + for (j, y) in (low_y..=hi_y).enumerate() { + let arg = args + .get(1) + .map(|s| s.replace("xi", &i.to_string())) + .map(|s| s.replace("yi", &j.to_string())) + .map(|s| s.replace("x", &x.to_string())) + .map(|s| s.replace("y", &y.to_string())); + grid.set_cell_raw((x, y), arg); + } } - } + }); app.mode = Mode::Normal } @@ -202,11 +207,7 @@ impl Mode { // Use gnuplot to plot the selected data. // * Temp data will be stored in /tmp/ // * Output will either be plot.png or a name that you pass in - let output_filename = if let Some(arg1) = args.get(1) { - arg1 - } else { - "plot.png" - }; + let output_filename = if let Some(arg1) = args.get(1) { arg1 } else { "plot.png" }; save_range("/tmp/plot.csv"); let plot = include_str!("../../template.gnuplot"); @@ -217,10 +218,12 @@ impl Mode { let s = s.replace("$OUTPUT", "/tmp/output.png"); let _ = fs::write("/tmp/plot.p", s); - let cmd_res= Command::new("gnuplot").arg("/tmp/plot.p").output(); + let cmd_res = Command::new("gnuplot").arg("/tmp/plot.p").output(); if let Err(err) = cmd_res { match err.kind() { - std::io::ErrorKind::NotFound => app.msg = StatusMessage::error("Error - Is gnuplot installed?"), + std::io::ErrorKind::NotFound => { + app.msg = StatusMessage::error("Error - Is gnuplot installed?") + } _ => app.msg = StatusMessage::error(format!("{err}")), }; } else { @@ -276,7 +279,7 @@ impl Mode { // Go to bottom of column 'G' => { let (x, _) = app.grid.cursor(); - app.grid.mv_cursor_to(x, super::logic::calc::LEN,); + app.grid.mv_cursor_to(x, super::logic::calc::LEN); return; } // edit cell @@ -319,6 +322,11 @@ impl Mode { app.mode = Mode::Command(Chord::new(':')) } } + // undo + 'u' => { + app.grid.undo(); + } + // paste 'p' => { app.clipboard.paste(&mut app.grid, true); app.grid.apply_momentum(app.clipboard.momentum()); @@ -398,7 +406,7 @@ impl Mode { let (_, y_height) = app.screen.get_screen_size(&app.vars); let y_origin = app.screen.scroll_y(); - app.grid.mv_cursor_to(x, y_origin+y_height); + app.grid.mv_cursor_to(x, y_origin + y_height); app.mode = Mode::Normal; return; } @@ -408,7 +416,7 @@ impl Mode { let (x_width, _) = app.screen.get_screen_size(&app.vars); let x_origin = app.screen.scroll_x(); - app.grid.mv_cursor_to(x_origin+x_width, y); + app.grid.mv_cursor_to(x_origin + x_width, y); app.mode = Mode::Normal; } // Go to the left edge of the current window @@ -508,9 +516,7 @@ pub struct Chord { impl From for Chord { fn from(value: String) -> Self { let b = value.as_bytes().iter().map(|f| *f as char).collect(); - Chord { - buf: b, - } + Chord { buf: b } } } @@ -519,9 +525,7 @@ impl Chord { let mut buf = Vec::new(); buf.push(inital); - Self { - buf, - } + Self { buf } } pub fn backspace(&mut self) {