diff --git a/src/app/app.rs b/src/app/app.rs index 3026c35..be26779 100644 --- a/src/app/app.rs +++ b/src/app/app.rs @@ -1,24 +1,13 @@ use std::{ - cmp::{max, min}, - collections::HashMap, - io, - path::PathBuf, + cmp::{max, min}, collections::HashMap, io, path::PathBuf }; use ratatui::{ - DefaultTerminal, Frame, - crossterm::event, - layout::{self, Constraint, Layout, Rect}, - prelude, - style::{Color, Modifier, Style}, - widgets::{Paragraph, Widget}, + DefaultTerminal, Frame, crossterm::event, layout::{self, Constraint, Layout, Rect}, prelude, style::{Color, Modifier, Style}, widgets::{Paragraph, Widget} }; use crate::app::{ - error_msg::ErrorMessage, - logic::calc::{CellType, Grid}, - mode::Mode, - screen::ScreenSpace, + clipboard::Clipboard, error_msg::ErrorMessage, logic::calc::{CellType, Grid}, mode::Mode, screen::ScreenSpace }; pub struct App { @@ -31,6 +20,7 @@ pub struct App { pub screen: ScreenSpace, // this could probably be a normal array pub marks: HashMap, + pub clipboard: Clipboard, } impl Widget for &App { @@ -39,7 +29,7 @@ impl Widget for &App { let is_selected = |x: usize, y: usize| -> bool { if let Mode::Visual((mut x1, mut y1)) = self.mode { - let (mut x2, mut y2) = self.grid.selected_cell; + let (mut x2, mut y2) = self.grid.cursor(); x1 += 1; y1 += 1; x2 += 1; @@ -96,7 +86,7 @@ impl Widget for &App { (true, false) => { display = y_idx.to_string(); - let bg = if y_idx == self.grid.selected_cell.1 { + let bg = if y_idx == self.grid.cursor().1 { Color::DarkGray } else if y_idx % 2 == 0 { ORANGE1 @@ -109,7 +99,7 @@ impl Widget for &App { (false, true) => { display = Grid::num_to_char(x_idx); - let bg = if x_idx == self.grid.selected_cell.0 { + let bg = if x_idx == self.grid.cursor().0 { Color::DarkGray } else if x_idx % 2 == 0 { ORANGE1 @@ -157,7 +147,7 @@ impl Widget for &App { } None => should_render = false, } - if (x_idx, y_idx) == self.grid.selected_cell { + if (x_idx, y_idx) == self.grid.cursor() { should_render = true; style = Style::new().fg(Color::Black).bg(Color::White); // modify the style of the cell you are editing @@ -201,6 +191,7 @@ impl App { vars: HashMap::new(), screen: ScreenSpace::new(), marks: HashMap::new(), + clipboard: Clipboard::new(), } } @@ -231,7 +222,7 @@ impl App { let len = match &self.mode { Mode::Insert(edit) | Mode::Command(edit) | Mode::Chord(edit) => edit.len(), Mode::Normal => { - let (x, y) = self.grid.selected_cell; + let (x, y) = self.grid.cursor(); let cell = self.grid.get_cell_raw(x, y).as_ref().map(|f| f.to_string().len()).unwrap_or_default(); cell } @@ -259,7 +250,7 @@ impl App { Mode::Chord(chord) => frame.render_widget(chord, cmd_line_left), Mode::Normal => frame.render_widget( Paragraph::new({ - let (x, y) = self.grid.selected_cell; + let (x, y) = self.grid.cursor(); let cell = self.grid.get_cell_raw(x, y).as_ref().map(|f| f.to_string()).unwrap_or_default(); cell }), @@ -274,7 +265,7 @@ impl App { frame.render_widget( Paragraph::new(format!( "x/w y/h: cursor{:?} scroll({}, {}) cell({}, {}) screen({}, {}) len{len}", - self.grid.selected_cell, + self.grid.cursor(), self.screen.scroll_x(), self.screen.scroll_y(), self.screen.get_cell_width(&self.vars), @@ -312,13 +303,13 @@ impl App { // try to insert as a float if let Ok(v) = v.parse::() { - self.grid.set_cell_raw(self.grid.selected_cell, Some(v)); + 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.selected_cell, Some(v)); + self.grid.set_cell_raw(self.grid.cursor(), Some(v)); } else { - self.grid.set_cell_raw::(self.grid.selected_cell, None); + self.grid.set_cell_raw::(self.grid.cursor(), None); } } @@ -378,7 +369,7 @@ impl App { } // make sure cursor is inside window - self.screen.scroll_based_on_cursor_location(self.grid.selected_cell, &self.vars); + self.screen.scroll_based_on_cursor_location(self.grid.cursor(), &self.vars); Ok(()) } diff --git a/src/app/clipboard.rs b/src/app/clipboard.rs new file mode 100644 index 0000000..3984a8a --- /dev/null +++ b/src/app/clipboard.rs @@ -0,0 +1,174 @@ +use std::cmp::{max, min}; + +use crate::app::logic::calc::{CellType, Grid}; + +#[cfg(test)] +use crate::app::{ + app::App, mode::{Chord, Mode} +}; + +pub struct Clipboard { + // could this just be a grid? + clipboard: Vec>>, + // top_left_cell: (usize, usize), + last_paste_cell: (usize, usize), + momentum: (i32, i32), +} + +impl Clipboard { + pub fn new() -> Self { + Self { + clipboard: Vec::new(), + last_paste_cell: (0, 0), + momentum: (0, 1), + } + } + + /// After pasting you gain momentum which can be used to + /// to move the cursor in the same direction for the next + /// paste. + pub fn momentum(&self) -> (i32, i32) { + // normalize to (-1,-1) to (1,1) + let (mx, my) = self.momentum; + let x = min(mx, 1); + let x = max(x, -1); + + let y = min(my, 1); + let y = max(y, -1); + + // prevent diagonal momentum + if y != 0 { + (0, y) + } else { + (x,0) + } + } + + pub fn paste(&mut self, into: &mut Grid) { + // cursor + let (cx, cy) = into.cursor(); + + for (x, row) in self.clipboard.iter().enumerate() { + for (y, cell) in row.iter().enumerate() { + into.set_cell_raw((x + cx, y + cy), cell.clone()); + } + } + + let (lx, ly) = self.last_paste_cell; + self.momentum = (cx as i32 - lx as i32, cy as i32 - ly as i32); + self.last_paste_cell = (cx, cy); + } + + /// Clones data from Grid into self. + /// Start and end don't have to be sorted in any sort of way. The function works with + /// any two points. + pub fn clipboard_copy(&mut self, start: (usize, usize), end: (usize, usize), from: &Grid) { + let (x1, y1) = start; + let (x2, y2) = end; + + let (low_x, hi_x) = if x1 < x2 { (x1, x2) } else { (x2, x1) }; + let (low_y, hi_y) = if y1 < y2 { (y1, y2) } else { (y2, y1) }; + + // 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()); + } + self.clipboard.push(col); + } + self.last_paste_cell = (low_x, low_y); + } +} + +#[test] +fn copy_paste() { + let mut app = App::new(); + + app.grid.set_cell("A0", "hello".to_string()); + app.grid.mv_cursor_to(0, 0); + + app.mode = super::mode::Mode::Chord(Chord::new('y')); + Mode::process_key(&mut app, 'y'); + // yy will have set mode back to normal at this point + + assert_eq!(app.clipboard.clipboard.len(), 1); + assert!(app.clipboard.clipboard[0][0].as_ref().is_some_and(|c| c.to_string() == "hello")); + + app.grid.mv_cursor_to(1, 1); + Mode::process_key(&mut app, 'p'); + + let a = app.grid.get_cell("B1").as_ref().expect("Should've been set by paste"); + assert_eq!(a.to_string(), "hello"); +} + +#[test] +fn momentum_y_pos() { + let mut app = App::new(); + + app.grid.set_cell("A0", "hello".to_string()); + app.grid.mv_cursor_to(0, 0); + + app.mode = super::mode::Mode::Chord(Chord::new('y')); + Mode::process_key(&mut app, 'y'); + // yy will have set mode back to normal at this point + + app.grid.mv_cursor_to(0, 1); + Mode::process_key(&mut app, 'p'); + + assert_eq!(app.clipboard.momentum(), (0,1)); +} + +#[test] +fn momentum_y_neg() { + let mut app = App::new(); + + app.grid.set_cell("A1", "hello".to_string()); + app.grid.mv_cursor_to(0, 1); + + app.mode = super::mode::Mode::Chord(Chord::new('y')); + Mode::process_key(&mut app, 'y'); + // yy will have set mode back to normal at this point + + app.grid.mv_cursor_to(0, 0); + Mode::process_key(&mut app, 'p'); + + assert_eq!(app.clipboard.momentum(), (0,-1)); +} + +#[test] +fn momentum_x_pos() { + let mut app = App::new(); + + app.grid.set_cell("A0", "hello".to_string()); + app.grid.mv_cursor_to(0, 0); + + app.mode = super::mode::Mode::Chord(Chord::new('y')); + Mode::process_key(&mut app, 'y'); + // yy will have set mode back to normal at this point + + app.grid.mv_cursor_to(1, 0); + Mode::process_key(&mut app, 'p'); + + assert_eq!(app.clipboard.momentum(), (1,0)); +} + +#[test] +fn momentum_x_neg() { + let mut app = App::new(); + + app.grid.set_cell("B0", "hello".to_string()); + app.grid.mv_cursor_to(1, 0); + + app.mode = super::mode::Mode::Chord(Chord::new('y')); + Mode::process_key(&mut app, 'y'); + // yy will have set mode back to normal at this point + + app.grid.mv_cursor_to(0, 0); + Mode::process_key(&mut app, 'p'); + + assert_eq!(app.clipboard.momentum(), (-1,0)); +} \ No newline at end of file diff --git a/src/app/logic/calc.rs b/src/app/logic/calc.rs index 1d1077f..4855383 100644 --- a/src/app/logic/calc.rs +++ b/src/app/logic/calc.rs @@ -7,7 +7,7 @@ use std::{ use evalexpr::*; -use crate::app::logic::ctx; +use crate::app::{app::App, logic::ctx}; pub const LEN: usize = 1000; @@ -19,7 +19,7 @@ pub struct Grid { // ... cells: Vec>>, /// (X, Y) - pub selected_cell: (usize, usize), + selected_cell: (usize, usize), /// Have unsaved modifications been made? dirty: bool, } @@ -34,6 +34,14 @@ const CSV_DELIMITER: char = ','; const CSV_ESCAPE: char = '"'; impl Grid { + pub fn cursor(&self) -> (usize, usize) { + self.selected_cell + } + + pub fn mv_cursor_to(&mut self, x: usize, y: usize) { + self.selected_cell = (x,y) + } + pub fn needs_to_be_saved(&self) -> bool { self.dirty } @@ -311,7 +319,7 @@ impl Grid { /// Helper for tests #[cfg(test)] - fn set_cell>(&mut self, cell_id: &str, val: T) { + 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)); } @@ -365,7 +373,7 @@ impl Default for Grid { } } -#[derive(Debug)] +#[derive(Debug, Clone)] pub enum CellType { Number(f64), String(String), @@ -710,5 +718,16 @@ fn ranges() { #[test] fn recursive_ranges() { // recursive ranges causes weird behavior - todo!(); + // todo!(); +} + +#[test] +fn cursor_fns() { + // surprisingly, this test was needed + let mut app = App::new(); + let c = app.grid.cursor(); + assert_eq!(c, (0,0)); + + app.grid.mv_cursor_to(1, 0); + assert_eq!(app.grid.cursor(), (1,0)); } \ No newline at end of file diff --git a/src/app/mod.rs b/src/app/mod.rs index f8cb699..e18abed 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -2,4 +2,5 @@ pub mod app; mod mode; mod error_msg; mod screen; -mod logic; \ No newline at end of file +mod logic; +mod clipboard; \ No newline at end of file diff --git a/src/app/mode.rs b/src/app/mode.rs index d1d0157..9da414a 100644 --- a/src/app/mode.rs +++ b/src/app/mode.rs @@ -116,31 +116,36 @@ impl Mode { match key { // < 'h' => { - app.grid.selected_cell.0 = app.grid.selected_cell.0.saturating_sub(1); + let (x, y) = app.grid.cursor(); + app.grid.mv_cursor_to(x.saturating_sub(1), y); return; } // v 'j' => { - app.grid.selected_cell.1 = min(app.grid.selected_cell.1.saturating_add(1), LEN - 1); + let (x, y) = app.grid.cursor(); + app.grid.mv_cursor_to(x, min(y.saturating_add(1), LEN - 1)); return; } // ^ 'k' => { - app.grid.selected_cell.1 = app.grid.selected_cell.1.saturating_sub(1); + let (x, y) = app.grid.cursor(); + app.grid.mv_cursor_to(x, y.saturating_sub(1)); return; } // > 'l' => { - app.grid.selected_cell.0 = min(app.grid.selected_cell.0.saturating_add(1), LEN - 1); + let (x, y) = app.grid.cursor(); + app.grid.mv_cursor_to(min(x.saturating_add(1), LEN - 1), y); return; } '0' => { - app.grid.selected_cell.0 = 0; + let (_, y) = app.grid.cursor(); + app.grid.mv_cursor_to(0, y); return; } // edit cell 'i' | 'a' => { - let (x, y) = app.grid.selected_cell; + let (x, y) = app.grid.cursor(); let val = app.grid.get_cell_raw(x, y).as_ref().map(|f| f.to_string()).unwrap_or(String::new()); @@ -154,8 +159,15 @@ impl Mode { 'A' => { /* insert col after */ } 'o' => { /* insert row below */ } 'O' => { /* insert row above */ } - 'v' => app.mode = Mode::Visual(app.grid.selected_cell), + 'v' => app.mode = Mode::Visual(app.grid.cursor()), ':' => app.mode = Mode::Command(Chord::new(':')), + 'p' => { + app.clipboard.paste(&mut app.grid); + let (cx, cy) = app.grid.cursor(); + let (mx, my) = app.clipboard.momentum(); + app.grid.mv_cursor_to((cx as i32 + mx) as usize, (cy as i32 + my) as usize); + return; + } // loose chars will put you into chord mode c => { if let Mode::Normal = app.mode { @@ -165,18 +177,24 @@ impl Mode { } if let Mode::Visual((x1, y1)) = app.mode { // TODO visual copy, paste, etc - let (x2, y2) = app.grid.selected_cell; + let (x2, y2) = app.grid.cursor(); let (low_x, hi_x) = if x1 < x2 { (x1, x2) } else { (x2, x1) }; let (low_y, hi_y) = if y1 < y2 { (y1, y2) } else { (y2, y1) }; - if key == 'd' { - for x in low_x..=hi_x { - for y in low_y..=hi_y { - app.grid.set_cell_raw::((x, y), None); + match key { + 'd' => { + for x in low_x..=hi_x { + for y in low_y..=hi_y { + app.grid.set_cell_raw::((x, y), None); + } } + app.mode = Mode::Normal } - app.mode = Mode::Normal + 'y' => { + app.clipboard.clipboard_copy((x1, y1), (x2, y2), &app.grid); + } + _ => {} } } } @@ -194,8 +212,8 @@ impl Mode { // For chords that can take a numeric input Ok(num) => match key { 'G' => { - let sel = app.grid.selected_cell; - app.grid.selected_cell = (sel.0, num); + let (x, _) = app.grid.cursor(); + app.grid.mv_cursor_to(x, num); app.mode = Mode::Normal; } _ => { @@ -210,36 +228,43 @@ impl Mode { Err(_) => { let c = chord.as_string(); // match everything up to, and then the new key - match (&c[..c.len()-1], key) { + match (&c[..c.len() - 1], key) { // delete cell under cursor ("d", ' ') | ("d", 'w') => { - let loc = app.grid.selected_cell; + let loc = app.grid.cursor(); app.grid.set_cell_raw::(loc, None); app.mode = Mode::Normal; } // go to top of row ("g", 'g') => { - app.grid.selected_cell.1 = 0; + let (x, _) = app.grid.cursor(); + app.grid.mv_cursor_to(x, 0); app.mode = Mode::Normal; } // center screen to cursor ("z", 'z') => { - app.screen.center_x(app.grid.selected_cell, &app.vars); - app.screen.center_y(app.grid.selected_cell, &app.vars); + app.screen.center_x(app.grid.cursor(), &app.vars); + app.screen.center_y(app.grid.cursor(), &app.vars); app.mode = Mode::Normal; } // mark cell ("m", i) => { - app.marks.insert(i, app.grid.selected_cell); + app.marks.insert(i, app.grid.cursor()); app.mode = Mode::Normal; } // goto marked cell ("'", i) => { - if let Some(coords) = app.marks.get(&i) { - app.grid.selected_cell = *coords; + if let Some((cx, cy)) = app.marks.get(&i) { + app.grid.mv_cursor_to(*cx, *cy); } app.mode = Mode::Normal; } + // copy 1 cell + ("y", 'y') => { + let point = app.grid.cursor(); + app.clipboard.clipboard_copy(point, point, &app.grid); + app.mode = Mode::Normal; + } _ => {} } } @@ -295,31 +320,47 @@ impl Widget for &Chord { } } +#[test] +fn movement_keybinds() { + let mut app = App::new(); + + assert_eq!(app.grid.cursor(), (0, 0)); + Mode::process_key(&mut app, 'j'); + assert_eq!(app.grid.cursor(), (0, 1)); + + Mode::process_key(&mut app, 'l'); + assert_eq!(app.grid.cursor(), (1, 1)); + + Mode::process_key(&mut app, 'k'); + assert_eq!(app.grid.cursor(), (1, 0)); + + Mode::process_key(&mut app, 'h'); + assert_eq!(app.grid.cursor(), (0, 0)); +} + #[test] fn keybinds() { let mut app = App::new(); - assert_eq!(app.grid.selected_cell, (0,0)); - // start at B1 - app.grid.selected_cell = (1,1); - assert_eq!(app.grid.selected_cell, (1,1)); + app.grid.mv_cursor_to(1, 1); + assert_eq!(app.grid.cursor(), (1, 1)); // gg app.mode = Mode::Chord(Chord::new('g')); Mode::process_key(&mut app, 'g'); - assert_eq!(app.grid.selected_cell, (1,0)); + assert_eq!(app.grid.cursor(), (1, 0)); // 0 app.mode = Mode::Normal; Mode::process_key(&mut app, '0'); - assert_eq!(app.grid.selected_cell, (0,0)); + assert_eq!(app.grid.cursor(), (0, 0)); // 10l // this should mean all the directions work - app.grid.selected_cell = (0,0); + app.grid.mv_cursor_to(0, 0); app.mode = Mode::Chord(Chord::new('1')); Mode::process_key(&mut app, '0'); Mode::process_key(&mut app, 'l'); - assert_eq!(app.grid.selected_cell, (10,0)); + assert_eq!(app.grid.cursor(), (10, 0)); }