Compare commits

27 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
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
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
d983995e8f solve failing test
All checks were successful
Test Rust project / test (ubuntu-latest, stable) (push) Successful in 53s
copy and pasting functions with ranges as arguments now works
2026-02-06 11:31:41 -07:00
045d1d6554 solve #36 by adding :fill command
Some checks failed
Test Rust project / test (ubuntu-latest, stable) (push) Failing after 53s
2026-01-28 16:29:16 -07:00
9691268d3d readme and create issue
Some checks failed
Test Rust project / test (ubuntu-latest, stable) (push) Failing after 38s
2026-01-27 16:53:46 -07:00
f3356c1398 fix #35, allows for strings to be returned from functions
All checks were successful
Test Rust project / test (ubuntu-latest, stable) (push) Successful in 44s
2026-01-27 16:07:23 -07:00
b3b2c59a36 fix #47
All checks were successful
Test Rust project / test (ubuntu-latest, stable) (push) Successful in 43s
2026-01-27 14:50:05 -07:00
077b53f6ff cleanup imports
Some checks failed
Test Rust project / test (ubuntu-latest, stable) (push) Failing after 41s
2026-01-27 13:50:00 -07:00
0c78e7834b fix #33 2026-01-27 13:49:46 -07:00
12 changed files with 785 additions and 377 deletions

View File

@@ -2,8 +2,12 @@
*New* Spreadsheet Calculator Improved
![Screenshot](readme/screenshot.png)
Based loosely off sc-im (spreadsheet calculator improvised), which has dumb keybinds and not many features.
[Checkout a demo file](readme/demo.nscim), download, then open with `neoscim demo.nscim`.
## Keybinds
### Normal mode
@@ -21,6 +25,8 @@ Based loosely off sc-im (spreadsheet calculator improvised), which has dumb keyb
| `gg` | Go to beginning of the column |
| `G` | Go to end of 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 |
| `r` | Enter insert mode on current cell, deleting contents |
| `v` | Enter visual mode |
@@ -39,6 +45,8 @@ Based loosely off sc-im (spreadsheet calculator improvised), which has dumb keyb
| n`G` | Jump to row "n" |
| 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
| Key | Action |
@@ -64,6 +72,7 @@ These commands operate on selections.
| - | - |
| `:plot` | Plots the current selection to `plot.png` using gnuplot |
| `:export <filename>` | Exports the current selection to a new file |
| `:fill <value>` | (Aliased as `f`) Fill the selection with `<value>`, special variables `x`,`y`,`xi`, and `yi` are avaliable. Variables `x` and `y` are the global coordinates of the cell, and `xi` and `yi` are the local coordinates of the selection. |
## Math / Functions

21
readme/demo.nscim Normal file
View File

@@ -0,0 +1,21 @@
,,,
Most of vim's movement works here as well,,,
,,,
,i,,
j,Movement,l,
,j,,
,,,
,"insert text with r, i, or a",,
,,,
1,,,
1,Text can overflow cells,,
1,But may get cut off if it's too long,,10
1,,,If it reaches another cell
1,,,
Total:,=sum(A:A),Sum A:A,
,=math::log2(B14),Do math on the output of another function,
,,,
,,,
,,11,Number.
,,=C18+1,Referencing the number and adding 1
,,,Copying the cell down will translate the reference

