Compare commits

1 Commits

Author SHA1 Message Date
1825460074 add failing test
Some checks failed
Test Rust project / test (ubuntu-latest, stable) (push) Failing after 48s
2026-02-07 19:30:08 -07:00
5 changed files with 267 additions and 327 deletions

View File

@@ -18,10 +18,7 @@ use ratatui::{
use crate::app::{ use crate::app::{
clipboard::Clipboard, clipboard::Clipboard,
error_msg::StatusMessage, error_msg::StatusMessage,
logic::{ logic::{calc::{Grid, get_header_size}, cell::CellType},
calc::{Grid, get_header_size},
cell::CellType,
},
mode::Mode, mode::Mode,
screen::ScreenSpace, screen::ScreenSpace,
}; };
@@ -45,7 +42,7 @@ impl Widget for &App {
let (x_max, y_max) = self.screen.how_many_cells_fit_in(&area, &self.vars); let (x_max, y_max) = self.screen.how_many_cells_fit_in(&area, &self.vars);
let is_selected = |x: usize, y: usize| -> bool { let is_selected = |x: usize, y: usize| -> bool {
if let Mode::Visual((mut x1, mut y1)) | Mode::VisualCmd((mut x1, mut y1), _) = self.mode { if let Mode::Visual((mut x1, mut y1)) | Mode::VisualCmd((mut x1, mut y1), _)= self.mode {
let (mut x2, mut y2) = self.grid.cursor(); let (mut x2, mut y2) = self.grid.cursor();
x1 += 1; x1 += 1;
y1 += 1; y1 += 1;
@@ -81,7 +78,7 @@ impl Widget for &App {
let mut x_idx: usize = 0; let mut x_idx: usize = 0;
let mut y_idx: usize = 0; let mut y_idx: usize = 0;
if x != 0 { if x != 0 {
x_idx = x as usize - 1 + self.screen.scroll_x(); x_idx = x as usize -1 + self.screen.scroll_x();
} }
if y != 0 { if y != 0 {
y_idx = y as usize - 1 + self.screen.scroll_y(); y_idx = y as usize - 1 + self.screen.scroll_y();
@@ -96,9 +93,9 @@ impl Widget for &App {
/// Center the text "99 " -> " 99 " /// Center the text "99 " -> " 99 "
fn center_text(text: &str, avaliable_space: i32) -> String { fn center_text(text: &str, avaliable_space: i32) -> String {
let margin = avaliable_space - text.len() as i32; let margin = avaliable_space - text.len() as i32;
let margin = margin / 2; let margin = margin/2;
let l_margin = (0..margin).into_iter().map(|_| ' ').collect::<String>(); let l_margin = (0..margin).into_iter().map(|_| ' ').collect::<String>();
let r_margin = (0..(margin - (l_margin.len() as i32))).into_iter().map(|_| ' ').collect::<String>(); let r_margin = (0..(margin-(l_margin.len() as i32))).into_iter().map(|_| ' ').collect::<String>();
format!("{l_margin}{text}{r_margin}") format!("{l_margin}{text}{r_margin}")
} }
@@ -217,7 +214,7 @@ impl Widget for &App {
} }
// If this is the row header column // If this is the row header column
let area = if x == 0 && y != 0 { let area = if x==0 && y != 0 {
Rect::new(x_off, y_off, row_header_width, cell_height) Rect::new(x_off, y_off, row_header_width, cell_height)
} else if let Some(suggestion) = suggest_upper_bound { } else if let Some(suggestion) = suggest_upper_bound {
let max_available_width = area.width - x_off; let max_available_width = area.width - x_off;
@@ -390,20 +387,17 @@ impl App {
event::KeyCode::Enter => { event::KeyCode::Enter => {
let v = editor.as_string(); let v = editor.as_string();
let cursor = self.grid.cursor(); // try to insert as a float
self.grid.transact_on_grid(|grid| { if let Ok(v) = v.parse::<f64>() {
// try to insert as a float self.grid.set_cell_raw(self.grid.cursor(), Some(v));
if let Ok(v) = v.parse::<f64>() { } else {
grid.set_cell_raw(cursor, Some(v)); // if you can't, then insert as a string
if !v.is_empty() {
self.grid.set_cell_raw(self.grid.cursor(), Some(v));
} else { } else {
// if you can't, then insert as a string self.grid.set_cell_raw::<CellType>(self.grid.cursor(), None);
if !v.is_empty() {
grid.set_cell_raw(cursor, Some(v.to_owned()));
} else {
grid.set_cell_raw::<CellType>(cursor, None);
}
} }
}); }
self.mode = Mode::Normal; self.mode = Mode::Normal;
} }
@@ -419,23 +413,10 @@ impl App {
}, },
Mode::Normal => match event::read()? { Mode::Normal => match event::read()? {
event::Event::Key(key_event) => match key_event.code { event::Event::Key(key_event) => match key_event.code {
event::KeyCode::F(n) => {}, event::KeyCode::F(_) => todo!(),
event::KeyCode::Char(c) => Mode::process_key(self, c), event::KeyCode::Char(c) => {
// Pretend that the arrow keys are vim movement keys Mode::process_key(self, c);
event::KeyCode::Left => Mode::process_key(self, 'h'), }
event::KeyCode::Right => Mode::process_key(self, 'l'),
event::KeyCode::Up => Mode::process_key(self, 'k'),
event::KeyCode::Down => Mode::process_key(self, 'j'),
// Getting ctrl to work isn't going will right now. Use page keys for the time being.
event::KeyCode::PageUp => self.grid.redo(),
event::KeyCode::PageDown => self.grid.undo(),
event::KeyCode::Modifier(modifier_key_code) => {
if let event::ModifierKeyCode::LeftControl | event::ModifierKeyCode::RightControl = modifier_key_code {
// TODO my terminal (alacritty) isn't showing me ctrl presses. I know
// that they work tho, since ctrl+r works here in neovim.
// panic!("heard ctrl");
}
},
_ => {} _ => {}
}, },
_ => {} _ => {}

View File

@@ -18,7 +18,12 @@ pub struct Clipboard {
impl Clipboard { impl Clipboard {
pub fn new() -> Self { pub fn new() -> Self {
Self { clipboard: Vec::new(), last_paste_cell: (0, 0), momentum: (0, 1), source_cell: (0, 0) } Self {
clipboard: Vec::new(),
last_paste_cell: (0, 0),
momentum: (0, 1),
source_cell: (0, 0),
}
} }
/// Panics if clipboard is 0 length (if you call after you /// Panics if clipboard is 0 length (if you call after you
@@ -44,28 +49,25 @@ impl Clipboard {
// cursor // cursor
let (cx, cy) = into.cursor(); let (cx, cy) = into.cursor();
let cursor = into.cursor(); // iterate thru the clipbaord's cells
into.transact_on_grid(|grid| { for (x, row) in self.clipboard.iter().enumerate() {
// iterate thru the clipbaord's cells for (y, cell) in row.iter().enumerate() {
for (x, row) in self.clipboard.iter().enumerate() { let idx = (x + cx, y + cy);
for (y, cell) in row.iter().enumerate() {
let idx = (x + cx, y + cy);
if translate { if translate {
if let Some(cell) = cell { if let Some(cell) = cell {
let trans = cell.translate_cell(self.source_cell, cursor); let trans = cell.translate_cell(self.source_cell, into.cursor());
grid.set_cell_raw(idx, Some(trans)); into.set_cell_raw(idx, Some(trans));
} else {
// The cell at this location doesn't exist (empty)
grid.set_cell_raw::<CellType>(idx, None);
}
} else { } else {
// translate = false // The cell at this location doesn't exist (empty)
grid.set_cell_raw::<CellType>(idx, cell.clone()); into.set_cell_raw::<CellType>(idx, None);
} }
} else {
// translate = false
into.set_cell_raw::<CellType>(idx, cell.clone());
} }
} }
}); }
let (lx, ly) = self.last_paste_cell; let (lx, ly) = self.last_paste_cell;
self.momentum = (cx as i32 - lx as i32, cy as i32 - ly as i32); self.momentum = (cx as i32 - lx as i32, cy as i32 - ly as i32);
@@ -107,19 +109,16 @@ impl Clipboard {
// size the clipboard appropriately // size the clipboard appropriately
self.clipboard.clear(); self.clipboard.clear();
// clone data into clipboard
from.transact_on_grid(|grid| { for x in low_x..=hi_x {
// clone data into clipboard let mut col = Vec::new();
for x in low_x..=hi_x { for y in low_y..=hi_y {
let mut col = Vec::new(); let a = from.get_cell_raw(x, y);
for y in low_y..=hi_y { col.push(a.clone());
let a = grid.get_cell_raw(x, y); from.set_cell_raw::<CellType>((x, y), None);
col.push(a.clone());
grid.set_cell_raw::<CellType>((x, y), None);
}
self.clipboard.push(col);
} }
}); self.clipboard.push(col);
}
self.last_paste_cell = (low_x, low_y); self.last_paste_cell = (low_x, low_y);
} }
} }
@@ -395,4 +394,5 @@ fn copy_paste_range_in_function() {
let a = app.grid.get_cell("B1").as_ref().expect("Should've been set by paste"); let a = app.grid.get_cell("B1").as_ref().expect("Should've been set by paste");
assert_eq!(a.to_string(), "=sum(A:A)"); assert_eq!(a.to_string(), "=sum(A:A)");
} }

View File

@@ -2,21 +2,20 @@ use std::{
cmp::{max, min}, cmp::{max, min},
fs::{self, File}, fs::{self, File},
io::{Read, Write}, io::{Read, Write},
path::PathBuf, path::PathBuf
}; };
use evalexpr::*; use evalexpr::*;
use crate::app::logic::{ use crate::app::{
calc::internal::CellGrid, logic::{
cell::{CSV_DELIMITER, CellType}, cell::{CSV_DELIMITER, CellType},
ctx, ctx,
}, mode::Mode,
}; };
#[cfg(test)] #[cfg(test)]
use crate::app::app::App; use crate::app::app::App;
#[cfg(test)]
use crate::app::mode::Mode;
pub fn get_header_size() -> usize { pub fn get_header_size() -> usize {
let row_header_width = LEN.to_string().len(); let row_header_width = LEN.to_string().len();
@@ -27,109 +26,13 @@ pub const LEN: usize = 1001;
pub const CSV_EXT: &str = "csv"; pub const CSV_EXT: &str = "csv";
pub const CUSTOM_EXT: &str = "nscim"; pub const CUSTOM_EXT: &str = "nscim";
mod internal {
use crate::app::logic::{calc::LEN, cell::CellType};
#[derive(Clone)]
pub struct CellGrid {
// a b c ...
// 0
// 1
// 2
// ...
cells: Vec<Vec<Option<CellType>>>,
}
impl CellGrid {
pub fn new() -> Self {
let mut a = Vec::with_capacity(LEN);
for _ in 0..LEN {
let mut b = Vec::with_capacity(LEN);
for _ in 0..LEN {
b.push(None)
}
a.push(b)
}
Self { cells: a }
}
pub fn insert_row(&mut self, y: usize) {
for x in 0..LEN {
self.cells[x].insert(y, None);
self.cells[x].pop();
}
}
pub fn insert_column(&mut self, x: usize) {
let mut v = Vec::with_capacity(LEN);
for _ in 0..LEN {
v.push(None);
}
// let clone = self.grid_history[self.current_grid].clone();
self.cells.insert(x, v);
// keep the grid LEN
self.cells.pop();
}
pub fn get_cell_raw(&self, x: usize, y: usize) -> &Option<CellType> {
if x >= LEN || y >= LEN {
return &None;
}
&self.cells[x][y]
}
pub fn set_cell_raw<T: Into<CellType>>(&mut self, (x, y): (usize, usize), val: Option<T>) {
// TODO check oob
self.cells[x][y] = val.map(|v| v.into());
}
/// Iterate over the entire grid and see where
/// the farthest modified cell is.
#[must_use]
pub fn max(&self) -> (usize, usize) {
let mut max_x = 0;
let mut max_y = 0;
for (xi, x) in self.cells.iter().enumerate() {
for (yi, cell) in x.iter().enumerate() {
if cell.is_some() {
if yi > max_y {
max_y = yi
}
if xi > max_x {
max_x = xi
}
}
}
}
(max_x, max_y)
}
#[must_use]
pub fn max_y_at_x(&self, x: usize) -> usize {
let mut max_y = 0;
if let Some(col) = self.cells.get(x) {
// we could do fancy things like .take_while(not null) but then
// we would have to deal with empty cells and stuff, which sounds
// boring. This will be fast "enough", considering the grid is
// probably only like 1k cells
for (yi, cell) in col.iter().enumerate() {
if cell.is_some() {
if yi > max_y {
max_y = yi
}
}
}
}
max_y
}
}
}
pub struct Grid { pub struct Grid {
/// Which grid in history are we currently on // a b c ...
current_grid: usize, // 0
/// An array of grids, thru history // 1
grid_history: Vec<CellGrid>, // 2
// ...
cells: Vec<Vec<Option<CellType>>>,
/// (X, Y) /// (X, Y)
selected_cell: (usize, usize), selected_cell: (usize, usize),
/// Have unsaved modifications been made? /// Have unsaved modifications been made?
@@ -144,9 +47,20 @@ impl std::fmt::Debug for Grid {
impl Grid { impl Grid {
pub fn new() -> Self { pub fn new() -> Self {
let x = CellGrid::new(); let mut a = Vec::with_capacity(LEN);
for _ in 0..LEN {
let mut b = Vec::with_capacity(LEN);
for _ in 0..LEN {
b.push(None)
}
a.push(b)
}
Self { current_grid: 0, grid_history: vec![x], selected_cell: (0, 0), dirty: false } Self {
cells: a,
selected_cell: (0, 0),
dirty: false,
}
} }
pub fn new_from_file(file: &mut File) -> std::io::Result<Self> { pub fn new_from_file(file: &mut File) -> std::io::Result<Self> {
@@ -154,17 +68,14 @@ impl Grid {
let mut buf = String::new(); let mut buf = String::new();
file.read_to_string(&mut buf)?; file.read_to_string(&mut buf)?;
for (yi, line) in buf.lines().enumerate() {
let cells = Self::parse_csv_line(line);
grid.transact_on_grid(|grid| { for (xi, cell) in cells.into_iter().enumerate() {
for (yi, line) in buf.lines().enumerate() { // This gets automatically duck-typed
let cells = Self::parse_csv_line(line); grid.set_cell_raw((xi, yi), cell);
for (xi, cell) in cells.into_iter().enumerate() {
// This gets automatically duck-typed
grid.set_cell_raw((xi, yi), cell);
}
} }
}); }
// force dirty back off, we just read the data so it's gtg // force dirty back off, we just read the data so it's gtg
grid.dirty = false; grid.dirty = false;
@@ -201,15 +112,19 @@ impl Grid {
} }
let mut f = fs::OpenOptions::new().write(true).append(false).truncate(true).create(true).open(path)?; let mut f = fs::OpenOptions::new().write(true).append(false).truncate(true).create(true).open(path)?;
let (mx, my) = self.get_grid().max(); let (mx, my) = self.max();
for y in 0..=my { for y in 0..=my {
for x in 0..=mx { for x in 0..=mx {
let cell = &self.get_grid().get_cell_raw(x, y); let cell = &self.cells[x][y];
// newline after the cell, because it's end of line. // newline after the cell, because it's end of line.
// else, just put a comma after the cell. // else, just put a comma after the cell.
let is_last = x == mx; let is_last = x==mx;
let delim = if is_last { '\n' } else { CSV_DELIMITER }; let delim = if is_last {
'\n'
} else {
CSV_DELIMITER
};
let data = if let Some(cell) = cell { let data = if let Some(cell) = cell {
if let Ok(val) = self.evaluate(&cell.to_string()) if let Ok(val) = self.evaluate(&cell.to_string())
@@ -231,40 +146,6 @@ impl Grid {
Ok(()) Ok(())
} }
pub fn get_grid<'a>(&'a self) -> &'a CellGrid {
&self.grid_history[self.current_grid]
}
fn get_grid_mut<'a>(&'a mut self) -> &'a mut CellGrid {
&mut self.grid_history[self.current_grid]
}
pub fn undo(&mut self) {
self.current_grid = self.current_grid.saturating_sub(1);
}
pub fn redo(&mut self) {
self.current_grid = min(self.grid_history.len() - 1, self.current_grid + 1);
}
pub fn transact_on_grid<F>(&mut self, mut action: F)
where
F: FnMut(&mut CellGrid) -> (),
{
// push on a new reality
let new = self.get_grid().clone();
self.grid_history.push(new);
self.current_grid += 1;
// delete the other fork of the history
for i in self.current_grid + 1..self.grid_history.len() {
self.grid_history.remove(i);
}
action(&mut self.grid_history[self.current_grid]);
self.dirty = true;
}
pub fn needs_to_be_saved(&self) -> bool { pub fn needs_to_be_saved(&self) -> bool {
self.dirty self.dirty
} }
@@ -371,55 +252,101 @@ impl Grid {
} }
pub fn insert_row_above(&mut self, (_x, insertion_y): (usize, usize)) { pub fn insert_row_above(&mut self, (_x, insertion_y): (usize, usize)) {
self.transact_on_grid(|grid: &mut CellGrid| { for x in 0..LEN {
grid.insert_row(insertion_y); self.cells[x].insert(insertion_y, None);
for x in 0..LEN { self.cells[x].pop();
for y in 0..LEN { for y in 0..LEN {
if let Some(cell) = grid.get_cell_raw(x, y).as_ref().map(|f| { if let Some(cell) = self.get_cell_raw(x, y).as_ref().map(|f| {
f.custom_translate_cell((0, 0), (0, 1), |rolling, old, new| { f.custom_translate_cell((0, 0), (0, 1), |rolling, old, new| {
if let Some((_, arg_y)) = Grid::parse_to_idx(old) { if let Some((_, arg_y)) = Grid::parse_to_idx(old) {
if arg_y < insertion_y { rolling.to_owned() } else { rolling.replace(old, new) } if arg_y < insertion_y { rolling.to_owned() } else { rolling.replace(old, new) }
} else { } else {
unimplemented!("Invalid variable wanted to be translated") unimplemented!("Invalid variable wanted to be translated")
} }
})
}) {
grid.set_cell_raw((x, y), Some(cell));
} }
)}) {
self.set_cell_raw((x,y), Some(cell));
} }
} }
}); }
} }
pub fn insert_row_below(&mut self, (x, y): (usize, usize)) { pub fn insert_row_below(&mut self, (x, y): (usize, usize)) {
self.insert_row_above((x, y + 1)); self.insert_row_above((x,y+1));
} }
pub fn insert_column_before(&mut self, (insertion_x, _y): (usize, usize)) { pub fn insert_column_before(&mut self, (insertion_x, _y): (usize, usize)) {
self.transact_on_grid(|grid| { let mut v = Vec::with_capacity(LEN);
grid.insert_column(insertion_x); for _ in 0..LEN {
for x in 0..LEN { v.push(None);
for y in 0..LEN { }
if let Some(cell) = grid.get_cell_raw(x, y).as_ref().map(|f| { self.cells.insert(insertion_x, v);
f.custom_translate_cell((0, 0), (1, 0), |rolling, old, new| { // keep the grid LEN
if let Some((arg_x, _)) = Grid::parse_to_idx(old) { self.cells.pop();
// add 1 because of the insertion for x in 0..LEN {
if arg_x < insertion_x { rolling.to_owned() } else { rolling.replace(old, new) } for y in 0..LEN {
} else { if let Some(cell) = self.get_cell_raw(x, y).as_ref().map(|f| {
unimplemented!("Invalid variable wanted to be translated") f.custom_translate_cell((0, 0), (1, 0), |rolling, old, new| {
} if let Some((arg_x, _)) = Grid::parse_to_idx(old) {
}) // add 1 because of the insertion
}) { if arg_x < insertion_x { rolling.to_owned() } else { rolling.replace(old, new) }
grid.set_cell_raw((x, y), Some(cell)); } else {
} // FIXME could be a range
unimplemented!("Invalid variable wanted to be translated")
}
})
}) {
self.set_cell_raw((x, y), Some(cell));
} }
} }
}); }
} }
pub fn insert_column_after(&mut self, (x, y): (usize, usize)) { pub fn insert_column_after(&mut self, (x, y): (usize, usize)) {
self.insert_column_before((x + 1, y)); self.insert_column_before((x + 1, y));
} }
/// Iterate over the entire grid and see where
/// the farthest modified cell is.
#[must_use]
fn max(&self) -> (usize, usize) {
let mut max_x = 0;
let mut max_y = 0;
for (xi, x) in self.cells.iter().enumerate() {
for (yi, cell) in x.iter().enumerate() {
if cell.is_some() {
if yi > max_y {
max_y = yi
}
if xi > max_x {
max_x = xi
}
}
}
}
(max_x, max_y)
}
#[must_use]
pub fn max_y_at_x(&self, x: usize) -> usize {
let mut max_y = 0;
if let Some(col) = self.cells.get(x) {
// we could do fancy things like .take_while(not null) but then
// we would have to deal with empty cells and stuff, which sounds
// boring. This will be fast "enough", considering the grid is
// probably only like 1k cells
for (yi, cell) in col.iter().enumerate() {
if cell.is_some() {
if yi > max_y {
max_y = yi
}
}
}
}
max_y
}
/// Only evaluates equations, such as `=10` or `=A1/C2`, not /// Only evaluates equations, such as `=10` or `=A1/C2`, not
/// strings or numbers. /// strings or numbers.
@@ -458,7 +385,10 @@ impl Grid {
EvalexprError::VariableIdentifierNotFound(var_not_found) => { EvalexprError::VariableIdentifierNotFound(var_not_found) => {
return Err(format!("\"{var_not_found}\" is not a variable")); return Err(format!("\"{var_not_found}\" is not a variable"));
} }
EvalexprError::TypeError { expected: e, actual: a } => { EvalexprError::TypeError {
expected: e,
actual: a,
} => {
// IE: You put a string into a function that wants a float // IE: You put a string into a function that wants a float
return Err(format!("Wanted {e:?}, got {a}")); return Err(format!("Wanted {e:?}, got {a}"));
} }
@@ -467,6 +397,20 @@ impl Grid {
} }
} }
pub fn range_as_indices(range: &str) -> Option<(usize, usize)> {
let v = range.split(':').collect::<Vec<&str>>();
if v.len() == 2 {
let start_col = v[0];
let end_col = v[1];
let start_idx = Grid::char_to_idx(start_col);
let end_idx = Grid::char_to_idx(end_col);
return Some((start_idx, end_idx))
}
None
}
pub fn char_to_idx(i: &str) -> usize { pub fn char_to_idx(i: &str) -> usize {
let x_idx = i let x_idx = i
.chars() .chars()
@@ -503,13 +447,15 @@ impl Grid {
/// Helper for tests /// Helper for tests
#[cfg(test)] #[cfg(test)]
/// Don't ever remove this from being just a test-helper.
/// This function doesn't correctly use the undo/redo api, which would require doing
/// transactions on the grid instead of direct access.
pub fn set_cell<T: Into<CellType>>(&mut self, cell_id: &str, val: T) { pub fn set_cell<T: Into<CellType>>(&mut self, cell_id: &str, val: T) {
if let Some(loc) = Self::parse_to_idx(cell_id) { if let Some(loc) = Self::parse_to_idx(cell_id) {
self.get_grid_mut().set_cell_raw(loc, Some(val)) self.set_cell_raw(loc, Some(val));
} }
}
pub fn set_cell_raw<T: Into<CellType>>(&mut self, (x, y): (usize, usize), val: Option<T>) {
// TODO check oob
self.cells[x][y] = val.map(|v| v.into());
self.dirty = true; self.dirty = true;
} }
@@ -528,7 +474,7 @@ impl Grid {
if x >= LEN || y >= LEN { if x >= LEN || y >= LEN {
return &None; return &None;
} }
&self.get_grid().get_cell_raw(x, y) &self.cells[x][y]
} }
pub fn num_to_char(idx: usize) -> String { pub fn num_to_char(idx: usize) -> String {
@@ -646,7 +592,7 @@ fn saving_neoscim() {
fn cell_strings() { fn cell_strings() {
let mut grid = Grid::new(); let mut grid = Grid::new();
assert!(&grid.get_grid().get_cell_raw(0, 0).is_none()); assert!(&grid.cells[0][0].is_none());
grid.set_cell("A0", "Hello".to_string()); grid.set_cell("A0", "Hello".to_string());
assert!(grid.get_cell("A0").is_some()); assert!(grid.get_cell("A0").is_some());
@@ -783,17 +729,17 @@ fn grid_max() {
let mut grid = Grid::new(); let mut grid = Grid::new();
grid.set_cell("A0", 1.); grid.set_cell("A0", 1.);
let (mx, my) = grid.get_grid().max(); let (mx, my) = grid.max();
assert_eq!(mx, 0); assert_eq!(mx, 0);
assert_eq!(my, 0); assert_eq!(my, 0);
grid.set_cell("B0", 1.); grid.set_cell("B0", 1.);
let (mx, my) = grid.get_grid().max(); let (mx, my) = grid.max();
assert_eq!(mx, 1); assert_eq!(mx, 1);
assert_eq!(my, 0); assert_eq!(my, 0);
grid.set_cell("B5", 1.); grid.set_cell("B5", 1.);
let (mx, my) = grid.get_grid().max(); let (mx, my) = grid.max();
assert_eq!(mx, 1); assert_eq!(mx, 1);
assert_eq!(my, 5); assert_eq!(my, 5);
} }
@@ -1078,6 +1024,36 @@ fn insert_col_before_3() {
assert_eq!(cell.to_string(), "=B0*B1"); assert_eq!(cell.to_string(), "=B0*B1");
} }
#[test]
fn insert_col_before_static_range() {
let mut grid = Grid::new();
grid.set_cell("A0", 2.);
grid.set_cell("A1", 2.);
grid.set_cell("B0", "=sum(A:A)".to_string());
grid.mv_cursor_to(0, 1);
grid.insert_column_before(grid.cursor());
let cell = grid.get_cell("C0").as_ref().expect("Just set it");
assert_eq!(cell.to_string(), "=sum(A:A)");
}
#[test]
fn insert_col_before_move_range() {
let mut grid = Grid::new();
grid.set_cell("B0", 2.);
grid.set_cell("B1", 2.);
grid.set_cell("A0", "=sum(B:B)".to_string());
grid.mv_cursor_to(0, 0);
grid.insert_column_after(grid.cursor());
let cell = grid.get_cell("A0").as_ref().expect("Just set it");
assert_eq!(cell.to_string(), "=sum(C:C)");
}
#[test] #[test]
fn insert_row_above_1() { fn insert_row_above_1() {
let mut grid = Grid::new(); let mut grid = Grid::new();
@@ -1144,7 +1120,7 @@ fn insert_row_above_3() {
#[test] #[test]
fn cell_eval_depth() { fn cell_eval_depth() {
use crate::app::mode::*; use crate::app::mode::*;
let mut app = App::new(); let mut app= App::new();
app.grid.set_cell("A0", 1.); app.grid.set_cell("A0", 1.);
app.grid.set_cell("A1", "=A0+$A$0".to_string()); app.grid.set_cell("A1", "=A0+$A$0".to_string());
@@ -1194,3 +1170,4 @@ fn return_string_from_fn() {
} }
} }
} }

View File

@@ -16,26 +16,12 @@ pub struct CallbackContext<'a> {
impl<'a> CallbackContext<'a> { impl<'a> CallbackContext<'a> {
fn expand_range(&self, range: &str) -> Option<Vec<&CellType>> { fn expand_range(&self, range: &str) -> Option<Vec<&CellType>> {
let v = range.split(':').collect::<Vec<&str>>(); if let Some((start, end)) = Grid::range_as_indices(range) {
if v.len() == 2 {
let start_col = v[0];
let end_col = v[1];
let as_index = |s: &str| {
s.char_indices()
// .filter(|f| f.1 as u8 >= 97) // prevent sub with overflow errors
.map(|(idx, c)| ((c.to_ascii_lowercase() as usize).saturating_sub(97)) + (26 * idx))
.fold(0, |a, b| a + b)
};
let start_idx = as_index(start_col);
let end_idx = as_index(end_col);
let mut buf = Vec::new(); let mut buf = Vec::new();
for x in start_idx..=end_idx { for x in start..=end {
for y in 0..=self.variables.get_grid().max_y_at_x(x) { for y in 0..=self.variables.max_y_at_x(x) {
if let Some(s) = self.variables.get_grid().get_cell_raw(x, y) { if let Some(s) = self.variables.get_cell_raw(x, y) {
buf.push(s); buf.push(s);
} }
} }

View File

@@ -2,8 +2,7 @@ use std::{
cmp::{max, min}, cmp::{max, min},
fmt::Display, fmt::Display,
fs, fs,
path::PathBuf, path::PathBuf, process::Command,
process::Command,
}; };
use ratatui::{ use ratatui::{
@@ -154,13 +153,11 @@ impl Mode {
let mut save_range = |to: &str| { let mut save_range = |to: &str| {
let mut g = Grid::new(); let mut g = Grid::new();
g.transact_on_grid(|grid| { for (i, x) in (low_x..=hi_x).enumerate() {
for (i, x) in (low_x..=hi_x).enumerate() { for (j, y) in (low_y..=hi_y).enumerate() {
for (j, y) in (low_y..=hi_y).enumerate() { g.set_cell_raw((i, j), app.grid.get_cell_raw(x, y).clone());
grid.set_cell_raw((i, j), grid.get_cell_raw(x, y).clone());
}
} }
}); }
if let Err(_e) = g.save_to(to) { if let Err(_e) = g.save_to(to) {
app.msg = StatusMessage::error("Failed to save file"); app.msg = StatusMessage::error("Failed to save file");
} }
@@ -174,24 +171,22 @@ impl Mode {
} }
} }
} }
return "unknown"; return "unknown"
}; };
match args[0] { match args[0] {
"f" | "fill" => { "f" | "fill" => {
app.grid.transact_on_grid(|grid| { for (i, x) in (low_x..=hi_x).enumerate() {
for (i, x) in (low_x..=hi_x).enumerate() { for (j, y) in (low_y..=hi_y).enumerate() {
for (j, y) in (low_y..=hi_y).enumerate() { let arg = args.get(1)
let arg = args .map(|s| s.replace("xi", &i.to_string()))
.get(1) .map(|s| s.replace("yi", &j.to_string()))
.map(|s| s.replace("xi", &i.to_string())) .map(|s| s.replace("x", &x.to_string()))
.map(|s| s.replace("yi", &j.to_string())) .map(|s| s.replace("y", &y.to_string()))
.map(|s| s.replace("x", &x.to_string())) ;
.map(|s| s.replace("y", &y.to_string())); app.grid.set_cell_raw((x,y), arg);
grid.set_cell_raw((x, y), arg);
}
} }
}); }
app.mode = Mode::Normal app.mode = Mode::Normal
} }
@@ -207,7 +202,11 @@ impl Mode {
// Use gnuplot to plot the selected data. // Use gnuplot to plot the selected data.
// * Temp data will be stored in /tmp/ // * Temp data will be stored in /tmp/
// * Output will either be plot.png or a name that you pass in // * Output will either be plot.png or a name that you pass in
let output_filename = if let Some(arg1) = args.get(1) { arg1 } else { "plot.png" }; let output_filename = if let Some(arg1) = args.get(1) {
arg1
} else {
"plot.png"
};
save_range("/tmp/plot.csv"); save_range("/tmp/plot.csv");
let plot = include_str!("../../template.gnuplot"); let plot = include_str!("../../template.gnuplot");
@@ -218,12 +217,10 @@ impl Mode {
let s = s.replace("$OUTPUT", "/tmp/output.png"); let s = s.replace("$OUTPUT", "/tmp/output.png");
let _ = fs::write("/tmp/plot.p", s); let _ = fs::write("/tmp/plot.p", s);
let cmd_res = Command::new("gnuplot").arg("/tmp/plot.p").output(); let cmd_res= Command::new("gnuplot").arg("/tmp/plot.p").output();
if let Err(err) = cmd_res { if let Err(err) = cmd_res {
match err.kind() { match err.kind() {
std::io::ErrorKind::NotFound => { std::io::ErrorKind::NotFound => app.msg = StatusMessage::error("Error - Is gnuplot installed?"),
app.msg = StatusMessage::error("Error - Is gnuplot installed?")
}
_ => app.msg = StatusMessage::error(format!("{err}")), _ => app.msg = StatusMessage::error(format!("{err}")),
}; };
} else { } else {
@@ -279,7 +276,7 @@ impl Mode {
// Go to bottom of column // Go to bottom of column
'G' => { 'G' => {
let (x, _) = app.grid.cursor(); let (x, _) = app.grid.cursor();
app.grid.mv_cursor_to(x, super::logic::calc::LEN); app.grid.mv_cursor_to(x, super::logic::calc::LEN,);
return; return;
} }
// edit cell // edit cell
@@ -322,11 +319,6 @@ impl Mode {
app.mode = Mode::Command(Chord::new(':')) app.mode = Mode::Command(Chord::new(':'))
} }
} }
// undo
'u' => {
app.grid.undo();
}
// paste
'p' => { 'p' => {
app.clipboard.paste(&mut app.grid, true); app.clipboard.paste(&mut app.grid, true);
app.grid.apply_momentum(app.clipboard.momentum()); app.grid.apply_momentum(app.clipboard.momentum());
@@ -406,7 +398,7 @@ impl Mode {
let (_, y_height) = app.screen.get_screen_size(&app.vars); let (_, y_height) = app.screen.get_screen_size(&app.vars);
let y_origin = app.screen.scroll_y(); let y_origin = app.screen.scroll_y();
app.grid.mv_cursor_to(x, y_origin + y_height); app.grid.mv_cursor_to(x, y_origin+y_height);
app.mode = Mode::Normal; app.mode = Mode::Normal;
return; return;
} }
@@ -416,7 +408,7 @@ impl Mode {
let (x_width, _) = app.screen.get_screen_size(&app.vars); let (x_width, _) = app.screen.get_screen_size(&app.vars);
let x_origin = app.screen.scroll_x(); let x_origin = app.screen.scroll_x();
app.grid.mv_cursor_to(x_origin + x_width, y); app.grid.mv_cursor_to(x_origin+x_width, y);
app.mode = Mode::Normal; app.mode = Mode::Normal;
} }
// Go to the left edge of the current window // Go to the left edge of the current window
@@ -516,7 +508,9 @@ pub struct Chord {
impl From<String> for Chord { impl From<String> for Chord {
fn from(value: String) -> Self { fn from(value: String) -> Self {
let b = value.as_bytes().iter().map(|f| *f as char).collect(); let b = value.as_bytes().iter().map(|f| *f as char).collect();
Chord { buf: b } Chord {
buf: b,
}
} }
} }
@@ -525,7 +519,9 @@ impl Chord {
let mut buf = Vec::new(); let mut buf = Vec::new();
buf.push(inital); buf.push(inital);
Self { buf } Self {
buf,
}
} }
pub fn backspace(&mut self) { pub fn backspace(&mut self) {