implement paste-translation for #16
This commit is contained in:
@@ -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" |
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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>();
|
||||
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user