Compare commits

19 Commits

Author SHA1 Message Date
9de22b9680 lol
All checks were successful
Test Rust project / test (ubuntu-latest, stable) (push) Successful in 46s
2026-02-09 14:16:35 -07:00
3f336578f5 Continue on #53, solve failing test
All checks were successful
Test Rust project / test (ubuntu-latest, stable) (push) Successful in 46s
2026-02-09 13:33:56 -07:00
5fbff13428 close #6
Some checks failed
Test Rust project / test (ubuntu-latest, stable) (push) Failing after 1m15s
2026-02-09 13:24:09 -07:00
7a23ee5bc0 don't let user go oob 2026-02-09 13:14:44 -07:00
ea2e633d7d Merge pull request 'Close #43' (#54) from issue-43 into master
All checks were successful
Test Rust project / test (ubuntu-latest, stable) (push) Successful in 48s
Reviewed-on: #54
2026-02-09 20:00:38 +00:00
d9f29434e9 Close #43
All checks were successful
Test Rust project / test (ubuntu-latest, stable) (push) Successful in 49s
This takes rendering time from ~0.5ms to ~0.5ms.
When highlighting a range it may get up to 1.5ms.
2026-02-09 12:58:49 -07:00
4239844e0e clippy styling
Some checks failed
Test Rust project / test (ubuntu-latest, stable) (push) Failing after 2m29s
2026-02-09 11:54:22 -07:00
5f9cd85faf work on #53
All checks were successful
Test Rust project / test (ubuntu-latest, stable) (push) Successful in 1m5s
fix adding rows with ranges
2026-02-09 11:11:24 -07:00
74955032cc document keybinds
All checks were successful
Test Rust project / test (ubuntu-latest, stable) (push) Successful in 47s
2026-02-08 22:52:16 -07:00
7b2eb751ab ranges now move correctly with column inserts
All checks were successful
Test Rust project / test (ubuntu-latest, stable) (push) Successful in 1m1s
2026-02-08 22:48:12 -07:00
b41a69781c merge cleanup
Some checks failed
Test Rust project / test (ubuntu-latest, stable) (push) Failing after 39s
2026-02-07 20:55:05 -07:00
f654ce37a6 Merge branch 'full-copy-history' 2026-02-07 20:52:14 -07:00
d5d58694bb redo
All checks were successful
Test Rust project / test (ubuntu-latest, stable) (push) Successful in 43s
Undo / Redo are currently bound to Pageup/down. This is ideally temporary, I just need to figure out ctrl+r for redo. "u" is also bound to undo as it should be
2026-02-07 20:46:22 -07:00
c2e0661a45 solve #29 2026-02-07 20:37:01 -07:00
6ec7d90ac5 undo 2026-02-07 20:30:19 -07:00
53dcf2ffc9 mock up history
All checks were successful
Test Rust project / test (ubuntu-latest, stable) (push) Successful in 56s
2026-02-06 17:04:12 -07:00
86756a94ef deprecate old function 2026-02-06 16:55:50 -07:00
d242e1af21 apply api changes 2026-02-06 16:46:15 -07:00
abffe6073f setup history of file 2026-02-06 15:55:27 -07:00
10 changed files with 541 additions and 356 deletions

View File

@@ -25,6 +25,8 @@ Based loosely off sc-im (spreadsheet calculator improvised), which has dumb keyb
| `gg` | Go to beginning of the column | | `gg` | Go to beginning of the column |
| `G` | Go to end of column | | `G` | Go to end of column |
| `gG` | Go to end of the the visual column | | `gG` | Go to end of the the visual column |
| `u`/`Page Down` | Undo[^undo] |
| `Page Up` | Redo[^undo] |
| `i`/`a` | Enter insert mode on current cell | | `i`/`a` | Enter insert mode on current cell |
| `r` | Enter insert mode on current cell, deleting contents | | `r` | Enter insert mode on current cell, deleting contents |
| `v` | Enter visual mode | | `v` | Enter visual mode |
@@ -43,6 +45,8 @@ Based loosely off sc-im (spreadsheet calculator improvised), which has dumb keyb
| n`G` | Jump to row "n" | | n`G` | Jump to row "n" |
| nX | Press "X", "n" times | | nX | Press "X", "n" times |
[^undo]: Page up/down keybinds are (probably) temporary until I can get ctrl+r to work for redo, which is the propper key for this action. See issue #25 for the status on this.
### Visual mode ### Visual mode
| Key | Action | | Key | Action |

View File

@@ -18,7 +18,10 @@ use ratatui::{
use crate::app::{ use crate::app::{
clipboard::Clipboard, clipboard::Clipboard,
error_msg::StatusMessage, error_msg::StatusMessage,
logic::{calc::{Grid, get_header_size}, cell::CellType}, logic::{
calc::{Grid, LEN, get_header_size},
cell::CellType,
},
mode::Mode, mode::Mode,
screen::ScreenSpace, screen::ScreenSpace,
}; };
@@ -39,10 +42,12 @@ pub struct App {
impl Widget for &App { impl Widget for &App {
fn render(self, area: prelude::Rect, buf: &mut prelude::Buffer) { fn render(self, area: prelude::Rect, buf: &mut prelude::Buffer) {
// let now = std::time::Instant::now();
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_visually_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;
@@ -60,30 +65,70 @@ impl Widget for &App {
false false
}; };
// cells that are related by reference to the cursor's cell
// (inputs to formulas and such)
let cells_of_interest: Vec<(usize, usize)> = {
let ctx = crate::app::logic::ctx::ExtractionContext::new();
let (x, y) = self.grid.cursor();
if let Some(cell) = self.grid.get_cell_raw(x, y) {
if let CellType::Equation(eq) = cell {
let _ = evalexpr::eval_with_context(&eq[1..], &ctx);
let vars = ctx.dump_vars();
let mut interest = Vec::new();
for var in vars {
if let Some(a) = Grid::parse_to_idx(&var) {
interest.push(a);
} else if let Some((start, end)) = Grid::range_as_indices(&var) {
// insert coords:
// (start, 0..len)
// ..
// (end, 0..len)
for x in start..=end {
for y in 0..=super::logic::calc::LEN {
interest.push((x,y))
}
}
}
}
interest
} else {
Vec::new()
}
} else {
Vec::new()
}
};
// Custom width for the header of each row
let row_header_width = get_header_size() as u16;
// ^^ Feels like it oculd be static but evaluating string lens doesn't work at
// compile time. Thus cannot be static.
let cell_width = self.screen.get_cell_width(&self.vars) as u16;
let cell_height = self.screen.get_cell_height(&self.vars) as u16;
for x in 0..x_max { for x in 0..x_max {
for y in 0..y_max { for y in 0..y_max {
let mut display = String::new(); let mut display = String::new();
let mut style = Style::new(); let mut style = Style::new();
// Custom width for the header of each row
let row_header_width = get_header_size() as u16;
// ^^ Feels like it oculd be static but evaluating string lens doesn't work at
// compile time. Thus cannot be static.
let cell_width = self.screen.get_cell_width(&self.vars) as u16;
let cell_height = self.screen.get_cell_height(&self.vars) as u16;
// Minus 1 because of header cells, // Minus 1 because of header cells,
// the grid is shifted over (1,1), so if you need // the grid is shifted over (1,1), so if you need
// to index the grid, these are you values. // to index the grid, these are you values.
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();
} }
// don't render non-accessible cells
if x_idx > LEN-1 {
continue;
}
const ORANGE1: Color = Color::Rgb(200, 160, 0); const ORANGE1: Color = Color::Rgb(200, 160, 0);
const ORANGE2: Color = Color::Rgb(180, 130, 0); const ORANGE2: Color = Color::Rgb(180, 130, 0);
@@ -93,9 +138,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).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))).map(|_| ' ').collect::<String>();
format!("{l_margin}{text}{r_margin}") format!("{l_margin}{text}{r_margin}")
} }
@@ -111,7 +156,7 @@ impl Widget for &App {
let bg = if y_idx == self.grid.cursor().1 { let bg = if y_idx == self.grid.cursor().1 {
Color::DarkGray Color::DarkGray
} else if y_idx % 2 == 0 { } else if y_idx.is_multiple_of(2) {
ORANGE1 ORANGE1
} else { } else {
ORANGE2 ORANGE2
@@ -124,7 +169,7 @@ impl Widget for &App {
let bg = if x_idx == self.grid.cursor().0 { let bg = if x_idx == self.grid.cursor().0 {
Color::DarkGray Color::DarkGray
} else if x_idx % 2 == 0 { } else if x_idx.is_multiple_of(2) {
ORANGE1 ORANGE1
} else { } else {
ORANGE2 ORANGE2
@@ -166,11 +211,12 @@ impl Widget for &App {
} }
} }
// ===================================================
// Allow for text in one cell to visually overflow into empty cells // Allow for text in one cell to visually overflow into empty cells
suggest_upper_bound = Some(display.len() as u16); suggest_upper_bound = Some(display.len() as u16);
// check for cells to the right, see if we should truncate the cell width // check for cells to the right, see if we should truncate the cell width
for i in 1..(display.len() as f32 / cell_width as f32).ceil() as usize { for i in 1..(display.len() as f32 / cell_width as f32).ceil() as usize {
if let Some(_) = self.grid.get_cell_raw(x_idx + i, y_idx) { if self.grid.get_cell_raw(x_idx + i, y_idx).is_some() {
suggest_upper_bound = Some(cell_width * i as u16); suggest_upper_bound = Some(cell_width * i as u16);
break; break;
} }
@@ -182,18 +228,25 @@ impl Widget for &App {
display.push('…'); display.push('…');
} }
} }
// ===================================================
} }
// Don't render blank cells // Don't render blank cells
None => should_render = false, None => should_render = false,
} }
if is_selected(x.into(), y.into()) { if cells_of_interest.contains(&(x_idx, y_idx)) {
style = style.fg(Color::Yellow);
should_render = true;
}
if is_visually_selected(x.into(), y.into()) {
style = style.bg(Color::Blue); style = style.bg(Color::Blue);
// Make it so that cells render when selected. This fixes issue #32 // Make it so that cells render when selected. This fixes issue #32
should_render = true; should_render = true;
} }
if (x_idx, y_idx) == self.grid.cursor() { if (x_idx, y_idx) == self.grid.cursor() {
should_render = true; should_render = true;
style = Style::new().fg(Color::Black).bg(Color::White); style = Style::new().fg(Color::Black).bg(Color::White);
// modify the style of the cell you are editing // modify the style of the cell you are editing
if let Mode::Insert(_) = self.mode { if let Mode::Insert(_) = self.mode {
@@ -214,12 +267,12 @@ 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;
// draw the biggest cell possible, without going OOB off the screen // draw the biggest cell possible, without going OOB off the screen
let width = min(max_available_width, suggestion as u16); let width = min(max_available_width, suggestion);
// Don't draw too small tho, we want full-sized cells, minium // Don't draw too small tho, we want full-sized cells, minium
let width = max(cell_width, width); let width = max(cell_width, width);
@@ -232,6 +285,9 @@ impl Widget for &App {
} }
} }
} }
// let ns = now.elapsed().as_nanos() as f64;
// eprintln!("Rendered in {}ms", ns/1_000_000.);
} }
} }
@@ -277,22 +333,19 @@ impl App {
} }
fn file_name_display(&self) -> String { fn file_name_display(&self) -> String {
let file_name_status = { let mut file_name = "[No Name]";
let mut file_name = "[No Name]"; let mut icon = "";
let mut icon = ""; if let Some(file) = &self.file {
if let Some(file) = &self.file { if let Some(f) = file.file_name() {
if let Some(f) = file.file_name() { if let Some(f) = f.to_str() {
if let Some(f) = f.to_str() { file_name = f;
file_name = f;
}
} }
} }
if self.grid.needs_to_be_saved() { }
icon = "[+]"; if self.grid.needs_to_be_saved() {
} icon = "[+]";
format!("{file_name}{icon}") }
}; format!("{file_name}{icon}")
file_name_status
} }
fn draw(&self, frame: &mut Frame) { fn draw(&self, frame: &mut Frame) {
@@ -323,6 +376,8 @@ impl App {
let cmd_line_left = cmd_line_split[0]; let cmd_line_left = cmd_line_split[0];
let cmd_line_status = cmd_line_split[1]; let cmd_line_status = cmd_line_split[1];
let cmd_line_right = cmd_line_split[2]; let cmd_line_right = cmd_line_split[2];
#[cfg(debug_assertions)]
let cmd_line_debug = cmd_line_split[3]; let cmd_line_debug = cmd_line_split[3];
// ====================================================== // ======================================================
@@ -357,7 +412,7 @@ impl App {
event::KeyCode::Char(c) => chord.add_char(c), event::KeyCode::Char(c) => chord.add_char(c),
event::KeyCode::Enter => { event::KeyCode::Enter => {
// tmp is to get around reference issues. // tmp is to get around reference issues.
let tmp = pos.clone(); let tmp = *pos;
Mode::process_cmd(self); Mode::process_cmd(self);
self.mode = Mode::Visual(tmp) self.mode = Mode::Visual(tmp)
} }
@@ -387,17 +442,20 @@ impl App {
event::KeyCode::Enter => { event::KeyCode::Enter => {
let v = editor.as_string(); let v = editor.as_string();
// try to insert as a float let cursor = self.grid.cursor();
if let Ok(v) = v.parse::<f64>() { self.grid.transact_on_grid(|grid| {
self.grid.set_cell_raw(self.grid.cursor(), Some(v)); // try to insert as a float
} else { if let Ok(v) = v.parse::<f64>() {
// if you can't, then insert as a string grid.set_cell_raw(cursor, Some(v));
if !v.is_empty() {
self.grid.set_cell_raw(self.grid.cursor(), Some(v));
} else { } else {
self.grid.set_cell_raw::<CellType>(self.grid.cursor(), None); // if you can't, then insert as a string
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;
} }
@@ -413,9 +471,24 @@ 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(_) => todo!(), event::KeyCode::F(_n) => {}
event::KeyCode::Char(c) => { event::KeyCode::Char(c) => Mode::process_key(self, c),
Mode::process_key(self, c); // Pretend that the arrow keys are vim movement keys
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,12 +18,7 @@ pub struct Clipboard {
impl Clipboard { impl Clipboard {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self { clipboard: Vec::new(), last_paste_cell: (0, 0), momentum: (0, 1), source_cell: (0, 0) }
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
@@ -49,25 +44,28 @@ impl Clipboard {
// cursor // cursor
let (cx, cy) = into.cursor(); let (cx, cy) = into.cursor();
// iterate thru the clipbaord's cells let cursor = into.cursor();
for (x, row) in self.clipboard.iter().enumerate() { into.transact_on_grid(|grid| {
for (y, cell) in row.iter().enumerate() { // iterate thru the clipbaord's cells
let idx = (x + cx, y + cy); for (x, row) in self.clipboard.iter().enumerate() {
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, into.cursor()); let trans = cell.translate_cell(self.source_cell, cursor);
into.set_cell_raw(idx, Some(trans)); grid.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 {
// The cell at this location doesn't exist (empty) // translate = false
into.set_cell_raw::<CellType>(idx, None); grid.set_cell_raw::<CellType>(idx, cell.clone());
} }
} 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);
@@ -109,16 +107,19 @@ impl Clipboard {
// size the clipboard appropriately // size the clipboard appropriately
self.clipboard.clear(); self.clipboard.clear();
// clone data into clipboard
for x in low_x..=hi_x { from.transact_on_grid(|grid| {
let mut col = Vec::new(); // clone data into clipboard
for y in low_y..=hi_y { for x in low_x..=hi_x {
let a = from.get_cell_raw(x, y); let mut col = Vec::new();
col.push(a.clone()); for y in low_y..=hi_y {
from.set_cell_raw::<CellType>((x, y), None); let a = grid.get_cell_raw(x, y);
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);
} }
} }
@@ -394,5 +395,4 @@ 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

@@ -47,7 +47,7 @@ impl Widget for &StatusMessage {
let msg = if self.start.elapsed().as_secs() > 3 { let msg = if self.start.elapsed().as_secs() > 3 {
String::new() String::new()
} else { } else {
self.msg.clone().unwrap_or(String::new()) self.msg.clone().unwrap_or_default()
}; };
let style = match self.msg_type { let style = match self.msg_type {

View File

@@ -2,37 +2,134 @@ 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::{ use crate::app::logic::{
logic::{ calc::internal::CellGrid,
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(); LEN.to_string().len()
row_header_width
} }
pub const LEN: usize = 1001; 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 {
// a b c ... /// Which grid in history are we currently on
// 0 current_grid: usize,
// 1 /// An array of grids, thru history
// 2 grid_history: Vec<CellGrid>,
// ...
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?
@@ -47,20 +144,9 @@ impl std::fmt::Debug for Grid {
impl Grid { impl Grid {
pub fn new() -> Self { pub fn new() -> Self {
let mut a = Vec::with_capacity(LEN); let x = CellGrid::new();
for _ in 0..LEN {
let mut b = Vec::with_capacity(LEN);
for _ in 0..LEN {
b.push(None)
}
a.push(b)
}
Self { Self { current_grid: 0, grid_history: vec![x], selected_cell: (0, 0), dirty: false }
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> {
@@ -68,14 +154,17 @@ 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);
for (xi, cell) in cells.into_iter().enumerate() { grid.transact_on_grid(|grid| {
// This gets automatically duck-typed for (yi, line) in buf.lines().enumerate() {
grid.set_cell_raw((xi, yi), cell); let cells = Self::parse_csv_line(line);
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;
@@ -112,25 +201,21 @@ 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.max(); let (mx, my) = self.get_grid().max();
for y in 0..=my { for y in 0..=my {
for x in 0..=mx { for x in 0..=mx {
let cell = &self.cells[x][y]; let cell = &self.get_grid().get_cell_raw(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 { let delim = if is_last { '\n' } else { CSV_DELIMITER };
'\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())
&& resolve_values && resolve_values
{ {
format!("{}{}", val.to_string(), delim) format!("{val}{delim}")
} else { } else {
format!("{}{}", cell.escaped_csv_string(), delim) format!("{}{}", cell.escaped_csv_string(), delim)
} }
@@ -146,6 +231,37 @@ impl Grid {
Ok(()) Ok(())
} }
pub fn get_grid(&self) -> &CellGrid {
&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
} }
@@ -252,101 +368,80 @@ 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)) {
for x in 0..LEN { self.transact_on_grid(|grid: &mut CellGrid| {
self.cells[x].insert(insertion_y, None); grid.insert_row(insertion_y);
self.cells[x].pop(); for x in 0..LEN {
for y in 0..LEN { for y in 0..LEN {
if let Some(cell) = self.get_cell_raw(x, y).as_ref().map(|f| { if let Some(cell) = grid.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 if Grid::range_as_indices(old).is_some() {
unimplemented!("Invalid variable wanted to be translated") // ranges are not changed when moved vertically
} rolling.to_string()
} else {
#[cfg(debug_assertions)]
unimplemented!("Invalid variable wanted to be translated");
#[cfg(not(debug_assertions))]
rolling.to_string()
}
})
}) {
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)) {
let mut v = Vec::with_capacity(LEN); self.transact_on_grid(|grid| {
for _ in 0..LEN { grid.insert_column(insertion_x);
v.push(None); for x in 0..LEN {
} for y in 0..LEN {
self.cells.insert(insertion_x, v); if let Some(cell) = grid.get_cell_raw(x, y).as_ref().map(|f| {
// keep the grid LEN f.custom_translate_cell((0, 0), (1, 0), |rolling, old, new| {
self.cells.pop(); if let Some((arg_x, _)) = Grid::parse_to_idx(old) {
for x in 0..LEN { // add 1 because of the insertion
for y in 0..LEN { if arg_x < insertion_x { rolling.to_owned() } else { rolling.replace(old, new) }
if let Some(cell) = self.get_cell_raw(x, y).as_ref().map(|f| { } else if let Some((start, end)) = Grid::range_as_indices(old) {
f.custom_translate_cell((0, 0), (1, 0), |rolling, old, new| { let mut range_start = Grid::num_to_char(start);
if let Some((arg_x, _)) = Grid::parse_to_idx(old) { let mut range_end = Grid::num_to_char(end);
// add 1 because of the insertion
if arg_x < insertion_x { rolling.to_owned() } else { rolling.replace(old, new) } if start >= insertion_x {
} else { range_start = Grid::num_to_char(start+1);
// FIXME could be a range }
unimplemented!("Invalid variable wanted to be translated")
} if end >= insertion_x {
}) range_end = Grid::num_to_char(end+1);
}) { }
self.set_cell_raw((x, y), Some(cell)); let new = format!("{range_start}:{range_end}");
rolling.replace(old, &new)
} else {
#[cfg(debug_assertions)]
unimplemented!("Invalid variable wanted to be translated");
#[cfg(not(debug_assertions))]
rolling.to_string()
}
})
}) {
grid.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.
@@ -358,7 +453,7 @@ impl Grid {
return Err(format!("\"{eq}\" is not an equation")); return Err(format!("\"{eq}\" is not an equation"));
} }
let ctx = ctx::CallbackContext::new(&self); let ctx = ctx::CallbackContext::new(self);
let prep_for_return = |v: Value| { let prep_for_return = |v: Value| {
if v.is_number() { if v.is_number() {
@@ -385,10 +480,7 @@ 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 { EvalexprError::TypeError { expected: e, actual: a } => {
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}"));
} }
@@ -397,6 +489,8 @@ impl Grid {
} }
} }
/// Gets the indices of the range labels.
/// A:B -> (0,1)
pub fn range_as_indices(range: &str) -> Option<(usize, usize)> { pub fn range_as_indices(range: &str) -> Option<(usize, usize)> {
let v = range.split(':').collect::<Vec<&str>>(); let v = range.split(':').collect::<Vec<&str>>();
if v.len() == 2 { if v.len() == 2 {
@@ -406,19 +500,18 @@ impl Grid {
let start_idx = Grid::char_to_idx(start_col); let start_idx = Grid::char_to_idx(start_col);
let end_idx = Grid::char_to_idx(end_col); let end_idx = Grid::char_to_idx(end_col);
return Some((start_idx, end_idx)) return Some((start_idx, end_idx));
} }
None None
} }
pub fn char_to_idx(i: &str) -> usize { pub fn char_to_idx(i: &str) -> usize {
let x_idx = i i
.chars() .chars()
.filter(|f| f.is_alphabetic()) .filter(|f| f.is_alphabetic())
.enumerate() .enumerate()
.map(|(idx, c)| ((c.to_ascii_lowercase() as usize).saturating_sub(97)) + (26 * idx)) .map(|(idx, c)| ((c.to_ascii_lowercase() as usize).saturating_sub(97)) + (26 * idx))
.fold(0, |a, b| a + b); .sum()
x_idx
} }
/// Parse values in the format of A0, C10 ZZ99, etc, and /// Parse values in the format of A0, C10 ZZ99, etc, and
@@ -447,15 +540,12 @@ impl Grid {
/// Helper for tests /// Helper for tests
#[cfg(test)] #[cfg(test)]
pub fn set_cell<T: Into<CellType>>(&mut self, cell_id: &str, val: T) { pub fn set_cell<T: Into<CellType> + Clone>(&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.set_cell_raw(loc, Some(val)); self.transact_on_grid(|grid| {
grid.set_cell_raw(loc, Some(val.clone()));
});
} }
}
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;
} }
@@ -474,7 +564,7 @@ impl Grid {
if x >= LEN || y >= LEN { if x >= LEN || y >= LEN {
return &None; return &None;
} }
&self.cells[x][y] self.get_grid().get_cell_raw(x, y)
} }
pub fn num_to_char(idx: usize) -> String { pub fn num_to_char(idx: usize) -> String {
@@ -491,7 +581,7 @@ impl Grid {
} }
word[1] = ((idx % 26) + 65) as u8 as char; word[1] = ((idx % 26) + 65) as u8 as char;
word.iter().collect() word.iter().filter(|a| !a.is_ascii_whitespace()).collect()
} }
} }
@@ -592,7 +682,7 @@ fn saving_neoscim() {
fn cell_strings() { fn cell_strings() {
let mut grid = Grid::new(); let mut grid = Grid::new();
assert!(&grid.cells[0][0].is_none()); assert!(&grid.get_grid().get_cell_raw(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());
@@ -617,8 +707,8 @@ fn alphanumeric_indexing() {
assert_eq!(Grid::parse_to_idx("="), None); assert_eq!(Grid::parse_to_idx("="), None);
assert_eq!(Grid::parse_to_idx("A:A"), None); assert_eq!(Grid::parse_to_idx("A:A"), None);
assert_eq!(Grid::num_to_char(0).trim(), "A"); assert_eq!(Grid::num_to_char(0), "A");
assert_eq!(Grid::num_to_char(25).trim(), "Z"); assert_eq!(Grid::num_to_char(25), "Z");
assert_eq!(Grid::num_to_char(26), "AA"); assert_eq!(Grid::num_to_char(26), "AA");
assert_eq!(Grid::num_to_char(51), "AZ"); assert_eq!(Grid::num_to_char(51), "AZ");
assert_eq!(Grid::num_to_char(701), "ZZ"); assert_eq!(Grid::num_to_char(701), "ZZ");
@@ -673,14 +763,16 @@ fn fn_of_fn() {
grid.set_cell("C0", "=A0+B0".to_string()); grid.set_cell("C0", "=A0+B0".to_string());
grid.set_cell("D0", "=C0*2".to_string()); grid.set_cell("D0", "=C0*2".to_string());
if let Some(cell) = grid.get_cell("D0") { let cell = grid.get_cell("D0");
assert!(cell.is_some());
if let Some(cell) = cell {
let res = grid.evaluate(&cell.to_string()); let res = grid.evaluate(&cell.to_string());
assert!(res.is_ok()); assert!(res.is_ok());
assert_eq!(res.unwrap(), (6.).into()); assert_eq!(res.unwrap(), (6.).into());
return; return;
} }
panic!("Cell not found");
} }
// Two cells that have a circular dependency to solve for a value // Two cells that have a circular dependency to solve for a value
@@ -690,12 +782,14 @@ fn circular_reference_cells() {
grid.set_cell("A0", "=B0".to_string()); grid.set_cell("A0", "=B0".to_string());
grid.set_cell("B0", "=A0".to_string()); grid.set_cell("B0", "=A0".to_string());
if let Some(cell) = grid.get_cell("A0") { let cell = grid.get_cell("A0");
assert!(cell.is_some());
if let Some(cell) = cell {
let res = grid.evaluate(&cell.to_string()); let res = grid.evaluate(&cell.to_string());
assert!(res.is_err()); assert!(res.is_err());
return; return;
} }
panic!("Cell not found");
} }
#[test] #[test]
@@ -729,17 +823,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.max(); let (mx, my) = grid.get_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.max(); let (mx, my) = grid.get_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.max(); let (mx, my) = grid.get_grid().max();
assert_eq!(mx, 1); assert_eq!(mx, 1);
assert_eq!(my, 5); assert_eq!(my, 5);
} }
@@ -1032,7 +1126,7 @@ fn insert_col_before_static_range() {
grid.set_cell("A1", 2.); grid.set_cell("A1", 2.);
grid.set_cell("B0", "=sum(A:A)".to_string()); grid.set_cell("B0", "=sum(A:A)".to_string());
grid.mv_cursor_to(0, 1); grid.mv_cursor_to(1, 0);
grid.insert_column_before(grid.cursor()); grid.insert_column_before(grid.cursor());
let cell = grid.get_cell("C0").as_ref().expect("Just set it"); let cell = grid.get_cell("C0").as_ref().expect("Just set it");
@@ -1054,6 +1148,36 @@ fn insert_col_before_move_range() {
assert_eq!(cell.to_string(), "=sum(C:C)"); assert_eq!(cell.to_string(), "=sum(C:C)");
} }
#[test]
fn insert_row_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, 0);
grid.insert_row_above(grid.cursor());
let cell = grid.get_cell("B1").as_ref().expect("Just set it");
assert_eq!(cell.to_string(), "=sum(A:A)");
}
#[test]
fn insert_row_before_move_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, 0);
grid.insert_row_below(grid.cursor());
let cell = grid.get_cell("B0").as_ref().expect("Just set it");
assert_eq!(cell.to_string(), "=sum(A:A)");
}
#[test] #[test]
fn insert_row_above_1() { fn insert_row_above_1() {
let mut grid = Grid::new(); let mut grid = Grid::new();
@@ -1120,7 +1244,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());
@@ -1170,4 +1294,3 @@ fn return_string_from_fn() {
} }
} }
} }

View File

@@ -11,15 +11,15 @@ pub enum CellType {
Equation(String), Equation(String),
} }
impl Into<CellType> for f64 { impl From<f64> for CellType {
fn into(self) -> CellType { fn from(value: f64) -> Self {
CellType::duck_type(self.to_string()) CellType::duck_type(value.to_string())
} }
} }
impl Into<CellType> for String { impl From<String> for CellType {
fn into(self) -> CellType { fn from(value: String) -> Self {
CellType::duck_type(self) CellType::duck_type(value)
} }
} }
@@ -37,14 +37,10 @@ impl CellType {
} }
// escape string of it has a comma // escape string of it has a comma
if display.contains(CSV_DELIMITER) { if display.contains(CSV_DELIMITER) { format!("\"{display}\"") } else { display }
format!("\"{display}\"")
} else {
display
}
} }
fn duck_type<'a>(value: impl Into<String>) -> Self { fn duck_type(value: impl Into<String>) -> Self {
let value = value.into(); let value = value.into();
if let Ok(parse) = value.parse::<f64>() { if let Ok(parse) = value.parse::<f64>() {
@@ -56,7 +52,12 @@ impl CellType {
/// `replace_fn` takes the string, the old value, and then the new value. /// `replace_fn` takes the string, the old value, and then the new value.
/// It can be thought of as `echo $1 | sed s/$2/$3/g` /// It can be thought of as `echo $1 | sed s/$2/$3/g`
pub fn custom_translate_cell(&self, from: (usize, usize), to: (usize, usize), replace_fn: impl Fn(&str, &str, &str) -> String) -> CellType { pub fn custom_translate_cell(
&self,
from: (usize, usize),
to: (usize, usize),
replace_fn: impl Fn(&str, &str, &str) -> String,
) -> CellType {
match self { match self {
// don't translate non-equations // don't translate non-equations
CellType::Number(_) | CellType::String(_) => return self.clone(), CellType::Number(_) | CellType::String(_) => return self.clone(),
@@ -73,10 +74,8 @@ impl CellType {
let mut lock_y = false; let mut lock_y = false;
if old_var.contains('$') { if old_var.contains('$') {
let locations = old_var let locations =
.char_indices() old_var.char_indices().filter(|(_, c)| *c == '$').map(|(i, _)| i).collect::<Vec<usize>>();
.filter(|(_, c)| *c == '$').map(|(i, _)| i)
.collect::<Vec<usize>>();
match locations.len() { match locations.len() {
1 => { 1 => {
if locations[0] == 0 { if locations[0] == 0 {
@@ -110,20 +109,11 @@ impl CellType {
let x2 = x2 as i32; let x2 = x2 as i32;
let y2 = y2 as i32; let y2 = y2 as i32;
let dest_x = if lock_x { let dest_x = if lock_x { src_x } else { (src_x as i32 + (x2 - x1)) as usize };
src_x as usize
} else {
(src_x as i32 + (x2 - x1)) as usize
};
let dest_y = if lock_y { let dest_y = if lock_y { src_y } else { (src_y as i32 + (y2 - y1)) as usize };
src_y as usize
} else {
(src_y as i32 + (y2 - y1)) as usize
};
let alpha = Grid::num_to_char(dest_x); let alpha = Grid::num_to_char(dest_x);
let alpha = alpha.trim();
// Persist the "$" locking // Persist the "$" locking
let new_var = if lock_x { let new_var = if lock_x {
@@ -141,37 +131,29 @@ impl CellType {
// why you coping invalid stuff, nerd? // why you coping invalid stuff, nerd?
// //
// could be copying a range // could be copying a range
if old_var.contains(':') { if let Some(parts) = Grid::range_as_indices(&old_var) {
let parts = old_var.split(':').collect::<Vec<&str>>(); // how far is the movement?
// This means the var was formatted as X:X let dx = to.0 as i32 - from.0 as i32;
if parts.len() == 2 {
// how far is the movement?
let dx = to.0 as i32 - from.0 as i32;
let range_start = parts[0]; let xs = parts.0 as i32;
let range_end = parts[1]; let xe = parts.1 as i32;
// get the letters as numbers // apply movement
let xs = Grid::char_to_idx(range_start) as i32; let mut new_range_start = xs + dx;
let xe = Grid::char_to_idx(range_end) as i32; let mut new_range_end = xe + dx;
// apply movement // bottom out at 0
let mut new_range_start = xs+dx; if new_range_start < 0 {
let mut new_range_end = xe+dx; new_range_start = 0;
// bottom out at 0
if new_range_start < 0 {
new_range_start = 0;
}
if new_range_end < 0 {
new_range_end = 0;
}
// convert the index back into a letter and then submit it
let start = Grid::num_to_char(new_range_start as usize);
let end = Grid::num_to_char(new_range_end as usize);
equation = replace_fn(&equation, &old_var, &format!("{}:{}", start.trim(), end.trim()));
} }
if new_range_end < 0 {
new_range_end = 0;
}
// convert the index back into a letter and then submit it
let start = Grid::num_to_char(new_range_start as usize);
let end = Grid::num_to_char(new_range_end as usize);
equation = replace_fn(&equation, &old_var, &format!("{start}:{end}"));
} }
} }
} }
@@ -181,7 +163,7 @@ impl CellType {
} }
pub fn translate_cell(&self, from: (usize, usize), to: (usize, usize)) -> CellType { pub fn translate_cell(&self, from: (usize, usize), to: (usize, usize)) -> CellType {
self.custom_translate_cell(from, to, |a,b,c| a.replace(b, c)) self.custom_translate_cell(from, to, |a, b, c| a.replace(b, c))
} }
} }
@@ -206,4 +188,3 @@ impl PartialEq for CellType {
} }
} }
} }

View File

@@ -20,7 +20,7 @@ impl<'a> CallbackContext<'a> {
let mut buf = Vec::new(); let mut buf = Vec::new();
for x in start..=end { for x in start..=end {
for y in 0..=self.variables.max_y_at_x(x) { for y in 0..=self.variables.get_grid().max_y_at_x(x) {
if let Some(s) = self.variables.get_cell_raw(x, y) { if let Some(s) = self.variables.get_cell_raw(x, y) {
buf.push(s); buf.push(s);
} }
@@ -188,7 +188,14 @@ impl<'a> Context for CallbackContext<'a> {
return None; return None;
} }
e => panic!("> Error {e}\n> Equation: '{eq}'"), e => {
let msg = format!("> Error {e}\n> Equation: '{eq}'");
#[cfg(debug_assertions)]
panic!("{msg}");
#[cfg(not(debug_assertions))]
eprintln!("{msg}");
},
} }
} }
} }

View File

@@ -2,7 +2,8 @@ use std::{
cmp::{max, min}, cmp::{max, min},
fmt::Display, fmt::Display,
fs, fs,
path::PathBuf, process::Command, path::PathBuf,
process::Command,
}; };
use ratatui::{ use ratatui::{
@@ -86,7 +87,7 @@ impl Mode {
path.file_name().map(|f| f.to_str().unwrap_or("n/a")).unwrap_or("n/a") path.file_name().map(|f| f.to_str().unwrap_or("n/a")).unwrap_or("n/a")
)); ));
if let None = app.file { if app.file.is_none() {
app.file = Some(path) app.file = Some(path)
} }
} }
@@ -153,11 +154,13 @@ impl Mode {
let mut save_range = |to: &str| { let mut save_range = |to: &str| {
let mut g = Grid::new(); let mut g = Grid::new();
for (i, x) in (low_x..=hi_x).enumerate() { g.transact_on_grid(|grid| {
for (j, y) in (low_y..=hi_y).enumerate() { for (i, x) in (low_x..=hi_x).enumerate() {
g.set_cell_raw((i, j), app.grid.get_cell_raw(x, y).clone()); for (j, y) in (low_y..=hi_y).enumerate() {
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");
} }
@@ -171,28 +174,30 @@ impl Mode {
} }
} }
} }
return "unknown" return "unknown";
}; };
match args[0] { match args[0] {
"f" | "fill" => { "f" | "fill" => {
for (i, x) in (low_x..=hi_x).enumerate() { app.grid.transact_on_grid(|grid| {
for (j, y) in (low_y..=hi_y).enumerate() { for (i, x) in (low_x..=hi_x).enumerate() {
let arg = args.get(1) for (j, y) in (low_y..=hi_y).enumerate() {
.map(|s| s.replace("xi", &i.to_string())) let arg = args
.map(|s| s.replace("yi", &j.to_string())) .get(1)
.map(|s| s.replace("x", &x.to_string())) .map(|s| s.replace("xi", &i.to_string()))
.map(|s| s.replace("y", &y.to_string())) .map(|s| s.replace("yi", &j.to_string()))
; .map(|s| s.replace("x", &x.to_string()))
app.grid.set_cell_raw((x,y), arg); .map(|s| s.replace("y", &y.to_string()));
grid.set_cell_raw((x, y), arg);
}
} }
} });
app.mode = Mode::Normal app.mode = Mode::Normal
} }
"export" => { "export" => {
if let Some(arg1) = args.get(1) { if let Some(arg1) = args.get(1) {
save_range(&arg1); save_range(arg1);
} else { } else {
app.msg = StatusMessage::error("export <path.csv>") app.msg = StatusMessage::error("export <path.csv>")
} }
@@ -202,11 +207,7 @@ 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) { let output_filename = if let Some(arg1) = args.get(1) { arg1 } else { "plot.png" };
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");
@@ -217,10 +218,12 @@ 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 => app.msg = StatusMessage::error("Error - Is gnuplot installed?"), std::io::ErrorKind::NotFound => {
app.msg = StatusMessage::error("Error - Is gnuplot installed?")
}
_ => app.msg = StatusMessage::error(format!("{err}")), _ => app.msg = StatusMessage::error(format!("{err}")),
}; };
} else { } else {
@@ -270,20 +273,20 @@ impl Mode {
// Go to end of row // Go to end of row
'$' => { '$' => {
let (_, y) = app.grid.cursor(); let (_, y) = app.grid.cursor();
app.grid.mv_cursor_to(super::logic::calc::LEN, y); app.grid.mv_cursor_to(super::logic::calc::LEN-1, y);
return; return;
} }
// 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-1);
return; return;
} }
// edit cell // edit cell
'i' | 'a' => { 'i' | 'a' => {
let (x, y) = app.grid.cursor(); let (x, y) = app.grid.cursor();
let val = app.grid.get_cell_raw(x, y).as_ref().map(|f| f.to_string()).unwrap_or(String::new()); let val = app.grid.get_cell_raw(x, y).as_ref().map(|f| f.to_string()).unwrap_or_default();
app.mode = Mode::Insert(Chord::from(val)); app.mode = Mode::Insert(Chord::from(val));
} }
@@ -319,6 +322,11 @@ 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());
@@ -398,7 +406,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;
} }
@@ -408,7 +416,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
@@ -469,14 +477,12 @@ impl Mode {
let len = match &self { let len = match &self {
Mode::Insert(edit) | Mode::VisualCmd(_, edit) | Mode::Command(edit) | Mode::Chord(edit) => edit.len(), Mode::Insert(edit) | Mode::VisualCmd(_, edit) | Mode::Command(edit) | Mode::Chord(edit) => edit.len(),
Mode::Normal => { Mode::Normal => {
let len = cell.as_ref().map(|f| f.to_string().len()).unwrap_or_default(); cell.as_ref().map(|f| f.to_string().len()).unwrap_or_default()
len
} }
Mode::Visual(_) => 0, Mode::Visual(_) => 0,
}; };
// min 20 chars, expand if needed // min 20 chars, expand if needed
let len = max(len as u16 + 1, 20); max(len as u16 + 1, 20)
len
} }
pub fn render(&self, f: &mut ratatui::Frame, area: prelude::Rect, cell: &Option<CellType>) { pub fn render(&self, f: &mut ratatui::Frame, area: prelude::Rect, cell: &Option<CellType>) {
@@ -490,8 +496,7 @@ impl Mode {
Mode::Chord(chord) => f.render_widget(chord, area), Mode::Chord(chord) => f.render_widget(chord, area),
Mode::Normal => f.render_widget( Mode::Normal => f.render_widget(
Paragraph::new({ Paragraph::new({
let cell = cell.as_ref().map(|f| f.to_string()).unwrap_or_default(); cell.as_ref().map(|f| f.to_string()).unwrap_or_default()
cell
}), }),
area, area,
), ),
@@ -508,20 +513,15 @@ 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 { Chord { buf: b }
buf: b,
}
} }
} }
impl Chord { impl Chord {
pub fn new(inital: char) -> Self { pub fn new(inital: char) -> Self {
let mut buf = Vec::new(); let buf = vec![inital];
buf.push(inital);
Self { Self { buf }
buf,
}
} }
pub fn backspace(&mut self) { pub fn backspace(&mut self) {

View File

@@ -1,4 +1,4 @@
use std::{collections::HashMap, sync::RwLock}; use std::{cmp::min, collections::HashMap, sync::RwLock};
use ratatui::prelude; use ratatui::prelude;
@@ -48,11 +48,11 @@ impl ScreenSpace {
// ======= X ======= // ======= X =======
// screen seems to be 2 cells smaller than it should be // screen seems to be 2 cells smaller than it should be
// this is probably related to issue #6 // this is probably related to issue #6
let x_cells = (screen_size.0 / self.get_cell_width(vars) as usize) -2; let x_cells = (screen_size.0 / self.get_cell_width(vars)) -2;
// ======= Y ======= // ======= Y =======
// screen seems to be 2 cells smaller than it should be // screen seems to be 2 cells smaller than it should be
// this is probably related to issue #6 // this is probably related to issue #6
let y_cells = (screen_size.1 / self.get_cell_height(vars) as usize) -2; let y_cells = (screen_size.1 / self.get_cell_height(vars)) -2;
(x_cells,y_cells) (x_cells,y_cells)
} else { } else {
(0,0) (0,0)
@@ -123,22 +123,12 @@ impl ScreenSpace {
l.1 = area.height as usize; l.1 = area.height as usize;
} }
// let width = (area.width as usize + calc::get_header_size() -1) / self.get_cell_width(vars); let width = (area.width as usize / self.get_cell_width(vars)) + 1;
let width = area.width as usize / self.get_cell_width(vars);
let height = area.height as usize / self.get_cell_height(vars); let height = area.height as usize / self.get_cell_height(vars);
let x_max = let x_max = min(LEN-1, width);
if width > LEN { let y_max = min(LEN-1, height);
LEN - 1
} else {
width
};
let y_max =
if height > LEN {
LEN - 1
} else {
height
};
(x_max as u16, y_max as u16) (x_max as u16, y_max as u16)
} }
@@ -152,7 +142,7 @@ fn fit_cells() {
app.vars.insert("height".to_string(), 1.to_string()); app.vars.insert("height".to_string(), 1.to_string());
let (x,y) = app.screen.how_many_cells_fit_in(&prelude::Rect::new(0, 0, 181, 14), &app.vars); let (x,y) = app.screen.how_many_cells_fit_in(&prelude::Rect::new(0, 0, 181, 14), &app.vars);
assert_eq!(x, 18); assert_eq!(x, 19);
assert_eq!(y, 14); assert_eq!(y, 14);
} }
@@ -166,7 +156,7 @@ fn scroll() {
// We have to check how many cells fit, because screen learns the width // We have to check how many cells fit, because screen learns the width
// of the area by rumour here. // of the area by rumour here.
let (x,y) = app.screen.how_many_cells_fit_in(&prelude::Rect::new(0, 0, 181, 14), &app.vars); let (x,y) = app.screen.how_many_cells_fit_in(&prelude::Rect::new(0, 0, 181, 14), &app.vars);
assert_eq!(x, 18); assert_eq!(x, 19);
assert_eq!(y, 14); assert_eq!(y, 14);
// we aren't scrolled at all yet // we aren't scrolled at all yet

View File

@@ -1,3 +1,10 @@
#![allow(clippy::needless_return)]
#![allow(clippy::len_zero)]
#![allow(clippy::collapsible_if)]
#![allow(clippy::collapsible_else_if)]
#![allow(clippy::collapsible_match)]
#![allow(clippy::single_match)]
mod app; mod app;
use std::env::args; use std::env::args;