BIN
readme/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -18,7 +18,10 @@ use ratatui::{
use crate::app::{
clipboard::Clipboard,
error_msg::StatusMessage,
logic::{self, calc::{Grid, get_header_size}, cell::CellType},
logic::{
calc::{Grid, LEN, get_header_size},
cell::CellType,
},
mode::Mode,
screen::ScreenSpace,
};
@@ -39,10 +42,12 @@ pub struct App {
impl Widget for &App {
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 is_selected = |x: usize, y: usize| -> bool {
if let Mode::Visual((mut x1, mut y1)) | Mode::VisualCmd((mut x1, mut y1), _)= self.mode {
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 {
let (mut x2, mut y2) = self.grid.cursor();
x1 += 1;
y1 += 1;
@@ -60,17 +65,52 @@ impl Widget for &App {
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 y in 0..y_max {
let mut display = String::new();
let mut style = Style::new().fg(Color::White);
// 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;
let mut style = Style::new();
// Minus 1 because of header cells,
// the grid is shifted over (1,1), so if you need
@@ -78,12 +118,17 @@ impl Widget for &App {
let mut x_idx: usize = 0;
let mut y_idx: usize = 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 {
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 ORANGE2: Color = Color::Rgb(180, 130, 0);
@@ -93,9 +138,9 @@ impl Widget for &App {
/// Center the text "99 " -> " 99 "
fn center_text(text: &str, avaliable_space: i32) -> String {
let margin = avaliable_space - text.len() as i32;
let margin = margin/2;
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 margin = margin / 2;
let l_margin = (0..margin).map(|_| ' ').collect::<String>();
let r_margin = (0..(margin - (l_margin.len() as i32))).map(|_| ' ').collect::<String>();
format!("{l_margin}{text}{r_margin}")
}
@@ -111,7 +156,7 @@ impl Widget for &App {
let bg = if y_idx == self.grid.cursor().1 {
Color::DarkGray
} else if y_idx % 2 == 0 {
} else if y_idx.is_multiple_of(2) {
ORANGE1
} else {
ORANGE2
@@ -124,7 +169,7 @@ impl Widget for &App {
let bg = if x_idx == self.grid.cursor().0 {
Color::DarkGray
} else if x_idx % 2 == 0 {
} else if x_idx.is_multiple_of(2) {
ORANGE1
} else {
ORANGE2
@@ -148,6 +193,9 @@ impl Widget for &App {
Ok(val) => {
display = val.to_string();
style = Style::new()
.fg(Color::White)
// TODO This breaks dumb terminals like the windows
// terminal
.underline_color(Color::DarkGray)
.add_modifier(Modifier::UNDERLINED);
}
@@ -163,11 +211,12 @@ impl Widget for &App {
}
}
// ===================================================
// Allow for text in one cell to visually overflow into empty cells
suggest_upper_bound = Some(display.len() as u16);
// 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 {
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);
break;
}
@@ -179,18 +228,25 @@ impl Widget for &App {
display.push('…');
}
}
// ===================================================
}
// Don't render blank cells
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);
// Make it so that cells render when selected. This fixes issue #32
should_render = true;
}
if (x_idx, y_idx) == self.grid.cursor() {
should_render = true;
style = Style::new().fg(Color::Black).bg(Color::White);
// modify the style of the cell you are editing
if let Mode::Insert(_) = self.mode {
@@ -211,12 +267,12 @@ impl Widget for &App {
}
// 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)
} else if let Some(suggestion) = suggest_upper_bound {
let max_available_width = area.width - x_off;
// 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
let width = max(cell_width, width);
@@ -229,6 +285,9 @@ impl Widget for &App {
}
}
}
// let ns = now.elapsed().as_nanos() as f64;
// eprintln!("Rendered in {}ms", ns/1_000_000.);
}
}
@@ -274,22 +333,19 @@ impl App {
}
fn file_name_display(&self) -> String {
let file_name_status = {
let mut file_name = "[No Name]";
let mut icon = "";
if let Some(file) = &self.file {
if let Some(f) = file.file_name() {
if let Some(f) = f.to_str() {
file_name = f;
}
let mut file_name = "[No Name]";
let mut icon = "";
if let Some(file) = &self.file {
if let Some(f) = file.file_name() {
if let Some(f) = f.to_str() {
file_name = f;
}
}
if self.grid.needs_to_be_saved() {
icon = "[+]";
}
format!("{file_name}{icon}")
};
file_name_status
}
if self.grid.needs_to_be_saved() {
icon = "[+]";
}
format!("{file_name}{icon}")
}
fn draw(&self, frame: &mut Frame) {
@@ -320,6 +376,8 @@ impl App {
let cmd_line_left = cmd_line_split[0];
let cmd_line_status = cmd_line_split[1];
let cmd_line_right = cmd_line_split[2];
#[cfg(debug_assertions)]
let cmd_line_debug = cmd_line_split[3];
// ======================================================
@@ -354,7 +412,7 @@ impl App {
event::KeyCode::Char(c) => chord.add_char(c),
event::KeyCode::Enter => {
// tmp is to get around reference issues.
let tmp = pos.clone();
let tmp = *pos;
Mode::process_cmd(self);
self.mode = Mode::Visual(tmp)
}
@@ -384,17 +442,20 @@ impl App {
event::KeyCode::Enter => {
let v = editor.as_string();
// try to insert as a float
if let Ok(v) = v.parse::<f64>() {
self.grid.set_cell_raw(self.grid.cursor(), Some(v));
} else {
// if you can't, then insert as a string
if !v.is_empty() {
self.grid.set_cell_raw(self.grid.cursor(), Some(v));
let cursor = self.grid.cursor();
self.grid.transact_on_grid(|grid| {
// try to insert as a float
if let Ok(v) = v.parse::<f64>() {
grid.set_cell_raw(cursor, Some(v));
} 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;
}
@@ -410,9 +471,24 @@ impl App {
},
Mode::Normal => match event::read()? {
event::Event::Key(key_event) => match key_event.code {
event::KeyCode::F(_) => todo!(),
event::KeyCode::Char(c) => {
Mode::process_key(self, c);
event::KeyCode::F(_n) => {}
event::KeyCode::Char(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 {
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
@@ -49,25 +44,28 @@ impl Clipboard {
// cursor
let (cx, cy) = into.cursor();
// iterate thru the clipbaord's cells
for (x, row) in self.clipboard.iter().enumerate() {
for (y, cell) in row.iter().enumerate() {
let idx = (x + cx, y + cy);
let cursor = into.cursor();
into.transact_on_grid(|grid| {
// iterate thru the clipbaord's cells
for (x, row) in self.clipboard.iter().enumerate() {
for (y, cell) in row.iter().enumerate() {
let idx = (x + cx, y + cy);
if translate {
if let Some(cell) = cell {
let trans = cell.translate_cell(self.source_cell, into.cursor());
into.set_cell_raw(idx, Some(trans));
if translate {
if let Some(cell) = cell {
let trans = cell.translate_cell(self.source_cell, cursor);
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 {
// The cell at this location doesn't exist (empty)
into.set_cell_raw::<CellType>(idx, None);
// translate = false
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;
self.momentum = (cx as i32 - lx as i32, cy as i32 - ly as i32);
@@ -109,16 +107,19 @@ impl Clipboard {
// size the clipboard appropriately
self.clipboard.clear();
// clone data into clipboard
for x in low_x..=hi_x {
let mut col = Vec::new();
for y in low_y..=hi_y {
let a = from.get_cell_raw(x, y);
col.push(a.clone());
from.set_cell_raw::<CellType>((x, y), None);
from.transact_on_grid(|grid| {
// clone data into clipboard
for x in low_x..=hi_x {
let mut col = Vec::new();
for y in low_y..=hi_y {
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);
}
}
@@ -353,7 +354,7 @@ fn copy_paste_y_locked_var() {
}
#[test]
fn issue_47() {
fn copy_paste_var_in_function() {
let mut app = App::new();
app.grid.set_cell("A0", 4.to_string());
Mode::process_key(&mut app, 'j');
@@ -367,3 +368,31 @@ fn issue_47() {
let a = app.grid.get_cell("A2").as_ref().expect("Should've been set by paste");
assert_eq!(a.to_string(), "=math::log2(A1)");
}
#[test]
fn copy_paste_range_in_function() {
let mut app = App::new();
app.grid.set_cell("A0", 1.to_string());
app.grid.set_cell("A1", 1.to_string());
app.grid.set_cell("A2", 1.to_string());
app.grid.set_cell("B0", "=sum(A:A)".to_string());
app.grid.mv_cursor_to(1, 0);
app.mode = super::mode::Mode::Chord(Chord::new('y'));
Mode::process_key(&mut app, 'y');
Mode::process_key(&mut app, 'l');
Mode::process_key(&mut app, 'p');
let a = app.grid.get_cell("C0").as_ref().expect("Should've been set by paste");
assert_eq!(a.to_string(), "=sum(B:B)");
// now copy the range the other direction
app.grid.mv_cursor_to(2, 0);
app.mode = super::mode::Mode::Chord(Chord::new('y'));
Mode::process_key(&mut app, 'y');
app.grid.mv_cursor_to(1, 1);
Mode::process_key(&mut app, 'p');
let a = app.grid.get_cell("B1").as_ref().expect("Should've been set by paste");
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 {
String::new()
} else {
self.msg.clone().unwrap_or(String::new())
self.msg.clone().unwrap_or_default()
};
let style = match self.msg_type {
@@ -57,4 +57,4 @@ impl Widget for &StatusMessage {
Paragraph::new(msg).style(style).render(area, buf);
}
}
}

View File

@@ -2,37 +2,134 @@ use std::{
cmp::{max, min},
fs::{self, File},
io::{Read, Write},
path::PathBuf
path::PathBuf,
};
use evalexpr::*;
use crate::app::{
logic::{
cell::{CSV_DELIMITER, CellType},
ctx,
}, mode::Mode,
use crate::app::logic::{
calc::internal::CellGrid,
cell::{CSV_DELIMITER, CellType},
ctx,
};
#[cfg(test)]
use crate::app::app::App;
#[cfg(test)]
use crate::app::mode::Mode;
pub fn get_header_size() -> usize {
let row_header_width = LEN.to_string().len();
row_header_width
LEN.to_string().len()
}
pub const LEN: usize = 1001;
pub const CSV_EXT: &str = "csv";
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 {
// a b c ...
// 0
// 1
// 2
// ...
cells: Vec<Vec<Option<CellType>>>,
/// Which grid in history are we currently on
current_grid: usize,
/// An array of grids, thru history
grid_history: Vec<CellGrid>,
/// (X, Y)
selected_cell: (usize, usize),
/// Have unsaved modifications been made?
@@ -47,20 +144,9 @@ impl std::fmt::Debug for Grid {
impl Grid {
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)
}
let x = CellGrid::new();
Self {
cells: a,
selected_cell: (0, 0),
dirty: false,
}
Self { current_grid: 0, grid_history: vec![x], selected_cell: (0, 0), dirty: false }
}
pub fn new_from_file(file: &mut File) -> std::io::Result<Self> {
@@ -68,14 +154,17 @@ impl Grid {
let mut buf = String::new();
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() {
// This gets automatically duck-typed
grid.set_cell_raw((xi, yi), cell);
grid.transact_on_grid(|grid| {
for (yi, line) in buf.lines().enumerate() {
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
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 (mx, my) = self.max();
let (mx, my) = self.get_grid().max();
for y in 0..=my {
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.
// else, just put a comma after the cell.
let is_last = x==mx;
let delim = if is_last {
'\n'
} else {
CSV_DELIMITER
};
let is_last = x == mx;
let delim = if is_last { '\n' } else { CSV_DELIMITER };
let data = if let Some(cell) = cell {
if let Ok(val) = self.evaluate(&cell.to_string())
&& resolve_values
{
format!("{}{}", val.to_string(), delim)
format!("{val}{delim}")
} else {
format!("{}{}", cell.escaped_csv_string(), delim)
}
@@ -146,6 +231,37 @@ impl Grid {
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 {
self.dirty
}
@@ -252,101 +368,84 @@ impl Grid {
}
pub fn insert_row_above(&mut self, (_x, insertion_y): (usize, usize)) {
for x in 0..LEN {
self.cells[x].insert(insertion_y, None);
self.cells[x].pop();
for y in 0..LEN {
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| {
if let Some((_, arg_y)) = Grid::parse_to_idx(old) {
if arg_y < insertion_y { rolling.to_owned() } else { rolling.replace(old, new) }
} else {
unimplemented!("Invalid variable wanted to be translated")
}
self.transact_on_grid(|grid: &mut CellGrid| {
grid.insert_row(insertion_y);
for x in 0..LEN {
for y in 0..LEN {
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| {
if let Some((_, arg_y)) = Grid::parse_to_idx(old) {
if arg_y < insertion_y { rolling.to_owned() } else { rolling.replace(old, new) }
} else if Grid::range_as_indices(old).is_some() {
// 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)) {
self.insert_row_above((x,y+1));
}
pub fn insert_column_before(&mut self, (insertion_x, _y): (usize, usize)) {
let mut v = Vec::with_capacity(LEN);
for _ in 0..LEN {
v.push(None);
}
self.cells.insert(insertion_x, v);
// keep the grid LEN
self.cells.pop();
for x in 0..LEN {
for y in 0..LEN {
if let Some(cell) = self.get_cell_raw(x, y).as_ref().map(|f| {
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) }
} else {
unimplemented!("Invalid variable wanted to be translated")
}
})
}) {
self.set_cell_raw((x, y), Some(cell));
}
}
}
});
}
pub fn insert_row_below(&mut self, (x, y): (usize, usize)) {
self.insert_row_above((x, y + 1));
}
pub fn insert_column_before(&mut self, (insertion_x, _y): (usize, usize)) {
self.transact_on_grid(|grid| {
grid.insert_column(insertion_x);
for x in 0..LEN {
for y in 0..LEN {
if let Some(cell) = grid.get_cell_raw(x, y).as_ref().map(|f| {
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) }
} else if let Some((start, end)) = Grid::range_as_indices(old) {
let mut range_start = Grid::num_to_char(start);
let mut range_end = Grid::num_to_char(end);
if start >= insertion_x {
range_start = Grid::num_to_char(start+1);
}
if end >= insertion_x {
range_end = Grid::num_to_char(end+1);
}
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)) {
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
/// strings or numbers.
pub fn evaluate(&self, mut eq: &str) -> Result<f64, String> {
pub fn evaluate(&self, mut eq: &str) -> Result<CellType, String> {
if eq.starts_with('=') {
eq = &eq[1..];
} else {
@@ -354,17 +453,21 @@ impl Grid {
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| {
if v.is_number() {
if v.is_float() {
let val = v.as_float().expect("Value lied about being a float");
return Ok(val);
return Ok(CellType::Number(val));
} else if v.is_int() {
let i = v.as_int().expect("Value lied about being an int");
return Ok(i as f64);
let val = v.as_int().expect("Value lied about being an int");
return Ok(CellType::Number(val as f64));
}
} else if v.is_string() {
// ^^ This allows for functions to return a string
let s = v.as_string().expect("Value lied about being a String");
return Ok(CellType::String(s));
}
return Err("Result is NaN".to_string());
};
@@ -377,10 +480,7 @@ impl Grid {
EvalexprError::VariableIdentifierNotFound(var_not_found) => {
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
return Err(format!("Wanted {e:?}, got {a}"));
}
@@ -389,12 +489,37 @@ impl Grid {
}
}
/// Gets the indices of the range labels.
/// A:B -> (0,1)
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 {
i
.chars()
.filter(|f| f.is_alphabetic())
.enumerate()
.map(|(idx, c)| ((c.to_ascii_lowercase() as usize).saturating_sub(97)) + (26 * idx))
.sum()
}
/// Parse values in the format of A0, C10 ZZ99, etc, and
/// turn them into an X,Y index.
pub fn parse_to_idx(i: &str) -> Option<(usize, usize)> {
let i = i.replace('$', "");
let chars = i.chars().take_while(|c| c.is_alphabetic()).collect::<Vec<char>>();
let chars = i.chars().take_while(|c| c.is_alphabetic()).collect::<String>();
let nums = i.chars().skip(chars.len()).take_while(|c| c.is_numeric()).collect::<String>();
// At least half the arguments are gone
@@ -403,11 +528,7 @@ impl Grid {
}
// get the x index from the chars
let x_idx = chars
.iter()
.enumerate()
.map(|(idx, c)| (c.to_ascii_lowercase() as usize - 97) + (26 * idx))
.fold(0, |a, b| a + b);
let x_idx = Self::char_to_idx(&chars);
// get the y index from the numbers
if let Ok(y_idx) = nums.parse::<usize>() {
@@ -419,15 +540,12 @@ impl Grid {
/// Helper for tests
#[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) {
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;
}
@@ -446,7 +564,7 @@ impl Grid {
if x >= LEN || y >= LEN {
return &None;
}
&self.cells[x][y]
self.get_grid().get_cell_raw(x, y)
}
pub fn num_to_char(idx: usize) -> String {
@@ -463,7 +581,7 @@ impl Grid {
}
word[1] = ((idx % 26) + 65) as u8 as char;
word.iter().collect()
word.iter().filter(|a| !a.is_ascii_whitespace()).collect()
}
}
@@ -499,11 +617,11 @@ fn saving_csv() {
// insure that the cells are there
let cell = app.grid.get_cell_raw(0, 10).as_ref().expect("Should've been set");
let res = app.grid.evaluate(&cell.to_string()).expect("Should evaluate");
assert_eq!(res, 11.0);
assert_eq!(res, (11.0).into());
assert_eq!(cell.escaped_csv_string(), "=A9+A$0");
let cell = app.grid.get_cell_raw(1, 10).as_ref().expect("Should've been set");
let res = app.grid.evaluate(&cell.to_string()).expect("Should evaluate");
assert_eq!(res, 121.0);
assert_eq!(res, (121.0).into());
assert_eq!(cell.escaped_csv_string(), "=A10^2");
// set saving the file
@@ -542,11 +660,11 @@ fn saving_neoscim() {
// insure that the cells are there
let cell = app.grid.get_cell_raw(0, 10).as_ref().expect("Should've been set");
let res = app.grid.evaluate(&cell.to_string()).expect("Should evaluate");
assert_eq!(res, 11.0);
assert_eq!(res, (11.0).into());
assert_eq!(cell.escaped_csv_string(), "=A9+A$0");
let cell = app.grid.get_cell_raw(1, 10).as_ref().expect("Should've been set");
let res = app.grid.evaluate(&cell.to_string()).expect("Should evaluate");
assert_eq!(res, 121.0);
assert_eq!(res, (121.0).into());
assert_eq!(cell.escaped_csv_string(), "=A10^2");
// set saving the file
@@ -564,7 +682,7 @@ fn saving_neoscim() {
fn cell_strings() {
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());
assert!(grid.get_cell("A0").is_some());
@@ -587,9 +705,10 @@ fn alphanumeric_indexing() {
assert_eq!(Grid::parse_to_idx("A"), None);
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::num_to_char(0).trim(), "A");
assert_eq!(Grid::num_to_char(25).trim(), "Z");
assert_eq!(Grid::num_to_char(0), "A");
assert_eq!(Grid::num_to_char(25), "Z");
assert_eq!(Grid::num_to_char(26), "AA");
assert_eq!(Grid::num_to_char(51), "AZ");
assert_eq!(Grid::num_to_char(701), "ZZ");
@@ -607,31 +726,31 @@ fn valid_equations() {
// cell math
let cell = grid.get_cell("C0").as_ref().expect("Just set it");
let res = grid.evaluate(&cell.to_string()).expect("Should evaluate.");
assert_eq!(res, 3.);
assert_eq!(res, (3.).into());
// divide floats
grid.set_cell("D0", "=5./2.".to_string());
let cell = grid.get_cell("D0").as_ref().expect("I just set this");
let res = grid.evaluate(&cell.to_string()).expect("Should be ok");
assert_eq!(res, 2.5);
assert_eq!(res, (2.5).into());
// Float / Int mix
grid.set_cell("D0", "=5./2".to_string());
let cell = grid.get_cell("D0").as_ref().expect("I just set this");
let res = grid.evaluate(&cell.to_string()).expect("Should be ok");
assert_eq!(res, 2.5);
assert_eq!(res, (2.5).into());
// divide "ints" (should become floats)
grid.set_cell("D0", "=5/2".to_string());
let cell = grid.get_cell("D0").as_ref().expect("I just set this");
let res = grid.evaluate(&cell.to_string()).expect("Should be ok");
assert_eq!(res, 2.5);
assert_eq!(res, (2.5).into());
// Non-equation that should still be valid
grid.set_cell("D0", "=10".to_string());
let cell = grid.get_cell("D0").as_ref().expect("I just set this");
let res = grid.evaluate(&cell.to_string()).expect("Should be ok");
assert_eq!(res, 10.);
assert_eq!(res, (10.).into());
}
// Cell = output of Cell = value of Cells.
@@ -643,15 +762,17 @@ fn fn_of_fn() {
grid.set_cell("B0", 1.);
grid.set_cell("C0", "=A0+B0".to_string());
grid.set_cell("D0", "=C0*2".to_string());
let cell = grid.get_cell("D0");
assert!(cell.is_some());
if let Some(cell) = grid.get_cell("D0") {
if let Some(cell) = cell {
let res = grid.evaluate(&cell.to_string());
assert!(res.is_ok());
assert_eq!(res.unwrap(), 6.);
assert_eq!(res.unwrap(), (6.).into());
return;
}
panic!("Cell not found");
}
// Two cells that have a circular dependency to solve for a value
@@ -661,12 +782,14 @@ fn circular_reference_cells() {
grid.set_cell("A0", "=B0".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());
assert!(res.is_err());
return;
}
panic!("Cell not found");
}
#[test]
@@ -684,7 +807,7 @@ fn invalid_equations() {
let cell = grid.get_cell("B0").as_ref().expect("Just set the cell");
let res = grid.evaluate(&cell.to_string());
assert!(res.is_ok());
assert!(res.is_ok_and(|v| v == 10.));
assert!(res.is_ok_and(|v| v == (10.).into()));
// Trailing comma in function call
grid.set_cell("A0", 5.);
@@ -692,7 +815,7 @@ fn invalid_equations() {
grid.set_cell("B0", "=avg(A0,A1,)".to_string());
let cell = grid.get_cell("B0").as_ref().expect("Just set the cell");
let res = grid.evaluate(&cell.to_string());
assert_eq!(res.unwrap(), 7.5);
assert_eq!(res.unwrap(), (7.5).into());
}
#[test]
@@ -700,17 +823,17 @@ fn grid_max() {
let mut grid = Grid::new();
grid.set_cell("A0", 1.);
let (mx, my) = grid.max();
let (mx, my) = grid.get_grid().max();
assert_eq!(mx, 0);
assert_eq!(my, 0);
grid.set_cell("B0", 1.);
let (mx, my) = grid.max();
let (mx, my) = grid.get_grid().max();
assert_eq!(mx, 1);
assert_eq!(my, 0);
grid.set_cell("B5", 1.);
let (mx, my) = grid.max();
let (mx, my) = grid.get_grid().max();
assert_eq!(mx, 1);
assert_eq!(my, 5);
}
@@ -723,19 +846,19 @@ fn avg_function() {
let cell = grid.get_cell("A0").as_ref().expect("Just set the cell");
let res = grid.evaluate(&cell.to_string());
assert!(res.is_ok());
assert_eq!(res.unwrap(), 5.);
assert_eq!(res.unwrap(), (5.).into());
grid.set_cell("A0", "=avg(5,10)".to_string());
let cell = grid.get_cell("A0").as_ref().expect("Just set the cell");
let res = grid.evaluate(&cell.to_string());
assert!(res.is_ok());
assert_eq!(res.unwrap(), 7.5);
assert_eq!(res.unwrap(), (7.5).into());
grid.set_cell("A0", "=avg(5,10,15)".to_string());
let cell = grid.get_cell("A0").as_ref().expect("Just set the cell");
let res = grid.evaluate(&cell.to_string());
assert!(res.is_ok());
assert_eq!(res.unwrap(), 10.);
assert_eq!(res.unwrap(), (10.).into());
grid.set_cell("A0", "=avg(foo)".to_string());
let cell = grid.get_cell("A0").as_ref().expect("Just set the cell");
@@ -761,13 +884,13 @@ fn sum_function() {
let cell = grid.get_cell("A0").as_ref().expect("Just set the cell");
let res = grid.evaluate(&cell.to_string());
assert!(res.is_ok());
assert_eq!(res.unwrap(), 5.);
assert_eq!(res.unwrap(), (5.).into());
grid.set_cell("A0", "=sum(5,10)".to_string());
let cell = grid.get_cell("A0").as_ref().expect("Just set the cell");
let res = grid.evaluate(&cell.to_string());
assert!(res.is_ok());
assert_eq!(res.unwrap(), 15.);
assert_eq!(res.unwrap(), (15.).into());
grid.set_cell("A0", "=sum(foo)".to_string());
let cell = grid.get_cell("A0").as_ref().expect("Just set the cell");
@@ -797,7 +920,7 @@ fn xlookup_function() {
let cell = grid.get_cell("B0").as_ref().expect("Just set the cell");
let res = grid.evaluate(&cell.to_string());
assert!(res.is_ok());
assert_eq!(res.unwrap(), 31.);
assert_eq!(res.unwrap(), (31.).into());
}
#[test]
@@ -851,6 +974,19 @@ fn parse_csv() {
);
}
#[test]
fn invalid_ranges() {
let mut grid = Grid::new();
grid.set_cell("A0", 2.);
grid.set_cell("A1", 1.);
// ASCII to number conversion needs to not overflow
grid.set_cell("B0", "=sum($:A)".to_string());
let cell = grid.get_cell("B0").as_ref().expect("Just set it");
let _ = grid.evaluate(&cell.to_string()).expect("Should evaluate.");
}
#[test]
fn ranges() {
let mut grid = Grid::new();
@@ -862,13 +998,13 @@ fn ranges() {
// range with numbers
let cell = grid.get_cell("B0").as_ref().expect("Just set it");
let res = grid.evaluate(&cell.to_string()).expect("Should evaluate.");
assert_eq!(res, 3.);
assert_eq!(res, (3.).into());
// use range output as input for other function
grid.set_cell("B1", "=B0*2".to_string());
let cell = grid.get_cell("B1").as_ref().expect("Just set it");
let res = grid.evaluate(&cell.to_string()).expect("Should evaluate.");
assert_eq!(res, 6.);
assert_eq!(res, (6.).into());
// use equation outputs as range input
grid.set_cell("A2", "=C0+1".to_string());
@@ -876,11 +1012,11 @@ fn ranges() {
let cell = grid.get_cell("A2").as_ref().expect("Just set it");
let res = grid.evaluate(&cell.to_string()).expect("Should evaluate.");
assert_eq!(res, 6.);
assert_eq!(res, (6.).into());
let cell = grid.get_cell("B0").as_ref().expect("Just set it");
let res = grid.evaluate(&cell.to_string()).expect("Should evaluate.");
assert_eq!(res, 9.);
assert_eq!(res, (9.).into());
// use function outputs as range input
grid.set_cell("B1", 2.);
@@ -889,7 +1025,7 @@ fn ranges() {
let cell = grid.get_cell("C0").as_ref().expect("Just set it");
let res = grid.evaluate(&cell.to_string()).expect("Should evaluate.");
assert_eq!(res, 5.);
assert_eq!(res, (5.).into());
// use range outputs as range input
grid.set_cell("D0", "=sum(C:C)".to_string());
@@ -897,7 +1033,7 @@ fn ranges() {
let cell = grid.get_cell("D0").as_ref().expect("Just set it");
let res = grid.evaluate(&cell.to_string()).expect("Should evaluate.");
assert_eq!(res, 6.);
assert_eq!(res, (6.).into());
}
#[test]
@@ -982,6 +1118,66 @@ fn insert_col_before_3() {
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(1, 0);
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]
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]
fn insert_row_above_1() {
let mut grid = Grid::new();
@@ -1048,7 +1244,7 @@ fn insert_row_above_3() {
#[test]
fn cell_eval_depth() {
use crate::app::mode::*;
let mut app= App::new();
let mut app = App::new();
app.grid.set_cell("A0", 1.);
app.grid.set_cell("A1", "=A0+$A$0".to_string());
@@ -1067,6 +1263,34 @@ fn cell_eval_depth() {
assert_eq!(c.to_string(), "=A5+$A$0");
let res = app.grid.evaluate(&c.to_string()).expect("Should evaluate");
assert_eq!(res, 7.);
assert_eq!(res, (7.).into());
}
#[test]
fn return_string_from_fn() {
let mut grid = Grid::new();
grid.set_cell("A0", "=if(2>1, \"A\", \"B\")".to_string()); // true, A
grid.set_cell("A1", "=if(1>2, \"A\", \"B\")".to_string()); // false, B
let cell = grid.get_cell("A0").as_ref().expect("Just set the cell");
let res = grid.evaluate(&cell.to_string());
assert!(res.is_ok());
if let Ok(f) = res {
if let CellType::String(s) = f {
assert_eq!("A", s);
} else {
unreachable!();
}
}
let cell = grid.get_cell("A1").as_ref().expect("Just set the cell");
let res = grid.evaluate(&cell.to_string());
assert!(res.is_ok());
if let Ok(f) = res {
if let CellType::String(s) = f {
assert_eq!("B", s);
} else {
unreachable!();
}
}
}

View File

@@ -11,15 +11,15 @@ pub enum CellType {
Equation(String),
}
impl Into<CellType> for f64 {
fn into(self) -> CellType {
CellType::duck_type(self.to_string())
impl From<f64> for CellType {
fn from(value: f64) -> Self {
CellType::duck_type(value.to_string())
}
}
impl Into<CellType> for String {
fn into(self) -> CellType {
CellType::duck_type(self)
impl From<String> for CellType {
fn from(value: String) -> Self {
CellType::duck_type(value)
}
}
@@ -37,14 +37,10 @@ impl CellType {
}
// escape string of it has a comma
if display.contains(CSV_DELIMITER) {
format!("\"{display}\"")
} else {
display
}
if display.contains(CSV_DELIMITER) { 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();
if let Ok(parse) = value.parse::<f64>() {
@@ -56,14 +52,19 @@ impl CellType {
/// `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`
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 {
// don't translate non-equations
CellType::Number(_) | CellType::String(_) => return self.clone(),
CellType::Equation(eq) => {
// Populate the context
let ctx = ExtractionContext::new();
let _ = eval_with_context(eq, &ctx);
let _ = eval_with_context(&eq[1..], &ctx);
let mut equation = eq.clone();
// translate standard vars A0 -> A1
@@ -73,7 +74,8 @@ impl CellType {
let mut lock_y = false;
if old_var.contains('$') {
let locations = old_var.char_indices().filter(|(_, c)| *c == '$').map(|(i, _)| i).collect::<Vec<usize>>();
let locations =
old_var.char_indices().filter(|(_, c)| *c == '$').map(|(i, _)| i).collect::<Vec<usize>>();
match locations.len() {
1 => {
if locations[0] == 0 {
@@ -86,7 +88,7 @@ impl CellType {
}
2 => {
// Ignore this variable all together, effectively lockng X & Y
continue;
continue;
}
_ => {
// There are 0 or >2 "$" in this string.
@@ -97,7 +99,7 @@ impl CellType {
}
}
}
if let Some((src_x, src_y)) = Grid::parse_to_idx(&old_var) {
// Use i32s instead of usize in case of negative numbers
let (x1, y1) = from;
@@ -107,20 +109,11 @@ impl CellType {
let x2 = x2 as i32;
let y2 = y2 as i32;
let dest_x = if lock_x {
src_x as usize
} else {
(src_x as i32 + (x2 - x1)) as usize
};
let dest_x = if lock_x { src_x } else { (src_x as i32 + (x2 - x1)) as usize };
let dest_y = if lock_y {
src_y as usize
} else {
(src_y as i32 + (y2 - y1)) as usize
};
let dest_y = if lock_y { src_y } else { (src_y as i32 + (y2 - y1)) as usize };
let alpha = Grid::num_to_char(dest_x);
let alpha = alpha.trim();
// Persist the "$" locking
let new_var = if lock_x {
@@ -136,6 +129,32 @@ impl CellType {
// rolling = rolling.replace(&old_var, &new_var);
} else {
// why you coping invalid stuff, nerd?
//
// could be copying a range
if let Some(parts) = Grid::range_as_indices(&old_var) {
// how far is the movement?
let dx = to.0 as i32 - from.0 as i32;
let xs = parts.0 as i32;
let xe = parts.1 as i32;
// apply movement
let mut new_range_start = xs + dx;
let mut new_range_end = xe + dx;
// 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}:{end}"));
}
}
}
return equation.into();
@@ -144,7 +163,7 @@ impl 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))
}
}
@@ -158,3 +177,14 @@ impl Display for CellType {
write!(f, "{d}")
}
}
impl PartialEq for CellType {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Number(left), Self::Number(right)) => left == right,
(Self::String(left), Self::String(right)) => left == right,
(Self::Equation(left), Self::Equation(right)) => left == right,
_ => false,
}
}
}

View File

@@ -16,25 +16,11 @@ pub struct CallbackContext<'a> {
impl<'a> CallbackContext<'a> {
fn expand_range(&self, range: &str) -> Option<Vec<&CellType>> {
let v = range.split(':').collect::<Vec<&str>>();
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 - 97) + (26 * idx))
.fold(0, |a, b| a + b)
};
let start_idx = as_index(start_col);
let end_idx = as_index(end_col);
if let Some((start, end)) = Grid::range_as_indices(range) {
let mut buf = Vec::new();
for x in start_idx..=end_idx {
for y in 0..=self.variables.max_y_at_x(x) {
for x in start..=end {
for y in 0..=self.variables.get_grid().max_y_at_x(x) {
if let Some(s) = self.variables.get_cell_raw(x, y) {
buf.push(s);
}
@@ -162,8 +148,8 @@ impl<'a> Context for CallbackContext<'a> {
}
if let Ok(mut trail) = self.eval_breadcrumbs.write() {
let find = trail.iter().filter(|id| *id == identifier).collect::<Vec<&String>>();
if find.len() > 0 {
let find = trail.iter().filter(|id| *id == identifier).count();
if find > 0 {
// recursion detected
return None;
} else {
@@ -195,14 +181,21 @@ impl<'a> Context for CallbackContext<'a> {
},
Err(e) => {
match e {
EvalexprError::VariableIdentifierNotFound(_) => {
EvalexprError::VariableIdentifierNotFound(_err) => {
// If the variable isn't found, that's ~~probably~~ because
// of recursive reference, considering all references
// are grabbed straight from the table.
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}");
},
}
}
}
@@ -217,8 +210,14 @@ impl<'a> Context for CallbackContext<'a> {
CellType::Number(e) => vals.push(Value::Float(*e)),
CellType::String(s) => vals.push(Value::String(s.to_owned())),
CellType::Equation(eq) => {
if let Ok(val) = eval_with_context(&eq[1..], self) {
vals.push(val);
match eval_with_context(&eq[1..], self) {
Ok(val) => vals.push(val),
Err(_err) => {
// At this point we are getting an error because
// recursion protection made this equation return
// None. We now don't get any evaluation.
return None
},
}
}
}
@@ -270,10 +269,19 @@ impl ExtractionContext {
}
}
pub fn dump_vars(&self) -> Vec<String> {
if let Ok(r) = self.var_registry.read() { r.clone() } else { Vec::new() }
if let Ok(r) = self.var_registry.read() {
r.clone()
} else {
Vec::new()
}
}
#[allow(dead_code)]
pub fn dump_fns(&self) -> Vec<String> {
if let Ok(r) = self.fn_registry.read() { r.clone() } else { Vec::new() }
if let Ok(r) = self.fn_registry.read() {
r.clone()
} else {
Vec::new()
}
}
}
@@ -301,8 +309,7 @@ impl Context for ExtractionContext {
} else {
panic!("The RwLock should always be write-able")
}
// Ok(Value::Int(1))
unimplemented!("Extracting function identifier not implemented yet")
Ok(Value::Int(1))
}
fn are_builtin_functions_disabled(&self) -> bool {

View File

@@ -2,7 +2,8 @@ use std::{
cmp::{max, min},
fmt::Display,
fs,
path::PathBuf, process::Command,
path::PathBuf,
process::Command,
};
use ratatui::{
@@ -15,7 +16,7 @@ use crate::app::{
app::App,
error_msg::StatusMessage,
logic::{
calc::{CSV_EXT, CUSTOM_EXT, Grid, LEN},
calc::{Grid, LEN},
cell::CellType,
},
};
@@ -86,7 +87,7 @@ impl Mode {
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)
}
}
@@ -153,11 +154,13 @@ impl Mode {
let mut save_range = |to: &str| {
let mut g = Grid::new();
for (i, x) in (low_x..=hi_x).enumerate() {
for (j, y) in (low_y..=hi_y).enumerate() {
g.set_cell_raw((i, j), app.grid.get_cell_raw(x, y).clone());
g.transact_on_grid(|grid| {
for (i, x) in (low_x..=hi_x).enumerate() {
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) {
app.msg = StatusMessage::error("Failed to save file");
}
@@ -171,13 +174,30 @@ impl Mode {
}
}
}
return "unknown"
return "unknown";
};
match args[0] {
"f" | "fill" => {
app.grid.transact_on_grid(|grid| {
for (i, x) in (low_x..=hi_x).enumerate() {
for (j, y) in (low_y..=hi_y).enumerate() {
let arg = args
.get(1)
.map(|s| s.replace("xi", &i.to_string()))
.map(|s| s.replace("yi", &j.to_string()))
.map(|s| s.replace("x", &x.to_string()))
.map(|s| s.replace("y", &y.to_string()));
grid.set_cell_raw((x, y), arg);
}
}
});
app.mode = Mode::Normal
}
"export" => {
if let Some(arg1) = args.get(1) {
save_range(&arg1);
save_range(arg1);
} else {
app.msg = StatusMessage::error("export <path.csv>")
}
@@ -187,11 +207,7 @@ impl Mode {
// Use gnuplot to plot the selected data.
// * Temp data will be stored in /tmp/
// * 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");
let plot = include_str!("../../template.gnuplot");
@@ -202,10 +218,12 @@ impl Mode {
let s = s.replace("$OUTPUT", "/tmp/output.png");
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 {
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}")),
};
} else {
@@ -255,20 +273,20 @@ impl Mode {
// Go to end of row
'$' => {
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;
}
// Go to bottom of column
'G' => {
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;
}
// edit cell
'i' | 'a' => {
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));
}
@@ -304,6 +322,11 @@ impl Mode {
app.mode = Mode::Command(Chord::new(':'))
}
}
// undo
'u' => {
app.grid.undo();
}
// paste
'p' => {
app.clipboard.paste(&mut app.grid, true);
app.grid.apply_momentum(app.clipboard.momentum());
@@ -383,7 +406,7 @@ impl Mode {
let (_, y_height) = app.screen.get_screen_size(&app.vars);
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;
return;
}
@@ -393,7 +416,7 @@ impl Mode {
let (x_width, _) = app.screen.get_screen_size(&app.vars);
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;
}
// Go to the left edge of the current window
@@ -454,14 +477,12 @@ impl Mode {
let len = match &self {
Mode::Insert(edit) | Mode::VisualCmd(_, edit) | Mode::Command(edit) | Mode::Chord(edit) => edit.len(),
Mode::Normal => {
let len = cell.as_ref().map(|f| f.to_string().len()).unwrap_or_default();
len
cell.as_ref().map(|f| f.to_string().len()).unwrap_or_default()
}
Mode::Visual(_) => 0,
};
// min 20 chars, expand if needed
let len = max(len as u16 + 1, 20);
len
max(len as u16 + 1, 20)
}
pub fn render(&self, f: &mut ratatui::Frame, area: prelude::Rect, cell: &Option<CellType>) {
@@ -475,8 +496,7 @@ impl Mode {
Mode::Chord(chord) => f.render_widget(chord, area),
Mode::Normal => f.render_widget(
Paragraph::new({
let cell = cell.as_ref().map(|f| f.to_string()).unwrap_or_default();
cell
cell.as_ref().map(|f| f.to_string()).unwrap_or_default()
}),
area,
),
@@ -493,20 +513,15 @@ pub struct Chord {
impl From<String> for Chord {
fn from(value: String) -> Self {
let b = value.as_bytes().iter().map(|f| *f as char).collect();
Chord {
buf: b,
}
Chord { buf: b }
}
}
impl Chord {
pub fn new(inital: char) -> Self {
let mut buf = Vec::new();
buf.push(inital);
let buf = vec![inital];
Self {
buf,
}
Self { buf }
}
pub fn backspace(&mut self) {

View File

@@ -1,8 +1,8 @@
use std::{collections::HashMap, sync::RwLock};
use std::{cmp::min, collections::HashMap, sync::RwLock};
use ratatui::prelude;
use crate::app::logic::calc::{self, LEN};
use crate::app::logic::calc::LEN;
pub struct ScreenSpace {
/// This is measured in cells.
@@ -48,11 +48,11 @@ impl ScreenSpace {
// ======= X =======
// screen seems to be 2 cells smaller than it should be
// 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 =======
// screen seems to be 2 cells smaller than it should be
// 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)
} else {
(0,0)
@@ -123,22 +123,12 @@ impl ScreenSpace {
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);
let width = (area.width as usize / self.get_cell_width(vars)) + 1;
let height = area.height as usize / self.get_cell_height(vars);
let x_max =
if width > LEN {
LEN - 1
} else {
width
};
let y_max =
if height > LEN {
LEN - 1
} else {
height
};
let x_max = min(LEN-1, width);
let y_max = min(LEN-1, height);
(x_max as u16, y_max as u16)
}
@@ -152,7 +142,7 @@ fn fit_cells() {
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);
assert_eq!(x, 18);
assert_eq!(x, 19);
assert_eq!(y, 14);
}
@@ -166,7 +156,7 @@ fn scroll() {
// We have to check how many cells fit, because screen learns the width
// 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);
assert_eq!(x, 18);
assert_eq!(x, 19);
assert_eq!(y, 14);
// 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;
use std::env::args;