implement paste-translation for #16

This commit is contained in:
2025-11-13 12:39:06 -07:00
parent 3dc9b991ec
commit 65f18c9abf
5 changed files with 217 additions and 24 deletions

View File

@@ -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<Vec<Option<CellType>>>,
// 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::<CellType>(idx, None);
}
} else {
// translate = false
into.set_cell_raw::<CellType>(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::<CellType>((x,y), None);
from.set_cell_raw::<CellType>((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));
}
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");
}

View File

@@ -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::<Vec<char>>();
let nums = i.chars().skip(chars.len()).take_while(|c| c.is_numeric()).collect::<String>();

View File

@@ -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<Vec<String>>,
fn_registry: RwLock<Vec<String>>,
}
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<String> {
if let Ok(r) = self.var_registry.read() {
r.clone()
} else {
Vec::new()
}
}
pub fn dump_fns(&self) -> Vec<String> {
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<Value<Self::NumericTypes>> {
if let Ok(mut registry) = self.var_registry.write() {
registry.push(identifier.to_owned());
}
None
}
fn call_function(
&self,
identifier: &str,
argument: &Value<Self::NumericTypes>,
) -> EvalexprResultValue<Self::NumericTypes> {
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(())
}
}

View File

@@ -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;
}
_ => {}
}
}