diff --git a/README.md b/README.md index 014be9c..5899a98 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,8 @@ if value.can_be_a_number() { | `:` | Enter command mode | | `yy` | Yank current cell | | `d `/`dw` | Cut current cell | -| `p` | Paste clipboard (cursor is top-left of multi-cell pastes) | +| `p` | Paste clipboard (cursor is top-left of multi-cell pastes). Automatically translates cell references | +| `gp` | Paste clipboard, no reference translation | | `zz` | Center grid on cursor | | `m`X | Mark cell "X" | | `'`X | Jump to cell "X" | diff --git a/src/app/clipboard.rs b/src/app/clipboard.rs index bb2c1e6..bc32122 100644 --- a/src/app/clipboard.rs +++ b/src/app/clipboard.rs @@ -1,14 +1,24 @@ -use crate::app::logic::calc::{CellType, Grid}; +use std::cmp::min; + +use evalexpr::eval_with_context; + +use crate::app::logic::{ + calc::{CellType, Grid}, + ctx::ExtractionContext, +}; #[cfg(test)] use crate::app::{ - app::App, mode::{Chord, Mode} + app::App, + mode::{Chord, Mode}, }; pub struct Clipboard { // could this just be a grid? clipboard: Vec>>, - // top_left_cell: (usize, usize), + /// For calculating variable translation + source_cell: (usize, usize), + /// For tracking momentum direction last_paste_cell: (usize, usize), momentum: (i32, i32), } @@ -19,6 +29,7 @@ impl Clipboard { clipboard: Vec::new(), last_paste_cell: (0, 0), momentum: (0, 1), + source_cell: (0, 0), } } @@ -29,29 +40,71 @@ impl Clipboard { let x_len = self.clipboard.len(); let y_len = self.clipboard[0].len(); - x_len*y_len + x_len * y_len } - /// After pasting you gain momentum which can be used to + /// 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) { let (x, y) = self.momentum; // prevent diagonal momentum - if y != 0 { - (0, y) - } else { - (x,0) - } + if y != 0 { (0, y) } else { (x, 0) } } - pub fn paste(&mut self, into: &mut Grid) { + pub fn paste(&mut self, into: &mut Grid, translate: bool) { // 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 idx = (x + cx, y + cy); + + if translate { + if let Some(cell) = cell { + match cell { + // don't translate non-equations + CellType::Number(_) | CellType::String(_) => into.set_cell_raw(idx, Some(cell.clone())), + CellType::Equation(eq) => { + // extract all the variables + let ctx = ExtractionContext::new(); + let _ = eval_with_context(eq, &ctx); + + let mut rolling = eq.clone(); + // translate standard vars A0 -> A1 + for old_var in ctx.dump_vars() { + if let Some((src_x, src_y)) = Grid::parse_to_idx(&old_var) { + let (x1, y1) = self.source_cell; + let x1 = x1 as i32; + let y1 = y1 as i32; + let (x2, y2) = into.cursor(); + let x2 = x2 as i32; + let y2 = y2 as i32; + + let dest_x = (src_x as i32 + (x2 - x1)) as usize; + let dest_y = (src_y as i32 + (y2 - y1)) as usize; + + let alpha = Grid::num_to_char(dest_x); + let alpha = alpha.trim(); + let new_var = format!("{alpha}{dest_y}"); + + // swap out vars + rolling = rolling.replace(&old_var, &new_var); + } else { + // why you coping invalid stuff, nerd? + } + } + into.set_cell_raw(idx, Some(rolling)); + } + } + } else { + // cell doesn't exist, no need to translate + into.set_cell_raw::(idx, None); + } + } else { + // translate = false + into.set_cell_raw::(idx, cell.clone()); + } } } @@ -70,6 +123,8 @@ impl Clipboard { 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) }; + self.source_cell = (low_x, low_y); + // size the clipboard appropriately self.clipboard.clear(); // clone data into clipboard @@ -99,7 +154,7 @@ impl Clipboard { 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.set_cell_raw::((x, y), None); } self.clipboard.push(col); } @@ -142,7 +197,7 @@ fn momentum_y_pos() { app.grid.mv_cursor_to(0, 1); Mode::process_key(&mut app, 'p'); - assert_eq!(app.clipboard.momentum(), (0,1)); + assert_eq!(app.clipboard.momentum(), (0, 1)); } #[test] @@ -159,7 +214,7 @@ fn momentum_y_neg() { app.grid.mv_cursor_to(0, 0); Mode::process_key(&mut app, 'p'); - assert_eq!(app.clipboard.momentum(), (0,-1)); + assert_eq!(app.clipboard.momentum(), (0, -1)); } #[test] @@ -176,7 +231,7 @@ fn momentum_x_pos() { app.grid.mv_cursor_to(1, 0); Mode::process_key(&mut app, 'p'); - assert_eq!(app.clipboard.momentum(), (1,0)); + assert_eq!(app.clipboard.momentum(), (1, 0)); } #[test] @@ -193,7 +248,7 @@ fn momentum_x_neg() { app.grid.mv_cursor_to(0, 0); Mode::process_key(&mut app, 'p'); - assert_eq!(app.clipboard.momentum(), (-1,0)); + assert_eq!(app.clipboard.momentum(), (-1, 0)); } #[test] @@ -211,10 +266,73 @@ fn diagonal_momentum() { assert_eq!(app.grid.cursor(), (1, 0)); Mode::process_key(&mut app, 'p'); - assert_eq!(app.clipboard.momentum(), (0,-1)); + assert_eq!(app.clipboard.momentum(), (0, -1)); assert_eq!(app.grid.cursor(), (1, 0)); app.grid.apply_momentum(app.clipboard.momentum()); - assert_eq!(app.grid.cursor(), (1,0)); -} \ No newline at end of file + assert_eq!(app.grid.cursor(), (1, 0)); +} + +#[test] +fn copy_paste_vars_translate() { + let mut app = App::new(); + + // Translate Right ==================================================== + // A0 = A1 = 1 + app.grid.set_cell("A0", "=A1".to_string()); + app.grid.set_cell("A1", 1.); + + // Copy A0 + app.grid.mv_cursor_to(0, 0); + app.mode = super::mode::Mode::Chord(Chord::new('y')); + Mode::process_key(&mut app, 'y'); + + assert!(app.clipboard.clipboard[0][0].as_ref().is_some_and(|c| c.to_string() == "=A1")); + + // Move cursor to B0 + app.grid.mv_cursor_to(1, 0); + Mode::process_key(&mut app, 'p'); + + let a = app.grid.get_cell("B0").as_ref().expect("Should've been set by paste"); + assert_eq!(a.to_string(), "=B1"); + + // Translate Left ==================================================== + // Copy B0 + app.grid.mv_cursor_to(1, 0); + app.mode = super::mode::Mode::Chord(Chord::new('y')); + Mode::process_key(&mut app, 'y'); + + // Move cursor to A0 + app.grid.mv_cursor_to(0, 0); + Mode::process_key(&mut app, 'p'); + + let a = app.grid.get_cell("A0").as_ref().expect("Should've been set by paste"); + assert_eq!(a.to_string(), "=A1"); + + // Translate Down ==================================================== + // Copy A0 + app.grid.mv_cursor_to(0, 0); + app.mode = super::mode::Mode::Chord(Chord::new('y')); + Mode::process_key(&mut app, 'y'); + + // Move cursor to A0 + app.grid.mv_cursor_to(0, 1); + Mode::process_key(&mut app, 'p'); + + let a = app.grid.get_cell("A1").as_ref().expect("Should've been set by paste"); + assert_eq!(a.to_string(), "=A2"); + + // Translate Up ==================================================== + // Copy A1 + app.grid.mv_cursor_to(0, 1); + app.mode = super::mode::Mode::Chord(Chord::new('y')); + Mode::process_key(&mut app, 'y'); + + // Move cursor to A0 + app.grid.mv_cursor_to(0, 0); + Mode::process_key(&mut app, 'p'); + + let a = app.grid.get_cell("A0").as_ref().expect("Should've been set by paste"); + assert_eq!(a.to_string(), "=A1"); +} diff --git a/src/app/logic/calc.rs b/src/app/logic/calc.rs index 27cbd13..44e2161 100644 --- a/src/app/logic/calc.rs +++ b/src/app/logic/calc.rs @@ -311,7 +311,7 @@ impl Grid { /// Parse values in the format of A0, C10 ZZ99, etc, and /// turn them into an X,Y index. - fn parse_to_idx(i: &str) -> Option<(usize, usize)> { + pub fn parse_to_idx(i: &str) -> Option<(usize, usize)> { let chars = i.chars().take_while(|c| c.is_alphabetic()).collect::>(); let nums = i.chars().skip(chars.len()).take_while(|c| c.is_numeric()).collect::(); diff --git a/src/app/logic/ctx.rs b/src/app/logic/ctx.rs index d6822e0..301fdcd 100644 --- a/src/app/logic/ctx.rs +++ b/src/app/logic/ctx.rs @@ -112,7 +112,6 @@ impl<'a> CallbackContext<'a> { let lookup_value = &args[1]; let return_array = &args[2]; - if lookup_array.is_tuple() && return_array.is_tuple() { let mut found_at = None; for (i, val) in lookup_array.as_tuple()?.iter().enumerate() { @@ -239,3 +238,70 @@ impl<'a> Context for CallbackContext<'a> { Ok(()) } } + + + +/// DOES NOT EVALUATE EQUATIONS!! +/// +/// This is used as a pseudo-context, just used for +/// learning all the variables in an expression. +#[derive(Debug)] +pub struct ExtractionContext { + var_registry: RwLock>, + fn_registry: RwLock>, +} + +impl ExtractionContext { + pub fn new() -> Self { + Self { + var_registry: RwLock::new(Vec::new()), + fn_registry: RwLock::new(Vec::new()), + } + } + pub fn dump_vars(&self) -> Vec { + if let Ok(r) = self.var_registry.read() { + r.clone() + } else { + Vec::new() + } + } + pub fn dump_fns(&self) -> Vec { + if let Ok(r) = self.fn_registry.read() { + r.clone() + } else { + Vec::new() + } + } +} + +impl Context for ExtractionContext { + type NumericTypes = DefaultNumericTypes; + + fn get_value(&self, identifier: &str) -> Option> { + if let Ok(mut registry) = self.var_registry.write() { + registry.push(identifier.to_owned()); + } + None + } + + fn call_function( + &self, + identifier: &str, + argument: &Value, + ) -> EvalexprResultValue { + let _ = argument; + if let Ok(mut registry) = self.fn_registry.write() { + registry.push(identifier.to_owned()) + } + Ok(Value::Empty) + } + + fn are_builtin_functions_disabled(&self) -> bool { + false + } + + fn set_builtin_functions_disabled(&mut self, disabled: bool) -> EvalexprResult<(), Self::NumericTypes> { + let _ = disabled; + Ok(()) + } +} diff --git a/src/app/mode.rs b/src/app/mode.rs index 62d4bbc..2c55526 100644 --- a/src/app/mode.rs +++ b/src/app/mode.rs @@ -167,7 +167,7 @@ impl Mode { 'v' => app.mode = Mode::Visual(app.grid.cursor()), ':' => app.mode = Mode::Command(Chord::new(':')), 'p' => { - app.clipboard.paste(&mut app.grid); + app.clipboard.paste(&mut app.grid, true); app.grid.apply_momentum(app.clipboard.momentum()); return; } @@ -264,6 +264,14 @@ impl Mode { app.mode = Mode::Normal; app.msg = StatusMessage::info("Yanked 1 cell"); } + ("g", 'p') => { + app.clipboard.paste(&mut app.grid, false); + app.grid.apply_momentum(app.clipboard.momentum()); + app.mode = Mode::Normal; + let plural = if app.clipboard.qty() > 1 {"cells"} else {"cell"}; + app.msg = StatusMessage::info(format!("Pasted {plural}, no formatting")); + return; + } _ => {} } }