implement paste-translation for #16
This commit is contained in:
@@ -38,7 +38,8 @@ if value.can_be_a_number() {
|
|||||||
| `:` | Enter command mode |
|
| `:` | Enter command mode |
|
||||||
| `yy` | Yank current cell |
|
| `yy` | Yank current cell |
|
||||||
| `d `/`dw` | Cut 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 |
|
| `zz` | Center grid on cursor |
|
||||||
| `m`X | Mark cell "X" |
|
| `m`X | Mark cell "X" |
|
||||||
| `'`X | Jump to 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)]
|
#[cfg(test)]
|
||||||
use crate::app::{
|
use crate::app::{
|
||||||
app::App, mode::{Chord, Mode}
|
app::App,
|
||||||
|
mode::{Chord, Mode},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct Clipboard {
|
pub struct Clipboard {
|
||||||
// could this just be a grid?
|
// could this just be a grid?
|
||||||
clipboard: Vec<Vec<Option<CellType>>>,
|
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),
|
last_paste_cell: (usize, usize),
|
||||||
momentum: (i32, i32),
|
momentum: (i32, i32),
|
||||||
}
|
}
|
||||||
@@ -19,6 +29,7 @@ impl Clipboard {
|
|||||||
clipboard: Vec::new(),
|
clipboard: Vec::new(),
|
||||||
last_paste_cell: (0, 0),
|
last_paste_cell: (0, 0),
|
||||||
momentum: (0, 1),
|
momentum: (0, 1),
|
||||||
|
source_cell: (0, 0),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,29 +40,71 @@ impl Clipboard {
|
|||||||
let x_len = self.clipboard.len();
|
let x_len = self.clipboard.len();
|
||||||
let y_len = self.clipboard[0].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
|
/// to move the cursor in the same direction for the next
|
||||||
/// paste.
|
/// paste.
|
||||||
pub fn momentum(&self) -> (i32, i32) {
|
pub fn momentum(&self) -> (i32, i32) {
|
||||||
let (x, y) = self.momentum;
|
let (x, y) = self.momentum;
|
||||||
// prevent diagonal momentum
|
// prevent diagonal momentum
|
||||||
if y != 0 {
|
if y != 0 { (0, y) } else { (x, 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
|
// cursor
|
||||||
let (cx, cy) = into.cursor();
|
let (cx, cy) = into.cursor();
|
||||||
|
|
||||||
for (x, row) in self.clipboard.iter().enumerate() {
|
for (x, row) in self.clipboard.iter().enumerate() {
|
||||||
for (y, cell) in row.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_x, hi_x) = if x1 < x2 { (x1, x2) } else { (x2, x1) };
|
||||||
let (low_y, hi_y) = if y1 < y2 { (y1, y2) } else { (y2, y1) };
|
let (low_y, hi_y) = if y1 < y2 { (y1, y2) } else { (y2, y1) };
|
||||||
|
|
||||||
|
self.source_cell = (low_x, low_y);
|
||||||
|
|
||||||
// size the clipboard appropriately
|
// size the clipboard appropriately
|
||||||
self.clipboard.clear();
|
self.clipboard.clear();
|
||||||
// clone data into clipboard
|
// clone data into clipboard
|
||||||
@@ -99,7 +154,7 @@ impl Clipboard {
|
|||||||
for y in low_y..=hi_y {
|
for y in low_y..=hi_y {
|
||||||
let a = from.get_cell_raw(x, y);
|
let a = from.get_cell_raw(x, y);
|
||||||
col.push(a.clone());
|
col.push(a.clone());
|
||||||
from.set_cell_raw::<CellType>((x,y), None);
|
from.set_cell_raw::<CellType>((x, y), None);
|
||||||
}
|
}
|
||||||
self.clipboard.push(col);
|
self.clipboard.push(col);
|
||||||
}
|
}
|
||||||
@@ -142,7 +197,7 @@ fn momentum_y_pos() {
|
|||||||
app.grid.mv_cursor_to(0, 1);
|
app.grid.mv_cursor_to(0, 1);
|
||||||
Mode::process_key(&mut app, 'p');
|
Mode::process_key(&mut app, 'p');
|
||||||
|
|
||||||
assert_eq!(app.clipboard.momentum(), (0,1));
|
assert_eq!(app.clipboard.momentum(), (0, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -159,7 +214,7 @@ fn momentum_y_neg() {
|
|||||||
app.grid.mv_cursor_to(0, 0);
|
app.grid.mv_cursor_to(0, 0);
|
||||||
Mode::process_key(&mut app, 'p');
|
Mode::process_key(&mut app, 'p');
|
||||||
|
|
||||||
assert_eq!(app.clipboard.momentum(), (0,-1));
|
assert_eq!(app.clipboard.momentum(), (0, -1));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -176,7 +231,7 @@ fn momentum_x_pos() {
|
|||||||
app.grid.mv_cursor_to(1, 0);
|
app.grid.mv_cursor_to(1, 0);
|
||||||
Mode::process_key(&mut app, 'p');
|
Mode::process_key(&mut app, 'p');
|
||||||
|
|
||||||
assert_eq!(app.clipboard.momentum(), (1,0));
|
assert_eq!(app.clipboard.momentum(), (1, 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -193,7 +248,7 @@ fn momentum_x_neg() {
|
|||||||
app.grid.mv_cursor_to(0, 0);
|
app.grid.mv_cursor_to(0, 0);
|
||||||
Mode::process_key(&mut app, 'p');
|
Mode::process_key(&mut app, 'p');
|
||||||
|
|
||||||
assert_eq!(app.clipboard.momentum(), (-1,0));
|
assert_eq!(app.clipboard.momentum(), (-1, 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -211,10 +266,73 @@ fn diagonal_momentum() {
|
|||||||
assert_eq!(app.grid.cursor(), (1, 0));
|
assert_eq!(app.grid.cursor(), (1, 0));
|
||||||
Mode::process_key(&mut app, 'p');
|
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));
|
assert_eq!(app.grid.cursor(), (1, 0));
|
||||||
|
|
||||||
app.grid.apply_momentum(app.clipboard.momentum());
|
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
|
/// Parse values in the format of A0, C10 ZZ99, etc, and
|
||||||
/// turn them into an X,Y index.
|
/// 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 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>();
|
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 lookup_value = &args[1];
|
||||||
let return_array = &args[2];
|
let return_array = &args[2];
|
||||||
|
|
||||||
|
|
||||||
if lookup_array.is_tuple() && return_array.is_tuple() {
|
if lookup_array.is_tuple() && return_array.is_tuple() {
|
||||||
let mut found_at = None;
|
let mut found_at = None;
|
||||||
for (i, val) in lookup_array.as_tuple()?.iter().enumerate() {
|
for (i, val) in lookup_array.as_tuple()?.iter().enumerate() {
|
||||||
@@ -239,3 +238,70 @@ impl<'a> Context for CallbackContext<'a> {
|
|||||||
Ok(())
|
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()),
|
'v' => app.mode = Mode::Visual(app.grid.cursor()),
|
||||||
':' => app.mode = Mode::Command(Chord::new(':')),
|
':' => app.mode = Mode::Command(Chord::new(':')),
|
||||||
'p' => {
|
'p' => {
|
||||||
app.clipboard.paste(&mut app.grid);
|
app.clipboard.paste(&mut app.grid, true);
|
||||||
app.grid.apply_momentum(app.clipboard.momentum());
|
app.grid.apply_momentum(app.clipboard.momentum());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -264,6 +264,14 @@ impl Mode {
|
|||||||
app.mode = Mode::Normal;
|
app.mode = Mode::Normal;
|
||||||
app.msg = StatusMessage::info("Yanked 1 cell");
|
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